Compare commits

...
Author SHA1 Message Date
koka fb8705369e docs: align beta.298 server kit instructions
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:58:46 +01:00
koka c3d1443732 docs: prepare beta.298 release notes
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:57:03 +01:00
koka 1967342247 fix: exclude invisible media anchors from PBR normals
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:36:47 +01:00
koka 89db24d7c2 docs: record complete beta.297 deployment
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:05:31 +01:00
koka bad2bdb0d1 docs: prepare beta.297 release notes
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 04:56:22 +01:00
koka 098a5aa1bd fix: correct media frame interaction and side geometry
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 18:02:18 +01:00
31 changed files with 746 additions and 203 deletions
+16 -2
View File
@@ -1,7 +1,7 @@
# Sanctuary
[Dernière release : beta.295](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.295) ·
[Reçu de livraison](docs/release-beta295.md) ·
[Dernière release : beta.297](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.297) ·
[Reçu de livraison](docs/release-beta297.md) ·
[Installation et mises à jour](docs/packwiz.md).
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
@@ -10,6 +10,20 @@ Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.298 — normales de l'ancre (préparation locale)
[Retrait du petit cadre résiduel dans la passe PBR](docs/media-anchor-normals-beta298.md).
Le modèle invisible de l'ancre ne doit plus laisser de reflet sur la vidéo
avec les shaders Sanctuary activés.
## beta.297 — correctifs des cadres disponibles
[Menu de taille à la pose, lecteur au Maj + clic droit et cadre en volume](docs/media-frame-fixes-beta297.md).
Retrait du repère rouge et du contour de sélection du bloc invisible.
Le redimensionnement reste accessible par Taille / Size dans le lecteur.
[Livraison vérifiée](docs/release-beta297.md) : serveur Beta, site et même
instance Prism à jour, sauvegardes et réglages conservés.
## beta.296 — écrans partagés disponibles pour essai
[Cadres de redstone, taille libre et menu Dream Displays](docs/media-beta296.md).
+68
View File
@@ -0,0 +1,68 @@
# MEDIA-07 — normales résiduelles de l'ancre
Retour du 27 octobre 2026 : une bordure légèrement brillante reste sur la
vidéo à l'emplacement de l'ancre, dans le coin inférieur gauche.
Branche `codex/media-anchor-model`, préparation **beta.298**, Media **0.3.2**.
## Cause et correction
Le bloc est invisible pour le rendu Minecraft, mais conserve un ancien modèle
de cadre 1 × 1 avec **30 faces**. La passe PBR de Sanctuary collectait aussi
les modèles des blocs invisibles. Elle dessinait donc les normales et le
matériau de ce petit cadre, alors que sa couleur n'était pas rendue.
Les essais graphiques de MEDIA-06 avaient les shaders Sanctuary désactivés ;
ils ne couvraient pas cette passe.
La collecte PBR des modèles opaques et du verre respecte maintenant
`RenderShape.MODEL`. Les fluides gardent leur chemin dédié, y compris l'eau
contenue dans un bloc. Le modèle de bloc de l'ancre est également vidé ;
sa texture de particules reste disponible. L'objet utilise déjà un modèle
indépendant et garde son apparence dans l'inventaire et dans la main.
Le grand cadre reste dessiné par son maillage aux dimensions de l'écran.
Aucun identifiant, paquet, format de données ou règle d'interaction ne change.
Les ancres existantes bénéficient du correctif au chargement du client.
La sauvegarde et les dimensions ne nécessitent aucune migration.
## Validation
Reproduction avec le JAR beta.297 publié : deux clients Vulkan, Sodium,
Sound Physics et **shaders Sanctuary/PBR actifs**. Le diagnostic constate
`INVISIBLE`, 30 faces et admission de l'ancre dans la passe PBR opaque.
Le bois et le verre visibles sont admis dans leurs passes respectives, l'eau
dans la passe transparente.
Après correction, les deux clients constatent **zéro face**, ancre refusée
par les deux passes de matériau, PBR réellement appliqué et vidéo en mouvement.
Les chemins bois, verre et eau restent admis. Pose, taille 9 × 5 et ouverture
du lecteur testées dans le laboratoire ; un clic de pose a dû être rejoué
après la réception de l'inventaire du test.
Captures finales du client A inspectées, en gros plan du coin inférieur gauche,
de face et en vue rasante : aucune empreinte du cadre 1 × 1 sur la vidéo.
La sortie OpenAL est active sur les deux clients. Le JAR Sanctuary testé,
avec un seul Media embarqué et sans sonde, a pour SHA-256
`e6fdc36a212dcfcc89d31fbb29bc8eeec9d002cc9223ea6dcbfb31b7df17ec23`.
`check build assemblePack assembleServerPack :sanctuary-test:exportDuoLaunch`
réussis en **3 min 6 s**, 162 tâches, **18 GameTests requis** au profil
`property270,claims272,claims277,claims278`, atlas ciblé. Contrats Media inclus.
Compilation des vérifications client PBR, y compris les nouveaux contrôles
des blocs invisibles et des modèles visibles, réussie en **37 s**. La suite
graphique PBR historique complète n'est pas rejouée ; ce parcours ciblé
exerce la sélection réelle des matériaux du JAR construit.
Un lancement de client a automatiquement basculé hors Vulkan après une course
d'extraction MoltenVK entre les deux processus : il a été rejeté, arrêté et
relancé sous Vulkan. Seuls les relevés Vulkan sont retenus. Après les captures
et les relevés valides, le client B s'est arrêté sur l'écriture du journal de
la sonde (`No space left on device`). Le disque local ne disposait plus que
de 136 Mio au contrôle suivant ; ce parcours ne vaut donc pas essai d'endurance.
Ce défaut d'espace appartient au laboratoire, la sonde n'est pas distribuée.
[Relevé structuré](media-anchor-normals-results.json). Pas de nouvelle mesure
de synchronisation vidéo/live ou de source publique. Mur graphique orienté sud.
Laboratoires isolés sous `build/media-prototype/anchor-model-*`, captures finales
dans `anchor-model-after/client-a/screenshots/`. Aucun monde personnel utilisé.
Cette préparation ne modifie pas la version beta.297 actuellement installée
sur le canal, la Beta et Prism.
+104
View File
@@ -0,0 +1,104 @@
{
"ticket": "MEDIA-07",
"state": "local preparation; beta.297 remains installed",
"artifact": {
"version": "beta.298",
"bytes": 18301295,
"sha256": "e6fdc36a212dcfcc89d31fbb29bc8eeec9d002cc9223ea6dcbfb31b7df17ec23",
"media_version": "0.3.2",
"nested_media_count": 1,
"anchor_block_faces": 0,
"probe_embedded": false
},
"backend": "Vulkan",
"clients": [
{
"client": "client-a",
"model": {
"anchor_model_quads": 0,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"pbr_applied": true,
"anchor_pbr_opaque": false,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
},
"vulkan_video_uploads": 1625
},
{
"client": "client-b",
"model": {
"anchor_model_quads": 0,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"pbr_applied": true,
"anchor_pbr_opaque": false,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
},
"vulkan_video_uploads": 1448
}
],
"before": {
"version": "beta.297",
"clients": [
{
"client": "client-a",
"anchor_model_quads": 30,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"anchor_pbr_opaque": true,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
},
{
"client": "client-b",
"anchor_model_quads": 30,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"anchor_pbr_opaque": true,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
}
]
},
"build": {
"check_build_assemble": "passed",
"duration": "3m06s",
"tasks": 162,
"required_gametests": 18,
"client_checks_compile": "passed, 37s, 6 tasks; historical PBR graphic suite not rerun"
},
"limits": [
"No new live synchronization or public-source validation",
"Graphical wall faces south",
"Canonical Prism and dedicated Beta unchanged",
"One automatic fallback after concurrent MoltenVK extraction was rejected and the client relaunched in Vulkan",
"Client B stopped after valid samples and captures because the probe could not write its log: No space left on device; not an endurance test"
],
"graphics_checks": {
"lower_left_front_video": "inspected, no 1x1 frame imprint",
"lower_left_grazing_video": "inspected, no 1x1 frame imprint",
"placement_resize_reader": "passed; placement replayed after inventory arrival",
"final_screenshots": [
"anchor-model-shaders-front-settled.png",
"anchor-model-shaders-side-settled.png"
]
}
}
+88
View File
@@ -0,0 +1,88 @@
# MEDIA-06 — interaction et épaisseur des cadres
Retours du 26 octobre 2026 après essai de beta.296. Branche
`codex/media-frame-fixes`, **beta.297**, module Media **0.3.1**.
[Livraison complète du 27 octobre 2026](release-beta297.md).
## Comportement demandé
Le menu largeur/hauteur s'ouvre automatiquement seulement à la pose. Ensuite,
clic droit et **Maj + clic droit** ouvrent le lecteur Dream. Le bouton
**Taille / Size** de ce lecteur reste le point d'entrée pour redimensionner,
avec les mêmes vérifications serveur de proximité et de construction.
Appliquer une taille garde le menu ouvert pour l'ajuster ; aucune modification
du coût, de la recette ou des tailles autorisées.
Les trois chemins sont alignés : raycast client, paquet d'ouverture serveur
et interaction directe avec le bloc. La pose initiale conserve sa demande
explicite du menu de taille.
## Rendu
Le pixel rouge était un repère d'ancrage ajouté par Sanctuary. Il est retiré
du cadre du monde et de l'aperçu de taille.
Le cadre précédent utilisait le repère vidéo Dream. Celui-ci applique une
échelle Z nulle : c'est un dessin plat, sans faces latérales. Le nouveau
maillage est un anneau fermé en unités de blocs : quatre bandes avant,
quatre arrière, quatre côtés extérieurs et quatre faces de feuillure.
Épaisseur et largeur du bois : **1/16 de bloc**, indépendamment de la taille.
Le contour extérieur est exactement celui de l'écran, au contact du mur.
La vidéo conserve son plan et ses proportions, en retrait du bois.
Les sommets sont calculés depuis la géométrie de l'ancre et ramenés dans le
repère du renderer Dream sans passer par sa transformation vidéo aplatie.
Ils gardent les mêmes positions dans les passes normale et de reprise :
aucune deuxième bordure décalée. Le pipeline Vulkan Dream et sa profondeur
restent utilisés, sans nouveau shader.
La vidéo et les sous-titres sont masqués depuis l'arrière du plan, et depuis
les côtés lorsque l'observateur est derrière la face avant du bois et hors
de l'ouverture. Cela évite un liseré vidéo en vue rasante ; une caméra située
dans l'ouverture conserve l'image. Le cadre en volume reste visible.
Le son reste actif, et les uploads vidéo
sont suspendus comme pour un écran hors champ. Les écrans Dream ordinaires
gardent leur rendu.
L'ancre invisible ne dessine plus son contour de sélection vanilla 1 × 1 à
l'intérieur d'un grand écran. Sa forme d'occlusion est explicitement vide ;
sa forme de ciblage reste disponible pour casser le bloc. Les validations
du mur et le retrait de l'écran côté serveur sont conservés.
## Données et livraison
Aucun identifiant, paquet ou format de sauvegarde ne change. Les écrans posés
en beta.296 retrouvent le nouveau rendu à partir de leurs données existantes,
sans remplacement de blocs ni conversion de monde. Aucun monde personnel
ou serveur personnel n'est utilisé pour la validation.
Le correctif est publié en beta.297 et installé sur la Beta, le site et la
même instance Prism. Les essais de développement ont précédé cette livraison.
## Validation
- `check build assemblePack assembleServerPack` : **réussi en 4 min 17 s**,
158 tâches, **18 GameTests requis** (`property270,claims272,claims277,claims278`).
- Contrat Java du maillage : 16 faces non dégénérées, bornes exactes et
épaisseur constante sur quatre tailles et les quatre orientations ; vidéo
visible dans l'ouverture et masquée derrière le bois depuis les côtés.
- **17 contrôles interactifs** réussis : nouvelle pose, réouverture normale,
Maj réellement maintenue, bouton Taille et protections/inventaire existants.
- Après correction du dernier liseré vidéo : contrats Media et assemblages
ciblés réussis en **21 s**, puis nouvel essai avec le JAR final embarqué.
- Deux clients **Vulkan**, Sodium et Sound Physics, vidéo synthétique sur
écran **9 × 5**. Captures inspectées de face, de droite, de gauche, du haut
et de l'arrière : côtés présents, aucun repère rouge ni fente colorée.
- Depuis droite/gauche/haut/arrière : uploads de l'image arrêtés et curseur
OpenAL toujours en mouvement ; retour devant : uploads repris.
- Sept tests du rapport et compilation syntaxique Python réussis.
Le premier laboratoire `frame-fixes-01` valide l'interaction ; ses premières
vues rasantes ont révélé le liseré restant. Seules les captures de
`build/media-prototype/frame-fixes-02/` valident son retrait final. Le maillage
est testé numériquement sur quatre orientations ; le parcours graphique est
orienté sud. Aucune nouvelle validation de live public ou d'écoute subjective.
[Relevé structuré](media-frame-fixes-results.json). Le
[reçu de livraison](release-beta297.md) distingue ces essais du déploiement
et de ses vérifications opérationnelles.
+77
View File
@@ -0,0 +1,77 @@
{
"ticket": "MEDIA-06",
"version": "beta.297",
"media_version": "0.3.1",
"state": "published and installed on Beta server, Web distribution and existing Prism instance",
"delivery": {
"date": "2026-10-27",
"receipt": "release-beta297.md",
"source_tag": "bad2bdb0d12295a736aee64e08b9f6f5a8ae81d3",
"packwiz_commit": "e40b0d372af9fa43a301b842661c131f4e69f7c7",
"fresh_check_build_assemble": "passed, 2m57s, 158 tasks, 18 required GameTests; identical JAR to final Vulkan validation"
},
"branch": "codex/media-frame-fixes",
"artifact_sha256": "f9bfffb0198abb8a4eae8a6d76a1ad3f1190edda7ce33f719bd47f0f4da27d9d",
"interaction_suite": {
"passed": [
"one frame consumed on placement",
"first placement opens dimensions",
"reopening near the anchor opens native player",
"Shift-right-click also opens native player",
"custom 7x4 and free enlargement",
"shrinking does not refund frames",
"resizing keeps the same display",
"oversize refused",
"incomplete wall refused",
"stale revision refused",
"native delete, overlap, obstacle and unloaded chunks refused",
"building permission required",
"original Dream reader on remote overlay while holding frame",
"forged remote resize refused",
"wall blocks normal interaction",
"wall blocks forged open packet",
"original Dream reader available to second spectator"
]
},
"final_graphics_checks": {
"placement_opens_dimensions": true,
"ordinary_click_player": true,
"shift_click_player": true,
"right_video_hidden_audio_continues": true,
"left_video_hidden_audio_continues": true,
"top_video_hidden_audio_continues": true,
"back_video_hidden_audio_continues": true,
"front_picture_resumes": true
},
"environment": {
"minecraft": "26.3",
"backend": "Vulkan",
"clients": 2,
"dream": "1.10.4",
"sound_physics": "1.5.1+26.3",
"screen_blocks": [
9,
5
],
"camera_views": [
"front",
"right grazing",
"left grazing",
"top grazing",
"back",
"front after hidden"
]
},
"validation": {
"full_check_build_assemble": "passed, 4m17s, 158 tasks, 18 required GameTests",
"final_client_visibility_contracts_and_assembly": "passed, 21s, 27 tasks",
"image_inspection": "five final views inspected; no red marker, no colored video slit through sides",
"python_report_tests": 7
},
"diagnostic": "frame-fixes-01 grazing images precede the final video visibility fix; final images are frame-fixes-02",
"limits": [
"No new public streaming or audio listening validation",
"Graphical wall faces south; all four orientations checked numerically",
"Graphical tests used development instances; deployment to the existing personal installation is documented in the delivery receipt, no personal world opened"
]
}
+18
View File
@@ -0,0 +1,18 @@
# Sanctuary beta.297
Le menu de dimensions s'ouvre automatiquement à la première pose du cadre.
Ensuite, clic droit et Maj + clic droit ouvrent directement le lecteur Dream ;
le bouton Taille / Size permet toujours de redimensionner près de l'ancre.
Le cadre possède maintenant une vraie épaisseur de 1/16 de bloc, avec des
faces latérales qui suivent exactement la surface de l'écran. Le repère rouge,
le contour de sélection du bloc invisible et les liserés vidéo visibles de
côté sont retirés. Le son continue lorsque l'image est masquée.
Les écrans déjà posés en beta.296 conservent leur source, leurs dimensions et
leurs données. Aucun bloc à remplacer et aucun changement de format de monde.
Media passe en 0.3.1 ; Minecraft 26.3, Fabric, Dream Displays 1.10.4 et les
autres dépendances restent inchangés.
Correctif vérifié sous Vulkan avec deux clients, 17 contrôles d'interaction
et des vues de face, des côtés, du haut et de l'arrière.
+15
View File
@@ -0,0 +1,15 @@
# Sanctuary beta.298
Le petit cadre brillant dans le coin inférieur gauche de la vidéo est corrigé
avec les shaders Sanctuary activés. Le modèle invisible de l'ancre ne fournit
plus de faces à la passe des normales PBR. Le grand cadre conserve sa taille
et son épaisseur ; le lecteur et le redimensionnement gardent leurs gestes.
Les écrans déjà posés conservent leur source, leurs dimensions et leurs données.
Aucun bloc à remplacer ni conversion de sauvegarde. Media passe en 0.3.2 ;
Minecraft 26.3, Fabric et Dream Displays 1.10.4 restent inchangés.
Correctif vérifié à deux clients Vulkan avec PBR actif, vidéo en mouvement et
gros plans de face et en vue rasante. Le détail des contrôles et leurs limites
est dans `docs/media-anchor-normals-beta298.md` ; pas de nouvelle validation
des sources publiques ni de l'endurance du lecteur.
+107
View File
@@ -0,0 +1,107 @@
# beta.297 — correctifs des cadres installés
Livraison complète du **27 octobre 2026**, branche `codex/media-frame-fixes`.
[Comportement et validation graphique](media-frame-fixes-beta297.md),
[notes de version](notes-beta297.md). Le socle beta.296 est conservé.
## Résultat en jeu
Le menu de dimensions apparaît automatiquement à la première pose. Ensuite,
clic droit et **Maj + clic droit** ouvrent le lecteur Dream ; son bouton
**Taille / Size** permet de redimensionner. Le repère rouge est retiré.
Le cadre possède des faces latérales et suit les dimensions de l'écran ;
la vidéo ne laisse plus de liseré coloré à travers le bois en vue rasante.
Le son continue lorsque l'image est masquée. Les écrans existants conservent
leurs dimensions, source et identité, sans conversion de données.
## Cibles vérifiées
| Cible | État |
| --- | --- |
| Source et tag `beta.297` | `bad2bdb0d12295a736aee64e08b9f6f5a8ae81d3` |
| Canal client/serveur | `e40b0d372af9fa43a301b842661c131f4e69f7c7` |
| Release | [beta.297](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.297), quatre artefacts publics vérifiés |
| CT 350 | Service actif, Sanctuary beta.297, Media 0.3.1 embarqué et Dream Displays 1.10.4 chargé |
| CT 320 | Distribution `releases/beta.297/prism`, ancienne beta.296 conservée |
| Prism historique | Même instance Sanctuary Beta, neuf JAR, deux synchronisations identiques |
Minecraft 26.3, Fabric 0.19.5, Java 25 et les dépendances restent inchangés.
Un seul JAR Sanctuary inclut JEI et Media ; Dream Displays reste un JAR
distinct obligatoire côté client et serveur. Le serveur comporte cinq JAR.
Le tag immuable désigne les sources de construction ; le présent reçu est
ajouté ensuite sans déplacer ce tag.
## Vérifications
- Nouvelle exécution `check build assemblePack assembleServerPack` réussie
en **2 min 57 s**, 158 tâches, **18 GameTests requis**, profil
`property270,claims272,claims277,claims278`, atlas ciblé. Contrats Media
inclus. Le JAR reconstruit a exactement le hash du JAR final testé sous Vulkan.
- Avant livraison : **17 contrôles interactifs** et deux clients **Vulkan**,
Sodium et Sound Physics, sur un écran 9 × 5. Captures finales de face,
droite, gauche, haut et arrière inspectées : aucun repère rouge ni liseré
vidéo, volume du cadre visible. Image suspendue hors ouverture, curseur
OpenAL actif, uploads repris en revenant devant. Les preuves et limites
sont dans [MEDIA-06](media-frame-fixes-beta297.md).
- Archives, plateformes, profils client/serveur et dépendances comparés à
leurs hashes. Deux synchronisations du véritable installateur dans une
instance isolée, puis deux dans l'instance personnelle : second passage
identique, aucun doublon Sanctuary, sélections facultatives conservées.
- Redémarrage CT 350 terminé (`Done`), versions Sanctuary, Media et Dream
confirmées dans le journal. Ping DNS/SRV depuis le LAN : Minecraft 26.3,
0/20 lors du contrôle, logo serveur inchangé.
- ZIP et MRpack publics : HTTP 200 et SHA-256 attendus. Le bouton du site
donne `Sanctuary-Beta.zip` ; le HTML annonce beta.297 et garde le lien direct.
- Admission : faux code refusé en HTTP 200 (`invalid`), aucun redeem.
Métriques Web fraîches, serveur en marche, même UUID communautaire
`cc6f4f5a-e58d-409a-879f-81daecf59461` côté jeu et site.
Les essais graphiques précèdent cette opération de publication et utilisent
le même binaire. Aucune nouvelle séance graphique ni mesure de synchronisation
vidéo/live n'est revendiquée pour le déploiement du 27 octobre.
## Artefacts
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.297.jar` | 18301448 | `f9bfffb0198abb8a4eae8a6d76a1ad3f1190edda7ce33f719bd47f0f4da27d9d` |
| `Sanctuary-beta.297.mrpack` | 16890964 | `89a031a85015615cfcf68c492bf1a73d32e0b10261c36f68045c985dc78d6886` |
| `Sanctuary-Prism-beta.297.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
| `Sanctuary-Server-beta.297.zip` | 4036720 | `f31bcf2c83929028a29a51eae53b37eae2bb7fa8253dc124acad1e2ac2846af0` |
Les ZIP d'amorçage sont identiques à beta.296 : mêmes outils et plateforme,
canal stable maintenant en beta.297. Le MRpack contient le nouveau JAR.
## Conservation
Serveur vide avant arrêt propre. Sauvegarde complète privée :
`/root/sanctuary-beta-backups/before-beta297-20261027T035832Z/instance.tar.gz`,
SHA-256 `6b7fc4b5a6569e8cd3a9bab7e138a67b4344fde84a5dfdd453db9fec273ccdc5`.
Les **176 fichiers protégés** sont identiques après synchronisation et avant
redémarrage. Environnement privé et toutes les clés/valeurs de
`server.properties` sont identiques après démarrage. Aucune régénération ni
migration de monde.
Prism : sauvegarde sous la même instance,
`sanctuary-backups/before-beta297-20261027T035256Z/instance.tar.gz`, SHA-256
`0c95dfd13aa45eb26ea62b16465c87598838a19b2835b7b641d197167c92af09`.
Les **933 fichiers personnels contrôlés**, `instance.cfg` et l'installateur
habillé Sanctuary sont inchangés. Le lien FFmpeg existant est conservé.
Aucun monde personnel ouvert. L'autre instance importée suit le canal au
prochain lancement ; elle n'a pas été réimportée ni modifiée manuellement.
Web : HEAD `eadc30e8c7b6dda50bc810584af057235e7fdba8` et **73 changements
préexistants** conservés, empreintes Git avant/après identiques. Seule la
distribution atomique est avancée. Alpha, réseau/NAT, comptes et permissions
ne sont pas modifiés ; le checkout principal du chantier voisin est préservé.
## Limites
Twitch/YouTube publics et l'écoute subjective n'ont pas de nouvelle validation.
Les essais graphiques utilisent une vidéo synthétique, un mur orienté sud ;
les quatre orientations sont vérifiées numériquement. ANI-CLI, le filtre
16 pixels/bloc et les disques blancs ne sont pas inclus. Les dimensions
restent entières, dans le budget serveur, sur un mur complet.
Reçus opérationnels : `build/release297/`, hors Git. Captures graphiques
finales : `build/media-prototype/frame-fixes-02/`.
+1 -1
View File
@@ -8,7 +8,7 @@ Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
## Préparer le kit
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
Le kit de la livraison courante est `build/Sanctuary-Server-beta.296.zip` ; les manifestes locaux sont
Le kit de la livraison courante est `build/Sanctuary-Server-beta.298.zip` ; les manifestes locaux sont
dans `build/packwiz-server/`. Le ZIP est un amorçage : il télécharge les mods
depuis le canal au premier démarrage, il n'est pas une copie hors ligne du pack.
Il n'est utilisable sur le canal public qu'après publication de cette version.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.296
pack_version=beta.296
mod_version=beta.298
pack_version=beta.298
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
@@ -41,6 +41,10 @@ public final class AnchorProbe {
}
case "unindex"->{var a=(AnchorBlockEntity)level.getBlockEntity(POS);Anchors.unindex(a);assertions.addProperty("index_removed",Anchors.loaded(level).isEmpty());}
case "near"->move(p,-3.5,81,4);
case "camera"->{
var v=ctl.getAsJsonArray("camera_a");p.setNoGravity(true);
move(p,v.get(0).getAsDouble(),v.get(1).getAsDouble(),v.get(2).getAsDouble());
}
case "wall"->{for(var q:BlockPos.betweenClosed(-6,81,6,6,86,6))level.setBlockAndUpdate(q,Blocks.STONE.defaultBlockState());}
case "clear-wall"->{for(var q:BlockPos.betweenClosed(-6,81,6,6,86,6))level.setBlockAndUpdate(q,Blocks.AIR.defaultBlockState());}
case "remove-support"->level.setBlockAndUpdate(new BlockPos(2,84,0),Blocks.AIR.defaultBlockState());
@@ -66,6 +70,7 @@ public final class AnchorProbe {
if(a!=null){row.addProperty("width",a.width);row.addProperty("height",a.height);row.addProperty("revision",a.revision);row.addProperty("display",String.valueOf(a.display));var d=com.dreamdisplays.api.server.DreamDisplaysServer.display(a.display);if(d!=null){row.addProperty("url",d.getUrl());row.addProperty("mode",d.getMode().name());}}
row.addProperty("anchor",a!=null);
if(p!=null){int n=0;for(int i=0;i<p.getInventory().getContainerSize();i++){var st=p.getInventory().getItem(i);if(st.is(Anchors.ITEM))n+=st.getCount();}row.addProperty("frames_in_inventory",n);}
if(p!=null)row.addProperty("crouching",p.isShiftKeyDown());
int drops=level.getEntitiesOfClass(ItemEntity.class,new AABB(-6,80,-1,6,87,5)).stream().filter(e->e.getItem().is(Anchors.ITEM)).mapToInt(e->e.getItem().getCount()).sum();row.addProperty("frame_drops",drops);
row.addProperty("displays",com.dreamdisplays.api.server.DreamDisplaysServer.isAvailable()?com.dreamdisplays.api.server.DreamDisplaysServer.getDisplays().getSize():-1);
Probe.write(Probe.LAB.resolve("anchor-server.jsonl"),row);
@@ -20,6 +20,11 @@ public final class AnchorProbeClient {
public static void tick(Minecraft mc){
if(mc.player==null||mc.level==null)return;
var ctl=Probe.control();String who=System.getProperty("sanctuary.mediaClient");
if(ctl.has("shaders_enabled")&&fr.koka.sanctuary.client.shader.VanillaLight.enabled()!=ctl.get("shaders_enabled").getAsBoolean())
fr.koka.sanctuary.client.shader.VanillaLight.setEnabled(ctl.get("shaders_enabled").getAsBoolean());
if(ctl.has("pbr_enabled")&&fr.koka.sanctuary.client.shader.VanillaLight.pbr()!=ctl.get("pbr_enabled").getAsBoolean())
fr.koka.sanctuary.client.shader.VanillaLight.setPbr(ctl.get("pbr_enabled").getAsBoolean());
if(ctl.has("hide_gui")&&mc.gui.hud.isHidden()!=ctl.get("hide_gui").getAsBoolean())mc.gui.hud.toggle();
boolean a=who.endsWith("a");
Vec3 aim=a?new Vec3(-3.5,81.5,.5):new Vec3(0,83,1);
if(ctl.has(a?"aim_a":"aim_b")){var xs=ctl.getAsJsonArray(a?"aim_a":"aim_b");aim=new Vec3(xs.get(0).getAsDouble(),xs.get(1).getAsDouble(),xs.get(2).getAsDouble());}
@@ -32,6 +37,7 @@ public final class AnchorProbeClient {
net.minecraft.client.Screenshot.grab(mc.gameDirectory,"anchor-"+Probe.phase+".png",mc.gameRenderer.mainRenderTarget(),1,m->{});captured=Probe.phase;
}
var row=new JsonObject();row.addProperty("step",done);row.addProperty("screen",mc.gui.screen()==null?"":mc.gui.screen().getClass().getSimpleName());
if(ctl.has("inspect_anchor_model"))inspectModel(mc,row);
var target=Anchors.target(mc.level,mc.player);row.addProperty("target",target!=null);if(target!=null)row.addProperty("distance",target.distance());
if(mc.gui.screen() instanceof AnchorScreen screen){var v=screen.snapshot();row.addProperty("settings",v.settings());row.addProperty("editable",v.editable());row.addProperty("message",v.message());row.addProperty("width",v.width());row.addProperty("height",v.height());}
else if(mc.gui.screen()!=null&&mc.gui.screen().getClass().getSimpleName().equals("DisplayMenu")){
@@ -39,11 +45,34 @@ public final class AnchorProbeClient {
}
Probe.write(Probe.LAB.resolve("anchor-"+who+".jsonl"),row);
}
private static void inspectModel(Minecraft mc,JsonObject row){
var state=mc.level.getBlockState(AnchorProbe.POS);
if(!state.is(Anchors.BLOCK))return;
var parts=new java.util.ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);
int quads=0;for(var part:parts)for(int f=0;f<7;f++)quads+=part.getQuads(f==6?null:Direction.values()[f]).size();
row.addProperty("anchor_model_quads",quads);row.addProperty("anchor_render_shape",state.getRenderShape().name());
row.addProperty("shaders_active",fr.koka.sanctuary.client.shader.VanillaLight.active());
row.addProperty("pbr_enabled",fr.koka.sanctuary.client.shader.VanillaLight.pbr());
try{
var pbr=fr.koka.sanctuary.client.shader.ProceduralPbr.class.getDeclaredMethod("applied");pbr.setAccessible(true);
row.addProperty("pbr_applied",(boolean)pbr.invoke(null));
var type=Class.forName("fr.koka.sanctuary.client.shader.PbrTerrain");var ctor=type.getDeclaredConstructor(boolean.class);ctor.setAccessible(true);
var candidate=type.getDeclaredMethod("candidate",net.minecraft.world.level.block.state.BlockState.class);candidate.setAccessible(true);
for(boolean transparent:new boolean[]{false,true})try(var terrain=(AutoCloseable)ctor.newInstance(transparent)){
row.addProperty(transparent?"anchor_pbr_transparent":"anchor_pbr_opaque",(boolean)candidate.invoke(terrain,state));
row.addProperty(transparent?"water_pbr_transparent":"water_pbr_opaque",(boolean)candidate.invoke(terrain,net.minecraft.world.level.block.Blocks.WATER.defaultBlockState()));
row.addProperty(transparent?"glass_pbr_transparent":"wood_pbr_opaque",(boolean)candidate.invoke(terrain,(transparent?net.minecraft.world.level.block.Blocks.GLASS:net.minecraft.world.level.block.Blocks.OAK_PLANKS).defaultBlockState()));
}
}catch(Exception e){throw new IllegalStateException(e);}
}
private static boolean perform(Minecraft mc,String step,JsonObject ctl){
if(step.equals("place")){
mc.gameMode.useItemOn(mc.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(-3.5,81.5,1),Direction.SOUTH,new BlockPos(-4,81,0),false));return true;
}
if(step.equals("close")){mc.gui.setScreen(null);return true;}
if(step.equals("shift-hold")){mc.gui.setScreen(null);mc.options.keyShift.setDown(true);return true;}
if(step.equals("shift-release")){mc.options.keyShift.setDown(false);return true;}
if(step.equals("unindex")){for(var a:Anchors.loaded(mc.level))Anchors.unindex(a);return true;}
if(step.equals("click")){mc.gui.setScreen(null);Access.privateCall(mc,"startUseItem");return true;}
if(step.equals("forged-open")){
+4 -4
View File
@@ -1,6 +1,6 @@
# Sanctuary Media — prototypes MEDIA-03/04
Module **0.3.0**, embarqué dans Sanctuary depuis **beta.296**,
Module **0.3.1**, embarqué dans Sanctuary depuis **beta.296**,
pour Dream Displays **1.10.4** sur Minecraft **26.3 / Java 25 / Fabric**.
Les clients décodent les médias ; le serveur fournit l'heure et garde l'état
partagé ainsi que la validation des constructions.
@@ -9,9 +9,9 @@ Compatibilité acoustique vérifiée avec Sound Physics Remastered **1.5.1+26.3*
L'extension conserve le moteur et le **menu original Dream Displays** pour la
recherche, le collage d'un lien, l'aperçu, la lecture, les pistes et les réglages.
Elle ajoute le cadre de redstone comme ancre : un objet à la pose, aucun coût
d'agrandissement, contour dessiné sur toute la surface. Maj + clic droit près
de l'ancre ou bouton Taille du menu natif ouvre le seul écran ajouté : largeur
et hauteur, formats rapides facultatifs et aperçu du ratio.
d'agrandissement, cadre en volume sur toute la surface. Le menu largeur/hauteur
s'ouvre à la pose, puis via le bouton Taille du lecteur. Clic droit et Maj +
clic droit ouvrent le lecteur. [Correctif et validation beta.297](../../docs/media-frame-fixes-beta297.md).
Les dimensions sont libres en blocs entiers. Les budgets du serveur sont dans
`config/sanctuary-media-anchors.json` (`maxWidth`, `maxHeight`, `maxArea`) ; une
+1 -1
View File
@@ -1,7 +1,7 @@
plugins { id 'java-library' }
group = rootProject.maven_group
version = '0.3.0'
version = '0.3.2'
base { archivesName = 'sanctuary-media' }
evaluationDependsOn(':sanctuary')
@@ -28,9 +28,10 @@ public final class AnchorBlock extends BaseEntityBlock {
};
}
@Override protected VoxelShape getCollisionShape(BlockState s,BlockGetter l,BlockPos p,CollisionContext c){return Shapes.empty();}
@Override protected VoxelShape getOcclusionShape(BlockState s){return Shapes.empty();}
@Override protected InteractionResult useWithoutItem(BlockState s,Level l,BlockPos pos,Player p,BlockHitResult hit){
if(p instanceof net.minecraft.server.level.ServerPlayer sp&&l.getBlockEntity(pos) instanceof AnchorBlockEntity a)
Anchors.open(sp,a,p.isShiftKeyDown());
Anchors.open(sp,a,false);
return InteractionResult.SUCCESS;
}
@Override protected InteractionResult useItemOn(ItemStack stack,BlockState s,Level l,BlockPos pos,Player p,InteractionHand hand,BlockHitResult hit){return useWithoutItem(s,l,pos,p,hit);}
@@ -29,7 +29,7 @@ public final class AnchorClient {
var hit=Anchors.target(mc.level,mc.player);
if(hit==null)return false;
var a=hit.info();
ClientPlayNetworking.send(new AnchorPackets.Action(a.geometry().anchor(),a.display(),mc.player.isShiftKeyDown()?6:0,0,0,a.revision(),"",0));
ClientPlayNetworking.send(new AnchorPackets.Action(a.geometry().anchor(),a.display(),0,0,0,a.revision(),"",0));
return true;
}
public static void openReader(UUID id){
@@ -0,0 +1,60 @@
package fr.koka.media.anchor;
import java.util.*;
import net.minecraft.world.phys.Vec3;
/** A closed wooden ring, in block units, flush to the wall and exactly inside the screen bounds. */
public final class AnchorFrameMesh {
public static final double THICKNESS=1.0/16;
public record Vertex(double u,double v,double depth){}
public record Face(Vertex a,Vertex b,Vertex c,Vertex d,int color){
public List<Vertex> vertices(){return List.of(a,b,c,d);}
}
public final int width,height;
public final List<Face> faces;
public AnchorFrameMesh(int width,int height){
if(width<1||height<1)throw new IllegalArgumentException("Invalid frame size");
this.width=width;this.height=height;
var result=new ArrayList<Face>();double t=THICKNESS,w=width,h=height;
for(double z:new double[]{t,0}){
boolean back=z==0;
rect(result,0,0,w,t,z,0x77512f,back);
rect(result,0,h-t,w,h,z,0xa47a48,back);
rect(result,0,t,t,h-t,z,0x90653a,back);
rect(result,w-t,t,w,h-t,z,0x694426,back);
}
// Outer sides, with outward winding.
side(result,0,0,w,0,0x684628);
side(result,w,0,w,h,0x795330);
side(result,w,h,0,h,0x9a7141);
side(result,0,h,0,0,0x876038);
// Inner reveal faces point into the opening around the recessed video.
side(result,w-t,t,t,t,0x51351e);
side(result,w-t,h-t,w-t,t,0x51351e);
side(result,t,h-t,w-t,h-t,0x684628);
side(result,t,t,t,h-t,0x684628);
faces=List.copyOf(result);
}
private static void rect(List<Face> out,double x0,double y0,double x1,double y1,double z,int color,boolean back){
var a=new Vertex(x0,y0,z);var b=new Vertex(x1,y0,z);var c=new Vertex(x1,y1,z);var d=new Vertex(x0,y1,z);
out.add(back?new Face(d,c,b,a,color):new Face(a,b,c,d,color));
}
private static void side(List<Face> out,double x0,double y0,double x1,double y1,int color){
out.add(new Face(new Vertex(x0,y0,0),new Vertex(x1,y1,0),new Vertex(x1,y1,THICKNESS),new Vertex(x0,y0,THICKNESS),color));
}
public static Vec3 world(AnchorGeometry g,Vertex v){
var n=g.facing();var right=g.right();
return Vec3.atCenterOf(g.wall()).add(n.getStepX()*(.5+v.depth),v.v-.5,n.getStepZ()*(.5+v.depth))
.add(right.getStepX()*(v.u-.5),0,right.getStepZ()*(v.u-.5));
}
/** Behind the raised rim, an observer outside the opening cannot see any part of the panel. */
public static boolean videoVisible(AnchorGeometry g,Vec3 eye,double videoClearance){
var n=g.facing();var wall=Vec3.atCenterOf(g.wall());
double outward=(eye.x-wall.x)*n.getStepX()+(eye.z-wall.z)*n.getStepZ()-.5;
if(outward<videoClearance)return false;
if(outward>=THICKNESS)return true;
var right=g.right();double u=(eye.x-wall.x)*right.getStepX()+(eye.z-wall.z)*right.getStepZ()+.5;
double v=eye.y-wall.y+.5;
return u>=THICKNESS&&u<=g.width()-THICKNESS&&v>=THICKNESS&&v<=g.height()-THICKNESS;
}
}
@@ -32,7 +32,7 @@ public final class AnchorScreen extends Screen {
g.centeredText(font,title,width/2,top+3,0xffffffff);g.centeredText(font,Anchors.label("one_frame"),width/2,top+18,0xffc3d4e4);
g.text(font,Anchors.label("width"),left+10,top+33,0xffffffff);g.text(font,Anchors.label("height"),left+112,top+33,0xffffffff);
int w=number(widthBox),h=number(heightBox);boolean valid=w>0&&h>0;
if(valid){double scale=Math.min((panelWidth-60.0)/w,42.0/h);int sw=Math.max(1,(int)(w*scale)),sh=Math.max(1,(int)(h*scale));int x=left+(panelWidth-sw)/2,y=top+99+(42-sh)/2;g.fill(x,y,x+sw,y+sh,0xff223a4b);g.outline(x,y,sw,sh,0xff87c8df);g.fill(x,y+sh-4,x+4,y+sh,0xffff3535);}
if(valid){double scale=Math.min((panelWidth-60.0)/w,42.0/h);int sw=Math.max(1,(int)(w*scale)),sh=Math.max(1,(int)(h*scale));int x=left+(panelWidth-sw)/2,y=top+99+(42-sh)/2;g.fill(x,y,x+sw,y+sh,0xff223a4b);g.outline(x,y,sw,sh,0xff87c8df);}
g.centeredText(font,Anchors.label("free_dimensions"),width/2,top+144,valid?0xffa5b6c7:0xffff7b7b);
if(!view.message().isEmpty())g.centeredText(font,Anchors.label(view.message()),width/2,top+180,0xffff8c8c);
}
@@ -161,7 +161,7 @@ public final class Anchors {
return;
}
if(!(p.level().getBlockEntity(m.pos()) instanceof AnchorBlockEntity a)||!Objects.equals(a.display,m.display()))return;
if(m.action()==0){open(p,a,p.isShiftKeyDown());return;}
if(m.action()==0){open(p,a,false);return;}
boolean edit=near(p,a)&&canBuild(p,a.getBlockPos());String error="";
if(m.action()==6){AnchorPackets.view(p,a,edit,edit?"":"protected");return;}
if(m.action()==1){
@@ -1,41 +1,56 @@
package fr.koka.media.mixin;
import fr.koka.media.anchor.Anchors;
import fr.koka.media.anchor.*;
import java.util.*;
import com.dreamdisplays.platform.client.displays.DisplayScreen;
import com.dreamdisplays.platform.client.render.*;
import com.dreamdisplays.libs.kotlin.Unit;
import com.dreamdisplays.libs.kotlin.jvm.functions.Function2;
import com.mojang.blaze3d.vertex.*;
import net.minecraft.client.Camera;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** The same pose, visibility and Vulkan draw path as the video, with constant block-space thickness. */
/** Block-space ring using Dream's Vulkan draw path; its video transform deliberately flattens Z. */
@Mixin(targets="com.dreamdisplays.platform.client.render.ScreenRenderer",remap=false)
abstract class AnchorBorderMixin {
@Unique private Vec3 media$eye=Vec3.ZERO;
@Unique private final Map<DisplayScreen,AnchorFrameMesh> media$meshes=new WeakHashMap<>();
@Inject(method="render(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/Camera;ZLcom/dreamdisplays/libs/kotlin/jvm/functions/Function2;)V",at=@At("HEAD"),remap=false)
private void media$camera(PoseStack stack,Camera camera,boolean replay,@Coerce Object renderer,CallbackInfo ci){media$eye=camera.position();}
@Redirect(method="render(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/Camera;ZLcom/dreamdisplays/libs/kotlin/jvm/functions/Function2;)V",
at=@At(value="INVOKE",target="Lcom/dreamdisplays/platform/client/displays/DisplayScreen;getHasTexture()Z"),remap=false)
private boolean media$blankScreen(DisplayScreen s){return s.getHasTexture()||s.getCustomData().containsKey(Anchors.MARKER);}
@SuppressWarnings("unchecked")
@Inject(method="renderScreenTexture",at=@At("HEAD"),cancellable=true,remap=false)
private void media$back(DisplayScreen screen,PoseStack stack,boolean replay,@Coerce Object renderer,CallbackInfo ci){
var info=AnchorInfo.decode(screen.getUuid(),screen.getCustomData().get(Anchors.MARKER));
if(info!=null&&!AnchorFrameMesh.videoVisible(info.geometry(),media$eye,DisplayPoseStack.INSTANCE.surfaceClearance()+(replay?.01:0))){
if(!replay)screen.fitTextureWhileHidden();
media$draw(screen,stack,renderer,info.geometry());ci.cancel();
}
}
@Inject(method="renderScreenTexture",at=@At("TAIL"),remap=false)
private void media$border(DisplayScreen screen,PoseStack stack,boolean replay,@Coerce Object renderer,CallbackInfo ci){
if(!screen.getCustomData().containsKey(Anchors.MARKER))return;
private void media$front(DisplayScreen screen,PoseStack stack,boolean replay,@Coerce Object renderer,CallbackInfo ci){
var info=AnchorInfo.decode(screen.getUuid(),screen.getCustomData().get(Anchors.MARKER));
if(info!=null)media$draw(screen,stack,renderer,info.geometry());
}
@Unique @SuppressWarnings("unchecked")
private void media$draw(DisplayScreen screen,PoseStack stack,Object renderer,AnchorGeometry geometry){
var mesh=media$meshes.get(screen);
if(mesh==null||mesh.width!=geometry.width()||mesh.height!=geometry.height()){
mesh=new AnchorFrameMesh(geometry.width(),geometry.height());media$meshes.put(screen,mesh);
}
var frame=mesh;var origin=Vec3.atLowerCornerOf(screen.getPos());
var draw=(Function2<RenderType,Function2<PoseStack.Pose,VertexConsumer,Unit>,Unit>)renderer;
stack.pushPose();
DisplayPoseStack.INSTANCE.liftTowardViewer(stack,screen.getFacing(),replay?.025f:.015f);
DisplayPoseStack.INSTANCE.applyScreenTransform(stack,screen.getFacing(),screen.getWidth(),screen.getHeight());
float x=1f/(16*screen.getWidth()),y=1f/(16*screen.getHeight());
draw.invoke(DisplayYuvRenderTypes.INSTANCE.solidColorType(),(pose,v)->{
media$rect(pose,v,0,0,1,y,100,69,39);media$rect(pose,v,0,1-y,1,1,151,115,65);
media$rect(pose,v,0,y,x,1-y,127,90,49);media$rect(pose,v,1-x,y,1,1-y,80,51,27);
media$rect(pose,v,x,y,x*2,y*2,184,19,13);
for(var face:frame.faces)for(var vertex:face.vertices()){
var p=AnchorFrameMesh.world(geometry,vertex).subtract(origin);int c=face.color();
v.addVertex(pose,(float)p.x,(float)p.y,(float)p.z).setUv(0,0).setColor((c>>16)&255,(c>>8)&255,c&255,255);
}
return Unit.INSTANCE;
});
stack.popPose();
}
@Unique private static void media$rect(PoseStack.Pose p,VertexConsumer v,float x0,float y0,float x1,float y1,int r,int g,int b){
v.addVertex(p,x0,y0,0).setUv(0,0).setColor(r,g,b,255);v.addVertex(p,x1,y0,0).setUv(0,0).setColor(r,g,b,255);
v.addVertex(p,x1,y1,0).setUv(0,0).setColor(r,g,b,255);v.addVertex(p,x0,y1,0).setUv(0,0).setColor(r,g,b,255);
}
}
@@ -0,0 +1,22 @@
package fr.koka.media.mixin;
import fr.koka.media.anchor.Anchors;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.renderer.state.level.BlockOutlineRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** The invisible 1x1 anchor must not leave its vanilla selection rectangle inside the large screen. */
@Mixin(LevelRenderer.class)
abstract class AnchorOutlineMixin {
@Inject(method="submitHitOutline",at=@At("HEAD"),cancellable=true)
private void media$outline(PoseStack pose,SubmitNodeCollector collector,RenderType type,BlockOutlineRenderState state,
int color,float width,boolean translucent,CallbackInfo ci){
var level=Minecraft.getInstance().level;
if(level!=null&&level.getBlockState(state.pos()).is(Anchors.BLOCK))ci.cancel();
}
}
@@ -1,170 +1,7 @@
{
"ambientocclusion": false,
"textures": {
"particle": "minecraft:block/oak_planks",
"wood": "minecraft:block/oak_planks",
"redstone": "minecraft:block/redstone_block"
"particle": "minecraft:block/oak_planks"
},
"elements": [
{
"from": [
0,
0,
0
],
"to": [
16,
1,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
0,
15,
0
],
"to": [
16,
16,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
0,
1,
0
],
"to": [
1,
15,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
15,
1,
0
],
"to": [
16,
15,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
1,
1,
0
],
"to": [
3,
3,
2
],
"faces": {
"north": {
"texture": "#redstone"
},
"south": {
"texture": "#redstone"
},
"east": {
"texture": "#redstone"
},
"west": {
"texture": "#redstone"
},
"up": {
"texture": "#redstone"
},
"down": {
"texture": "#redstone"
}
}
}
]
"elements": []
}
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"id": "sanctuary_media",
"version": "0.3.0",
"version": "0.3.2",
"name": "Sanctuary Media",
"license": "GPL-3.0-or-later",
"environment": "*",
@@ -2,7 +2,33 @@
"required": true,
"package": "fr.koka.media.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["AnchorDisplayGuardMixin", "AnchorActionGuardMixin"],
"client": ["AnchorUseMixin", "AnchorMenuMixin", "AnchorModeMixin", "AnchorBorderMixin", "AnchorTextureBudgetMixin", "AnchorResizePlaybackMixin", "AudioSinkMixin", "MasterClockMixin", "PlayerClockMixin", "SessionStartMixin", "FrameGateMixin", "TimelineMixin", "VideoUrlMixin", "AudioUrlMixin", "SoundManagerAccess", "SoundEngineAccess", "LibraryAccess", "SoundLifecycleMixin", "VideoOptionsMixin"],
"injectors": {"defaultRequire": 1}
"mixins": [
"AnchorDisplayGuardMixin",
"AnchorActionGuardMixin"
],
"client": [
"AnchorUseMixin",
"AnchorMenuMixin",
"AnchorModeMixin",
"AnchorBorderMixin",
"AnchorOutlineMixin",
"AnchorTextureBudgetMixin",
"AnchorResizePlaybackMixin",
"AudioSinkMixin",
"MasterClockMixin",
"PlayerClockMixin",
"SessionStartMixin",
"FrameGateMixin",
"TimelineMixin",
"VideoUrlMixin",
"AudioUrlMixin",
"SoundManagerAccess",
"SoundEngineAccess",
"LibraryAccess",
"SoundLifecycleMixin",
"VideoOptionsMixin"
],
"injectors": {
"defaultRequire": 1
}
}
@@ -0,0 +1,41 @@
package fr.koka.media;
import fr.koka.media.anchor.*;
import net.minecraft.core.*;
import net.minecraft.world.phys.Vec3;
final class AnchorFrameMeshTest {
static void run(){
for(int[] size:new int[][]{{1,1},{7,4},{33,7},{256,64}}){
var mesh=new AnchorFrameMesh(size[0],size[1]);assert mesh.faces.size()==16;
for(var face:mesh.faces){
for(var v:face.vertices()){
assert v.u()>=0&&v.u()<=size[0]&&v.v()>=0&&v.v()<=size[1];
assert v.depth()==0||v.depth()==AnchorFrameMesh.THICKNESS;
}
assert ((face.color()>>8)&255)>40; // No red anchor marker remains.
Vec3 a=local(face.a()),b=local(face.b()),c=local(face.c());
assert b.subtract(a).cross(c.subtract(a)).lengthSqr()>0;
}
for(Direction facing:Direction.Plane.HORIZONTAL){
var g=new AnchorGeometry(new BlockPos(12,70,24),facing,size[0],size[1]);
var n=new Vec3(facing.getStepX(),0,facing.getStepZ());
assert AnchorFrameMesh.videoVisible(g,g.center().add(n.scale(2)),.008);
assert !AnchorFrameMesh.videoVisible(g,g.center().subtract(n.scale(2)),.008);
for(var v:new AnchorFrameMesh.Vertex[]{new AnchorFrameMesh.Vertex(-1,size[1]/2.0,.03),
new AnchorFrameMesh.Vertex(size[0]+1,size[1]/2.0,.03),new AnchorFrameMesh.Vertex(size[0]/2.0,size[1]+1,.03),
new AnchorFrameMesh.Vertex(size[0]/2.0,-1,.03)})assert !AnchorFrameMesh.videoVisible(g,AnchorFrameMesh.world(g,v),.008);
assert AnchorFrameMesh.videoVisible(g,AnchorFrameMesh.world(g,new AnchorFrameMesh.Vertex(size[0]/2.0,size[1]/2.0,.03)),.008);
assert AnchorFrameMesh.videoVisible(g,AnchorFrameMesh.world(g,new AnchorFrameMesh.Vertex(size[0]+1,size[1]/2.0,.25)),.008);
for(var face:mesh.faces)for(var v:face.vertices()){
var p=AnchorFrameMesh.world(g,v).subtract(Vec3.atCenterOf(g.wall()));
assert Math.abs(p.dot(n)-(.5+v.depth()))<1e-7;
assert Math.abs(p.y-(v.v()-.5))<1e-7;
var r=g.right();assert Math.abs(p.x*r.getStepX()+p.z*r.getStepZ()-(v.u()-.5))<1e-7;
}
}
}
System.out.println("Frame mesh: exact bounds, 16 nondegenerate faces, four orientations and front-only video passed");
}
private static Vec3 local(AnchorFrameMesh.Vertex v){return new Vec3(v.u(),v.v(),v.depth());}
}
@@ -66,5 +66,6 @@ public final class MediaSyncTest {
HlsProxy.forget("other");
System.out.println("Media clock / reconnect / timeline / HLS contracts passed");
AnchorGeometryTest.run();
AnchorFrameMeshTest.run();
}
}
@@ -142,6 +142,12 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
check(failures==0,"Close PBR stays stable under subpixel motion: failing distances="+failures);
}
private static void materials(){
try(var materials=new PbrMaterials()){
check(!materials.candidate(Blocks.BARRIER.defaultBlockState())&&!materials.candidate(Blocks.MOVING_PISTON.defaultBlockState()),
"Invisible and entity-rendered blocks cannot leave normals from their baked terrain model");
check(materials.candidate(Blocks.OAK_PLANKS.defaultBlockState())&&materials.candidate(Blocks.GLASS.defaultBlockState()),
"Visible wooden and glass terrain models keep their material pass");
}
int[] flat=new int[16];Arrays.fill(flat,0xFF808080);var profile=PbrMaterials.profile("bricks");
for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal");
flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes");
@@ -24,7 +24,9 @@ final class PbrMaterials implements AutoCloseable {
return state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)&&state.liquid()?2:
(name.equals("glass")||name.endsWith("_glass")||name.equals("glass_pane")||name.endsWith("_glass_pane"))?1:0;
}
boolean candidate(BlockState state){return !state.isAir();}
// Invisible blocks may still have a baked model (for particles or items).
// Their hidden faces must never enter the normal/material buffer.
boolean candidate(BlockState state){return state.getRenderShape()==net.minecraft.world.level.block.RenderShape.MODEL;}
Profile profile(BlockState state){return profiles.computeIfAbsent(state,s->profile(BuiltInRegistries.BLOCK.getKey(s.getBlock()).getPath()));}
static Profile profile(String name){
if(name.equals("water"))return new Profile(.12f,0);
@@ -31,7 +31,7 @@ final class PbrTerrain implements AutoCloseable {
PbrTerrain(){this(false);}
PbrTerrain(boolean transparent){this.transparent=transparent;maxBytes=(transparent?8L:24L)*1024*1024;}
private boolean candidate(net.minecraft.world.level.block.state.BlockState state){
return transparent ? PbrMaterials.family(state)==1||state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)
return transparent ? (materials.candidate(state)&&PbrMaterials.family(state)==1)||state.getFluidState().is(net.minecraft.tags.FluidTags.WATER)
: materials.candidate(state)&&!state.liquid()&&PbrMaterials.family(state)==0;
}
private final Long2ObjectOpenHashMap<Section> sections=new Long2ObjectOpenHashMap<>();
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.296"
version = "beta.298"
pack-format = "packwiz:1.1.0"
[index]
+7
View File
@@ -40,10 +40,17 @@ def run(name):
step("anchor-place", anchor_a="place", aim_a=[-3.5, 81.5, .5])
state = last("anchor-server")
check("one frame consumed on placement", state.get("anchor") and state["frames_in_inventory"] == 3)
check("first placement opens dimensions", last("anchor-client-a").get("screen") == "AnchorScreen" and last("anchor-client-a").get("settings"))
inventory = state["frames_in_inventory"]
original_id = state["display"]
step("near-anchor", anchor_server_step="near", aim_a=[-3.5, 81.5, .5], anchor_a="close")
step("open-anchor", anchor_a="click")
check("reopening near the anchor opens native player", last("anchor-client-a").get("screen") == "DisplayMenu")
step("shift-held", anchor_a="shift-hold")
crouching = last("anchor-server").get("crouching")
step("shift-reader", anchor_a="click")
check("Shift-right-click also opens native player", crouching and last("anchor-client-a").get("screen") == "DisplayMenu")
step("release-shift", anchor_a="shift-release")
step("custom-size", anchor_a="size:7:4")
check("custom 7x4 and free enlargement", last("anchor-server")["width"] == 7 and last("anchor-server")["height"] == 4 and last("anchor-server")["frames_in_inventory"] == inventory)
step("shrink", anchor_a="size:1:1")