Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3eb791fe | ||
|
|
83c377acc1 | ||
|
|
7eef9454d4 | ||
|
|
580b2414da | ||
|
|
97ff2b54cb | ||
|
|
59346d7f0a | ||
|
|
ea151dd5df | ||
|
|
7ce01c47f2 | ||
|
|
93ed5574a4 | ||
|
|
c9f5c56e68 | ||
|
|
6e1b3b7108 | ||
|
|
c05c338e6b | ||
|
|
dc33ffa546 | ||
|
|
9d84a02ed9 | ||
|
|
91181865d5 | ||
|
|
daf36fdeac | ||
|
|
7f576d14d9 | ||
|
|
2d7072b8a8 | ||
|
|
0781cbd964 | ||
|
|
71296a9cfb | ||
|
|
07d09c8508 | ||
|
|
7fada570ef | ||
|
|
b7cf4bc294 | ||
|
|
fc4a0cb68e | ||
|
|
17172fd18b | ||
|
|
216d3f75b5 | ||
|
|
e48fc656c7 | ||
|
|
7e21781a9a | ||
|
|
d941b91265 | ||
|
|
078d3e6833 | ||
|
|
afa77f3d62 | ||
|
|
534dfec4f8 | ||
|
|
26e305d15f | ||
|
|
35103a7ea8 | ||
|
|
6502883cc4 | ||
|
|
a4e96c0d99 | ||
|
|
7c63865157 | ||
|
|
00be1eb0d0 | ||
|
|
adde24aa7e | ||
|
|
99860af312 | ||
|
|
1547396c06 | ||
|
|
4ec5b0d028 | ||
|
|
67e6e7c4f2 | ||
|
|
00179e30ef | ||
|
|
87f823af67 | ||
|
|
4552cc347b | ||
|
|
49131b4c53 | ||
|
|
edc7fc3dfb | ||
|
|
9b0104d27a | ||
|
|
d9b3dd6bed | ||
|
|
e732da8800 | ||
|
|
248dff47b7 | ||
|
|
6aaabf1cb9 | ||
|
|
72e9263339 | ||
|
|
3fd64e1f2a | ||
|
|
8af406a028 | ||
|
|
0a81b3c147 | ||
|
|
68fd97d287 | ||
|
|
0f1efdec83 | ||
|
|
5c666a4ebc | ||
|
|
e3ca9fcd63 | ||
|
|
22e58b4b91 | ||
|
|
ed0e0b26aa | ||
|
|
21845bd736 | ||
|
|
4a6f63c52d | ||
|
|
6166d0aa9e | ||
|
|
064701f210 | ||
|
|
1eaa976ecf | ||
|
|
8c0020c774 | ||
|
|
289697becf | ||
|
|
e38f6cbdbe | ||
|
|
2090380712 | ||
|
|
468743bfe7 |
+130
@@ -1,5 +1,135 @@
|
||||
# Changelog
|
||||
|
||||
## beta.110 — Atelier d’argile inclus
|
||||
|
||||
- Une seule livraison réunit l’atelier d’argile, les statuaires et l’import GLB de beta.106 avec les œufs et la neige saisonnière de beta.107 à beta.109.
|
||||
- Fonctionnement, identifiants et formats conservés ; accumulation toujours plafonnée à deux blocs par défaut.
|
||||
- [Contrat et vérifications](docs/clay-workshop-integration-beta110.md).
|
||||
|
||||
## beta.109 — Fonte saisonnière
|
||||
|
||||
- Fonte par couches des dépôts météo suivis, de haut en bas, plus rapide en été.
|
||||
- Arrêt en hiver, dans les biomes froids, sous un profil neigeux ou en mode météo Vanilla.
|
||||
- Constructions et neige antérieure préservées ; suivi sauvegardé par chunk, retiré lors des modifications manuelles.
|
||||
- Gamerule `sanctuary:seasonal_snow_melt` active par défaut ; accumulation toujours limitée à deux blocs par défaut.
|
||||
- [Contrat et essais](docs/seasonal-snow-melt-beta109.md), sur la base beta.108, Statuaire beta.106 livré séparément.
|
||||
|
||||
## beta.108 — Accumulation de neige
|
||||
|
||||
- Chaque précipitation ajoute une couche ; la huitième forme un bloc de neige natif solide, puis le dépôt continue au-dessus.
|
||||
- Plafond de deux blocs par défaut, gamerule réglable (0 = arrêt, -1 = sans plafond particulier).
|
||||
- Collisions, supports, lumière et chaudrons natifs conservés ; accumulation seulement pendant la chute de neige.
|
||||
- [Contrat et essais](docs/snow-accumulation-beta108.md). Assemblage isolé sur beta.107, avec les œufs, sans le Statuaire beta.106 livré séparément.
|
||||
|
||||
## beta.107 — Naissances en œufs
|
||||
|
||||
- Reproduction : œuf natif au lieu d’un bébé vivant, variantes et caractéristiques héritées ; bébé garanti à l’éclosion.
|
||||
- Grenouille/têtard, tortue, sniffer, allay et villageois : chemins natifs particuliers pris en charge.
|
||||
- Générateur classique cassé en survie sans Toucher de soie : un œuf de son type.
|
||||
- Catalogue des 88 espèces : 35 voies couvertes et 53 propositions à valider, sans ajouter ces propositions au jeu.
|
||||
- [Contrat et contrôles](docs/spawn-eggs-acquisition-beta107.md), [catalogue](docs/spawn-eggs-catalogue-beta107.md).
|
||||
- Livraison isolée sur la base beta.105 ; le chantier Statuaire beta.106 est conservé séparément.
|
||||
|
||||
## beta.106 — Atelier d’argile et modèles 3D
|
||||
|
||||
- Dossier commun pour schémas et GLB ; sélection des modèles 3D dans Statues au Métabli.
|
||||
- Atelier d’argile : un bloc d’argile pour un statuaire 16³ à poser. Couleurs du modèle avec l’argile normale, couleur unie avec les seize argiles Sanctuary.
|
||||
- Modèle identifié et embarqué dans l’objet, sauvegarde serveur et rendu natif de la miniature dans le monde et l’inventaire.
|
||||
- Contrat d’import, de sauvegarde et limites : `docs/statuary-beta106.md`.
|
||||
|
||||
## beta.105 — Saisons et chapeaux vivants
|
||||
|
||||
- Molette pour les pages du fût ; panoramas avec les effets actifs du shader Sanctuary.
|
||||
- Prévisions saisonnières, neige continue ou averses de neige avec accumulation native.
|
||||
- Chapeaux réactifs : paratonnerre, briquet, spawner, fusée, cultures et aliments attirants.
|
||||
- Livre et plume : journal personnel selon le caractère et les caresses/blessures.
|
||||
- Les boules de neige activent les objets portés. Aucune Weather TNT.
|
||||
- [Contrat et vérifications](docs/living-hats-seasons-beta105.md).
|
||||
|
||||
## beta.104 — Contraste des argiles
|
||||
|
||||
- Gris soutenu et noir plus sombre, dessin vanilla plus contrasté.
|
||||
- Boules d’argile accordées aux blocs, autres textures intactes.
|
||||
- Rangement beta.103 conservé ; [détails](docs/clay-contrast-beta104.md).
|
||||
|
||||
## beta.103 — Rangement créatif
|
||||
|
||||
- Séries de 16 par type : briques, escaliers, dalles, argiles et leurs items.
|
||||
- Ordre des couleurs identique à l’onglet natif Minecraft 26.3.
|
||||
- [Détails](docs/colored-bricks-order-beta103.md).
|
||||
|
||||
## beta.102 — Briques et argile de couleur
|
||||
|
||||
- 16 familles : briques, dalles, escaliers, argile pastel, boules d’argile et briques en items.
|
||||
- Textures fournies intactes, recoloration pixel par pixel des trois textures vanilla.
|
||||
- Onglets natifs Couleurs/Ingrédients, recettes, cuisson, tailleur, butin et découvertes.
|
||||
- [Contrat et vérifications](docs/colored-bricks-beta102.md).
|
||||
|
||||
## beta.101 — Atterrissage des familiers volants
|
||||
|
||||
- Les profils volants ne subissent plus les dégâts et effets de chute de leur entité commune.
|
||||
- Les vrais coups et les chutes des familiers terrestres sont conservés.
|
||||
- [Reproduction et vérifications](docs/flying-landing-beta101.md).
|
||||
|
||||
## beta.100 — Exploration et promenade des familiers
|
||||
|
||||
- Statues limitées aux espèces rencontrées, vaincues ou ayant tué le joueur.
|
||||
- Catalogue et génération filtrés par une liste de découvertes fournie par le serveur.
|
||||
- Promenade avec destinations stables et pauses selon le tempérament, y compris en vol et en garde.
|
||||
- Import `.schem` : conversion native des anciennes palettes dont la version est connue, diagnostic du bloc incompatible.
|
||||
- [Contrat et vérifications](docs/exploration-familiers-beta100.md).
|
||||
|
||||
## beta.099 — Catalogue, compétences et gestes K
|
||||
|
||||
- Compétences avant les progrès ; arbre natif intégré et suivi par clic droit.
|
||||
- Fiches illustrées communes, trois plans récents terminés, infrastructures et décoration.
|
||||
- Palette de statue étendue aux blocs du registre, avec comparaison couleur et forme.
|
||||
- K bref : ancrer au point visé ou tourner au même point ; K maintenu : commandes.
|
||||
- Métabli préassemblé sur le piédestal des nouvelles salles souterraines.
|
||||
- Clé dorée en outil plat, texture vanilla provisoire en attente de celle du créateur.
|
||||
- [Contrat et vérifications](docs/catalogue-progression-beta099.md).
|
||||
|
||||
## beta.098 — Construction créative
|
||||
|
||||
- Bouton « Construire le plan… » dans K, visible uniquement en créatif.
|
||||
- Confirmation du plan complet à l’origine et dans l’orientation sélectionnées.
|
||||
- Statues, bâtiments et composants de machines ; assemblage multibloc à la clé.
|
||||
- [Détails et vérifications](docs/creative-construction-beta098.md).
|
||||
|
||||
## beta.097 — Temples et aperçu texturé
|
||||
|
||||
- Recherche de secours pour les temples des expéditions, avec pièces vanilla et fondation locale si nécessaire.
|
||||
- Conserve la première recherche et les emplacements déjà admissibles.
|
||||
- Aperçu des plans avec les modèles et textures des blocs, formes natives et nom du matériau visé.
|
||||
- [Contrat et vérifications](docs/temple-crash-beta097.md).
|
||||
|
||||
## beta.096 — Métabli
|
||||
|
||||
- Atelier de neuf établis avec les textures fournies, assemblage et dissociation à la clé.
|
||||
- Catalogue Statues, Machines, Bâtiments et Mes plans ; sélection unique à l’atelier.
|
||||
- K garde les commandes de placement, rotation, couches et matériaux.
|
||||
- Construction manuelle ; changer de projet ou l’abandonner exige de revenir à l’atelier.
|
||||
- [Détails et vérifications](docs/metabli-beta096.md).
|
||||
|
||||
## beta.095 — Pilier des super pistons
|
||||
|
||||
- Bois du centre de la grande plaque sur la tige, sans bordure ni étirement.
|
||||
- Découpage adapté aux segments courts du socle et de la tête.
|
||||
- [Détails et vérifications](docs/piston-shaft-beta095.md).
|
||||
|
||||
## beta.094 — Feu du Fourneau
|
||||
|
||||
- Braises dans les deux ouvertures, allumage commun lié à la chaleur.
|
||||
- Flammes et fumée alignées sur la façade dans les quatre orientations.
|
||||
- Pierre originale et fours isolés conservés.
|
||||
- [Détails et vérifications](docs/fourneau-allume-beta094.md).
|
||||
|
||||
## beta.093 — Crash des infobulles pendant la recherche
|
||||
|
||||
- Retire les accès à la police graphique lors de l’indexation asynchrone des
|
||||
descriptions de familiers ; texte complet et mise en page visible conservés.
|
||||
- [Diagnostic et vérifications](docs/tooltip-crash-beta093.md).
|
||||
|
||||
## beta.092 — Clic molette des super pistons
|
||||
|
||||
- Socles, têtes et tiges renvoient le piston normal ou collant d’origine.
|
||||
|
||||
@@ -20,10 +20,23 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
Les sources Git sont synchronisées jusqu’à **beta.092**. Le récapitulatif de
|
||||
[synchronisation](docs/git-sync-beta092.md) précise les vérifications et les
|
||||
limites. Les archives bêta mentionnées dans ce document sont des fichiers
|
||||
locaux ; la dernière release du canal packwiz reste **alpha.30.7**.
|
||||
La version **beta.144** ajoute une option d’**eau naturelle pixélisée** :
|
||||
couleur du biome, trois profils selon le fond, vert olive d’étang et visibilité
|
||||
adaptée en immersion.
|
||||
Les océans gardent leur couleur native et le profil clair.
|
||||
[Contrat et vérifications](docs/natural-water-beta144.md).
|
||||
La beta.144 est publiée et synchronisée dans **Sanctuary Beta** ;
|
||||
les 923 fichiers personnels suivis sont conservés.
|
||||
|
||||
La version **beta.143** garde le sampler natif de précipitation pour la neige
|
||||
saisonnière, mais ralentit son rythme : l'accumulation est espacée par un
|
||||
facteur 12 et la fonte par le facteur temps réel 72. Les protections existantes
|
||||
restent inchangées : neige construite, biomes froids, hiver, profils neigeux
|
||||
et météo Vanilla. Le [ticket](docs/realtime-snow-melt-beta143.md) documente le
|
||||
contrat et les vérifications.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
|
||||
Les [archives pour le site](archives/web/README.md) conservent la maquette
|
||||
Blocodex autonome, les documents et les images retrouvées jusqu'à alpha.30.7.
|
||||
@@ -31,6 +44,427 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.144 — Eau naturelle pixélisée
|
||||
|
||||
Eau claire, légèrement teintée ou vert olive d’étang selon le fond, avec
|
||||
textures et animation natives. Océans clairs, visibilité sous-marine adaptée,
|
||||
interrupteur et intensité dans les options Shaders.
|
||||
[Contrat et mesures](docs/natural-water-beta144.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Test-beta.144.mrpack) ·
|
||||
[Tous les livrables](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144).
|
||||
|
||||
## beta.143 — Neige saisonnière en temps réel
|
||||
|
||||
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
|
||||
facteur naturel 72, tout en conservant le sampler natif des colonnes de
|
||||
précipitation. Deux blocs de neige fondent en environ 2,7 jours réels en été,
|
||||
5,4 au printemps et 11 en automne.
|
||||
[Contrat et vérifications](docs/realtime-snow-melt-beta143.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-beta.143.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.143/Sanctuary-Test-beta.143.mrpack).
|
||||
|
||||
## beta.142 — Vulkan par défaut et stabilité PBR
|
||||
|
||||
Moteur natif Vulkan prioritaire, relief conservé pendant les reconstructions,
|
||||
calculs de reflets bornés et intensité initiale 80 %. Choix personnels conservés.
|
||||
[Contrat et vérifications](docs/pbr-refresh-beta142.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-beta.142.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.142/Sanctuary-Test-beta.142.mrpack).
|
||||
|
||||
## beta.141 — Direction locale et normales PBR
|
||||
|
||||
Relief et reflets orientés par les lumières locales. Cache compact, confiance
|
||||
réduite pour les sources opposées, objets portés et fondus. Activer PBR et
|
||||
Lumières colorées ; portée PBR 32 blocs.
|
||||
[Contrat et vérifications](docs/directional-lights-beta141.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-beta.141.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.141/Sanctuary-Test-beta.141.mrpack).
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Mains et tête, distances 8/16/32/64, fondus, cuivre vert, redstone rouge et
|
||||
option pour la chaleur des torches. Lumières colorées activées à 30 % par défaut.
|
||||
[Contrat et vérifications](docs/colored-dynamic-beta140.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-beta.140.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.140/Sanctuary-Test-beta.140.mrpack).
|
||||
|
||||
## beta.139 — Lumières colorées
|
||||
|
||||
Teintes chaudes, sources des âmes bleues, redstone rouge, froglights et matériaux lumineux.
|
||||
Option désactivée initialement, intensité 35 % ; propagation locale avec obstacles.
|
||||
[Contrat et vérifications](docs/colored-lights-beta139.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-beta.139.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.139/Sanctuary-Test-beta.139.mrpack).
|
||||
|
||||
## beta.138 — Rayons et brouillard
|
||||
|
||||
Densité automatique des rayons selon le brouillard et les précipitations.
|
||||
[Contrat et vérifications](docs/shader-beta138.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.138/Sanctuary-beta.138.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.138/Sanctuary-Test-beta.138.mrpack).
|
||||
|
||||
## beta.137 — Rayons solaires
|
||||
|
||||
Faisceaux volumétriques légers, intensité réglable et feuillage perméable.
|
||||
[Contrat et vérifications](docs/shader-beta137.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-beta.137.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.137/Sanctuary-Test-beta.137.mrpack).
|
||||
|
||||
## beta.136 — Lens flare rectangulaire
|
||||
|
||||
Soleil et lune, reflets doux et intensité indépendante du bloom.
|
||||
[Contrat et vérifications](docs/shader-beta136.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-beta.136.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.136/Sanctuary-Test-beta.136.mrpack).
|
||||
|
||||
## beta.135 — SSGI près des surfaces
|
||||
|
||||
Correction des coupures à courte distance, sans flouter les textures.
|
||||
[Contrat et vérifications](docs/shader-beta135.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-beta.135.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.135/Sanctuary-Test-beta.135.mrpack).
|
||||
|
||||
## beta.134 — PBR optionnel et SSGI filtré
|
||||
|
||||
Relief et reflets des matériaux ; lissage de la lumière indirecte.
|
||||
[Contrat et vérifications](docs/shader-beta134.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-beta.134.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.134/Sanctuary-Test-beta.134.mrpack).
|
||||
|
||||
## beta.133 — SSGI renforcé
|
||||
|
||||
Couleurs voisines plus visibles, même budget de rayons et de cibles GPU.
|
||||
[Contrat et vérifications](docs/shader-beta133.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.133/Sanctuary-beta.133.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.133/Sanctuary-Test-beta.133.mrpack).
|
||||
|
||||
## beta.132 — SSGI : diffusion locale des couleurs
|
||||
|
||||
Rebond diffus des couleurs visibles, intensité réglable et reconstruction
|
||||
préservant les silhouettes. Expérimental, initialement désactivé.
|
||||
[Contrat et vérifications](docs/shader-beta132.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.132/Sanctuary-beta.132.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.132/Sanctuary-Test-beta.132.mrpack).
|
||||
|
||||
## beta.131 — Émissifs indépendants et précision étendue
|
||||
|
||||
Bloom, émissifs et highlight activés initialement ; halo facultatif et cinq
|
||||
précisions communes de 8 à 128 pixels/bloc.
|
||||
[Contrat et vérifications](docs/shader-beta131.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.131/Sanctuary-beta.131.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.131/Sanctuary-Test-beta.131.mrpack).
|
||||
|
||||
## beta.130 — Bloom, minerais émissifs et précision commune
|
||||
|
||||
Halo réglable des sources lumineuses et inclusions des vingt textures de
|
||||
minerais ; réglages par défaut 32/128/16 et ombres activées.
|
||||
[Contrat et vérifications](docs/shader-beta130.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.130/Sanctuary-beta.130.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.130/Sanctuary-Test-beta.130.mrpack).
|
||||
|
||||
## beta.129 — Reflets CTM et ombres jusqu'à 256 blocs
|
||||
|
||||
Reflets connectés réglables, cinq portées indépendantes, soleil plein à midi
|
||||
et correction du réalignement cyclique de la grille d'ombres.
|
||||
[Contrat et vérifications](docs/shader-beta129.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.129/Sanctuary-beta.129.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.129/Sanctuary-Test-beta.129.mrpack).
|
||||
|
||||
## beta.128 — Ombres stables, lune et feuillage
|
||||
|
||||
Grille de réception stabilisée, contours nets des blocs, atténuation à midi,
|
||||
ombre lunaire légère et feuillage nuancé selon son épaisseur.
|
||||
[Contrat et vérifications](docs/shader-beta128.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.128/Sanctuary-beta.128.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.128/Sanctuary-Test-beta.128.mrpack).
|
||||
|
||||
## beta.127 — Shader : ombres portées pixélisées
|
||||
|
||||
Ombres solaires natives, finesse 8/16/32 pixels par bloc et portée 16/32/64 blocs.
|
||||
Option désactivée par défaut, réglable sans rechargement des ressources.
|
||||
[Contrat et vérifications](docs/shader-beta127.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.127/Sanctuary-beta.127.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.127/Sanctuary-Test-beta.127.mrpack).
|
||||
|
||||
## beta.126 — Gemmes, argile et découvertes
|
||||
|
||||
Rubis et saphir avec filons configurables, couleurs originales avec l'argile,
|
||||
commandes de découverte globales ou ciblées.
|
||||
[Contrat et vérifications](docs/gems-beta126.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.126/Sanctuary-beta.126.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.126/Sanctuary-Test-beta.126.mrpack).
|
||||
|
||||
## beta.125 — Sculptures : placement, éclats et animaux
|
||||
|
||||
Coins et trois hauteurs sur la grille 1/16 ; particules issues des voxels ;
|
||||
modèles découverts disponibles dans le Clay Workshop.
|
||||
[Contrat et vérifications](docs/sculpture-placement-beta125.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.125/Sanctuary-beta.125.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.125/Sanctuary-Test-beta.125.mrpack).
|
||||
|
||||
## beta.124 — Une boîte par sculpture
|
||||
|
||||
Sélection et collisions simplifiées, dimensions ajustées et lumière conservée.
|
||||
[Contrat et vérifications](docs/sculpture-bounds-beta124.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.124/Sanctuary-beta.124.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.124/Sanctuary-Test-beta.124.mrpack).
|
||||
|
||||
## beta.123 — Clé dorée et clé de Steve
|
||||
|
||||
Orientations cohérentes, inventaires et eau préservés ; clé de Steve pour
|
||||
l’édition libre, utilisable aussi en survie. [Contrat et vérifications](docs/wrench-safety-beta123.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-beta.123.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-Test-beta.123.mrpack).
|
||||
|
||||
## beta.122 — Sculptures : forme et lumière
|
||||
|
||||
Collisions et sélection sur les voxels réels, creux traversables, lumière sans
|
||||
occlusion cubique. [Contrat et vérifications](docs/sculpture-shape-light-beta122.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-beta.122.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Test-beta.122.mrpack).
|
||||
|
||||
## beta.121 — Texture de clé et pack personnel
|
||||
|
||||
Copie complète des ressources, création depuis le jeu et guide FR/EN inclus.
|
||||
[Contrat et vérifications](docs/resourcepack-template-beta121.md) ·
|
||||
[Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
|
||||
|
||||
## beta.120 — Clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation de textures et assemblage des machines conservé.
|
||||
[Contrat et vérifications](docs/golden-wrench-beta120.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.120/Sanctuary-beta.120.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.120/Sanctuary-Test-beta.120.mrpack).
|
||||
|
||||
## beta.119 — Sculptures au centre ou au bord
|
||||
|
||||
Pose selon le point visé, centrage sur la grille entière et ancrage conservé
|
||||
en sauvegarde. [Contrat et vérifications](docs/sculpture-center-beta119.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.119/Sanctuary-beta.119.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.119/Sanctuary-Test-beta.119.mrpack).
|
||||
|
||||
## beta.118 — Murets en briques colorées
|
||||
|
||||
Seize couleurs, fabrication et tailleur de pierre, eau, raccords et collisions
|
||||
natifs. [Contrat et vérifications](docs/colored-brick-walls-beta118.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.118/Sanctuary-beta.118.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.118/Sanctuary-Test-beta.118.mrpack).
|
||||
|
||||
## beta.117 — Grain des textures et sculptures en chapeau
|
||||
|
||||
Les petits cubes d'une sculpture reprennent plusieurs nuances des textures
|
||||
de la matière. Le modèle reste intact dans le slot chapeau et au retrait.
|
||||
[Contrat et vérifications](docs/sculpture-texture-grain-beta117.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.117/Sanctuary-beta.117.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.117/Sanctuary-Test-beta.117.mrpack).
|
||||
|
||||
## beta.116 — Matières de sculpture et Fourneau
|
||||
|
||||
Un bloc de matière devient une sculpture aux couleurs de sa texture. Hotbar,
|
||||
orientation et ancrage conservés. Le Fourneau accepte les dépôts dans chacune
|
||||
de ses 27 cases et explique sa chauffe commune.
|
||||
[Contrat et vérifications](docs/sculpture-materials-beta116.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.116/Sanctuary-beta.116.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.116/Sanctuary-Test-beta.116.mrpack).
|
||||
|
||||
## beta.115 — Fourneau : dépôts et chauffe
|
||||
|
||||
Correctif du décalage par trois cases, combustible stable et progression
|
||||
par cuisson. [Diagnostic et essais](docs/fourneau-transfers-beta115.md).
|
||||
Étape locale intégrée à la livraison commune beta.116.
|
||||
|
||||
## beta.114 — Animaux portés et fuite en vol
|
||||
|
||||
Un animal volant porté entraîne son porteur après un coup. La trajectoire
|
||||
aléatoire prend brièvement le dessus sur les commandes ; déposer l’animal
|
||||
interrompt le vol. [Contrat et vérifications](docs/carried-flight-panic-beta114.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.114/Sanctuary-beta.114.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.114/Sanctuary-Test-beta.114.mrpack).
|
||||
|
||||
## beta.113 — Sculptures d’argile et alignement
|
||||
|
||||
Pose contre le bord visé, orientation selon le regard et coordonnées entières
|
||||
pour chaque voxel, y compris les modèles de largeur impaire. L’accroche est
|
||||
conservée à la reconnexion. [Contrat et vérifications](docs/clay-sculpture-grid-beta113.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.113/Sanctuary-beta.113.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.113/Sanctuary-Test-beta.113.mrpack).
|
||||
|
||||
## beta.112 — Atelier d’argile et hotbar
|
||||
|
||||
Neuf cases de hotbar et deux emplacements de fabrication, dans un cadre fixe.
|
||||
Les transferts restent dans la hotbar ; une barre pleine ne consomme pas d’argile.
|
||||
[Contrat et vérifications](docs/clay-workshop-hotbar-beta112.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.112/Sanctuary-beta.112.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.112/Sanctuary-Test-beta.112.mrpack).
|
||||
|
||||
## beta.111 — Atelier d’argile et pose orientée
|
||||
|
||||
Cadre nine-slice, aperçu, fabrication et inventaire dans un menu commun.
|
||||
Les statuaires s’orientent selon le regard horizontal à chaque pose ; les
|
||||
anciens modèles gardent leur apparence. [Contrat et vérifications](docs/clay-workshop-ui-beta111.md).
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.111/Sanctuary-beta.111.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.111/Sanctuary-Test-beta.111.mrpack) ·
|
||||
[Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.111).
|
||||
|
||||
## beta.110 — Atelier d’argile et neige saisonnière réunis
|
||||
|
||||
Le Clay Workshop / atelier d’argile, les statuaires et les modèles 3D rejoignent
|
||||
les œufs, l’accumulation et la fonte saisonnière dans un seul pack.
|
||||
[Contrat et vérifications](docs/clay-workshop-integration-beta110.md).
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.110/Sanctuary-beta.110.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.110/Sanctuary-Test-beta.110.mrpack) ·
|
||||
[Release et installation Prism](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110).
|
||||
|
||||
## beta.109 — Fonte saisonnière
|
||||
|
||||
La neige météo fond progressivement hors hiver, avec un rythme adapté à la
|
||||
saison. Les constructions en neige sont préservées ; accumulation plafonnée
|
||||
à deux blocs par défaut. [Contrat et essais](docs/seasonal-snow-melt-beta109.md).
|
||||
Base beta.108, Statuaire beta.106 livré séparément.
|
||||
[Pack normal](build/Sanctuary-beta.109.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.109.mrpack).
|
||||
|
||||
## beta.108 — Neige en volume
|
||||
|
||||
Huit couches deviennent un bloc de neige solide ; les dépôts se poursuivent
|
||||
au-dessus. Hauteur par défaut de deux blocs, réglable par gamerule.
|
||||
[Contrat et contrôles](docs/snow-accumulation-beta108.md).
|
||||
Base beta.107 conservée, avec ses œufs ; Statuaire beta.106 livré séparément.
|
||||
[Pack normal](build/Sanctuary-beta.108.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.108.mrpack).
|
||||
|
||||
## beta.107 — Œufs de reproduction et de générateurs
|
||||
|
||||
Les naissances deviennent des œufs transportables, avec bébé et héritage natif.
|
||||
Les générateurs classiques donnent leur œuf lorsqu’ils sont cassés sans Toucher
|
||||
de soie. [Détails](docs/spawn-eggs-acquisition-beta107.md) ·
|
||||
[Catalogue des 88 espèces à valider](docs/spawn-eggs-catalogue-beta107.md).
|
||||
Assemblage isolé sur la base beta.105, pendant le chantier Statuaire beta.106.
|
||||
[Pack normal](build/Sanctuary-beta.107.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.107.mrpack).
|
||||
|
||||
## beta.106 — Atelier d’argile et modèles 3D
|
||||
|
||||
Un même dossier pour schémas et fichiers GLB. Le Métabli les transforme en
|
||||
grandes statues en blocs ; l’atelier d’argile fabrique des statuaires 16³
|
||||
transportables et posables. Argile normale : couleurs d’origine ; argile
|
||||
colorée : sculpture unie. [Contrat et validation](docs/statuary-beta106.md).
|
||||
[Pack normal](build/Sanctuary-beta.106.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.106.mrpack).
|
||||
|
||||
## beta.105 — Saisons et chapeaux vivants
|
||||
|
||||
Molette dans le fût, panoramas avec le shader Sanctuary, prévisions saisonnières
|
||||
et neige au sol. Les mobs équipés utilisent leurs objets, attirent les animaux
|
||||
et écrivent leur journal. [Détails](docs/living-hats-seasons-beta105.md).
|
||||
[Pack normal](build/Sanctuary-beta.105.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.105.mrpack).
|
||||
|
||||
## beta.104 — Argiles grise et noire
|
||||
|
||||
Teintes plus distinctes et contraste renforcé, sur les blocs et leurs items.
|
||||
Le rangement par séries de 16 est conservé. [Détails](docs/clay-contrast-beta104.md) ·
|
||||
[Pack normal](build/Sanctuary-beta.104.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.104.mrpack).
|
||||
|
||||
## beta.103 — Rangement par séries de 16
|
||||
|
||||
Briques, escaliers, dalles et argiles regroupés par type, dans l’ordre des
|
||||
couleurs natif. [Détails](docs/colored-bricks-order-beta103.md) ·
|
||||
[Pack normal](build/Sanctuary-beta.103.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.103.mrpack).
|
||||
|
||||
## beta.102 — Briques et argile de couleur
|
||||
|
||||
Seize couleurs : briques, dalles, escaliers, argile pastel et leurs items.
|
||||
Blocs dans l’onglet natif des couleurs ; recettes et textures 16 × 16.
|
||||
[Détails](docs/colored-bricks-beta102.md) ·
|
||||
[Pack normal](build/Sanctuary-beta.102.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.102.mrpack).
|
||||
|
||||
## beta.101 — Atterrissage des familiers volants
|
||||
|
||||
Les familiers volants ne prennent plus de coup de chute au contact du sol.
|
||||
Les vrais dégâts de combat restent actifs.
|
||||
[Validation](docs/flying-landing-beta101.md) ·
|
||||
[Pack normal](build/Sanctuary-beta.101.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.101.mrpack).
|
||||
|
||||
## beta.100 — Exploration et familiers
|
||||
|
||||
Les statues se débloquent par rencontre ou combat. Les familiers se promènent
|
||||
avec des pauses selon leur tempérament ; les volants ne tournent plus en orbite
|
||||
permanente. [Détails et validation](docs/exploration-familiers-beta100.md).
|
||||
|
||||
[Pack normal](build/Sanctuary-beta.100.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.100.mrpack).
|
||||
|
||||
## beta.099 — Catalogue du Métabli et Progression
|
||||
|
||||
Compétences en haut, arbre des progrès directement dessous. Le Métabli propose
|
||||
un catalogue à aperçus, les trois derniers plans terminés et une palette étendue
|
||||
pour les statues. K bref place/tourne, K maintenu ouvre les commandes.
|
||||
Les nouvelles grandes salles souterraines accueillent un Métabli sur leur piédestal.
|
||||
[Détails et vérifications](docs/catalogue-progression-beta099.md).
|
||||
[Pack normal](build/Sanctuary-beta.099.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.099.mrpack).
|
||||
|
||||
## beta.098 — Construire le plan en créatif
|
||||
|
||||
Après sélection au Métabli, K propose **Construire le plan…** uniquement en
|
||||
créatif. Statues, bâtiments et composants de machines sont posés selon
|
||||
l’origine et l’orientation choisies, après confirmation.
|
||||
[Détails et vérifications](docs/creative-construction-beta098.md).
|
||||
[Pack normal](build/Sanctuary-beta.098.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.098.mrpack).
|
||||
|
||||
## beta.097 — Temples et aperçu texturé
|
||||
|
||||
Corrige le chargement de certaines graines sans emplacement naturel de temple :
|
||||
recherche élargie, puis présence forcée des pièces vanilla avec fondation locale
|
||||
si nécessaire. Les aperçus de construction utilisent les modèles et textures
|
||||
des blocs du pack actif et indiquent le matériau visé.
|
||||
[Détails et vérifications](docs/temple-crash-beta097.md).
|
||||
[Pack normal](build/Sanctuary-beta.097.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.097.mrpack).
|
||||
|
||||
## beta.096 — Métabli et construction guidée
|
||||
|
||||
Neuf établis à plat, assemblés à la clé, ouvrent le catalogue de statues,
|
||||
machines, bâtiments et plans personnels. Un choix ferme le catalogue ; K
|
||||
permet ensuite de placer et d’orienter l’aperçu. Changer ou abandonner le projet
|
||||
se fait au Métabli. Construction manuelle avec ses matériaux.
|
||||
[Détails et vérifications](docs/metabli-beta096.md).
|
||||
[Pack normal](build/Sanctuary-beta.096.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.096.mrpack).
|
||||
|
||||
## beta.095 — Texture du pilier des super pistons
|
||||
|
||||
La tige centrale utilise uniquement le bois au centre de la grande plaque,
|
||||
y compris ses segments courts dans le socle et la tête.
|
||||
[Détails et vérifications](docs/piston-shaft-beta095.md).
|
||||
[Pack normal](build/Sanctuary-beta.095.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.095.mrpack).
|
||||
|
||||
## beta.094 — Fourneau allumé
|
||||
|
||||
Les deux foyers brillent pendant la chauffe ; flammes et fumée sortent des
|
||||
ouvertures de la façade. La pierre d’origine est conservée.
|
||||
[Détails et vérifications](docs/fourneau-allume-beta094.md).
|
||||
[Pack normal](build/Sanctuary-beta.094.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.094.mrpack).
|
||||
|
||||
## beta.093 — Crash de recherche créative
|
||||
|
||||
Les descriptions de familiers peuvent être indexées en arrière-plan sans accéder
|
||||
à la police graphique. Leur mise en page reste conservée à l’affichage.
|
||||
[Diagnostic et vérifications](docs/tooltip-crash-beta093.md).
|
||||
[Pack normal](build/Sanctuary-beta.093.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.093.mrpack).
|
||||
|
||||
## beta.092 — Clic molette des super pistons
|
||||
|
||||
Viser un socle, une tête ou une tige sélectionne le piston normal ou collant
|
||||
@@ -1896,7 +2330,7 @@ Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembr
|
||||
| Fabric API | 0.160.5+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | beta.092 |
|
||||
| Sanctuary / pack | beta.122 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -1925,7 +2359,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.092.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.122.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -117,3 +117,22 @@ commit `62513191f6a3497447595c2215d466ad1d2bdb92`, sous Unlicense.
|
||||
Le comportement des contours et leurs coordonnées d’atlas suivent cette source ;
|
||||
l’intégration, la synchronisation et le calcul de capacité sont propres à Sanctuary.
|
||||
Texte de licence : [Unlicense amont](https://github.com/squeek502/AppleSkin/blob/62513191f6a3497447595c2215d466ad1d2bdb92/LICENSE).
|
||||
|
||||
## Briques et argile beta.102
|
||||
|
||||
Les seize textures de blocs de briques ont été fournies par le créateur de
|
||||
Sanctuary. Les variantes d’argile, de boule d’argile et de brique sont des
|
||||
recolorations des textures Minecraft 26.3 de Mojang/Microsoft. Les ressources
|
||||
de modèles, butin et états reprennent leurs équivalents natifs 26.3.
|
||||
`tools/generate-colored-bricks.py` documente cette transformation ;
|
||||
`tools/colored-bricks-palette.json` conserve couleurs et empreintes des PNG fournis.
|
||||
Ces ressources dérivées ne sont pas présentées comme des créations originales.
|
||||
|
||||
## Clé dorée beta.121
|
||||
|
||||
Le PNG 16 × 16 `golden_wrench.png` a été fourni par le créateur de Sanctuary
|
||||
le 17 septembre 2026. Il est conservé octet pour octet dans le mod, le pack
|
||||
intégré et le template personnel (SHA-256
|
||||
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`).
|
||||
Le template regroupe les ressources client déjà distribuées par Sanctuary ;
|
||||
leurs crédits et conditions respectives restent applicables.
|
||||
|
||||
+19
-1
@@ -15,7 +15,7 @@ tasks.register('verifyPack', Exec) {
|
||||
tasks.register('assemblePack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Assemble a local packwiz pack including the built Sanctuary mod.'
|
||||
dependsOn(':sanctuary:build', 'verifyPack', 'assembleResourcePack')
|
||||
dependsOn(':sanctuary:build', 'verifyPack', 'assembleResourcePack', 'assembleResourceTemplate')
|
||||
commandLine('python3', 'scripts/pack.py', 'assemble')
|
||||
}
|
||||
|
||||
@@ -46,3 +46,21 @@ tasks.register('assembleTestPack', Exec) {
|
||||
dependsOn('assemblePack', ':sanctuary-test:build')
|
||||
commandLine('python3', 'scripts/test_pack.py')
|
||||
}
|
||||
|
||||
tasks.register('assembleResourceTemplate', Zip) {
|
||||
group = 'distribution'
|
||||
description = 'Export all Sanctuary client assets as an editable personal resource pack.'
|
||||
dependsOn(':sanctuary:processResources', 'verifyResourcePack')
|
||||
def resources = project(':sanctuary').layout.buildDirectory.dir('resources/main')
|
||||
// Same precedence as the native exporter: built-in textures override mod defaults.
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from(resources.map { it.dir('resourcepacks/textures/assets') }) { into 'assets' }
|
||||
from(resources.map { it.dir('assets') }) { into 'assets' }
|
||||
from(resources.map { it.dir('resourcepacks/template') })
|
||||
from(resources.map { it.file('resourcepacks/textures/pack.png') })
|
||||
exclude '**/.DS_Store'
|
||||
destinationDirectory = layout.buildDirectory
|
||||
archiveFileName = "Sanctuary-Template-${mod_version}.zip"
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
+135
@@ -1,5 +1,44 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## STAT-03 — Atelier d’argile et hotbar — beta.112
|
||||
|
||||
Branche `codex/clay-workshop-hotbar-beta112`. [Contrat et vérifications](clay-workshop-hotbar-beta112.md).
|
||||
Onze cases : argile, résultat et hotbar active ; hauteur fixe et transferts
|
||||
limités aux neuf cases, même avec un inventaire agrandi.
|
||||
|
||||
## STAT-02 — Atelier d’argile et orientation — beta.111
|
||||
|
||||
Branche `codex/clay-workshop-ui-beta111`. [Contrat et vérifications](clay-workshop-ui-beta111.md).
|
||||
Cadre nine-slice fourni, inventaire intégré et pose selon le regard horizontal.
|
||||
Propriété `facing` additive ; les anciens statuaires gardent leur apparence.
|
||||
|
||||
## STAT-01 — Atelier d’argile et modèles 3D — beta.106
|
||||
|
||||
Branche `codex/statuary-import-beta106`. [Contrat et validation](statuary-beta106.md).
|
||||
Dossier commun, GLB au Métabli pour les statues en blocs, atelier d’argile
|
||||
pour les miniatures 16³ avec consommation native, teinte et identité du modèle.
|
||||
Cycle natif de fabrication/pose/récupération/rechargement vérifié, y compris
|
||||
sans fichier source. Build complet et archives normale/Test vérifiés localement.
|
||||
|
||||
## BUILD-104 — Contraste des argiles
|
||||
|
||||
Branche `codex/clay-contrast-beta104`. [Ticket](clay-contrast-beta104.md).
|
||||
Gris et noir plus distincts ; blocs et items accordés. Aperçu, contraste,
|
||||
transparence, build complet et archives normal/Test vérifiés.
|
||||
|
||||
## BUILD-103 — Rangement créatif par séries
|
||||
|
||||
Branche `codex/colored-bricks-order-beta103`. [Ticket](colored-bricks-order-beta103.md).
|
||||
Groupes de seize par type et ordre de couleurs natif. Build complet et
|
||||
archives normal/Test vérifiés ; ressources beta.102 inchangées.
|
||||
|
||||
## BUILD-102 — Briques et argile de couleur
|
||||
|
||||
Branche `codex/colored-bricks-beta102`. [Ticket](colored-bricks-beta102.md).
|
||||
Seize familles natives et textures recolorées à partir des originaux vanilla.
|
||||
Parcours natif, recettes, butin, modèles, sauvegarde/réouverture, build complet
|
||||
et archives normal/Test vérifiés.
|
||||
|
||||
## SKY-077 — tourbillon au-dessus du vide
|
||||
|
||||
Branche `codex/cloud-vortex-beta077`. [Ticket](cloud-vortex-beta077.md).
|
||||
@@ -40,6 +79,18 @@ Capture native des 88 espèces, parcours creeper créatif/survie FR/EN,
|
||||
interopérabilité Litemapy, sept tests serveur et 130 tâches Gradle réussis.
|
||||
Archives normal/Test vérifiées ; anciennes `.schematic` à convertir en externe.
|
||||
|
||||
## BUILD-02 — Métabli et menu de construction
|
||||
|
||||
**Premier lot implémenté en beta.096**, branche `codex/metabli-beta096`.
|
||||
[Contrat et vérifications](metabli-beta096.md) ·
|
||||
[Conception et extension future du catalogue](metabli-construction.md).
|
||||
Neuf établis à plat ouvrent le catalogue ; choix d’un projet à l’atelier,
|
||||
placement et orientation avec K. Statues, quatre machines existantes, abri,
|
||||
passerelle et imports ; construction manuelle guidée, sans bouton pause ajouté.
|
||||
Changer/importer/abandonner exige le Métabli. Projet local à la session actuelle,
|
||||
exportable ; chantiers partagés persistants, catalogue serveur en datapack et
|
||||
fonctions commerciales restent à réaliser.
|
||||
|
||||
## MB-01 — Reprendre le catalogue des multiblocs
|
||||
|
||||
**Ticket local ouvert le 15 septembre 2026 ; premier lot implémenté en beta.084.**
|
||||
@@ -1726,3 +1777,87 @@ Voir [la génération](generation-alpha30.5.md) et [l’atlas](terrain-atlas.md)
|
||||
Livré et vérifié : trois modes de combat, inscriptions sans plafond pour les arènes,
|
||||
paris en objets, annonces et liste publique. Joueurs avec morts réelles.
|
||||
[Contrat et essais](arenas-beta073.md).
|
||||
|
||||
## EXP-03 — Crash de recherche de temple
|
||||
|
||||
Correctif local beta.097 sur `codex/expedition-temple-crash-beta097`.
|
||||
[Contrat et vérifications](temple-crash-beta097.md). Graine du rapport
|
||||
`-4700804240597771092` ; le créateur demande de garantir le temple plutôt que
|
||||
d’omettre le bâtiment ou de refuser la graine. Les plans de construction gagnent
|
||||
aussi leur aperçu texturé à sa demande pendant ce correctif.
|
||||
|
||||
## CONSTRUCTION-098 — construction directe en créatif
|
||||
|
||||
Branche `codex/creative-construction-beta098`.
|
||||
[Ticket](creative-construction-beta098.md) : bouton K créatif, confirmation et
|
||||
placement serveur du projet choisi au Métabli ; parcours manuel en survie.
|
||||
Essai client natif (statue, machine, bâtiment, annulation et contrôles créatif),
|
||||
build complet et archives normal/Test vérifiés.
|
||||
|
||||
## CATALOGUE-099 — Progression et Métabli illustré
|
||||
|
||||
Branche `codex/catalogue-progression-beta099`.
|
||||
[Contrat](catalogue-progression-beta099.md) : arbre des progrès intégré sous
|
||||
les compétences, catalogue partagé entre catégories avec aperçus et trois
|
||||
plans personnels terminés, palette étendue, K bref/maintenu et Métabli dans
|
||||
les nouvelles salles souterraines. Anciennes pièces sauvegardées préservées.
|
||||
|
||||
## EXPLORATION-100 — Statues découvertes et promenade
|
||||
|
||||
Branche `codex/exploration-familiers-beta100`.
|
||||
[Contrat](exploration-familiers-beta100.md) : catalogue de statues basé sur les
|
||||
rencontres et statistiques serveur ; promenade avec pauses selon le tempérament,
|
||||
sans orbite permanente ni concurrence avec les ordres et le combat.
|
||||
|
||||
EXPLORATION-100 vérifié : douze promenades, régression autonomie/ordres, catalogue
|
||||
serveur, imports `.schem`, réouverture et deuxième monde ; build et archives
|
||||
normal/Test réussis. Incident Windows non reproduit sans son journal.
|
||||
|
||||
## LANDING-101 — Atterrissage des familiers volants
|
||||
|
||||
Branche `codex/flying-landing-beta101`.
|
||||
[Ticket](flying-landing-beta101.md) : supprimer le faux coup de chute des
|
||||
profils volants, conserver les attaques réelles et les chutes terrestres.
|
||||
|
||||
LANDING-101 vérifié : reproduction native avant correctif, douze espèces
|
||||
volantes, contacts lents/rapides, vraies attaques et chute terrestre ;
|
||||
build complet et archives normal/Test contrôlés.
|
||||
|
||||
## beta.105 — Saisons et chapeaux vivants
|
||||
|
||||
Livré localement et vérifié en copie isolée : [contrat et contrôles](living-hats-seasons-beta105.md).
|
||||
|
||||
## EGGS-107 — Acquisition des œufs
|
||||
|
||||
Branche `codex/eggs-beta107`. [Contrat](spawn-eggs-acquisition-beta107.md) :
|
||||
naissances en œufs et œuf du générateur sans Toucher de soie.
|
||||
[Catalogue](spawn-eggs-catalogue-beta107.md) : 88 espèces, 35 voies couvertes,
|
||||
53 propositions en attente de validation du créateur. Les rangs et Anomaly
|
||||
restent hors de ce ticket. Les sources sont intégrées en préservant le chantier
|
||||
Statuaire ; la validation binaire est isolée sur la dernière base livrée beta.105.
|
||||
|
||||
## SNOW-108 — Neige en volume
|
||||
|
||||
Branche `codex/snow-accumulation-beta108`.
|
||||
[Contrat](snow-accumulation-beta108.md) : épaississement par précipitations,
|
||||
blocs pleins à huit couches et accumulation verticale. Limite serveur de deux
|
||||
blocs par défaut, réglable. Les blocs sont natifs, sans migration de terrain.
|
||||
Livraison locale vérifiée : essais natifs dans deux mondes neufs, build complet
|
||||
et archives normal/Test contrôlés ; assemblage isolé sur beta.107.
|
||||
|
||||
## SNOW-109 — Fonte saisonnière
|
||||
|
||||
Branche `codex/seasonal-snow-melt-beta109`.
|
||||
[Contrat](seasonal-snow-melt-beta109.md) : fonte progressive par saison,
|
||||
provenance des dépôts persistante par chunk, constructions préservées.
|
||||
Livraison locale vérifiée sur beta.108 : essais natifs, rechargement de
|
||||
chunk, build complet et archives normal/Test contrôlés.
|
||||
|
||||
## INTEGRATE-110 — Atelier d’argile dans le pack complet
|
||||
|
||||
Branche `codex/clay-workshop-integration-beta110`.
|
||||
[Ticket](clay-workshop-integration-beta110.md) : assemblage commun de
|
||||
beta.106 et beta.109, avec tous les systèmes intermédiaires. Essais natifs de
|
||||
l’atelier, des œufs et de la neige, build et archives réussis. Publication
|
||||
et synchronisation de l’instance Sanctuary Beta terminées ; 923 fichiers
|
||||
personnels et réglages suivis conservés, aucun monde ouvert.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# CARRY-08 — Fuite des animaux volants portés — beta.114
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/carried-flight-panic-beta114`.
|
||||
|
||||
Un coup reçu par un animal volant porté sur la tête déclenche une fuite de
|
||||
40 à 60 ticks : il entraîne son porteur vers le haut, dans une direction
|
||||
aléatoire, avec des embardées. Le serveur choisit le départ et la trajectoire,
|
||||
transmis au client pour la physique native. Les touches de déplacement et le
|
||||
regard ne pilotent pas cette brève fuite. La caméra reste libre. Un nouveau coup peut relancer la fuite avec un autre
|
||||
cap après dix ticks, sans cumuler les vitesses.
|
||||
|
||||
Le porteur peut viser et frapper son animal volant. Les animaux ordinaires
|
||||
reçoivent les dégâts habituels ; le familier conserve son coup amical sans
|
||||
perte de santé. Les attaques extérieures acceptées déclenchent aussi la fuite.
|
||||
Les autres protections entre passagers et porteurs restent en place.
|
||||
Les espèces reprennent le groupe déjà utilisé pour planer, poule comprise.
|
||||
|
||||
La vitesse reste bornée, les collisions sont natives et aucun bloc n’est
|
||||
traversé ni déplacé. Déposer l’animal (Maj + clic droit), le perdre, mourir,
|
||||
se déconnecter ou changer de dimension arrête la propulsion. Immersion,
|
||||
mode spectateur, vol créatif, élytres et porteur lui-même passager l’inhibent.
|
||||
La fin de fuite rend le contrôle au joueur et conserve le planeur existant
|
||||
si l’animal reste porté. Les montures colossales gardent leur pilotage actuel.
|
||||
|
||||
L’état de fuite est temporaire, synchronisé et non sauvegardé. Aucun format
|
||||
persistant, identifiant existant ou monde personnel n’est modifié. Les nouveaux
|
||||
retours d’interface sont traduits FR/EN. Client et serveur utilisent beta.114.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`CarryPanic114ClientChecks` passe en **56 secondes** avec un serveur intégré
|
||||
Minecraft 26.3 et un monde plat jetable de graine 114 :
|
||||
|
||||
- Véritable ciblage du perroquet sur la tête et frappe avec la touche native,
|
||||
perte de santé de l’animal ordinaire, décollage et déplacement du joueur.
|
||||
- Trajectoire synchronisée et priorité sur les touches avant/droite/saut et
|
||||
le regard ; comparaison exacte avec la vitesse du tick de physique.
|
||||
- Expiration, suppression de l’état temporaire et retour au planeur normal.
|
||||
- 32 trajectoires tirées côté serveur couvrent les quatre quadrants, toutes
|
||||
dans les bornes de durée et de vitesse ; Maj + clic droit natif interrompt.
|
||||
- Sept espèces ordinaires : poule, chauve-souris, abeille, allay, perroquet,
|
||||
blaze et breeze. Une vache portée reste protégée et ne déclenche aucun vol.
|
||||
- Attaque extérieure, mort, activation du vol créatif et détachement arrêtent
|
||||
ou déclenchent la réaction selon le contrat.
|
||||
- Plafond bas : aucune traversée ; le passager bloqué est déposé par les
|
||||
contrôles de place existants et la propulsion s’interrompt.
|
||||
- Véritable coup amical sur un familier perroquet : lancement sans perte de
|
||||
santé, puis arrêt immédiat lors du détachement.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryCarryPanic114ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `CARRY114_NATIVE_FLIGHT_PASS` et `CARRY114_PASS` dans
|
||||
`build/carry-panic114-client.log`. Capture en jeu relue dans
|
||||
`build/carry-panic114-evidence/`. Le GameTest dédié reste exclu conformément
|
||||
au refus antérieur de son EULA. Pas de test Windows, de réseau distant ou de
|
||||
latence simulée ; aucun monde personnel n’a été ouvert.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 22 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. Les classes et ressources hors de ce ticket restent
|
||||
identiques à beta.113, notamment les sculptures d’argile. Les deux packs
|
||||
contiennent le même JAR, sans monde, GLB ni classes de test.
|
||||
|
||||
- Sanctuary-beta.114.mrpack : 10179428 octets, SHA-256
|
||||
`92aac61c2c00a2206f3f5ac3d170ab058f6765526cf5a2a64c5336261db3f5bf`.
|
||||
- Sanctuary-Test-beta.114.mrpack : 10198350 octets, SHA-256
|
||||
`5e8ec1f18e34ca51e09e2738f2b04f9a1bbac5d14c7a7c2d766218a4cff6021f`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`18105b76f63008210c62d5728f999c7abc083a861ecc1de2c7ee87262dde5a40`.
|
||||
|
||||
Reçu local : `build/carry-panic114-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.114](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.114)
|
||||
est publiée depuis `21845bd7367a260ee6a3d189417a8620aa5bf738`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `c26f3bb59491c9bb4bbdd27f59571ef84f417f7b`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.114 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.114/` dans l’instance existante. Reçus locaux :
|
||||
`build/carry-panic114-isolated.json` et `build/carry-panic114-prism.json`.
|
||||
@@ -0,0 +1,127 @@
|
||||
# beta.099 — Progression, catalogue du Métabli et gestes K
|
||||
|
||||
Contrat de réalisation, 16 septembre 2026. Branche `codex/catalogue-progression-beta099`.
|
||||
|
||||
- Compétences en haut de la colonne gauche, arbre des progrès Minecraft intégré
|
||||
dessous ; aptitudes à droite. Prestige et historique sous l’arbre.
|
||||
- Catalogue visuel natif : trois derniers plans terminés en accueil, statues,
|
||||
machines, bâtiments, infrastructures, décoration et bibliothèque. Même format
|
||||
de fiches (aperçu, nom, dimensions, description et sélection).
|
||||
- Historique personnel local de trois plans terminés, sans autorité sur le monde :
|
||||
aucun choix de plan n’est compté comme construction et chaque placement reste
|
||||
contrôlé par le serveur. Le stockage est distinct des sauvegardes Minecraft.
|
||||
- Palette de statue étendue aux blocs ayant un objet utilisable, échantillonnée
|
||||
depuis le pack actif. Pas de blocs techniques sans objet ni de commandes.
|
||||
- Clé dorée : modèle plat d’outil Minecraft. Texture provisoire vanilla, à
|
||||
remplacer par la texture que le créateur fournira ; aucune texture dessinée
|
||||
à sa place.
|
||||
- K bref ancre au bloc visé ; rappuyer sur le même point tourne de 90 degrés.
|
||||
K maintenu ouvre le cockpit. Viser le vide ne repositionne pas le projet.
|
||||
|
||||
## Contrat de génération et de compatibilité
|
||||
|
||||
Le module EXPANSION_HALL conserve sa géométrie, ses appuis, son piédestal et son
|
||||
identifiant. Les nouvelles pièces natives de la salle indépendante portent un
|
||||
champ optionnel `SanctuaryMetatable099=true` : neuf parties de Métabli assemblées
|
||||
sont posées au centre, un bloc au-dessus du piédestal. Une pièce enregistrée sans
|
||||
ce champ conserve son comportement ancien (aucun ajout), même si un de ses chunks
|
||||
n’était pas encore décoré. Le champ ne modifie ni le journal ni la graine ni les
|
||||
identifiants de génération. Aucune régénération ou écriture dans un monde
|
||||
personnel existant. Les nouvelles salles générées après cette version reçoivent
|
||||
l’exemple ; les salles déjà enregistrées ne sont pas modifiées.
|
||||
|
||||
Une fois généré, le Métabli suit les règles normales : le casser le désassemble,
|
||||
et le reconstruire exige la clé dorée. Pas de réparation automatique au chargement.
|
||||
Vérification prévue sur nouvelles sauvegardes de développement et round-trip NBT
|
||||
avec et sans le champ optionnel, génération en plusieurs ordres de chunks.
|
||||
|
||||
## Validation
|
||||
|
||||
Deux essais natifs sur Minecraft **26.3 finale**, nouveaux mondes de développement :
|
||||
|
||||
- `Catalogue099ClientChecks` : **réussi en 1 min 33 s**. Génération et construction
|
||||
d’une statue de Creeper de 32 blocs (1 542 cellules) avec la palette étendue ;
|
||||
construction des plans Fourneau et Abri, confirmation/annulation, retrait du
|
||||
créatif pendant la confirmation et refus d’un envoi falsifié en survie. Pose
|
||||
manuelle via le contrôleur Minecraft consommant exactement un bloc. Trois plans
|
||||
terminés mémorisés, conservés au changement de monde, écrits sur disque et
|
||||
décodés. Catalogue physique, catégories, aperçus, import/export, abandon et
|
||||
destruction du Métabli ; K bref/rotation/visée vide/maintien. Arbre des progrès
|
||||
sous les compétences, clic droit passant par l’écran et activant réellement le
|
||||
suivi. Interfaces FR/EN, échelles 2 et 3, petite fenêtre et fenêtre 1280 × 800.
|
||||
- `Temple097ClientChecks`, étendu à la salle : **réussi en 4 min 48 s**, graine
|
||||
**-4700804240597771092**, génération racine `sanctuary:island_v24`. Origine des
|
||||
neuf parties du Métabli : **(-56, 169, -117)**, sur le piédestal de pierre lisse.
|
||||
Les neuf états `part=0..8` sont présents après génération puis après sauvegarde
|
||||
et réouverture du monde de développement. Aller-retour NBT du nouveau champ ;
|
||||
ancien tag sans champ restant sans champ. Régression des deux temples natifs,
|
||||
coffres, pièges et cache de planification réussie.
|
||||
|
||||
Les captures d’interface vérifiées sont dans `build/catalog099-evidence/`.
|
||||
Journaux : `build/catalog099-client.log` et `build/catalog099-hall.log`.
|
||||
La capture de la salle prise immédiatement après téléportation précédait le
|
||||
rendu des chunks et n’est pas utilisée comme preuve visuelle ; les assertions
|
||||
sur les blocs, appuis et sauvegardes constituent la validation de sa génération.
|
||||
|
||||
Les premiers passages ont détecté deux problèmes corrigés : les petits blocs
|
||||
(fleurs) favorisés par une comparaison de couleur seule, et la protection native
|
||||
des vestiges refusant l’ajout du Métabli hors de leur contexte de génération.
|
||||
Des clics automatisés immédiatement après redimensionnement donnaient aussi un
|
||||
rayon de caméra périmé : les interactions physiques répétées du test passent
|
||||
maintenant par le contrôleur client Minecraft et son protocole normal, avec
|
||||
contrôle serveur de la consommation et de l’ouverture. Les gestes K et le suivi
|
||||
des progrès restent testés par les événements d’entrée natifs.
|
||||
|
||||
`check build assemblePack assembleTestPack` : **réussi en 3 min 7 s**, 124 tâches.
|
||||
Le GameTest sur serveur dédié est exclu ; les tests client natifs ci-dessus
|
||||
utilisent uniquement les mondes de développement.
|
||||
|
||||
Les deux exports packwiz sont vérifiés : version, sources Java, contenu du JAR,
|
||||
absence des classes de test et conservation des archives beta.098. Reçu complet :
|
||||
`build/beta099-artifact.json`.
|
||||
|
||||
| Archive | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `Sanctuary-beta.099.mrpack` | 9 908 941 | `9722e2410e18bba34cf5db835facec4bb29fc3bd7f071942e7c8321a60d955ed` |
|
||||
| `Sanctuary-Test-beta.099.mrpack` | 9 927 866 | `7ce02718c2e4c780197f9984b1ac7bc7a443f78eaa3e7d8ea248aa6b2e75745a` |
|
||||
|
||||
Aucune publication du canal ni installation personnelle effectuée.
|
||||
|
||||
## Détails du catalogue
|
||||
|
||||
Les vignettes de plans utilisent les quads, UV et textures du pack actif.
|
||||
Les blocs rendus par un moteur spécial sans quads (certains conteneurs) ont une
|
||||
silhouette issue de leur forme. Les vignettes sont mises en cache (48 au maximum),
|
||||
invalidées lors d’un rechargement du pack. Les échantillons sont bornés à 64 × 64
|
||||
par texture et prennent la première image des textures animées.
|
||||
Les statues du catalogue affichent le modèle natif adulte ; le choix ouvre les
|
||||
réglages de hauteur et de palette avant génération. Sur les petites fenêtres,
|
||||
la description et les dimensions passent dans l’infobulle pour garder le choix
|
||||
visible sous la vignette.
|
||||
|
||||
La palette « Tous les blocs » parcourt les blocs enregistrés ayant un objet,
|
||||
sans inclure les blocs techniques sans objet ou les blocs de commande. La
|
||||
comparaison de couleur utilise Oklab ; un coût de forme favorise les volumes
|
||||
pleins, et les blocs soumis à la gravité sont moins favorisés. Les feuilles
|
||||
utilisent leur état persistant, comme après une pose par un joueur. Les palettes
|
||||
mixte, laine et béton restent disponibles.
|
||||
|
||||
L’historique `schematics/sanctuary/recent-completed.nbt` contient au maximum
|
||||
trois plans distincts, écrits atomiquement hors du fil de rendu. Il enregistre
|
||||
une transition d’incomplet à complet observée dans le monde, en créatif ou en
|
||||
survie. Sélectionner un plan, l’apercevoir ou l’envoyer au serveur ne suffit pas.
|
||||
Il s’agit d’un historique personnel de cette installation, pas d’un catalogue
|
||||
partagé par le serveur. Aucun inventaire ou entité n’est enregistré.
|
||||
|
||||
K bref utilise un rayon de 96 blocs sur les blocs du monde. La case adjacente à
|
||||
la face visée sert de centre d’ancrage horizontal. Une seconde pression sur ce
|
||||
même point tourne autour de cet ancrage ; déplacer le regard vers un autre
|
||||
point déplace le plan. Le maintien de 10 ticks ouvre les commandes sans exécuter
|
||||
le geste bref à la relâche. Les raccourcis remappés utilisent la même logique.
|
||||
|
||||
## Texture de la clé
|
||||
|
||||
Le modèle est `minecraft:item/handheld` ; la texture provisoire est une copie
|
||||
de la pioche dorée vanilla. Le créateur pourra remplacer uniquement
|
||||
`mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/golden_wrench.png`
|
||||
par son propre PNG. Aucune illustration personnalisée n’a été créée à sa place.
|
||||
@@ -0,0 +1,40 @@
|
||||
# beta.104 — Contraste des argiles grise et noire
|
||||
|
||||
Branche `codex/clay-contrast-beta104`, Minecraft 26.3.
|
||||
|
||||
Le gris devient plus soutenu et le noir plus sombre, pour distinguer clairement
|
||||
les trois argiles gris clair/gris/noir. Les nuances du dessin vanilla sont
|
||||
renforcées : facteur 1,35 au lieu de 0,85. Le mélange conserve davantage de la
|
||||
couleur des briques fournies (80 % pour le gris, 90 % pour le noir).
|
||||
|
||||
Les textures de bloc et de boule d’argile sont accordées, toujours en 16 × 16
|
||||
avec les silhouettes et transparences vanilla. La recoloration exacte par
|
||||
script reste la méthode choisie par le créateur. Les quatorze autres couleurs
|
||||
et toutes les briques restent identiques. Le rangement beta.103 est conservé.
|
||||
|
||||
[Aperçu avant/après](../build/clay-contrast-beta104-preview.png).
|
||||
Le générateur écrit désormais son aperçu au nom de la version courante,
|
||||
pour préserver les aperçus historiques.
|
||||
|
||||
## Validation et livraison
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussi
|
||||
en 2 min 22 s (124 tâches). Comparaison visuelle avant/après effectuée.
|
||||
Pas de nouvelle session Minecraft pour cette modification de textures.
|
||||
|
||||
La vérification des archives confirme exactement quatre textures modifiées :
|
||||
blocs et boules d’argile gris/noir. Toutes les classes Java, recettes, modèles,
|
||||
autres assets et anciennes archives beta.103 sont inchangés.
|
||||
Les images conservent leur taille 16 × 16 et leur transparence pixel par pixel.
|
||||
Le contraste mesuré augmente de plus de 40 % sur chacune des quatre textures.
|
||||
Luminance moyenne des blocs : gris clair 168,4 ; gris 118,0 ; noir 68,4.
|
||||
Sources, métadonnées et contenu des packs vérifiés ; `git diff --check` réussi.
|
||||
Reçu : `build/beta104-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.104.mrpack), SHA-256 :
|
||||
`83f087cce41316eed620e9764ff0b643c3d5af40fa5b4cebdff5720740708631`.
|
||||
- [Pack de test](../build/Sanctuary-Test-beta.104.mrpack), SHA-256 :
|
||||
`adbaad161550ab80dcbc88c27d760e2f735eef9a69ba3f9e2e69bbd25e895147`.
|
||||
|
||||
Les GameTests dédiés restent exclus. Aucun monde, canal ou installation
|
||||
personnelle modifié ; archives locales uniquement.
|
||||
@@ -0,0 +1,90 @@
|
||||
# STAT-04 — Sculptures d’argile et accroche au bord — beta.113
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-sculpture-grid-beta113`.
|
||||
|
||||
Les miniatures de l’atelier deviennent des « sculptures d’argile » / « Clay
|
||||
sculptures ». Le nom du modèle reste dans leur infobulle. Les statues en blocs
|
||||
du Métabli restent une catégorie distincte.
|
||||
|
||||
À la pose, le bord horizontal le plus proche du point visé accueille la
|
||||
sculpture. L’autre axe est centré au pixel entier le plus proche, sur la grille
|
||||
de 1/16 de bloc. Sur une face latérale, elle touche le bord du bloc support.
|
||||
À égalité exacte entre deux bords, l’axe Z départage ; au centre exact,
|
||||
le bord côté joueur est retenu. L’orientation suit toujours le regard.
|
||||
Le bouton Tourner prépare le modèle avant fabrication. Les marges horizontales vides du modèle ne créent pas de décalage au bord.
|
||||
Tous les sommets restent
|
||||
sur la grille entière, même avec des dimensions impaires et après rotation.
|
||||
|
||||
## Compatibilité préalable
|
||||
|
||||
La propriété native additive `anchor` du bloc `sanctuary:statuary` mémorise
|
||||
`center`, `north`, `south`, `east` ou `west`. Un ancien état sans cette propriété
|
||||
prend `center` : il reste centré, avec la correction visuelle du demi-voxel.
|
||||
Le schéma 1, les voxels, noms de modèles et identifiants ne changent pas.
|
||||
Il n’y a aucun parcours ni réécriture de chunks existants. La sauvegarde native
|
||||
conserve l’accroche ; les rotations et miroirs de structures la transforment.
|
||||
Casser et reposer choisit une nouvelle accroche sans modifier le modèle.
|
||||
Les nouveaux objets emploient le nom traduit de leur type ; les noms déjà
|
||||
enregistrés et les noms personnalisés restent conservés. La collision cubique
|
||||
et la limite d’une sculpture par bloc restent inchangées. Client et serveur
|
||||
doivent utiliser ensemble beta.113.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Statuary106ClientChecks`, étendu par `Sculpture113ClientChecks`,
|
||||
passe en **1 min 25 s**, sur Minecraft 26.3 et un monde plat jetable de graine 106 :
|
||||
|
||||
- 5 120 combinaisons de largeur/profondeur, orientation et accroche : contrôle
|
||||
de la géométrie émise, grille exacte de 1/16 et absence de dépassement du bloc.
|
||||
- Modèles avec marges transparentes, repères colorés pour vérifier le sens du
|
||||
rendu et égalité de la géométrie centrée entre l’objet et le bloc.
|
||||
- Vrais clics de pose sur les quatre bords : orientation, bord côté serveur,
|
||||
réception côté client, butin/repose et conservation après reconnexion.
|
||||
- Contextes natifs de pose sur les faces latérales et coordonnées négatives.
|
||||
- Rotation/miroir de chaque combinaison d’accroche et d’orientation ; lecture
|
||||
des anciens états sans `anchor`, avec et sans `facing`.
|
||||
- Noms français/anglais des objets fabriqués, menu aux échelles 2 et 3,
|
||||
fabrication payée, Maj-clic limité à la hotbar et régression Métabli.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `STATUARY113_PASS` et `SCULPTURE113_GEOMETRY_PASS` dans
|
||||
`build/clay-grid113-client.log`. Captures relues dans
|
||||
`build/clay-grid113-evidence/`. Le GameTest dédié reste exclu conformément au
|
||||
refus antérieur de son EULA ; ces essais utilisent le serveur intégré macOS.
|
||||
Pas de validation Windows ni depuis deux ordinateurs. Aucun monde personnel
|
||||
n’a été ouvert.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 11 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. Les classes et ressources hors du ticket sont identiques
|
||||
à beta.112. Les deux packs contiennent le même JAR, sans monde, GLB ni test.
|
||||
|
||||
- Sanctuary-beta.113.mrpack : 10173807 octets, SHA-256
|
||||
`ed082ff29a0e9a70fd6786bc024241b276cb516bc09fb5151d6cb324604429bb`.
|
||||
- Sanctuary-Test-beta.113.mrpack : 10192733 octets, SHA-256
|
||||
`387f54dc77884a9c9bccdcc1904778ca7f0b2fbda126754fdc49b10a492e4602`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`e242d9fd2edeede922a72a10e988e29c2b49dea52105cfc9fc8573154849ead3`.
|
||||
|
||||
Reçu local : `build/clay-grid113-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.113](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.113)
|
||||
est publiée depuis `6166d0aa9eba3c3dd84b8b9f1501e3346231ac82`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `e4c16d517bfda4f2b688604cc61945a46a4513a9`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.113 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.113/` dans l’instance existante. Reçus locaux :
|
||||
`build/clay-grid113-isolated.json` et `build/clay-grid113-prism.json`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# STAT-03 — Atelier d’argile limité à la hotbar — beta.112
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-workshop-hotbar-beta112`.
|
||||
|
||||
Le menu utilise uniquement les neuf cases de la hotbar active du joueur,
|
||||
avec les deux emplacements de fabrication. Les rangées de réserve, y compris
|
||||
les extensions Sanctuary, ne sont ni ajoutées au menu ni utilisées par ses
|
||||
transferts. La rangée choisie avant l’ouverture reste la hotbar courante.
|
||||
La touche Tab retrouve la navigation des boutons dans ce menu.
|
||||
|
||||
Le cadre nine-slice tient dans une hauteur fixe de 202 pixels d’interface.
|
||||
Il ne grandit plus avec la capacité de l’inventaire. Le Maj-clic du résultat
|
||||
cherche uniquement une place dans la hotbar ; sans place, il ne consomme pas
|
||||
d’argile. Les touches 1–9 restent utilisables, pas l’échange avec la seconde
|
||||
main. À la fermeture, la restitution native des objets restants est conservée.
|
||||
|
||||
Les identifiants, sauvegardes, données de sculpture et orientations beta.111
|
||||
restent inchangés. Aucun monde existant n’est ouvert ou réécrit. Client et
|
||||
serveur doivent utiliser ensemble beta.112 pour les nouveaux indices du menu
|
||||
temporaire (11 cases au total).
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Statuary106ClientChecks`, étendu à beta.112, passe en **1 min 13 s**
|
||||
sur Minecraft 26.3 avec serveur intégré et monde plat jetable de graine 106 :
|
||||
|
||||
- Menu FR/EN aux échelles 2 et 3 : hauteur fixe, neuf cases de hotbar native,
|
||||
deux cases de fabrication et absence de panneau d’inventaire évolutif.
|
||||
- Fabrication par Maj-clic depuis la hotbar, avec de l’argile aussi présente
|
||||
dans la réserve, l’extension et la seconde main : ces stocks restent intacts.
|
||||
- Rejet des indices de cases absents et de l’échange avec la seconde main.
|
||||
- Hotbar remplie de neuf piles : Maj-clic sans fabrication ni consommation.
|
||||
- Une seule place dans une pile de 63 statuaires : production d’un seul objet,
|
||||
coût d’une argile, aucune statue transférée dans une case cachée.
|
||||
- Restitution de l’argile restante à la fermeture, seize teintes, quatre
|
||||
orientations, butin/repose et sauvegarde/reconnexion sans fichier GLB.
|
||||
- Régression du Métabli et annulation d’import conservées.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueur `STATUARY112_PASS` dans `build/clay-hotbar112-client.log` ; captures
|
||||
relues dans `build/clay-hotbar112-evidence/`. Le GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. Pas de test Windows ni depuis
|
||||
deux ordinateurs ; aucun monde personnel n’a été ouvert.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 6 s**, 125 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. Les classes et ressources hors de ce ticket sont identiques
|
||||
à beta.111, notamment les données et le rendu orienté des statuaires.
|
||||
Les deux packs contiennent le même JAR, sans monde, GLB ni classes de tests.
|
||||
|
||||
- Pack normal : 10 169 337 octets, SHA-256
|
||||
`55723a5d7cb99e350ad8fe150e92e0567430b5261ca4685abd900be34068658f`.
|
||||
- Pack Test : 10 188 262 octets, SHA-256
|
||||
`987818bf817ad80fd055b97e84bf4775fca54820d7c3055d6fd4089dc7a5a3c0`.
|
||||
- JAR : SHA-256
|
||||
`9007a770bbe69db5846f93140d910514f10839cc34ec7c6315a9381d8850adaa`.
|
||||
|
||||
Reçu local : `build/clay-hotbar112-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.112](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.112)
|
||||
est publiée depuis `1eaa976ecf328c6c20afe5e4eb67c48118f87f95`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `573c5b0d84fcb3e9fec7cc8a83981cf35eaa1ead`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.112 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.112/` dans l’instance existante. Reçus locaux :
|
||||
`build/clay-hotbar112-isolated.json` et `build/clay-hotbar112-prism.json`.
|
||||
@@ -0,0 +1,128 @@
|
||||
# INTEGRATE-110 — Atelier d'argile dans la version complète
|
||||
|
||||
Demande du 17 septembre 2026 : inclure le Clay Workshop dans la version avec
|
||||
la fonte saisonnière. Branche `codex/clay-workshop-integration-beta110`.
|
||||
État : version commune vérifiée, publiée et synchronisée dans Sanctuary Beta.
|
||||
|
||||
## Périmètre et compatibilité
|
||||
|
||||
La beta.110 réunit le Clay Workshop / atelier d'argile, le Statuaire et l'import
|
||||
GLB de beta.106 avec les œufs de beta.107, la neige en volume de beta.108 et
|
||||
la fonte saisonnière de beta.109. Aucune fonction retirée de ces livraisons.
|
||||
Le Métabli conserve son import de modèles 3D en plans de construction.
|
||||
|
||||
Les sources du dossier principal correspondent déjà à cette union : les
|
||||
30 fichiers de production du Statuaire sont ceux de la livraison beta.106 ;
|
||||
les traductions FR/EN et listes de mixins sont la réunion exacte des deux bases,
|
||||
sans conflit de valeur ni doublon. Les autres sources restent celles de beta.109.
|
||||
Les fichiers Finder `.DS_Store` sont exclus par la configuration Gradle existante.
|
||||
|
||||
Les identifiants et contrats de données restent ceux des tickets existants :
|
||||
[Statuaire](statuary-beta106.md), [œufs](spawn-eggs-acquisition-beta107.md),
|
||||
[accumulation](snow-accumulation-beta108.md),
|
||||
[fonte](seasonal-snow-melt-beta109.md). Aucun format changé, aucune migration,
|
||||
aucune génération ou expansion activée. Les tests créent des mondes de
|
||||
laboratoire et rouvrent seulement leurs propres sauvegardes jetables.
|
||||
|
||||
Les compteurs `mod_version`, `pack_version` et le manifeste packwiz passent
|
||||
ensemble à beta.110. Les archives beta.106 à beta.109 restent immuables.
|
||||
Le créateur a ensuite demandé de tout mettre à jour : publication de la
|
||||
version vérifiée sur le canal stable puis synchronisation de l’instance
|
||||
Sanctuary Beta existante, après sauvegarde des fichiers gérés.
|
||||
|
||||
## Validation exécutée
|
||||
|
||||
Réutilisation des essais natifs de l'atelier (fabrication, rendu, Métabli et
|
||||
sauvegarde/rechargement), des œufs (reproduction, éclosion et générateurs), et
|
||||
de la neige (accumulation et fonte selon les saisons, provenance et rechargement)
|
||||
sur les mêmes sources réunies. Puis `check build assemblePack assembleTestPack`.
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA ;
|
||||
les tests natifs tournent avec le serveur intégré.
|
||||
|
||||
## Contrat de mise à jour de l'instance existante
|
||||
|
||||
Cible unique : `Sanctuary-0.1.0-alpha.1`, nom affiché **Sanctuary Beta**, déjà
|
||||
reliée au canal `sanctuary-beta/packwiz`. Les instances historiques 26.2 et les
|
||||
instances importées séparément restent hors de cette synchronisation.
|
||||
|
||||
Minecraft 26.3-pre-2 passe à 26.3 finale, Fabric Loader reste en 0.19.5,
|
||||
Fabric API passe de 0.160.0+26.3 à 0.160.5+26.3 ; Java 25 est déjà configuré.
|
||||
L'installateur packwiz est chargé de remplacer les deux JAR suivis et d'ajuster
|
||||
les composants de lancement. Sauvegarde préalable des JAR gérés, du suivi
|
||||
packwiz, de `instance.cfg` et de `mmc-pack.json` hors de `mods/`.
|
||||
|
||||
Aucun monde personnel ne sera ouvert, converti, régénéré ou exploré pendant
|
||||
l'opération. Les fichiers de sauvegarde, réglages, captures et packs personnels
|
||||
sont comparés par empreintes avant/après. Seuls les composants gérés du pack et
|
||||
la version du lanceur changent. Les anciennes règles de génération enregistrées
|
||||
dans les mondes restent intactes ; aucune promesse de migration automatique de
|
||||
sauvegarde entre versions Minecraft n'est déduite de la mise à jour des fichiers.
|
||||
Le canal est publié avec un artefact immuable et testé deux fois dans une
|
||||
installation isolée avant les deux synchronisations de l'instance existante.
|
||||
|
||||
## Résultats de la version commune
|
||||
|
||||
Tous les parcours sont exécutés sur les mêmes sources beta.110, sous Java 25,
|
||||
Minecraft 26.3 et Fabric API 0.160.5+26.3 :
|
||||
|
||||
- Atelier d'argile : **1 min 6 s**, `STATUARY106_PASS` et import Khronos indépendant.
|
||||
Fabrication payante, 16 argiles, placement, butin, Métabli et rechargement sans
|
||||
le fichier source. Captures relues dans `build/integration110-evidence/`.
|
||||
- Œufs : **29 s**, `EGGS107_NATIVE_PASS` ; reproduction, variantes, œufs utilisés
|
||||
et distribués, générateurs et monde sans règles Sanctuary.
|
||||
- Neige : **42 s**, `SNOW108_NATIVE_PASS` et `MELT109_NATIVE_PASS` ; limite de
|
||||
deux blocs, saisons, protection des constructions et sauvegarde/rechargement.
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
|
||||
**2 min 20 s**, **125 tâches**, réussite.
|
||||
|
||||
Le JAR est comparé à l'union exacte des classes et ressources des deux livraisons
|
||||
immuables beta.106 et beta.109. Les traductions et mixins réunissent tous les
|
||||
éléments, sans doublon. Les sources JAR correspondent aux fichiers Java locaux.
|
||||
Aucun GLB de test, classe de test, monde ou fichier Finder embarqué.
|
||||
|
||||
[Pack normal](../build/Sanctuary-beta.110.mrpack) ·
|
||||
[Pack Test](../build/Sanctuary-Test-beta.110.mrpack).
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| Normal | `12a7b7279e831bf5d3489d033b875437ad9e8ba1e8cbecc6107a890d312e3029` |
|
||||
| Test | `ffed5e4fcd32ca32ac96186da8e987905cd7fc4de792afe61aca9ad064895e0b` |
|
||||
| JAR Sanctuary | `7cb121a6e208cdd184d80ebe61f8bc52b6703b1c1d7611eeeb700aa88d158b46` |
|
||||
|
||||
Reçu : `build/integration110-artifact.json`, manifeste des sources :
|
||||
`build/integration110-source-manifest.json`. Journaux :
|
||||
`build/integration110-{statuary,eggs,snow,check-build}.log`.
|
||||
Les essais natifs sont macOS avec serveur intégré ; pas de test Windows ni
|
||||
connexion depuis un second ordinateur. La neige antérieure non suivie garde
|
||||
la limite de fonte documentée dans beta.109.
|
||||
|
||||
Le script de publication a été corrigé pour autoriser uniquement `icon.png`
|
||||
en plus des manifestes TOML, après contrôle de l'index et comparaison à l'icône
|
||||
source. Les JAR restent exclusivement des pièces jointes de release. Cette
|
||||
correction de distribution ne change aucun binaire ni le tag source beta.110.
|
||||
|
||||
## Publication et installation terminées
|
||||
|
||||
[Release beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110),
|
||||
tag source `468743bfe75087ecb0160b85a0864fa81bcdded5`. Les sources cumulatives
|
||||
depuis beta.092 sont enregistrées ; les anciens tags et artefacts sont conservés.
|
||||
Les corrections du script de distribution et ce reçu complètent la branche
|
||||
source sans déplacer ce tag ni remplacer les binaires.
|
||||
|
||||
Canal packwiz : `828353a6e2bd747f4f473ae9fb9e965d38032419`. Chaque manifeste et
|
||||
l'icône publiés correspondent aux fichiers validés localement. JAR, archives
|
||||
normale/Test et ZIP d'amorçage Prism sont disponibles dans la release.
|
||||
|
||||
Deux synchronisations de l'installateur packwiz dans un dossier neuf, puis deux
|
||||
dans la même instance **Sanctuary Beta**, ont réussi. Le second passage ne
|
||||
change aucun fichier géré. Un seul JAR Sanctuary beta.110 est actif, Fabric API
|
||||
0.160.5+26.3 est installé, Minecraft est réglé sur 26.3 et Loader sur 0.19.5.
|
||||
L'icône déclarée dans le pack est ajoutée. `instance.cfg` et les **923 fichiers
|
||||
personnels et réglages suivis** conservent leurs empreintes. Aucun monde ouvert,
|
||||
converti ou régénéré ; aucune autre instance ni serveur personnel modifié.
|
||||
|
||||
Sauvegarde ciblée dans l'instance : `sanctuary-backups/before-beta.110/`.
|
||||
Reçus ignorés : `build/integration110-publication.json`,
|
||||
`build/integration110-isolated.json`, `build/integration110-prism.json`.
|
||||
Les journaux de chaque synchronisation et les listes d'empreintes avant/après
|
||||
sont conservés dans le dossier de développement et dans la sauvegarde ciblée.
|
||||
@@ -0,0 +1,99 @@
|
||||
# STAT-02 — Atelier d’argile et orientation — beta.111
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-workshop-ui-beta111`.
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
Le menu utilise le cadre `task_frame_unobtained.png` fourni par le créateur,
|
||||
sans retouche, comme sprite Sanctuary avec les métadonnées nine-slice natives
|
||||
de Minecraft. Les coins restent à leur taille d’origine. Catalogue, aperçu,
|
||||
fabrication, explications et inventaire évolutif tiennent dans ce cadre.
|
||||
La texture reste remplaçable par un pack de ressources.
|
||||
|
||||
La pose d’un statuaire suit le regard horizontal du joueur, dans les quatre
|
||||
directions. Le bouton Tourner règle toujours l’orientation du modèle importé.
|
||||
Le placement ajoute ensuite l’orientation du bloc sans réécrire ses voxels.
|
||||
|
||||
## Contrat de compatibilité préalable
|
||||
|
||||
Ajout de la propriété native `facing` au bloc `sanctuary:statuary`, avec `south`
|
||||
par défaut : cette orientation correspond au rendu historique, sans rotation.
|
||||
Les anciens états sans propriété prennent cette valeur à la lecture et gardent
|
||||
leur apparence. Aucun parcours ni réécriture des anciens chunks n’est effectué.
|
||||
Le schéma 1 de la sculpture, ses voxels, ses identifiants et les données des
|
||||
objets restent identiques. Casser puis reposer utilise le nouveau regard,
|
||||
sans cumuler les orientations précédentes. Rotation et miroir de structures
|
||||
transforment l’état du bloc. La collision cubique reste celle déjà livrée.
|
||||
|
||||
## Texture
|
||||
|
||||
Le fichier fourni, 26 × 26 pixels, est conservé octet pour octet sous
|
||||
`assets/sanctuary/textures/gui/sprites/container/clay_workshop/frame.png`.
|
||||
Le fichier adjacent `.png.mcmeta` utilise le moteur `nine_slice` de Minecraft
|
||||
avec une bordure fixe de quatre pixels et un centre extensible. L’identifiant
|
||||
du sprite est `sanctuary:container/clay_workshop/frame` ; aucun sprite global
|
||||
Minecraft n’est remplacé. Les emplacements utilisent `minecraft:container/slot`.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`Statuary106ClientChecks`, étendu aux cas beta.111, passe en **1 min 3 s** sur
|
||||
Minecraft 26.3, avec serveur intégré et monde plat jetable de graine 106 :
|
||||
|
||||
- Fabrication payée, restitutions, Maj-clic et teintes des seize argiles.
|
||||
- Menu FR/EN aux échelles 2 et 3, cases et actions contenues dans le cadre.
|
||||
- Six rangées d’inventaire à l’échelle 3 : affichage borné et défilement.
|
||||
- Vrais clics de pose aux quatre points cardinaux : état côté serveur,
|
||||
transmission au client et rotation du rendu alignés sur le regard.
|
||||
- Rotation et miroir de structures ; lecture d’un ancien état sans `facing`.
|
||||
- Butin d’une statue orientée au nord puis repose vers l’ouest : nouvelle
|
||||
orientation, mêmes voxels. Sauvegarde/réouverture et transmission sans GLB.
|
||||
- Conversion au Métabli et annulation d’import conservées.
|
||||
|
||||
Commande :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `STATUARY106_PASS`, `STATUARY106_INTEROP_PASS` et `STATUARY111_PASS`
|
||||
dans `build/clay-ui111-client.log`. Captures dans `build/clay-ui111-evidence/`,
|
||||
dont les menus français et anglais relus visuellement.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Les essais tournent sur macOS avec serveur intégré ; pas de validation depuis
|
||||
deux ordinateurs ni sur Windows. Aucun monde personnel n’est ouvert.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 33 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux fichiers Java du dépôt. Le contenu du mod hors classes et ressources de
|
||||
ce ticket est identique à beta.110 ; œufs, neige et fonte restent inclus.
|
||||
Les deux MRpack contiennent le même JAR vérifié, sans monde, GLB ni classe de test.
|
||||
|
||||
- Pack normal : `build/Sanctuary-beta.111.mrpack`, 10 169 372 octets,
|
||||
SHA-256 `f83098f80ac2edb8e32877bf8d5a362cd75f2fa9020593c2c7e4f9abe8b3e9f6`.
|
||||
- Pack Test : `build/Sanctuary-Test-beta.111.mrpack`, 10 188 298 octets,
|
||||
SHA-256 `1234b10eb4685d99aa7421de46bc54fd114b791f1b4d63803a09afc926d2c9d9`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`b3e967db7b336b970e4121981e3ef8140fe90f0ecc5850ce90a70caaf3772684`.
|
||||
|
||||
Reçu local : `build/clay-ui111-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.111](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.111)
|
||||
est publiée depuis le commit source `289697becf19db4acf50d224b1dd3d753eff1153`.
|
||||
Le tag exact et les artefacts sont immuables ; leurs téléchargements publics
|
||||
ont été vérifiés. Le canal packwiz avance au commit
|
||||
`87c9f81761309316c601ed32fa922c1a5893952c`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans l’instance existante
|
||||
**Sanctuary Beta** ont réussi. Un seul JAR Sanctuary beta.111 est actif ; le
|
||||
second passage conserve les mêmes hashes. Les **923 fichiers personnels et
|
||||
réglages suivis** restent identiques. Aucun monde personnel n’a été ouvert.
|
||||
La sauvegarde ciblée est dans `sanctuary-backups/before-beta.111/` de cette
|
||||
instance. Reçus : `build/clay-ui111-isolated.json` et
|
||||
`build/clay-ui111-prism.json`.
|
||||
@@ -249,6 +249,15 @@ Identifiant proposé : `sanctuary:clay_and_bricks`. **10 recettes primaires.**
|
||||
|
||||
**Précision :** Le vase décoré appartient à Archéologie ; la terre cuite à C11.
|
||||
|
||||
**Extension Sanctuary beta.102 :** seize couleurs de briques, dalles, escaliers,
|
||||
argile pastel et leurs deux items, soit 192 recettes supplémentaires. Elles
|
||||
sont déclarées dans `scripts/data/collections-sanctuary.json`, sans changer
|
||||
le recensement vanilla ci-dessus.
|
||||
|
||||
**Extension Sanctuary beta.106 :** atelier d’argile et statuaires importés. La
|
||||
recette de l’atelier rejoint C06 ; la fabrication d’une miniature utilise son
|
||||
menu et un bloc d’argile, avec le modèle choisi localement.
|
||||
|
||||
[Recettes et inventaire exacts de C06](collections-minecraft-inventaire.md#c06).
|
||||
|
||||
[Définition serveur beta.035](../mods/sanctuary/src/main/resources/data/sanctuary/sanctuary_recipe_collections/clay_and_bricks.json).
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# beta.118 — Murets en briques de couleur
|
||||
|
||||
Ticket sur `codex/colored-brick-walls-beta118`, Minecraft 26.3.
|
||||
|
||||
Les seize familles de briques reçoivent un muret natif `WallBlock`, sous
|
||||
l'identifiant `sanctuary:<couleur>_brick_wall`. Chaque muret réutilise la texture
|
||||
de briques fournie de sa couleur, sans nouvelle image ni recoloration.
|
||||
|
||||
Les murets forment une série de seize dans **Blocs colorés**, après les dalles
|
||||
et avant les argiles, dans l'ordre des couleurs vanilla retenu en beta.103.
|
||||
Les libellés sont traduits FR/EN.
|
||||
|
||||
Recettes : six briques de même couleur, en deux rangées de trois, donnent six
|
||||
murets ; le tailleur de pierre donne un muret par bloc de briques. Les deux
|
||||
recettes par couleur rejoignent la collection Argile et briques et leurs
|
||||
déblocages. Totaux des ajouts colorés : 80 blocs, 112 objets dont ces blocs,
|
||||
224 recettes. Les recettes et identifiants préexistants sont conservés.
|
||||
|
||||
Piliers, raccords entre couleurs et aux murets vanilla, côtés hauts/bas,
|
||||
collisions et immersion dans l'eau viennent du jeu. Tags natifs `walls`
|
||||
pour blocs et objets, pioche et butin de la bonne couleur. États, modèles
|
||||
multipart et butins sont repris des ressources exactes Minecraft 26.3.
|
||||
|
||||
La génération reste reproductible dans `tools/generate-colored-bricks.py` ;
|
||||
elle conserve les ajouts externes à ses briques, notamment la recette du
|
||||
Clay Workshop et les membres Sculpture/Atelier. Le compilateur des collections
|
||||
reste leur seul générateur. Aucun ajout au
|
||||
terrain, changement de format ou migration de sauvegarde. Les dépendances
|
||||
et le pack de textures intégré beta.090 restent inchangés.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le scénario natif `Bricks102ClientChecks`, complété par `Walls118Checks`,
|
||||
passe en **38 secondes** sur un nouveau monde plat de graine 102 avec serveur
|
||||
intégré Minecraft 26.3 :
|
||||
|
||||
- Seize murets réellement posés, dont une seconde série dans l'eau.
|
||||
- Recette de six murets et tailleur de pierre pour chaque couleur ; les
|
||||
anciennes recettes de briques, dalles, escaliers et argiles passent aussi.
|
||||
- Raccords entre couleurs et au muret vanilla, retrait des piliers intermédiaires,
|
||||
côtés relevés sous un bloc plein, collisions identiques au muret vanilla.
|
||||
- Eau conservée, pioche, butin de la bonne couleur, collection et recettes
|
||||
découvertes, recette du Clay Workshop toujours découverte, série créative
|
||||
de seize dans l'ordre natif.
|
||||
- États et modèles de tous les blocs de la famille vérifiés, sans texture
|
||||
manquante ; sauvegarde et reconnexion conservent les murets et leur eau.
|
||||
|
||||
Log : `build/walls118-client.log`, marqueur `WALLS118_PASS`. Capture de la
|
||||
galerie relue dans `build/walls118-evidence/`. Aucun essai Windows ou LAN ;
|
||||
GameTest dédié exclu selon le refus antérieur de son EULA. Aucun monde personnel
|
||||
ouvert. La régénération finale conserve à l'identique 2 024 fichiers de
|
||||
ressources et métadonnées.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en **2 min 5 s**, 125 tâches dont 101 exécutées. Sources et JAR concordants,
|
||||
intégrité ZIP, versions, modèles natifs, recettes et empreintes vérifiés.
|
||||
La comparaison avec beta.117 limite les changements de classes à
|
||||
`ColoredBricks` et ses classes internes ; les sculptures et chapeaux restent
|
||||
identiques. Les textures existantes sont toutes conservées octet pour octet.
|
||||
|
||||
- `Sanctuary-beta.118.mrpack` : 10216437 octets, SHA-256
|
||||
`75bb04aad981644888690aaeda9a293a94c5d32f6439ca24613365a41aba1b6b`.
|
||||
- `Sanctuary-Test-beta.118.mrpack` : 10235359 octets, SHA-256
|
||||
`f7f0a177b8472f0e8c34ad99a1ccef5d61396014017d260e934e910c4e6eb00f`.
|
||||
- JAR Sanctuary :
|
||||
`fc6edf4046a8ace357d561ed942213a22c7a27164fa160cdb4316f72ec302575`.
|
||||
|
||||
Reçu : `build/walls118-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.118](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.118)
|
||||
est publiée : JAR, MRpack normal/Test et amorçage Prism, téléchargements publics
|
||||
et empreintes vérifiés. Le tag immuable désigne
|
||||
`0a81b3c1471c43d705eec4e31a8f6f1820049ca8`. Le canal packwiz avance au commit
|
||||
`e6bab433bea9528f67d39af0d6c7bcb6443e4817`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans l'unique instance **Sanctuary
|
||||
Beta** réussissent, la seconde passe laissant les fichiers gérés identiques.
|
||||
Un seul JAR Sanctuary beta.118 est actif. Les 923 fichiers personnels et
|
||||
réglages suivis sont inchangés ; aucun monde personnel n'a été ouvert.
|
||||
Copie préalable des fichiers remplacés dans `sanctuary-backups/before-beta.118/`.
|
||||
Reçus : `build/walls118-isolated.json`, `build/walls118-prism.json`.
|
||||
@@ -0,0 +1,73 @@
|
||||
# beta.102 — Briques et argile de couleur
|
||||
|
||||
Branche `codex/colored-bricks-beta102`, Minecraft 26.3.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les seize PNG de briques fournis sont présents. Ils sont intégrés sans altération.
|
||||
Par couleur : bloc de briques, dalle, escalier, bloc d’argile pastel, boule d’argile
|
||||
et brique (items). Les 64 blocs apparaissent dans l’onglet natif des blocs colorés,
|
||||
les 32 items dans les ingrédients. Les composants ont des identifiants stables
|
||||
`sanctuary:<couleur>_*` et des libellés français et anglais.
|
||||
|
||||
La recoloration exacte par script a été choisie explicitement par le créateur.
|
||||
Les silhouettes, la transparence et les nuances des textures vanilla 16 × 16
|
||||
sont conservées ; les couleurs proviennent des PNG fournis. Aucun dessin IA.
|
||||
|
||||
Recettes natives : teinture de l’argile et des briques, assemblage des blocs,
|
||||
cuisson de l’argile en briques, argile colorée en terre cuite vanilla de même
|
||||
couleur, fabrication des dalles/escaliers et tailleur de pierre. Les recettes
|
||||
rejoignent la collection existante argile/briques et les découvertes Sanctuary.
|
||||
Dalles doubles, escaliers connectés, orientation, eau, outils et butin utilisent
|
||||
les mécanismes natifs. Les blocs ne sont pas ajoutés à la génération du terrain.
|
||||
Aucune migration de sauvegarde et aucune modification d’un monde existant.
|
||||
|
||||
## Génération reproductible
|
||||
|
||||
`tools/generate-colored-bricks.py --minecraft-jar <minecraft-client.jar 26.3>`
|
||||
requiert Python avec Pillow. Les briques sources sont conservées dans les assets
|
||||
et les couleurs/empreintes sont consignées dans `tools/colored-bricks-palette.json`.
|
||||
Les extensions de collection sont compilées par `scripts/compile_collections.py`
|
||||
depuis `scripts/data/collections-sanctuary.json` ; le recensement vanilla reste intact.
|
||||
|
||||
## Validation native
|
||||
|
||||
`Bricks102ClientChecks` sur Minecraft 26.3, monde plat neuf avec serveur intégré :
|
||||
|
||||
- 16 familles, 64 blocs et 96 entrées d’items (dont 64 BlockItems).
|
||||
- 192 recettes chargées ; fabrications, teintures, cuissons et tailleur exécutés.
|
||||
- Butin des briques et escaliers ; dalle simple/double ; argile normale et Toucher de soie.
|
||||
- Tags pioche/pelle et états immergés des dalles/escaliers.
|
||||
- Onglets natifs : 64 blocs colorés, 32 ingrédients.
|
||||
- Tous les états des 64 blocs possèdent des modèles cuits sans texture manquante.
|
||||
- Découverte de l’argile : la collection ajoute les nouvelles recettes.
|
||||
- Sauvegarde/réouverture du monde neuf : blocs et état de dalle haute conservés.
|
||||
|
||||
Parcours final réussi en 41 s, `build/bricks102-client.log`.
|
||||
[Aperçu des textures](../build/colored-bricks-beta102-preview.png) ·
|
||||
[Capture Minecraft](../build/bricks102-game.png).
|
||||
|
||||
La vérification du rendu utilise le client natif local. Aucun test multijoueur
|
||||
distant n’est revendiqué, aucune sauvegarde personnelle ni installation modifiée.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussi
|
||||
en 2 min 19 s (124 tâches). Les GameTests du serveur dédié restent exclus ;
|
||||
le parcours client/serveur intégré décrit ci-dessus a été exécuté séparément.
|
||||
`git diff --check` et la régénération identique octet pour octet sont vérifiés.
|
||||
|
||||
Les archives, sources, 192 recettes, 192 progrès de recettes, 96 libellés FR/EN,
|
||||
les seize PNG fournis et les transparences vanilla ont été contrôlés.
|
||||
La comparaison avec beta.101 ne trouve aucune autre classe de jeu modifiée
|
||||
que l’initialisation Sanctuary et les trois nouvelles classes ColoredBricks.
|
||||
Les archives beta.101 conservent leurs empreintes.
|
||||
Reçu : `build/beta102-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.102.mrpack), 10 052 986 octets.
|
||||
SHA-256 : `f324db41e9992df0635d870a766ec9c486167d7fc2e1ed5080f05deaf824d3c4`.
|
||||
- [Pack de test](../build/Sanctuary-Test-beta.102.mrpack), 10 071 908 octets.
|
||||
SHA-256 : `bf6b4a207f6b952295c01d59e788eb742d7dbee18ff5e0ad1e61ba78078348fb`.
|
||||
|
||||
Livraison locale, sans publication de canal, tag ou déploiement Prism.
|
||||
@@ -0,0 +1,33 @@
|
||||
# beta.103 — Rangement des blocs colorés
|
||||
|
||||
Branche `codex/colored-bricks-order-beta103`, Minecraft 26.3.
|
||||
|
||||
Dans Blocs colorés, les ajouts sont regroupés par type : les 16 blocs de briques,
|
||||
puis les 16 escaliers, les 16 dalles, les 16 blocs d’argile. Dans Ingrédients :
|
||||
les 16 boules d’argile, puis les 16 briques.
|
||||
|
||||
Chaque série suit l’ordre de couleurs de l’onglet natif Minecraft 26.3,
|
||||
vérifié dans `CreativeModeTabs.bootstrap` : blanc, gris clair, gris, noir,
|
||||
marron, rouge, orange, jaune, vert clair, vert, cyan, bleu clair, bleu, violet,
|
||||
magenta, rose. L’ordre d’enregistrement des blocs/items reste stable.
|
||||
Les identifiants, recettes, textures et sauvegardes sont inchangés.
|
||||
|
||||
## Validation et livraison
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussi
|
||||
en 2 min 41 s, 124 tâches. `git diff --check` réussi. Pas de nouveau test
|
||||
ni de nouvelle session graphique pour ce changement limité au rangement.
|
||||
|
||||
Les sources et les deux archives ont été vérifiées. Les textures, recettes,
|
||||
modèles et autres données sont identiques à beta.102, dont les archives sont
|
||||
conservées. Seul le comportement de `ColoredBricks` change ; la classe interne
|
||||
Stairs conserve exactement son code exécutable (seuls les numéros de lignes
|
||||
de débogage changent). Reçu : `build/beta103-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.103.mrpack), SHA-256 :
|
||||
`5c0b475410c7586275a59a8b14ac1770648f8196983e62c85a98dcd0fa5a1207`.
|
||||
- [Pack de test](../build/Sanctuary-Test-beta.103.mrpack), SHA-256 :
|
||||
`222765733711a08861e25db94ae12a4a6483bc11dbfdcaf54930786c10511d0e`.
|
||||
|
||||
Les GameTests du serveur dédié restent exclus. Aucun monde personnel,
|
||||
aucune installation ni aucun canal de distribution modifié.
|
||||
@@ -0,0 +1,112 @@
|
||||
# LIGHT-140 — Lumières colorées portées et distance
|
||||
|
||||
Socle beta.139 `c05c338`, branche `codex/colored-dynamic-beta140`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.140.
|
||||
|
||||
## Contrat
|
||||
|
||||
Relier les couleurs aux vraies sources de lumière dynamique : mains, tête,
|
||||
cosmétiques, objets au sol et feu, selon le système existant. Renforcer la
|
||||
teinte et activer les lumières colorées par défaut à 30 %. Ajouter une distance
|
||||
d'affichage de 8, 16, 32 ou 64 blocs (16 par défaut), indépendante de la portée
|
||||
native de chaque source. Augmenter la distance coûte davantage en mémoire et
|
||||
calcul ; aucun chargement forcé de chunk. Les choix sauvegardés sont conservés.
|
||||
|
||||
Sources portées actualisées avec le système dynamique, sans reconstruire tout
|
||||
le champ statique à chaque mouvement. Aucun changement de monde ou de règle
|
||||
serveur. Vérifications natives OpenGL et Vulkan, puis construction et archives.
|
||||
|
||||
## Couleurs et fondus
|
||||
|
||||
Torches ordinaires : température native par défaut ; option « Torches chaudes »
|
||||
pour retrouver l'orangé. Glowstone jaune, lave jaune-rouge, redstone rouge saturé,
|
||||
cuivre vert (posé, en main ou sur la tête). Le cuivre est reconnu avant les
|
||||
familles génériques de torches et lanternes. Les palettes explicites restent
|
||||
indépendantes des packs ; le repli des autres matériaux suit leurs textures.
|
||||
|
||||
Le rendu s'atténue sur le dernier quart de la distance choisie. Le champ précédent
|
||||
reste disponible pendant un fondu de 300 ms vers le nouveau champ ; l'activation
|
||||
et les sources dynamiques ont aussi un fondu. Le changement de distance est
|
||||
interpolé. Les sources mobiles utilisent la même sélection bornée et les mêmes
|
||||
intensités que la lumière dynamique existante, avec interpolation visuelle des
|
||||
positions et couleurs. Elles héritent de sa propagation radiale, y compris de
|
||||
ses limites d'occultation ; les blocs posés conservent leur propagation avec murs.
|
||||
|
||||
Le volume varie réellement selon la distance (56, 72, 104 ou 168 blocs de côté,
|
||||
avec marge de propagation). Les grandes distances augmentent la mémoire et le
|
||||
délai de reconstruction ; la construction reste répartie entre les images.
|
||||
Deux champs coexistent brièvement pour le fondu. Seuls les chunks déjà chargés
|
||||
sont lus. La portée lumineuse native des torches n'est pas allongée.
|
||||
|
||||
## Frontières de saturation
|
||||
|
||||
Le stockage beta.139 tronquait les canaux faibles séparément en fin de portée :
|
||||
un bleu pouvait perdre son rouge avant son vert, puis devenir plus saturé sur
|
||||
une frontière visible. Le stockage logarithmique conserve maintenant une marge
|
||||
sous le niveau lumineux natif zéro. Le shader applique la coupure à l'énergie
|
||||
globale en conservant les rapports RGB. La réponse devient linéaire près du noir,
|
||||
sans amplification par racine quatrième des très faibles contributions.
|
||||
|
||||
La transition des champs interpole le résultat colorimétrique rendu, pas une
|
||||
énergie ensuite amplifiée. Les sources mobiles ont une atténuation de fondu
|
||||
compensée pour la même réponse. Le test contrôle les rapports rouge/bleu et
|
||||
vert/bleu d'une torche des âmes de 1 à 9 blocs, jusqu'au bord de sa portée.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Minecraft 26.3 / Java 25 sur Apple M1, mondes de test neufs de graine 122 :
|
||||
|
||||
- OpenGL 4.1 Metal : **2 min 40 s**, `build/colored140-opengl.log`.
|
||||
- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée :
|
||||
**2 min 26 s**, `build/colored140-vulkan.log`.
|
||||
|
||||
Les deux exécutions passent `COLORED139_PASS` et `COLORED140_PASS` :
|
||||
propagation avec murs, sources hors champ, lampes allumées/éteintes, retrait,
|
||||
rechargement des ressources, cache, déplacement/retour de caméra, OFF/zéro,
|
||||
préférences et traductions FR/EN. Les nouveaux contrôles couvrent la teinte
|
||||
bleue jusqu'à neuf blocs, la torche native inchangée, l'option chaude, le cuivre
|
||||
vert et le rouge saturé, les deux mains et le cosmétique de tête, la position
|
||||
en troisième personne, les quatre distances et un fondu sans dépassement.
|
||||
|
||||
Dans la même scène contrôlée, la somme d'écarts OFF/torche chaude à 30 % vaut
|
||||
3 926 093 / 3 926 092 sur OpenGL/Vulkan, contre 2 025 577 en beta.139 à 35 %.
|
||||
Cette mesure vérifie l'accentuation demandée, pas la performance. Le test fige
|
||||
uniquement le scintillement de la lightmap native ; cette sonde et les tests
|
||||
client ne sont pas distribués. Les captures finales sont conservées sous
|
||||
`build/colored140-opengl-screenshots/` et `build/colored140-vulkan-screenshots/`.
|
||||
Comparatif autonome : `build/Shader-beta.140-comparaison.html`.
|
||||
|
||||
Pas de validation des pilotes Windows de l'utilisateur revendiquée. Les
|
||||
limites de beta.139 pour le rendu sous l'eau/lave et les panoramas restent
|
||||
ouvertes. Les sources dynamiques gardent l'occultation du système existant.
|
||||
Aucun monde personnel ouvert ni modifié.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus EULA antérieur ; les tests clients ci-dessus sont exécutés
|
||||
séparément. Log : `build/shader140-build.log`.
|
||||
|
||||
La comparaison avec beta.139 trouve **18 entrées de production modifiées**,
|
||||
limitées aux lumières dynamiques/colorées, options et aides FR/EN. Les sources
|
||||
des JAR correspondent aux fichiers du dépôt. Les archives normale/Test contiennent
|
||||
le même JAR Sanctuary ; la version, les dépendances exactes, les textures de
|
||||
gemmes et de la clé de Steve, l'absence de classes de test et le template complet
|
||||
sont vérifiés. Reçu : `build/shader140-artifact.json`.
|
||||
|
||||
SHA-256 du JAR : `853284c1a61c7fce12edd2f7763717321eef56977290eb0e7c5db5e30bff442f`.
|
||||
|
||||
- Sanctuary-beta.140.mrpack : `1b366746e4d6bebfbe0115c849524bdeb81c14eee89a2510f9dbe18808974d6a`.
|
||||
- Sanctuary-Test-beta.140.mrpack : `50c3cdf759117bb5286d41b4535fde15d9d84deaac833b7f8f61c4a0d29b72e7`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||
publiée sur le commit source `6e1b3b71088437fe57cf2b9b979b6d62711628d3` ; canal stable
|
||||
`2c360a0759d4a164f82c89ac2bef04ff69d50875` vérifié après publication.
|
||||
Deux synchronisations isolées puis deux dans la même instance Sanctuary Beta
|
||||
réussissent. Les **923 fichiers personnels** suivis gardent leurs hashes.
|
||||
Sauvegarde préalable : `sanctuary-backups/before-beta.140/`. Aucun monde personnel
|
||||
ouvert. Reçus : `build/shader140-isolated.json` et `build/shader140-prism.json`.
|
||||
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||
@@ -0,0 +1,107 @@
|
||||
# LIGHT-139 — Lumières colorées
|
||||
|
||||
Socle beta.138 + vérification Vulkan `9d84a02`, Minecraft 26.3 / Java 25.
|
||||
Branche `codex/colored-lights-beta139`, livraison beta.139.
|
||||
|
||||
## Contrat
|
||||
|
||||
Option locale « Lumières colorées » avec intensité. Les vrais blocs émetteurs
|
||||
teintent discrètement leurs environs, y compris hors champ. Torches/lanternes,
|
||||
lave et feu chauds ; sources des âmes bleues ; redstone active rouge ;
|
||||
froglights par variante ; sources Sanctuary selon leur texture/matière.
|
||||
Les blocs éteints n'émettent pas. Le bloom et les inclusions émissives des
|
||||
minerais restent indépendants ; aucune lumière de gameplay n'est ajoutée.
|
||||
|
||||
Propagation RGB dans un volume local de blocs chargés, obstacles et formes
|
||||
natives, construction répartie entre les images, cache des couleurs de
|
||||
matériaux. Aucun chargement forcé de chunk, accès aux mondes personnels,
|
||||
changement de sauvegarde, réseau ou règles serveur.
|
||||
|
||||
## Réalisation
|
||||
|
||||
Dans les options du shader : « Lumières colorées », désactivées initialement,
|
||||
et une intensité de 0 à 100 %, réglée à 35 %. Zéro ou OFF libère les ressources.
|
||||
Les préférences existantes sont conservées. Libellés et aides FR/EN.
|
||||
|
||||
Les familles vanilla demandées ont une palette dédiée. Les autres émetteurs,
|
||||
y compris ceux de Sanctuary, utilisent la couleur des pixels lumineux de leurs
|
||||
textures de modèle, calculée puis mise en cache. Ce repli suit les packs de
|
||||
ressources ; les palettes dédiées vanilla restent constantes. Une texture
|
||||
illisible ou sans pixels exploitables utilise du blanc. Les blocs purement
|
||||
émissifs du bloom, sans émission lumineuse native, ne deviennent pas des lampes.
|
||||
|
||||
Le champ couvre un cube de 64 blocs de côté autour de la caméra. Une marge de
|
||||
14 blocs réserve la propagation aux sources présentes dans le volume ; le rendu
|
||||
s'atténue progressivement sur 4 blocs à sa périphérie. C'est donc un effet de
|
||||
proximité, pas une illumination de tous les chunks visibles. Les formes et
|
||||
l'opacité natives règlent le passage de la lumière. Les canaux utilisent des
|
||||
maxima indépendants, sans addition illimitée des sources superposées.
|
||||
|
||||
Le calcul CPU est réparti avec un budget visé de 2 ms par image, plus les
|
||||
allocations et chargements ponctuels de matériaux ; aucun chiffre de FPS n'est
|
||||
garanti. Un monde immobile réutilise son champ. Les changements rapides et les
|
||||
déplacements peuvent présenter un bref retard, le temps de publier un champ
|
||||
complet. Aucun chargement forcé de chunk. Une texture 512 × 512 contient le champ.
|
||||
La passe GPU teinte les surfaces visibles, atténue l'effet au soleil et dans le
|
||||
brouillard, puis laisse agir le SSGI et le bloom. Les sources tenues, entités,
|
||||
particules et le rendu sous l'eau/lave ne sont pas couverts par cette option.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Essais client natifs réussis dans des mondes neufs (graine 122) sur Apple M1 :
|
||||
|
||||
- OpenGL 4.1 Metal : **1 min 46 s**, `build/colored139-opengl.log`.
|
||||
- Vulkan / MoltenVK 1.4.2, profondeur 0–1 et transparence améliorée :
|
||||
**1 min 30 s**, `build/colored139-vulkan.log`.
|
||||
|
||||
Les deux journaux contiennent `COLORED139_PASS` et le backend réellement actif.
|
||||
Torche et torche des âmes hors champ, trois froglights, pierre lumineuse dont
|
||||
la couleur vient de sa texture, intensité 35/100, cache statique, déplacement
|
||||
et retour de caméra, mur fermé puis ouvert, lampe redstone allumée puis éteinte,
|
||||
retrait de source, rechargement des ressources et OFF/zéro vérifiés.
|
||||
Les préférences persistantes et les libellés FR/EN sont contrôlés ; une dernière
|
||||
capture active conjointement PBR, SSGI, bloom, ombres, highlights et rayons.
|
||||
|
||||
Les sommes d'écart entre OFF et 35 % sont presque identiques entre backends :
|
||||
torche 2 025 577 / 2 025 576 ; âmes 964 200 / 964 201. Il s'agit de mesures
|
||||
sur les pixels de cette scène, pas d'un indice de performance.
|
||||
Comparatif autonome : `build/Shader-beta.139-comparaison.html`.
|
||||
Pas de validation du pilote Vulkan Windows de l'utilisateur revendiquée.
|
||||
Le test fige uniquement le scintillement vanilla des torches afin de comparer
|
||||
les images ; cette sonde est exclue du JAR distribué. Aucun monde personnel ouvert.
|
||||
|
||||
## Diagnostic des essais
|
||||
|
||||
La première comparaison de stabilité détectait le scintillement natif de la
|
||||
lightmap des torches, même avec un champ coloré inchangé. La sonde de test
|
||||
stabilise ce seul facteur ; aucune désactivation de scintillement en production.
|
||||
La pièce d'essai est désormais construite après le chargement de ses chunks,
|
||||
et l'absence de lumière du ciel est vérifiée avant les captures.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader139-build.log`.
|
||||
La comparaison à beta.138 trouve **13 entrées de production** modifiées,
|
||||
limitées au shader, à son menu et aux aides FR/EN. Les sondes de test sont
|
||||
absentes du JAR distribué. Sources, archives normale/Test et template complets
|
||||
vérifiés ; textures des gemmes et clé de Steve conservées à l'identique.
|
||||
Reçu local : `build/shader139-artifact.json`.
|
||||
|
||||
JAR SHA-256 : `7b958f6576adbabb2b1af589b866cb52b1a65600ea2fa505b05dd283f8d595df`.
|
||||
|
||||
- `Sanctuary-beta.139.mrpack` : 10450317 octets ; SHA-256 `13f3a283c260876d28ce0c6504615b4a8f4d575906badfbb2b39558f312e9fbb`.
|
||||
- `Sanctuary-Test-beta.139.mrpack` : 10469244 octets ; SHA-256 `c1e0b684dc2650950a0925c4625ef5eb4b78fb5f1c5680f5e302550424fa6d81`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
[Release beta.139](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.139)
|
||||
publiée depuis `dc33ffa54697f61cd8f579235e7cebdbf0765f7c`, tag exact `beta.139`.
|
||||
Canal packwiz : `9f10167b6acfece9c045573580e186aa72bc91dc`.
|
||||
Deux synchronisations isolées puis deux synchronisations de la même instance
|
||||
Prism réussissent. Un seul JAR Sanctuary beta.139 actif, empreinte identique
|
||||
à l'archive vérifiée ; les **923 fichiers personnels suivis** conservent leurs
|
||||
empreintes. Sauvegarde des fichiers gérés dans
|
||||
`sanctuary-backups/before-beta.139/`. Aucun monde personnel ouvert.
|
||||
Reçus : `build/shader139-isolated.json`, `build/shader139-prism.json`.
|
||||
@@ -0,0 +1,74 @@
|
||||
# beta.098 — Construire le plan en créatif
|
||||
|
||||
Ticket CONSTRUCTION-098, branche `codex/creative-construction-beta098`.
|
||||
À la demande du créateur, le parcours manuel du Métabli est complété par une
|
||||
construction immédiate réservée au mode créatif.
|
||||
|
||||
## Parcours
|
||||
|
||||
Choisir une statue, une machine, un bâtiment ou un plan importé au Métabli.
|
||||
Positionner et orienter l’aperçu avec K. Le bouton **Construire le plan…**
|
||||
apparaît sous les coordonnées en créatif seulement. La confirmation indique
|
||||
le nombre de blocs, l’origine et le fait que le plan entier sera construit,
|
||||
même si une seule couche est affichée. Annuler revient aux réglages.
|
||||
|
||||
Confirmer pose les blocs sans consommer de matériaux, via le protocole serveur
|
||||
existant. Le plan garde son origine, son orientation et son suivi d’avancement.
|
||||
Les machines reçoivent leurs composants natifs : la clé dorée reste nécessaire
|
||||
pour assembler le multibloc fonctionnel. Les inventaires et entités ne sont
|
||||
pas copiés depuis les fichiers de plans.
|
||||
|
||||
Le bouton disparaît si le mode de jeu change. Une confirmation devenue périmée
|
||||
(changement de plan, position, monde ou perte du créatif) ne lance rien.
|
||||
L’envoi en cours désactive le bouton et ne peut pas être remplacé par un second.
|
||||
En survie, la construction reste manuelle et le serveur refuse toujours les
|
||||
envois fabriqués sans passer par l’interface.
|
||||
|
||||
Les validations serveur existantes restent applicables : cases libres ou déjà
|
||||
conformes, zone chargée, hauteur et bordure du monde, distance de 192 blocs,
|
||||
absence d’entité à l’emplacement, permissions et interdiction des blocs
|
||||
techniques sans objet. Aucun changement de génération ni format de sauvegarde.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite native `Plans072ClientChecks`, Minecraft 26.3, nouveau monde plat de
|
||||
développement, graine 72 : **réussie en 47 secondes**.
|
||||
|
||||
- Bouton créatif visible et confirmation inspectés en capture native.
|
||||
- Annulation sans placement ; statue de Creeper de 32 blocs de haut, 1 542
|
||||
cellules, construite avec vérification de chaque matériau côté serveur.
|
||||
- Fourneau et abri choisis dans le catalogue physique, rotation, filtre sur
|
||||
une seule couche : le bouton construit bien toutes les couches du plan.
|
||||
- Perte du créatif pendant la confirmation : aucun bloc posé, bouton absent.
|
||||
- Envoi falsifié en survie refusé ; pose manuelle consommant exactement un bloc.
|
||||
- Régression des 88 modèles natifs, export, matériaux et interfaces FR/EN.
|
||||
|
||||
Le premier passage enchaînait les plans en moins d’une seconde et rencontrait
|
||||
la limite d’envoi serveur existante. Le test attend désormais 25 ticks entre
|
||||
constructions ; aucun changement de cette limite n’a été nécessaire.
|
||||
Le test Métabli conserve son assertion d’absence de catalogue à distance,
|
||||
mais ne considère plus le nouveau bouton créatif comme interdit.
|
||||
|
||||
Captures dans `build/creative098-evidence/`, journal
|
||||
`build/creative098-client.log`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussi en **2 min 7 s**, 124 tâches. Les GameTests sur serveur dédié restent
|
||||
exclus conformément au refus antérieur d’accepter son EULA. Aucun serveur
|
||||
dédié lancé ; essais uniquement sur un nouveau monde client de développement.
|
||||
|
||||
Les archives ont été vérifiées : version Minecraft 26.3 et compteur beta.098,
|
||||
JAR sources identiques aux fichiers de travail, aucun test embarqué. Par
|
||||
rapport à beta.097, seules les classes PlanClient/PlansScreen et trois libellés
|
||||
FR/EN changent dans Sanctuary. Textures, données, resource pack et archives
|
||||
beta.097 conservés. Reçu : `build/beta098-artifact.json`.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
- `build/Sanctuary-beta.098.mrpack`, 9 880 395 octets ; SHA-256
|
||||
`f06d0cc0256c6c55b1081019d0f00d2f9d29963cbf06d63b8cf1d9b303970f7a`.
|
||||
- `build/Sanctuary-Test-beta.098.mrpack`, 9 899 316 octets ; SHA-256
|
||||
`f2424a39b6b96076bdff8ac2a93e74ce83d77f453aa9bfbdd18a5fa17444f573`.
|
||||
|
||||
Aucun déploiement dans une instance personnelle, aucune sauvegarde personnelle
|
||||
ouverte ou modifiée, aucun avancement du canal packwiz.
|
||||
@@ -0,0 +1,127 @@
|
||||
# LIGHT-141 — Direction locale pour les normales PBR
|
||||
|
||||
Socle beta.140 `c9f5c56`, branche `codex/directional-lights-beta141`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.141.
|
||||
|
||||
## Contrat
|
||||
|
||||
Estimer une direction d'arrivée depuis les six voisins du champ lumineux,
|
||||
avec intensité et confiance. Conserver cette estimation dans une texture
|
||||
compacte reconstruite avec le champ, uniquement quand le PBR est actif.
|
||||
Les sources portées utilisent leurs positions déjà disponibles. Brancher ces
|
||||
directions sur les normales et reflets PBR existants, sans nouveau bouton.
|
||||
Les sources opposées doivent réduire la directionnalité, sans bascule brutale.
|
||||
Les champs précédent/courant gardent leur fondu et les obstacles restent pris
|
||||
en compte pour les blocs posés. Aucun changement de lumière de gameplay,
|
||||
de sauvegarde ou de génération ; aucune promesse de lancer de rayons exact.
|
||||
|
||||
## Programme de vérification
|
||||
|
||||
Contrôles natifs OpenGL/Vulkan : direction, sources opposées, murs, rendu de
|
||||
relief en intérieur, déplacement des sources, transitions et PBR OFF.
|
||||
Régression des lumières beta.139/140 puis check/build et assemblage. GameTest
|
||||
serveur dédié exclu selon le refus EULA antérieur. Aucun monde personnel ouvert.
|
||||
|
||||
## Réalisation
|
||||
|
||||
Le champ RGB conserve une texture directionnelle RGBA8 supplémentaire :
|
||||
les trois composantes codent la direction pondérée, l'alpha sa réponse lumineuse.
|
||||
Les voisins plus lumineux et accessibles contribuent à l'estimation ; les
|
||||
arrivées opposées s'annulent dans le vecteur, sans supprimer l'éclairage natif.
|
||||
Le shader interpole les moments pondérés avant normalisation, y compris entre
|
||||
les champs précédent et courant. La confiance réduit le relief quand aucune
|
||||
direction ne domine.
|
||||
|
||||
Cette texture est préparée dans le budget de reconstruction existant, avec
|
||||
un maximum de six voisins par cellule éclairée et une table d'énergie réutilisée.
|
||||
Aucune recherche de sources ni rayons par pixel pour les blocs posés. La
|
||||
lecture GPU interpole huit cellules par champ ; les deux champs sont utilisés
|
||||
pendant le fondu. Les sources mobiles réutilisent les positions, intensités et
|
||||
transitions déjà collectées, dans la limite existante de 128 sources.
|
||||
|
||||
Une texture directionnelle occupe environ 1,5 Mo à 16 blocs de distance et
|
||||
19 Mo à 64 blocs, par copie CPU/GPU ; deux champs coexistent lors des transitions.
|
||||
Le champ directionnel n'est construit que si PBR est actif avec une intensité
|
||||
non nulle. Sa désactivation libère les directions après la transition.
|
||||
Le coût est borné par ces réglages, sans garantie de FPS.
|
||||
|
||||
La passe PBR applique la différence entre normale de base et normale perturbée,
|
||||
puis un reflet GGX local. Elle garde sa portée de 32 blocs et les réglages PBR
|
||||
existants ; Lumières colorées doit être actif. Le soleil et la lune continuent
|
||||
à fonctionner lorsque les lumières colorées sont coupées. Les aides FR/EN
|
||||
expliquent cette dépendance.
|
||||
|
||||
C'est une direction dominante approximative : deux lumières colorées opposées
|
||||
ne produisent pas deux reflets indépendants. Les sources mobiles gardent la
|
||||
propagation radiale et les limites d'occultation existantes. Les normales PBR
|
||||
concernent les surfaces de blocs prises en charge, pas tous les modèles de mobs.
|
||||
Les exclusions existantes des panoramas et de l'immersion restent inchangées.
|
||||
|
||||
## Premier essai ciblé
|
||||
|
||||
L'essai natif OpenGL ciblé réussit en **1 min 12 s** : direction gauche/droite,
|
||||
annulation des sources opposées avec énergie conservée, occultation par un mur,
|
||||
relief sur la pierre en intérieur et avec une torche en main, stabilité au repos,
|
||||
libération des directions avec PBR OFF et maintien du chemin solaire avec
|
||||
Lumières colorées OFF. La somme d'écarts PBR OFF/ON sur la pierre vaut 878 114
|
||||
au réglage PBR 100 % ; c'est une comparaison de pixels, pas une mesure de FPS.
|
||||
Log : `build/directional141-focused.log`.
|
||||
|
||||
Les essais de préparation ont révélé un conflit de nom de matrice GLSL, corrigé,
|
||||
puis des erreurs de fixture (angle 180° normalisé par Minecraft, suppression
|
||||
d'entités alors que la sélection était vide, commande d'heure déjà à minuit),
|
||||
corrigées sans changer le rendu.
|
||||
|
||||
## Validation complète OpenGL
|
||||
|
||||
La chaîne complète réussit en **3 min 1 s** sur OpenGL / Apple M1 :
|
||||
`COLORED139_PASS`, `COLORED140_PASS` et `DIRECTIONAL141_PASS`.
|
||||
Elle réunit les contrôles de couleurs, objets portés, caméra, occultation,
|
||||
rechargement des ressources, préférences, distances/fondus et directions PBR.
|
||||
Le test ciblé est reproductible avec `-PsanctuaryDirectional141Only=true` ;
|
||||
la chaîne complète utilise `-PsanctuaryDirectional141ClientTests=true` avec
|
||||
les deux suites de lumières existantes.
|
||||
Log : `build/directional141-opengl.log`. Captures natives conservées dans
|
||||
`build/directional141-opengl-screenshots/`. Comparaison autonome sans retraitement :
|
||||
`build/Shader-beta.141-comparaison.html`.
|
||||
|
||||
## Validation complète Vulkan
|
||||
|
||||
La même chaîne réussit en **2 min 56 s** sur Vulkan / Apple M1, MoltenVK 1.4.2,
|
||||
profondeur 0–1 et transparence améliorée. Les trois marqueurs de réussite sont
|
||||
présents dans `build/directional141-vulkan.log`. L'écart PBR local OFF/ON vaut
|
||||
878 107, contre 878 114 sous OpenGL dans cette scène. Les captures natives sont
|
||||
conservées dans `build/directional141-vulkan-screenshots/`.
|
||||
Les deux moteurs vérifient la compilation, les chemins actifs et désactivés,
|
||||
le cache, les murs, les sources opposées et portées, ainsi que les régressions
|
||||
beta.139/140. Pas de validation des pilotes Windows de l'utilisateur revendiquée.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 35 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur. Les suites clientes ci-dessus ont été exécutées
|
||||
séparément. Log : `build/shader141-build.log`.
|
||||
|
||||
Comparaison à beta.140 : **8 entrées de production modifiées**, limitées au
|
||||
champ lumineux, à sa préparation avant le PBR, au shader PBR et aux aides FR/EN.
|
||||
Les archives normale/Test contiennent le même JAR Sanctuary, sans classes
|
||||
de test. Version, dépendances exactes, sources des JAR, textures de gemmes et
|
||||
de la clé de Steve, et template complet sont vérifiés.
|
||||
Reçu : `build/shader141-artifact.json`.
|
||||
|
||||
SHA-256 du JAR : `660f29c3fa77fee4de6a8db5673c61619985804494f70e961998da36f396d0f1`.
|
||||
|
||||
- Sanctuary-beta.141.mrpack : `08df23dac9e98fbfa2935f747345c38244bddc3a3d9371c6416f251e85a7eb19`.
|
||||
- Sanctuary-Test-beta.141.mrpack : `a300ef5bfff7b6990c9f5c2760e9c3a23d7a27aeb10b8f04d29db9a7409fa0e1`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée sur le commit source `93ed5574a4f77bd3a083b254498f4994efe31179` ; canal stable
|
||||
`7e6c84400f80db24bf5204cd21d628194ae9f0f7` vérifié après publication.
|
||||
Deux synchronisations isolées puis deux dans la même instance Sanctuary Beta
|
||||
réussissent. Les **923 fichiers personnels** suivis gardent leurs hashes.
|
||||
Sauvegarde préalable : `sanctuary-backups/before-beta.141/`. Aucun monde personnel
|
||||
ouvert. Reçus : `build/shader141-isolated.json` et `build/shader141-prism.json`.
|
||||
Archives normale/Test/template et comparaison copiées dans `sanctuary-beta/build/`.
|
||||
@@ -29,6 +29,11 @@ et ses règles restent versionnées pour éviter de déplacer un site au recharg
|
||||
|
||||
## Placement des temples
|
||||
|
||||
**Évolution beta.097** : le refus bloquant décrit dans cette version historique
|
||||
est remplacé par une recherche de secours, puis un placement garanti des pièces
|
||||
vanilla avec fondation si nécessaire. Les sites historiques admissibles restent
|
||||
prioritaires. Voir le [contrat du correctif](temple-crash-beta097.md).
|
||||
|
||||
Les candidats sont des chunks de l’intérieur de chaque île, à au moins 48 blocs
|
||||
de son centre, ordonnés par un tirage déterministe. La recherche filtre le type
|
||||
voulu, puis conserve les conditions natives de biome, de pente et de fondation.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# beta.100 — Statues découvertes et promenade des familiers
|
||||
|
||||
Branche `codex/exploration-familiers-beta100`. Minecraft 26.3.
|
||||
|
||||
## Contrat
|
||||
|
||||
Le catalogue des statues et son sélecteur ne proposent que les espèces
|
||||
observées, tuées ou ayant tué le joueur. Le Métabli envoie une liste calculée
|
||||
par le serveur à partir des notes `seen_mobs` et des statistiques natives.
|
||||
Le champ d’identifiant et la capture de cible respectent aussi cette liste.
|
||||
Les plans déjà construits/importés restent des plans de blocs ordinaires.
|
||||
Aucun nouveau format de sauvegarde, aucune migration ni changement de terrain.
|
||||
|
||||
Les familiers au repos choisissent une destination atteignable puis font une
|
||||
pause. Le tempérament existant règle leur rayon, leur allure et leurs pauses :
|
||||
curieux explorateur, joueur plus vif, calme plus posé. Les protecteurs restent
|
||||
près du point gardé. Les déplacements d’attaque, la fuite, les ordres, le portage
|
||||
et les montures gardent la priorité. Les volants ne suivent plus une orbite
|
||||
perpétuellement recalculée. Les chemins ratés sont abandonnés et les recherches
|
||||
restent bornées dans les chunks chargés. Les identités et caractères existants
|
||||
ne sont pas retirés au sort.
|
||||
|
||||
## Validation
|
||||
|
||||
`Exploration100ClientChecks` : **réussi en 4 min 37 s**. Nouveau monde plat
|
||||
avec serveur intégré, sans serveur dédié :
|
||||
|
||||
- Une vache réellement visée débloque sa statue. Les statistiques natives
|
||||
tué/par-qui-tué ajoutent zombie et squelette. Un œuf de cochon porté ne suffit
|
||||
pas. Le client reçoit la liste du serveur ; une génération de cochon reste
|
||||
refusée même en créatif. Capture du catalogue inspectée.
|
||||
- Douze promenades réelles : loup, poule, slime, chauve-souris, chacun calme,
|
||||
curieux et joueur. Plusieurs cases traversées, pauses, maintien près du joueur.
|
||||
Le tempérament calme est testé avec personnalité protectrice, les deux autres
|
||||
avec pacifiste pour isoler le déplacement au repos.
|
||||
- Régression `Autonomy074ClientChecks` complète : quatre personnalités,
|
||||
déplacements et dégâts réels, garde, cibles neutres, permissions, obstacles,
|
||||
mode travail, ordres H bref/maintenu et touche reconfigurée.
|
||||
- `.schem` Sponge v2/v3 : renommage natif ancien `grass` vers `short_grass`,
|
||||
conservation exacte du fichier source et refus explicite d’un bloc de mod absent.
|
||||
|
||||
Journal : `build/exploration100-client.log`. Captures :
|
||||
`build/exploration100-evidence/`. La matrice ne constitue pas un nouveau playtest
|
||||
visuel de toutes les espèces, ni une validation à plusieurs clients distants.
|
||||
`Schematics100ClientChecks` : **réussi en 1 min 2 s**. Le client refuse un
|
||||
fichier incompatible dans le Métabli sans fermer le menu, affiche son identifiant
|
||||
fautif, puis importe un `.schem` valide. Fermeture, réouverture de la même
|
||||
sauvegarde et ouverture d’un deuxième monde passent, avec conservation de la
|
||||
structure et remise à zéro du plan/de la session d’atelier. Le fichier rejeté
|
||||
reste identique. Journal : `build/schematics100-client.log`.
|
||||
|
||||
Ces essais macOS ne reproduisent pas le crash Windows rapporté ; ils ne
|
||||
constituent pas une correction démontrée de cet incident. `check build assemblePack assembleTestPack` réussit en **2 min 17 s**,
|
||||
124 tâches, avec `-x :sanctuary:runGameTest` (serveur dédié exclu conformément
|
||||
au refus de son EULA). Les deux packs sont vérifiés : même JAR testé, sources
|
||||
correspondantes, libellés FR/EN, textures et archives beta.099 conservées.
|
||||
Reçu : `build/beta100-artifact.json`. Aucun déploiement personnel ni publication
|
||||
du canal packwiz.
|
||||
|
||||
## Import `.schem` et incident Windows signalé
|
||||
|
||||
Les palettes Sponge v2/v3 utilisent maintenant le convertisseur natif
|
||||
`References.BLOCK_STATE` quand leur `DataVersion` est connue et antérieure.
|
||||
Seule une copie de palette en mémoire est convertie ; le fichier importé et
|
||||
les sauvegardes ne sont jamais réécrits par cette opération. Une version
|
||||
future, un bloc de mod absent ou une propriété réellement incompatible restent
|
||||
refusés. Sans version source, le parseur reste strict et ne devine pas une
|
||||
conversion. Le message d’import indique le bloc/état fautif.
|
||||
|
||||
Le signalement Windows décrit un échec d’ouverture, un monde absent de la liste
|
||||
puis une fermeture du client lors du choix d’un autre monde. Aucun journal ni
|
||||
fichier précis n’est disponible. L’inspection ne trouve aucun accès de suppression
|
||||
des mondes dans l’importeur ; les plans actifs sont effacés en mémoire lors des
|
||||
changements de connexion. Cela ne prouve pas la cause du signalement : sa
|
||||
reproduction et le diagnostic Windows restent ouverts. Les fichiers utiles sont
|
||||
`logs/latest.log` et, s’il existe, le rapport daté de `crash-reports/` dans le
|
||||
dossier Minecraft de l’instance concernée. Ne pas supprimer ni convertir les
|
||||
sauvegardes pour tenter de résoudre cet incident.
|
||||
|
||||
## Archives locales
|
||||
|
||||
- [Sanctuary-beta.100.mrpack](../build/Sanctuary-beta.100.mrpack), 9914561 octets.
|
||||
SHA-256 : `7e24ba5e77c7fbb88cfadb55b3e373ad7e998381fd1c56e431aa79b4619a8075`.
|
||||
- [Sanctuary-Test-beta.100.mrpack](../build/Sanctuary-Test-beta.100.mrpack), 9933483 octets.
|
||||
SHA-256 : `a43bf61dc38c3c91f4cb09022040099ee1ca9274a6b00751178c80ee204dd944`.
|
||||
@@ -0,0 +1,58 @@
|
||||
# beta.101 — Atterrissage des familiers volants
|
||||
|
||||
Branche `codex/flying-landing-beta101`, Minecraft 26.3.
|
||||
|
||||
Le profil de vol du familier doit désactiver l’accumulation et le traitement des
|
||||
chutes de son entité commune. Une descente pilotée, autonome ou un contact avec
|
||||
le sol ne doit causer ni dégâts, ni animation/son de coup, ni entrée en combat.
|
||||
Les vraies attaques et les chutes des familiers terrestres gardent leurs règles.
|
||||
Aucun changement de navigation, de sauvegarde, de texture ou de terrain.
|
||||
|
||||
## Cause et correction
|
||||
|
||||
L’entité Sanctuary commune hérite de `PathfinderMob`. Son contrôle de vol et
|
||||
l’absence de gravité ne remplacent pas les méthodes natives de chute. Ces
|
||||
méthodes pouvaient appeler `FamiliarBattle.hurt`, produire son animation/son de
|
||||
coup, interrompre une technique et enregistrer du combat.
|
||||
|
||||
`checkFallDamage` remet maintenant la distance à zéro pour les profils volants.
|
||||
`causeFallDamage` refuse aussi un appel avec une distance déjà calculée, avant
|
||||
les sons et les dégâts natifs. Les autres profils délèguent toujours au code
|
||||
Minecraft. Les sources de dégâts ordinaires ne sont pas filtrées.
|
||||
|
||||
## Validation
|
||||
|
||||
Reproduction native sur beta.100 : le perroquet accepte le callback de chute,
|
||||
et l’assertion « Flying familiar rejects native fall callback » échoue.
|
||||
Journal conservé : `build/landing101-before.log`.
|
||||
|
||||
`Landing101ClientChecks` réussit après correction en **1 min 5 s**, dans un
|
||||
nouveau monde plat de développement avec le serveur intégré :
|
||||
|
||||
- Les douze espèces du registre ayant un profil volant : perroquet,
|
||||
chauve-souris, abeille, allay, Wither, breeze, phantom, vex, blaze, ghast,
|
||||
ghast joyeux et dragon.
|
||||
- Callback natif de chute à sept blocs, descente à 0,25 bloc par tick jusqu’au
|
||||
contact réel avec le sol, puis mouvement rapide vers le sol.
|
||||
- Santé inchangée, distance de chute nulle et absence de mise à jour de
|
||||
`lastHurt`, donc pas de passage dans la branche qui diffuse le faux coup.
|
||||
- Une attaque de zombie reste acceptée et enlève de la santé pour chaque espèce.
|
||||
- Un loup terrestre conserve les dégâts de sa chute.
|
||||
|
||||
Les descentes sont pilotées par le test via les collisions natives ; ce n’est
|
||||
pas un nouveau playtest de tous les trajets autonomes ou un essai multiclient.
|
||||
Journal : `build/landing101-client.log`. Aucun monde personnel n’est utilisé.
|
||||
`check build assemblePack assembleTestPack` réussit en **2 min 22 s**,
|
||||
124 tâches, avec le serveur dédié exclu (`-x :sanctuary:runGameTest`).
|
||||
Les archives normal/Test embarquent le même JAR vérifié et ses sources exactes.
|
||||
Seul `FamiliarEntity` et les numéros de ligne de deux classes internes changent
|
||||
par rapport à beta.100. Les ressources, textures et archives beta.100 sont
|
||||
conservées. Reçu : `build/beta101-artifact.json`. Aucun déploiement personnel
|
||||
ni publication du canal packwiz.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.101.mrpack](../build/Sanctuary-beta.101.mrpack), 9914792 octets.
|
||||
SHA-256 : `3008127d2bb09556c4997c00350e82ff26af5402b69bc7c09c822b24ff4bfe74`.
|
||||
- [Sanctuary-Test-beta.101.mrpack](../build/Sanctuary-Test-beta.101.mrpack), 9933717 octets.
|
||||
SHA-256 : `854a8b6a42ac7ca0236e0c19b9000808535df273911b9c6a72844097a49358a0`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# beta.094 — Foyers allumés du Fourneau
|
||||
|
||||
La chaleur partagée allume toute la façade du Fourneau, y compris si la cuisson
|
||||
se déroule dans une case intérieure. Les deux ouvertures reçoivent des braises
|
||||
orange et jaunes. La pierre et les trois textures originales sont conservées.
|
||||
Quand la chaleur est épuisée, la façade reprend son aspect éteint.
|
||||
|
||||
Les 27 sources de particules des fours composants sont remplacées par une seule
|
||||
animation de façade : une flamme et une fumée dans chacun des deux foyers.
|
||||
Leurs positions correspondent aux pixels des ouvertures, à 0,02 bloc devant la
|
||||
face, dans les quatre orientations. Un four isolé garde son animation native.
|
||||
La dissociation rétablit l'état lumineux individuel des fours.
|
||||
|
||||
## Assets et rendu
|
||||
|
||||
- Original inchangé : `assets/sanctuary/textures/block/fourneau/front.png`.
|
||||
- Variante de feu : `assets/sanctuary/textures/block/fourneau/front_on.png`.
|
||||
- `tools/fourneau-embers.json` délimite les bandes des ouvertures à illuminer.
|
||||
- `python3 tools/generate-fourneau-models.py` régénère les modèles éteints/allumés.
|
||||
|
||||
La variante a été créée avec l'outil intégré **imagegen**, puis réduite en
|
||||
48 × 48 par échantillonnage au plus proche pour la densité native du modèle.
|
||||
La génération ayant aussi redessiné la pierre, le rendu prélève uniquement les
|
||||
pixels de feu dans les ouvertures ; tout le reste utilise l'image originale.
|
||||
Les fines faces de braises sont décalées de 0,002 pixel pour éviter le z-fighting.
|
||||
|
||||
Prompt retenu : « Edit the provided 48 by 48 pixel Minecraft furnace facade
|
||||
texture. Create its LIT variant. Preserve the pixel-art grid, grey stone, mortar,
|
||||
arches, outer edges and dimensions. Change only the black/dark interiors of the
|
||||
two arched openings to Minecraft-style orange, red and yellow embers/fire pixels,
|
||||
bright yellow near the bottom, orange above. Flames stay inside the holes.
|
||||
No perspective, border, labels or new elements. »
|
||||
|
||||
Le choix du modèle utilise l'état `LIT` natif. Les snapshots d'assemblage, leurs
|
||||
paquets réseau et le schéma de sauvegarde restent identiques. Aucun paquet de
|
||||
particules n'est envoyé par le serveur. Aucun monde personnel n'est modifié.
|
||||
Le pack de textures intégré reste à son édition beta.090 ; ces assets sont ceux
|
||||
du mod beta.094.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test client natif avec serveur intégré est passé sur un nouveau monde plat de
|
||||
développement, graine 85 (`build/fourneau094-client.log`, 1 min 5 s) :
|
||||
|
||||
- 27 composants synchronisés à cheval sur quatre chunks, quatre orientations.
|
||||
- Chauffe réelle depuis une case intérieure, façade entière allumée puis éteinte.
|
||||
- 800 émissions interceptées à la sortie de l'animation native : coordonnées
|
||||
devant la façade, sur un pixel de feu situé dans une ouverture sombre originale.
|
||||
- Aucune nouvelle particule à froid ; particules natives d'un four isolé conservées.
|
||||
- Rechargement des ressources, départ/retour dans les chunks, dissociation,
|
||||
reconstruction et destruction ; conservation du contenu d'origine.
|
||||
- Captures allumé/éteint inspectées dans `build/fourneau094-evidence/`.
|
||||
|
||||
Commande du test :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryFourneau085ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Les particules déjà émises finissent naturellement leur durée de vie après
|
||||
l'extinction, comme celles des fours Minecraft. Test natif macOS ; pas de test
|
||||
avec shaders tiers ni de session LAN à deux clients pour cette livraison.
|
||||
Le serveur dédié de GameTest est exclu, conformément au refus antérieur de son
|
||||
EULA. Aucun déploiement personnel ou changement de sauvegarde.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` : réussi
|
||||
en 2 min 31 s, 124 tâches (`build/fourneau094-check.log`). La régénération des
|
||||
216 modèles est reproductible. `git diff --check` passe.
|
||||
|
||||
Les archives normal/Test ont été exportées et vérifiées : Minecraft 26.3,
|
||||
version beta.094, même JAR Sanctuary intégré aux deux packs, sondes de test
|
||||
absentes. Les assets existants, JEI et les archives beta.093 sont inchangés.
|
||||
Le reçu `build/fourneau094-artifact.json` conserve les empreintes SHA-256 ;
|
||||
`build/verify-fourneau094.py` reproduit ces contrôles.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.094.mrpack).
|
||||
- [Pack de test plat](../build/Sanctuary-Test-beta.094.mrpack).
|
||||
@@ -0,0 +1,74 @@
|
||||
# beta.115 — Dépôts et chauffe du Fourneau
|
||||
|
||||
Ticket sur `codex/fourneau-transfers-beta115`, après le signalement de piles
|
||||
qui se réalignent pendant les dépôts rapides avec Maj, dans Ingrédients et
|
||||
Combustibles, et d'une chauffe difficile à comprendre.
|
||||
|
||||
## Diagnostic
|
||||
|
||||
La méthode `mayPlace` de la case serveur utilisait `index`, masqué par le
|
||||
champ hérité `Slot.index` (position affichée). Elle validait donc successivement
|
||||
une entrée, un combustible et une sortie, au lieu des 27 entrées ou des
|
||||
27 combustibles. Le client acceptait le dépôt puis le serveur le refusait.
|
||||
Le test reproduit le refus d’un déplacement de la case 1 vers la case 23.
|
||||
Le correctif utilise explicitement `physicalIndex` pour la validation.
|
||||
|
||||
## Comportement
|
||||
|
||||
Les transferts Maj dans les vues complètes du Fourneau sont anticipés sur le
|
||||
client, avec les mêmes destinations et règles de combustible que le serveur.
|
||||
Les cases se remplissent dans l'ordre, après fusion des piles compatibles.
|
||||
Un dépôt manuel conserve la case choisie ; aucun tri périodique des ingrédients
|
||||
n'est ajouté. Les recherches masquent une partie du stock : leurs transferts
|
||||
entrants continuent d'attendre la réponse du serveur, qui connaît tout le stock.
|
||||
|
||||
La chauffe emprunte du combustible à une autre case uniquement pendant le tick
|
||||
natif. La pile non brûlée retourne immédiatement à sa case d'origine, avec tous
|
||||
ses composants. Les restes natifs restent associés à leur cuisson : un seau de
|
||||
lave devient un seau, puis un seau d'eau avec une éponge mouillée.
|
||||
|
||||
Les 27 entrées ont chacune une barre et une infobulle : cuisson en cours,
|
||||
manque de combustible avec progression conservée, résultat bloqué, absence de
|
||||
recette. Le panneau indique le nombre de cuissons et la réserve allumée en
|
||||
secondes de travail cumulées. Ce n'est pas une durée réelle restante : les
|
||||
cuissons parallèles se partagent la réserve. « Fonctionnement » explique les
|
||||
onglets, les gestes et le rendement normal du combustible en FR/EN.
|
||||
|
||||
Les recettes, résultats, expérience et règles de trémies restent natifs.
|
||||
Aucun changement de format, de monde ou de génération ; les 27 fours physiques
|
||||
conservent l'unique copie des objets et de leurs temps de chauffe.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test natif `Furnace115ClientChecks` passe avec un serveur intégré, sur un
|
||||
nouveau monde plat de graine 115 : vrai glisser avec Maj sur neuf piles nommées,
|
||||
remplissage consécutif, déplacement manuel vers la case 23 et répartition native
|
||||
sur six cases de colonnes différentes. Les 27 cases de chaque onglet valident
|
||||
leur rôle physique. Le combustible nommé conserve sa position et ses composants.
|
||||
|
||||
La cuisson parallèle, ses états synchronisés, la progression conservée à court
|
||||
de combustible, les 27 cuissons ravitaillées, le seau de lave et l'éponge,
|
||||
l'extraction des résultats et la sauvegarde native passent. Captures FR/EN
|
||||
relues dans `build/furnace115-evidence/`. Marqueur `FURNACE115_PASS` dans
|
||||
`build/furnace115-client.log`.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en 2 min 29 s, 125 tâches. Le panneau d'aide reçoit ensuite une dernière
|
||||
vérification client et une reconstruction ; la livraison commune beta.116
|
||||
repasse les contrôles complets en 2 min 23 s et le scénario Fourneau en 42 s.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Pas d'essai Windows ou de réseau distant avec latence simulée. Aucun monde
|
||||
personnel ouvert. Cette étape reste locale et rejoint la livraison commune
|
||||
avec les matériaux de sculpture : [beta.116](sculpture-materials-beta116.md).
|
||||
|
||||
Archives locales vérifiées (même JAR Sanctuary, sources concordantes, aucune
|
||||
classe de test ni changement de production extérieur au ticket) :
|
||||
|
||||
- `Sanctuary-beta.115.mrpack` : 10184430 octets, SHA-256
|
||||
`8fcac63b1bf5a01f680fb46666924705db3a2675e102037278e9d650d04cbf22`.
|
||||
- `Sanctuary-Test-beta.115.mrpack` : 10203354 octets, SHA-256
|
||||
`5b98fdb8842690158a20b422d6aa5496d74e572b3429498ffb25476d24e87f8a`.
|
||||
- JAR Sanctuary : `2066d3191d7ee27904927fab269d8160136f048742c3223713e8ed4fe90dd8f5`.
|
||||
|
||||
Reçu : `build/furnace115-artifact.json`.
|
||||
@@ -0,0 +1,152 @@
|
||||
# GEM-01 — Gemmes, argile et découvertes — beta.126
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/gems-beta126`, Minecraft 26.3.
|
||||
|
||||
Deux gemmes natives complètent l'émeraude : `sanctuary:ruby` et
|
||||
`sanctuary:sapphire`. Chaque famille comprend un bloc de stockage, un minerai
|
||||
ordinaire et un minerai des abîmes. Les huit PNG du créateur sont copiés à
|
||||
l'identique ; leurs empreintes figurent dans `tools/gems-textures-beta126.json`.
|
||||
Les noms des fichiers français `saphir` sont associés aux identifiants anglais
|
||||
stables `sapphire`, sans modifier les pixels. Les deux items utilisent les fichiers de remplacement
|
||||
`Desktop/ruby.png` et `Desktop/saphir.png` fournis en fin de livraison.
|
||||
|
||||
L'ordre créatif est émeraude, rubis, saphir : ingrédients, blocs de stockage,
|
||||
minerais ordinaires et minerais des abîmes dans leurs onglets natifs respectifs.
|
||||
Minage à la pioche de fer ou mieux, résistance et expérience comme l'émeraude,
|
||||
Fortune, Toucher de soie, cuisson/four à fusion et conversion 9 gemmes ↔ 1 bloc.
|
||||
Les blocs peuvent former un socle de balise et les gemmes servir de paiement.
|
||||
Les recettes rejoignent la collection actuelle de l'émeraude, sans ajouter de
|
||||
transactions de villageois. Libellés français et anglais, tags communs `c:`.
|
||||
|
||||
## Génération procédurale et contrat de compatibilité
|
||||
|
||||
Profil initial **gemmes 1 / beta.126** : quatre tentatives par chunk et par
|
||||
gemme, filons natifs de taille 3, hauteur uniforme Y0–256, probabilité de rejet
|
||||
au contact de l'air de 0,25. Ces nombres sont un réglage initial à équilibrer
|
||||
lors de la future refonte ; aucun quota de gemmes par chunk n'est garanti.
|
||||
Les variantes ordinaires remplacent les roches du tag vanilla
|
||||
`stone_ore_replaceables`, les variantes des abîmes celles de
|
||||
`deepslate_ore_replaceables`.
|
||||
|
||||
Les fichiers `worldgen/feature/ore_<gemme>.json`,
|
||||
`worldgen/placed_feature/ore_<gemme>.json` et les tags de biomes
|
||||
`has_ore/<gemme>` séparent les cibles, la forme, la fréquence, la hauteur et
|
||||
les régions. Un datapack peut les remplacer sans recompiler le mod. Le code
|
||||
réutilise `OreFeature` et le hasard natif issu de la graine ; il limite ces
|
||||
filons aux mondes utilisant `SanctuaryChunkGenerator`. Les biomes vanilla
|
||||
partagés avec Sanctuary restent compatibles sans ajouter les gemmes aux
|
||||
mondes vanilla, au Nether ou à l'End.
|
||||
|
||||
Cette évolution additive est autorisée pour les **nouveaux chunks uniquement**.
|
||||
Aucun remplissage rétrospectif, aucune régénération, aucun remplacement de chunk
|
||||
sauvegardé ni activation d'expansion. Les codecs du relief, les dimensions et
|
||||
le format des sauvegardes restent inchangés. Les filons suivent les protections
|
||||
existantes de la décoration native. Les anciens profils expérimentaux précédant
|
||||
le générateur Sanctuary unifié ne sont pas étendus.
|
||||
|
||||
## Argile spéciale dans le Clay Workshop
|
||||
|
||||
Le bloc d'argile vanilla conserve les couleurs échantillonnées sur le modèle
|
||||
original, animal découvert ou import GLB. Les argiles colorées et les autres
|
||||
blocs continuent à transférer leur palette de texture. Une même règle est
|
||||
appliquée à l'aperçu client et au résultat serveur. Le changement de matériau
|
||||
invalide le résultat même si les deux palettes sont identiques. Le texte de
|
||||
l'atelier explique l'exception en français et en anglais.
|
||||
|
||||
Une sculpture coûte toujours un bloc ; aucune recette, découverte, donnée de
|
||||
sculpture déjà fabriquée ni règle de placement n'est modifiée.
|
||||
|
||||
## Commande de découverte commune
|
||||
|
||||
`/sanctuary discovery all` découvre tous les blocs, objets et espèces enregistrés.
|
||||
Les variantes `blocks all`, `items all` et `mobs all` ciblent une catégorie ;
|
||||
`block <identifiant>`, `item <identifiant>` et `mob <identifiant>` ciblent une
|
||||
entrée avec la complétion native. Un sélecteur de joueurs en fin de commande
|
||||
est facultatif ; sans cible, elle s'applique au joueur qui l'exécute.
|
||||
|
||||
Exemples :
|
||||
|
||||
```mcfunction
|
||||
/sanctuary discovery all
|
||||
/sanctuary discovery mobs all
|
||||
/sanctuary discovery block minecraft:stone
|
||||
/sanctuary discovery item minecraft:diamond
|
||||
/sanctuary discovery mob minecraft:cow
|
||||
/sanctuary discovery all @a
|
||||
```
|
||||
|
||||
La permission suit « Autoriser les commandes » en solo et les droits opérateur
|
||||
sur serveur. La console doit préciser la cible. Les identifiants invalides,
|
||||
l'air et les types d'entités sans espèce découvrable sont refusés.
|
||||
|
||||
Les blocs passent par le registre de découvertes et son événement existant ;
|
||||
les objets passent par `RecipeService.discover`. Les collections et recettes
|
||||
sont ensuite recalculées normalement. Les espèces rejoignent `seen_mobs`,
|
||||
la même donnée que l'observation naturelle, consommée par les familiers,
|
||||
le Métabli et le Clay Workshop. Il n'existe aucun drapeau de déblocage parallèle.
|
||||
Les catalogues déjà ouverts sont à rouvrir après la commande.
|
||||
|
||||
La commande conserve les quantités possédées et les compteurs de combat,
|
||||
minage, pose et fabrication. Elle ne donne pas d'objets, n'achète pas les skills
|
||||
et n'attribue pas d'advancements. Les recettes réservées aux opérateurs gardent
|
||||
leurs règles. Les historiques personnels sont enrichis sans changer leur
|
||||
schéma ; la commande refuse les données illisibles plutôt que les remplacer.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Client natif Minecraft 26.3 / Java 25, `Gems126ClientChecks`, réussi en
|
||||
1 min 08 s. Les mondes sont jetables, sans sauvegarde personnelle ouverte :
|
||||
|
||||
- Les huit items et six blocs sont présents, avec les huit textures exactes.
|
||||
Quatre groupes créatifs respectent émeraude → rubis → saphir, sans doublons.
|
||||
- Douze recettes natives, découverte du bloc de stockage, outil fer minimum,
|
||||
Fortune III, Toucher de soie et balises passent les contrôles serveur.
|
||||
- Graine **126**, profil gemmes 1, 81 chunks autour de l'origine : 7 rubis dans
|
||||
la pierre et 8 dans la roche des abîmes ; 15 saphirs dans la pierre et 10 dans
|
||||
la roche des abîmes. La génération est refusée dans le monde plat vanilla.
|
||||
Les minerais persistent à la reconnexion, et un minerai retiré ne réapparaît pas.
|
||||
- L'argile conserve exactement les couleurs des voxels d'une vache et d'un GLB
|
||||
texturé. Bois et argile colorée transfèrent toujours leur palette. Le retour
|
||||
à l'argile, l'égalité aperçu/résultat et le coût d'un bloc en survie sont vérifiés.
|
||||
- La commande native respecte « Autoriser les commandes », accepte une cible
|
||||
et la console, refuse les identifiants invalides et permet une espèce, un
|
||||
bloc, un objet, toutes les espèces ou tout le contenu. Les recettes liées
|
||||
sont recalculées. La répétition est idempotente et les découvertes survivent
|
||||
à la reconnexion sans inventer de compteurs de combat ou de possession.
|
||||
|
||||
Logs et captures dans `build/gems126-client.log` et
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`. Le GameTest dédié
|
||||
reste exclu conformément au refus antérieur de son EULA. Le build complet et
|
||||
les archives sont vérifiés séparément avant publication.
|
||||
|
||||
|
||||
Build final `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest` réussi en **2 min 10 s**, 126 tâches. Les contrôles
|
||||
natifs de gameplay précèdent uniquement le remplacement final des deux PNG
|
||||
d'items ; les huit fichiers livrés sont vérifiés à l'octet près contre les
|
||||
sources, tous en 16 × 16. Les JAR de sources correspondent aux sources courantes.
|
||||
L'audit entre les binaires beta.125 et beta.126 ne trouve que les 112 entrées
|
||||
de production prévues. Les archives normal/Test contiennent le même mod et
|
||||
le template reproduit exactement ses ressources exportables.
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `Sanctuary-beta.126.mrpack` | `716221ce24549781ab91f5437ad2310497b198a4b61c43c40c67a27984365ae5` |
|
||||
| `Sanctuary-Test-beta.126.mrpack` | `bbf22d7dd72e6746538b4ec28686f699225d4a59372304e5db42140a80e88bd1` |
|
||||
| `sanctuary-beta.126.jar` | `85dbe1b41d98751029bb62e87d259522f8609758896c797b1e02fa7656ec0342` |
|
||||
|
||||
## Publication et synchronisation
|
||||
|
||||
[Release beta.126](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.126)
|
||||
publiée depuis `99860af312a1737d938812f3515fcc4acb4291b2` ; artefacts
|
||||
immuables retéléchargés et vérifiés. Canal packwiz : `31749b95d21ce5bced6b9e5759499da6f8a67e6e`.
|
||||
Deux synchronisations isolées, puis deux synchronisations de l'instance existante
|
||||
**Sanctuary Beta** ont réussi. Un seul JAR Sanctuary beta.126, correspondant
|
||||
à l'empreinte ci-dessus. Les **923 fichiers personnels** suivis sont conservés,
|
||||
sauvegardes restées fermées ; copie préalable dans
|
||||
`sanctuary-backups/before-beta.126/`.
|
||||
|
||||
Packs normal/Test et template copiés et vérifiés dans
|
||||
`sanctuary-beta/build/`. Reçus locaux : `build/gems126-artifact.json`,
|
||||
`build/gems126-isolated.json`, `build/gems126-prism.json` et
|
||||
`build/gems126-publication.log`.
|
||||
@@ -0,0 +1,109 @@
|
||||
# TOOL-01 — Clé dorée et états de blocs — beta.120
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/golden-wrench-beta120`.
|
||||
Le créateur valide les gestes du debug stick : clic gauche choisit la propriété,
|
||||
clic droit parcourt ses valeurs, Maj inverse le parcours. L'orientation est
|
||||
proposée en premier, puis les propriétés de forme et les autres états natifs.
|
||||
La sélection est personnelle, par type de bloc, pour la session courante.
|
||||
Une indication dans la barre d'action affiche la propriété et sa valeur.
|
||||
La clé ne casse pas le bloc, y compris en créatif, et ne remplace pas son type.
|
||||
|
||||
Les propriétés natives sont disponibles en survie avec la clé, sans condition
|
||||
opérateur, dans le respect des droits de construction, protections, portée et
|
||||
verrous de conteneurs. Comme avec le debug stick, les formes choisies restent
|
||||
en place immédiatement ; les mises à jour ultérieures du jeu peuvent recalculer
|
||||
les états dynamiques (redstone, connexions, cultures…).
|
||||
|
||||
Les multiblocs assemblés gardent leurs commandes d'ouverture/dissociation et
|
||||
leurs propriétés techniques ne sont pas éditées individuellement. Un assemblage
|
||||
complet garde priorité au clic droit ; Maj permet de régler les composants
|
||||
non assemblés. Hors d'un assemblage complet, même un four, un baril, un piston
|
||||
ou un établi reçoit l'action générique.
|
||||
|
||||
## Rotation de texture et contrat de stockage
|
||||
|
||||
La terre n'a pas d'orientation dans son blockstate vanilla. Une propriété
|
||||
supplémentaire de la clé, « Rotation des textures », parcourt 0°, 90°, 180° et
|
||||
270° sur les faces des modèles de blocs. La géométrie, l'éclairage, les teintes,
|
||||
les ressources actives, le bloc, son inventaire et son butin restent natifs.
|
||||
Les rendus spéciaux d'entités de blocs ne sont pas transformés par ce réglage
|
||||
des faces ; leur orientation demeure modifiable par leurs états natifs.
|
||||
|
||||
Nouvelle donnée facultative `sanctuary:wrench_textures_v1`, attachée au chunk :
|
||||
table immuable position entière → quart de tour (1 à 3). Une absence signifie
|
||||
0°. Elle n'est créée qu'après une action de clé ; revenir à 0° retire l'entrée,
|
||||
puis l'attachement s'il est vide. Aucun état vanilla ni format existant n'est
|
||||
modifié ; aucun parcours, conversion ou régénération des anciens mondes.
|
||||
Les modifications de blockstate du même type conservent la rotation. Casser,
|
||||
remplacer ou déplacer par piston retire la rotation à l'ancienne position ;
|
||||
les objets récupérés restent ordinaires. Les nouveaux clients la reçoivent
|
||||
après le chunk natif, par lots bornés de 1 024 entrées au maximum.
|
||||
Déconnexion, changement de dimension et déchargement libèrent le cache visuel.
|
||||
Revenir à une ancienne version du mod supprime seulement cet effet visuel ;
|
||||
les blocs conservent leurs identifiants, données et états natifs.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours `Wrench120ClientChecks` passe en **47 s** sur Minecraft 26.3,
|
||||
client macOS et serveur intégré, dans un monde plat jetable de graine 120 :
|
||||
|
||||
- **2 522 propriétés natives** parcourues, avec valeurs inverses et type conservé.
|
||||
- Vrais clics sur les escaliers : orientation, moitié supérieure et forme,
|
||||
sans casser le bloc en créatif ; gestes inverses et sélection distincte.
|
||||
Les doubles événements créatifs d'un même clic sont filtrés à la cadence
|
||||
native de cinq ticks.
|
||||
- Terre en survie : rotation réelle des UV émis, positions des sommets et nombre
|
||||
de faces identiques ; retour exact après quatre quarts de tour ou un inverse.
|
||||
- Rechargement des ressources conservant les UV tournés ; capture de terre relue.
|
||||
- Coffre tourné avec ses 23 diamants intacts, sans ouverture accidentelle.
|
||||
- Assemblage/dissociation par vrais clics des quatre familles : Métabli, Fût,
|
||||
Fourneau et super piston. Un clic gauche sur un assemblage ne modifie pas
|
||||
les numéros de composants ; barils et pistons isolés tournent normalement.
|
||||
- Refus en aventure, spectateur ou hors portée ; retrait des données visuelles
|
||||
après remplacement, maintien lors d'un changement d'état du même bloc.
|
||||
- Sauvegarde/reconnexion : rotation de terre côté serveur et client, état
|
||||
natif de l'escalier et inventaire du coffre conservés.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryWrench120ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `WRENCH120_PASS` et `WRENCH120_PROPERTIES 2522` dans
|
||||
`build/wrench120-client.log`, captures dans `build/wrench120-evidence/`.
|
||||
Aucun monde personnel ouvert ; pas de validation Windows ni depuis deux
|
||||
ordinateurs. Les rendus de faces natifs et le rechargement du pack sont vérifiés ;
|
||||
pas de validation spécifique d'un shader tiers.
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 12 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. La comparaison avec beta.119 limite les différences de
|
||||
production aux classes de clé/assemblage, aux deux nouveaux hooks, au rendu
|
||||
et aux libellés prévus ; toutes les autres ressources sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR Sanctuary et aucune sauvegarde
|
||||
ni fixture de test.
|
||||
|
||||
- Sanctuary-beta.120.mrpack : 10237973 octets, SHA-256
|
||||
`86b5e480a12ba20b5c1e308c77dbb54bdf8a56806915fb32ca2d16f9a72cc9c9`.
|
||||
- Sanctuary-Test-beta.120.mrpack : 10256895 octets, SHA-256
|
||||
`2e3818483ba7dfa6e5652b97933e105a44e7cc4b269fc567116bf895d54bd8d9`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`8e35ef0f134a46b7760604175ad768264946d31fdd0694982e049e7a928fde2a`.
|
||||
|
||||
Reçu local : `build/wrench120-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.120](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.120)
|
||||
est publiée depuis `248dff47b7a0ebd11026230581fe69641dbde76d`. Le tag exact et les
|
||||
artefacts sont immuables ; leurs téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `e8060764a444a58b0a17318a3821dfeb1a0c33c6`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.120 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés, sans
|
||||
ouvrir de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.120/` de l'instance existante. Reçus locaux :
|
||||
`build/wrench120-isolated.json` et `build/wrench120-prism.json`.
|
||||
@@ -0,0 +1,143 @@
|
||||
# beta.105 — Météo saisonnière, captures et chapeaux vivants
|
||||
|
||||
Branche `codex/living-hats-seasons-beta105`, Minecraft 26.3.
|
||||
|
||||
## Contrat avant modification
|
||||
|
||||
- Molette dans le fût : navigation entre pages, bornée, sans perdre les objets
|
||||
du curseur ni déplacer la souris au centre.
|
||||
- Panoramas : couleurs et brume du shader Sanctuary actif, six faces cohérentes,
|
||||
profondeur conservée et caméra restaurée même après erreur.
|
||||
- Real Time : probabilités liées aux saisons astronomiques du calendrier réel.
|
||||
Été surtout ensoleillé sans profil ciel gris ; automne plus humide ; hiver
|
||||
avec neige. Des journées claires restent possibles dans les quatre saisons.
|
||||
Le créateur demande explicitement flocons et accumulation naturelle au sol.
|
||||
Le tirage du jour déjà sauvegardé reste stable ; nouveaux tirages saisonniers
|
||||
les jours suivants, commandes opérateur disponibles. Aucune Weather TNT.
|
||||
- Mobs équipés : coups et boules de neige activent l’objet ; briquet, feu
|
||||
d’artifice propulsant le porteur, spawner natif configuré, paratonnerre attirant
|
||||
les éclairs. Les consommables et l’usure restent réels.
|
||||
- Cultures sur tête : croissance et récolte des produits/graines, sans duplication
|
||||
de la graine initiale au retrait. Les aliments attirent les espèces qui les
|
||||
mangent ; une carotte sur bâton guide les cochons vers le porteur.
|
||||
- Livre et plume : journal intime écrit en réaction aux caresses et blessures,
|
||||
voix différente selon les quatre personnalités ; souvenirs variés, pas de log
|
||||
technique. Pages existantes conservées et limites natives respectées.
|
||||
|
||||
## Contrat de données
|
||||
|
||||
Aucune sauvegarde personnelle n’est modifiée pendant les essais. Les objets et
|
||||
leur état continuent d’utiliser les données additives existantes de chapeau.
|
||||
Les journaux restent de véritables livres Minecraft récupérables. L’éventuel
|
||||
nouveau profil neige étend l’énumération météo : anciens états lisibles, aucune
|
||||
conversion automatique du jour courant. Un retour à une ancienne version après
|
||||
une journée neige nécessite d’abord de sélectionner un profil météo ancien.
|
||||
La neige peut modifier les blocs exposés pendant le jeu, selon la demande du
|
||||
créateur ; pas de régénération, d’expansion ni de changement de terrain au chargement.
|
||||
|
||||
## Utilisation
|
||||
|
||||
- Dans le fût, molette vers le bas : page suivante ; vers le haut : précédente.
|
||||
Le défilement concerne le coffre et sa colonne de contrôles. Un objet tenu au
|
||||
curseur bloque le changement de page, comme les boutons existants.
|
||||
- `/sanctuary weather forecast` annonce les sept prochains jours.
|
||||
`/sanctuary weather snow` et `snow_showers` sélectionnent respectivement neige
|
||||
continue et averses ; leurs équivalents `/weather` sont également disponibles.
|
||||
Le mode Vanilla conserve le cycle Minecraft.
|
||||
- Sur un mob équipé, clic droit à main vide : caresse du livre, activation du
|
||||
briquet/de la fusée, récolte d’une culture mûre. Coup ou boule de neige :
|
||||
activation. Maj + clic droit à main vide : récupération de l’objet.
|
||||
- Blé, carottes, pommes de terre, betteraves : croissance des cultures portées,
|
||||
récolte native et replantation. Les produits mûrs sont aussi rendus si l’on
|
||||
retire la graine. Les plantes décoratives et tiges de melon/citrouille ne
|
||||
deviennent pas des cultures de fruit mobiles.
|
||||
- Le spawner conserve l’espèce configurée et les contraintes Minecraft :
|
||||
proximité d’un joueur, espace, lumière et limite d’entités. Une activation
|
||||
lance une tentative immédiate ; elle ne garantit pas une apparition si
|
||||
ces conditions ne sont pas réunies.
|
||||
- Le paratonnerre attire les éclairs naturels sous un ciel dégagé à portée
|
||||
native, puis émet son impulsion redstone. Le briquet respecte la règle de modification du terrain par les mobs.
|
||||
La fusée consomme l’objet, utilise ses effets natifs et propulse le porteur.
|
||||
- Les aliments portés attirent les animaux qui les consomment dans un rayon
|
||||
de 10 blocs, avec abandon à 12 blocs ou sans visibilité. La carotte sur bâton
|
||||
guide ainsi les cochons. Priorités de fuite et de reproduction conservées.
|
||||
- Le journal reprend la personnalité du familier ; celle d’un animal ordinaire
|
||||
est déterminée de façon stable par son identité et son espèce. FR/EN selon
|
||||
la langue du premier lecteur-interacteur. Les pages précédentes sont
|
||||
conservées (limite native 100 pages), mais ne sont pas diffusées dans le
|
||||
paquet d’apparence du chapeau.
|
||||
|
||||
## Calendrier et périmètre
|
||||
|
||||
Longitude solaire apparente calculée localement à partir des formules publiques
|
||||
[NOAA Solar Calculator](https://gml.noaa.gov/grad/solcalc/), avec contrôle des
|
||||
quatre passages 2026 contre les horaires de l’[US Naval Observatory](https://aa.usno.navy.mil/data/Earth_Seasons).
|
||||
Le tirage est journalier : le jour civil d’un équinoxe/solstice adopte la nouvelle
|
||||
saison, selon le fuseau Real Time. L’hémisphère sud inverse les saisons via la
|
||||
latitude solaire configurée. Il ne s’agit pas de la météo réelle d’une ville.
|
||||
Les pondérations de `weather.json` restent la base, modulée par la saison.
|
||||
Sur 10 000 graines par saison : journées claires 64,41 % / 94,79 % / 40,25 % /
|
||||
49,05 % (printemps/été/automne/hiver) avec la configuration par défaut.
|
||||
|
||||
La neige passe par les précipitations natives des chunks chargés, respecte
|
||||
la limite native d’accumulation, la lumière et les supports Minecraft. Les biomes
|
||||
sans précipitations restent secs. Les chaudrons reçoivent aussi de la neige.
|
||||
La neige déposée reste un bloc natif : pas de balayage saisonnier supprimant
|
||||
les blocs des constructions. Sa fonte suit les règles Minecraft.
|
||||
Les captures intègrent le post-traitement et la brume Sanctuary ; aucune
|
||||
compatibilité avec un moteur externe Iris/Canvas/Oculus n’est déclarée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- `seasonal105Smoke` : 40 000 tirages, stabilité par graine/jour,
|
||||
équinoxes/solstices 2026 à ± une heure, inversion sud, proportions saisonnières,
|
||||
persistance d’une journée de neige et passage au lendemain.
|
||||
- `Living105ClientChecks` : client natif Minecraft 26.3 avec serveur intégré,
|
||||
nouveau monde plat jetable. Journal avec page préalable, caresse/blessure,
|
||||
récupération, récolte/replantation/retrait, vache attirée par le blé, cochon
|
||||
guidé, vraie méthode d’impact de boule de neige, feu/usure, vol d’une vache,
|
||||
apparition du spawner configuré, éclair/impulsion/extinction du paratonnerre,
|
||||
neige déposée par `tickPrecipitation`, requête de flocons côté client,
|
||||
molette suivant/précédent/bornes et deux panoramas complets de six faces.
|
||||
- Pour chaque face à saturation 0, contrôle des pixels désaturés ; comparaison
|
||||
à saturation 200, présence de `sanctuary:vanilla_light` et profondeur du sol
|
||||
conservée. Les vues sont soumises séparément au GPU pour éviter de réutiliser
|
||||
les tampons natifs avant leur soumission. Aucun échec de post-traitement dans
|
||||
l’essai final (53 secondes).
|
||||
|
||||
Commande de reproduction :
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/chemin/vers/jdk25 ./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryLiving105ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Le délai serveur de trois ticks entre changements de page est supprimé : les
|
||||
paquets sont toujours validés contre l’identifiant du menu ouvert et son curseur.
|
||||
Les anciennes pages ne peuvent donc pas accepter de clics après remplacement.
|
||||
Le client et le serveur doivent utiliser ensemble beta.105 pour les nouveaux
|
||||
profils réseau de neige. Les anciens états enregistrés restent lisibles.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussi
|
||||
sur la copie isolée `build/release-beta105` : 2 min 43 s, 125 tâches.
|
||||
Le test dédié est exclu conformément au refus antérieur de son EULA ; le test
|
||||
natif de cette livraison utilise un serveur intégré dans un monde neuf jetable.
|
||||
|
||||
La copie part des sources et ressources vérifiées beta.104, auxquelles sont
|
||||
appliqués les 25 fichiers Java concernés, les libellés météo FR/EN et les mixins.
|
||||
Elle évite d’embarquer les modifications du chantier Statuaire beta.106 arrivé
|
||||
en parallèle dans le dossier partagé. Le travail beta.106 est conservé.
|
||||
`build/beta105-source-manifest.json` décrit les sources de cette compilation.
|
||||
|
||||
Les sources JAR correspondent à cette copie ; les ressources du pack beta.104
|
||||
restent identiques sauf les deux fichiers de langue. JAR sans classes de test,
|
||||
archives et métadonnées 26.3 vérifiées ; archives beta.104 inchangées. Rapport :
|
||||
`build/beta105-artifact.json`. Aucun déploiement ni publication du canal.
|
||||
|
||||
- [Sanctuary-beta.105.mrpack](../build/Sanctuary-beta.105.mrpack), 10083681 octets.
|
||||
SHA-256 : `396c31ad80b4826e5cc0530001e90e498dfb3d36f951cb46d01b32870bf3212a`.
|
||||
- [Sanctuary-Test-beta.105.mrpack](../build/Sanctuary-Test-beta.105.mrpack), 10102603 octets.
|
||||
SHA-256 : `6398eb413ea45cf383b71d5be07cb1eef6995158fe0909b06deabb36c1623ce4`.
|
||||
@@ -0,0 +1,104 @@
|
||||
# beta.096 — Métabli et commandes de construction
|
||||
|
||||
Le parcours créatif est complété depuis [beta.098](creative-construction-beta098.md)
|
||||
par un bouton de construction immédiate. Le présent document décrit beta.096.
|
||||
|
||||
## Contrat de monde avant implémentation
|
||||
|
||||
Neuf établis à plat, en carré 3 × 3, sont assemblés volontairement à la clé
|
||||
avec la confirmation habituelle. Aucun établi existant n'est converti au
|
||||
chargement. Les neuf composants deviennent `sanctuary:metatable`, propriété
|
||||
`part` de 0 à 8 (x + 3z). Leur origine est déduite de cette propriété.
|
||||
Ces états sont stockés normalement dans les chunks ; aucun SavedData ancien
|
||||
n'est modifié, aucun inventaire ou contenu n'est copié. Pas de BlockEntity.
|
||||
Le contrôle des neuf cases est borné et n'impose pas de chargement de chunk.
|
||||
|
||||
Une dissociation restaure neuf établis. Casser un composant dépose son établi
|
||||
selon le butin natif et restaure les autres sans drops supplémentaires.
|
||||
Les composants déchargés sont contrôlés à leur reprise. Une machine partielle
|
||||
est inaccessible jusqu'à validation/restauration. Les pistons ne déplacent pas
|
||||
un Métabli assemblé. Les permissions serveur s'appliquent à chaque composant.
|
||||
|
||||
Les deux PNG fournis sont conservés octet pour octet : dessus 48 × 48,
|
||||
côtés 48 × 16, prélevés par tiers sans étirement. Le dessous utilise les
|
||||
planches de chêne du pack actif.
|
||||
|
||||
## Parcours retenu
|
||||
|
||||
Clic droit au Métabli → catalogue → choix unique du projet → retour dans
|
||||
le monde. K ouvre les commandes du projet : ancrer au point visé, décaler,
|
||||
tourner, afficher une couche, masquer/afficher et consulter les besoins.
|
||||
Changer de projet, créer une autre statue, importer ou abandonner le plan
|
||||
se fait au Métabli. Esc ferme seulement l'interface et ne retire pas le plan.
|
||||
Un import ou une génération en cours n'est accepté que si le joueur est encore
|
||||
à l'atelier qui a initié le choix. Le choix ne pose aucun bloc.
|
||||
|
||||
Construction manuelle avec l'inventaire natif. Aucun collage ni chantier
|
||||
automatique dans ce parcours, même en créatif. Les fonctions serveur de collage
|
||||
créatif historiques restent contrôlées par leurs règles existantes.
|
||||
Le projet et son aperçu restent locaux au joueur, comme dans beta.072 ; ils
|
||||
sont effacés au changement de monde/dimension ou à la déconnexion. L'export
|
||||
reste disponible pour les conserver. Le chantier partagé persistant, les
|
||||
catalogues en datapack serveur et les services commerciaux sont des lots futurs.
|
||||
Aucune économie n'est ajoutée.
|
||||
|
||||
## Catalogue initial et extension
|
||||
|
||||
- Statues : les modèles natifs déjà pris en charge, avec hauteur et palette.
|
||||
- Machines : Fourneau, Fût, super piston normal et gluant. Le plan guide les
|
||||
blocs sources ; la clé réalise ensuite leur assemblage fonctionnel.
|
||||
- Bâtiments : abri de 5 × 5 et passerelle de 3 × 7, deux plans simples de départ.
|
||||
- Mes plans : bibliothèque locale et imports existants `.litematic`, `.schem`
|
||||
et `.nbt`. Les anciens `.schematic` demandent une conversion externe.
|
||||
|
||||
`ConstructionCatalog.ENTRIES` est le point d’extension des plans intégrés :
|
||||
identifiant, catégorie et fabrique de `BuildPlan`, avec nom et aide FR/EN.
|
||||
Une machine fonctionnelle conserve son implémentation serveur propre : un plan
|
||||
ne lui confère ni inventaire prérempli ni commandes ni service commercial.
|
||||
Le catalogue configurable par datapack reste à implémenter.
|
||||
|
||||
## Utilisation
|
||||
|
||||
Poser neuf établis en carré à plat. Deux clics à la clé dorée confirment le
|
||||
Métabli ; clic droit pour l’ouvrir. Choisir un plan ferme le catalogue.
|
||||
K ouvre les commandes de construction, avec aperçu, décalage d’un bloc,
|
||||
rotation par quart de tour, couches et matériaux restants. Poser les blocs
|
||||
normalement ; revenir au Métabli pour changer de choix ou abandonner.
|
||||
Maj + clic droit à la clé dissocie l’atelier.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Deux suites natives sur Minecraft 26.3, en nouveaux mondes de développement :
|
||||
|
||||
- `Metatable096ClientChecks`, graine 96 : assemblage en deux clics sur quatre
|
||||
chunks, ouverture par clic droit natif, quatre rubriques, sélection unique,
|
||||
K réservé aux réglages, refus de changement/import à distance, rotation et
|
||||
translation, pose native payée, annulation d’une génération en quittant
|
||||
l’atelier, statue puis export/import, interfaces FR/EN, casse pendant le menu,
|
||||
un seul drop et huit établis restaurés, réassemblage, déchargement réel des
|
||||
chunks puis retour, dissociation des neuf cases. Réussi en 1 min 11 s.
|
||||
- Régression `Plans072ClientChecks` adaptée au Métabli : capture des **88 modèles**
|
||||
sans refus, statue de 32 blocs, rotations, matériaux, export, ancien protocole
|
||||
de collage créatif et refus en survie, pose native consommant son bloc,
|
||||
interfaces FR/EN. Réussi en 1 min 7 s.
|
||||
|
||||
Captures inspectées dans `build/metabli096-evidence/`. Le premier passage du
|
||||
nouveau test attendait le déchargement via `ClientLevel.hasChunkAt`, qui ne
|
||||
reflète pas le cache client ; le test utilise désormais `getChunkNow`, comme
|
||||
les autres essais natifs. Le parcours fonctionnel précédant cette attente
|
||||
avait déjà réussi. Ce premier journal est conservé séparément.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussi : 124 tâches, 2 min 20 s. Voir `build/metabli096-check.log` et le reçu
|
||||
`build/metabli096-artifact.json`. Textures d’origine comparées octet pour octet,
|
||||
modèles découpés par tiers, JAR sources et archives vérifiés ; archives beta.095
|
||||
conservées. Les tests dédiés sont exclus conformément au refus antérieur d’EULA.
|
||||
Aucun serveur dédié démarré, aucune EULA acceptée, aucun déploiement personnel.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
Packs normal et Test `build/Sanctuary-beta.096.mrpack` et
|
||||
`build/Sanctuary-Test-beta.096.mrpack`, Minecraft 26.3 finale.
|
||||
Le canal packwiz et l’instance Prism ne sont pas modifiés. Les dépendances et
|
||||
le pack de textures intégré beta.090 restent identiques ; les textures du
|
||||
Métabli sont les ressources propres du mod.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Métabli — atelier de construction et catalogue extensible
|
||||
|
||||
Statut : **conception de référence**, le 16 septembre 2026.
|
||||
Le catalogue, les commandes K et l’exemple généré évoluent dans
|
||||
[beta.099](catalogue-progression-beta099.md).
|
||||
Le premier lot est livré dans [beta.096](metabli-beta096.md), branche
|
||||
`codex/metabli-beta096` : multibloc, catalogue et commandes de construction.
|
||||
Les chantiers partagés persistants, datapacks de catalogue et services décrits
|
||||
ci-dessous restent des propositions pour les lots suivants. Le contrat beta.096
|
||||
précise le fonctionnement effectivement implémenté.
|
||||
|
||||
## Direction demandée
|
||||
|
||||
Un multibloc posé dans le monde ouvre le menu de construction. Aucun bouton
|
||||
supplémentaire dans le menu pause. Le système réunit statues, machines et
|
||||
bâtiments, et doit pouvoir accueillir de nouveaux contenus et usages.
|
||||
« Métabli » reprend ici le nom du nouveau message ; le catalogue historique
|
||||
employait « Métablit ». Aucun identifiant publié n'est renommé.
|
||||
|
||||
## Ce que le jeu possède déjà
|
||||
|
||||
- `PlansScreen` propose Statue, Bibliothèque et Construction via K.
|
||||
- `StatueVoxelizer` et la capture cliente produisent des statues à partir des
|
||||
modèles et textures du jeu ; `BuildPlan` porte les blocs et leurs états.
|
||||
- `PlanClient` sait ancrer un aperçu, le tourner, isoler une couche et compter
|
||||
les blocs corrects, restants et en conflit. Cet état est actuellement local.
|
||||
- `SchematicIO` lit les formats pris en charge dans la bibliothèque ; l'import
|
||||
ne reprend pas les inventaires, scripts ou entités des fichiers tiers.
|
||||
- `PlanService` autorise le collage uniquement en créatif. En survie, le joueur
|
||||
pose aujourd'hui ses blocs lui-même : aucun moteur de chantier automatique.
|
||||
- La clé dorée et les machines existantes donnent un précédent pour l'assemblage
|
||||
volontaire, la dissociation et l'autorité serveur.
|
||||
|
||||
Références : [plans existants](statues-plans-beta072.md),
|
||||
[multiblocs existants](multiblocs-beta084.md),
|
||||
[catalogue historique](multiblocs-conception.md).
|
||||
|
||||
## Le Métabli dans le monde
|
||||
|
||||
Proposition de forme : **neuf établis à plat, en 3 × 3 sur un bloc de hauteur**,
|
||||
assemblés volontairement avec la clé dorée. Forme et apparence à confirmer
|
||||
avant le code ; ne pas imposer les 27 composants du Fourneau à cette machine.
|
||||
Un clic droit sur un composant assemblé ouvre le même atelier.
|
||||
|
||||
Le Métabli sert à choisir, préparer et gérer un projet. La construction est
|
||||
ancrée à un autre emplacement choisi dans le monde ; elle n'a pas à occuper
|
||||
la place de l'atelier. L'aperçu indique clairement sa base et son orientation.
|
||||
Proposition initiale : un chantier actif par Métabli, plusieurs participants.
|
||||
|
||||
K reste disponible pour l'aperçu et les réglages du plan en cours. Le premier
|
||||
lot ne supprime pas les outils locaux déjà livrés ; le nouveau catalogue et
|
||||
la gestion du chantier partagé sont accessibles depuis le Métabli.
|
||||
|
||||
## Un menu commun
|
||||
|
||||
Interface native et sobre : catégories en haut, liste et recherche à gauche,
|
||||
aperçu et fiche du projet à droite, actions en bas. Les catégories n'ouvrent
|
||||
pas quatre nouveaux menus indépendants.
|
||||
|
||||
| Catégorie | Contenu | Réglages utiles |
|
||||
| --- | --- | --- |
|
||||
| Statues | Générateur actuel de mobs ; autres modèles ultérieurement | Modèle, taille, palette |
|
||||
| Machines | Fourneau, Fût, super pistons ; plans de circuits redstone validés | Orientation, variante, entrées et sorties |
|
||||
| Bâtiments | Maisons, ateliers, ponts et autres plans ajoutés au catalogue | Dimensions, variantes de matériaux compatibles |
|
||||
| Mes plans | Bibliothèque locale et imports actuels | Nom, dimensions, matériaux, rotation |
|
||||
|
||||
Une fiche affiche la fonction réelle, l'encombrement, les matériaux requis
|
||||
et manquants, ainsi que la condition de déblocage éventuelle. Les exemples de
|
||||
bâtiments et de circuits ci-dessus sont des contenus à créer, pas un catalogue
|
||||
déjà présent. Une fonction absente du jeu n'est pas présentée comme disponible.
|
||||
|
||||
Parcours proposé : choisir → configurer → positionner l'aperçu → confirmer
|
||||
l'emplacement → construire → utiliser. Une fiche de chantier conserve
|
||||
l'avancement et les besoins en matériaux. Le mode retenu par le créateur est la **construction manuelle guidée** :
|
||||
les joueurs apportent leurs matériaux et posent eux-mêmes les blocs.
|
||||
|
||||
## Mode retenu : construction manuelle guidée
|
||||
|
||||
Décision confirmée par le créateur le 16 septembre 2026. Le Métabli ne place
|
||||
pas les blocs et ne prélève pas les objets. La pose, les outils, les matériaux
|
||||
et les règles de voisinage restent ceux du jeu. Aucun moteur automatique,
|
||||
réserve de chantier, délai de fabrication ou consommation différée à créer.
|
||||
|
||||
Réutiliser l'aperçu existant : les blocs terminés s'effacent de la projection,
|
||||
les conflits sont signalés et les couches permettent de suivre un grand plan.
|
||||
Le chantier commun pourra partager l'ancrage et la progression ; chaque joueur
|
||||
contribue avec son propre inventaire. Le contrôle de droits sur la construction
|
||||
elle-même reste celui du monde, indépendamment des droits d'édition du plan.
|
||||
|
||||
La fiche distingue **blocs à placer** et **objets nécessaires**. Le compteur
|
||||
actuel travaille sur les états de blocs ; annoncer une liste exacte d'objets
|
||||
exige d'adapter les portes et lits (un objet, deux cases), les dalles doubles
|
||||
(deux objets) et les autres cas particuliers. Aucun de ces calculs ne peut
|
||||
modifier ou doubler la consommation native du joueur.
|
||||
|
||||
## Une construction a un plan et, parfois, une fonction
|
||||
|
||||
Le **plan** décrit la forme : blocs, états, variantes et points de raccordement.
|
||||
La **fonction** décrit un comportement éventuellement ajouté par Sanctuary.
|
||||
|
||||
- Une statue ou une maison ordinaire a seulement besoin de son plan.
|
||||
- Un circuit redstone fonctionne grâce aux blocs réellement construits.
|
||||
- Un Fourneau doit en plus être validé et assemblé par le système existant.
|
||||
- Une boutique aura besoin d'un composant commercial : propriétaire, stock,
|
||||
offres et échanges. Construire son bâtiment ne crée pas ce service à lui seul.
|
||||
|
||||
Pour les machines reconnues, le plan complet et les permissions sont vérifiés
|
||||
avant l'assemblage final ; un simple import de blocs ne peut pas s'attribuer une
|
||||
fonction spéciale. Les ports indiqués dans le plan tournent avec la structure
|
||||
(entrée d'objets, sortie, commande redstone, face d'interaction).
|
||||
|
||||
La demande de boutique donne un point d'extension, **pas une livraison de
|
||||
l'économie maintenant**. On pourra préparer un local de boutique et lui
|
||||
rattacher le service lorsque le commerce sera défini et implémenté. Son futur
|
||||
signal redstone pourra par exemple indiquer un stock disponible ou une vente,
|
||||
sans confondre le signal avec l'opération d'échange elle-même.
|
||||
|
||||
## Étendre le catalogue sans refaire l'interface
|
||||
|
||||
Proposition : des définitions de catalogue en datapack serveur, avec des
|
||||
identifiants stables et une version de définition. Une entrée comporte :
|
||||
|
||||
- nom et description FR/EN, catégorie et tags ;
|
||||
- source du plan ou générateur connu, paramètres et variantes autorisées ;
|
||||
- condition de déblocage vérifiable côté serveur ;
|
||||
- matériaux et opérations de placement, calculés depuis la variante retenue ;
|
||||
- ports de raccordement et type de fonction facultatif.
|
||||
|
||||
Ajouter un bâtiment ou un circuit utilisant les mécanismes existants revient
|
||||
à ajouter un plan et sa fiche. Une nouvelle mécanique, comme le commerce ou
|
||||
une machine inédite, demande un module de code serveur enregistré, puis une
|
||||
fiche qui l'utilise. Un datapack ne devient pas un interpréteur de commandes
|
||||
arbitraires et un fichier importé ne fournit pas son propre code exécutable.
|
||||
|
||||
Les générateurs de statues et les fichiers locaux deviennent deux fournisseurs
|
||||
de plans parmi d'autres. Les catégories servent à naviguer ; elles ne dictent
|
||||
pas le moteur de placement. Découvertes, advancements ou aptitudes pourront
|
||||
débloquer des entrées, sans supposer que ces règles existent déjà pour les plans.
|
||||
|
||||
## Persistance et reprise à définir avant le code
|
||||
|
||||
Le registre actuel des multiblocs décrit un cube de 27 cases avec un booléen
|
||||
Fourneau/Fût. Il n'est pas extensible à un Métabli par ajout d'une troisième
|
||||
valeur. Prévoir un registre dédié pour cette nouvelle machine et ses chantiers,
|
||||
sans modifier le schéma existant ; un contrôle commun empêchera de partager
|
||||
un composant avec les anciennes machines.
|
||||
|
||||
Le futur contrat versionné devra fixer l'identité du Métabli et du projet,
|
||||
la dimension, l'ancrage, l'orientation, les droits d'édition et la reprise
|
||||
du suivi. Les matériaux restent dans les inventaires Minecraft ; aucun stock
|
||||
dupliqué n'appartient au plan. Conserver une copie bornée du plan
|
||||
validé et de sa révision évite qu'une mise à jour du catalogue transforme un
|
||||
chantier en cours ou un bâtiment déjà posé. Les limites actuelles de `BuildPlan`
|
||||
restent le point de départ, pas une promesse de gros plans sans limite.
|
||||
|
||||
Proposition de règles : aucune transformation automatique des établis existants ;
|
||||
la projection n'écrase jamais le terrain ; suivi suspendu hors chunks chargés,
|
||||
sans forcer leur chargement. Une casse du Métabli ou une annulation du projet
|
||||
ne supprime aucun bloc déjà posé et ne restitue pas de matériaux déjà utilisés.
|
||||
Les projets complets ne déclenchent pas une deuxième production.
|
||||
L'activation d'une machine reconnue reste une action explicite, après contrôle
|
||||
serveur, selon les règles de la clé. Le suivi partagé doit persister le plan,
|
||||
pas une copie ancienne du monde à rejouer après reconnexion.
|
||||
|
||||
Ces règles et ce registre sont un cadrage, **pas une migration approuvée ni un
|
||||
format livré**. La forme du Métabli, les droits de gestion, le rayon d'ancrage
|
||||
et les déblocages restent à fixer avant le lot concerné.
|
||||
|
||||
## Lots proposés
|
||||
|
||||
1. Métabli ouvrable, interface commune, statues et bibliothèque actuelles,
|
||||
sélection d'un plan et projection à l'emplacement voulu. Aucun bouton pause.
|
||||
2. Chantier manuel partagé et persistant ; liste correcte des matériaux,
|
||||
synchronisation du suivi et vérifications de reprise/annulation.
|
||||
3. Catalogue initial de machines et bâtiments validés, assemblage final des
|
||||
machines existantes et protocole d'ajout de contenu en datapack.
|
||||
4. Modules de fonction supplémentaires lorsque leur gameplay est défini,
|
||||
notamment le commerce ; hors premier lot.
|
||||
|
||||
Chaque lot devra avoir un résultat jouable, un contrat de sauvegarde s'il en
|
||||
crée un, les libellés FR/EN et les vérifications client/serveur adaptées.
|
||||
La validation du chantier manuel devra notamment couvrir deux joueurs, une
|
||||
pose qui consomme réellement l'objet natif, un obstacle ajouté pendant la
|
||||
construction, une porte/un lit, un circuit, le déchargement, la reconnexion et
|
||||
la casse du Métabli sans disparition des blocs construits.
|
||||
@@ -0,0 +1,209 @@
|
||||
# WATER-144 — Eau naturelle pixélisée
|
||||
|
||||
Branche `codex/water-beta144`, socle beta.143 publiée (`97ff2b5`) et suivi
|
||||
`580b241`. Minecraft 26.3, Java 25, dépendances conservées.
|
||||
|
||||
## Contrat
|
||||
|
||||
**Options → Shaders → Eau naturelle** est activée par défaut avec Sanctuary.
|
||||
L’intensité (0–100 %, défaut 100 %) agit sur la surface et l’immersion.
|
||||
L’interrupteur ou 0 % retrouve le rendu précédent. Les préférences existantes
|
||||
restent conservées ; un moteur externe suspend l’effet comme les autres effets
|
||||
natifs Sanctuary.
|
||||
|
||||
La couleur native du biome reste la base, y compris les variantes d’océans.
|
||||
Les océans (`minecraft:is_ocean`) imposent le profil clair.
|
||||
Hors océan, le premier support situé au maximum huit blocs sous la cellule
|
||||
observée choisit le profil :
|
||||
|
||||
| Profil | Fond | Mélange olive #788448 | Alpha natif | Distance de brume native |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| Claire | Pierre, matériaux construits, inconnus | 0 % | ×0,55 | ×1 |
|
||||
| Légèrement teintée | Sable, sable rouge, gravier, argile | 10 % | ×0,80 | ×0,70 |
|
||||
| Étang | Terre, terre stérile/enracinée, herbe, podzol, mycélium, boue, racines de palétuvier boueuses | 85 % | ×1 | ×0,40 |
|
||||
|
||||
L’eau et les plantes aquatiques sans collision sont traversées. Un support
|
||||
immergé reste un fond ; absence de support, air ou données manquantes donnent
|
||||
une eau claire. La référence visuelle fournie pendant l’implémentation demande un véritable
|
||||
vert olive légèrement jaunâtre : le profil étang passe donc de 45 % à 85 %
|
||||
de mélange, tout en laissant les eaux claires et océaniques natives.
|
||||
Les surfaces profondes sans fond dans la limite restent donc
|
||||
claires. Les profils se mélangent aux sommets sur un bloc, sans filtrer les
|
||||
textures. Aux bords océaniques, les sommets partagés restent clairs et la
|
||||
transition occupe le bloc voisin hors océan. L’eau sous un toit suit son fond,
|
||||
pas l’éclairage du bâtiment.
|
||||
|
||||
La ressource client `assets/sanctuary/water_materials.json` accepte deux listes
|
||||
d’identifiants de blocs, `light` et `pond`, remplaçables par un resource pack.
|
||||
Les blocs absents des listes sont clairs. Une ressource invalide est signalée
|
||||
et reprend les familles embarquées. Le template personnel inclut cette ressource.
|
||||
|
||||
## Rendu et coût
|
||||
|
||||
Modification des seules couleurs/alpha des sommets de `FluidRenderer`, sans
|
||||
passe écran ni duplication de maillage. Textures, UV, animation, éclairage
|
||||
et tri des transparences restent natifs. Les calculs de profils sont différés
|
||||
jusqu’à l’émission d’un sommet visible, puis mutualisés dans la région compilée.
|
||||
Un test de palette élimine les sections sans eau avant toute copie de biomes.
|
||||
Les états de blocs viennent des copies natives ; les appartenances océaniques
|
||||
sont capturées avant l’envoi au worker. La présence du terrain est vérifiée dans le cache client avec chargement
|
||||
désactivé, sans utiliser `ClientLevel.hasChunk` (toujours vrai). Aucun
|
||||
chargement de chunk n’est demandé par le classificateur.
|
||||
|
||||
Un changement de bloc invalide la zone dépendante, huit blocs au-dessus et
|
||||
un bloc horizontalement. Les reconstructions natives recopient leurs données ;
|
||||
l’effet ne déclenche aucune reconstruction pour une lumière mobile.
|
||||
Un changement d’option reconstruit seulement les sections chargées contenant de
|
||||
l’eau ; les caches des autres effets restent disponibles. Le cache temporaire
|
||||
disparaît avec sa compilation, sans historique de monde.
|
||||
|
||||
Sous l’eau, le profil est échantillonné au maximum une fois par tick, avec
|
||||
transition de 0,5 seconde. La couleur et les distances environnementales sont
|
||||
adaptées dans le rendu natif du brouillard. La teinte finale est appliquée après
|
||||
la normalisation de la vision aquatique, pour éviter de transformer l’olive en
|
||||
jaune fluorescent ; la luminosité issue des modificateurs natifs de vision
|
||||
et le bonus du compagnon calmar sont conservés. Lave et brume atmosphérique ne changent
|
||||
pas. Il n’y a pas de simulation d’absorption volumétrique ni de reflet ajouté.
|
||||
|
||||
## Validation native
|
||||
|
||||
Les essais utilisent un monde plat jetable, graine 122, avec bassins sur quartz,
|
||||
sable et boue, support du sable, plantes aquatiques, toit, frontière de sections
|
||||
et changement de biome océan. Une copie personnelle de ressource remplace
|
||||
réellement les familles de matériaux puis est désactivée.
|
||||
|
||||
Les suites graphiques finales OpenGL et Vulkan sont réussies le 18 septembre
|
||||
2026 (`WATER144_PASS` et `WATER144_EXTRAS_PASS`). Elles couvrent surface,
|
||||
immersion, fond modifié, priorité océan, toit, plantes, cascades, lave inchangée,
|
||||
blocs gorgés d’eau, préférences FR/EN et rechargement des ressources/chunks.
|
||||
Les sommets partagés aux limites océaniques et le repli clair lorsque le cache
|
||||
client ne contient pas le terrain font l’objet d’assertions supplémentaires.
|
||||
Aucun autre client de test concurrent n’a été détecté pendant ces séries.
|
||||
Deux passages ciblés supplémentaires (`WATER144_TEXTURE_PASS`), Vulkan en
|
||||
1 min 18 s et OpenGL en 1 min 4 s, chargent de vraies textures personnelles
|
||||
32×32 à deux images animées pour l’eau calme et courante. Grille, animation
|
||||
visible et retour aux textures natives sont vérifiés après désactivation.
|
||||
Les fixtures, leurs alpha et les captures restent dans les preuves locales.
|
||||
|
||||
### Protocole de performance
|
||||
|
||||
MacBookPro17,1, Apple M1, 8 Gio, macOS 26.5.1 (25F80), Java 25,
|
||||
fenêtre logique 854×480, cible Retina **1708×960 pixels**, distance de rendu 6 chunks,
|
||||
ombres 32 blocs / 16 pixels, PBR 50 %, autres effets activés.
|
||||
VSync désactivée, limite 260 (illimitée), limitation AFK désactivée.
|
||||
Le mod de test vérifie le limiteur effectif et journalise le mode de présentation.
|
||||
Pendant les échantillons uniquement, il omet l’attente artificielle de
|
||||
`FramerateLimiter.limitDisplayFPS`, afin de mesurer sans temporisation
|
||||
applicative. Cette sonde reste absente du JAR de production. Les temps couvrent
|
||||
la frame native, présentation comprise, et ne sont pas des temps GPU isolés.
|
||||
Pour chaque scène : OFF/ON/ON/OFF/ON/OFF/OFF/ON, dix secondes par passage,
|
||||
avec stabilisation des sections puis quatre secondes de chauffe entre passages.
|
||||
Le déplacement avance de 120 blocs et laisse les sections se compiler
|
||||
naturellement, sans vidange artificielle des caches des autres effets.
|
||||
La scène de déplacement prolonge le bassin hors de la vue initiale : son terrain
|
||||
est préparé côté serveur de test, puis découvert normalement par le client.
|
||||
Le vérificateur exige des compilations réelles dans chaque passage en mouvement.
|
||||
Les moyennes des quatre médianes et P95 par état sont comparées avec les seuils
|
||||
+5 % et +2 ms. Les temps de compilation de sections sont consignés également.
|
||||
|
||||
Les essais courts initiaux, les exécutions concurrentes et les vérifications
|
||||
incomplètes du limiteur restent conservés comme preuves de développement ;
|
||||
ils ne fondent pas ces résultats. Les séries finales ont été exécutées après
|
||||
la fin du chantier SSR. Les variations négatives sont du bruit de mesure,
|
||||
pas une promesse d’accélération. Vulkan reste proche de 120 images/s dans
|
||||
cette chaîne de présentation, malgré le mode `IMMEDIATE` et l’absence de
|
||||
limiteur applicatif : les conclusions sont limitées à la machine et aux
|
||||
conditions ci-dessus.
|
||||
|
||||
| Moteur / scène | Médiane OFF → ON (ms) | Variation | P95 OFF → ON (ms) | Δ P95 (ms) |
|
||||
| --- | --- | ---: | --- | ---: |
|
||||
| vulkan / mixed | 8.069 → 8.092 | +0.29 % | 9.091 → 9.101 | +0.010 |
|
||||
| vulkan / large | 8.078 → 8.062 | -0.20 % | 9.126 → 9.114 | -0.012 |
|
||||
| vulkan / moving | 8.123 → 8.103 | -0.24 % | 9.103 → 9.080 | -0.023 |
|
||||
| opengl / mixed | 4.545 → 4.407 | -3.02 % | 7.080 → 6.733 | -0.347 |
|
||||
| opengl / large | 4.879 → 5.110 | +4.73 % | 7.519 → 7.767 | +0.248 |
|
||||
| opengl / moving | 6.817 → 6.338 | -7.02 % | 10.618 → 9.759 | -0.859 |
|
||||
|
||||
Les six comparaisons respectent les seuils : médiane ≤ +5 %, P95 ≤ +2 ms.
|
||||
Le cas limitant est la grande surface OpenGL (+4,73 %, +0,248 ms).
|
||||
Les compilations de sections sont mesurées sur le parcours en mouvement :
|
||||
|
||||
| Moteur | Sections OFF / ON | Médiane OFF → ON (ms) | P95 OFF → ON (ms) |
|
||||
| --- | ---: | --- | --- |
|
||||
| vulkan | 222 / 220 | 1.889 → 2.186 | 4.307 → 4.898 |
|
||||
| opengl | 222 / 220 | 1.528 → 1.540 | 5.492 → 5.227 |
|
||||
|
||||
Pour reproduire, avec Java 25 et un environnement graphique disponible :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryWater144ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan > build/water144-vulkan.log 2>&1
|
||||
python3 scripts/check_water_performance.py build/water144-vulkan.log
|
||||
```
|
||||
|
||||
Répéter avec `opengl`, sans autre client graphique simultané. Le vérificateur
|
||||
refuse les séries incomplètes et les dépassements ; il écrit le reçu détaillé
|
||||
près du journal. Le marqueur `WATER144_PASS` atteste les contrôles fonctionnels.
|
||||
|
||||
### Assemblage et publication
|
||||
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. La commande de livraison est `./gradlew check build assemblePack
|
||||
assembleTestPack -x :sanctuary:runGameTest` ; la suite complète non exclue
|
||||
n’est pas déclarée réussie. Les contrôles natifs de l’eau utilisent le serveur
|
||||
intégré d’un monde jetable. Aucun essai Windows n’est revendiqué.
|
||||
|
||||
L’assemblage avec l’exclusion ci-dessus a réussi en 2 min 33 s (126 tâches).
|
||||
`python3 scripts/pack.py check` réussit. Les cinq archives passent le contrôle
|
||||
ZIP ; les deux MRpack embarquent exactement le même JAR Sanctuary et déclarent
|
||||
Fabric API 0.160.5+26.3 avec l’empreinte du manifeste source. L’export MRpack est
|
||||
exécuté depuis chaque dossier de staging packwiz. Les dépendances imbriquées
|
||||
Demeure et JEI sont vérifiées par l’assemblage natif du dépôt.
|
||||
|
||||
[Release beta.144](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144) publiée depuis
|
||||
le commit source `83c377acc1b5a4d4137d7a93c397531f77915303` ; tag exact `beta.144`.
|
||||
Canal packwiz stable : `c8e28d878a55703bdc28fc046242b42b2fcfa9f6`.
|
||||
|
||||
Deux synchronisations isolées puis deux synchronisations de l’instance
|
||||
**Sanctuary Beta** ont réussi, jeu et lanceur fermés. Les secondes passes sont
|
||||
idempotentes. Un seul JAR Sanctuary est actif, et son SHA-256 correspond
|
||||
exactement à celui de la release et du manifeste stable.
|
||||
Les **923 fichiers personnels** suivis (sauvegardes, configurations,
|
||||
packs, captures et mods non gérés) ont conservé leurs empreintes. Aucun monde
|
||||
personnel ouvert. Sauvegarde des fichiers remplacés :
|
||||
`sanctuary-backups/before-beta.144/`.
|
||||
|
||||
Les neuf liens publics (cinq livrables, trois captures, manifeste SHA-256)
|
||||
ont été retéléchargés anonymement et leurs empreintes comparées. Les reçus de
|
||||
publication, de téléchargement et des quatre synchronisations restent dans
|
||||
`build/water144-publish.log`, `build/water144-public-downloads.json`,
|
||||
`build/water144-isolated.json` et `build/water144-prism.json`.
|
||||
Le checkout initial beta.122 et ses modifications restent préservés.
|
||||
Les sources sont sur `codex/water-beta144`, dans le worktree isolé.
|
||||
Les archives vérifiées sont aussi copiées dans `sanctuary-beta/build/`.
|
||||
Les preuves détaillées restent dans les dossiers ignorés :
|
||||
`build/water144-*.log`, `build/water144-*-performance.json`,
|
||||
`build/water144-*-isolation.json` et `build/water144-evidence/`.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [sanctuary-beta.144.jar](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/sanctuary-beta.144.jar) | 11343257 | `1ddbe941f3262da8a1497ea5df4a992e260b80ae95b341623dd68f8f9729ae88` |
|
||||
| [Sanctuary-beta.144.mrpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144.mrpack) | 10483196 | `d5ef5a781f5ee796117210d1f6afc96172461315faee34097323beff541ce6f0` |
|
||||
| [Sanctuary-Test-beta.144.mrpack](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Test-beta.144.mrpack) | 10502119 | `e7cc32ac19ae98ebbe7a9aac3dc08decdf09396c8175b597f87051399ff3812e` |
|
||||
| [Sanctuary-Template-beta.144.zip](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Template-beta.144.zip) | 4354678 | `e510c0040dc9053492715ac91381b7dbc5a6f7c7defbf81e7502d8e18dbb0ee6` |
|
||||
| [Sanctuary-Prism-auto-update.zip](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-Prism-auto-update.zip) | 92423 | `c0920823f40d709c14e074d8d949f2cc3681c24140bf86f21c3be345cfc3e1f6` |
|
||||
|
||||
## Captures comparatives
|
||||
|
||||
Pièces jointes de la release, issues du client Vulkan de validation :
|
||||
[OFF](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-off.png),
|
||||
[ON](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-on.png),
|
||||
[profil étang en proximité](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-water-pond.png).
|
||||
Les captures sont exportées à 854×480 ; la cible de présentation mesurée est
|
||||
1708×960. Le [manifeste SHA-256](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.144/Sanctuary-beta.144-SHA256SUMS.txt) inclut les cinq
|
||||
livrables et ces trois captures. Les journaux et échantillons détaillés restent
|
||||
locaux, dans `build/water144-evidence/` et les reçus `build/water144-*.json`.
|
||||
+461
-1
@@ -8,11 +8,442 @@ https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Les sources sont sur la branche de travail du ticket. La branche `packwiz`
|
||||
contient uniquement les manifestes de distribution produits par le build ; les
|
||||
contient les manifestes de distribution produits par le build et l’icône
|
||||
`icon.png` déclarée dans leur index ; les
|
||||
JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## beta.144 — Eau naturelle pixélisée
|
||||
|
||||
Trois profils selon le fond, teinte olive d’étang, océans natifs clairs et
|
||||
brume aquatique adaptée. [Contrat, captures et mesures](natural-water-beta144.md).
|
||||
[Release beta.144](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.144)
|
||||
publiée depuis `83c377acc1b5a4d4137d7a93c397531f77915303` ; canal
|
||||
`c8e28d878a55703bdc28fc046242b42b2fcfa9f6`. Deux synchronisations isolées puis deux
|
||||
dans l’instance Sanctuary Beta réussies et idempotentes. Un seul JAR Sanctuary
|
||||
actif, identique à l’artefact public. Les 923 fichiers personnels suivis gardent
|
||||
leurs empreintes ; aucun monde personnel ouvert. Sauvegarde préalable :
|
||||
`sanctuary-backups/before-beta.144/`. Les cinq livrables et leurs SHA-256 sont
|
||||
vérifiés dans `sanctuary-beta/build/`. Textures beta.121, Minecraft 26.3 et
|
||||
dépendances exactes conservés.
|
||||
|
||||
## beta.143 — Neige saisonnière en temps réel
|
||||
|
||||
Accumulation espacée par un facteur 12 et fonte par le facteur temps réel 72,
|
||||
avec le sampler natif de précipitation conservé. Protections inchangées pour
|
||||
la neige construite, les biomes froids, l'hiver, les profils neigeux et la
|
||||
météo Vanilla. [Contrat et vérifications](realtime-snow-melt-beta143.md).
|
||||
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
|
||||
publiée ; canal `bafff5e9fcdd95c5dd184953a34ada805e455797`. Deux synchronisations
|
||||
isolées puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.143/`. Archives vérifiées
|
||||
dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.142 — Vulkan par défaut et stabilité PBR
|
||||
|
||||
Vulkan natif prioritaire, rafraîchissements PBR stables et intensité initiale
|
||||
80 %, calculs de reflets bornés. [Contrat et vérifications](pbr-refresh-beta142.md).
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée ; canal `d661260e2bbabb0326c86d6cc543daf986979596`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.142/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
Un choix OpenGL explicite reste conservé : sélectionner Vulkan dans les réglages
|
||||
vidéo et redémarrer pour changer ce choix. La capture Windows de bruit en
|
||||
mouvement reste à reconfirmer sur son pilote ; les contrôles OpenGL/Vulkan Mac
|
||||
et la correction du domaine numérique du reflet sont documentés dans le ticket.
|
||||
|
||||
## beta.141 — Direction des lumières pour les normales PBR
|
||||
|
||||
Directions locales mises en cache, sources portées, confiance réduite lorsque
|
||||
les sources s'opposent et fondus. Activer PBR et Lumières colorées ; portée PBR
|
||||
de 32 blocs. [Contrat et vérifications](directional-lights-beta141.md).
|
||||
[Release beta.141](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.141)
|
||||
publiée ; canal `7e6c84400f80db24bf5204cd21d628194ae9f0f7`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.141/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.140 — Lumières portées et fondus
|
||||
|
||||
Couleurs des objets en main et sur la tête, distances 8/16/32/64 et fondus
|
||||
préservant la teinte. Torches natives par défaut, option chaude, cuivre vert
|
||||
et redstone rouge. [Contrat et vérifications](colored-dynamic-beta140.md).
|
||||
[Release beta.140](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.140)
|
||||
publiée ; canal `2c360a0759d4a164f82c89ac2bef04ff69d50875`. Deux synchronisations isolées
|
||||
puis deux dans l'instance Sanctuary Beta réussies. Les 923 fichiers personnels
|
||||
suivis gardent leurs hashes. Aucun monde personnel ouvert ; sauvegarde préalable
|
||||
`sanctuary-backups/before-beta.140/`. Archives vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.138 — Densité des rayons selon la météo
|
||||
|
||||
Brouillard plus dense et diffus, précipitations et atténuation de l'orage.
|
||||
Réglage manuel conservé, aucune nouvelle option ni modification de sauvegarde.
|
||||
[Contrat et vérifications](shader-beta138.md).
|
||||
[Release beta.138](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.138)
|
||||
publiée ; canal `c049423de1971079b9d61874d83adeb6f1ad49f7`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.138/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.137 — Rayons solaires volumétriques
|
||||
|
||||
Rayons à travers les ouvertures et le feuillage, activés à 35 %, indépendants
|
||||
du bloom et de l'affichage des ombres de surface. Carte solaire partagée.
|
||||
[Contrat et vérifications](shader-beta137.md).
|
||||
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
|
||||
publiée ; canal `68cb39a5edbf5d34def02f670e2488ee852605b8`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.137/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.136 — Lens flare rectangulaire
|
||||
|
||||
Rectangles à dégradé doux pour le soleil et la lune, activés à 30 % et
|
||||
réglables indépendamment du bloom. Occultation par les blocs et phases lunaires.
|
||||
[Contrat et vérifications](shader-beta136.md).
|
||||
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
|
||||
publiée ; canal `dd5b545a7cfbb957f2c70e9dc17034b44caef98f`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.136/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.135 — SSGI en proximité et nouveaux défauts
|
||||
|
||||
Normales stables près des surfaces, atténuation progressive du rebond au contact.
|
||||
Bloom à 20 %, PBR et SSGI activés par défaut, choix personnels existants conservés.
|
||||
[Contrat et vérifications](shader-beta135.md).
|
||||
[Release beta.135](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.135)
|
||||
publiée ; canal `5b0df577fcd036937c25be3a1f4580f9db3103a1`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.135/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.134 — PBR optionnel et SSGI filtré
|
||||
|
||||
Normales procédurales en cache, rugosité/métallicité par famille et reflets
|
||||
directionnels. Filtrage SSGI et suppression de sa forte réinjection neutre.
|
||||
[Contrat et vérifications](shader-beta134.md).
|
||||
[Release beta.134](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.134)
|
||||
publiée ; canal `35ec0f6e145002f6367944df5b75e2703958eaef`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.134/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.133 — SSGI renforcé
|
||||
|
||||
Diffusion des couleurs rendue visible au réglage 35, même budget de rayons
|
||||
et cibles GPU. [Contrat et vérifications](shader-beta133.md).
|
||||
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
|
||||
publiée ; canal `9ceb5a5353c9d1b8bb7834a20e818fd65096ff98`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.133/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.132 — SSGI expérimental
|
||||
|
||||
Diffusion locale des couleurs visibles, rayon 2,5 blocs, intensité réglable,
|
||||
initialement OFF. [Contrat et vérifications](shader-beta132.md).
|
||||
[Release beta.132](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.132)
|
||||
publiée ; canal `9edaaedc0205c525ecce9de66b5ca7827c56c28f`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis gardent leurs hashes. Aucun monde personnel ouvert ;
|
||||
sauvegarde préalable `sanctuary-backups/before-beta.132/`.
|
||||
Packs normal/Test/template vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.131 — émissifs indépendants et précision étendue
|
||||
|
||||
Bloom, émissifs et highlight ON initialement. Couper le halo ou régler son
|
||||
intensité à zéro garde les inclusions lumineuses. Précisions 64/128 ajoutées,
|
||||
défaut 16 conservé. [Contrat et vérifications](shader-beta131.md).
|
||||
[Release beta.131](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.131)
|
||||
publiée ; canal `efb18c274d95d988a9566b8c52751469f002cdda`. Deux synchronisations isolées,
|
||||
puis deux dans la même instance Sanctuary Beta réussies. Les 923 fichiers
|
||||
personnels suivis sont conservés, préférences et sauvegardes comprises.
|
||||
Aucun monde personnel ouvert ; sauvegarde `sanctuary-backups/before-beta.131/`.
|
||||
Archives normal/Test/template vérifiées dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.130 — bloom et précision commune
|
||||
|
||||
Bloom réglable, masques sélectifs des vingt textures de minerais, précision
|
||||
commune aux ombres et au highlight. Réglages initiaux 32/128/16 et Pixel Shadows
|
||||
activé ; anciennes préférences conservées. [Contrat et vérifications](shader-beta130.md).
|
||||
[Release beta.130](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.130)
|
||||
publiée ; canal `68e06353bf880af04c1c72d4e3a159f11f94b619`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, gardent leurs hashes. Aucun
|
||||
monde personnel ouvert ; sauvegarde préalable `sanctuary-backups/before-beta.130/`.
|
||||
Packs normal/Test et template vérifiés et copiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.129 — reflets CTM et portées 16–256 blocs
|
||||
|
||||
Reflets connectés entre tous les matériaux coplanaires, portée indépendante,
|
||||
ombres à 128/256 blocs, intensité rétablie à midi et correction du réalignement
|
||||
cyclique de la grille lumineuse. [Contrat et vérifications](shader-beta129.md).
|
||||
[Release beta.129](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.129)
|
||||
publiée ; canal `2eaa0130a4cd475c598beefdab40f48fc8681ab7`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, sont conservés. Aucun monde
|
||||
personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.129/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.128 — stabilité des ombres, lune et feuillage
|
||||
|
||||
Ombres des blocs nettes et stabilisées, atténuation à midi, lune légère et
|
||||
feuillage nuancé selon son épaisseur. [Contrat et vérifications](shader-beta128.md).
|
||||
[Release beta.128](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.128)
|
||||
publiée ; canal `eba01726ee132a6e548d129fa7ee04f24c880917`. Deux synchronisations isolées
|
||||
puis deux dans la même instance **Sanctuary Beta** réussies. Les 923 fichiers
|
||||
personnels suivis, réglages de shaders compris, sont conservés. Aucun monde
|
||||
personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.128/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.127 — shader et ombres pixélisées
|
||||
|
||||
Ombres solaires optionnelles intégrées à Sanctuary · Vanilla Light, avec finesse
|
||||
et portée réglables. Socle beta.126 conservé. [Contrat et vérifications](shader-beta127.md).
|
||||
[Release beta.127](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.127)
|
||||
publiée ; canal `7fd24a43e01ee497a23311e3c78e1dd3dddd877a`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies.
|
||||
Les 923 fichiers personnels suivis, réglages de shaders compris, sont conservés.
|
||||
Aucun monde personnel ouvert ; copie préalable dans `sanctuary-backups/before-beta.127/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.126 — gemmes, argile et découvertes
|
||||
|
||||
Rubis et saphir, nouvelles textures d'items, argile conservant les couleurs
|
||||
originales et commandes de découverte communes aux recettes et catalogues.
|
||||
[Contrat et vérifications](gems-beta126.md).
|
||||
[Release beta.126](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.126)
|
||||
publiée ; canal `31749b95d21ce5bced6b9e5759499da6f8a67e6e`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies.
|
||||
Les 923 fichiers personnels et réglages suivis sont inchangés ; sauvegardes
|
||||
restées fermées. Copie préalable dans `sanctuary-backups/before-beta.126/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
## beta.125 — placement, éclats et animaux sculptés
|
||||
|
||||
Coins et trois hauteurs, particules de voxels et espèces découvertes au Clay
|
||||
Workshop. [Contrat et vérifications](sculpture-placement-beta125.md).
|
||||
[Release beta.125](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.125)
|
||||
publiée ; canal `47eb833911a96eef0b13805dafe65b2ab7b95c87`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.125/`. Packs normal/Test et template copiés
|
||||
et vérifiés dans le dossier habituel `sanctuary-beta/build/`.
|
||||
|
||||
## beta.124 — une boîte par sculpture
|
||||
|
||||
Une boîte ajustée commune à la sélection et aux collisions ; détails physiques
|
||||
simplifiés, orientation/ancrage et éclairage conservés.
|
||||
[Contrat et vérifications](sculpture-bounds-beta124.md).
|
||||
[Release beta.124](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.124)
|
||||
publiée ; canal `67ebcc7d07f8b75e8988f6099e3d6e5704f123fb`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.124/`. Packs normal/Test et template également
|
||||
copiés et vérifiés dans le dossier habituel `sanctuary-beta/build/`.
|
||||
|
||||
## beta.123 — clé dorée et clé de Steve
|
||||
|
||||
La clé dorée se limite aux orientations cohérentes ; la clé de Steve reprend
|
||||
les états et textures libres, utilisables aussi en survie dès sa récupération.
|
||||
[Contrat et vérifications](wrench-safety-beta123.md).
|
||||
[Release beta.123](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.123)
|
||||
publiée ; canal `f988d01b901f2d3eda0b4ab47a48540dc627d1b5`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.123/`.
|
||||
|
||||
## beta.122 — formes et lumière des sculptures
|
||||
|
||||
Sélection et collisions sur les voxels réels ; suppression de l'occlusion cubique.
|
||||
[Contrat et vérifications](sculpture-shape-light-beta122.md).
|
||||
[Release beta.122](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.122)
|
||||
publiée ; canal `3d340bea0093b709c6018624795a1b3ca557b72d`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.122/`.
|
||||
|
||||
## beta.121 — texture de clé et pack personnel
|
||||
|
||||
PNG du créateur et copie complète modifiable depuis l'écran des packs.
|
||||
[Contrat et vérifications](resourcepack-template-beta121.md).
|
||||
[Release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
|
||||
publiée ; canal `e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.121/`. Le template personnel est créé par le
|
||||
joueur, jamais géré ou réécrit par packwiz.
|
||||
|
||||
## beta.120 — clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation des textures et assemblages prioritaires.
|
||||
[Contrat et vérifications](golden-wrench-beta120.md).
|
||||
[Release beta.120](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.120)
|
||||
publiée ; canal `e8060764a444a58b0a17318a3821dfeb1a0c33c6`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.120/`.
|
||||
|
||||
## beta.119 — pose centrée des sculptures
|
||||
|
||||
Centre accessible au point visé, bords conservés et grille entière de 1/16.
|
||||
[Contrat et vérifications](sculpture-center-beta119.md).
|
||||
[Release beta.119](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.119)
|
||||
publiée ; canal `5e823af7807f524f6394acad402fe62328c7e4ab`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.119/`.
|
||||
|
||||
## beta.118 — murets en briques colorées
|
||||
|
||||
Seize murets natifs, recettes, tailleur, eau et série créative par couleurs.
|
||||
[Contrat et vérifications](colored-brick-walls-beta118.md).
|
||||
[Release beta.118](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.118)
|
||||
publiée ; canal `e6bab433bea9528f67d39af0d6c7bcb6443e4817`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.118/`.
|
||||
|
||||
## beta.117 — grain des textures et sculptures en chapeau
|
||||
|
||||
Échantillonnage des pixels de plusieurs textures, grain stable même sur un
|
||||
modèle uni et conservation des données du chapeau.
|
||||
[Contrat et vérifications](sculpture-texture-grain-beta117.md).
|
||||
[Release beta.117](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.117)
|
||||
publiée ; canal `a5b75a70f86e42afb8e06ba4daa9880d61c6373e`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.117/`.
|
||||
|
||||
## beta.116 — palettes de sculpture et Fourneau
|
||||
|
||||
Le Clay Workshop utilise les nuances des textures de blocs ; le Fourneau
|
||||
corrige les décalages de cases et explique la chauffe.
|
||||
[Contrat et vérifications](sculpture-materials-beta116.md).
|
||||
[Release beta.116](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.116)
|
||||
publiée ; canal `c741870160f8a21f0a9793f20f0e624ebdbacdb6`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.116/`.
|
||||
|
||||
## beta.110 — version commune
|
||||
|
||||
Clay Workshop, Statuaire et import GLB intégrés aux œufs et à la neige saisonnière.
|
||||
[Contrat, vérifications et suivi de mise à jour](clay-workshop-integration-beta110.md).
|
||||
[Release beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110)
|
||||
publiée ; canal `828353a6e2bd747f4f473ae9fb9e965d38032419`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies. Minecraft
|
||||
26.3 finale, Loader 0.19.5, Fabric API 0.160.5+26.3. Les 923 fichiers personnels
|
||||
et réglages suivis sont inchangés ; sauvegardes restées fermées. Copie préalable
|
||||
des fichiers remplacés : `sanctuary-backups/before-beta.110/`.
|
||||
|
||||
## beta.105 — archives locales
|
||||
|
||||
Saisons et neige, chapeaux vivants, molette du fût et panoramas avec shader.
|
||||
[Contrat, tests et empreintes](living-hats-seasons-beta105.md).
|
||||
Archives normales/Test construites en copie isolée pour préserver le chantier
|
||||
beta.106 simultané. Aucun déploiement ni publication du canal.
|
||||
|
||||
## beta.104 — archives locales
|
||||
|
||||
Contraste des argiles grise et noire et rangement beta.103.
|
||||
[Validation](clay-contrast-beta104.md). Packs normal/Test locaux, aucun déploiement.
|
||||
|
||||
## beta.103 — archives locales
|
||||
|
||||
Rangement créatif par séries de 16. [Détails](colored-bricks-order-beta103.md).
|
||||
Packs normal/Test locaux ; aucune publication ni installation personnelle.
|
||||
|
||||
## beta.102 — archives locales
|
||||
|
||||
Briques et argiles des seize couleurs, Minecraft 26.3. Les nouvelles textures
|
||||
sont embarquées dans le mod ; le resource pack beta.090 reste compatible.
|
||||
[Contrat et validation](colored-bricks-beta102.md).
|
||||
Aucune publication du canal ni installation personnelle.
|
||||
|
||||
## beta.101 — archives locales
|
||||
|
||||
Correctif d’atterrissage des familiers volants, Minecraft 26.3, resource pack
|
||||
beta.090 inchangé. [Validation](flying-landing-beta101.md).
|
||||
Aucune publication du canal ni installation personnelle.
|
||||
|
||||
## beta.100 — archives locales
|
||||
|
||||
`Sanctuary-beta.100.mrpack` et `Sanctuary-Test-beta.100.mrpack` : statues
|
||||
conditionnées aux découvertes et promenade des familiers. Minecraft 26.3,
|
||||
resource pack beta.090 inchangé. Voir [validation](exploration-familiers-beta100.md).
|
||||
Aucun canal publié ni déploiement personnel pour cette livraison.
|
||||
|
||||
## beta.099 — Catalogue et Progression
|
||||
|
||||
Packs normal/Test locaux pour Minecraft 26.3 finale.
|
||||
[Contrat, génération et vérifications](catalogue-progression-beta099.md).
|
||||
Les anciennes salles sauvegardées restent inchangées. Aucun canal ou instance
|
||||
personnelle modifié.
|
||||
|
||||
## beta.098 — Construction créative
|
||||
|
||||
Packs normal/Test locaux pour Minecraft 26.3 finale. Ajout du bouton créatif
|
||||
pour construire le plan du Métabli. [Contrat](creative-construction-beta098.md).
|
||||
Aucun canal ni instance personnelle modifié.
|
||||
|
||||
## beta.097 — Temples et plans texturés
|
||||
|
||||
Packs normal/Test locaux pour Minecraft 26.3 finale. Présence des temples de
|
||||
secours et aperçu avec les textures du pack actif.
|
||||
[Contrat et vérifications](temple-crash-beta097.md).
|
||||
Le rapport initial concerne beta.089 sur 26.3-pre-2 ; le correctif cible la
|
||||
version 26.3 finale du dépôt. Aucun canal ni instance personnelle modifié.
|
||||
|
||||
## beta.096 — Métabli
|
||||
|
||||
Les packs locaux normal/Test ajoutent l’atelier et séparent son catalogue des
|
||||
commandes de construction accessibles avec K. Minecraft 26.3, dépendances et
|
||||
pack intégré beta.090 conservés. [Détails et vérifications](metabli-beta096.md).
|
||||
Aucun canal publié ni instance personnelle modifié.
|
||||
|
||||
## beta.095 — Texture de la tige des super pistons
|
||||
|
||||
Les packs locaux normal/Test corrigent quatre modèles de la tige centrale.
|
||||
Minecraft 26.3, dépendances et pack intégré beta.090 conservés.
|
||||
[Détails et vérifications](piston-shaft-beta095.md).
|
||||
Aucun canal publié ni instance personnelle modifié.
|
||||
|
||||
## beta.094 — Fourneau allumé
|
||||
|
||||
Les packs locaux normal/Test ajoutent les braises de façade et corrigent les
|
||||
particules du Fourneau. Minecraft 26.3, dépendances et pack de textures intégré
|
||||
beta.090 conservés. [Détails et vérifications](fourneau-allume-beta094.md).
|
||||
Aucun canal publié ni instance personnelle modifié.
|
||||
|
||||
## beta.093 — Recherche créative
|
||||
|
||||
Les packs locaux normal/Test beta.093 corrigent l’accès graphique depuis les
|
||||
infobulles indexées en arrière-plan. Cible Minecraft 26.3 finale ; le rapport
|
||||
initial provenait de beta.089 sur 26.3-pre-2. Dépendances et textures inchangées.
|
||||
[Diagnostic et vérifications](tooltip-crash-beta093.md). Aucun canal publié
|
||||
ni instance personnelle modifié.
|
||||
|
||||
## beta.092 — Clic molette des super pistons
|
||||
|
||||
Les packs locaux normal/Test beta.092 corrigent la sélection des composants
|
||||
@@ -1766,3 +2197,32 @@ sauvegardes personnelles sont restées fermées pendant les essais et la synchro
|
||||
Références officielles : [installation packwiz](https://packwiz.infra.link/tutorials/installing/packwiz-installer/),
|
||||
[commandes Prism](https://prismlauncher.org/wiki/help-pages/custom-commands/),
|
||||
[bootstrap v0.0.3](https://github.com/packwiz/packwiz-installer-bootstrap/releases/tag/v0.0.3).
|
||||
|
||||
### beta.107 — Œufs, essai isolé
|
||||
|
||||
Archives locales normal/Test vérifiées : [contrat](spawn-eggs-acquisition-beta107.md).
|
||||
Construites sur beta.105 dans `sanctuary-eggs-beta107` ; le Statuaire beta.106,
|
||||
en développement parallèle, n’est pas embarqué. Les sources des œufs sont aussi
|
||||
présentes dans le répertoire principal. Aucun canal, instance Prism, serveur ou
|
||||
sauvegarde personnelle mis à jour.
|
||||
|
||||
|
||||
### beta.108 — Neige en volume, essai isolé
|
||||
|
||||
Archives locales normal/Test vérifiées : [contrat](snow-accumulation-beta108.md).
|
||||
Construites sur beta.107 dans `sanctuary-snow-beta108`, avec les œufs et un
|
||||
plafond de neige confirmé à deux blocs. Statuaire beta.106 reste dans sa livraison
|
||||
séparée. Sources de neige aussi présentes dans le répertoire principal, sans
|
||||
modifier ses compteurs beta.106. Aucun canal, instance Prism, serveur ou
|
||||
sauvegarde personnelle mis à jour.
|
||||
|
||||
|
||||
### beta.109 — Fonte saisonnière, essai isolé
|
||||
|
||||
Archives locales normale/Test vérifiées. [Contrat et vérifications](seasonal-snow-melt-beta109.md). Base beta.108, avec
|
||||
accumulation et œufs ; Statuaire beta.106 reste une livraison séparée. Les
|
||||
nouveaux dépôts sont suivis dans les chunks et fondent progressivement selon
|
||||
la saison. Les blocs antérieurs et les constructions sont conservés.
|
||||
Assemblage dans `sanctuary-melt-beta109` ; sources aussi intégrées au répertoire
|
||||
principal sans modifier son chantier et ses compteurs beta.106.
|
||||
Aucune publication du canal, installation Prism ou sauvegarde personnelle.
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# PBR-142 — Stabilité du relief et Vulkan par défaut
|
||||
|
||||
Socle beta.141 `7ce01c4`, branche `codex/pbr-refresh-beta142`.
|
||||
Minecraft 26.3 / Java 25. Livraison beta.142.
|
||||
|
||||
## Contrat
|
||||
|
||||
Corriger le clignotement lors de la reconstruction des sections PBR : conserver
|
||||
le rendu précédent jusqu'à ce que le remplaçant soit disponible et éviter les
|
||||
reconstructions dues seulement aux lumières dynamiques. Une invalidation pendant
|
||||
une reconstruction doit être conservée, sans annuler indéfiniment le travail.
|
||||
Le PBR passe à 80 % par défaut, valeur confirmée par l'utilisateur ; les choix
|
||||
explicitement enregistrés restent conservés.
|
||||
|
||||
La capture `2026-09-18_04.19.34.png` montre des points très clairs, plus fins
|
||||
que les pixels des textures, sur des parois et un plafond de grotte. L'utilisateur
|
||||
confirme OpenGL et leur disparition quand seul le PBR est coupé. Examiner
|
||||
les lectures de textures et les calculs aux angles rasants séparément du cache.
|
||||
|
||||
Tests natifs prévus : recalcul de section sous observation, invalidations
|
||||
répétées, remplacement atomique, suppression réelle de blocs, lumière portée,
|
||||
cache et préférences ; OpenGL et Vulkan. Pas de monde personnel ouvert.
|
||||
|
||||
## Précision du signalement et implémentation
|
||||
|
||||
L'utilisateur précise ensuite que le bruit apparaît **en mouvement**, et demande
|
||||
le passage à Vulkan. Il ne faut donc pas attribuer les points de sa capture au
|
||||
seul clignotement de reconstruction : ce sont deux vérifications distinctes.
|
||||
|
||||
- Les sections PBR publiées restent affichées jusqu'au remplacement de leur
|
||||
maillage. Une nouvelle invalidation durant la construction est mémorisée ;
|
||||
elle n'annule plus continuellement le travail. Le budget permanent reste
|
||||
24 Mio, avec temporairement l'ancien et le nouveau maillage d'une section.
|
||||
- Les invalidations de lumière dynamique ne régénèrent plus la géométrie PBR.
|
||||
Les changements de blocs et de lumière du ciel restent pris en compte.
|
||||
- Lectures explicites des texels de profondeur/scène et du niveau de matériau,
|
||||
gradients de texture calculés avant les rejets de fragments. Les produits
|
||||
scalaires des reflets sont bornés, la puissance entière de Fresnel est
|
||||
explicite, les résultats non finis ne remplacent pas la scène native.
|
||||
Aucun flou ajouté aux textures.
|
||||
- Le choix graphique natif « Par défaut » devient « Vulkan (par défaut) »
|
||||
(FR/EN) et essaie Vulkan en premier. Minecraft conserve son repli OpenGL si
|
||||
Vulkan ne peut pas être initialisé. Un choix **OpenGL explicite** reste
|
||||
disponible ; pour une installation ainsi réglée, sélectionner Vulkan dans
|
||||
les réglages vidéo puis redémarrer. Aucun moteur externe ni VulkanMod ajouté.
|
||||
- 80 % est la nouvelle intensité PBR initiale. Les valeurs déjà enregistrées
|
||||
explicitement restent conservées.
|
||||
|
||||
Le code Minecraft 26.3 a été inspecté : `PreferredGraphicsApi.VULKAN` essaie
|
||||
Vulkan puis OpenGL ; le choix natif `DEFAULT` essayait auparavant OpenGL d'abord.
|
||||
Tous les effets Sanctuary utilisent déjà l'API graphique native. Le seul hook
|
||||
OpenGL direct est le correctif spécifique de lecture des panoramas dans
|
||||
`GlCommandEncoder` ; il ne remplace pas la voie Vulkan native.
|
||||
|
||||
Référence pour les lectures après contrôle divergent :
|
||||
[documentation Khronos des samplers](https://wikis.khronos.org/opengl/Sampler_%28GLSL%29).
|
||||
La correction rend ces lectures explicites ; elle ne constitue pas à elle seule
|
||||
une reproduction du défaut sur le pilote Windows de l'utilisateur.
|
||||
|
||||
## Observations de diagnostic
|
||||
|
||||
Le détecteur de points blancs isolés (minimum RGB 170, au moins cinq voisins
|
||||
sur huit sous 120) compte 669 points sur l'image utilisateur et zéro sur la
|
||||
première grotte de contrôle. Il s'agit de scènes, résolutions et pilotes différents,
|
||||
pas d'un avant/après reproduisant exactement le bug utilisateur. Les rendus
|
||||
natifs OpenGL de contrôle étaient déjà exempts de ce grain avant les nouvelles
|
||||
protections du shader ; le signalement Windows reste donc à reconfirmer là-bas.
|
||||
|
||||
Le test de rafraîchissement conserve les mêmes maillages publiés durant douze
|
||||
invalidations consécutives, compare l'image et vérifie que des remplacements
|
||||
aboutissent. Le test de torche alterne quatre fois les torches normales/des âmes
|
||||
sans reconstruction des normales. Le contrôle de mouvement presse réellement
|
||||
Avancer, maintient la torche et le PBR actifs et vérifie un déplacement d'au moins
|
||||
un bloc avant de vérifier les 24 captures. Les angles -30°, 0° et 35° sont aussi
|
||||
contrôlés, ainsi que les effets combinés et les préférences.
|
||||
|
||||
### Domaine invalide du reflet de la torche en main
|
||||
|
||||
Pour la lumière du joueur en vue subjective, la source est la caméra : le
|
||||
vecteur lumière et le vecteur de vue sont parallèles. L'ancien
|
||||
`pow(1 - max(dot(v,h),0),5)` n'avait aucune borne supérieure pour ce produit
|
||||
scalaire. Un calcul reproductible Float32 (100 000 positions, graine 142)
|
||||
donne 19 991 bases légèrement négatives, jusqu'à -2,3841858e-7, contre zéro
|
||||
après clamp. C'est une reproduction numérique d'un **domaine de calcul invalide**,
|
||||
pas une reproduction des pixels produits par le pilote utilisateur.
|
||||
|
||||
GLSL 3.30 ne définit pas `pow` pour une base négative, même avec un exposant
|
||||
entier : [spécification Khronos, fonctions exponentielles](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.3.30.pdf).
|
||||
Le correctif borne les cosinus à [0,1] et remplace cette puissance par des
|
||||
multiplications. La torche alignée sur la caméra ne dépend donc plus du traitement
|
||||
de cette entrée invalide par le pilote. Reçu numérique ignoré :
|
||||
`build/pbr142-fresnel-domain.json`.
|
||||
|
||||
### Mise à jour des fixtures antérieures
|
||||
|
||||
La suite Vulkan complète s'est arrêtée au contrôle SSGI de coin : l'oracle
|
||||
ne trouvait plus de pierre neutre (`rows=0`), car les lumières colorées ajoutées
|
||||
depuis beta.139/140 teintaient la pierre avec la lave. Le test de proximité SSGI
|
||||
et le test PBR solaire isolent désormais leur effet en coupant cette autre passe.
|
||||
Aucun seuil n'a été diminué et aucun changement de rendu SSGI n'est livré.
|
||||
Le sous-ensemble en échec est relancé séparément ; les étapes précédentes
|
||||
(ombres, CTM, bloom, lens flare, déplacement SSGI) avaient réussi.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Sur Mac Apple M1, Java 25 et Minecraft 26.3 :
|
||||
|
||||
- OpenGL : suite couleurs/directions/rafraîchissement en 3 min 36 s, puis
|
||||
déplacement continu avec torche active en 1 min 36 s. Zéro point blanc isolé
|
||||
sur les 24 images de mouvement et les trois angles de grotte.
|
||||
- Vulkan/MoltenVK 1.4.2 : sélection automatique sans argument de moteur,
|
||||
couleurs/directions/rafraîchissement en 3 min 47 s. Dernier test continu avec
|
||||
torche active en 1 min 37 s, zéro point blanc isolé sur les 24 images.
|
||||
- Ombrages lune/midi/feuilles, distances et précisions, stabilité temps réel,
|
||||
CTM, bloom (minerais, torche, lampe, lave, soleil), émissifs indépendants et
|
||||
lens flare validés sous Vulkan dans la suite d'effets. Le déplacement SSGI
|
||||
sur douze blocs passe également. La suite s'arrête ensuite sur l'oracle
|
||||
ancien de couleur de pierre décrit ci-dessus : ne pas présenter ce lancement
|
||||
de 7 min 15 s comme une réussite complète.
|
||||
- Reprise ciblée après isolation de cet oracle : SSGI près des surfaces puis
|
||||
PBR extérieur réussis sous Vulkan en 2 min 55 s. Proximité 35/100, haute
|
||||
résolution, léger mouvement, toit ajouté/retiré, reload, resize, zéro/OFF et
|
||||
effets combinés. Gain SSGI au coin test : 6,01 sur 99 lignes à 35 %.
|
||||
- Les nouvelles valeurs initiales et les choix enregistrés sont testés, dont
|
||||
une intensité PBR explicitement enregistrée à 50 qui reste à 50.
|
||||
|
||||
Les premiers essais du nouveau contrôle de mouvement ont permis de corriger
|
||||
son entrée clavier : `pressKey` ne maintient pas une touche enfoncée. Le test
|
||||
final utilise `holdKey` puis `releaseKey`, vérifie le déplacement réel et refuse
|
||||
la disparition de la lumière portée ou du PBR pendant les captures.
|
||||
|
||||
Les jeux de test utilisent uniquement des mondes neufs et ignorés. La capture
|
||||
et le pilote Windows utilisateur n'ont pas été reproduits à l'identique ; leur
|
||||
retest reste ouvert malgré les corrections numériques et les contrôles natifs.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 38 s**, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
selon le refus EULA antérieur ; les suites clientes ci-dessus ont réellement
|
||||
utilisé les deux moteurs natifs. `git diff --check` passe.
|
||||
|
||||
Les archives normale/Test embarquent le même JAR, leurs versions/index/hashes
|
||||
sont vérifiés, leurs sources correspondent aux fichiers courants. Le template
|
||||
contient exactement les ressources attendues. Les onze entrées de production
|
||||
modifiées par rapport à beta.141 sont limitées au PBR, au choix graphique et
|
||||
aux traductions ; les ressources de jeu antérieures sont conservées.
|
||||
|
||||
SHA-256 du JAR : `0bc2da9df2ef004e6dc78932cfde7b31b7717c63c83ceed789d151177c41bc1a`.
|
||||
|
||||
- `Sanctuary-beta.142.mrpack` : 10464746 octets ; SHA-256 `dba4fd8e1e3b20136faa1c1de3494df3cb250c87de199c04e07883c786ca48ee`.
|
||||
- `Sanctuary-Test-beta.142.mrpack` : 10483672 octets ; SHA-256 `d56b81a4aea6649a40a3c49dedee904989943a518ad0ae7c520b808dd0f9b9a7`.
|
||||
|
||||
Reçu détaillé ignoré : `build/shader142-artifact.json`.
|
||||
|
||||
## Livraison réalisée
|
||||
|
||||
[Release beta.142](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.142)
|
||||
publiée depuis `ea151dd5df8c7c5abd0107656f40c723efbf2ebe`. Canal stable :
|
||||
`d661260e2bbabb0326c86d6cc543daf986979596`. Archives normale/Test, ZIP d'amorçage Prism et template
|
||||
publiés avec vérification de leurs téléchargements ; tag et artefacts immuables.
|
||||
|
||||
Deux synchronisations isolées puis deux synchronisations de l'instance
|
||||
**Sanctuary Beta** réussies. Les 923 fichiers personnels suivis ont conservé
|
||||
leurs hashes, notamment mondes, réglages et packs personnels. Aucun monde
|
||||
personnel ouvert. Sauvegarde préalable : `sanctuary-backups/before-beta.142/`.
|
||||
L'instance locale conserve son réglage graphique natif « default », qui démarre
|
||||
désormais avec Vulkan prioritaire. Un Windows explicitement réglé sur OpenGL
|
||||
doit sélectionner Vulkan dans les réglages vidéo et redémarrer.
|
||||
|
||||
Archives locales également dans `sanctuary-beta/build/`. Le checkout de travail
|
||||
historique modifié `sanctuary-beta` n'a pas été réinitialisé ; seules ces archives
|
||||
ignorées y ont été déposées. Sources sur `codex/pbr-refresh-beta142`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# beta.095 — Texture du pilier des super pistons
|
||||
|
||||
La tige centrale utilise seulement le carré de bois central de la grande plaque
|
||||
`sanctuary:block/super_piston/top`, pour les super pistons normaux et gluants.
|
||||
Le prélèvement reste dans les pixels 16 à 32 de l'image originale de 48 × 48,
|
||||
sans les bordures. Les UV suivent les dimensions réelles du pilier, à un pixel
|
||||
par unité de modèle, sans étirement. Cela couvre aussi les segments courts dans
|
||||
le socle ouvert et derrière la tête.
|
||||
|
||||
Les grandes plaques, le corps et les textures originales fournies restent
|
||||
inchangés. La course, les collisions et les six orientations ne changent pas.
|
||||
Seuls quatre modèles JSON et leur générateur sont modifiés ; aucun code Java,
|
||||
identifiant ou format de sauvegarde n'évolue. La référence à la texture
|
||||
Sanctuary respecte le pack de ressources actif. Aucune nouvelle image ni dépendance ajoutée.
|
||||
|
||||
Régénération : `python3 tools/generate-super-piston-models.py`.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours client natif `Piston087ClientChecks` a réussi en 1 min 22 s
|
||||
(`build/piston095-client.log`), dans un nouveau monde plat de développement,
|
||||
graine 87 : six directions, normal/gluant, déplacement et retour, collisions,
|
||||
charges de 108/109, obstacles, inventaires, adhérence, déchargement/rechargement
|
||||
et démontage. Aucun avertissement de référence de texture manquante.
|
||||
|
||||
Les captures horizontales et verticales ont été inspectées dans
|
||||
`build/piston095-evidence/`, notamment `0015_piston087-normal-face-open.png`
|
||||
et `0001_piston087-normal-up.png`. Le bois est à la même densité que la plaque.
|
||||
|
||||
Le contrôle des quatre modèles vérifie que tous les prélèvements restent dans
|
||||
le carré central 16–32 pixels et que leurs dimensions en pixels correspondent
|
||||
exactement à celles de leurs faces, raccords courts inclus. Géométrie, autres
|
||||
faces et textures originales identiques à beta.094. Le générateur est reproductible.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` a réussi
|
||||
en 2 min 28 s, 124 tâches (`build/piston095-check.log`).
|
||||
Les deux archives normal/Test ont été exportées et vérifiées : version beta.095,
|
||||
Minecraft 26.3, même JAR Sanctuary embarqué. Les classes Java, les autres assets,
|
||||
JEI et les archives beta.094 sont inchangés. Le reçu et les empreintes sont dans
|
||||
`build/piston095-artifact.json` ; `build/verify-piston095.py` reproduit ces contrôles.
|
||||
`git diff --check` passe.
|
||||
|
||||
[Pack normal](../build/Sanctuary-beta.095.mrpack) ·
|
||||
[Pack test](../build/Sanctuary-Test-beta.095.mrpack).
|
||||
|
||||
Le serveur dédié GameTest est exclu (`-x :sanctuary:runGameTest`) pour respecter
|
||||
le choix antérieur de tests client uniquement. Aucun déploiement dans une
|
||||
instance personnelle, changement de monde ou publication du canal packwiz.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Préparation locale des ombres portées pixélisées (ancien numéro beta.122)
|
||||
|
||||
Ticket du 17 septembre 2026, branche locale `codex/pixel-shadows-beta122`.
|
||||
Cette préparation n'a jamais été publiée sous le numéro beta.122, déjà utilisé
|
||||
par une autre livraison. Elle est intégrée au socle beta.126 et livrée en
|
||||
[beta.127](shader-beta127.md). Les chemins et mesures ci-dessous décrivent
|
||||
les essais historiques dans le dossier de préparation.
|
||||
Demande : ajouter aux réglages de Sanctuary · Vanilla Light des ombres portées
|
||||
pixélisées, avec une finesse et une distance réglables.
|
||||
|
||||
## Utilisation
|
||||
|
||||
Dans **Options → Shaders**, activer **Sanctuary · Vanilla Light**, puis
|
||||
**Ombres portées pixélisées**. L'option est désactivée par défaut pour conserver
|
||||
le coût et le rendu des installations existantes. Les choix sont :
|
||||
|
||||
- finesse de **8, 16 ou 32 pixels par bloc**, défaut 16 ;
|
||||
- distance de **16, 32 ou 64 blocs**, défaut 32.
|
||||
|
||||
Ces préférences sont conservées dans `config/sanctuary-shaders.json`, avec les
|
||||
anciens champs de couleur. Leurs valeurs numériques sont ramenées aux choix
|
||||
admis au chargement. Les libellés et explications sont disponibles en FR/EN.
|
||||
Les réglages s'appliquent sans recharger les ressources.
|
||||
|
||||
## Rendu et périmètre
|
||||
|
||||
Une carte de profondeur rend les formes d'occultation des blocs solides chargés
|
||||
autour de la caméra, indépendamment de leur présence à l'écran. Les dalles et
|
||||
escaliers conservent leurs formes. Le cache est invalidé par les changements de
|
||||
blocs, les paquets de chunks et les reconstructions du moteur natif.
|
||||
La direction solaire vient du ciel natif extrait par Minecraft, et respecte
|
||||
ainsi l'angle solaire de Sanctuary. L'effet s'atténue sous la pluie, à l'horizon,
|
||||
dans la brume et à la limite de sa portée. Il s'arrête la nuit, dans les dimensions
|
||||
sans soleil, dans les fluides et lorsqu'un moteur externe suspend Vanilla Light.
|
||||
|
||||
La profondeur du monde est lue directement par le GPU après le rendu du monde,
|
||||
avant la main et l'interface. L'inverse de la projection réelle inclut les effets
|
||||
de caméra. Les points recevant l'ombre sont arrondis sur une grille ancrée dans
|
||||
le monde ; le plan de la face est conservé pour limiter les auto-ombres.
|
||||
La carte solaire utilise également une origine alignée sur ses texels.
|
||||
Le HUD, la main, les étoiles et les captures panoramiques techniques gardent leur
|
||||
chemin de rendu. Aucun niveau de lumière de gameplay, règle serveur, sauvegarde
|
||||
ou générateur n'est changé. Aucune dépendance de rendu n'est ajoutée.
|
||||
|
||||
Cette première version couvre les **formes d'occultation des blocs** : les entités
|
||||
conservent leurs ombres natives ; les feuillages ajourés, modèles non occultants,
|
||||
block entities animés, eau et verre ne projettent pas de nouvelles silhouettes.
|
||||
Les transparences et particules déjà composées peuvent recevoir l'assombrissement
|
||||
du terrain derrière elles. Il n'y a pas d'ombres de lune ou de torches, ni de
|
||||
séparation de l'éclairage local émissif dans cette passe d'assombrissement.
|
||||
|
||||
## Coût et cycle de vie
|
||||
|
||||
Les maillages sont mis en cache par section de 16³, avec au plus quatre sections
|
||||
commencées par image et un budget souple de 3 ms pour leur construction. Les
|
||||
ombres apparaissent progressivement pendant la préparation après activation ou
|
||||
déplacement. Le volume des obstacles part de la portée choisie avec une marge de huit blocs,
|
||||
puis s’étend vers le soleil sur deux fois cette portée, arrondi aux sections.
|
||||
Les obstacles au-delà de ce volume ou dans des chunks non chargés ne sont pas
|
||||
représentés. Les mises à jour de lumière seule ne reconstruisent pas les formes.
|
||||
Le cache de sommets est plafonné à 96 Mio et omet les formes pathologiques qui
|
||||
dépassent ses limites. Les textures de profondeur font entre 512 et 4 096 pixels
|
||||
de côté selon finesse et portée. Le coût exact dépend de la scène et du GPU.
|
||||
|
||||
Les ressources sont libérées à la désactivation, au changement de monde, à la
|
||||
reconstruction des ressources et à l'arrêt du client. Cette livraison est locale :
|
||||
aucun avancement du canal packwiz ni installation personnelle ne fait partie du
|
||||
ticket.
|
||||
|
||||
## Vérification
|
||||
|
||||
Validation native sur **Minecraft 26.3, Java 25, macOS / Apple M1, OpenGL 4.1**,
|
||||
dans un nouveau monde plat de développement, graine **122** :
|
||||
|
||||
- Comparaison GPU activation/désactivation sur une plateforme blanche avec
|
||||
pilier, dalle et escalier : **920 échantillons assombris**, 63 412 inchangés,
|
||||
aucun éclairci sur les 64 332 échantillons de sol retenus.
|
||||
- Pilier entièrement hors du frustum : **42 048 échantillons** restent dans
|
||||
son ombre. Le retrait du pilier restitue la lumière sur **42 048 / 42 048**
|
||||
échantillons et préserve les 23 616 échantillons déjà éclairés. Sa repose
|
||||
restitue la même ombre, sans désactiver le cache entre ces manipulations.
|
||||
- Les trois finesses et les trois portées sont appliquées en direct ; les cartes
|
||||
GPU sont redimensionnées aux dimensions attendues. Désactiver l'option libère
|
||||
la texture et ne soumet plus de passe d'ombres.
|
||||
- À minuit, activer ou désactiver l'option donne la même image et aucune passe
|
||||
solaire n'est soumise. Au retour du jour, le rendu reprend.
|
||||
- Le rechargement des ressources restitue la même image. Les préférences
|
||||
persistent à la relecture du fichier et leurs valeurs extrêmes sont bornées.
|
||||
- Menus FR/EN inspectés en capture ; les libellés tiennent dans les contrôles
|
||||
natifs à la taille de test. Les vues de finesse 8/16/32 sont conservées.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Succès en **1 min 17 s**, marqueur `PIXEL_SHADOWS122_CLIENT_PASS` dans
|
||||
`build/pixel-shadows122-client.log`. Captures inspectées dans
|
||||
`build/pixel-shadows122-evidence/final/`. Les pixels sont relus uniquement dans
|
||||
le test ; le code de rendu de production n'effectue aucune lecture CPU de GPU.
|
||||
Les premiers démarrages ont permis de corriger les identifiants de pipelines,
|
||||
la directive GLSL `#include` attendue par 26.3 et deux libellés répétés du menu.
|
||||
|
||||
La commande de livraison est :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest
|
||||
```
|
||||
|
||||
Le serveur GameTest dédié est exclu, conformément au refus de son EULA déjà
|
||||
consigné dans les livraisons précédentes. Le test graphique utilise le serveur
|
||||
intégré de son monde neuf. Aucun monde personnel n'est ouvert.
|
||||
La construction finale **réussit en 2 min 18 s** (126 tâches), journal
|
||||
`build/pixel-shadows122-build-final.log`. Le contrôle des archives confirme
|
||||
**868 sources Java** et **1 869 classes** identiques aux sorties construites,
|
||||
les ressources GLSL/FR/EN exactes dans le JAR et le template, et le même JAR
|
||||
dans les profils packwiz normal et Test. Aucune fixture cliente n'est embarquée.
|
||||
Reçu : `build/pixel-shadows122-artifact.json`.
|
||||
|
||||
Artefacts locaux : `mods/sanctuary/build/libs/sanctuary-beta.122.jar`,
|
||||
`build/packwiz/`, `build/packwiz-test/` et `build/Sanctuary-Template-beta.122.zip`.
|
||||
SHA-256 du JAR :
|
||||
`66987681cede1d2adc1dda449078e8fa85310eb2c3fecd7a905d772ce75739c9`.
|
||||
|
||||
Pas de mesure de FPS, d'essai Windows/Vulkan, de pack Iris ni de silhouettes
|
||||
animées revendiqués. Le shader reste un assombrissement graphique appliqué au
|
||||
rendu opaque et déjà composé, avec les limites de transparence décrites plus haut.
|
||||
@@ -0,0 +1,68 @@
|
||||
# SNOW-143 — Neige saisonnière en temps réel
|
||||
|
||||
Branche `codex/realtime-snow-melt-beta143`, socle beta.142. Minecraft 26.3 /
|
||||
Java 25. Livraison beta.143.
|
||||
|
||||
## Comportement
|
||||
|
||||
L'accumulation et la fonte saisonnière conservent le sampler natif de
|
||||
précipitation `ServerLevel.tickPrecipitation`. Sanctuary ne scanne donc toujours
|
||||
pas les chunks et ne force aucun chargement de terrain. Le changement porte
|
||||
seulement sur la probabilité des mutations de neige.
|
||||
|
||||
Pendant une chute de neige, Sanctuary ajoute désormais une couche une fois sur
|
||||
12 échantillons natifs en moyenne. La neige reste visible pendant une session,
|
||||
mais les colonnes ne montent plus instantanément. À `randomTickSpeed = 3`, une
|
||||
couche apparaît en moyenne après environ 40 minutes de neige continue.
|
||||
|
||||
Pour la fonte, chaque tirage saisonnier est multiplié par `72`, le rapport
|
||||
entre une journée réelle de 24 heures et une journée Minecraft de 20 minutes.
|
||||
|
||||
Une couche fond désormais en moyenne en 4 h 05 en été,
|
||||
8 h 10 au printemps et 16 h 25 en automne. Avec le plafond actuel de deux blocs,
|
||||
une colonne météo suivie met environ 2,7 jours réels à disparaître en été,
|
||||
5,4 jours au printemps et 11 jours en automne. L'hiver ne fond toujours pas.
|
||||
|
||||
Les règles de protection restent celles de beta.109 : seule la neige déposée
|
||||
par la météo Sanctuary depuis l'ajout du suivi `sanctuary:natural_snow_v1`
|
||||
peut fondre. La neige construite, l'ancienne neige non suivie, les couches
|
||||
manuelles, les biomes froids, les profils neigeux et la météo Vanilla restent
|
||||
préservés. Aucun format de sauvegarde ne change.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Vérifié le 18 septembre 2026 sur macOS, Java 25.
|
||||
|
||||
- `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryMelt109ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true` : réussi en 57 s, avec le marqueur
|
||||
`MELT109_NATIVE_PASS`. Le test couvre l'accumulation beta.108 ralentie,
|
||||
la fonte couche par couche, la provenance persistante, les saisons, la
|
||||
gamerule, la météo Vanilla, les profils neigeux, les biomes froids et le
|
||||
rechargement de monde.
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
|
||||
réussi en 2 min 31 s, 126 tâches. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus antérieur de son EULA ; les contrôles natifs utilisent
|
||||
le serveur intégré.
|
||||
- `python3 scripts/pack.py check` : versions, icône et index packwiz vérifiés.
|
||||
- Exports MRpack produits avec `packwiz modrinth export`, intégrité ZIP vérifiée.
|
||||
Les deux MRpack embarquent exactement le JAR Sanctuary construit.
|
||||
|
||||
| Artefact | Taille | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.143.jar` | 11 323 665 octets | `fd4b663ac2b2a106624c42b09997db229cc14d6c39d44ae83079bc6aa9d39a08` |
|
||||
| `Sanctuary-beta.143.mrpack` | 10 464 931 octets | `e55caaa41c5a3a2666ff5415846e18f44d504ac34b698f3d974091cfb52364c5` |
|
||||
| `Sanctuary-Test-beta.143.mrpack` | 10 483 858 octets | `b14bca04c4a48771209d0f7e234ff9ef6b800eb236fb969a6f0dfcee5620dafc` |
|
||||
| `Sanctuary-Template-beta.143.zip` | 4 354 153 octets | `071fd8da67d1d0224b89889a5fcbc4cd269673dddb7a52f89aa78802f154605a` |
|
||||
|
||||
## Publication
|
||||
|
||||
[Release beta.143](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.143)
|
||||
publiée depuis le commit `97ff2b54cb63afe8fdd88bfa8f2ebb25dbe3b86d`.
|
||||
Le canal packwiz stable pointe sur
|
||||
`bafff5e9fcdd95c5dd184953a34ada805e455797`.
|
||||
|
||||
Deux synchronisations isolées, puis deux synchronisations dans l'instance
|
||||
Sanctuary Beta ont réussi. Les 923 fichiers personnels suivis gardent leurs
|
||||
hashes ; aucun monde personnel n'a été ouvert. Sauvegarde préalable :
|
||||
`sanctuary-backups/before-beta.143/`.
|
||||
@@ -0,0 +1,125 @@
|
||||
# RP-02 — Clé dorée et pack personnel — beta.121
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/resourcepack-template-beta121`.
|
||||
Le créateur fournit `golden_wrench.png` et valide un bouton qui crée une copie
|
||||
personnelle complète, modifiable et jamais écrasée par les mises à jour.
|
||||
|
||||
## Résultat
|
||||
|
||||
La clé dorée reprend exactement le PNG fourni, 16 × 16, sans conversion ni
|
||||
redessin. Le modèle reste un item plat `minecraft:item/handheld`. Empreinte :
|
||||
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`.
|
||||
Le même fichier est fourni par le mod et par « Textures Sanctuary » ; désactiver
|
||||
le pack intégré conserve donc aussi la nouvelle clé.
|
||||
|
||||
Dans **Options → Packs de ressources**, **Personnaliser Sanctuary** crée
|
||||
`resourcepacks/Sanctuary-Custom/`. Le bouton devient **Ouvrir ma copie**.
|
||||
La copie contient toutes les ressources client de Sanctuary : textures, modèles,
|
||||
étiquettes d'items, blockstates, sons, shaders, langues et remplacements Minecraft
|
||||
présents dans le pack intégré. Celui-ci prend priorité sur les assets de base
|
||||
lors de l'export. Un guide FR/EN, la licence et les crédits accompagnent la copie.
|
||||
|
||||
Le joueur active sa copie avec les commandes natives et la place au-dessus des
|
||||
packs qu'il veut remplacer. Créer la copie ne change ni l'ordre ni la sélection.
|
||||
Une modification se recharge avec F3 + T ; désactiver le pack retrouve les
|
||||
ressources fournies par le mod. L'écran de sélection des datapacks n'est pas modifié.
|
||||
|
||||
La copie est un **instantané**. Elle ne se synchronise pas avec les mises à jour :
|
||||
les fichiers personnalisés restent toujours intacts. Le guide recommande de
|
||||
supprimer les fichiers non personnalisés pour laisser les prochaines ressources
|
||||
du mod s'appliquer. Pour repartir d'un instantané récent, renommer le dossier
|
||||
existant puis créer une nouvelle copie. Aucun fichier utilisateur n'est écrasé.
|
||||
|
||||
La copie s'effectue hors du fil de rendu, dans un dossier temporaire voisin,
|
||||
puis le résultat complet rejoint la liste native. Une erreur de copie est
|
||||
signalée par le bouton et le journal du jeu ; un chemin occupé par un fichier
|
||||
n'est pas remplacé. Aucune extraction au démarrage ni pendant les mises à jour.
|
||||
|
||||
## Distribution
|
||||
|
||||
`./gradlew assembleResourceTemplate` produit
|
||||
`build/Sanctuary-Template-beta.121.zip`, un pack personnel complet à décompresser
|
||||
dans `resourcepacks/Sanctuary-Custom/`. `assemblePack` le produit aussi.
|
||||
`build/Sanctuary-Resource-Pack-beta.121.zip` reste l'export du pack graphique
|
||||
intégré ; son manifeste versionné contient 151 fichiers et les images historiques
|
||||
sont conservées. L'identifiant intégré `sanctuary:textures` reste stable et son
|
||||
activation reste contrôlée par le joueur.
|
||||
|
||||
Le template ne contient pas tous les assets vanilla ni ceux des autres mods.
|
||||
Il ne contient pas les recettes, règles serveur ou sauvegardes. Il conserve
|
||||
les crédits et conditions des ressources d'origine. Aucune génération, aucun
|
||||
identifiant de bloc et aucun format de sauvegarde ne changent.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `ResourceTemplate121ClientChecks` passe en **35 s** sur le client
|
||||
Minecraft 26.3 macOS, sans ouvrir de monde :
|
||||
|
||||
- Bouton natif et disposition FR/EN aux échelles 2 et 3, captures relues.
|
||||
- Export complet de **1 202 fichiers**, empreintes identiques aux ressources
|
||||
sources après application des priorités du pack intégré.
|
||||
- PNG fourni vérifié par SHA-256 et affiché avec sa transparence.
|
||||
- Création sans changer la sélection ni l'ordre des packs.
|
||||
- Modification d'une texture et ajout d'un fichier personnel : aucun octet
|
||||
réécrit lors d'un second export ni au rechargement des ressources.
|
||||
- Priorité réelle de la texture personnalisée, puis retour à la clé fournie
|
||||
après désactivation. Activation et désactivation conservées à la redécouverte.
|
||||
- Chemin occupé par un fichier refusé sans le modifier ; aucun bouton ajouté
|
||||
à l'écran des datapacks.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryResourceTemplate121ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueur `TEMPLATE121_PASS` dans `build/template121-client.log` ; captures et
|
||||
empreintes de la copie dans `build/template121-evidence/`. Le premier essai
|
||||
avait échoué dans l'aperçu du test qui créait un ItemStack avant connexion à
|
||||
un monde ; cet aperçu affiche maintenant directement la texture résolue.
|
||||
Le modèle plat existant n'est pas modifié. Pas d'essai Windows ni d'ouverture
|
||||
automatisée du gestionnaire de fichiers. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 10 s**, 126 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. La comparaison avec beta.120 limite les différences de production
|
||||
aux deux nouvelles classes, au hook d'interface, aux libellés et aux ressources
|
||||
prévues (12 entrées). Aucun changement de gameplay, de données serveur ou de
|
||||
monde. Les packs normal/Test contiennent le même JAR Sanctuary, sans fixture
|
||||
ni sauvegarde. Le ZIP du template et l'export natif ont exactement les mêmes
|
||||
1 202 fichiers et empreintes. Le pack intégré conserve ses 151 fichiers vérifiés.
|
||||
|
||||
Reçu : `build/template121-artifact.json`. Artefacts locaux validés :
|
||||
|
||||
- `Sanctuary-beta.121.mrpack` : 10261791 octets, SHA-256
|
||||
`040d68f342155120fa21e0ffcdf34aa38e3225c78862960536622de53ed9fd9f`.
|
||||
- `Sanctuary-Test-beta.121.mrpack` : 10280716 octets, SHA-256
|
||||
`7f5bbd4e13aea4c245d072725c094ca64826ee8debc8607623783c114d437685`.
|
||||
- `Sanctuary-Resource-Pack-beta.121.zip` : 3757952 octets, SHA-256
|
||||
`bbf3c18ea6abc64d951911215975cb6c48c7fc5e0246f6e5f5192a3d061c211d`.
|
||||
- `Sanctuary-Template-beta.121.zip` : 4315131 octets, SHA-256
|
||||
`6fad376c53474fa8bcb0a2e6ae4c13ea61ab91bdd78735be443032d485ed541a`.
|
||||
|
||||
JAR Sanctuary : SHA-256
|
||||
`4276f4fc9834805e3e6d12cd19adba7837e3007656b77f31d525c7b976f9925d`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
|
||||
est publiée depuis `d9b3dd6bedf32448f8c6e5bfa4da10cb69968bc7`. Tag et artefacts immuables,
|
||||
téléchargements publics vérifiés. Canal packwiz :
|
||||
`e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.121 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés,
|
||||
y compris les packs de ressources. Aucune copie personnelle n'est créée par
|
||||
la mise à jour : chaque joueur utilise le bouton quand il le souhaite.
|
||||
Aucun monde personnel ouvert. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.121/` de l'instance existante ; reçus
|
||||
`build/template121-isolated.json` et `build/template121-prism.json`.
|
||||
|
||||
- [Template personnel complet](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip).
|
||||
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack).
|
||||
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
|
||||
@@ -0,0 +1,93 @@
|
||||
# SCULPT-10 — Une boîte par sculpture — beta.124
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-bounds-beta124`.
|
||||
Le créateur choisit **une seule boîte pour la sélection et les collisions**.
|
||||
Ce ticket remplace la forme physique détaillée de beta.122.
|
||||
|
||||
La boîte englobe exactement les dimensions occupées par la sculpture, après
|
||||
orientation et ancrage au centre ou au bord. Elle suit toujours la grille 1/16,
|
||||
y compris les tailles impaires et les modèles comportant des marges vides.
|
||||
Les trous et petits détails restent visibles mais n'ont plus leurs propres
|
||||
surfaces de collision : on ne grimpe plus sur leurs petites marches. Une arche
|
||||
se sélectionne et bloque désormais sur tout son volume englobant.
|
||||
|
||||
La pose utilise la même boîte avant la création de l'entité de bloc : elle
|
||||
refuse une intersection avec le joueur, y compris dans les trous visuels.
|
||||
Les petites sculptures laissent libre le reste du bloc autour de leur boîte.
|
||||
La forme reste mise en cache par modèle et état ; elle ne se recalcule qu'après
|
||||
un changement de modèle, d'orientation ou d'ancrage. Les requêtes physiques et
|
||||
le contour partagent cette même forme native.
|
||||
|
||||
Le calcul parcourt les cellules une seule fois, sans créer de copies tournées
|
||||
ni de grille de collisions 16³. Le contour émet toujours douze arêtes.
|
||||
Il s'agit d'une simplification du travail géométrique ; aucun gain de FPS global
|
||||
n'est annoncé sans mesure sur une scène représentative.
|
||||
|
||||
Le rendu des sculptures et les règles de lumière de beta.122 restent inchangés :
|
||||
pas d'occlusion de cube plein, pas d'émission artificielle de lumière. Aucun
|
||||
identifiant, format d'item ou de sauvegarde ne change. Les anciennes sculptures
|
||||
utilisent la nouvelle forme à partir de leurs données existantes, sans réécriture
|
||||
des mondes ni régénération de chunks. L'atelier et les clés restent inchangés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `Sculpture122ClientChecks`, adapté au nouveau contrat, passe en
|
||||
**39 s** sur Minecraft 26.3, client macOS et serveur intégré jetable de graine 122.
|
||||
|
||||
- Sélection native de tout le volume, y compris le trou visuel d'une arche ;
|
||||
contour rectangulaire relu dans la capture.
|
||||
- Même boîte physique et de sélection sur client et serveur ; intérieur rempli,
|
||||
dimensions exactes sans profondeur artificielle de bloc entier.
|
||||
- **80 cas** : boîte unique comparée aux limites du maillage réellement émis,
|
||||
douze arêtes exactement, même objet réutilisé lors des requêtes répétées.
|
||||
Arche, marges vides, cube de 4 096 cellules et voxel unique, dans les quatre
|
||||
orientations et cinq ancrages.
|
||||
- **5 120 placements** de rendu toujours alignés sur la grille entière.
|
||||
- Pose refusée dans le joueur, y compris entre deux piliers séparés ; petite
|
||||
sculpture autorisée à côté du joueur, hors de sa boîte physique.
|
||||
- Cache renouvelé après changement d'orientation/ancrage ou de modèle ;
|
||||
conservation après sauvegarde/reconnexion.
|
||||
- **75 sculptures superposées** : ciel à 15 ; toit de pierre témoin sombre ;
|
||||
lumière de bloc transmise avec atténuation normale ; pièce close à 0.
|
||||
|
||||
Marqueurs dans `build/sculpture124-client.log`, notamment
|
||||
`SCULPTURE124_BOUNDS_PASS 80`, plus les contrôles de régression de beta.122.
|
||||
Captures relues dans `build/sculpture124-evidence/`. Aucun essai Windows,
|
||||
client distant ou shader tiers ; le coût des boîtes est simplifié sans benchmark
|
||||
FPS global.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 9 s**, 126 tâches (110 exécutées, 16 à jour).
|
||||
Les sources des JAR correspondent aux sources du dépôt. La comparaison avec
|
||||
beta.123 limite les différences de production aux trois classes de
|
||||
`SculptureGeometry` ; ressources, clés, interfaces, données et éclairage restent
|
||||
identiques. Les packs normal/Test contiennent le même JAR, sans sauvegarde
|
||||
ni fixture. Le template personnel conserve exactement les ressources de beta.123.
|
||||
|
||||
- Pack normal : 10 271 059 octets, SHA-256
|
||||
`2075e4ac7a7e7e2effcd82586d46769607ca1a67e8d9110b000fa378d1bc5506`.
|
||||
- Pack Test : 10 289 985 octets, SHA-256
|
||||
`88263e6896590ea9694b18084666e7234e934064e8d7d596d34866a68cc5ea23`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`b3969b1a3738b8a5d332b2083db0dd722900239f73035c335b738f70328d43f1`.
|
||||
|
||||
Reçu local : `build/sculpture124-artifact.json`. Les packs normal/Test et le
|
||||
template sont aussi copiés, avec vérification octet pour octet, dans le dossier
|
||||
habituel `sanctuary-beta/build/`.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Aucun monde personnel n'est ouvert par les essais.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.124](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.124)
|
||||
est publiée depuis `00179e30ef0b45ef98378bc164a7810552dd1bdd`.
|
||||
Le tag exact et les artefacts sont immuables, leurs téléchargements publics
|
||||
vérifiés. Canal packwiz : `67ebcc7d07f8b75e8988f6099e3d6e5704f123fb`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.124 est actif, conforme au hash vérifié ; le second
|
||||
passage ne change rien. Les **923 fichiers personnels et réglages suivis**
|
||||
restent inchangés, sans ouverture de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.124/` de l'instance existante.
|
||||
Reçus : `build/sculpture124-isolated.json` et `build/sculpture124-prism.json`.
|
||||
@@ -0,0 +1,84 @@
|
||||
# STAT-07 — Pose centrée des sculptures — beta.119
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-center-beta119`.
|
||||
|
||||
Viser le carré central de 8 × 8 pixels de la face supérieure ou inférieure
|
||||
du bloc place la sculpture au centre. Les limites de cette zone sont incluses.
|
||||
Autour, le bord horizontal le plus proche reste choisi ; une pose latérale
|
||||
continue à s'appuyer contre le bloc support. L'orientation suit le regard.
|
||||
L'aide du Clay Workshop explique les deux possibilités en français et anglais.
|
||||
|
||||
Le centrage utilise l'emprise réelle des voxels après rotation, en ignorant les
|
||||
marges transparentes. Tous les sommets restent sur la grille de 1/16 de bloc.
|
||||
Une dimension impaire ne peut pas être parfaitement symétrique dans un bloc de
|
||||
16 pixels sans décaler ses voxels : elle prend donc le centrage entier le plus
|
||||
proche, avec un pixel de différence entre les deux marges. Aucune position à
|
||||
un demi-voxel n'est introduite.
|
||||
|
||||
## Compatibilité
|
||||
|
||||
La pose réutilise `anchor=center`, déjà présent depuis beta.113. Aucun nouvel
|
||||
état, identifiant, composant ou format de sauvegarde. Les sculptures existantes
|
||||
gardent leur ancrage ; casser et reposer choisit l'emplacement visé. Le rendu,
|
||||
les couleurs, les chapeaux et les collisions restent inchangés. Aucun parcours
|
||||
de monde existant, aucune régénération. Client et serveur doivent utiliser
|
||||
ensemble la nouvelle version pour le choix de pose.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours natif `Statuary106ClientChecks`, avec `Sculpture113ClientChecks`,
|
||||
passe en **54 s** sur Minecraft 26.3/macOS :
|
||||
|
||||
- Vrais clics de pose centrée dans quatre orientations, sur client et serveur,
|
||||
puis pose contre les quatre bords sans modifier les couleurs du modèle.
|
||||
- Limites incluses et points immédiatement de part et d'autre de la zone,
|
||||
faces supérieure/inférieure et positions positives/négatives ; faces
|
||||
latérales toujours calées contre le support.
|
||||
- 5 120 combinaisons de largeur/profondeur, orientation et ancrage : géométrie
|
||||
effectivement rendue sur la grille exacte, emprise sans dépassement,
|
||||
centrage pair/impair et marges transparentes.
|
||||
- Sauvegarde/reconnexion des sculptures centrées et aux bords, sans fichier
|
||||
source ; rotations/miroirs, anciens états, fabrication et hotbar vérifiés.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `SCULPTURE119_PASS`, `STATUARY113_PASS` et
|
||||
`SCULPTURE113_GEOMETRY_PASS` dans `build/sculpture119-client.log`.
|
||||
Captures conservées dans `build/sculpture119-evidence/`, vue centrée relue.
|
||||
Monde plat jetable de graine 106 ; aucun monde personnel ouvert. Pas de
|
||||
validation Windows ni de test depuis deux ordinateurs.
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA ;
|
||||
le parcours natif utilise le serveur intégré.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 24 s**, 125 tâches. Les sources JAR correspondent aux sources
|
||||
du dépôt ; seuls `SculptureAnchor.class` et le texte d'aide FR/EN diffèrent
|
||||
parmi les classes et ressources de production de beta.118. Les deux packs
|
||||
contiennent le même JAR, sans monde ni fixture de test.
|
||||
|
||||
- Sanctuary-beta.119.mrpack : 10216447 octets, SHA-256
|
||||
`ce29702eaaac7301c77fb6af63a76c6bc1c0c704313ce1b8243c95b5fe59d893`.
|
||||
- Sanctuary-Test-beta.119.mrpack : 10235369 octets, SHA-256
|
||||
`a1a2845f9678b1144d7d9eab4867970cc1776fe0ad5d51722c8110b7c1d4c682`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`d353bd89b16c53ee1ee48836fa2ff4b311249b280992300c28bf03048d805bff`.
|
||||
|
||||
Reçu local : `build/sculpture119-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.119](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.119)
|
||||
est publiée depuis `3fd64e1f2a77ea2584b087d6339cf605d8ee0e9b`. Le tag exact et les
|
||||
artefacts sont immuables ; leurs téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `5e823af7807f524f6394acad402fe62328c7e4ab`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.119 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés, sans
|
||||
ouvrir de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.119/` de l'instance existante. Reçus locaux :
|
||||
`build/sculpture119-isolated.json` et `build/sculpture119-prism.json`.
|
||||
@@ -0,0 +1,102 @@
|
||||
# beta.116 — Sculptures à partir des textures de blocs
|
||||
|
||||
Ticket sur `codex/sculpture-materials-beta116`, comprenant aussi le correctif
|
||||
Fourneau beta.115 dans la livraison commune.
|
||||
|
||||
Le Clay Workshop accepte les objets-blocs comme matière. Le client prélève une
|
||||
palette bornée dans la texture du bloc issue du pack actif, avec sa teinte, puis
|
||||
le modèle conserve sa géométrie et transpose ses valeurs claires/sombres dans
|
||||
cette palette. L'aperçu et le résultat utilisent le même calcul. Un bloc est
|
||||
consommé par sculpture ; les neuf cases de hotbar restent les seules accessibles.
|
||||
|
||||
## Contrat des données et du réseau
|
||||
|
||||
Les sculptures existantes conservent exactement leurs voxels sauvegardés :
|
||||
schéma 1 inchangé, identifiants `sanctuary:*` stables. Les nouvelles sculptures
|
||||
stockent elles aussi leurs couleurs finales dans ce format. Aucun chargement
|
||||
ne recolore les objets ou constructions existants. Orientation et ancrage
|
||||
beta.113 inchangés, sans migration ni modification d'un monde personnel.
|
||||
|
||||
La sélection du menu ajoute l'identité de la matière et une palette de 256
|
||||
couleurs opaques au maximum. Ce sont des données d'apparence fournies par le
|
||||
client, comme le modèle déjà importé ; elles n'octroient ni objets ni propriétés
|
||||
de bloc. Le serveur vérifie le menu, son jeton, l'accès à l'atelier, les limites
|
||||
et l'objet effectivement présent. Il applique lui-même la palette et facture
|
||||
chaque résultat. Un changement de matière invalide immédiatement l'ancien
|
||||
résultat jusqu'à réception de la sélection correspondante. Les deux côtés
|
||||
doivent utiliser beta.116. Aucun nouveau format de sauvegarde.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`Sculpture116ClientChecks` passe en 1 min 7 s, sur un nouveau monde plat de
|
||||
Minecraft 26.3, graine 116, puis la régression complète de l'atelier :
|
||||
|
||||
- Les 1 207 objets-blocs enregistrés ont une palette opaque, non vide et bornée.
|
||||
- Fabrication payée avec pierre, planches de chêne, or, laine rouge, verre,
|
||||
feuilles de chêne, argile et ardoise ; aperçu identique au résultat serveur.
|
||||
- Conservation des tons clairs/sombres du modèle et de ses coordonnées, sans
|
||||
convertir les 16 argiles colorées en une simple couleur unie.
|
||||
- Une matière différente efface aussitôt l'ancien résultat ; une sélection
|
||||
périmée ou un jeton incorrect ne fabrique rien et ne consomme aucun objet.
|
||||
- Les conteneurs contenant des objets sont refusés ; ils peuvent servir de
|
||||
matière une fois vidés. Les objets qui ne sont pas des blocs sont refusés.
|
||||
- Bornes et opacité des palettes, véritable rechargement des ressources,
|
||||
neuf cases de hotbar, stock plein, consommation exacte et restitution.
|
||||
- Poses selon les quatre regards, accroche au bord, grille de voxels,
|
||||
casse/repose, sauvegarde et reconnexion après suppression du fichier GLB.
|
||||
- Interface FR/EN aux échelles 2/3, génération de plan au Métabli inchangée.
|
||||
|
||||
Marqueurs `SCULPTURE116_PALETTES`, `SCULPTURE116_MATERIALS_PASS`,
|
||||
`SCULPTURE116_PASS` et régressions 106/111/112/113 dans
|
||||
`build/sculpture116-client.log`. Captures relues dans
|
||||
`build/sculpture116-evidence/`. Un libellé est raccourci en « Bloc » pour éviter
|
||||
le contact avec « Sculpture » dans le cadre compact.
|
||||
|
||||
La palette vient de la texture représentative du modèle de bloc du pack actif,
|
||||
avec la teinte locale ; une texture animée utilise sa première image. Les blocs
|
||||
sans texture lisible utilisent leur couleur de carte. Il s'agit d'un transfert
|
||||
de couleurs sur les voxels opaques, sans transparence, émission ou fonction
|
||||
spéciale du bloc d'origine. Les sculptures existantes gardent leurs couleurs.
|
||||
|
||||
Essais natifs macOS avec serveur intégré ; pas de client Windows ni de test
|
||||
LAN à plusieurs joueurs. Le serveur dédié de GameTest reste exclu selon le
|
||||
refus antérieur de son EULA. Aucun monde personnel ouvert.
|
||||
|
||||
La régression native du Fourneau passe également dans beta.116, en 42 s :
|
||||
`build/sculpture116-furnace.log`, captures FR/EN dans
|
||||
`build/sculpture116-furnace-evidence/`. Les 27 cases, le glisser-déposer, les
|
||||
combustibles et les états de cuisson restent conformes au ticket beta.115.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en 2 min 23 s : 125 tâches, dont 105 exécutées. Sources Java concordantes avec
|
||||
les JAR sources, versions alignées et intégrité des deux MRpack vérifiées.
|
||||
Le JAR conserve à l'identique le correctif Fourneau beta.115 ; seuls les
|
||||
classes de matière de sculpture et libellés FR/EN changent dans la production.
|
||||
Les classes de test et modèles GLB de test sont absents des archives.
|
||||
|
||||
- `Sanctuary-beta.116.mrpack` : 10189831 octets, SHA-256
|
||||
`7df04a6e2a57e76a55ca57bd7520b05cbb573392690b401b868837498807d252`.
|
||||
- `Sanctuary-Test-beta.116.mrpack` : 10208754 octets, SHA-256
|
||||
`f4c9293e20384b05366d092fc7ee1abea275940a02373ee4036f73f0caa43747`.
|
||||
- JAR Sanctuary :
|
||||
`76a9205783c8b18506c32c9eef6c481d95fd3ba0746a2bdd558a194534a51a32`.
|
||||
|
||||
Reçu local : `build/sculpture116-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.116](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.116)
|
||||
est publiée avec le JAR, les deux MRpack et l'amorçage Prism. Le tag immuable
|
||||
désigne `e3ca9fcd6383f7646e4fea7e5098e7097ebb30aa` ; téléchargements publics
|
||||
et empreintes vérifiés. Le canal packwiz avance au commit
|
||||
`c741870160f8a21f0a9793f20f0e624ebdbacdb6`.
|
||||
|
||||
Deux synchronisations isolées, puis deux dans l'unique instance **Sanctuary
|
||||
Beta**, réussissent. La seconde passe laisse les fichiers gérés inchangés,
|
||||
avec un seul JAR Sanctuary beta.116. Minecraft 26.3, Loader 0.19.5 et Fabric
|
||||
API 0.160.5+26.3 conservés. Les 923 fichiers personnels et réglages suivis
|
||||
restent identiques ; aucun monde personnel n'a été ouvert. Sauvegarde des
|
||||
fichiers remplacés dans `sanctuary-backups/before-beta.116/` de cette instance.
|
||||
Reçus : `build/sculpture116-isolated.json`, `build/sculpture116-prism.json`.
|
||||
@@ -0,0 +1,108 @@
|
||||
# SCULPT-11 — Placement, éclats et modèles découverts — beta.125
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-placement-beta125`.
|
||||
|
||||
Le point visé choisit neuf positions horizontales (centre, quatre bords et quatre
|
||||
coins) et trois hauteurs (bas, milieu et haut). Sur un support, la sculpture
|
||||
repose au bas de son bloc ; sous un plafond, elle touche le haut ; sur une face
|
||||
latérale, elle suit la hauteur visée et reste contre le support. Les sculptures
|
||||
restent debout et s'orientent selon le regard horizontal du joueur.
|
||||
La bande centrale occupe les huit pixels médians, limites incluses. Toute pose
|
||||
reste sur la grille 1/16 : une dimension impaire utilise le centre entier le
|
||||
plus proche, jamais un demi-voxel. Rendu, boîte unique de sélection et collision
|
||||
partagent les mêmes offsets. La lumière conserve les règles de beta.122.
|
||||
|
||||
## Compatibilité additive des états existants
|
||||
|
||||
Les cinq anciennes valeurs `anchor` gardent exactement leurs noms. Quatre
|
||||
valeurs de coin sont ajoutées. La nouvelle propriété `vertical_anchor` accepte
|
||||
`bottom`, `center`, `top` et `original`, valeur par défaut réservée à la
|
||||
compatibilité. La lecture d'un ancien bloc sans cette propriété conserve
|
||||
l'ancienne hauteur, même si le modèle a des marges transparentes en bas.
|
||||
Le bloc possède 144 états : 108 placements nouveaux et 36 états de compatibilité.
|
||||
|
||||
Le schéma 1 des sculptures, leurs données de voxels et tous leurs identifiants
|
||||
restent inchangés. Aucun monde n'est parcouru ou réécrit, aucun chunk régénéré.
|
||||
Les nouveaux ancrages nécessitent beta.125 ; le retour à une version antérieure
|
||||
ne garantit pas leur position. Ce contrat autorise l'ajout compatible des états,
|
||||
pas une migration destructive des sauvegardes personnelles.
|
||||
|
||||
## Particules
|
||||
|
||||
Les coups et la casse émettent des éclats de la couleur des surfaces voxelisées
|
||||
réellement affichées, avec leur orientation et leur ancrage. Un coup vise la
|
||||
surface réelle la plus proche du point sélectionné, même devant un trou visuel.
|
||||
La casse échantillonne au plus 64 faces ; aucun calcul de texture n'est refait.
|
||||
Les éclats utilisent une taille de voxel, la gravité et la lumière natives.
|
||||
Une petite mémoire par monde (128 entrées de 64 faces, validité 40 ticks) conserve les
|
||||
surfaces si la suppression du bloc précède l'événement de casse. Les autres
|
||||
blocs gardent leurs particules habituelles.
|
||||
|
||||
## Clay Workshop
|
||||
|
||||
L'onglet Animaux utilise exactement les découvertes serveur du catalogue de
|
||||
statues (rencontre, victoire ou mort face à l'espèce). Le menu transmet cette
|
||||
liste à chaque ouverture, sans dépendre d'une visite préalable au Métabli.
|
||||
Le modèle natif et sa texture sont capturés localement, voxelisés en miniature,
|
||||
puis recolorés par la matière choisie. Le résultat reste fabriqué et payé côté
|
||||
serveur. Les sélections d'espèces non découvertes sont refusées. L'onglet Imports
|
||||
conserve les fichiers GLB. Aucun nouveau format de sculpture n'est introduit.
|
||||
Libellés FR et EN.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours natif `Sculpture125ClientChecks` passe en **1 min 2 s**, client
|
||||
Minecraft 26.3 sur macOS et serveur intégré jetable (graines 125 et 122).
|
||||
|
||||
- 54 clics réels sur les six faces : centres, bords, coins et hauteurs.
|
||||
- 432 contextes de pose, coordonnées positives/négatives, quatre orientations,
|
||||
rotations et miroirs ; 144 états valides.
|
||||
- 20 anciens états sans `vertical_anchor` conservent leur hauteur et marges.
|
||||
- Couleur exacte des éclats au point frappé, surfaces réellement occupées,
|
||||
limite de 64 fragments, événement après suppression et particules normales
|
||||
d'un bloc de pierre témoin.
|
||||
- Atelier : découvertes par rencontre, victoire et mort, transmission native
|
||||
du menu, refus serveur d'une espèce inconnue, trois modèles natifs (vache,
|
||||
mouton, cochon), paiement d'un bloc en survie et import GLB conservé.
|
||||
- Sauvegarde et reconnexion avec les nouveaux coins et hauteurs.
|
||||
- 576 formes comparées aux limites du maillage affiché, boîte unique partagée,
|
||||
cache réutilisé et 12 arêtes ; 9 216 empreintes orientées sur grille entière.
|
||||
- 75 sculptures superposées : ciel à 15, lumière de bloc à 13 après traversée,
|
||||
pièce noire à 0. Pose dans le joueur refusée, même devant un trou visuel.
|
||||
|
||||
Marqueurs dans `build/sculpture125-client.log`. Captures de plafond, murs et
|
||||
atelier relues dans `build/sculpture125-evidence/`. Aucun essai Windows,
|
||||
client distant ou shader tiers ; trois espèces vérifiées visuellement, sans
|
||||
prétendre avoir inspecté chaque espèce. Aucun benchmark FPS global.
|
||||
|
||||
Le GameTest dédié demeure exclu conformément au refus antérieur de son EULA.
|
||||
Les mondes de test sont jetables ; aucun monde personnel n'est ouvert.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 16 s**, 126 tâches (110 exécutées, 16 à jour). Les sources
|
||||
des JAR correspondent exactement au dépôt. La comparaison avec beta.124
|
||||
limite les différences aux classes de placement, particules et atelier prévues,
|
||||
aux nouveaux libellés FR/EN, au mixin client et à la source blanche de particules.
|
||||
Les packs normal/Test partagent le même JAR, sans monde ni fixture ; le template
|
||||
reproduit les ressources du mod et du pack intégré.
|
||||
|
||||
- Sanctuary-beta.125.mrpack : 10286360 octets, SHA-256 `9277e59c640c40392ab3750b402bb840020884878f6ece6978e6c4653e18655e`.
|
||||
- Sanctuary-Test-beta.125.mrpack : 10305282 octets, SHA-256 `9e3dbfa14b5382ba4eb62a78fa7d2161758c2f0e1fdc50fbd5ed776fd4ce32c5`.
|
||||
- JAR Sanctuary : SHA-256 `b38b75b900dd8cbe015318c739d5547fd7eac52f4337fa517e98f3f6f0ce3b46`.
|
||||
|
||||
Reçu local : `build/sculpture125-artifact.json`. Les deux packs et le template
|
||||
sont copiés et vérifiés dans le dossier habituel `sanctuary-beta/build/`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.125](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.125)
|
||||
est publiée depuis `4ec5b0d028a23a9f753770988ffba61015e2e49a`.
|
||||
Tag et artefacts immuables, téléchargements publics vérifiés. Canal packwiz :
|
||||
`47eb833911a96eef0b13805dafe65b2ab7b95c87`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans l'instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR Sanctuary beta.125 est actif, conforme au hash vérifié ;
|
||||
le second passage ne change rien. Les **923 fichiers personnels et réglages
|
||||
suivis** restent inchangés, sans ouverture de monde personnel. Copie préalable
|
||||
sous `sanctuary-backups/before-beta.125/`. Reçus :
|
||||
`build/sculpture125-isolated.json` et `build/sculpture125-prism.json`.
|
||||
@@ -0,0 +1,108 @@
|
||||
# SCULPT-09 — Forme physique et lumière — beta.122
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-shape-light-beta122`.
|
||||
Le créateur demande des bounding boxes adaptées aux sculptures et la suppression
|
||||
des ombres cubiques visibles lorsqu'on en pose plusieurs côte à côte.
|
||||
|
||||
La sélection, le contact avec les entités et les collisions suivent les voxels
|
||||
occupés de chaque sculpture, y compris les creux. Le rendu et la forme physique
|
||||
partagent leur placement entier sur la grille 1/16, après orientation et ancrage.
|
||||
Le centre, les quatre bords et les dimensions impaires gardent leur alignement.
|
||||
La pose vérifie aussi les voxels portés avant que l’entité de bloc existe :
|
||||
elle refuse une intersection avec le joueur et autorise les espaces libres.
|
||||
La forme est construite directement sur une grille native 16³ et mise en cache
|
||||
par entité de bloc, modèle et état ; aucun calcul complet à chaque interrogation.
|
||||
|
||||
Les sculptures transmettent la lumière et ne sont plus utilisées comme cubes
|
||||
pleins par l'occlusion ambiante. Leurs faces restent éclairées par le monde,
|
||||
sans émission de lumière et sans rendu à luminosité maximale. Cette règle est
|
||||
commune aux sculptures : l'éclairage natif stocké par bloc ne permet pas une
|
||||
ombre distincte pour chaque petit voxel. L'atelier d'argile reste inchangé.
|
||||
|
||||
Aucun identifiant, blockstate, format d'item ou schéma sauvegardé ne change.
|
||||
Les formes sont recalculées à partir des voxels existants ; aucune migration,
|
||||
aucun parcours de monde ni régénération de chunk. Les données inconnues gardent
|
||||
leur conservation opaque et une petite base sélectionnable pour récupérer l'item.
|
||||
Le travail de documentation déjà présent dans l'autre branche reste intact.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours `Sculpture122ClientChecks` passe en **37 s** sur Minecraft 26.3,
|
||||
client macOS et serveur intégré, dans un monde plat jetable de graine 122 :
|
||||
|
||||
- Pose par la commande native d'utilisation ; sélection du pilier et visée
|
||||
à travers le trou d'une arche jusqu'au bloc derrière, captures relues.
|
||||
- Requêtes natives de collision côté serveur et client : voxels occupés solides,
|
||||
espace vide libre, aucune profondeur artificielle d'un bloc entier.
|
||||
- Pose refusée dans le joueur, sans consommer l'objet ; petite sculpture acceptée
|
||||
dans la partie libre du même bloc, sans intersection avec le joueur.
|
||||
- **80 cas** confrontent le volume et les faces de collision au maillage réellement
|
||||
émis : arche creuse, marges vides, cube de 4 096 voxels, voxel unique, quatre
|
||||
orientations et cinq ancrages. Chaque interrogation répétée réutilise la forme.
|
||||
- Régression de **5 120 placements** du rendu existant : dimensions paires/impaires,
|
||||
orientations et ancrages, plus marqueurs de direction et modèles avec marges.
|
||||
- Changement d'orientation/ancrage puis changement des voxels sur la même entité :
|
||||
cache recalculé et forme client synchronisée.
|
||||
- **75 sculptures en trois couches** : skylight natif conservé à 15 partout.
|
||||
Un toit de pierre témoin produit toujours une ombre. La lumière d'un bloc
|
||||
traverse une sculpture avec l'atténuation de distance native (13 à deux blocs).
|
||||
- Le rendu reçoit la lumière réelle du monde : 15 au ciel, **0 dans une pièce
|
||||
fermée et obscure**. Aucun rendu artificiellement lumineux.
|
||||
- Sauvegarde/reconnexion : modèle, collisions et lumière conservés sur les
|
||||
deux côtés, sans fichier 3D source.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuarySculpture122ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `SCULPTURE122_PASS`, `SCULPTURE122_GEOMETRY_PASS`,
|
||||
`SCULPTURE122_PLACEMENT_PASS`, `SCULPTURE122_LIGHT_PASS` et
|
||||
`SCULPTURE113_GEOMETRY_PASS` dans `build/sculpture122-client.log` ; captures
|
||||
conservées dans `build/sculpture122-evidence/`. Le test prépare explicitement
|
||||
son sol à Y=0 : le preset plat nu commence à Y=-1.
|
||||
Aucun monde personnel ouvert ; pas d'essai Windows ni depuis deux ordinateurs,
|
||||
ni de garantie sur les ombres ajoutées par des shaders tiers.
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 13 s**, 126 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. Par rapport à beta.121, les différences de production se limitent
|
||||
aux classes des sculptures et à la nouvelle géométrie partagée (14 entrées,
|
||||
y compris le déplacement du sélecteur synthétique d'orientation). Toutes les
|
||||
ressources client, données serveur et configurations de mixins sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR, sans sauvegarde ni fixture.
|
||||
Le template beta.122 contient les mêmes ressources que beta.121, octet pour octet.
|
||||
|
||||
Reçu local : `build/sculpture122-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.122.mrpack` : 10267459 octets, SHA-256
|
||||
`b6e24d65797eb7f16670d2f3a591b5f2476820608f72241ecd37fb78a486008e`.
|
||||
- `Sanctuary-Test-beta.122.mrpack` : 10286380 octets, SHA-256
|
||||
`90487ee3ad4298fccb8c24e8781af4cbf043d88dc0379fba5c21d99733dac92d`.
|
||||
|
||||
JAR Sanctuary : SHA-256
|
||||
`ff8ebf89f39a64809fae6912253386793e9b5e66ec0adb9fa7521729893d406e`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.122](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.122)
|
||||
est publiée depuis `edc7fc3dfb117cde61be42c1d79b03af11f5dec6`. Tag et artefacts immuables,
|
||||
téléchargements publics vérifiés. Canal packwiz :
|
||||
`3d340bea0093b709c6018624795a1b3ca557b72d`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.122 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés.
|
||||
Aucun monde personnel ouvert. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.122/` de l'instance existante ; reçus
|
||||
`build/sculpture122-isolated.json` et `build/sculpture122-prism.json`.
|
||||
|
||||
Le ticket a été réalisé dans le worktree `sanctuary-sculpture-beta122` afin de
|
||||
préserver les changements de `docs/vision.md` et `docs/villageois-illageois.md`
|
||||
déjà présents sur la branche de documentation du checkout principal.
|
||||
|
||||
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-beta.122.mrpack).
|
||||
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Test-beta.122.mrpack).
|
||||
- [Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Template-beta.122.zip).
|
||||
@@ -0,0 +1,110 @@
|
||||
# beta.117 — Grain des textures et sculptures en chapeau
|
||||
|
||||
Ticket sur `codex/sculpture-texture-grain-beta117`, Minecraft 26.3.
|
||||
|
||||
En beta.116, une couleur du modèle source était associée à une seule nuance
|
||||
de matière. Un modèle uni restait donc uni malgré les pixels différents de la
|
||||
texture. La sélection utilise maintenant les textures des faces du modèle de
|
||||
bloc, avec leurs propres teintes du pack actif. Un échantillonnage de pixels
|
||||
répartit leurs couleurs exactes dans les voxels, sans couleur interpolée.
|
||||
|
||||
## Coût et comportement
|
||||
|
||||
Au maximum 32 matériaux de faces et 16 × 16 échantillons par matériau sont lus,
|
||||
puis ramenés à la palette existante de 256 entrées. Les répétitions conservent
|
||||
la fréquence des nuances. Les pixels transparents sont écartés ; les textures
|
||||
animées utilisent la première image. Les blocs sans texture lisible gardent
|
||||
le repli antérieur sur la texture représentative, puis la couleur de carte.
|
||||
|
||||
La sélection d'une nuance utilise un mélange entier déterministe des coordonnées
|
||||
de chaque voxel. Un modèle uni utilise toute la palette ; un modèle ombré
|
||||
utilise des plages de nuances qui suivent ses zones claires et sombres.
|
||||
Ce grain est un transfert de palette, pas une projection UV conservant les
|
||||
motifs ou les veinures du bloc. Les voxels restent opaques.
|
||||
|
||||
Le calcul utilise Java standard et les modèles/textures déjà chargés par
|
||||
Minecraft, sans bibliothèque supplémentaire. Aucun calcul de matière dans
|
||||
le rendu par image. Le client ne rééchantillonne qu'au changement de matière
|
||||
ou de ressources ; une rotation réutilise la palette. Le serveur conserve
|
||||
le modèle recoloré pour les fabrications suivantes avec la même sélection.
|
||||
|
||||
## Chapeaux et données
|
||||
|
||||
Le système de blocs portés retirait `BLOCK_ENTITY_DATA` des objets pour isoler
|
||||
les inventaires des coffres et fourneaux. Sur une sculpture, ce composant est
|
||||
la géométrie elle-même : le slot contenait encore un objet, devenu invisible.
|
||||
Les sculptures conservent désormais ce composant à l'équipement et au retrait.
|
||||
|
||||
Si une ancienne sculpture équipée a perdu ce composant, mais que sa session
|
||||
de chapeau possède encore le modèle, il est restauré depuis cette donnée
|
||||
serveur. La restauration ne fabrique aucune géométrie manquante. Les objets
|
||||
déjà retirés et dépourvus de toute copie du modèle ne sont pas reconstructibles
|
||||
par ce correctif. Les règles de contenu des coffres restent inchangées.
|
||||
|
||||
Identifiants, schéma 1 des sculptures, structure des accessoires et paquet
|
||||
de sélection inchangés. Les couleurs déjà sauvegardées restent intactes.
|
||||
Les deux côtés utilisent beta.117 pour le même calcul de prévisualisation et
|
||||
de fabrication. Aucun monde personnel ouvert ni migration de sauvegarde.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le scénario client natif complet passe en 2 min 1 s sur macOS avec serveur
|
||||
intégré, monde plat jetable de graine 116. Il reprend le scénario beta.116
|
||||
avec un GLB entièrement uni, plus les régressions de l'atelier 106/111/112/113 :
|
||||
|
||||
- 1 207 objets-blocs échantillonnés, huit fabrications payées avec aperçu
|
||||
identique au résultat serveur, consommation exacte d'un bloc.
|
||||
- Répartition 25 % / 75 % de deux couleurs de texture sur un modèle uni,
|
||||
aucune couleur inventée, conservation des zones claires et sombres.
|
||||
- Couleurs réellement présentes dans les textures natives des faces, avec
|
||||
plusieurs matériaux d'une bûche ; grain présent sur pierre, bois et minerai.
|
||||
- Motif indépendant de l'ordre des voxels, stable et sauvegardé ; rechargement
|
||||
des ressources, palette bornée, inventaire plein et sélections périmées.
|
||||
- Vrais clics des slots hotbar/chapeau, conservation exacte des composants
|
||||
et du nom personnalisé, icône et modèle porté visibles, retrait/rééquipement.
|
||||
- Restauration d'un objet privé de sa géométrie depuis une ancienne session
|
||||
équipée, puis sauvegarde et reconnexion du client avec le chapeau intact.
|
||||
- Pose, bord visé, orientation, casse/repose, reconnexion sans fichier source,
|
||||
menus FR/EN et génération du plan du Métabli inchangés.
|
||||
|
||||
Mesure indicative après échauffement, modèle maximal de 4 096 voxels,
|
||||
101 applications sur le Mac de développement : médiane **0,283 ms**, p95
|
||||
**0,971 ms**. Ce n'est pas une garantie de temps sur d'autres machines ; le
|
||||
calcul n'est exécuté ni chaque image ni à chaque objet fabriqué identique.
|
||||
|
||||
Marqueurs `SCULPTURE117_GRAIN_PASS`, `SCULPTURE117_HAT_PASS`,
|
||||
`SCULPTURE117_HAT_RECONNECT_PASS`, `SCULPTURE116_PASS` dans
|
||||
`build/sculpture117-final-client.log`. Captures relues dans
|
||||
`build/sculpture117-evidence/`. Pas d'essai Windows ou LAN. Le GameTest dédié
|
||||
reste exclu selon le refus antérieur de son EULA.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en **3 min 7 s**, 125 tâches dont 105 exécutées. JAR sources concordants,
|
||||
versions, contenu et intégrité des deux archives vérifiés. La comparaison
|
||||
avec beta.116 limite les changements de production au calcul des matières,
|
||||
à son cache dans le menu et au traitement des sculptures en chapeau.
|
||||
Les modèles de test et classes de GameTest sont absents de la livraison.
|
||||
|
||||
- `Sanctuary-beta.117.mrpack` : 10191137 octets, SHA-256
|
||||
`adbe5a40bbd050ebc84ff8aca61a7a3643dcf25ec8f2ac59b91561e48275cbbb`.
|
||||
- `Sanctuary-Test-beta.117.mrpack` : 10210059 octets, SHA-256
|
||||
`0dacb12b30bde66c647871101a4f4c6b0b58e7ceb29f7743398f8a569b958a6e`.
|
||||
- JAR Sanctuary :
|
||||
`1ce63f97f17c6098f3c86c6753f9b912bce39d88e05ba240423c38bdc97307ee`.
|
||||
|
||||
Reçu : `build/sculpture117-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.117](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.117)
|
||||
est publiée : JAR, MRpack normal/Test et amorçage Prism, téléchargements publics
|
||||
et empreintes vérifiés. Le tag immuable désigne
|
||||
`0f1efdec830342e25d3316b356330ad937d0d942`. Le canal packwiz avance au commit
|
||||
`a5b75a70f86e42afb8e06ba4daa9880d61c6373e`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans l'unique instance **Sanctuary
|
||||
Beta** réussissent, la seconde passe laissant les fichiers gérés identiques.
|
||||
Un seul JAR Sanctuary beta.117 est actif. Les 923 fichiers personnels et
|
||||
réglages suivis sont inchangés ; aucun monde personnel n'a été ouvert.
|
||||
Copie préalable des fichiers remplacés dans `sanctuary-backups/before-beta.117/`.
|
||||
Reçus : `build/sculpture117-isolated.json`, `build/sculpture117-prism.json`.
|
||||
@@ -0,0 +1,94 @@
|
||||
# SNOW-109 — Fonte saisonnière
|
||||
|
||||
Branche `codex/seasonal-snow-melt-beta109`, base beta.108, Minecraft 26.3.
|
||||
État : livraison locale vérifiée. Demande : ajouter la fonte saisonnière.
|
||||
|
||||
## Comportement livré
|
||||
|
||||
Neige déposée par la météo Sanctuary : retrait progressif d'une couche de haut
|
||||
en bas, bloc plein puis sept couches, jusqu'au sol. Deux blocs maximum pour
|
||||
l'accumulation restent la valeur par défaut confirmée. La fonte est active
|
||||
avec la météo Real Time, suspendue en hiver et sous un profil neige/averses de
|
||||
neige, même pendant ses pauses. Les biomes naturellement froids conservent
|
||||
leur neige. Printemps : une chance sur deux par sondage météo natif ; été :
|
||||
chaque sondage ; automne : une chance sur quatre. Aucun rattrapage hors ligne,
|
||||
ni chargement forcé de chunks ; seuls les sommets exposés des chunks simulés.
|
||||
À 20 ticks/s et `randomTickSpeed = 3`, une couche fond en moyenne en 3,4 minutes en été,
|
||||
6,8 au printemps et 13,7 en automne. Pas d'eau ni de butin lors de la fonte.
|
||||
Une règle serveur `sanctuary:seasonal_snow_melt`, active par défaut, permet
|
||||
d'arrêter la fonte sans désactiver la neige.
|
||||
|
||||
## Contrat de données et migration, avant implémentation
|
||||
|
||||
Ajout d'une attache Fabric persistante par chunk,
|
||||
`sanctuary:natural_snow_v1`, sans modifier les formats existants. Elle mémorise
|
||||
les positions des nouveaux dépôts avec le nombre de couches antérieures et
|
||||
le nombre attendu après dépôt (0 à 8). Les blocs restent les blocs Minecraft
|
||||
`minecraft:snow` et `minecraft:snow_block` ; pas de nouvel identifiant de bloc.
|
||||
L'absence de données signifie aucun dépôt suivi. Aucune recherche rétrospective,
|
||||
conversion ou réécriture de terrain au chargement : la neige d'avant beta.109
|
||||
est conservée car son origine n'est pas connue. Les sauvegardes personnelles
|
||||
restent fermées pendant le développement. Graine de test : 109.
|
||||
|
||||
Seules les couches ajoutées par les précipitations depuis beta.109 fondent.
|
||||
La neige construite, la neige générée et les couches antérieures à un dépôt
|
||||
sont préservées. Une modification extérieure du bloc (casse, placement,
|
||||
piston, commande) retire son suivi : le remplacement ne doit jamais hériter
|
||||
d'une autorisation de fonte. Une incohérence de suivi est oubliée sans fonte.
|
||||
Les informations suivent le cycle natif de sauvegarde/déchargement du chunk,
|
||||
sans fichier global ni lecture disque par colonne. Le retrait du mod laisse
|
||||
des blocs de neige natifs ; aucun effacement n'est lancé par migration.
|
||||
|
||||
## Vérifications ciblées
|
||||
|
||||
Le client natif lance les contrôles d'accumulation beta.108 (deux mondes neufs),
|
||||
puis un monde neuf de graine 109 pour la fonte et le recharge après fermeture.
|
||||
Les quatre saisons utilisent une horloge de test déterministe, sans commande
|
||||
supplémentaire dans le mod livré. Les sondages appellent le véritable
|
||||
`ServerLevel.tickPrecipitation`, sans accélération ajoutée au jeu distribué.
|
||||
|
||||
Cas vérifiés : bloc plein vers sept couches, fonte de haut en bas jusqu'au sol,
|
||||
neige construite et couches anciennes, dépôt météo sur une construction,
|
||||
casse/repose et ajout manuel de couches, toit et capuchon de neige manuel,
|
||||
activation/désactivation par gamerule, les quatre saisons, profils neige et
|
||||
pauses d'averses, biome froid et mode Vanilla, sauvegarde native des deux blocs
|
||||
suivis et de la gamerule, puis fonte après rechargement.
|
||||
|
||||
Le choix du sommet utilise la heightmap native mise à jour immédiatement,
|
||||
sans dépendre d'une propagation de lumière encore en attente après la fonte.
|
||||
Le suivi est invalidé après toute modification effective de bloc ; chaque
|
||||
nouveau dépôt ou retrait météo enregistre ensuite uniquement ses couches.
|
||||
Les instantanés de suivi sont immuables pour la sauvegarde asynchrone des chunks.
|
||||
|
||||
## Livraison — 17 septembre 2026
|
||||
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
|
||||
réussi, 125 tâches, 2 min 20 s.
|
||||
- `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryMelt109ClientTests=true -PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true` :
|
||||
réussi, 54 s ; marqueurs `SNOW108_NATIVE_PASS` et `MELT109_NATIVE_PASS`.
|
||||
- Java 25. Le GameTest sur serveur dédié reste exclu conformément au refus
|
||||
antérieur de son EULA ; les essais natifs utilisent le serveur intégré.
|
||||
- Archives ZIP, métadonnées, sources embarquées, mixins et traductions vérifiés
|
||||
contre l'archive immuable beta.108. Deux classes existantes modifiées, trois
|
||||
nouvelles classes plus la classe synthétique du switch de saison. Aucun
|
||||
test inclus dans le JAR. Assets identiques, sauf deux nouveaux libellés FR/EN.
|
||||
|
||||
[Pack normal](../build/Sanctuary-beta.109.mrpack) ·
|
||||
[Pack Test](../build/Sanctuary-Test-beta.109.mrpack).
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| Normal | `4f80292b97398e993d05e831682c879d2e6b40e7ab8cb385a15142b68c2d3ba4` |
|
||||
| Test | `c0c7363ec3a2a8c568b77692b65dd8a4138c247bc928adecd5626f83bb5097bb` |
|
||||
| JAR Sanctuary | `0b53f5c2d9b50890067c42a1c71b84bf70f0c6af90c876020955bbe426ff6ffa` |
|
||||
|
||||
Reçu : `build/melt109-artifact.json`. Journaux : `build/melt109-native.log`,
|
||||
`build/melt109-check-build.log`. Manifeste : `build/melt109-source-manifest.json`.
|
||||
|
||||
Assemblage isolé sur beta.108, avec accumulation et œufs beta.107 ; Statuaire
|
||||
beta.106 reste livré séparément. Sources intégrées au répertoire principal en
|
||||
préservant ses autres travaux et ses compteurs beta.106 ; le worktree de
|
||||
livraison utilise beta.109 dans les deux compteurs et le manifeste packwiz.
|
||||
Aucun tag, publication du canal, déploiement, installation Prism ou monde
|
||||
personnel modifié. Aucun essai Windows. La fonte n'est pas appliquée
|
||||
rétrospectivement à la neige ancienne dont l'origine est inconnue.
|
||||
@@ -0,0 +1,88 @@
|
||||
# SHADER-127 — intégration des ombres portées pixélisées
|
||||
|
||||
Demande du 17 septembre 2026 : livrer le shader dans **beta.127**.
|
||||
Branche `codex/shader-beta127`, socle publié beta.126, Minecraft 26.3 / Java 25.
|
||||
|
||||
La préparation locale [des ombres pixélisées](pixel-shadows-source122.md) est
|
||||
intégrée sans écraser les changements non publiés du dossier d'origine.
|
||||
Les gemmes, dernières textures de rubis/saphir, l'argile spéciale et les
|
||||
commandes de découverte de beta.126 sont conservées.
|
||||
|
||||
## Utilisation
|
||||
|
||||
**Options → Shaders → Sanctuary · Vanilla Light → Ombres portées pixélisées**.
|
||||
L'option est désactivée par défaut ; son activation reste un choix du joueur.
|
||||
Finesse : 8/16/32 pixels par bloc ; portée : 16/32/64 blocs. Défauts : 16 et 32.
|
||||
Les paramètres FR/EN s'appliquent immédiatement et persistent dans
|
||||
`config/sanctuary-shaders.json`. La mise à jour préserve les réglages personnels.
|
||||
Le rendu est natif au mod : aucun pack Iris, téléchargement supplémentaire ou
|
||||
nouvelle dépendance de rendu.
|
||||
|
||||
## Périmètre
|
||||
|
||||
Ombres solaires des formes d'occultation des blocs chargés, incluant dalles,
|
||||
escaliers et obstacles hors écran. Carte de profondeur GPU, grille ancrée dans
|
||||
le monde, atténuation selon soleil, pluie et brume. L'effet s'arrête la nuit,
|
||||
dans les dimensions sans soleil, dans les fluides ou avec un moteur externe.
|
||||
Le HUD et la main ne passent pas dans cet effet. La lumière de gameplay,
|
||||
les règles serveur, les sauvegardes et la génération ne sont pas modifiées.
|
||||
|
||||
La préparation limite le cache à 96 Mio et sa construction à quatre sections
|
||||
par image dans un budget souple de 3 ms. L'intégration supprime seulement une
|
||||
invalidation de bord exécutée deux fois à l'identique dans le cache de terrain.
|
||||
Les formes non occultantes, feuillages ajourés, eau/verre et entités animées ne
|
||||
projettent pas de nouvelles silhouettes. Les transparences déjà composées
|
||||
peuvent recevoir l'assombrissement du terrain situé derrière elles. Pas
|
||||
d'ombres de torches/lune ni de mesure de FPS revendiquée.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Client natif Minecraft 26.3 / Java 25 sur macOS, monde neuf plat, graine 122 :
|
||||
`PixelShadows122ClientChecks` réussit sur la beta.127 en **1 min 26 s**.
|
||||
Le nom de cette fixture historique est conservé pour suivre la même scène.
|
||||
|
||||
- Activation : 920 échantillons de sol assombris, 63 412 inchangés, aucun éclairci.
|
||||
- Pilier hors écran : 42 048 échantillons restent ombrés ; le retrait du pilier
|
||||
restitue la lumière sur les 42 048 et conserve les 23 616 déjà éclairés.
|
||||
La repose rétablit la même ombre sans désactiver le cache.
|
||||
- Trois finesses et trois portées, redimensionnement GPU, désactivation/libération,
|
||||
arrêt nocturne, reprise diurne et rechargement des ressources vérifiés.
|
||||
- Préférences persistantes et bornées ; captures FR/EN inspectées, sans texte
|
||||
tronqué. La scène pilier/dalle/escalier est également inspectée visuellement.
|
||||
|
||||
Logs : `build/shader127-client.log`. Captures :
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. Aucun monde personnel n'est ouvert. Aucun essai Windows/Vulkan ou FPS
|
||||
n'est revendiqué ; la validation graphique est celle du client natif local.
|
||||
|
||||
|
||||
Build `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest` réussi en **2 min 25 s**, 126 tâches. Les sources
|
||||
Java dans les archives correspondent aux sources du ticket. La comparaison
|
||||
avec le JAR beta.126 ne trouve que **21 entrées de production modifiées**,
|
||||
toutes liées au shader, à son initialisation, ses mixins et ses libellés.
|
||||
Les règles de jeu, données, textures de gemmes, sculptures et découvertes
|
||||
sont identiques. Normal et Test contiennent le même JAR ; le template exporte
|
||||
exactement les ressources du mod. Aucune fixture de test ni monde embarqué.
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `Sanctuary-beta.127.mrpack` | `23fa6ca0c5d1f1033e13e5370d669491b398d5a99e13f8bd1e983f998a24a20a` |
|
||||
| `Sanctuary-Test-beta.127.mrpack` | `f185f3a743ba23cd6c024d1c1e46587706bf4f3edb5e14d7e5dee522a1ebf2c2` |
|
||||
| `sanctuary-beta.127.jar` | `3ef3fab67f4f703864a1acc9ae2395c636d206f3128d3399140eaf045a46d5f3` |
|
||||
|
||||
## Publication et synchronisation
|
||||
|
||||
[Release beta.127](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.127)
|
||||
publiée depuis `00be1eb0d0bfe042442d90255385033a7765092f` ; artefacts immuables retéléchargés
|
||||
et vérifiés. Canal packwiz : `7fd24a43e01ee497a23311e3c78e1dd3dddd877a`.
|
||||
Deux synchronisations isolées puis deux dans l'instance existante
|
||||
**Sanctuary Beta** ont réussi. Les **923 fichiers personnels** suivis,
|
||||
y compris les préférences de shaders, sont conservés. Aucun monde personnel
|
||||
ouvert ; copie préalable dans `sanctuary-backups/before-beta.127/`.
|
||||
|
||||
Les packs normal/Test et le template sont aussi copiés et vérifiés dans
|
||||
`sanctuary-beta/build/`. Reçus locaux : `build/shader127-artifact.json`,
|
||||
`build/shader127-isolated.json`, `build/shader127-prism.json` et journal de
|
||||
publication `build/shader127-publication.log`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-128 — stabilité, lune et feuillage
|
||||
|
||||
Demandes du 17 septembre 2026 : réduire le jitter des bords et à midi, conserver
|
||||
des ombres de blocs nettes et un feuillage nuancé, ajouter une ombre lunaire
|
||||
fine et une transmission
|
||||
irrégulière du feuillage selon son épaisseur. Branche `codex/moon-foliage-beta128`,
|
||||
socle publié beta.127, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Les faces de voxels reçoivent l'ombre sur une grille fixe ; leur plan est
|
||||
stabilisé au 1/16 de bloc et les faces inclinées conservent leur normale. La
|
||||
carte de profondeur utilise un ancrage par 16 blocs avec alignement de ses
|
||||
texels. **Les ombres des blocs restent nettes** : le créateur a écarté leur
|
||||
adoucissement après l'essai visuel. Le filtrage est réservé au feuillage.
|
||||
Le soleil continue de se déplacer : ce mouvement naturel peut toujours faire
|
||||
avancer une ombre d'une case. L'objectif est de supprimer les oscillations
|
||||
liées à la caméra, pas de figer le temps.
|
||||
|
||||
Entre une élévation solaire de 0,92 et le zénith, l'intensité diminue doucement
|
||||
jusqu'à 25 % de sa valeur habituelle : alpha maximal 0,095 au lieu de 0,38.
|
||||
La lune réutilise la même carte et son angle natif ; alpha maximal 0,10 selon
|
||||
sa phase, nul à la nouvelle lune. Horizon, pluie, portée et brume atténuent
|
||||
les deux lumières. Pas de seconde passe de carte lunaire simultanée.
|
||||
|
||||
Les blocs du tag vanilla `leaves` projettent une couverture probabiliste fixe
|
||||
sur une grille de quart de bloc. Les faces internes restent présentes : plus
|
||||
le rayon traverse de couches, moins il a de chances de passer. Il s'agit d'une
|
||||
approximation statistique de l'épaisseur, pas d'un comptage des feuilles ni
|
||||
d'une lecture des trous de leur texture. Quatre échantillons bilinéaires sur
|
||||
un quart de bloc mélangent cette couverture
|
||||
en nuances, évaluées sur la grille pixélisée. Un masque R8 distingue feuilles
|
||||
et blocs dans la même passe de profondeur, pour garder les silhouettes opaques
|
||||
nettes. Les comparaisons suivent le plan récepteur pour éviter l'auto-ombrage
|
||||
sur les surfaces obliques. Le motif ne dépend ni du temps ni de la caméra et
|
||||
ne demande aucune lecture GPU côté CPU en production.
|
||||
Les budgets du cache restent 96 Mio, quatre sections et 3 ms souples par image.
|
||||
Le feuillage ajoute des faces ; son filtrage utilise seize comparaisons de
|
||||
profondeur et, selon le cas, autant de lectures du masque. Les ombres opaques
|
||||
pleines utilisent le chemin court. Le masque prend 4 Mio à la définition
|
||||
par défaut, 16 Mio au maximum ; aucune promesse de FPS n'est faite.
|
||||
|
||||
Réglages existants et libellés FR/EN conservés/actualisés. Les ombres restent
|
||||
optionnelles et désactivées par défaut. Règles, éclairage de gameplay, mondes
|
||||
et génération inchangés. Les limitations de beta.127 concernant les entités,
|
||||
fluides, verre et autres formes non occultantes subsistent.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`PixelShadows122ClientChecks`, fixture historique étendue, réussit en **1 min
|
||||
44 s** dans un monde neuf plat, graine 122, client natif macOS/OpenGL.
|
||||
|
||||
- Soleil : 920 échantillons de sol ombrés et 63 412 inchangés ; retrait/repose
|
||||
d'un pilier hors écran restitue puis rétablit son ombre.
|
||||
- Lune : ombre localisée mesurée ; arrêt des passes à la nouvelle lune.
|
||||
- Midi : ombre atténuée et **zéro échantillon de transition adoucie sur 65 664**
|
||||
dans la scène de contrôle du contour opaque.
|
||||
- Caméra : petits déplacements autour de l'ancien ancrage entier et du nouvel
|
||||
ancrage par 16 blocs, retour au point initial, petites rotations en vue
|
||||
oblique, instants avant/après midi et passage de 5999 à 6001 ticks.
|
||||
Comparaisons d'images avec tolérance de trois niveaux par canal et moins
|
||||
de 1 % d'échantillons changés pour ces petits mouvements.
|
||||
- Vue oblique : 19 637 échantillons ombrés, 46 027 inchangés sur le sol blanc.
|
||||
- Feuillage : assombrissement moyen mesuré d'environ **18,6 %** pour une couche,
|
||||
**31,7 %** pour quatre, contre **36,8 %** sous une couverture solide.
|
||||
Motif fixe dans le temps ; modification des couches invalide le cache actif.
|
||||
- Trois finesses/trois portées, désactivation/libération GPU, préférences
|
||||
persistantes, rechargement des ressources, textes FR/EN et captures inspectés.
|
||||
|
||||
Commande native : `./gradlew :sanctuary:runClientGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true` sous Java 25.
|
||||
Logs : `build/shader128-client.log` ; captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest` réussit en **2 min 24 s**, 126 tâches.
|
||||
Les sources archivées correspondent au ticket. **13 entrées de production**
|
||||
diffèrent de beta.127, limitées aux classes du shader, GLSL et libellés FR/EN.
|
||||
Gemmes, sculptures, découvertes, données et règles de jeu restent identiques.
|
||||
Normal et Test embarquent le même JAR ; le template correspond exactement aux
|
||||
ressources exportables. Aucun monde ni fixture de test embarqué.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de son
|
||||
EULA. Aucun monde personnel utilisé. Aucun essai Windows/Vulkan revendiqué.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `Sanctuary-beta.128.mrpack` | `84125b18366afde52c8cba4bc7007257b1c3d8b79d413338abe64d0e0ae8feaf` |
|
||||
| `Sanctuary-Test-beta.128.mrpack` | `6bfb10d8d63875bfb84e813d4f0a9faf5db30902cc0400deda0993c08b86e597` |
|
||||
| `sanctuary-beta.128.jar` | `c54cbae7efaa7059c8fd4d28801087983379958fe34eedff51682d106d649c6a` |
|
||||
|
||||
## Publication et synchronisation
|
||||
|
||||
[Release beta.128](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.128)
|
||||
publiée depuis `a4e96c0d99c976231d7ca92b1db6bdb6aa7083c6` ; les artefacts immuables ont été
|
||||
retéléchargés et vérifiés. Canal packwiz : `eba01726ee132a6e548d129fa7ee04f24c880917`.
|
||||
Deux synchronisations isolées puis deux dans l'instance existante
|
||||
**Sanctuary Beta** ont réussi. Les **923 fichiers personnels** suivis,
|
||||
y compris les réglages de shaders, sont conservés. Aucun monde personnel
|
||||
ouvert ; copie préalable dans `sanctuary-backups/before-beta.128/`.
|
||||
Packs normal/Test et template copiés et vérifiés dans `sanctuary-beta/build/`.
|
||||
|
||||
Reçus locaux : `build/shader128-artifact.json`, `build/shader128-isolated.json`,
|
||||
`build/shader128-prism.json` ; journal : `build/shader128-publication.log`.
|
||||
@@ -0,0 +1,119 @@
|
||||
# SHADER-129 — arêtes connectées et portées étendues
|
||||
|
||||
Demandes du 17 septembre 2026 : highlight optionnel, puis raccordement CTM de
|
||||
tous les blocs coplanaires, portée indépendante 16–256 blocs, ombres à 128/256,
|
||||
suppression de l'atténuation au zénith et du réalignement cyclique des ombres.
|
||||
Branche `codex/edge-highlights-beta129`, socle beta.128, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Dans **Options → Shaders → Sanctuary · Vanilla Light**, les reflets sur les
|
||||
arêtes sont désactivés par défaut. L'intensité va de 0 à 100 ; la valeur 30
|
||||
éclaircit au maximum de 12 %. Leur portée se règle indépendamment des ombres :
|
||||
**16, 32, 64, 128 ou 256 blocs**. Les anciens réglages restent conservés.
|
||||
|
||||
Le highlight raccorde les surfaces plates, y compris entre matériaux différents,
|
||||
sans dessiner les jonctions intérieures. Il suit les contours visibles du relief,
|
||||
y compris les dalles et escaliers. Il multiplie la couleur existante pour garder
|
||||
les pixels des textures et composer avec les ombres. Ce CTM géométrique utilise
|
||||
la profondeur déjà produite par Minecraft : aucune liste de blocs autorisés,
|
||||
aucun pack CTM à fournir, aucune exploration CPU du volume de 256 blocs.
|
||||
Une cassure/repose modifie le contour dès que le terrain natif est réaffiché.
|
||||
|
||||
Le trait proche suit la grille de 1/16 de bloc. À distance, la couverture devient
|
||||
partielle pour limiter le scintillement des traits plus petits qu'un pixel.
|
||||
La portée choisie s'efface progressivement dans son dernier quart et respecte
|
||||
la brume ainsi que la distance de rendu. Une passe plein écran, quatre sondes
|
||||
de profondeur et un tampon uniforme de 176 octets ; aucun retour GPU vers CPU
|
||||
en production. L'arrêt de l'option ou une intensité nulle libèrent ce tampon.
|
||||
|
||||
Ce n'est pas un remplacement des textures par des variantes CTM. Le calcul
|
||||
concerne les faces visibles alignées sur la grille : les faces hors écran,
|
||||
les surfaces transparentes absentes de la profondeur et les géométries
|
||||
arbitrairement inclinées ne sont pas reconstruites. À très grande distance,
|
||||
les détails plus petits qu'un pixel peuvent rester imperceptibles. Aucun
|
||||
chargement de chunk forcé, aucune promesse de FPS.
|
||||
|
||||
Les ombres proposent les cinq mêmes portées. La carte reste plafonnée à
|
||||
4096 × 4096 : les grandes distances réduisent sa précision spatiale. Le cache
|
||||
garde ses limites de 96 Mio, quatre sections et 3 ms souples de construction
|
||||
par image ; la réconciliation des chunks est limitée aux ticks et changements
|
||||
de caméra/terrain plutôt qu'à toutes les images.
|
||||
|
||||
L'intensité solaire reste pleine au zénith. Les contours opaques restent nets,
|
||||
le filtrage du feuillage et les ombres légères de la lune sont conservés.
|
||||
Le réalignement de la grille lumineuse a lieu au changement d'ancrage de la
|
||||
caméra, avec transfert de phase par texels entiers. La rotation du soleil ne
|
||||
réarrondit plus la projection de l'origine mondiale à chaque image, source
|
||||
de sauts périodiques amplifiés aux grandes coordonnées. Le soleil continue
|
||||
d'avancer ; une ombre pixélisée peut naturellement changer de case. Cette
|
||||
correction ne modifie pas l'horloge serveur ni sa cadence native.
|
||||
|
||||
Textes FR/EN. Aucun changement de règles, sauvegardes, génération, gemmes,
|
||||
sculptures ou découvertes.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le client natif réussit en **2 min 41 s**, dans un monde neuf plat, graine 122,
|
||||
sur macOS / Apple M1 / OpenGL :
|
||||
|
||||
- CTM : 596 échantillons éclaircis sur le contour et 65 068 conservés ; les
|
||||
20 centres et 20 jonctions coplanaires ciblés restent intacts, y compris
|
||||
au changement de matériau. L'intensité 90 produit environ trois fois le gain.
|
||||
- Ouverture d'un trou : 716 échantillons éclaircis ; la fermeture restitue
|
||||
l'image connectée. Au-delà de 16 blocs, le réglage 16 supprime le contour ;
|
||||
le réglage 64 le rétablit (254 échantillons éclaircis).
|
||||
- Cinq portées CTM et cinq portées d'ombres, persistance/clamp, migration des
|
||||
anciennes préférences, arrêt/libération, rechargement, caméra et vue oblique
|
||||
avec ombres. Captures françaises/anglaises inspectées ; suppression ensuite
|
||||
d'un préfixe redondant dans le libellé de portée.
|
||||
- Soleil, lune, nouvelle lune, ombres opaques nettes à midi et transmission
|
||||
du feuillage restent vérifiés. Casters hors écran et retrait/repose actifs.
|
||||
- 2 000 petits pas solaires aux coordonnées ±100 000 vérifient l'absence de
|
||||
saut de translation de la carte ; changement d'ancrage par texels entiers.
|
||||
La séquence du vrai service realtime suit 294 images : angle avancé de
|
||||
0,000694 radian, aucun rebond dans les 65 664 échantillons de sol suivis.
|
||||
Sur cette courte séquence, aucune case opaque n'a changé ; cela ne constitue
|
||||
pas une mesure de mouvement visible sur une journée entière.
|
||||
|
||||
La fixture initiale capturait parfois le terrain avant la fin de sa construction
|
||||
native : elle attend désormais la réception de toute la plateforme et la fin
|
||||
compilée des sections. Aucun changement de gameplay pour contourner ce problème.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log `build/shader129-client.log`, captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 17 s**, 126 tâches. Une dernière passe d’assemblage intègre
|
||||
la simplification du libellé de portée (2 min 18 s). Les archives sont vérifiées :
|
||||
**17 entrées de production** diffèrent de beta.128, limitées au shader et à ses
|
||||
interfaces. Même JAR dans normal/Test, sources archivées conformes, ressources
|
||||
du template exactes, assets des gemmes inchangés. Aucun monde ni test embarqué.
|
||||
Le serveur GameTest dédié
|
||||
reste exclu conformément au refus antérieur de son EULA. Aucun monde personnel
|
||||
utilisé pour les essais, aucun résultat Windows/Vulkan revendiqué.
|
||||
|
||||
|
||||
Archives vérifiées dans `build/` et recopiées dans le dossier de livraison
|
||||
habituel `sanctuary-beta/build/` :
|
||||
|
||||
- Normal : 10 353 946 octets, SHA-256
|
||||
`2c7735194faab8a699004e69ad543ce6e8fabdebe297b759bffd31fea2dcc2e8`.
|
||||
- Test : 10 372 872 octets, SHA-256
|
||||
`cca572ee313d1d7e66ed745bb48bac62490998760d9b7bbfc7a1f6d60c430dce`.
|
||||
- JAR Sanctuary :
|
||||
`ca3e2ff4e0717a3a54c1d57478d43aade39d001b71c4eec745d76f010f7def54`.
|
||||
|
||||
[Release beta.129](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.129)
|
||||
publiée depuis `35103a7ea8635dc4ed26293f00e86d99aaacfca8`. Canal packwiz
|
||||
`2eaa0130a4cd475c598beefdab40f48fc8681ab7`. Deux synchronisations isolées puis deux synchronisations
|
||||
dans l'instance **Sanctuary Beta** ont réussi. Les **923 fichiers personnels**
|
||||
suivis, dont les réglages de shader, sont inchangés. Aucun monde personnel ouvert.
|
||||
Sauvegarde préalable des fichiers gérés : `sanctuary-backups/before-beta.129/`.
|
||||
Le JAR installé a le hash vérifié ci-dessus. Reçus locaux :
|
||||
`build/shader129-artifact.json`, `build/shader129-isolated.json` et
|
||||
`build/shader129-prism.json`. Le checkout initial avec ses modifications en
|
||||
cours a été préservé ; seule la copie des archives versionnées rejoint son
|
||||
répertoire `build/`.
|
||||
@@ -0,0 +1,131 @@
|
||||
# SHADER-130 — bloom et précision commune
|
||||
|
||||
Demandes du 17 septembre 2026 : bloom réglable du soleil et des sources
|
||||
lumineuses ; inclusions émissives du fer, cuivre, redstone, lapis, diamant,
|
||||
émeraude, rubis, saphir, quartz et or ; réglage commun « Précision » pour
|
||||
les ombres et le highlight. Branche `codex/bloom-beta130`, socle beta.129,
|
||||
Minecraft 26.3 / Java 25.
|
||||
|
||||
## Résultat
|
||||
|
||||
Dans **Options → Shaders → Sanctuary · Vanilla Light**, le bloom possède
|
||||
un interrupteur, une intensité et une diffusion réglables de 0 à 100.
|
||||
Les minerais ont leur propre interrupteur et intensité. Le bloom reste
|
||||
optionnel, désactivé initialement, avec une intensité préparée à 35, une
|
||||
diffusion à 50 et une émission des minerais à 50. Une intensité globale
|
||||
nulle arrête les passes et libère les ressources de cet effet.
|
||||
|
||||
Les nouveaux réglages par défaut sont **Pixel Shadows activé**, distance
|
||||
des ombres **128 blocs**, highlight **32 blocs** et **Précision 16 pixels
|
||||
par bloc**. La précision unique propose 8, 16 et 32 et contrôle simultanément
|
||||
ombres et arêtes. La clé JSON historique `shadowPixels` est conservée :
|
||||
les préférences déjà personnalisées ne sont pas écrasées.
|
||||
|
||||
Les vingt textures de minerais sont analysées depuis les ressources actives.
|
||||
La palette de pierre, de deepslate ou de netherrack est comparée aux pixels
|
||||
colorés des inclusions, avec un traitement du quartz et de l'or du Nether.
|
||||
Les reflets clairs accolés aux inclusions sont conservés. Le masque est
|
||||
calculé une fois par texture et conservé en cache jusqu'au rechargement des
|
||||
ressources. Une texture optionnelle `<nom>_e.png` peut préciser ce masque
|
||||
pour une texture candidate ; sa luminance maximale multipliée par l'alpha
|
||||
remplace la détection automatique. Les couleurs affichées viennent toujours
|
||||
de l'atlas Minecraft actif, y compris son animation.
|
||||
|
||||
Les sources sont les faces visibles des modèles de blocs lumineux, la lave,
|
||||
le soleil et la lune. Les lampes éteintes ne produisent pas de halo. Le
|
||||
niveau lumineux natif pondère les sources ordinaires ; l'émission des minerais
|
||||
est purement graphique. La profondeur du terrain élimine les inclusions
|
||||
cachées et masque le soleil derrière les constructions. Le cœur émissif
|
||||
conserve ses pixels nets, tandis qu'un halo séparé est diffusé à résolution
|
||||
réduite puis composé sur l'image.
|
||||
|
||||
Aucun niveau de lumière serveur, spawn, règle de jeu, format de sauvegarde,
|
||||
identifiant ou paramètre de génération ne change. Aucun monde personnel
|
||||
n'est ouvert pour les essais.
|
||||
|
||||
## Coût et limites
|
||||
|
||||
La géométrie suit uniquement les sections natives visibles et déjà chargées.
|
||||
Leur palette permet d'écarter rapidement les sections sans source ; le cache
|
||||
invalide les sections touchées lors des modifications. Construction répartie
|
||||
sur les images, au plus quatre sections démarrées et un budget souple de
|
||||
3 ms, cache GPU plafonné à 32 Mio et 16 384 faces par section. Une section
|
||||
qui dépasse les limites est omise. Aucun chargement de chunk forcé.
|
||||
|
||||
Le masque GPU est plafonné à 256 textures, chacune échantillonnée au plus en
|
||||
64 × 64 ; les textures natives 16 × 16 conservent leurs texels. Les masques
|
||||
animés utilisent leur première image, tandis que la couleur reste animée.
|
||||
La carte d'émission est à la résolution de l'écran ; la diffusion séparable
|
||||
utilise deux cibles flottantes à 1/8 de largeur et de hauteur, sans retour
|
||||
GPU vers CPU en production. Les allocations sont libérées à l'arrêt, au
|
||||
rechargement et à la déconnexion, et recréées au changement de taille.
|
||||
|
||||
Cette première version ne reconstruit pas les sources hors écran, les
|
||||
particules, les entités lumineuses ni les modèles dessinés exclusivement par
|
||||
un renderer de block entity. Les packs très différents peuvent demander un
|
||||
masque explicite. Les silhouettes des liquides sont approximées puis
|
||||
masquées par la profondeur native. Aucune promesse de FPS ni validation
|
||||
Windows/Vulkan ; les essais graphiques portent sur macOS / Apple M1 / OpenGL.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test client ciblé réussit en **1 min 29 s**, dans un nouveau monde plat,
|
||||
graine 122. Il analyse les vingt textures, refuse les pixels gris de leur
|
||||
roche, mesure le gain nocturne et les variations réelles d'intensité/diffusion.
|
||||
Couvrir les minerais de pierre supprime toute source émissive. Retirer les
|
||||
blocs rétablit l'émission ; le rechargement des ressources fait de même.
|
||||
Torche, glowstone, lampe alimentée, lave et soleil sont vérifiés, ainsi que
|
||||
l'arrêt de la lampe et l'occultation du soleil par de la pierre. Les valeurs
|
||||
par défaut et la conservation d'anciens choix personnalisés passent.
|
||||
|
||||
Log ciblé : `build/shader130-client-bloom.log` ; masques inspectables dans
|
||||
`mods/sanctuary/build/run/clientGameTest/bloom130-ore-masks.png` et son CSV.
|
||||
La suite shader complète réussit en **3 min 16 s** : casters hors écran,
|
||||
retrait/repose, trois précisions, cinq portées, lune, zénith, feuillage,
|
||||
realtime, caméra et rechargement restent vérifiés. Les arêtes CTM restent
|
||||
raccordées entre matériaux ; 1 295 échantillons du contour sont éclaircis
|
||||
à précision 8, 596 à 16 et 120 à 32. Les captures françaises/anglaises et
|
||||
le rendu des minerais/du soleil sont inspectés.
|
||||
|
||||
Commandes natives : `./gradlew :sanctuary:runClientGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true`. Le test ciblé utilise
|
||||
`-PsanctuaryBloom130ClientTests=true` à la place de la propriété PixelShadows.
|
||||
Log complet : `build/shader130-client-full.log` ; captures sous
|
||||
`mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 24 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. La comparaison au binaire beta.129
|
||||
trouve **24 entrées de production** modifiées, toutes limitées au shader et à
|
||||
ses interfaces. Gameplay et textures des gemmes inchangés ; aucun test,
|
||||
monde ou modèle GLB embarqué. Les sources archivées correspondent aux sources
|
||||
Java, les packs normal/Test embarquent le même JAR et le template contient
|
||||
exactement les ressources prévues.
|
||||
|
||||
Archives vérifiées dans `build/` et recopiées dans le dossier habituel
|
||||
`sanctuary-beta/build/` :
|
||||
|
||||
|
||||
- `Sanctuary-beta.130.mrpack` : 10386021 octets, SHA-256
|
||||
`7a3fcd4e496fa136eb3e0896fa7b856ae91771123186fa8df13c8fb1e0cf3e9a`.
|
||||
|
||||
- `Sanctuary-Test-beta.130.mrpack` : 10404944 octets, SHA-256
|
||||
`8de094fcbc58a300cc24d64c8d1b7931fdb8db354c75defa6bf2a2aa4a123a23`.
|
||||
|
||||
JAR : `1ac4d1b1339361fb665f03ca3bd359163db5037ffd90e684c5a461759f6076e7`.
|
||||
|
||||
[Release beta.130](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.130)
|
||||
publiée depuis `534dfec4f88169a200702b50d1ba87d85c107bee`. Canal packwiz
|
||||
`68e06353bf880af04c1c72d4e3a159f11f94b619`. Les téléchargements publics sont vérifiés par SHA-256.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary
|
||||
Beta**, réussissent. Un seul JAR Sanctuary beta.130 est actif. Les
|
||||
**923 fichiers personnels suivis** conservent leurs hashes, dont les mondes,
|
||||
packs et préférences de shaders. Sauvegarde préalable dans
|
||||
`sanctuary-backups/before-beta.130/` de l'instance. Aucun monde personnel ouvert.
|
||||
|
||||
Reçus locaux ignorés : `build/shader130-artifact.json`,
|
||||
`build/shader130-isolated.json`, `build/shader130-prism.json` et
|
||||
`build/shader130-publication.log`. Le checkout original conserve ses
|
||||
modifications préexistantes ; seuls les trois artefacts versionnés vérifiés
|
||||
sont recopiés dans son dossier `build/`.
|
||||
@@ -0,0 +1,91 @@
|
||||
# SHADER-131 — émissifs indépendants et réglages initiaux
|
||||
|
||||
Demande : activer par défaut bloom, minerais émissifs et highlight ; garder
|
||||
les émissifs avec halo coupé ou intensité du bloom à zéro, sans bouton ajouté ;
|
||||
ajouter 64 et 128 pixels/bloc en options. Précision initiale 16, distances
|
||||
highlight 32 et ombres 128 conservées. Préférences personnalisées préservées.
|
||||
Branche `codex/emissive-beta131`, socle beta.130, Minecraft 26.3 / Java 25.
|
||||
|
||||
Le futur effet demandé est une légère teinte diffusée depuis les blocs
|
||||
voisins, pas des reflets reconnaissables. Étude seulement pour ce ticket ;
|
||||
aucun SSR ou éclairage indirect expérimental activé.
|
||||
|
||||
## Comportement et validation
|
||||
|
||||
Le curseur à zéro et l'interrupteur Bloom coupent uniquement le halo.
|
||||
Le réglage existant des minerais contrôle leurs inclusions nettes séparément.
|
||||
Sans halo, les cibles de diffusion GPU sont libérées ; l'émission garde
|
||||
seulement les ressources nécessaires. Tout désactiver libère l'ensemble.
|
||||
La grille commune propose 8, 16, 32, 64 et 128, sans augmenter le plafond
|
||||
4096 de la carte d'ombres. Libellés FR/EN et essais clients natifs sur les
|
||||
inclusions, l'absence de halo, les transitions et les nouvelles précisions.
|
||||
Aucun changement de lumière serveur, de génération ou de sauvegardes.
|
||||
|
||||
Le cœur émissif et le halo utilisent maintenant deux chemins indépendants.
|
||||
Les textures et leur analyse restent celles de beta.130. Avec le halo coupé,
|
||||
la carte d'émission est effacée avant les faces, le cœur net est composé,
|
||||
et les passes de ciel, réduction, diffusion et application du halo sont
|
||||
omises. Le retour au bloom recrée ses cibles. Aucun bouton ajouté, aides
|
||||
FR/EN adaptées et choix explicitement désactivés toujours conservés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite client native réussie en **3 min 46 s**, monde plat neuf de graine 122,
|
||||
macOS / Apple M1 / OpenGL. Cinq précisions sauvegardées et chargées, bornes
|
||||
et seuils de normalisation, carte d'ombres plafonnée et arêtes CTM rendues
|
||||
à 64/128. Le gain mesuré des traits diminue de 0,02778 à précision 32 à
|
||||
0,02094 à 64, puis 0,01013 à 128 dans la même scène.
|
||||
|
||||
Les inclusions restent nettes sans halo : le rendu Bloom OFF correspond
|
||||
au rendu intensité zéro. Aucun gain mesuré hors du masque source, ressources
|
||||
de diffusion absentes ; couper aussi les émissifs libère l'ensemble et
|
||||
restitue l'image initiale. Réactiver le bloom restitue son image précédente.
|
||||
Captures OFF/zéro inspectées. Valeurs initiales ON vérifiées ; un ancien
|
||||
choix explicitement OFF reste OFF. Les essais beta.130 (vingt masques,
|
||||
occultation, sources, rechargement), lune, feuillage et realtime passent.
|
||||
|
||||
Le premier essai rencontrait une assertion historique attendant le highlight
|
||||
OFF lorsqu'une clé était absente. Elle attend désormais la nouvelle valeur
|
||||
ON ; les cas de préférences explicitement désactivées restent testés.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log : `build/shader131-client-full.log` ; captures
|
||||
sous `mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 30 s**, 126 tâches. Les archives normal/Test, leurs sources
|
||||
et le template sont vérifiés. Six entrées de production changent : moteur
|
||||
Bloom, préférences, interface et libellés. Gameplay, shaders GLSL et textures
|
||||
restent identiques à beta.130. Aucun résultat Windows/Vulkan
|
||||
revendiqué ; le serveur GameTest dédié reste exclu conformément au refus
|
||||
antérieur de son EULA.
|
||||
|
||||
## Piste suivante — diffusion des couleurs
|
||||
|
||||
Choix confirmé : légère teinte des surfaces voisines. Une estimation SSGI
|
||||
utilise la couleur et la profondeur visibles pour des rebonds diffus ; voir
|
||||
la [documentation primaire Unity](https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/manual/Override-Screen-Space-GI.html).
|
||||
Proposition à expérimenter : faible intensité, courte portée autour des
|
||||
surfaces, calcul réduit, rejet par profondeur pour ne pas teinter au travers
|
||||
des murs. Vérifier les bords de l'écran, les blocs hors champ, la stabilité
|
||||
en mouvement et les textures avant de fixer une qualité. Cette piste n'est
|
||||
pas implémentée dans beta.131.
|
||||
|
||||
## Livraison
|
||||
|
||||
- `Sanctuary-beta.131.mrpack` : 10386312 octets ; SHA-256 `e02067e424bf4f54512d5cfb1b5cde0fe232aa6bdadc0d38981cd15817a6387a`.
|
||||
- `Sanctuary-Test-beta.131.mrpack` : 10405238 octets ; SHA-256 `298387cce5a5b7cdc09b3a9d24859b674a736b25abcbafa7995b813757641265`.
|
||||
|
||||
JAR : `87210da23f7781efccdbdbf5d4e2d3c1ff1857a7f326027b93d6a301c7dfdb0d`.
|
||||
|
||||
Archives vérifiées recopiées dans `sanctuary-beta/build/`. [Release beta.131](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.131)
|
||||
publiée depuis `078d3e6833daa7de7f6c6d0e1a6c17d5dbefe075`. Canal packwiz
|
||||
`efb18c274d95d988a9566b8c52751469f002cdda`. Téléchargements publics vérifiés par hash.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR Sanctuary beta.131 ; les **923 fichiers personnels
|
||||
suivis** conservent leurs hashes, dont les préférences de shaders et les mondes.
|
||||
Sauvegarde préalable `sanctuary-backups/before-beta.131/` ; aucun monde personnel
|
||||
ouvert. Reçus ignorés : `build/shader131-artifact.json`,
|
||||
`build/shader131-isolated.json`, `build/shader131-prism.json` et
|
||||
`build/shader131-publication.log`. Le checkout original conserve ses changements.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-132 — diffusion locale SSGI expérimentale
|
||||
|
||||
Demande : légère teinte diffusée des surfaces voisines, sans reflets miroir
|
||||
ni shader d'eau. Branche `codex/ssgi-beta132`, socle beta.131, Minecraft 26.3 /
|
||||
Java 25. Aucun changement de génération, sauvegarde ou lumière serveur.
|
||||
|
||||
## Résultat
|
||||
|
||||
**Options → Shaders → SSGI · couleurs diffusées** active un rebond diffus
|
||||
local. L'effet expérimental est initialement OFF, avec une intensité préparée
|
||||
à **35 %** ; le curseur propose 0–100. À zéro ou OFF, ses cibles et uniformes
|
||||
GPU sont libérés. Les autres effets et leurs préférences sont conservés.
|
||||
|
||||
La couleur d'une surface visible peut teinter les surfaces proches qui lui
|
||||
font face. Le calcul porte à **2,5 blocs** du récepteur et s'estompe entre
|
||||
24 et 32 blocs de la caméra. Il utilise une copie de la couleur native après
|
||||
les ombres, avec la profondeur et des normales reconstruites. Le premier
|
||||
obstacle arrête chaque rayon, même si son épaisseur empêche de retenir une
|
||||
intersection. Les plans coplanaires ne s'éclairent pas eux-mêmes.
|
||||
|
||||
Douze directions hémisphériques fixes, au maximum dix étapes par rayon,
|
||||
sont évaluées à demi-résolution en largeur et hauteur. La reconstruction
|
||||
plein écran est guidée par la profondeur pour conserver les silhouettes et
|
||||
les détails des textures. Le cœur des émissifs, le highlight et le bloom
|
||||
sont composés ensuite : pas de halo réinjecté dans le rebond ni de boucle
|
||||
de rétroaction d'une image sur l'autre. Aucune lecture GPU vers CPU ni
|
||||
recherche de chunks en production. Pas de bruit temporel ou d'historique.
|
||||
|
||||
Deux cibles, RGBA8 plein écran et RGBA16 flottant à demi-résolution, soit
|
||||
environ six octets par pixel de l'écran, plus 176 octets d'uniformes par
|
||||
segment du tampon natif. Redimensionnement, arrêt, déconnexion et
|
||||
rechargement libèrent/recréent les ressources possédées. Ce budget n'est
|
||||
pas une garantie de FPS ; le SSGI coûte davantage qu'un simple réglage de
|
||||
colorimétrie.
|
||||
|
||||
## Limites de cette version
|
||||
|
||||
L'estimation utilise seulement la couleur et la profondeur visibles. Les
|
||||
blocs hors champ ou cachés n'apportent pas leur couleur. Le résultat peut
|
||||
changer lorsqu'ils entrent ou sortent de l'image ; une atténuation près
|
||||
des bords limite la coupure. Les surfaces transparentes absentes de la
|
||||
profondeur ne sont pas reconstruites. Petits objets et intersections plus
|
||||
minces que l'échantillonnage peuvent manquer. Il s'agit d'une approximation
|
||||
diffuse locale, pas d'une simulation complète d'éclairage global ni de
|
||||
reflets spéculaires. Aucune modification de l'eau et aucune source hors
|
||||
champ déduite ou générée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Test ciblé natif réussi en **1 min** sur macOS / Apple M1 / OpenGL, nouveau
|
||||
monde plat de graine 122. À intensité 90, les échantillons du sol blanc
|
||||
montrent un gain rouge cumulé de 8 968 contre 20 bleu devant le mur rouge,
|
||||
puis 9 168 bleu contre 23 rouge devant le mur bleu. Le sol seul est inchangé.
|
||||
OFF/zéro restituent l'image de base et libèrent les cibles ; intensité 20
|
||||
réduit le gain. Changer la couleur du mur complètement occulté laisse
|
||||
l'image inchangée. Rechargement, caméra fixe, mouvement infinitésimal et
|
||||
retour à la caméra d'origine passent, sans accumulation de couleur.
|
||||
|
||||
Deux ajustements de fixture ont précédé ce résultat : inclure dans les
|
||||
mesures le blanc légèrement bleuté par l'éclairage natif, et formater les
|
||||
petits angles de `/tp` en décimal car la commande refuse la notation
|
||||
scientifique. Aucun comportement du jeu modifié pour ces corrections.
|
||||
|
||||
Log ciblé : `build/shader132-client-ssgi.log`. La suite complète réussit en
|
||||
**4 min 18 s** et vérifie aussi une fenêtre 961 × 541, le retour à sa taille
|
||||
initiale, le SSGI avec ombres/highlight/bloom/émissifs actifs, ainsi que
|
||||
l'arrêt global du shader. Les captures françaises et anglaises passent.
|
||||
Les contrôles précédents des cinq précisions, ombres, lune, feuillage, CTM,
|
||||
bloom et émissifs restent réussis. Log : `build/shader132-client-full.log`.
|
||||
|
||||
Commandes : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Pour le test ciblé, remplacer la propriété
|
||||
PixelShadows par `-PsanctuarySsgi132ClientTests=true`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 25 s**, 126 tâches. Les packs normal/Test contiennent le
|
||||
même JAR ; sources archivées et template de ressources correspondent aux
|
||||
sources. Comparaison avec beta.131 : **11 entrées de production** modifiées,
|
||||
limitées au SSGI, à ses options et à ses points d'appel. Gameplay et textures
|
||||
des gemmes inchangés, aucun monde ni test embarqué. Aucun résultat
|
||||
Windows/Vulkan revendiqué.
|
||||
Le serveur GameTest dédié reste exclu conformément au refus antérieur de
|
||||
son EULA. Les captures panoramiques et les milieux de brume sous l'eau/lave
|
||||
ou neige poudreuse suspendent cette passe, comme les autres effets de
|
||||
profondeur Sanctuary.
|
||||
|
||||
## Livraison
|
||||
|
||||
- `Sanctuary-beta.132.mrpack` : 10394026 octets, SHA-256 `a7c0823e00f36925919645e32e7792242ec1d01015b41bee5b4f6285b9fc533d`.
|
||||
- `Sanctuary-Test-beta.132.mrpack` : 10412949 octets, SHA-256 `9bc30a7d6d5834f9323f15f0c39d3f49d200e1a97d739266ab373c9e1ba4a8fd`.
|
||||
|
||||
JAR : `83eb2d091a7acd85eaa8b3b6a00a40ea483f9eeb2a30c033bdf68b4d717c5142`.
|
||||
|
||||
Archives vérifiées recopiées dans `sanctuary-beta/build/`. [Release beta.132](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.132)
|
||||
publiée depuis `7e21781a9a08c669c7d6b65653bd9d408887689c`. Canal packwiz
|
||||
`9edaaedc0205c525ecce9de66b5ca7827c56c28f` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées, puis deux dans la même instance **Sanctuary Beta**,
|
||||
réussissent. Un seul JAR beta.132 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences de shaders, gardent leurs hashes. Sauvegarde
|
||||
préalable `sanctuary-backups/before-beta.132/` ; aucun monde personnel ouvert.
|
||||
Le checkout original conserve ses changements préexistants.
|
||||
|
||||
Reçus locaux ignorés : `build/shader132-artifact.json`,
|
||||
`build/shader132-isolated.json`, `build/shader132-prism.json`,
|
||||
`build/shader132-publication.log`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# SHADER-133 — SSGI visible au réglage courant
|
||||
|
||||
Demande : le rebond diffus de beta.132 est trop discret pour être distingué.
|
||||
Branche `codex/ssgi-strength-beta133`, socle beta.132, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Changement
|
||||
|
||||
La collecte conserve douze directions, dix étapes, une demi-résolution et
|
||||
un rayon de 2,5 blocs. L'atténuation spatiale devient linéaire plutôt que
|
||||
quadratique. Le mélange est reconstruit en lumière approximativement linéaire,
|
||||
puis réencodé une seule fois. Un gain artistique plus fort rend la couleur
|
||||
lisible et une courbe progressive évite de couper brutalement les canaux
|
||||
à blanc. Les surfaces sans rebond restent intactes. La force demeure réglable
|
||||
de 0 à 100, avec le même réglage initial de 35 ; aucune préférence enregistrée
|
||||
n'est écrasée. SSGI reste optionnel, initialement désactivé.
|
||||
|
||||
Aucun rayon, cible GPU ou texture de matériau supplémentaire. La composition
|
||||
ajoute une exponentielle et une racine carrée par canal ; le coût exact en FPS
|
||||
reste à mesurer sur la machine du joueur. Les normales de géométrie orientent
|
||||
déjà le rebond. Pas de PBR ni de normales procédurales de textures ajoutés.
|
||||
|
||||
## Piste matériaux
|
||||
|
||||
Pour une étape distincte, des normales de faible amplitude calculées au
|
||||
chargement des textures, mises en cache, et une rugosité par famille de matériaux
|
||||
permettraient d'étudier du relief et des reflets orientés. Une simple conversion
|
||||
luminosité → hauteur ne connaît pas le relief réel : un motif peint peut devenir
|
||||
une bosse artificielle. Commencer sur pierre, briques et bois avec une amplitude
|
||||
réglable serait préférable à une génération aveugle pour tous les blocs.
|
||||
La passe actuelle ne possède ni atlas de normales, ni identifiants de matériaux,
|
||||
ni albédo non éclairé ; des images seules ne suffiraient pas à les exploiter.
|
||||
|
||||
## Limites conservées
|
||||
|
||||
Approximation diffuse à partir de la couleur déjà éclairée, pas un transport
|
||||
physique complet. Seules les surfaces visibles contribuent, avec variations
|
||||
possibles aux entrées/sorties d'écran. Premier obstacle bloquant, pas d'historique
|
||||
ni de bruit temporel ; aucune modification d'eau, monde ou lumière serveur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Suite client native complète réussie en **4 min 29 s**, sur macOS / Apple M1 /
|
||||
OpenGL, dans un monde de test plat (graine 122). Les comparaisons ON/OFF à 35 %
|
||||
mesurent, sur les pixels neutres du sol :
|
||||
|
||||
| Mur | Pixels avec gain ≥ 8/255 | Pixels avec gain ≥ 16/255 | Gain cumulé du canal dominant |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Rouge | 16 487 | 7 311 | 305 221 |
|
||||
| Bleu | 14 954 | 7 328 | 309 991 |
|
||||
|
||||
À 90 %, les gains dominants atteignent 654 683 et 655 867, contre 8 968 et
|
||||
9 168 au même réglage dans la fixture beta.132. Ce rapport concerne uniquement
|
||||
cette scène et ces pixels, pas une promesse de gain uniforme dans tous les mondes.
|
||||
Les captures confirment une bande colorée au pied du mur. Un premier réglage
|
||||
intermédiaire a été rejeté car aucun pixel du sol ne dépassait 8/255 à 35 % ;
|
||||
un second a passé les contrôles ciblés, puis la force a encore été augmentée
|
||||
après examen visuel. Les valeurs finales sont celles de la suite complète.
|
||||
|
||||
Le test impose maintenant plus de 5 000 pixels à 8/255 et plus de 500 à 16/255
|
||||
au réglage 35, pour les deux couleurs. Intensité, OFF/zéro, absence de rebond
|
||||
coplanaire, occultation d'un mur changeant de couleur, caméra fixe, déplacement
|
||||
infinitésimal, retour de caméra, redimensionnement et rechargement passent.
|
||||
Ombres, lune, feuillage, CTM, cinq précisions, bloom, émissifs indépendants et
|
||||
interfaces FR/EN restent validés, y compris avec le SSGI actif.
|
||||
|
||||
Commande : `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPixelShadows122ClientTests=true -PsanctuaryClientNoVsync=true
|
||||
-PsanctuaryQuickTests=true`. Log : `build/shader133-client-full.log`.
|
||||
Le nom de la propriété ciblée `sanctuarySsgi132ClientTests` est conservé.
|
||||
|
||||
Comparateur local autonome : `build/SSGI-beta.133-comparaison.html`, captures
|
||||
natives OFF/35/90 des murs rouge/bleu, sans retouche. Pas de validation
|
||||
Windows/Vulkan ni de mesure de coût FPS revendiquée.
|
||||
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 21 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader133-build.log`.
|
||||
Les archives normal/Test contiennent le même JAR ; sources archivées et template
|
||||
de ressources correspondent aux sources. Comparaison avec beta.132 : **5 entrées
|
||||
de production** changées (classe du compositeur, deux shaders, libellés FR/EN).
|
||||
Gameplay, textures, sauvegardes et dépendances inchangés.
|
||||
|
||||
## Artefacts
|
||||
|
||||
- `Sanctuary-beta.133.mrpack` : 10394282 octets, SHA-256 `0c90a070da7f969706abeeec1bdfb0830bb52244c20fccbca4fd876f3fbe0564`.
|
||||
- `Sanctuary-Test-beta.133.mrpack` : 10413204 octets, SHA-256 `e01f0355bf2cce18156fe94a069e142bedc498624ba0b6e2729ae0f23ea73e80`.
|
||||
|
||||
JAR : `a01cae4a98606007b533ca09ec9f072f19a1db2e9b8de466f4ad075d5c1fc185`.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.133](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.133)
|
||||
publiée depuis `216d3f75b576bd2ed531653012f9cc6c4801d87a`. Canal packwiz
|
||||
`9ceb5a5353c9d1b8bb7834a20e818fd65096ff98` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux synchronisations de la même instance
|
||||
**Sanctuary Beta** réussissent. Un seul JAR beta.133 actif ; les **923 fichiers
|
||||
personnels suivis**, dont sauvegardes et préférences, gardent leurs hashes.
|
||||
Sauvegarde préalable `sanctuary-backups/before-beta.133/`, aucun monde personnel
|
||||
ouvert. Le checkout original conserve ses changements préexistants.
|
||||
|
||||
Les archives normal/Test/template et le comparateur natif sont également dans
|
||||
`sanctuary-beta/build/`. Reçus locaux ignorés : `build/shader133-artifact.json`,
|
||||
`build/shader133-isolated.json`, `build/shader133-prism.json` et
|
||||
`build/shader133-publication.log`.
|
||||
@@ -0,0 +1,109 @@
|
||||
# SHADER-134 — PBR procédural optionnel et herbe SSGI
|
||||
|
||||
Demande : ajouter en option des normales calculées à partir des textures actives,
|
||||
avec rugosité des matériaux et reflets orientés. Retour : le SSGI renforcé
|
||||
éclaire artificiellement l'intérieur de l'herbe ; vérifier et corriger ce cas.
|
||||
Branche `codex/pbr-beta134`, socle beta.133, Minecraft 26.3 / Java 25.
|
||||
|
||||
## Contrat
|
||||
|
||||
PBR initialement OFF, intensité 50 réglable 0–100. Cartes de normales à partir
|
||||
des différences de luminance, amplitude limitée ; rugosité/métallicité par
|
||||
famille de blocs, repli rugueux pour les types inconnus. Analyse des ressources
|
||||
actives conservée en cache jusqu'à désactivation ou rechargement ; pas de fichiers
|
||||
écrits dans le pack personnel, pas de GPU readback en production.
|
||||
|
||||
Portée 32 blocs, sections visibles construites progressivement. Budget de maillage
|
||||
24 Mio, atlas de 4 096 matériaux en 16 × 16 avec bordures (1 280 × 1 280 RGBA8,
|
||||
6,25 Mio), copie RGBA8 de l'image courante et uniformes. Les textures de plus haute
|
||||
résolution sont échantillonnées à 16 × 16 ; première image des textures animées.
|
||||
La couleur et l'animation restent celles de l'atlas natif. Pas de parallax ni
|
||||
de géométrie ajoutée aux blocs ; les motifs clairs peuvent donner un faux relief.
|
||||
|
||||
La passe utilise une réponse spéculaire GGX (normales, rugosité, métallicité,
|
||||
Fresnel) et module l'éclairage diffus déjà rendu. Elle ne remplace pas tout le
|
||||
pipeline Minecraft par un moteur PBR physique. Soleil/lune, exposition au ciel et
|
||||
carte d'ombres active limitent les reflets ; pas de reflets miroir, d'environnement
|
||||
SSR, ni de direction locale déduite pour chaque torche. Aucun rendu PBR d'eau,
|
||||
d'entités, d'items ou de modèles spéciaux hors maillage de terrain.
|
||||
|
||||
## SSGI
|
||||
|
||||
Les normales des faces presque axiales sont stabilisées sur leur axe. Deux passes
|
||||
spatiales filtrent la lumière indirecte à demi-résolution, avec rejet des voisins
|
||||
hors du plan du récepteur et interpolation de l'échantillonnage. Les textures de
|
||||
l'image restent nettes. Un tampon RGBA16F supplémentaire à demi-résolution porte
|
||||
le budget des cibles SSGI à environ 8 octets par pixel plein écran (contre 6).
|
||||
Pas d'historique temporel ni de rémanence ajoutés.
|
||||
|
||||
Un parcours de 12 blocs a reproduit des bandes sur un mur et un éclaircissement
|
||||
artificiel d'herbe à 35 et 100. Le filtrage seul ne suffisait pas : la collecte
|
||||
réinjectait aussi trop fortement la lumière ambiante neutre. Elle conserve
|
||||
maintenant la composante chromatique de la source, en soustrayant son canal
|
||||
linéaire minimum. L'éclairage ambiant neutre reste celui de Minecraft. Ce réglage
|
||||
vise le transfert de couleur voulu, pas une simulation physique complète du
|
||||
rebond blanc. Les sources hors champ/cachées restent absentes du SSGI ; le résultat
|
||||
peut encore varier lors d'un déplacement important. PBR et SSGI restent indépendants.
|
||||
|
||||
## Vérification
|
||||
|
||||
Test PBR ciblé réussi en 1 min 13 s : relief éclairé/ombré, intensités 20/50/100,
|
||||
soleil matin/soir, cache stable, préférences, OFF/zéro, mouvement infinitésimal,
|
||||
redimensionnement, rechargement et combinaison des effets. Le premier parcours
|
||||
SSGI est conservé comme reproduction ; résultats finaux ci-dessous.
|
||||
|
||||
|
||||
Le parcours corrigé a réussi en **2 min 47 s** avec 13 positions réparties sur
|
||||
12 blocs, à 35 et 100, chaque fois comparées à OFF. Un mob immobile, des brins
|
||||
d'herbe et un mur figurent dans la scène. Retour au point initial sans rémanence.
|
||||
Sur les captures de ce parcours, le maximum du gain vert moyen mesuré dans
|
||||
l'herbe passe sous 9,73 niveaux à 35 et 24,51 à 100. Le test impose ensuite
|
||||
respectivement des limites de 12 et 30 à chaque position. L'essai reproduisant
|
||||
le défaut atteignait notamment 23,68 et 49,20 à l'extrémité droite.
|
||||
Ces mesures décrivent cette scène, pas une garantie d'absence d'artefacts
|
||||
dans tous les mondes ni sur toutes les cartes graphiques.
|
||||
|
||||
|
||||
La suite client complète réussit en **7 min** sur macOS / Apple M1 / OpenGL.
|
||||
Elle inclut le parcours latéral et ses seuils de gain, les deux couleurs de rebond,
|
||||
le PBR, les ombres, le feuillage, les cinq précisions, le CTM, les émissifs, le
|
||||
bloom, les préférences et les écrans FR/EN. Log : `build/shader134-client-full.log`.
|
||||
Le contrôle PBR final réussit en **1 min 19 s** : placement/retrait d'un toit,
|
||||
invalidations de lumière/maillage et rendu combiné avec les ombres actives. Les
|
||||
reflets utilisent maintenant la grille des ombres pour leur test de visibilité. Aucun résultat Windows/Vulkan ni
|
||||
mesure de FPS revendiqués.
|
||||
|
||||
|
||||
## Construction et artefacts
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 22 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader134-build.log`.
|
||||
Packs normal/Test : même JAR Sanctuary. Sources archivées et template identiques
|
||||
aux sources. Comparaison à beta.133 : **23 entrées de production** changées,
|
||||
limitées au rendu, à ses options et points d'appel. Gameplay et gemmes inchangés.
|
||||
|
||||
- `Sanctuary-beta.134.mrpack` : 10421971 octets, SHA-256 `d5bc79acb1d56e7dcf83c976156fdb2fcde80c98e443bde8a6ab312d56887b7f`.
|
||||
- `Sanctuary-Test-beta.134.mrpack` : 10440898 octets, SHA-256 `04308ebb9adaf76b30a3cbf4fa32ab897e76842803fc8c8411cfa32952d1bdd5`.
|
||||
|
||||
JAR : `a6e69622a5e6629f70dd19b7494ad3b00916bc02169b73635f0e2d563929b7fa`.
|
||||
|
||||
Comparateur local : `build/Shader-beta.134-comparaison.html`. Il contient les
|
||||
captures natives PBR OFF/50/100 et le parcours SSGI de 12 blocs OFF/35/100.
|
||||
Le parcours est une succession de positions enregistrées, pas une mesure des FPS.
|
||||
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.134](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.134)
|
||||
publiée depuis `fc4a0cb68ed2d89163c8e74773976df5bab4f3f1`. Canal packwiz
|
||||
`35ec0f6e145002f6367944df5b75e2703958eaef` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.134 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.134/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparateur natif vérifiés également dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader134-artifact.json`, `build/shader134-isolated.json`,
|
||||
`build/shader134-prism.json`, `build/shader134-publication.log`.
|
||||
@@ -0,0 +1,116 @@
|
||||
# SHADER-135 — SSGI en contre-plongée et près des surfaces
|
||||
|
||||
Demande : supprimer les déchirures horizontales sous une surface, notamment
|
||||
près de la lave, et atténuer proprement le rebond quand la caméra s'approche
|
||||
d'une source ou d'un mur. Branche `codex/shader-closeup-beta135`, socle beta.134.
|
||||
Minecraft 26.3 / Java 25 ; aucune modification de monde ou de gameplay.
|
||||
|
||||
## Diagnostic et correction
|
||||
|
||||
Le calcul des normales comparait l'aire du pixel en unités du monde à un seuil
|
||||
fixe. À courte distance ou à haute résolution, cette aire devient naturellement
|
||||
très petite : le rejet pouvait couper une surface continue. Les deux directions
|
||||
sont maintenant normalisées avant leur produit vectoriel. Les texels de bord
|
||||
sont bornés et les voisins sans profondeur ne créent plus de fausse surface.
|
||||
|
||||
La composition utilise le centre exact du texel de profondeur. Le rebond
|
||||
s'atténue entre 0,4 et 0,06 bloc de distance au **plan** receveur, et lorsque la
|
||||
vue devient presque tangente. Cette distance évite une découpe circulaire sur
|
||||
un mur plat. L'image native reste nette ; aucune texture du décor n'est floutée.
|
||||
|
||||
Les sondes sortant du volume de projection sont rejetées. Leur contribution
|
||||
s'atténue près du plan de caméra et de la limite d'épaisseur d'un impact, au lieu
|
||||
d'une contribution pleine jusqu'à cette limite. Trois ressources GLSL modifiées ;
|
||||
aucune passe, cible GPU, lecture CPU, histoire temporelle ou option ajoutée.
|
||||
Les 12 rayons × 10 pas et les deux passes de filtrage spatial restent inchangés.
|
||||
|
||||
## Réglages initiaux
|
||||
|
||||
Demande ajoutée pendant la livraison : **bloom 20 %**, **PBR ON** et **SSGI ON**.
|
||||
Les intensités PBR 50 et SSGI 35 restent inchangées. Les champs absents du fichier
|
||||
de préférences prennent ces nouvelles valeurs ; les choix explicites du joueur
|
||||
restent conservés. Aides françaises et anglaises mises à jour. Vérification
|
||||
native supplémentaire des préférences vierges, partielles et personnalisées.
|
||||
|
||||
## Reproduction native
|
||||
|
||||
Deux scènes isolées : stone au-dessus de la lave, puis coin à 0,5 bloc d'une
|
||||
paroi rouge. Caméra en contre-plongée, sept distances au plafond de 2 à 0,04 bloc,
|
||||
comparaisons OFF/35/100 avant/après. Dans la première scène, à 0,1 bloc et
|
||||
intensité 100, le saut maximal de gain rouge moyen entre lignes de pixels neutres
|
||||
passe de **14,88 à 0,052** niveaux sur 255. Dans le coin, à 0,4 bloc et intensité
|
||||
35, le rebond auparavant rejeté revient (gain moyen de **6,01** niveaux), puis
|
||||
s'atténue à l'approche du contact. Ces chiffres concernent ces scènes uniquement.
|
||||
|
||||
Les deux essais ciblés corrigés réussissent en 1 min 18 s et 1 min 17 s sur
|
||||
Apple M1 / OpenGL. Images avant/après conservées dans `build/closeup-*` ;
|
||||
comparateur autonome `build/Shader-beta.135-comparaison.html`.
|
||||
|
||||
## Régression et livraison
|
||||
|
||||
Les contrôles natifs de la suite générale réussissent jusqu'au parcours SSGI :
|
||||
ombres, lune, feuillage, temps réel, CTM, émissifs/bloom, couleurs de rebond,
|
||||
occlusion, réglages, rechargement et déplacement latéral de 12 blocs à 35/100.
|
||||
Log : `build/shader135-client-main.log`. La suite n'est pas présentée comme
|
||||
entièrement réussie : elle s'est arrêtée sur une attente incorrecte du nouveau
|
||||
test, qui demandait du rebond alors que sa source était sortie du champ.
|
||||
|
||||
Le scénario corrigé distingue la haute résolution avec source visible de la
|
||||
vue très relevée sans source visible. Le bassin possède un fond pour isoler la
|
||||
lave des essais suivants ; les particules du scénario précédent sont effacées
|
||||
avant la comparaison PBR statique. Une première tentative avait aussi rencontré
|
||||
le refus natif de remettre l'horloge à sa valeur actuelle ; la préparation de
|
||||
l'heure est désormais répétable. Ces changements concernent uniquement les tests.
|
||||
|
||||
Le contrôle final ciblé **réussit en 2 min 52 s** : les deux scènes, 35/100,
|
||||
sept distances, 1280 × 720 avec source visible, source hors champ, mouvement
|
||||
minuscule, rendu combiné, puis contrôles PBR complets (cache, intensités,
|
||||
matériaux, soleil, toit, OFF/zéro, redimensionnement et rechargement).
|
||||
Log : `build/shader135-client-final.log` ; marqueurs `SSGI135_CLOSEUP_PASS` et
|
||||
`PBR134_PASS`. Les seuils de proximité et PBR sont conservés.
|
||||
Le contrôle supplémentaire des nouveaux défauts et du PBR **réussit en
|
||||
1 min 16 s** : préférences vierges, champs absents, valeurs personnelles conservées,
|
||||
puis rendu PBR et combinaison des effets. Log : `build/shader135-defaults-client.log`.
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 18 s**, 126 tâches. Le serveur GameTest dédié demeure exclu
|
||||
conformément au refus EULA antérieur. Les classes des scénarios clients compilent
|
||||
aussi après leur isolation explicite des effets activés par défaut.
|
||||
Log : `build/shader135-build.log` ; compilation : `build/shader135-test-compile.log`.
|
||||
|
||||
## Archives vérifiées
|
||||
|
||||
Comparaison à beta.134 : **6 entrées de production** modifiées, limitées aux
|
||||
trois ressources SSGI, aux valeurs initiales et aux aides FR/EN. Gameplay et
|
||||
textures des gemmes inchangés. Packs normal/Test : JAR Sanctuary identique ;
|
||||
archives de sources et template conformes aux sources. Reçu local ignoré :
|
||||
`build/shader135-artifact.json`.
|
||||
|
||||
|
||||
## Limites
|
||||
|
||||
Le SSGI utilise seulement les surfaces visibles : les sources hors champ restent
|
||||
absentes et un grand changement de vue peut modifier le rebond. L'atténuation
|
||||
proche est volontaire ; ce n'est pas une simulation volumétrique. Le brouillard
|
||||
natif de submersion continue de suspendre le SSGI, comme en beta.134. Aucun
|
||||
résultat Windows/Vulkan ni mesure de FPS revendiqués.
|
||||
|
||||
- `Sanctuary-beta.135.mrpack` : 10422391 octets ; SHA-256 `36ac278f9f5ecd2cefd69da16ea7d153ff44c5fc8e6bf0b3a3d6b71f5d767af6`.
|
||||
|
||||
- `Sanctuary-Test-beta.135.mrpack` : 10441314 octets ; SHA-256 `e096374f7ea0856b20392e5d277e94f6c3d411723d7ffc10ceccdd9f0bf5c4d4`.
|
||||
|
||||
JAR : `a68ccd04a92a962c46a76b26a80cb44a4faf7a13a562094dde51010ade594c10`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.135](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.135)
|
||||
publiée depuis `7fada570efb5ed579e6e4d7bc2b87079c7131b31`. Canal packwiz
|
||||
`5b0df577fcd036937c25be3a1f4580f9db3103a1` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.135 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.135/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparateur natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader135-artifact.json`, `build/shader135-isolated.json`,
|
||||
`build/shader135-prism.json`, `build/shader135-publication.log`.
|
||||
@@ -0,0 +1,85 @@
|
||||
# SHADER-136 — Lens flare rectangulaire doux
|
||||
|
||||
Demande : ajouter un lens flare avec des rectangles dégradés, exclusivement
|
||||
pour le soleil et la lune. Socle beta.135, branche `codex/lens-flare-beta136`.
|
||||
Minecraft 26.3 / Java 25. Aucun changement de gameplay ou de monde.
|
||||
|
||||
## Contrat
|
||||
|
||||
Effet activé initialement à **30 %**, intensité réglable 0–100, indépendant du halo de bloom.
|
||||
Rectangles translucides aux bords doux, suivant la position de l'astre à l'écran.
|
||||
La visibilité provient de l'image émissive céleste native, avant les blocs :
|
||||
les murs masquent l'effet, les sources de terrain ne le déclenchent pas.
|
||||
L'astre sortant du champ doit s'atténuer sans sauter sur le bord opposé.
|
||||
Pas d'historique, ni lecture GPU vers le CPU, ni texture générée sur disque.
|
||||
|
||||
## Rendu et budget
|
||||
|
||||
Trois rectangles à dégradé doux par astre : un halo horizontal près de sa
|
||||
position et deux reflets plus petits le long de l'axe passant par le centre de
|
||||
l'écran. Teinte solaire chaude, lunaire légèrement bleue. Intégration de la
|
||||
lumière réellement dessinée par le ciel natif ; phases de lune conservées.
|
||||
Atténuation aux bords de l'écran et à l'horizon, sans projection derrière la caméra.
|
||||
|
||||
Le masque céleste est mesuré avant l'ajout des émissifs de terrain. Réduction
|
||||
vers une texture 2 × 1 RGBA16F (512 échantillons au maximum au total), puis une
|
||||
passe de composition avec deux lectures de cette texture et des dégradés analytiques.
|
||||
La cible d'émission plein écran existante est réutilisée. Quand seul le flare
|
||||
fonctionne, aucun maillage d'émissif de terrain ni tampon de flou n'est construit.
|
||||
OFF et zéro libèrent la cible du flare ; les autres effets gardent leur autonomie.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le scénario client natif réussit en **1 min 52 s** sur macOS / Apple M1 / OpenGL.
|
||||
Soleil OFF/30/80, lune et nouvelle lune, mur occultant, source hors champ,
|
||||
glowstone exclue, image statique, redimensionnement et rechargement des ressources.
|
||||
Le flare reste actif sans bloom ; OFF/zéro libèrent ses ressources. Préférences
|
||||
vierges, partielles, personnalisées, bornage et libellés FR/EN vérifiés.
|
||||
Les contrôles existants du bloom et des minerais émissifs passent également.
|
||||
Log : `build/shader136-client-final.log`, marqueurs `EMISSIVE131_PASS` et
|
||||
`LENS_FLARE136_PASS`. Les captures natives restent dans le dossier de tests ; comparatif autonome
|
||||
`build/Shader-beta.136-comparaison.html` (soleil OFF/30/80 et lune OFF/30).
|
||||
|
||||
Une première tentative s'est arrêtée sur l'attente d'un angle de caméra :
|
||||
Minecraft normalise 180° en −180°. Le scénario utilise maintenant un angle
|
||||
canonique ; ce changement concerne uniquement le test.
|
||||
|
||||
## Limites
|
||||
|
||||
Effet optique en espace écran : il s'atténue quand l'astre quitte le champ et
|
||||
à l'horizon. Le masque natif du ciel contrôle l'occultation ; la lune reste
|
||||
volontairement beaucoup plus discrète. Le brouillard de submersion suspend
|
||||
l'effet comme le bloom existant. Aucun essai Windows/Vulkan ni mesure de FPS
|
||||
revendiqués.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 29 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader136-build.log`.
|
||||
Comparaison à beta.135 : **9 entrées de production** modifiées, uniquement
|
||||
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
|
||||
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader136-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.136.mrpack` : 10424821 octets ; SHA-256 `808006bf2b0cb3fe63d3e16ae3a0003d27ad0b7e793a3a05f25b5d73ae88bc8d`.
|
||||
|
||||
- `Sanctuary-Test-beta.136.mrpack` : 10443744 octets ; SHA-256 `70f593ad16d1f4fd4305b276ce2727a63f87bcb84df45e55840c1427a314a1eb`.
|
||||
|
||||
JAR : `086afede22903f8988889f6085631451f65d6d55c2e4c81f249d2234698e68e5`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.136](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.136)
|
||||
publiée depuis `71296a9cfb0f6b1a6e6f0c48145f3cc432c8afd0`. Canal packwiz
|
||||
`dd5b545a7cfbb957f2c70e9dc17034b44caef98f` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.136 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.136/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader136-artifact.json`, `build/shader136-isolated.json`,
|
||||
`build/shader136-prism.json`, `build/shader136-publication.log`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# SHADER-137 — Rayons solaires volumétriques
|
||||
|
||||
Demande validée : faisceaux légers découpés par les blocs, carte d'ombres partagée,
|
||||
feuillage perméable, rendu natif préservé, option et intensité indépendantes du bloom.
|
||||
Socle beta.136, branche `codex/sunshafts-beta137`, Minecraft 26.3 / Java 25.
|
||||
Aucun changement de monde ni de gameplay.
|
||||
|
||||
## Contrat
|
||||
|
||||
Soleil uniquement ; activé initialement à 35 %, intensité 0–100.
|
||||
Intégration de 16 segments par pixel à un quart de chaque dimension.
|
||||
Reconstruction guidée par la profondeur, sans historique ni bruit animé.
|
||||
Réutilisation du masque de feuillage déjà stable dans les ombres.
|
||||
La carte solaire peut fonctionner sans afficher les ombres sur les surfaces.
|
||||
Portée bornée à 64 blocs et à la moitié de la portée des ombres.
|
||||
Atténuation aux limites de la carte, à l'horizon et avec la météo.
|
||||
|
||||
## Rendu et coût borné
|
||||
|
||||
Deux passes : intégration dans une cible RGBA16F de largeur et hauteur divisées
|
||||
par quatre, puis reconstruction guidée par la profondeur. Seize segments
|
||||
quadratiques privilégient les premiers mètres ; quatre lectures fixes de la
|
||||
carte d'ombres par segment adoucissent seulement la lumière dans l'air.
|
||||
La dernière portion du rayon rétrécit continûment à l'approche d'une surface.
|
||||
La lumière utilise une teinte solaire chaude mélangée au brouillard natif.
|
||||
|
||||
La carte d'ombres et ses maillages sont partagés, y compris les trous de
|
||||
transmission déterministes des feuilles. Aucune copie d'image plein écran,
|
||||
aucune duplication du terrain, aucune lecture GPU côté CPU ni accumulation
|
||||
entre images dans le rendu de production. OFF/zéro libèrent le volume ; la
|
||||
carte d'ombres reste seulement si les ombres de surface sont activées.
|
||||
Le soleil sous l'horizon et la submersion suspendent les rayons.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Scénario natif réussi en **1 min 41 s** sur macOS / Apple M1 / OpenGL : pièce
|
||||
entièrement fermée, toit ajouré OFF/35/100, feuilles puis ciel ouvert, déplacement
|
||||
de quatre blocs et retour, caméra proche d'un mur à 0,5 puis 0,125 bloc,
|
||||
soleil hors champ, nuit, OFF/zéro, carte d'ombres partagée, effet combiné,
|
||||
redimensionnement à 961 × 541 et rechargement des ressources. Préférences
|
||||
vierges/partielles/personnelles, bornage et libellés FR/EN également vérifiés.
|
||||
Log : `build/shader137-client-final.log`, marqueur `SUNSHAFT137_PASS`.
|
||||
Comparatif autonome : `build/Shader-beta.137-comparaison.html`.
|
||||
|
||||
La première tentative s'est arrêtée sur une comparaison de gain brut entre
|
||||
feuillage et ciel ouvert. La composition dispose de moins de marge sur un mur
|
||||
déjà clair : le test compare maintenant la contribution normalisée sur les mêmes
|
||||
surfaces, et les feuilles du scénario sont persistantes. Aucun seuil du rendu
|
||||
n'a été assoupli pour faire passer ce contrôle.
|
||||
|
||||
Les contrôles natifs existants du bloom, des vingt textures de minerais émissifs
|
||||
et du lens flare réussissent également en **1 min 32 s**, avec leurs seuils
|
||||
inchangés. Log : `build/shader137-regression.log`, marqueurs `EMISSIVE131_PASS`
|
||||
et `LENS_FLARE136_PASS`. Les autres effets sont explicitement isolés dans les
|
||||
scénarios qui nécessitent une image témoin sans rayons.
|
||||
|
||||
## Limites
|
||||
|
||||
Le volume utilise les formes présentes dans la carte d'ombres existante :
|
||||
il n'ajoute pas de projection d'ombre des entités ou des nuages. Le feuillage
|
||||
reste une approximation par transmission déterministe. Portée maximale de
|
||||
64 blocs, atténuée à son extrémité. Le faible nombre de segments et la résolution
|
||||
réduite peuvent lisser les ouvertures très fines. Aucun résultat Windows/Vulkan
|
||||
ni mesure de FPS revendiqués.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 27 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader137-build.log`.
|
||||
Comparaison à beta.136 : **11 entrées de production** modifiées, uniquement
|
||||
le rendu, ses préférences et ses libellés. Gameplay et textures des gemmes
|
||||
strictement conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader137-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.137.mrpack` : 10431723 octets ; SHA-256 `6684d1dbe48cd3031b373a03d4380519fdbf5d841e1c7cb5e290dec63b5ba9f6`.
|
||||
|
||||
- `Sanctuary-Test-beta.137.mrpack` : 10450650 octets ; SHA-256 `32b0cee60152b6a5c8f31b248391c79a5fdf47b649de74902b066457ad4785ef`.
|
||||
|
||||
JAR : `f1fbc106b4b048753f9a669af37e9c12f92168af486d4072f514e86920c81faa`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.137](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.137)
|
||||
publiée depuis `2d7072b8a8febac53354e9d0063246ac1e843e37`. Canal packwiz
|
||||
`68cb39a5edbf5d34def02f670e2488ee852605b8` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.137 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.137/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader137-artifact.json`, `build/shader137-isolated.json`,
|
||||
`build/shader137-prism.json`, `build/shader137-publication.log`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# SHADER-138 — Épaisseur des rayons et météo
|
||||
|
||||
Demande : épaissir les sunshafts avec la météo, surtout le brouillard.
|
||||
Socle beta.137, branche `codex/sunshaft-weather-beta138`, Minecraft 26.3 / Java 25.
|
||||
Aucune modification des règles météo, des sauvegardes ou du gameplay.
|
||||
|
||||
## Contrat
|
||||
|
||||
Densité et diffusion liées au brouillard environnemental réellement rendu,
|
||||
indépendamment de la limite de distance de rendu. Pluie/neige natives :
|
||||
renforcement plus modéré ; orage : réduction supplémentaire du soleil.
|
||||
Transitions continues, mêmes deux passes et seize segments. Le réglage
|
||||
manuel conserve sa valeur, aucune nouvelle option.
|
||||
|
||||
## Rendu
|
||||
|
||||
Le brouillard utilise `FogData.environmentalEnd`, séparé de la distance de
|
||||
rendu native. Sa contribution croît en douceur entre 192 et 80 blocs de
|
||||
visibilité. La densité passe de ×1 à ×3,5 pour le brouillard seul ; la diffusion
|
||||
angulaire s'élargit et la lumière lointaine s'éteint plus vite. Les coefficients
|
||||
ne dépendent d'aucun bruit animé ni historique d'image.
|
||||
|
||||
Pluie et neige utilisent le niveau natif de précipitation : densité jusqu'à
|
||||
×1,8, diffusion plus légère. La lumière disponible tombe progressivement à
|
||||
25 % sous une pluie maximale et à 8,75 % sous un orage maximal. La carte solaire
|
||||
reste disponible pour le volume même lorsque Minecraft cache entièrement
|
||||
son disque solaire ; les ombres sur les surfaces conservent leur comportement.
|
||||
Le brouillard et la météo continuent leurs transitions natives.
|
||||
|
||||
Même cible au quart de chaque dimension, deux passes, seize segments et
|
||||
quatre lectures de la carte par segment. L'empreinte de l'ombre ne change pas :
|
||||
la météo ne crée pas d'ouverture artificielle à travers un mur.
|
||||
|
||||
## Vérification
|
||||
|
||||
Scénario client natif réussi en **3 min 29 s**, macOS / Apple M1 / OpenGL.
|
||||
Les contrôles des volumes beta.137 passent à nouveau : ouvertures, feuilles,
|
||||
pièce fermée, mouvement/retour, proximité, soleil hors champ, intensité,
|
||||
OFF/zéro/nuit, redimensionnement, rechargement, préférences et libellés FR/EN.
|
||||
|
||||
La suite utilise ensuite les vraies commandes serveur de météo : clair,
|
||||
`weather fog`, pluie, orage et retour au clair, dans le même décor avec le
|
||||
curseur maintenu à 35. Transition progressive du brouillard, rechargement
|
||||
sous brouillard, absence d'éclairage d'une pièce fermée et restitution de
|
||||
l'image claire initiale vérifiés. La contribution normalisée du volume dans
|
||||
cette scène passe d'environ 0,0034 au clair à 0,0236 sous brouillard ; ce n'est
|
||||
pas un multiplicateur universel de luminosité. L'orage reste sous la pluie.
|
||||
|
||||
Log : `build/shader138-client-final.log`, marqueurs `SUNSHAFT137_PASS` et
|
||||
`SUNSHAFT138_WEATHER_PASS`. Comparatif natif autonome :
|
||||
`build/Shader-beta.138-comparaison.html`.
|
||||
|
||||
## Diagnostic de la pluie
|
||||
|
||||
La première exécution native a validé le brouillard, puis s'est arrêtée en
|
||||
attendant des rayons sous une pluie maximale. Minecraft 26.3 fournit alors
|
||||
`rainBrightness = 1 - rainLevel`, soit zéro : beta.137 ne construisait plus la
|
||||
carte solaire. Le volume reçoit désormais son atténuation météo propre et
|
||||
conserve la carte nécessaire, sans réactiver les ombres de surface sous la
|
||||
pluie maximale. Log de diagnostic : `build/shader138-client-first.log`.
|
||||
|
||||
## Limites
|
||||
|
||||
Coefficients artistiques bornés, sans simulation physique de gouttes ni de
|
||||
nuages. Le brouillard environnemental est distinct de la simple distance
|
||||
d'affichage ; la pluie et la neige partagent le niveau natif de précipitation.
|
||||
Les limites des volumes beta.137 restent applicables : 64 blocs maximum,
|
||||
formes de la carte d'ombres et feuillage approximé. Pas de mesure de FPS ni
|
||||
de validation Windows/Vulkan revendiquée.
|
||||
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 20 s**, 126 tâches. Le serveur GameTest dédié reste exclu
|
||||
conformément au refus EULA antérieur. Log : `build/shader138-build.log`.
|
||||
Comparaison à beta.137 : **6 entrées de production** modifiées, uniquement
|
||||
le rendu et les aides FR/EN. Gameplay et textures des gemmes strictement
|
||||
conservés. JAR normal/Test identique, sources et template vérifiés.
|
||||
Reçu local : `build/shader138-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.138.mrpack` : 10432860 octets ; SHA-256 `67093d3a53e3d90c31e8fb940d709ccf41ad730595e358346fa27ad744b44460`.
|
||||
|
||||
- `Sanctuary-Test-beta.138.mrpack` : 10451787 octets ; SHA-256 `1ff39158b47f44a831b6506c8c2636441b2bff02644d20b1698330097e5ff059`.
|
||||
|
||||
JAR : `44b6dd14f0e962fcb6f1efd61e0e7bb2c8dbb033cef81598582ec16c46a46518`.
|
||||
|
||||
## Livraison
|
||||
|
||||
[Release beta.138](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.138)
|
||||
publiée depuis `daf36fdeacca707930092363589c89984af78f3b`. Canal packwiz
|
||||
`c049423de1971079b9d61874d83adeb6f1ad49f7` ; téléchargements publics vérifiés par SHA-256.
|
||||
Deux synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussissent. Un seul JAR beta.138 actif ; les **923 fichiers personnels suivis**,
|
||||
dont sauvegardes et préférences, gardent leurs hashes. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.138/`, aucun monde personnel ouvert.
|
||||
|
||||
Packs normal/Test/template et comparatif natif également vérifiés dans
|
||||
`sanctuary-beta/build/`. Le checkout original garde ses changements préexistants.
|
||||
Reçus locaux ignorés : `build/shader138-artifact.json`, `build/shader138-isolated.json`,
|
||||
`build/shader138-prism.json`, `build/shader138-publication.log`.
|
||||
@@ -0,0 +1,99 @@
|
||||
# SHADER-139 — Compatibilité Vulkan du shader beta.138
|
||||
|
||||
Socle **beta.138**, Minecraft **26.3**, Java **25**, branche
|
||||
`codex/vulkan-shaders-beta139`.
|
||||
|
||||
Le signalement initial concernait un bloom invisible même à 100 %. Le joueur
|
||||
indique ensuite que le rendu fonctionne de nouveau sur Windows et demande
|
||||
la compatibilité Vulkan. Les changements expérimentaux d'intensité ont été
|
||||
retirés : les shaders et les valeurs par défaut restent ceux de beta.138.
|
||||
|
||||
## Contrat
|
||||
|
||||
Utiliser les mêmes pipelines RenderPearl sur OpenGL et Vulkan. Vérifier le
|
||||
backend réellement utilisé, car Minecraft peut retomber sur OpenGL lorsque
|
||||
Vulkan ne démarre pas. Tester les effets dans de nouveaux mondes ignorés,
|
||||
sans ouvrir de sauvegarde personnelle ni modifier le backend de l'instance Prism.
|
||||
|
||||
Le lancement des tests accepte désormais `-PsanctuaryClientGraphicsBackend=vulkan`
|
||||
ou `opengl`. Les tests de shaders refusent tout repli silencieux et consignent
|
||||
le nom du backend, du GPU et du pilote. Cette option concerne uniquement
|
||||
le client de développement.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/chemin/vers/jdk-25 ./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPixelShadows122ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Sur Windows, utiliser `gradlew.bat` avec les mêmes propriétés et Java 25.
|
||||
Ajouter `-PsanctuaryClientImprovedTransparency=true` pour tester la transparence
|
||||
améliorée ; le choix initial est restauré après le test.
|
||||
Pour les rayons et la météo, remplacer `sanctuaryPixelShadows122ClientTests`
|
||||
par `sanctuarySunshaft137ClientTests`. Pour isoler le bloom et le lens flare,
|
||||
utiliser `sanctuaryBloom130ClientTests`.
|
||||
|
||||
## Diagnostic initial
|
||||
|
||||
Le client Vulkan démarre ici sur **Apple M1 / MoltenVK 1.4.2**, API 1.2.350.
|
||||
Le test du bloom réussit en **2 min 19 s** : les masques des vingt minerais,
|
||||
les contrôles OFF/zéro, intensité/diffusion, l'occlusion, le rechargement,
|
||||
les torches, glowstone, lampes, lave, soleil et le lens flare sont vérifiés.
|
||||
Les mesures de référence du bloom sont identiques à celles d'OpenGL :
|
||||
14649 pixels de minerai, gain du halo faible 218131, fort 1970502,
|
||||
gain solaire 7708. Log : `build/shader139-vulkan-first.log`.
|
||||
|
||||
Cela confirme cette combinaison précise ; ce n'est pas une validation des
|
||||
pilotes NVIDIA/AMD/Intel sous Windows. En cas de retour du problème, le
|
||||
`logs/latest.log` de la session Windows concernée est nécessaire pour relever
|
||||
le backend effectif, le GPU, le pilote et les éventuelles erreurs de pipeline.
|
||||
|
||||
## Suite Vulkan
|
||||
|
||||
`sanctuaryPixelShadows122ClientTests` réussit en **8 min 50 s**. Les contrôles
|
||||
couvrent ombres diurnes/nocturnes, feuillage, stabilité en temps réel,
|
||||
highlights CTM, bloom et lens flare, SSGI en déplacement et près des surfaces,
|
||||
PBR directionnel, rechargement, redimensionnement et interfaces FR/EN.
|
||||
Log : `build/shader139-vulkan-suite2.log`.
|
||||
|
||||
Le bloom et le lens flare réussissent aussi avec
|
||||
`sanctuaryClientImprovedTransparency=true`, en **1 min 45 s**. Le backend
|
||||
est bien Vulkan, sa profondeur est en `[0, 1]` et l'option de transparence
|
||||
est active. Les mesures du bloom restent identiques.
|
||||
Log : `build/shader139-vulkan-transparency.log`.
|
||||
|
||||
La suite météo/rayons beta.138 n'a pas été rejouée sous Vulkan dans ce ticket ;
|
||||
les résultats ci-dessus portent sur les effets explicitement listés.
|
||||
Le joueur confirme que le retour à OpenGL rétablit l'effet sur Windows.
|
||||
Ce défaut particulier reste à reproduire à partir d'un journal récent.
|
||||
|
||||
Un premier lancement utilisait un nom de propriété incomplet et a exécuté
|
||||
la suite générale, échouant dans Blocodex avant les tests de shaders.
|
||||
Ce lancement n'est pas compté comme une validation Vulkan des shaders.
|
||||
La commande corrigée utilise `sanctuaryPixelShadows122ClientTests`.
|
||||
|
||||
## Construction
|
||||
|
||||
`./gradlew check build -x :sanctuary:runGameTest` réussit ; le serveur
|
||||
GameTest dédié reste exclu conformément au refus EULA antérieur.
|
||||
Log : `build/shader139-check-build.log`. Le JAR reconstruit est **strictement
|
||||
identique, octet pour octet**, au JAR Sanctuary de la release beta.138.
|
||||
La distribution ne change pas et ne nécessite donc pas de nouvel assemblage.
|
||||
|
||||
## Distribution
|
||||
|
||||
Ce ticket ajoute des options de lancement de tests et leur documentation.
|
||||
Les shaders de production et les réglages de beta.138 ne sont pas changés.
|
||||
Aucune nouvelle livraison binaire, aucun tag beta.139 ni mise à jour de
|
||||
l'instance personnelle n'est effectué pour une vérification seule.
|
||||
|
||||
## Référence du moteur
|
||||
|
||||
Minecraft 26.3 compile les shaders OpenGL et Vulkan via ShaderC et expose
|
||||
`RENDERPEARL_DEPTH_IS_ZERO_TO_ONE` pour la convention de profondeur.
|
||||
[Notes officielles Minecraft 26.3](https://feedback.minecraft.net/hc/en-us/articles/48913133328013-Minecraft-Java-Edition-26-3).
|
||||
Les effets Sanctuary transmettent déjà la convention du périphérique aux
|
||||
reconstructions de position ; aucun remplacement spéculatif n'est appliqué.
|
||||
@@ -102,3 +102,14 @@ client natif : témoin `SHADER040_CLIENT_PASS` dans
|
||||
`build/operator040-client-final.log`. Captures dans
|
||||
`build/operator040-evidence/final/`, reçu dans `build/operator040-artifact.json`.
|
||||
Les contrôles ne revendiquent pas un essai avec Iris ou un autre moteur externe.
|
||||
|
||||
|
||||
Les ombres portées pixélisées sont livrées dans [beta.127](shader-beta127.md),
|
||||
intégrées sur la beta.126. Leur préparation locale portait un numéro beta.122
|
||||
non publié ; la release beta.122 existante n'est pas remplacée.
|
||||
|
||||
## beta.144 — Eau naturelle
|
||||
|
||||
Le [ticket WATER-144](natural-water-beta144.md) ajoute les profils clair,
|
||||
légèrement teinté et étang au rendu natif des fluides et à leur brouillard.
|
||||
La couleur du biome et les textures pixélisées restent la base.
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# SNOW-108 — Accumulation de neige en volume
|
||||
|
||||
Branche `codex/snow-accumulation-beta108`. Base livrée beta.107, Minecraft 26.3.
|
||||
État : livraison locale vérifiée. Statuaire beta.106 reste une livraison séparée.
|
||||
|
||||
## Contrat avant modification
|
||||
|
||||
Demande : la neige doit s’accumuler et occuper réellement des blocs.
|
||||
Les précipitations déposent une couche à la fois ; la huitième devient un bloc
|
||||
natif de neige solide. Le dépôt suivant commence au-dessus. Même cadence de
|
||||
précipitation que Minecraft, uniquement dans les chunks simulés pendant une
|
||||
chute de neige, y compris les averses Sanctuary et les biomes froids vanilla
|
||||
lorsque les règles Sanctuary sont actives.
|
||||
|
||||
Valeur confirmée par le créateur : deux blocs de hauteur totale par défaut,
|
||||
réglables côté serveur.
|
||||
Le plafond compte la colonne contiguë de neige déjà présente (couches et blocs),
|
||||
jusqu’au premier support d’une autre matière. Zéro désactive l’accumulation,
|
||||
moins un permet de continuer jusqu’à la limite de hauteur du monde.
|
||||
|
||||
La lumière, les supports, la place libre et les biomes acceptables restent
|
||||
contrôlés par Minecraft. Ni plantes, ni équipements, ni eau, ni constructions
|
||||
ne sont remplacés. La croissance pousse les entités vers le haut comme le dépôt
|
||||
natif, avec la nouvelle collision. Les chaudrons gardent leur fonctionnement.
|
||||
|
||||
## Données et compatibilité
|
||||
|
||||
Uniquement des blocs natifs `minecraft:snow` et `minecraft:snow_block` : collision,
|
||||
minage, butin et sauvegarde natifs. Pas de bloc, BlockEntity ou format propriétaire.
|
||||
La nouvelle gamerule `sanctuary:snow_accumulation_height` utilise la sauvegarde
|
||||
native des règles, sans réécriture des règles préexistantes. Elle s’ajoute avec
|
||||
sa valeur par défaut quand absente. Aucune sauvegarde personnelle ouverte, aucun
|
||||
balayage de chunks, aucune expansion ni conversion de terrain au chargement.
|
||||
Seuls les dépôts futurs pendant le jeu, autorisés par la demande, changent.
|
||||
|
||||
Les blocs de neige pleins suivent le comportement Minecraft : ils ne fondent pas
|
||||
comme les fines couches éclairées. Cette livraison n’ajoute pas une fonte
|
||||
saisonnière qui pourrait effacer des constructions en neige.
|
||||
|
||||
La valeur native `minecraft:max_snow_accumulation_height = 0` reste un arrêt
|
||||
prioritaire. Dans un monde Sanctuary, toute valeur native positive autorise le
|
||||
dépôt volumétrique, dont le plafond est la nouvelle règle en blocs. Hors Sanctuary,
|
||||
le fonctionnement et la limite vanilla sont conservés.
|
||||
|
||||
## Utilisation
|
||||
|
||||
- Valeur par défaut : `/gamerule sanctuary:snow_accumulation_height 2`.
|
||||
- Quatre blocs : `/gamerule sanctuary:snow_accumulation_height 4`.
|
||||
- Arrêt des nouveaux dépôts : valeur `0`.
|
||||
- Sans plafond particulier, sauf celui du monde : valeur `-1`.
|
||||
- Valeurs admises : de `-1` à `128`, positives exprimées en blocs complets.
|
||||
|
||||
Réduire le plafond ne supprime pas la neige déjà présente. La limite native
|
||||
`minecraft:max_snow_accumulation_height` n’est pas réécrite automatiquement.
|
||||
Les noms et descriptions de la nouvelle règle sont traduits en FR/EN dans
|
||||
l’interface native de création du monde.
|
||||
|
||||
## Essais natifs
|
||||
|
||||
Nouveaux mondes plats de développement, graine `108`, un avec Sanctuary et
|
||||
un sans. Le test appelle le véritable `ServerLevel.tickPrecipitation` :
|
||||
|
||||
- Sept couches successives, huitième dépôt en `snow_block`, collision pleine.
|
||||
- Un cochon debout sur la neige monte avec le nouveau volume.
|
||||
- Empilement sur le bloc formé ; arrêt à deux blocs ; commande à quatre blocs,
|
||||
puis mode sans plafond et six blocs atteints.
|
||||
- Arrêt à zéro sur la règle Sanctuary et priorité du zéro sur la règle native.
|
||||
- Dépôt pendant les averses, aucun dépôt pendant leurs pauses ni sous pluie tiède.
|
||||
- Fleur et eau libre préservées, neige sur le toit et non dans la pièce ;
|
||||
lumière propagée par un bloc lumineux empêchant le dépôt.
|
||||
- Le chaudron continue de recevoir sa neige poudreuse native.
|
||||
- Météo vanilla, biome froid et Sanctuary actif : accumulation volumétrique.
|
||||
- Second monde sans Sanctuary : une couche native même après 40 tentatives.
|
||||
|
||||
La décision d’activation est mise en cache au démarrage du serveur, d’après le
|
||||
choix Sanctuary fixé à la création. Aucun accès disque par colonne de pluie.
|
||||
Le comptage de profondeur s’arrête dès le plafond atteint ; le mode illimité
|
||||
n’a pas besoin de compter. Pas de boucle ajoutée sur les chunks ou les joueurs.
|
||||
|
||||
Les tests ne changent aucune sauvegarde personnelle. Les dépôts sont déclenchés
|
||||
explicitement pour valider leur résultat ; aucun facteur d’accélération météo
|
||||
n’est ajouté au code livré.
|
||||
|
||||
## Livraison et vérifications — 17 septembre 2026
|
||||
|
||||
Validation dans le worktree `sanctuary-snow-beta108`, sur la base immuable
|
||||
beta.107. Les œufs de beta.107 sont inclus ; les sources Statuaire beta.106
|
||||
présentes dans le répertoire principal restent préservées, mais ne sont pas
|
||||
incluses dans ces archives. Les compteurs de ce worktree sont synchronisés à
|
||||
beta.108 ; ceux du répertoire principal restent attachés à son chantier beta.106.
|
||||
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` :
|
||||
réussi, 125 tâches, 3 min 34 s.
|
||||
- `./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuarySnow108ClientTests=true -PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true` :
|
||||
réussi, 1 min 17 s, marqueur `SNOW108_NATIVE_PASS`.
|
||||
- Java 25 ; GameTest dédié exclu conformément au refus antérieur d'accepter son
|
||||
EULA. Les essais ci-dessus utilisent le serveur intégré dans deux mondes neufs.
|
||||
- Intégrité ZIP, versions Minecraft/mod, JAR embarqués, sources, mixins et libellés
|
||||
vérifiés. Par rapport au JAR beta.107 : seulement `WeatherService.class`
|
||||
modifié et les deux classes d'accumulation ajoutées ; assets inchangés sauf
|
||||
les deux nouveaux libellés de règle FR/EN. Aucun test embarqué dans le JAR livré.
|
||||
|
||||
Archives locales : [pack normal](../build/Sanctuary-beta.108.mrpack) et
|
||||
[pack Test](../build/Sanctuary-Test-beta.108.mrpack).
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| Normal | `18f2b0c6a12c3618a9b70e22999b89697a41034e86bc1aa33f0c9d19caadd0c2` |
|
||||
| Test | `66f9395cd47b031ca52162081c397da7a5ab7fdfa3e236dec5110dbcf95306bc` |
|
||||
| JAR Sanctuary | `dc2b026cd0b6475e512283f72225593666fd60a3a265b8e91bf6bdae9f875bb4` |
|
||||
|
||||
Reçu : `build/snow108-artifact.json`. Journaux : `build/snow108-native.log`,
|
||||
`build/snow108-check-build.log`. Manifeste : `build/snow108-source-manifest.json`.
|
||||
Aucun tag, publication, canal packwiz, installation Prism ou monde personnel
|
||||
modifié. Pas d'essai Windows ; pas de fonte saisonnière des blocs pleins.
|
||||
@@ -0,0 +1,107 @@
|
||||
# EGGS-107 — Naissances en œufs et récupération des générateurs
|
||||
|
||||
Branche `codex/eggs-beta107`. Travail isolé du chantier Statuaire beta.106.
|
||||
Base vérifiée : sources de la livraison beta.105. État : livré localement, vérifié ; aucun déploiement ni publication.
|
||||
|
||||
## Contrat
|
||||
|
||||
Dans les mondes où Sanctuary est actif, une reproduction réussie remet un
|
||||
œuf natif de l'espèce au sol au lieu d'ajouter directement le nouveau mob.
|
||||
L'enfant est calculé par Minecraft : croisements, variantes, propriétaire et
|
||||
caractéristiques héritées sont conservés. Les délais, nourriture, statistiques
|
||||
et progrès restent ceux de la reproduction native. L'œuf d'un animal qui a un
|
||||
stade bébé fait naître un bébé, y compris avec un distributeur.
|
||||
|
||||
Grenouille : œuf de têtard, pour conserver son cycle naturel. Tortue et sniffer :
|
||||
œuf d'apparition de bébé à la place de la ponte. Allay : duplication contre
|
||||
améthyste et musique, avec le délai natif conservé. Villageois : nourriture et
|
||||
lit libre restent requis ; le lit réservé est libéré tant que l'œuf est stocké.
|
||||
|
||||
Un générateur classique réellement cassé par un joueur en survie, avec un outil
|
||||
adapté sans Toucher de soie, remet un seul œuf correspondant à son prochain mob
|
||||
configuré. Fortune ne multiplie pas ce résultat. Ni explosion, ni annulation de
|
||||
casse, ni mode créatif ne produisent cet œuf. Les générateurs d'épreuves ne sont
|
||||
pas transformés en source d'œufs par cette règle.
|
||||
|
||||
Les identifiants natifs des items restent inchangés. Les données de naissance
|
||||
utilisent les composants d'item natifs ENTITY_DATA et CUSTOM_DATA ; aucune
|
||||
migration de sauvegarde, régénération ou ouverture de monde personnel.
|
||||
Les naissances et pontes déjà présentes restent en place. Aucune modification
|
||||
hors des mondes Sanctuary ; un œuf déjà obtenu garde ses données de naissance.
|
||||
|
||||
Un catalogue distinct des 88 espèces sépare les voies livrées des propositions
|
||||
à valider. Les méthodes spéciales restantes ne sont pas annoncées comme livrées.
|
||||
|
||||
## Catalogue
|
||||
|
||||
[Les 88 espèces et leurs voies](spawn-eggs-catalogue-beta107.md) : 28 par
|
||||
reproduction ou duplication, 7 par les générateurs classiques vanilla et
|
||||
53 propositions. Réponse du créateur : **catalogue à valider d’abord**.
|
||||
Aucune des 53 propositions n’est implémentée dans ce ticket.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Nouveau monde de test plat, graine `107`, Minecraft 26.3 / Fabric 0.160.5+26.3,
|
||||
Java 25, macOS. Le test utilise le vrai client et son serveur intégré :
|
||||
|
||||
- Naissance native de 22 espèces via `Animal.spawnChildFromBreeding` : un seul
|
||||
œuf, aucun enfant vivant ajouté, âge de bébé stocké, délai des deux parents.
|
||||
- Cheval × âne → mule ; moutons rouge × bleu → bébé violet, à la pose et au clic
|
||||
sur un adulte ; loup apprivoisé → bébé encore lié au même propriétaire.
|
||||
- Sérialisation/relecture de l’item avec le codec natif ; position, UUID et
|
||||
réservations de cerveau absents ; éclosion en survie consomme un œuf.
|
||||
- Vrai comportement de distributeur : bébé et couleur héritée.
|
||||
- Grenouille sans seconde ponte, tortue sans état de ponte, renifleur sans œuf
|
||||
bloc supplémentaire ; allay avec délai conservé ; villageois avec résultat
|
||||
vide pour que l’appelant rende le lit réservé, bébé à l’éclosion.
|
||||
- Existence native des 88 œufs ; destruction réelle d’un générateur par le
|
||||
contrôleur joueur avec pioche normale/Fortune/Toucher de soie, main vide,
|
||||
créatif et règle de drops de blocs désactivée.
|
||||
- Deuxième monde neuf, Sanctuary désactivé à la création : bébé de vache vivant,
|
||||
aucun œuf et générateurs sans nouveau butin.
|
||||
|
||||
Les appels natifs de naissance sont testés directement ; il ne s’agit pas d’un
|
||||
élevage de chaque espèce laissé tourner pendant plusieurs minutes. Les tests
|
||||
n’effectuent pas de parcours complet de raid, donjon ou guérison : ces voies
|
||||
spéciales sont des propositions. Aucun monde personnel modifié.
|
||||
|
||||
## Binaire et distribution
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit : **125 tâches, 3 min 44 s**. Le serveur GameTest dédié reste exclu,
|
||||
l’acceptation de son EULA n’étant pas autorisée. Les tests natifs ci-dessus
|
||||
utilisent le serveur intégré ; dernier passage **29 s**, marqueur
|
||||
`EGGS107_NATIVE_PASS`, après les derniers changements de code.
|
||||
|
||||
Commande des essais :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryEggs107ClientTests=true -PsanctuaryClientNoVsync=true \
|
||||
-PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
L’assemblage et les essais se font dans le worktree voisin
|
||||
`sanctuary-eggs-beta107`, branche `codex/eggs-beta107`, sur les sources vérifiées
|
||||
de beta.105. Les changements propres aux œufs sont aussi intégrés au répertoire
|
||||
principal sans réécrire le code ni les compteurs en cours du chantier beta.106.
|
||||
**Ces archives ne contiennent pas le Statuaire beta.106 en cours de validation.**
|
||||
Les compteurs du worktree et du pack livré sont tous `beta.107`.
|
||||
|
||||
Comparaison binaire avec beta.105 : neuf classes ajoutées, aucune ancienne classe
|
||||
modifiée. Les seules ressources graphiques/données différentes sont les deux
|
||||
fichiers de langue FR/EN, avec deux nouveaux libellés ; le pack de textures,
|
||||
les recettes et la génération restent byte pour byte identiques. Les mixins
|
||||
ajoutés figurent dans le manifeste. Sources/JAR, ZIP, versions, dépendances,
|
||||
88 lignes uniques du catalogue et empreintes sont vérifiés.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.107.mrpack) — SHA-256
|
||||
`1b92c5abcc07be9c36d507d2ffdaf6d420a541c6624f105f8643594a02884600`.
|
||||
- [Pack de test](../build/Sanctuary-Test-beta.107.mrpack) — SHA-256
|
||||
`db9e32591cb263667b397fdb78738fbe45f58689b5574f2cb483f076635e7eef`.
|
||||
|
||||
Logs et reçus ignorés : `build/eggs107-check-build.log`,
|
||||
`build/eggs107-native.log`, `build/eggs107-artifact.json`,
|
||||
`build/eggs107-source-manifest.json`. Les fichiers sources des tests sont inclus
|
||||
dans le dépôt ; aucune classe de test n’est embarquée dans le mod livré.
|
||||
Aucun test Windows, installation Prism, publication du canal, commit ou tag.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Acquisition des 88 œufs — catalogue à valider
|
||||
|
||||
Demande du créateur : **valider les voies spéciales avant de les implémenter**.
|
||||
Ce catalogue utilise les 88 identifiants de `CompanionType`, pour Minecraft 26.3.
|
||||
Les œufs natifs servent aussi aux familiers. Aucun nouvel œuf propriétaire.
|
||||
|
||||
**28 espèces** ont une voie par reproduction/duplication dans EGGS-107,
|
||||
**7** par les générateurs classiques des structures vanilla,
|
||||
et **53** attendent une voie spéciale.
|
||||
|
||||
Les structures doivent être présentes et accessibles dans la partie : cette
|
||||
livraison n’ajoute aucun donjon, ne régénère aucun chunk et ne garantit pas ces
|
||||
structures dans un monde plat. Un générateur classique configuré autrement
|
||||
remet aussi l’œuf de son type, s’il existe ; cela ne crée pas une voie naturelle
|
||||
pour toutes les espèces. Les générateurs d’épreuves ont une proposition distincte.
|
||||
|
||||
## Voies intégrées à EGGS-107
|
||||
|
||||
Une reproduction donne **un œuf**, jamais simultanément un bébé. Les règles
|
||||
natives d’accouplement, d’apprivoisement, de nourriture et de délai restent
|
||||
applicables. Les poissons crus ne remplacent pas un seau quand le jeu exige un
|
||||
poisson vivant. Les œufs de bébé restent bébés après stockage ; le temps de
|
||||
croissance commence à l’éclosion.
|
||||
|
||||
| Espèce (identifiant natif) | Obtention |
|
||||
| --- | --- |
|
||||
| `cat` | Deux chats apprivoisés, morue ou saumon crus. |
|
||||
| `chicken` | Deux poules, graines. |
|
||||
| `cow` | Deux vaches, blé. |
|
||||
| `donkey` | Deux ânes apprivoisés, carottes ou pommes dorées. |
|
||||
| `fox` | Deux renards, baies sucrées ou lumineuses ; confiance héritée. |
|
||||
| `horse` | Deux chevaux apprivoisés, carottes ou pommes dorées ; caractéristiques héritées. |
|
||||
| `llama` | Deux lamas apprivoisés, bottes de paille ; force et variante héritées. |
|
||||
| `mule` | Croisement cheval × âne. La mule reste stérile. |
|
||||
| `pig` | Deux cochons, carotte, pomme de terre ou betterave. |
|
||||
| `rabbit` | Deux lapins, carotte, carotte dorée ou pissenlit. |
|
||||
| `sheep` | Deux moutons, blé ; mélange de couleurs natif. |
|
||||
| `tadpole` | Deux grenouilles, boules de slime ; œuf de têtard à la place de la ponte aquatique. |
|
||||
| `armadillo` | Deux tatous, yeux d’araignée. |
|
||||
| `bee` | Deux abeilles, fleurs. |
|
||||
| `camel` | Deux dromadaires, cactus. |
|
||||
| `goat` | Deux chèvres, blé. |
|
||||
| `ocelot` | Deux ocelots, morue ou saumon crus. |
|
||||
| `panda` | Deux pandas, bambou et environnement de reproduction natif ; gènes hérités. |
|
||||
| `strider` | Deux arpenteurs, champignons biscornus. |
|
||||
| `turtle` | Deux tortues, herbes aquatiques ; œuf de bébé à la place de la ponte. |
|
||||
| `villager` | Reproduction native des villageois : nourriture suffisante et lit libre accessible. L’œuf donne un bébé. |
|
||||
| `wolf` | Deux loups apprivoisés, nourriture native ; propriétaire hérité. |
|
||||
| `allay` | Duplication native : allay dansant avec un jukebox, éclat d’améthyste. Délai de 5 minutes conservé. |
|
||||
| `axolotl` | Deux axolotls, poissons tropicaux en seau. |
|
||||
| `hoglin` | Deux hoglins, champignons carmin. |
|
||||
| `mooshroom` | Deux champimeuhs, blé. |
|
||||
| `nautilus` | Deux nautiles apprivoisés, nourriture acceptée par la reproduction native. |
|
||||
| `sniffer` | Deux renifleurs, graines de plante torche ; œuf d’apparition à la place de l’œuf bloc. |
|
||||
|
||||
Pour les suivants : casser le générateur avec un outil adapté, sans Toucher de
|
||||
soie. Un œuf par générateur ; Fortune ne multiplie pas le résultat. Les outils
|
||||
avec Toucher de soie ne donnent pas d’œuf ; cette livraison ne leur ajoute pas
|
||||
non plus une récupération du bloc générateur.
|
||||
|
||||
| Espèce (identifiant natif) | Source vanilla si présente |
|
||||
| --- | --- |
|
||||
| `skeleton` | Générateur de donjon. |
|
||||
| `spider` | Générateur de donjon. |
|
||||
| `zombie` | Générateur de donjon. |
|
||||
| `cave_spider` | Générateur de mine abandonnée. |
|
||||
| `blaze` | Générateur de forteresse du Nether. |
|
||||
| `magma_cube` | Générateur de salle au trésor de bastion. |
|
||||
| `silverfish` | Générateur de la salle du portail du stronghold. |
|
||||
|
||||
## Propositions — pas encore implémentées
|
||||
|
||||
Les pourcentages ci-dessous sont des valeurs de départ à valider. Les captures
|
||||
**convertissent le mob en œuf** et ne laissent pas son double dans le monde.
|
||||
Pour les butins et récompenses, un mob né d’un œuf ne pourra pas produire son
|
||||
propre œuf par cette voie. Les récompenses uniques devront avoir un suivi serveur
|
||||
persistant : contrat de migration à définir lors du prochain ticket. Ces règles
|
||||
ne sont pas encore ajoutées au jeu.
|
||||
|
||||
| Espèce | Type de voie | Proposition concrète |
|
||||
| --- | --- | --- |
|
||||
| `bat` | Interaction | Capturer une chauve-souris endormie avec un sac et une boule de slime. Le mob devient un œuf ; sac rendu. |
|
||||
| `cod` | Capture | Morue en seau + œuf de poule : convertir le poisson en œuf d’apparition ; seau vide rendu. |
|
||||
| `frog` | Interaction | Élever un têtard jusqu’à la métamorphose, puis donner une boule de slime : un œuf de grenouille, une seule fois par grenouille élevée. |
|
||||
| `salmon` | Capture | Saumon en seau + œuf de poule ; seau vide rendu. |
|
||||
| `slime` | Butin conditionnel | Tuer un petit slime au corps à corps : 5 % de chance, uniquement sur la génération issue d’un slime naturel. |
|
||||
| `squid` | Capture | Interagir avec un poulpe sous l’eau avec un sac et une morue : le convertir en œuf ; sac rendu. |
|
||||
| `tropical_fish` | Capture | Poisson tropical en seau + œuf de poule ; conserver exactement ses couleurs et son motif. |
|
||||
| `copper_golem` | Construction | Construire un golem de cuivre puis lui faire terminer 16 tris réels : un œuf, une seule fois par golem construit. |
|
||||
| `dolphin` | Exploration | Nourrir un dauphin et atteindre le trésor qu’il indique : un œuf à la découverte, une fois par trésor et joueur. |
|
||||
| `drowned` | Butin conditionnel | Noyé naturel tué sous l’eau par un joueur : 5 %. |
|
||||
| `glow_squid` | Capture | Même capture que le poulpe, dans une caverne immergée ; conversion du mob en œuf lumineux. |
|
||||
| `husk` | Butin conditionnel | Zombie momifié naturel tué dans le désert en plein jour : 5 %. |
|
||||
| `parrot` | Interaction | Apprivoiser un perroquet puis lui offrir des graines pendant sa danse au jukebox : un œuf, une seule fois par perroquet sauvage apprivoisé. |
|
||||
| `pufferfish` | Capture | Poisson-globe en seau + œuf de poule ; seau vide rendu. |
|
||||
| `polar_bear` | Interaction | Donner du saumon à deux adultes sur neige : reproduction Sanctuary spécifique donnant un œuf de bébé, délai de 5 minutes. |
|
||||
| `snow_golem` | Construction | Monter le golem de neige natif puis l’emballer avec une citrouille sculptée : conversion du golem en œuf. |
|
||||
| `bogged` | Épreuve | Après une vague terminée, toucher son générateur d’épreuves avec un œuf de poule : un œuf de bogged, une fois par joueur et recharge native. |
|
||||
| `parched` | Butin conditionnel | Parched naturel vaincu dans le désert avec un bouclier après avoir bloqué sa flèche : 5 %. |
|
||||
| `breeze` | Épreuve | Même extraction après victoire sur les vagues de Breeze ; coût d’un œuf de poule, recharge native du générateur d’épreuves. |
|
||||
| `camel_husk` | Rencontre | Désarçonner les cavaliers hostiles puis apprivoiser la monture avec sa nourriture native : un œuf, une seule fois pour cette monture sauvage. |
|
||||
| `creeper` | Butin conditionnel | Creeper naturel tué par un squelette, comme pour les disques : 10 %. |
|
||||
| `endermite` | Interaction | Toucher une endermite apparue d’une perle avec un fruit de chorus : conversion en œuf avant sa disparition. |
|
||||
| `guardian` | Butin conditionnel | Gardien naturel vaincu à proximité d’un monument : 5 %. |
|
||||
| `phantom` | Butin conditionnel | Phantom naturel vaincu dans les airs avec une arme de mêlée : 10 %. |
|
||||
| `piglin` | Troc | Ajouter l’œuf au troc contre un lingot d’or : 2 %, poids exact à équilibrer. |
|
||||
| `pillager` | Butin conditionnel | Pillard capitaine naturel vaincu par un joueur : 10 %. |
|
||||
| `sulfur_cube` | Capture | Cube de soufre en seau + œuf de poule : convertir le contenu du seau en œuf, avec taille et contenu conservés. |
|
||||
| `trader_llama` | Commerce | Offre rare du marchand ambulant : œuf de lama de marchand contre émeraudes et botte de paille. Les lamas ordinaires restent obtenus par reproduction. |
|
||||
| `wandering_trader` | Commerce | Terminer toutes les offres d’un marchand : une offre finale d’œuf contre 32 émeraudes. Une fois par marchand naturel. |
|
||||
| `zombie_horse` | Rencontre | Libérer puis apprivoiser un cheval-zombie sauvage avec la nourriture native : un œuf, une seule fois par monture. |
|
||||
| `zombie_villager` | Guérison | Guérir un villageois zombie naturel : un œuf de villageois zombie remis au soigneur ; un seul par individu, sans boucle de réinfection. |
|
||||
| `zombified_piglin` | Butin conditionnel | Piglin zombifié naturel vaincu au Nether par un joueur : 5 %. |
|
||||
| `stray` | Butin conditionnel | Vagabond naturel vaincu pendant une chute de neige : 10 %. |
|
||||
| `vindicator` | Butin conditionnel | Vindicator vaincu dans un manoir ou un raid : 5 %. |
|
||||
| `zoglin` | Transformation | Amener un hoglin dans l’Overworld et le voir devenir zoglin : première interaction avec un champignon carmin remet un œuf, une seule fois. |
|
||||
| `creaking` | Exploration | Neutraliser un grinceur lié puis casser son cœur actif sans Toucher de soie : un œuf. Le cœur ne donne pas simultanément sa version récupérable. |
|
||||
| `elder_guardian` | Boss | Vaincre un grand gardien du monument : un œuf garanti, uniquement pour les gardiens générés avec le monument. |
|
||||
| `enderman` | Butin conditionnel | Enderman naturel vaincu dans l’End par un joueur : 5 %. |
|
||||
| `evoker` | Butin conditionnel | Évocateur vaincu dans un manoir ou un raid : 10 %. |
|
||||
| `ghast` | Défi | Tuer un ghast naturel en lui renvoyant sa propre boule de feu : 25 %. |
|
||||
| `happy_ghast` | Élevage | Réhydrater un ghast desséché puis élever le ghastling : premier nourrissage adulte avec une boule de neige remet un œuf de ghastling, une fois par ghast élevé. |
|
||||
| `iron_golem` | Construction | Construire un golem de fer puis lui offrir un coquelicot et un lingot : conversion volontaire du golem en œuf. |
|
||||
| `piglin_brute` | Exploration | Piglin barbare du bastion vaincu par un joueur : 10 %. |
|
||||
| `ravager` | Raid | Dernier ravageur vaincu dans un raid remporté : un œuf au vainqueur, une récompense par raid. |
|
||||
| `shulker` | Exploration | Shulker d’une cité de l’End vaincu par un joueur : 10 % ; les copies par projectiles ne déclenchent pas cette voie. |
|
||||
| `skeleton_horse` | Événement | Survivre au piège de cavaliers squelettes puis monter un cheval libéré : un œuf, une récompense par piège. |
|
||||
| `vex` | Défi | Tuer un vex invoqué après avoir vaincu l’évocateur qui l’a invoqué : 10 %, une récompense maximum par évocateur. |
|
||||
| `witch` | Butin conditionnel | Sorcière naturelle vaincue pendant qu’elle boit une potion : 10 %. |
|
||||
| `wither_skeleton` | Butin conditionnel | Wither squelette naturel vaincu dans une forteresse : 5 %, indépendant du crâne. |
|
||||
| `zombie_nautilus` | Rencontre | Libérer un nautile-zombie de son cavalier puis l’apprivoiser : un œuf, une seule fois par monture sauvage. |
|
||||
| `ender_dragon` | Boss | Vaincre le dragon invoqué par le cycle natif de l’End : un œuf au participant désigné ; jamais pour un dragon apparu avec un œuf. |
|
||||
| `warden` | Défi | Vaincre un Warden sorti d’un hurleur naturel : un œuf garanti pour ce combat ; aucun œuf sur un Warden issu d’œuf. |
|
||||
| `wither` | Boss | Vaincre un Wither invoqué par sable des âmes et crânes : un œuf garanti ; aucun œuf sur un Wither issu d’œuf. |
|
||||
|
||||
## Points à décider ensemble
|
||||
|
||||
- Le principe de conversion avec un sac / un seau pour les espèces capturables.
|
||||
- La place des défis et du butin rare, et leurs chances exactes.
|
||||
- Les récompenses de boss garanties, avec exclusion des boss issus d’œuf.
|
||||
- Les offres du marchand et le troc restent de simples échanges natifs ; aucun
|
||||
système d’économie, de banque ou de marché n’est introduit ici.
|
||||
|
||||
Cas à ne pas confondre : nourrir ne veut pas toujours dire reproduire.
|
||||
Le ghast heureux, le dromadaire momifié et le cheval-zombie refusent le mode
|
||||
amoureux natif ; le nautile-zombie n’a pas de descendance native. Le lama de
|
||||
marchand suit la reproduction du lama et ne garantit pas un nouvel œuf de
|
||||
`trader_llama`. Le cube de soufre n’emprunte pas la reproduction d’`Animal`.
|
||||
Les œufs pondus naturellement par une poule et leur lancer natif restent inchangés :
|
||||
c’est l’accouplement nourri par le joueur qui produit le nouvel œuf garanti.
|
||||
|
||||
Sources de vérification : catalogue `CompanionType`, classes et données du JAR
|
||||
Minecraft **26.3** installé (méthodes de reproduction, composants d’items et
|
||||
aliments natifs), tests intégrés EGGS-107. Le catalogue ne prétend pas qu’un
|
||||
comportement proposé est déjà une règle vanilla.
|
||||
@@ -0,0 +1,172 @@
|
||||
# STAT-01 — Atelier d’argile et modèles 3D — beta.106
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/statuary-import-beta106`.
|
||||
Livraison locale vérifiée, cycle natif et archives normale/Test validés.
|
||||
Aucune publication ni installation personnelle.
|
||||
|
||||
## Parcours retenu avec le créateur
|
||||
|
||||
Un seul dossier utilisateur : `schematics/sanctuary/`. La lecture historique
|
||||
au premier niveau de `schematics/` reste possible, sans déplacement de fichiers.
|
||||
Les boutons des deux ateliers ouvrent exactement le même dossier.
|
||||
|
||||
- **Métabli → Bibliothèque** : schémas `.litematic`, `.schem`, `.nbt` et `.schematic`
|
||||
historiques, avec leurs règles d’import existantes.
|
||||
- **Métabli → Statues → Mes modèles 3D** : modèles GLB, hauteur 4–96 blocs et
|
||||
palette. La conversion produit un plan et rejoint les commandes K, matériaux,
|
||||
export et règles de placement créatif/survie existants. Les statues des
|
||||
créatures découvertes gardent leur accès habituel.
|
||||
- **Atelier d’argile** (`sanctuary:clay_workshop`) : choix du GLB et aperçu à
|
||||
proportions conservées, limité à 16 × 16 × 16 voxels dans un seul bloc.
|
||||
Un emplacement reçoit l’argile, l’autre fournit la statue. **Un bloc d’argile
|
||||
est consommé pour chaque statue récupérée**, y compris en créatif.
|
||||
Les blocs d’argile normale conservent les couleurs du modèle ; les seize
|
||||
argiles Sanctuary imposent leur couleur unie. Ni boule d’argile ni terre
|
||||
cuite n’est acceptée. Fermer l’atelier restitue l’argile non consommée.
|
||||
|
||||
Recette de l’atelier : un bloc d’argile au centre supérieur, trois dalles de
|
||||
pierre lisse au milieu, trois planches en bas. Le bloc utilise les textures
|
||||
actives de planches, argile et pierre lisse. Le résultat, **Statuaire**
|
||||
(`sanctuary:statuary`), est un objet transportable et posable. Sa miniature se
|
||||
voit dans l’inventaire et le monde. Il n’ouvre pas un éditeur lorsqu’on le pose.
|
||||
|
||||
Le fichier se choisit dans la liste ou par dépôt sur l’écran de l’atelier.
|
||||
« Actualiser » relit le dossier. La rotation proposée à l’atelier d’argile
|
||||
est un quart de tour. Il n’y a pas d’éditeur de sommets ou d’animations.
|
||||
|
||||
## Identité des modèles et sauvegardes — contrat préalable
|
||||
|
||||
Les identifiants des anciens blocs, formats et générations restent inchangés.
|
||||
Les nouveaux blocs n’apparaissent que par fabrication ou placement volontaire.
|
||||
Aucun monde personnel n’est ouvert et aucun chunk existant n’est régénéré.
|
||||
|
||||
Chaque GLB reçoit un identifiant `sanctuary:model/<SHA-256 du fichier source>`.
|
||||
Renommer ou déplacer un fichier ne change pas cet identifiant. Réexporter avec
|
||||
un contenu binaire différent produit un nouvel identifiant. Les différentes
|
||||
argiles et rotations du même modèle conservent l’identité de la source.
|
||||
Il s’agit d’une identité de modèle dans les données, et non d’un nouvel
|
||||
identifiant de bloc enregistré pour chaque import. Elle ne donne aucun droit
|
||||
serveur et ne désigne ni une URL ni un chemin à ouvrir.
|
||||
|
||||
Le statuaire a une BlockEntity dédiée avec un champ `sculpture`, schéma **1** :
|
||||
nom, identifiant du modèle, dimensions et couples position/couleur ARGB des
|
||||
voxels. La position est bornée à 16³, la couleur opaque, les doublons refusés.
|
||||
L’objet transporte ces données dans son composant natif de bloc ; pose, butin,
|
||||
repose et sauvegarde utilisent le cycle Minecraft. Le bloc réplique les données
|
||||
aux clients qui chargent son chunk. Aucun joueur n’a besoin du fichier source
|
||||
pour voir une statue déjà fabriquée. Un schéma inconnu ou invalide chargé dans
|
||||
un bloc est préservé sans être interprété ni édité.
|
||||
|
||||
L’atelier utilise un menu natif temporaire, sans stock caché persistant.
|
||||
Le serveur vérifie le menu actif, son jeton, la proximité, le bloc, les droits
|
||||
et le veto d’utilisation Fabric. La sélection envoie seulement des voxels bornés
|
||||
(un paquet atomique de moins de 32 Kio), jamais un chemin ou un fichier exécutable.
|
||||
La teinte vient du véritable objet d’argile côté serveur et chaque retrait du
|
||||
résultat consomme une unité. La fermeture ou la destruction de l’atelier annule
|
||||
le droit de produire ; le travail local tardif ne change pas un autre menu.
|
||||
|
||||
## Contrat d’import et limites
|
||||
|
||||
GLB **2.0 autonome**, scène par défaut (ou première scène), triangles indexés
|
||||
ou non, transformations hiérarchiques matrice ou TRS, matériaux de couleur
|
||||
et textures PNG/JPEG intégrées, UV avec répétition/clamp/miroir. Le matériau
|
||||
opaque ou à découpe alpha est voxelisé en surface. Le calcul s’effectue sur
|
||||
un fil de travail ; les faces visibles du résultat sont mises en cache pour
|
||||
le rendu. L’extension de matériau non éclairé peut être lue pour sa couleur.
|
||||
|
||||
Limites explicites : fichier 8 Mio, JSON 1 Mio, hiérarchie de 64 niveaux,
|
||||
16 384 triangles, 4 194 304 pixels décodés et autant de pixels de matériaux.
|
||||
Les plans gardent les plafonds historiques de cellules et de volume.
|
||||
Les grands scans demandent donc souvent une simplification préalable.
|
||||
|
||||
Squelettes et morphing doivent être figés avant export. Les animations ne sont
|
||||
pas jouées (la pose statique des nœuds est utilisée). Couleurs de sommets,
|
||||
accessors sparse, primitives non triangulaires, transparence mélangée,
|
||||
compressions et extensions de textures non gérées sont refusés explicitement.
|
||||
Les couleurs de sommets doivent être converties en texture. Aucun téléchargement
|
||||
ni référence externe du GLB n’est résolu. OBJ, FBX, STL, VOX et glTF multifichier
|
||||
ne font pas partie de ce premier import. Un GLB sans surface visible est refusé.
|
||||
|
||||
Une miniature représente au maximum 4096 voxels de 1/16 de bloc, centrés en X/Z,
|
||||
posés au sol. Les petits détails peuvent disparaître. Sa collision est le cube
|
||||
contenant la sculpture ; les voxels ne sont pas des blocs exploitables ou des
|
||||
inventaires. Les argiles utilisent la palette de couleur publiée du mod, avec
|
||||
éclairage du rendu, sans texture de matière distincte sur chaque voxel.
|
||||
|
||||
Aucune dépendance tierce supplémentaire : Minecraft **26.3**, Fabric et Java 25
|
||||
restent ceux du dépôt. Lecture suivant la [spécification Khronos glTF 2.0](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html).
|
||||
|
||||
## Vérifications
|
||||
|
||||
`Statuary106ClientChecks` passe sur un client natif Minecraft 26.3 avec serveur
|
||||
intégré, dans un nouveau monde plat jetable, graine 106. Il vérifie :
|
||||
|
||||
- Lecture d’un GLB texturé avec transformation de scène, conservation des
|
||||
proportions et des couleurs ; refus des fichiers hors limites, références
|
||||
externes, offsets invalides et hiérarchies cycliques.
|
||||
- Import indépendant de [BoxTextured de Khronos](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/BoxTextured),
|
||||
triangles indexés et texture embarquée. Le fichier n’est pas distribué avec le mod.
|
||||
- Dossier partagé avec filtres distincts, rotation, identité du modèle et
|
||||
recoloration par les seize argiles.
|
||||
- Fabrication native en survie : absence de résultat sans argile, coût d’une
|
||||
unité, restitution du reliquat à la fermeture et production par Maj-clic.
|
||||
- Pose par clic natif dans la portée Sanctuary du joueur, butin complet,
|
||||
repose, rendu dans le monde, dans l’inventaire et dans l’aperçu.
|
||||
- Même source convertie au Métabli en plan de 32 blocs de hauteur, sans
|
||||
modifier une miniature existante ; annulation d’un import à la fermeture.
|
||||
- Sauvegarde, suppression du GLB, réouverture du monde : modèle transmis au
|
||||
nouveau client et sculptures colorées conservées dans l’inventaire.
|
||||
- Captures FR/EN relues dans `build/statuary106-evidence/`.
|
||||
|
||||
Commande de reproduction :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Le contrôle de régression `Plans072ClientChecks` passe également sur la copie
|
||||
livrée en **47 s** : **88 modèles natifs, aucun refus**, textures UV, palette
|
||||
active, statues de 32 blocs, rotations, matériaux, export gzip, aperçu, confirmation
|
||||
et annulation en créatif, constructions, refus d’usurpation et pose payée en
|
||||
survie. Commande identique avec `-PsanctuaryPlans072ClientTests=true` à la place
|
||||
de `-PsanctuaryStatuary106ClientTests=true`. Journal :
|
||||
`build/statuary106-plans-regression.log`.
|
||||
|
||||
Les premiers échecs ont permis de corriger le champ natif `recipes` du critère
|
||||
de découverte de recette. Les attentes du test ont ensuite été ajustées pour
|
||||
attendre la confirmation serveur des clics et respecter la portée de pose du
|
||||
nouveau joueur. Aucun contournement de ces règles n’est ajouté au mod.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
La copie `build/release-beta106` part de la livraison beta.105 et ajoute les
|
||||
30 fichiers de production concernés par ce ticket. Les sources beta.107 des
|
||||
œufs, arrivées en parallèle dans le dossier partagé, restent conservées dans
|
||||
leur propre livraison. `build/statuary106-source-manifest.json` décrit la copie.
|
||||
La commande suivante a réussi sur cette copie en **5 min**, **132 tâches** :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack assembleTestPack :sanctuary:runClientGameTest \
|
||||
-x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur d’accepter son
|
||||
EULA. Le serveur intégré a vérifié les écritures, le coût, le butin, la sauvegarde
|
||||
et la transmission au client. Les essais ont tourné sur macOS ; une connexion
|
||||
depuis deux ordinateurs et les autres pilotes graphiques n’ont pas été testés.
|
||||
|
||||
Les sources JAR correspondent exactement aux sources Java de la copie isolée.
|
||||
Les deux archives contiennent le même JAR Sanctuary, avec métadonnées beta.106
|
||||
et Minecraft 26.3. Aucune classe de test ni modèle GLB de test n’est distribué.
|
||||
Rapport et empreintes : `build/statuary106-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.106.mrpack), 10 144 169 octets.
|
||||
SHA-256 : `b8b76ba914c8ceaed1b1d25f0817bd4cdeb7a4bd5ad43222c6970162811ff901`.
|
||||
- [Pack de test](../build/Sanctuary-Test-beta.106.mrpack), 10 163 092 octets.
|
||||
SHA-256 : `e55d5871f1e31d3ed46de32d748eba51be2029c60462cf0155be90b8dd40118b`.
|
||||
|
||||
Aucun tag publié, canal packwiz avancé ou déploiement dans Prism.
|
||||
@@ -0,0 +1,102 @@
|
||||
# beta.097 — crash de préparation des temples
|
||||
|
||||
Ticket local EXP-03, branche `codex/expedition-temple-crash-beta097`.
|
||||
Rapport du 16 septembre 2026 à 13:12:19, Sanctuary beta.089, Minecraft
|
||||
26.3-pre-2 / Windows. Graine du monde : `-4700804240597771092` ; graine
|
||||
locale de Kai/Efe : `7023521385479800534`.
|
||||
|
||||
Le chargement s'arrête dans `ExpansionStructures23.ancientTemples002` : aucun
|
||||
site accepté pour `minecraft:desert_pyramid` après 1 926 colonnes. La recherche
|
||||
historique couvre seulement 72 % du rayon nominal de l'île. Le défaut existe
|
||||
encore dans beta.096 sur Minecraft 26.3 finale.
|
||||
|
||||
## Contrat avant correction
|
||||
|
||||
Les identifiants, biomes, graines, centres des îles et journaux restent inchangés.
|
||||
Le relief reste identique, sauf l’assise locale explicitement autorisée pour un
|
||||
temple de secours lorsqu’aucun sol naturel ne convient. Aucun monde personnel n'est ouvert/modifié,
|
||||
aucun chunk joué n'est régénéré, aucune expansion n'est activée.
|
||||
|
||||
Conserver la recherche historique et son ordre exact en premier : les sites
|
||||
admissibles déjà trouvés ne doivent pas changer. Étudier une recherche de
|
||||
secours finie dans les autres chunks de l'île uniquement après l'échec de cette
|
||||
recherche, avec les mêmes règles natives de biome, de pente, de fondation et
|
||||
de protection. Après l’échec réel de cette recherche sur la graine du rapport, le créateur a
|
||||
explicitement demandé de **forcer la présence du temple**. Un dernier recours
|
||||
classe les sites intérieurs par pente et utilise les pièces vanilla, même si
|
||||
le biome exact n’est pas celui requis pour leur apparition spontanée. La
|
||||
fondation naturelle est testée en premier ; sinon une assise locale est
|
||||
préparée sous le bâtiment. Aucun temple manquant n’est déclaré présent. Le budget de calcul
|
||||
reste borné. Les cas auparavant réussis doivent conserver leurs coordonnées.
|
||||
|
||||
La vérification utilise exclusivement de nouveaux mondes de développement et
|
||||
leur réouverture, avec le client et son serveur intégré. Aucun serveur dédié
|
||||
ni nouvelle acceptation d'EULA.
|
||||
|
||||
La recherche naturelle élargie échoue également sur la graine du rapport
|
||||
(440 candidats supplémentaires, 2 366 colonnes). Le dernier recours trouve une
|
||||
pyramide au chunk `[-4, 43]`, sur sol naturel, sans assise ajoutée pour cette
|
||||
reproduction. Le temple de jungle reste au chunk `[43, 2]`.
|
||||
|
||||
La recherche historique garde 32 768 colonnes au maximum, portée à 65 536 pour
|
||||
la recherche élargie. Le dernier recours utilise un échantillonneur distinct
|
||||
borné à 65 536 colonnes, classe les candidats intérieurs par pente, et examine
|
||||
au plus 32 bâtiments complets. La variante au sol naturel est privilégiée ;
|
||||
l’assise, si nécessaire, garde les vérifications d’emprise, de hauteur et de
|
||||
protection. Les contraintes naturelles exactes de biome/pente ne peuvent alors
|
||||
plus annuler à elles seules l’existence du temple demandé.
|
||||
|
||||
## Contrat du complément NBT
|
||||
|
||||
Le champ entier optionnel `sanctuary_native.temple_foundation_min_y` accompagne
|
||||
seulement les nouveaux starts exigeant une fondation. Son absence conserve le
|
||||
comportement antérieur ; aucun champ n’est ajouté aux starts ordinaires. La
|
||||
fondation en grès ou pierre moussue est produite pendant la décoration native,
|
||||
uniquement dans le chunk en cours, avant la construction vanilla des salles,
|
||||
coffres et pièges. Elle ne s’exécute jamais au chargement d’un chunk déjà généré.
|
||||
Le cache natif conserve ce complément via la sérialisation existante des starts.
|
||||
Aucun journal ni chunk historique n’est migré. Les protections spatiales restent
|
||||
actives, et l’assise est incluse dans la réservation du temple.
|
||||
|
||||
## Aperçu des plans (demande complémentaire)
|
||||
|
||||
Le créateur demande aussi d’identifier les matériaux directement dans l’aperçu.
|
||||
Les modèles et UV du pack actif remplacent les aplats ; états/orientations,
|
||||
formes non cubiques et couleurs de biome sont conservés. Les conflits gardent
|
||||
un contour rouge, les blocs déjà posés disparaissent de l’aperçu. Le nom du bloc
|
||||
visé est affiché. Rendu seulement : aucun bloc n’est posé par cet affichage.
|
||||
|
||||
## Vérifications beta.097
|
||||
|
||||
- Avant correction, reproduction sur 26.3 finale du même échec que beta.089 :
|
||||
graine locale `7023521385479800534`, exactement 1 926 colonnes échantillonnées.
|
||||
- `Temple097ClientChecks` : création réelle avec la graine du rapport, puis
|
||||
réouverture du monde de développement et restauration du cache. Deux temples
|
||||
natifs complets : pyramide **4 coffres et 9 TNT**, jungle **2 coffres et
|
||||
2 distributeurs**. Starts NBT, pièges, blocs et localisation identiques après
|
||||
reprise. Réussi en 3 min 30 s. Le premier essai corrigé avait déjà chargé le
|
||||
monde, mais son pilote attendait au Hello World ; l’automatisation de cette
|
||||
étape a ensuite été ajoutée au test.
|
||||
- Régression du Métabli avec les nouveaux modèles : sélection, K, génération,
|
||||
import/export, pose payée, rotation, masquage, interfaces FR/EN et huit
|
||||
matériaux dont coffre, escalier, four et feuillage. Les contrôles de fondation
|
||||
grès/pierre moussue vérifient aussi le complément NBT, la continuité des
|
||||
écritures, leur découpage strict par chunk et l’absence d’écriture sur un
|
||||
ancien start non marqué. La fondation est vérifiée via un WorldGenLevel de
|
||||
test ; la graine réelle n’en a pas besoin.
|
||||
- Captures inspectées dans `build/preview097-evidence/`. Les modèles ordinaires
|
||||
sont translucides, les modèles spéciaux gardent leur rendu natif. La sélection
|
||||
graphique est bornée aux 4 096 blocs les plus proches dans le champ de vision,
|
||||
à moins de 96 blocs ; les matériaux/comptages concernent toujours le plan entier.
|
||||
|
||||
Les journaux sont `build/temple097-repro.log`, `build/temple097-fallback.log`,
|
||||
`build/temple097-client.log`, `build/preview097-client.log` et
|
||||
`build/beta097-check.log`. Le reçu `build/beta097-artifact.json` vérifie les
|
||||
archives, les sources embarquées et la conservation des textures existantes.
|
||||
Aucun essai Windows ni shader tiers n’est revendiqué. Aucun monde personnel ni
|
||||
instance Prism n’a été ouvert ou mis à jour ; le canal packwiz reste inchangé.
|
||||
|
||||
Validation finale : `./gradlew check build assemblePack assembleTestPack
|
||||
-x :sanctuary:runGameTest` réussit en **2 min 13 s**, 124 tâches. La suite native
|
||||
Métabli/aperçu/fondations réussit en **59 s**. Archives normal et Test beta.097
|
||||
exportées et vérifiées ; les archives beta.096 restent inchangées.
|
||||
@@ -0,0 +1,85 @@
|
||||
# beta.093 — Recherche créative et infobulles des familiers
|
||||
|
||||
Branche `codex/async-tooltip-crash-beta093`, Minecraft 26.3.
|
||||
|
||||
## Diagnostic
|
||||
|
||||
Le rapport fourni concerne Windows, Sanctuary beta.089 et Minecraft 26.3-pre-2.
|
||||
Pendant la saisie d'un `w`, la recherche créative attend son index asynchrone.
|
||||
Cet index construit les infobulles sur un worker (`SessionSearchTrees`), puis
|
||||
`CompanionTooltips` appelle `Font.getSplitter().splitLines`. La mesure d'un glyphe
|
||||
non encore chargé peut déclencher une écriture de texture, en concurrence avec
|
||||
une passe de rendu : `Close the existing render pass before performing additional
|
||||
commands`. L'exception du worker remonte ensuite par la saisie de caractères.
|
||||
|
||||
La même mise en page était encore présente en beta.092 ; le correctif cible la
|
||||
version actuelle 26.3 finale. Il ne nécessite aucun changement de pilote graphique.
|
||||
|
||||
## Correction
|
||||
|
||||
Les deux chemins de descriptions, catalogue actuel et pouvoirs historiques,
|
||||
utilisent un même point de mise en page. Sur le fil client, il conserve la largeur
|
||||
adaptée à la fenêtre et le découpage habituel. Sur un worker, il renvoie des
|
||||
composants textuels complets avec leur couleur, sans accéder à la police ni à la
|
||||
fenêtre. La recherche garde les noms et descriptions ; aucune ligne n'est cachée
|
||||
pour contourner le problème. Aucun travail graphique n'est renvoyé au fil client
|
||||
avec attente, ce qui éviterait mal le crash et risquerait de bloquer l'indexation.
|
||||
|
||||
Aucune modification de sauvegarde, génération, règles de jeu, images ou registres.
|
||||
Aucun déploiement dans une instance personnelle et aucun monde existant ouvert.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `Tooltip093ClientChecks` utilise un monde plat de développement neuf,
|
||||
graine 42, et une sonde uniquement dans les GameTests. Cette sonde interdit tout
|
||||
accès de mesure à la police depuis un worker, même lorsque ses glyphes sont déjà
|
||||
en cache ; elle n'est pas embarquée dans le mod distribué.
|
||||
|
||||
Avant le correctif, le test échoue dans `CompanionTooltips` avec
|
||||
`TOOLTIP093_OFF_THREAD_FONT`, ce qui reproduit l'accès interdit identifié dans le
|
||||
rapport sans dépendre d'une course aléatoire du pilote. Journal :
|
||||
`build/tooltip093-before-client.log`.
|
||||
|
||||
Après correction, le même parcours passe en **1 min 13 s** sur le client natif
|
||||
Minecraft 26.3/macOS :
|
||||
|
||||
- 88 œufs, français/anglais, descriptions cryptiques/détaillées : construction
|
||||
sur worker sans aucune mesure de police.
|
||||
- Comparaison du texte indexé avec le texte affiché : contenu complet conservé,
|
||||
malgré les retours à la ligne réservés à l'affichage.
|
||||
- `SessionSearchTrees.updateCreativeTooltips`, recherche native et reconstruction
|
||||
de l'index après changement de langue.
|
||||
- Rechargement effectif des ressources, puis saisie native de `w` et `olf` dans
|
||||
la recherche créative. Capture anglaise inspectée : le champ contient `wolf`
|
||||
et affiche l'œuf de loup et son armure. Captures conservées dans
|
||||
`build/tooltip093-evidence/`.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryTooltip093ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journal : `build/tooltip093-client.log`, marqueur `TOOLTIP093_PASS`.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 36 s**, 124 tâches (`build/tooltip093-check.log`).
|
||||
Les exports, JAR et sources correspondent. Seule la classe de production
|
||||
`CompanionTooltips` diffère de beta.092 ; aucune sonde GameTest n'est distribuée.
|
||||
Images, données, JEI et anciennes archives restent identiques.
|
||||
Reçu local : `build/tooltip093-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.093.mrpack), SHA-256
|
||||
`35a1582c044955579a7120997864ff2080e276e81180ab9982397241ff4c3a1d`.
|
||||
- [Pack Test](../build/Sanctuary-Test-beta.093.mrpack), SHA-256
|
||||
`48c9abe733c2993a752413a34852223d35ad33e99d4f7e1d4f284969824dc275`.
|
||||
|
||||
## Limites
|
||||
|
||||
Le rapport initial est Windows/pre-2 ; les essais du correctif sont macOS/26.3
|
||||
finale, avec serveur intégré de développement. L'accès fautif est reproduit
|
||||
avec une sonde déterministe, sans revendiquer la reproduction exacte de la
|
||||
course du pilote AMD. Serveur dédié exclu et aucune EULA acceptée. Les archives
|
||||
locales ne mettent pas à jour une installation personnelle ni le canal packwiz.
|
||||
@@ -0,0 +1,129 @@
|
||||
# TOOL-02 — Clé dorée et clé de Steve — beta.123
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/wrench-safety-beta123`.
|
||||
Ce ticket remplace le contrat de la clé dorée de beta.120 : l'édition libre
|
||||
reste disponible dans un objet distinct, la **Clé à molette de Steve**.
|
||||
Le créateur précise que la clé de Steve reste utilisable en survie dès qu’un
|
||||
joueur la récupère, sans condition opérateur. Elle n’a pas de recette.
|
||||
|
||||
## Clé dorée : orientations natives
|
||||
|
||||
L'identifiant `sanctuary:golden_wrench`, sa recette et sa texture restent stables.
|
||||
Clic droit parcourt l'orientation native ; Maj parcourt en sens inverse.
|
||||
Clic gauche rappelle les gestes, sans casser le bloc ni sélectionner un état.
|
||||
Les familles de propriétés autorisées sont les orientations horizontales,
|
||||
les six directions, les trois axes, la rotation native des panneaux et
|
||||
l'orientation native des blocs qui la proposent. La règle porte sur les
|
||||
propriétés Minecraft, sans catalogue à maintenir pour chaque type de bloc.
|
||||
|
||||
L'eau, la moitié d'un escalier, les inventaires, les états de croissance,
|
||||
les quantités, la puissance et les autres propriétés ne sont jamais proposées.
|
||||
Aucune transformation indépendante des textures n'est créée par la clé dorée.
|
||||
Les textures suivent seulement le modèle orienté par Minecraft.
|
||||
Les connexions et formes sont recalculées à partir des voisins réels, avec les
|
||||
mises à jour natives actives. Une orientation privée de support est refusée
|
||||
sans casser le bloc. L'eau déjà contenue dans un bloc est conservée.
|
||||
|
||||
Les coffres doubles, portes, lits, étagères reliées, pistons sortis et leurs
|
||||
pièces mobiles ne peuvent pas être tournés individuellement. Ils restent dans
|
||||
leur état courant et une indication explique le refus. Il faut replacer les
|
||||
éléments concernés ou rétracter le piston ; ce ticket ne déplace pas un
|
||||
ensemble lié. Les coffres simples conservent leur entité et leur inventaire.
|
||||
|
||||
L'assemblage, l'ouverture et la dissociation des Métablis, Fûts, Fourneaux et
|
||||
super pistons restent prioritaires avec les deux clés. Les parties techniques
|
||||
d'un multibloc Sanctuary assemblé ne sont pas éditées individuellement.
|
||||
|
||||
## Clé de Steve
|
||||
|
||||
Nouvel identifiant `sanctuary:steve_wrench`, dans la section créative des outils,
|
||||
sans recette de survie. La possession de cet objet donne accès à ses pouvoirs,
|
||||
y compris en survie sans commandes et sans statut opérateur, dans les deux
|
||||
mains. Les restrictions natives de construction, aventure, spectateur, portée,
|
||||
protection et verrouillage restent appliquées côté serveur.
|
||||
|
||||
Les gestes de beta.120 sont conservés : clic gauche choisit la propriété,
|
||||
clic droit la modifie, Maj inverse le parcours. Tous les états natifs et la
|
||||
rotation indépendante des textures y restent volontairement accessibles.
|
||||
La clé de Steve est donc volontairement capable de produire des états non
|
||||
naturels, y compris pour le joueur qui la récupère en survie.
|
||||
|
||||
La texture finale est le PNG 16 × 16 bleu fourni par le créateur dans ce ticket,
|
||||
copié sans transformation dans `assets/sanctuary/textures/item/steve_wrench.png`.
|
||||
SHA-256 : `0084c41e1f46958903075e21866facd97d85e03fb8e06742e25b2ab254956fa3`.
|
||||
Le modèle reste un objet Minecraft 2D tenu en main, avec rareté épique et reflet.
|
||||
La proposition imagegen intermédiaire n'est pas livrée. Les noms, messages et
|
||||
indications des deux outils existent en français et en anglais.
|
||||
|
||||
## Sauvegardes et ressources
|
||||
|
||||
Aucune migration, modification de génération ni parcours des mondes existants.
|
||||
Les transformations de texture précédemment sauvegardées sont conservées ;
|
||||
la clé de Steve permet de les modifier. Les états non naturels déjà présents
|
||||
ne sont pas automatiquement réécrits. Les clés dorées existantes reçoivent
|
||||
immédiatement les nouvelles restrictions par leur identifiant inchangé.
|
||||
Le template personnel inclut le nouvel objet à sa prochaine création ; une
|
||||
copie personnelle existante n'est jamais écrasée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `Wrench123ClientChecks` passe en **1 min 12 s**, client macOS et
|
||||
serveur intégré, Minecraft 26.3, dans des mondes plats jetables de graines
|
||||
123 et 120. Il reprend le parcours avancé de beta.120 sur la clé de Steve :
|
||||
|
||||
- Vrais clics dorés répétés sur escalier, coffre et terre : pas de nouvelle eau,
|
||||
pas de sélection libre, pas de faux coffre double ni de rotation de texture.
|
||||
- Rotation inverse, eau existante conservée, formes recalculées selon les voisins,
|
||||
trois axes de bûches et six orientations des pistons isolés.
|
||||
- Coffre double avec ses deux inventaires intacts ; portes, lits, pistons sortis
|
||||
et pièces mobiles protégés ; torche murale sans support laissée intacte.
|
||||
- Vrais clics Steve en survie sans commandes : états et textures accessibles,
|
||||
dans les deux mains. Créatif accessible aussi sans commandes ; restrictions
|
||||
aventure/spectateur/portée conservées.
|
||||
- **2 522 propriétés natives** toujours réversibles pour Steve, UV réellement
|
||||
tournés sans déplacer la géométrie, rechargement des ressources, inventaires
|
||||
et sauvegarde/reconnexion conservés.
|
||||
- Assemblage et dissociation des quatre familles avec la clé dorée par vrais
|
||||
clics ; le PNG de Steve apparaît en main et dans la barre rapide.
|
||||
|
||||
Marqueurs `WRENCH123_SAFE_PASS`, `WRENCH123_PASS`, `WRENCH120_PASS` dans
|
||||
`build/wrench123-client.log`. Captures relues dans `build/wrench123-evidence/`.
|
||||
Pas de validation Windows, de client distant ou de shader tiers.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 19 s**, 126 tâches (103 exécutées, 23 à jour).
|
||||
Les sources des JAR correspondent aux sources du dépôt. La comparaison avec
|
||||
beta.122 limite les dix entrées de production modifiées aux classes des clés,
|
||||
aux libellés et aux ressources du nouvel objet. Les autres ressources,
|
||||
notamment les sculptures et la texture dorée, sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR Sanctuary, sans sauvegarde ni
|
||||
fixture de test. Le template ZIP correspond exactement aux ressources du JAR
|
||||
et à ses ressources embarquées, dont le PNG bleu du créateur.
|
||||
|
||||
- Pack normal : 10 270 954 octets, SHA-256
|
||||
`a7401dbef1bd1b888da2686368943eddc82d0d43e20449330c4849b4dff93e5f`.
|
||||
- Pack Test : 10 289 877 octets, SHA-256
|
||||
`c57cc6fc47f887b90850473e51407b610a6c5639c35fc2b78dc9ddb74f795b80`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`23e1a81c793a048c6375a0da07eb0bbe1a18f833a9ceb61a3c1dc5f1a3e65a30`.
|
||||
|
||||
Reçu local : `build/wrench123-artifact.json`.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Aucun monde personnel n'est ouvert par les essais.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.123](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.123)
|
||||
est publiée depuis `4552cc347b7fded610b1eda7a67ffaad56a5e201`.
|
||||
Le tag exact et les artefacts sont immuables ; les téléchargements publics
|
||||
ont été vérifiés. Canal packwiz : `f988d01b901f2d3eda0b4ab47a48540dc627d1b5`.
|
||||
Le template personnel publié inclut la clé de Steve ; SHA-256
|
||||
`f84d3c1c0e38232add90fb943dc39fd3e89138576df86024ceaa311478ed5cb9`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.123 est actif, conforme au hash vérifié ; le second
|
||||
passage ne change rien. Les **923 fichiers personnels et réglages suivis**
|
||||
restent inchangés, sans ouverture de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.123/` de l'instance existante.
|
||||
Reçus : `build/wrench123-isolated.json` et `build/wrench123-prism.json`.
|
||||
+3
-3
@@ -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.092
|
||||
pack_version=beta.092
|
||||
resource_pack_version=beta.090
|
||||
mod_version=beta.144
|
||||
pack_version=beta.144
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -25,6 +25,22 @@ if (clientRenderTests) {
|
||||
java.srcDir('src/clientGametest/java')
|
||||
}
|
||||
tasks.named('runClientGameTest') {
|
||||
def graphicsBackend = providers.gradleProperty('sanctuaryClientGraphicsBackend')
|
||||
if (graphicsBackend.isPresent()) {
|
||||
args('--graphicsBackend', graphicsBackend.get())
|
||||
systemProperty('sanctuary.test.graphicsBackend', graphicsBackend.get())
|
||||
}
|
||||
def improvedTransparency = providers.gradleProperty('sanctuaryClientImprovedTransparency')
|
||||
if (improvedTransparency.isPresent()) {
|
||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
||||
}
|
||||
systemProperty('sanctuary.test.water144', providers.gradleProperty('sanctuaryWater144ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144ExtrasOnly', providers.gradleProperty('sanctuaryWater144ExtrasOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.water144PerfOnly', providers.gradleProperty('sanctuaryWater144PerfOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.pbr142', providers.gradleProperty('sanctuaryPbr142ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.directional141Only', providers.gradleProperty('sanctuaryDirectional141Only').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.directional141', providers.gradleProperty('sanctuaryDirectional141ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.colored140', providers.gradleProperty('sanctuaryColored140ClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.noVsync', providers.gradleProperty('sanctuaryClientNoVsync').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.quick', providers.gradleProperty('sanctuaryQuickTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.introVisualOnly', providers.gradleProperty('sanctuaryIntroVisualOnly').getOrElse('false'))
|
||||
@@ -93,6 +109,69 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def colored139Client = providers.gradleProperty('sanctuaryColored139ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryColored139ClientTests', colored139Client)
|
||||
if (colored139Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.ColoredLights139ClientChecks') }
|
||||
|
||||
def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPbr134ClientTests', pbr134Client)
|
||||
if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') }
|
||||
def ssgi132Client = providers.gradleProperty('sanctuarySsgi132ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySsgi132ClientTests', ssgi132Client)
|
||||
if (ssgi132Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Ssgi132ClientChecks') }
|
||||
def closeup135Client = providers.gradleProperty('sanctuarySsgiCloseup135ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySsgiCloseup135ClientTests', closeup135Client)
|
||||
if (closeup135Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.SsgiCloseup135ClientChecks') }
|
||||
|
||||
def sunshaft137Client = providers.gradleProperty('sanctuarySunshaft137ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySunshaft137ClientTests', sunshaft137Client)
|
||||
if (sunshaft137Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Sunshafts137ClientChecks') }
|
||||
def bloom130Client = providers.gradleProperty('sanctuaryBloom130ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBloom130ClientTests', bloom130Client)
|
||||
if (bloom130Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Bloom130ClientChecks') }
|
||||
|
||||
def shadows122Client = providers.gradleProperty('sanctuaryPixelShadows122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPixelShadows122ClientTests', shadows122Client)
|
||||
if (shadows122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks') }
|
||||
|
||||
def gems126Client = providers.gradleProperty('sanctuaryGems126ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryGems126ClientTests', gems126Client)
|
||||
if (gems126Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Gems126ClientChecks') }
|
||||
|
||||
def sculpture125Client = providers.gradleProperty('sanctuarySculpture125ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture125ClientTests', sculpture125Client)
|
||||
if (sculpture125Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture125ClientChecks') }
|
||||
|
||||
def wrench123Client = providers.gradleProperty('sanctuaryWrench123ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWrench123ClientTests', wrench123Client)
|
||||
if (wrench123Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench123ClientChecks') }
|
||||
|
||||
def sculpture122Client = providers.gradleProperty('sanctuarySculpture122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture122ClientTests', sculpture122Client)
|
||||
if (sculpture122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture122ClientChecks') }
|
||||
|
||||
def template121Client = providers.gradleProperty('sanctuaryResourceTemplate121ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryResourceTemplate121ClientTests', template121Client)
|
||||
if (template121Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ResourceTemplate121ClientChecks') }
|
||||
|
||||
def wrench120Client = providers.gradleProperty('sanctuaryWrench120ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWrench120ClientTests', wrench120Client)
|
||||
if (wrench120Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench120ClientChecks') }
|
||||
|
||||
def sculpture116Client = providers.gradleProperty('sanctuarySculpture116ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture116ClientTests', sculpture116Client)
|
||||
if (sculpture116Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture116ClientChecks') }
|
||||
def furnace115Client = providers.gradleProperty('sanctuaryFurnace115ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryFurnace115ClientTests', furnace115Client)
|
||||
if (furnace115Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Furnace115ClientChecks') }
|
||||
def carryPanic114Client = providers.gradleProperty('sanctuaryCarryPanic114ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryCarryPanic114ClientTests', carryPanic114Client)
|
||||
if (carryPanic114Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.CarryPanic114ClientChecks') }
|
||||
|
||||
def statuary106Client = providers.gradleProperty('sanctuaryStatuary106ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryStatuary106ClientTests', statuary106Client)
|
||||
if (statuary106Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Statuary106ClientChecks') }
|
||||
|
||||
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
|
||||
def focusClasses = [plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
@@ -123,6 +202,39 @@ tasks.named('processGametestResources') {
|
||||
def welcome083Client = providers.gradleProperty('sanctuaryWelcome083ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWelcome083ClientTests', welcome083Client)
|
||||
if (welcome083Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Welcome083ClientChecks') }
|
||||
def melt109Client = providers.gradleProperty('sanctuaryMelt109ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMelt109ClientTests', melt109Client)
|
||||
if (melt109Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Melt109ClientChecks') }
|
||||
def snow108Client = providers.gradleProperty('sanctuarySnow108ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySnow108ClientTests', snow108Client)
|
||||
if (snow108Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Snow108ClientChecks') }
|
||||
def eggs107Client = providers.gradleProperty('sanctuaryEggs107ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryEggs107ClientTests', eggs107Client)
|
||||
if (eggs107Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Eggs107ClientChecks') }
|
||||
def living105Client = providers.gradleProperty('sanctuaryLiving105ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryLiving105ClientTests', living105Client)
|
||||
if (living105Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Living105ClientChecks') }
|
||||
def bricks102Client = providers.gradleProperty('sanctuaryBricks102ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBricks102ClientTests', bricks102Client)
|
||||
if (bricks102Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Bricks102ClientChecks') }
|
||||
def landing101Client = providers.gradleProperty('sanctuaryLanding101ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryLanding101ClientTests', landing101Client)
|
||||
if (landing101Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Landing101ClientChecks') }
|
||||
def schematics100Client = providers.gradleProperty('sanctuarySchematics100ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySchematics100ClientTests', schematics100Client)
|
||||
if (schematics100Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Schematics100ClientChecks') }
|
||||
def exploration100Client = providers.gradleProperty('sanctuaryExploration100ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryExploration100ClientTests', exploration100Client)
|
||||
if (exploration100Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Exploration100ClientChecks') }
|
||||
def catalogue099Client = providers.gradleProperty('sanctuaryCatalogue099ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryCatalogue099ClientTests', catalogue099Client)
|
||||
if (catalogue099Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Catalogue099ClientChecks') }
|
||||
def temple097Client = providers.gradleProperty('sanctuaryTemple097ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryTemple097ClientTests', temple097Client)
|
||||
if (temple097Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Temple097ClientChecks') }
|
||||
def metatable096Client = providers.gradleProperty('sanctuaryMetatable096ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMetatable096ClientTests', metatable096Client)
|
||||
if (metatable096Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Metatable096ClientChecks') }
|
||||
def piston087Client = providers.gradleProperty('sanctuaryPiston087ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPiston087ClientTests', piston087Client)
|
||||
if (piston087Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Piston087ClientChecks') }
|
||||
@@ -132,6 +244,9 @@ tasks.named('processGametestResources') {
|
||||
def fourneau085Client = providers.gradleProperty('sanctuaryFourneau085ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryFourneau085ClientTests', fourneau085Client)
|
||||
if (fourneau085Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Fourneau085ClientChecks') }
|
||||
def tooltip093Client = providers.gradleProperty('sanctuaryTooltip093ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryTooltip093ClientTests', tooltip093Client)
|
||||
if (tooltip093Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Tooltip093ClientChecks') }
|
||||
def pick092Client = providers.gradleProperty('sanctuaryPick092ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPick092ClientTests', pick092Client)
|
||||
if (pick092Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Pick092ClientChecks') }
|
||||
@@ -390,6 +505,15 @@ tasks.withType(JavaCompile).configureEach {
|
||||
}
|
||||
|
||||
processResources {
|
||||
from(rootProject.file('ressources-pack/template')) {
|
||||
exclude '**/.DS_Store'
|
||||
into 'resourcepacks/template'
|
||||
}
|
||||
from(rootProject.file('LICENSE')) {
|
||||
rename { 'LICENSE_sanctuary' }
|
||||
into 'resourcepacks/template'
|
||||
}
|
||||
from(rootProject.file('THIRD_PARTY_NOTICES.md')) { into 'resourcepacks/template' }
|
||||
from(rootProject.file('ressources-pack/sanctuary')) {
|
||||
include 'assets/**', 'pack.mcmeta', 'pack.png'
|
||||
exclude '**/.DS_Store'
|
||||
@@ -1077,3 +1201,11 @@ tasks.register('atlas059Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.atlas.AtlasMarker059Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('atlas059Smoke') }
|
||||
|
||||
tasks.register('seasonal105Smoke', JavaExec) {
|
||||
group = 'verification'
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.weather.Seasonal105Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('seasonal105Smoke') }
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.building.ColoredBricks;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.item.enchantment.Enchantments;
|
||||
|
||||
/** Native registry, recipes, loot, creative tabs and baked models in a fresh integrated world. */
|
||||
public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Bricks102 "+UUID.randomUUID());s.setSeed("102");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var level=p.level();var pos=new BlockPos(0,3,0);
|
||||
check(ColoredBricks.families().size()==16,"All colors registered");Walls118Checks.server(s);int index=0;
|
||||
for(var f:ColoredBricks.families()){
|
||||
String color=f.color().getName();
|
||||
for(var block:List.of(f.bricks(),f.slab(),f.stairs(),f.wall(),f.clay()))check(block.asItem()!=Items.AIR,"Block item exists "+block);
|
||||
check(f.bricks().defaultBlockState().is(BlockTags.MINEABLE_WITH_PICKAXE),"Pickaxe tag "+color);
|
||||
check(f.clay().defaultBlockState().is(BlockTags.MINEABLE_WITH_SHOVEL),"Shovel tag "+color);
|
||||
var shovel=new ItemStack(Items.DIAMOND_SHOVEL);var silk=shovel.copy();silk.enchant(level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH),1);
|
||||
drop(Block.getDrops(f.clay().defaultBlockState(),level,pos,null,p,shovel),f.clayBall(),4);
|
||||
drop(Block.getDrops(f.clay().defaultBlockState(),level,pos,null,p,silk),f.clay().asItem(),1);
|
||||
drop(Block.getDrops(f.slab().defaultBlockState().setValue(SlabBlock.TYPE,SlabType.DOUBLE),level,pos,null),f.slab().asItem(),2);
|
||||
drop(Block.getDrops(f.slab().defaultBlockState(),level,pos,null),f.slab().asItem(),1);
|
||||
drop(Block.getDrops(f.bricks().defaultBlockState(),level,pos,null),f.bricks().asItem(),1);
|
||||
drop(Block.getDrops(f.stairs().defaultBlockState(),level,pos,null),f.stairs().asItem(),1);
|
||||
check(f.slab().defaultBlockState().setValue(SlabBlock.WATERLOGGED,true).getFluidState().isSource(),"Slab water "+color);
|
||||
check(f.stairs().defaultBlockState().setValue(StairBlock.WATERLOGGED,true).getFluidState().isSource(),"Stair water "+color);
|
||||
for(String suffix:List.of("clay_ball_from_dye","clay_from_dye","brick_from_dye","bricks_from_dye","clay","bricks","brick_slab","brick_stairs","brick_slab_from_stonecutting","brick_stairs_from_stonecutting","brick_wall","brick_wall_from_stonecutting","clay_ball_smelting","clay_smelting"))check(s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+suffix))).isPresent(),"Recipe loaded "+color+"_"+suffix);
|
||||
craft(s,level,2,2,List.of(f.brick(),f.brick(),f.brick(),f.brick()),f.bricks().asItem(),1);
|
||||
craft(s,level,2,2,List.of(f.clayBall(),f.clayBall(),f.clayBall(),f.clayBall()),f.clay().asItem(),1);
|
||||
craft(s,level,3,1,List.of(f.bricks().asItem(),f.bricks().asItem(),f.bricks().asItem()),f.slab().asItem(),6);
|
||||
craft(s,level,3,3,List.of(f.bricks().asItem(),Items.AIR,Items.AIR,f.bricks().asItem(),f.bricks().asItem(),Items.AIR,f.bricks().asItem(),f.bricks().asItem(),f.bricks().asItem()),f.stairs().asItem(),4);
|
||||
var dye=BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(color+"_dye"));
|
||||
for(var pair:List.of(new Item[]{Items.CLAY_BALL,f.clayBall()},new Item[]{Items.CLAY,f.clay().asItem()},new Item[]{Items.BRICK,f.brick()},new Item[]{Items.BRICKS,f.bricks().asItem()}))craft(s,level,3,3,List.of(pair[0],pair[0],pair[0],pair[0],dye,pair[0],pair[0],pair[0],pair[0]),pair[1],8);
|
||||
var input=new SingleRecipeInput(new ItemStack(f.clayBall()));var cooked=s.getRecipeManager().getRecipeFor(RecipeType.SMELTING,input,level).orElseThrow().value().assemble(input);check(cooked.is(f.brick()),"Smelt brick "+color);
|
||||
input=new SingleRecipeInput(new ItemStack(f.clay()));cooked=s.getRecipeManager().getRecipeFor(RecipeType.SMELTING,input,level).orElseThrow().value().assemble(input);check(cooked.is(BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(color+"_terracotta"))),"Smelt terracotta "+color);
|
||||
var stoneInput=new SingleRecipeInput(new ItemStack(f.bricks()));
|
||||
for(var variant:List.of(new Object[]{"brick_slab",f.slab().asItem(),2},new Object[]{"brick_stairs",f.stairs().asItem(),1},new Object[]{"brick_wall",f.wall().asItem(),1})){
|
||||
var recipe=(StonecutterRecipe)s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+variant[0]+"_from_stonecutting"))).orElseThrow().value();check(recipe.matches(stoneInput,level),"Stonecutting accepts input");var cut=recipe.assemble(stoneInput);check(cut.is((Item)variant[1])&&cut.getCount()==(int)variant[2],"Stonecutting result");
|
||||
}
|
||||
int x=(index%8)*3,z=(index/8)*5;level.setBlockAndUpdate(new BlockPos(x,1,z),f.bricks().defaultBlockState());level.setBlockAndUpdate(new BlockPos(x,2,z),f.slab().defaultBlockState().setValue(SlabBlock.TYPE,SlabType.TOP));level.setBlockAndUpdate(new BlockPos(x+1,1,z),f.stairs().defaultBlockState().setValue(StairBlock.FACING,Direction.SOUTH));level.setBlockAndUpdate(new BlockPos(x,1,z+1),f.clay().defaultBlockState());index++;
|
||||
}
|
||||
fr.koka.sanctuary.recipes.RecipeService.discover(p,Items.CLAY_BALL);fr.koka.sanctuary.recipes.RecipeService.refresh(p);
|
||||
var known=fr.koka.sanctuary.recipes.RecipeService.knowledge(p);
|
||||
check(known.recipes.contains("sanctuary:clay_workshop"),"Existing workshop recipe remains discoverable");
|
||||
for(var f:ColoredBricks.families())check(known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_stairs_from_stonecutting")&&known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_wall")&&known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_wall_from_stonecutting"),"Collection unlock "+f.color());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
p.teleportTo(11,8,19);p.setYRot(180);p.setXRot(26);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
});
|
||||
c.runOnClient(m->{
|
||||
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());
|
||||
var blocks=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.COLORED_BLOCKS).value().getDisplayItems();
|
||||
var ingredients=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.INGREDIENTS).value().getDisplayItems();Walls118Checks.creative(new ArrayList<>(blocks));
|
||||
for(var f:ColoredBricks.families()){
|
||||
for(var b:List.of(f.bricks(),f.slab(),f.stairs(),f.wall(),f.clay())){
|
||||
check(blocks.stream().anyMatch(i->i.is(b.asItem())),"Creative color tab "+b);
|
||||
for(var state:b.getStateDefinition().getPossibleStates()){
|
||||
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);check(b instanceof WallBlock||!parts.isEmpty(),"Baked model "+state);
|
||||
for(var part:parts)for(var direction:new Direction[]{Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST,null})for(var quad:part.getQuads(direction))check(!quad.materialInfo().sprite().contents().name().toString().contains("missingno"),"No missing texture "+state);
|
||||
}
|
||||
}
|
||||
for(var item:List.of(f.clayBall(),f.brick()))check(ingredients.stream().anyMatch(i->i.is(item)),"Creative ingredients "+item);
|
||||
}
|
||||
});
|
||||
c.waitTicks(20);c.runOnClient(m->{m.player.setYRot(180);m.player.setXRot(26);});c.waitTicks(4);c.takeScreenshot("bricks102-gallery");saved=world.getWorldSave();System.out.println("BRICKS102_NATIVE_PASS colors=16 recipes=224 blocks=80 items=112");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var level=s.getPlayerList().getPlayers().getFirst().level();Walls118Checks.saved(level);int i=0;for(var f:ColoredBricks.families()){var pos=new BlockPos((i%8)*3,1,(i/8)*5);check(level.getBlockState(pos).is(f.bricks()),"Saved bricks "+f.color());check(level.getBlockState(pos.above()).getValue(SlabBlock.TYPE)==SlabType.TOP,"Saved slab state");i++;}});
|
||||
}
|
||||
System.out.println("BRICKS102_PASS");System.out.println("WALLS118_PASS native placement, mixed connections, tall sides, waterlogging, collision, crafting, stonecutting, loot, collection, creative order, baked models and reconnect");
|
||||
}
|
||||
private static void craft(net.minecraft.server.MinecraftServer s,net.minecraft.server.level.ServerLevel level,int w,int h,List<Item> items,Item result,int count){var input=CraftingInput.of(w,h,items.stream().map(ItemStack::new).toList());var stack=s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,level).orElseThrow().value().assemble(input);check(stack.is(result)&&stack.getCount()==count,"Craft "+result);}
|
||||
private static void drop(List<ItemStack> drops,Item item,int count){check(drops.size()==1&&drops.getFirst().is(item)&&drops.getFirst().getCount()==count,"Loot "+item+" expected "+count+" got "+drops);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real cursor/attack packets, synchronized player travel, interruption and collision regression. */
|
||||
public final class CarryPanic114ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("CarryPanic114 "+UUID.randomUUID());s.setSeed("114");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;p.onUpdateAbilities();reset(p);
|
||||
for(int x=-24;x<=24;x++)for(int z=-24;z<=24;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
int parrot=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
|
||||
c.waitFor(m->m.level.getEntity(parrot)!=null&&CarryService.carried(m.level.getEntity(parrot)),150);
|
||||
float health=server.computeOnServer(s->((LivingEntity)player(s).level().getEntity(parrot)).getHealth());
|
||||
attack(c,parrot);c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(CarryPanic.active(p),"Real native attack triggers server panic");check(((LivingEntity)p.level().getEntity(parrot)).getHealth()<health,"Ordinary carried animal takes native damage");});
|
||||
c.getInput().holdKey(o->o.keyUp);c.getInput().holdKey(o->o.keyRight);c.getInput().holdKey(o->o.keyJump);
|
||||
c.runOnClient(m->{m.player.setYRot(170);m.player.setXRot(15);});c.waitTicks(12);
|
||||
c.runOnClient(m->{check(m.player.getY()>2.5&&m.player.position().subtract(new Vec3(.5,1,.5)).horizontalDistance()>1,"Actual client moves upward and sideways");
|
||||
check(CarryPanic.active(m.player),"Panic remains active through steering keys");
|
||||
var wanted=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime());
|
||||
// Level time can advance immediately after the player physics tick. Match either exact tick.
|
||||
var previous=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime()-1);
|
||||
check(Math.min(m.player.getDeltaMovement().distanceTo(wanted),m.player.getDeltaMovement().distanceTo(previous))<.00001,"Movement follows animal, not camera or held keys: actual="+m.player.getDeltaMovement()+", expected="+wanted+", flight="+m.player.getAttached(CarryPanic.FLIGHT)+", time="+m.level.getGameTime());check(m.player.fallDistance<.2,"Flight clears falling distance");
|
||||
m.options.setCameraType(CameraType.THIRD_PERSON_BACK);});c.waitTicks(2);c.takeScreenshot("carry114-chaotic-flight");
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);
|
||||
c.waitTicks(65);c.waitFor(m->!CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(!p.hasAttached(CarryPanic.FLIGHT)&&CarryGlide.supported(p),"Timeout removes panic and retains ordinary glider");check(p.connection.isAcceptingMessages(),"Flight is accepted by server");});
|
||||
c.runOnClient(m->{check(m.player.getDeltaMovement().y<=0&&m.player.getDeltaMovement().y>=-CarryGlide.DESCENT-.02,"Normal gentle descent resumes");m.options.setCameraType(CameraType.FIRST_PERSON);});
|
||||
System.out.println("CARRY114_NATIVE_FLIGHT_PASS cursor, real attack, damage, movement, input override, timeout, glide");
|
||||
|
||||
server.runOnServer(s->{var p=player(s);p.getRandom().setSeed(114);var bird=p.level().getEntity(parrot);var quadrants=new HashSet<Integer>();
|
||||
for(int i=0;i<32;i++){
|
||||
CarryPanic.clear(p);CarryPanic.start(bird);var flight=p.getAttached(CarryPanic.FLIGHT);check(flight.duration()>=40&&flight.duration()<=60,"Duration bounded at 2–3 seconds");quadrants.add((int)(flight.yaw()/(Math.PI/2)));
|
||||
for(int t=0;t<=flight.duration();t++){var v=flight.velocity(flight.start()+t);check(v.horizontalDistance()<=.331&&v.y>=0&&v.y<=.241,"Velocity bounded without cumulative impulses");}
|
||||
}
|
||||
check(quadrants.size()==4,"Server random headings cover every quadrant");
|
||||
});c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(2);c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitFor(m->!CarryService.carrying(m.player)&&!CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{check(!player(s).hasAttached(CarryPanic.FLIGHT),"Native drop clears flight immediately");player(s).level().getEntity(parrot).discard();reset(player(s));});
|
||||
|
||||
int cow=server.computeOnServer(s->carry(player(s),EntityTypes.COW).getId());
|
||||
server.runOnServer(s->{var p=player(s);var animal=(LivingEntity)p.level().getEntity(cow);float before=animal.getHealth();p.attack(animal);
|
||||
check(CarryService.blocksAttack(p,animal)&&animal.getHealth()==before&&!animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&!CarryPanic.active(p),"Terrestrial carry pair stays protected");CarryPanic.start(animal);check(!CarryPanic.active(p),"Ground animal cannot launch player");CarryService.release(animal,true);animal.discard();reset(p);});
|
||||
|
||||
// Every supported appearance can trigger the shared flight, including the gliding chicken.
|
||||
server.runOnServer(s->{var p=player(s);for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.BAT,EntityTypes.BEE,EntityTypes.ALLAY,EntityTypes.PARROT,EntityTypes.BLAZE,EntityTypes.BREEZE)){
|
||||
var animal=carry(p,type);check(CarryGlide.flyer(animal)&&CarryPanic.canStrike(p,animal),"Carried flying species can be struck: "+type);
|
||||
check(animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&CarryPanic.active(p),"Accepted damage launches carrier: "+type);CarryService.release(animal,true);animal.discard();check(!p.hasAttached(CarryPanic.FLIGHT),"Release clears each flight");
|
||||
}});
|
||||
int external=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
|
||||
server.runOnServer(s->{var p=player(s);var bird=(LivingEntity)p.level().getEntity(external);var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);
|
||||
check(bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),1)&&CarryPanic.active(p),"External accepted hit also triggers flight");
|
||||
p.getAbilities().flying=true;CarryGlide.tick(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Creative flying invalidates propulsion");p.getAbilities().flying=false;
|
||||
CarryPanic.start(bird);bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),100);CarryGlide.tick(p);check(!CarryPanic.active(p),"Death stops propulsion");CarryService.detach(p);bird.discard();reset(p);});
|
||||
|
||||
int ceiling=server.computeOnServer(s->{var p=player(s);var bird=carry(p,EntityTypes.PARROT);for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().setBlockAndUpdate(new BlockPos(x,4,z),Blocks.STONE.defaultBlockState());return bird.getId();});
|
||||
c.waitFor(m->m.level.getEntity(ceiling)!=null&&CarryService.carried(m.level.getEntity(ceiling)),100);
|
||||
server.runOnServer(s->{var p=player(s);((LivingEntity)p.level().getEntity(ceiling)).hurtServer(p.level(),p.damageSources().playerAttack(p),1);});c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getY()<2&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Player cannot fly through low ceiling");check(!CarryService.carrying(p)&&!CarryPanic.active(p),"Blocked carried animal is safely released");
|
||||
p.level().getEntity(ceiling).discard();for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().removeBlock(new BlockPos(x,4,z),false);reset(p);
|
||||
var egg=new ItemStack(Items.PARROT_SPAWN_EGG);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(25);
|
||||
int familiar=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Familiar spawned");pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Flying familiar picked up");return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(familiar)!=null&&CarryService.carried(m.level.getEntity(familiar)),100);
|
||||
float petHealth=server.computeOnServer(s->FamiliarService.active(player(s)).getHealth());attack(c,familiar);c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarService.active(p).getHealth()==petHealth&&CarryPanic.active(p),"Friendly familiar tap launches without hurting it");CarryService.detach(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Lifecycle detach clears synchronized flight");});
|
||||
System.out.println("CARRY114_PASS random flight, native ordinary/familiar strikes, seven species, outside hits, ground protection, drop/death/creative cancellation, ceiling collisions, timeout and normal glide");
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);c.getInput().releaseKey(o->o.keyShift);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(o->o.keyUse);}
|
||||
}
|
||||
private static void attack(ClientGameTestContext c,int id){
|
||||
c.waitFor(m->m.level.getEntity(id).position().distanceTo(CarryService.head(m.player,m.level.getEntity(id)))<.01,100);
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,m.level.getEntity(id).getBoundingBox().getCenter());});c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof EntityHitResult hit&&hit.getEntity().getId()==id,"Native cursor can pick own carried flyer: hit="+m.hitResult+", target="+m.level.getEntity(id).getBoundingBox()+", eye="+m.player.getEyePosition()+", view="+m.player.getLookAngle()+", eligible="+CarryPanic.canStrike(m.player,m.level.getEntity(id))));
|
||||
c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);
|
||||
}
|
||||
private static Mob carry(ServerPlayer p,EntityType<? extends Mob> type){var mob=type.create(p.level(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(p.position().add(0,0,2));p.level().addFreshEntity(mob);check(CarryService.interact(p,mob),"Animal picked up: "+type);return mob;}
|
||||
private static void reset(ServerPlayer p){CarryService.detach(p);p.teleportTo(.5,1,.5);p.setDeltaMovement(Vec3.ZERO);p.resetFallDistance();p.setShiftKeyDown(false);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
|
||||
/** End-to-end builds, personal history, shared catalogue, inline advancements and tap/hold K. */
|
||||
public final class Catalogue099ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c){
|
||||
new Plans072ClientChecks().runTest(c);
|
||||
c.runOnClient(m->{if(PlanHistory.entries().size()!=3)throw new AssertionError("Three genuinely completed plans retained");});
|
||||
new Metatable096ClientChecks().runTest(c);
|
||||
try{
|
||||
var root=net.minecraft.nbt.NbtIo.readCompressed(PlanClient.directory().resolve("recent-completed.nbt"),net.minecraft.nbt.NbtAccounter.create(fr.koka.sanctuary.plans.SchematicIO.MAX_NBT));
|
||||
var entries=root.getList("Completed").orElseThrow();if(entries.size()!=3)throw new AssertionError("Three completed plans persisted");
|
||||
for(var entry:entries)fr.koka.sanctuary.plans.SchematicIO.decode((net.minecraft.nbt.CompoundTag)entry,"History");
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
System.out.println("CATALOGUE099_PASS: complete creative builds, three-entry persisted history, uniform catalogue, native textured thumbnails, all-block palette, tap/hold K, inline tree and tracking, GUI scales 2/3, FR/EN");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
|
||||
/** Native preview, server result and survival payment for the special plain-clay material. */
|
||||
final class Clay126Checks {
|
||||
static void exercise(ClientGameTestContext c,TestServerContext server){
|
||||
var pos=new BlockPos(4,1,4);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(3,1,3);p.setGameMode(GameType.SURVIVAL);p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.level().setBlockAndUpdate(pos,Statuary.WORKSHOP.defaultBlockState());CompanionService.save(p,CompanionService.record(p).note("seen_mobs","cow"));Statuary.open(p,pos);p.containerMenu.getSlot(0).set(new ItemStack(Items.CLAY,2));p.containerMenu.broadcastChanges();});
|
||||
c.waitForScreen(ClayWorkshopScreen.class);c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();((ClayWorkshopScreen)m.gui.screen()).load(EntityTypes.COW);});
|
||||
c.waitFor(m->{var screen=(ClayWorkshopScreen)m.gui.screen();return !screen.busy()&&screen.selected()!=null&&screen.selected().equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem()));},250);
|
||||
var cow=c.computeOnClient(m->((ClayWorkshopScreen)m.gui.screen()).selected());check(cow.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>3,"Cow source retains texture colors");
|
||||
for(var material:List.of(Items.OAK_PLANKS,fr.koka.sanctuary.building.ColoredBricks.families().getFirst().clay().asItem(),Items.CLAY)){
|
||||
server.runOnServer(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;menu.getSlot(0).set(new ItemStack(material,2));menu.broadcastChanges();});
|
||||
var expected=c.computeOnClient(m->ClayWorkshopMenu.appearance(cow,material,SculptureMaterialSampler.sample(new ItemStack(material))));
|
||||
check(material==Items.CLAY?expected.equals(cow):!expected.equals(cow),"Only plain clay retains original colors");
|
||||
c.waitFor(m->expected.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
|
||||
c.runOnClient(m->check(ItemStack.matches(((ClayWorkshopScreen)m.gui.screen()).preview(),m.player.containerMenu.getSlot(1).getItem()),"Preview matches authoritative material switch"));
|
||||
}
|
||||
c.takeScreenshot("clay126-original-cow");
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
server.waitFor(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;return cow.equals(StatuaryEntity.sculpture(menu.getCarried()))&&menu.getSlot(0).getItem().getCount()==1;},100);
|
||||
c.waitFor(m->cow.equals(StatuaryEntity.sculpture(m.player.containerMenu.getCarried()))&&m.player.containerMenu.getSlot(0).getItem().getCount()==1,100);
|
||||
Path file=PlanClient.directory().resolve("clay126-"+UUID.randomUUID()+".glb");
|
||||
try{Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture());var id=GlbModel.load(file).id();c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));
|
||||
c.waitFor(m->{var screen=(ClayWorkshopScreen)m.gui.screen();return !screen.busy()&&screen.selected()!=null&&screen.selected().modelId().equals(id)&&screen.selected().equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem()));},250);
|
||||
c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();check(screen.selected().cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Imported textured GLB retains its original colors");check(ItemStack.matches(screen.preview(),m.player.containerMenu.getSlot(1).getItem()),"Imported preview equals native result");});
|
||||
}catch(Exception e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(Exception ignored){}}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.closeContainer();p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();});c.waitFor(m->m.gui.screen()==null,100);
|
||||
System.out.println("CLAY126_PASS original animal and GLB colors, oak and colored clay palette transfer, switch back, matching previews and one clay paid in survival");
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.knowledge.*;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
final class Discovery126Checks {
|
||||
static void exercise(ClientGameTestContext c,TestServerContext server){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var dispatcher=s.getCommands().getDispatcher();
|
||||
var source=p.createCommandSourceStack();var root=dispatcher.getRoot().getChild("sanctuary").getChild("discovery");
|
||||
s.getWorldData().setAllowCommands(false);check(!root.canUse(source),"Discovery commands respect native Allow Commands");
|
||||
try{dispatcher.execute("sanctuary discovery all",source);throw new AssertionError("Denied command executed");}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){}
|
||||
s.getWorldData().setAllowCommands(true);
|
||||
var before=CompanionService.record(p);var items=new TreeSet<>(RecipeService.knowledge(p).items);int kills=p.getStats().getValue(Stats.ENTITY_KILLED,EntityTypes.WITHER);
|
||||
command(p,"mob minecraft:wither");check(StatueDiscoveries.known(p).contains("minecraft:wither"),"Precise species unlocks sculpture catalogue");check(CompanionService.record(p).notes().get("seen_mobs").contains("wither"),"Same actual seen_mobs record");check(RecipeService.knowledge(p).items.equals(items),"Mob discovery invents no unrelated item discovery");check(p.getStats().getValue(Stats.ENTITY_KILLED,EntityTypes.WITHER)==kills,"No fake kill stat");
|
||||
command(p,"block minecraft:pink_glazed_terracotta");var block=BlockKnowledgeService.knowledge(p,Blocks.GLAZED_TERRACOTTA.pick(DyeColor.PINK));check(block.seen()&&!block.possessed()&&block.mined()==0&&block.placed()==0,"Precise block is seen without fabricated possession or stats");check(RecipeService.knowledge(p).items.contains("minecraft:pink_glazed_terracotta")&&RecipeService.knowledge(p).collections.contains("sanctuary:terracotta"),"Block event unlocks its normal recipe collection");
|
||||
command(p,"item minecraft:amethyst_shard");check(RecipeService.knowledge(p).items.contains("minecraft:amethyst_shard")&&RecipeService.knowledge(p).recipes.contains("minecraft:spyglass"),"Precise item unlocks linked recipes");
|
||||
String old=CompanionService.record(p).json();check(run(p,"mob minecraft:item")==0,"Nonmob type refused");check(CompanionService.record(p).json().equals(old),"Rejected request does not change discoveries");
|
||||
try{dispatcher.execute("sanctuary discovery item sanctuary:missing_item",source);throw new AssertionError("Missing ID accepted");}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){}
|
||||
command(p,"mobs all");allSpecies(p);var pos=new BlockPos(4,1,4);Statuary.open(p,pos);check(((ClayWorkshopMenu)p.containerMenu).view.statues().equals(StatueDiscoveries.known(p)),"Workshop sees the shared full catalogue");p.closeContainer();
|
||||
command(p,"all");all(p);String species=CompanionService.record(p).json(),recipes=RecipeService.knowledge(p).json();var blocks=BlockKnowledgeService.snapshot(p);command(p,"all @s");check(species.equals(CompanionService.record(p).json())&&recipes.equals(RecipeService.knowledge(p).json())&&blocks.equals(BlockKnowledgeService.snapshot(p)),"Repeated all with target selector is idempotent");
|
||||
check(CompanionService.record(p).paid()==before.paid()&&CompanionService.record(p).unlocked()==before.unlocked(),"Discoveries do not fabricate paid skills");
|
||||
try{check(dispatcher.execute("sanctuary discovery mob minecraft:cow "+p.getGameProfile().name(),s.createCommandSourceStack())==1,"Console can address an online player");}catch(Exception e){throw new AssertionError(e);}
|
||||
System.out.println("DISCOVERY126_PASS native permissions, precise block/item/species, linked recipes, shared catalogue, all three categories, all, selector, console, idempotence and unchanged combat/ownership counters");
|
||||
});
|
||||
c.waitFor(m->m.gui.screen()==null,100);
|
||||
}
|
||||
static void all(ServerPlayer p){
|
||||
allSpecies(p);check(BlockKnowledgeService.snapshot(p).stream().allMatch(BlockKnowledge::known),"All block discoveries");
|
||||
var knowledge=RecipeService.knowledge(p);for(var item:BuiltInRegistries.ITEM)if(item!=Items.AIR)check(knowledge.items.contains(BuiltInRegistries.ITEM.getKey(item).toString()),"All registered items discovered");
|
||||
check(knowledge.recipes.contains("minecraft:spyglass")&&knowledge.recipes.contains("sanctuary:ruby_block"),"Vanilla and Sanctuary recipes are derived from discoveries");
|
||||
}
|
||||
static void allSpecies(ServerPlayer p){check(StatueDiscoveries.known(p).size()==BuiltInRegistries.ENTITY_TYPE.stream().filter(t->SpawnEggItem.byId(t).isPresent()).count(),"All registered species discovered");}
|
||||
static void command(ServerPlayer p,String args){check(run(p,args)==1,"Discovery command succeeds: "+args);}
|
||||
private static int run(ServerPlayer p,String args){try{return p.level().getServer().getCommands().getDispatcher().execute("sanctuary discovery "+args,p.createCommandSourceStack());}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.companion.CompanionType;
|
||||
import fr.koka.sanctuary.eggs.BirthEggs;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.dispenser.BlockSource;
|
||||
import net.minecraft.core.registries.*;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.animal.Animal;
|
||||
import net.minecraft.world.entity.animal.sheep.Sheep;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.item.enchantment.Enchantments;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Eggs107ClientChecks implements FabricClientGameTest {
|
||||
private static final AABB AREA = new AABB(-20,70,-20,20,100,20);
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var intro = new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}});
|
||||
try {
|
||||
for(boolean enabled:new boolean[]{true,false}) try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Eggs107 "+enabled+" "+UUID.randomUUID());s.setSeed("107");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
s.getGameRules().set(SanctuaryGameplay.ENABLED,enabled,null);
|
||||
}).create()) {
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&(!enabled||ProgressionClient.active()&&!IntroSpawnFlash.active()),1200);
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var l=p.level();
|
||||
check(BirthEggs.active(l)==enabled,"Gameplay scope "+enabled);
|
||||
for(int x=-15;x<=15;x++)for(int z=-15;z<=15;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.GRASS_BLOCK.defaultBlockState());
|
||||
p.teleportTo(0,80,-4);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);
|
||||
if(enabled) { ordinaryBirths(l,p); specialBirths(l,p); spawners(l,p,true); }
|
||||
else { vanillaBirth(l);spawners(l,p,false); }
|
||||
});
|
||||
}
|
||||
System.out.println("EGGS107_NATIVE_PASS: births, variants, crosses, ownership, hatch/use/dispenser, special births, spawners, vanilla scope");
|
||||
} finally { intro.set(false); }
|
||||
}
|
||||
private static void ordinaryBirths(ServerLevel l,ServerPlayer p) {
|
||||
// Exercise every common Animal breeding path and native child constructor.
|
||||
String[] breeders={"armadillo","axolotl","bee","camel","cat","chicken","cow","donkey","fox","goat","hoglin","horse","llama","mooshroom","nautilus","ocelot","panda","pig","rabbit","sheep","strider","wolf"};
|
||||
for(String id:breeders){
|
||||
clear(l);var type=BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.withDefaultNamespace(id));
|
||||
var a=(Animal)spawn(l,type);var b=(Animal)spawn(l,type);a.setInLove(p);b.setInLove(p);
|
||||
a.spawnChildFromBreeding(l,b);
|
||||
ItemStack egg=oneEgg(l);check(SpawnEggItem.getType(egg)==type,"Native species "+id);
|
||||
check(l.getEntitiesOfClass(Mob.class,AREA).size()==2,"No simultaneous live child "+id);
|
||||
check(a.getAge()==6000&&b.getAge()==6000&&!a.isInLove()&&!b.isInLove(),"Cooldown/love "+id);
|
||||
var payload=egg.get(DataComponents.ENTITY_DATA).copyTagWithoutId();check(payload.getIntOr("Age",0)<0,"Baby payload "+id);
|
||||
check(!payload.contains("UUID")&&!payload.contains("Pos")&&!payload.contains("Brain"),"Portable data "+id);
|
||||
}
|
||||
clear(l);var a=(Animal)spawn(l,EntityTypes.HORSE);var b=(Animal)spawn(l,EntityTypes.DONKEY);a.spawnChildFromBreeding(l,b);
|
||||
check(SpawnEggItem.getType(oneEgg(l))==EntityTypes.MULE,"Horse/donkey gives mule");
|
||||
clear(l);var red=(Sheep)spawn(l,EntityTypes.SHEEP);var blue=(Sheep)spawn(l,EntityTypes.SHEEP);red.setColor(DyeColor.RED);blue.setColor(DyeColor.BLUE);red.spawnChildFromBreeding(l,blue);
|
||||
var egg=roundTrip(l,oneEgg(l));check(egg.get(DataComponents.LORE)!=null,"Birth tooltip persisted");
|
||||
var sheep=(Sheep)hatch(l,p,egg.copy(),EntityTypes.SHEEP);check(sheep.isBaby()&&sheep.getColor()==DyeColor.PURPLE,"Native colour inheritance and guaranteed baby");
|
||||
var used=egg.copy();var onAdult=SpawnEggItem.spawnOffspringFromSpawnEgg(p,red,EntityTypes.SHEEP,l,new Vec3(7,80,7),used).orElseThrow();
|
||||
check(((Sheep)onAdult).getColor()==DyeColor.PURPLE&&onAdult.isBaby()&&used.isEmpty(),"Use on adult preserves actual birth");
|
||||
var dispenserPos=new BlockPos(10,80,10);var state=Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING,Direction.WEST);l.setBlockAndUpdate(dispenserPos,state);
|
||||
var dispenser=(DispenserBlockEntity)l.getBlockEntity(dispenserPos);
|
||||
dispenser.setItem(0,egg.copy());
|
||||
try{var method=DispenserBlock.class.getDeclaredMethod("dispenseFrom",ServerLevel.class,net.minecraft.world.level.block.state.BlockState.class,BlockPos.class);method.setAccessible(true);method.invoke(Blocks.DISPENSER,l,state,dispenserPos);}catch(Exception e){throw new RuntimeException(e);}
|
||||
check(dispenser.getItem(0).isEmpty(),"Dispenser spends egg");
|
||||
check(l.getEntitiesOfClass(Sheep.class,new AABB(8,79,9,10,82,12)).stream().anyMatch(s->s.isBaby()&&s.getColor()==DyeColor.PURPLE),"Dispenser baby/variant");
|
||||
clear(l);var wolf=(net.minecraft.world.entity.animal.wolf.Wolf)spawn(l,EntityTypes.WOLF);wolf.tame(p);var other=(Animal)spawn(l,EntityTypes.WOLF);wolf.spawnChildFromBreeding(l,other);
|
||||
var puppy=(net.minecraft.world.entity.animal.wolf.Wolf)hatch(l,p,oneEgg(l).copy(),EntityTypes.WOLF);check(puppy.isBaby()&&puppy.isTame()&&puppy.isOwnedBy(p),"Tamed wolf inherits owner");
|
||||
for(var species:CompanionType.values())check(SpawnEggItem.byId(BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.withDefaultNamespace(species.id()))).isPresent(),"Catalogue native egg "+species.id());
|
||||
}
|
||||
private static void specialBirths(ServerLevel l,ServerPlayer p) {
|
||||
clear(l);var frog=(Animal)spawn(l,EntityTypes.FROG);var mate=(Animal)spawn(l,EntityTypes.FROG);frog.spawnChildFromBreeding(l,mate);
|
||||
check(SpawnEggItem.getType(oneEgg(l))==EntityTypes.TADPOLE,"Frog tadpole egg");
|
||||
check(!frog.getBrain().hasMemoryValue(net.minecraft.world.entity.ai.memory.MemoryModuleType.IS_PREGNANT),"No second frogspawn");
|
||||
clear(l);var sniffer=(Animal)spawn(l,EntityTypes.SNIFFER);sniffer.spawnChildFromBreeding(l,(Animal)spawn(l,EntityTypes.SNIFFER));
|
||||
check(SpawnEggItem.getType(oneEgg(l))==EntityTypes.SNIFFER,"Sniffer replaces native block egg");check(hatch(l,p,oneEgg(l).copy(),EntityTypes.SNIFFER).isBaby(),"Snifflet");
|
||||
clear(l);var turtle=(net.minecraft.world.entity.animal.turtle.Turtle)spawn(l,EntityTypes.TURTLE);var partner=(Animal)spawn(l,EntityTypes.TURTLE);
|
||||
try {var cls=Class.forName("net.minecraft.world.entity.animal.turtle.Turtle$TurtleBreedGoal");var ctor=cls.getDeclaredConstructor(net.minecraft.world.entity.animal.turtle.Turtle.class,double.class);ctor.setAccessible(true);var goal=ctor.newInstance(turtle,1.0);var field=net.minecraft.world.entity.ai.goal.BreedGoal.class.getDeclaredField("partner");field.setAccessible(true);field.set(goal,partner);var breed=cls.getDeclaredMethod("breed");breed.setAccessible(true);breed.invoke(goal);}catch(Exception e){throw new RuntimeException(e);}
|
||||
check(!turtle.hasEgg()&&turtle.getAge()==6000&&partner.getAge()==6000,"Turtle no double laying");check(SpawnEggItem.getType(oneEgg(l))==EntityTypes.TURTLE,"Turtle egg");
|
||||
clear(l);var allay=spawn(l,EntityTypes.ALLAY);invoke(allay,"duplicateAllay");var allayEgg=oneEgg(l);check(SpawnEggItem.getType(allayEgg)==EntityTypes.ALLAY,"Allay egg");check(allayEgg.get(DataComponents.ENTITY_DATA).copyTagWithoutId().getLongOr("DuplicationCooldown",0)==6000,"Allay cooldown stored");check(l.getEntitiesOfClass(Mob.class,AREA).size()==1,"No duplicate allay entity");
|
||||
clear(l);var villager=(net.minecraft.world.entity.npc.villager.Villager)spawn(l,EntityTypes.VILLAGER);var other=(net.minecraft.world.entity.npc.villager.Villager)spawn(l,EntityTypes.VILLAGER);
|
||||
try{var cls=net.minecraft.world.entity.ai.behavior.VillagerMakeLove.class;var method=cls.getDeclaredMethod("breed",ServerLevel.class,villager.getClass(),villager.getClass());method.setAccessible(true);var result=(Optional<?>)method.invoke(new net.minecraft.world.entity.ai.behavior.VillagerMakeLove(),l,villager,other);check(result.isEmpty(),"Caller releases reserved bed");}catch(Exception e){throw new RuntimeException(e);}
|
||||
check(SpawnEggItem.getType(oneEgg(l))==EntityTypes.VILLAGER&&villager.getAge()==6000&&other.getAge()==6000,"Villager birth");
|
||||
check(hatch(l,p,oneEgg(l).copy(),EntityTypes.VILLAGER).isBaby(),"Baby villager");
|
||||
}
|
||||
private static void spawners(ServerLevel l,ServerPlayer p,boolean enabled) {
|
||||
for(String mode:List.of("normal","silk","fortune","hand","creative","blockDropsOff")) {
|
||||
clear(l);p.setGameMode(mode.equals("creative")?GameType.CREATIVE:GameType.SURVIVAL);var tool=mode.equals("hand")?ItemStack.EMPTY:new ItemStack(Items.DIAMOND_PICKAXE);
|
||||
if(mode.equals("silk")||mode.equals("fortune"))tool.enchant(l.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(mode.equals("silk")?Enchantments.SILK_TOUCH:Enchantments.FORTUNE),mode.equals("silk")?1:3);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,tool);var pos=new BlockPos(1,80,1);l.setBlockAndUpdate(pos,Blocks.SPAWNER.defaultBlockState());
|
||||
((SpawnerBlockEntity)l.getBlockEntity(pos)).getSpawner().setEntityId(EntityTypes.BLAZE,l,l.getRandom(),pos);
|
||||
var rule=net.minecraft.world.level.gamerules.GameRules.BLOCK_DROPS;l.getGameRules().set(rule,!mode.equals("blockDropsOff"),l.getServer());
|
||||
check(p.gameMode.destroyBlock(pos),"Actual spawner break "+mode);
|
||||
int count=l.getEntitiesOfClass(ItemEntity.class,AREA).stream().filter(e->e.getItem().is(Items.BLAZE_SPAWN_EGG)).mapToInt(e->e.getItem().getCount()).sum();
|
||||
check(count==(enabled&&(mode.equals("normal")||mode.equals("fortune"))?1:0),"Spawner egg count "+mode+" enabled="+enabled+" count="+count);
|
||||
l.getGameRules().set(rule,true,l.getServer());
|
||||
}
|
||||
p.setGameMode(GameType.CREATIVE);
|
||||
}
|
||||
private static void vanillaBirth(ServerLevel l) {clear(l);var a=(Animal)spawn(l,EntityTypes.COW);a.spawnChildFromBreeding(l,(Animal)spawn(l,EntityTypes.COW));check(l.getEntitiesOfClass(Animal.class,AREA).size()==3&&l.getEntitiesOfClass(ItemEntity.class,AREA).isEmpty(),"Vanilla birth unchanged");}
|
||||
private static ItemStack roundTrip(ServerLevel l,ItemStack egg){var ops=l.registryAccess().createSerializationContext(NbtOps.INSTANCE);return ItemStack.CODEC.parse(ops,ItemStack.CODEC.encodeStart(ops,egg).getOrThrow()).getOrThrow();}
|
||||
private static Mob hatch(ServerLevel l,ServerPlayer p,ItemStack egg,EntityType<?> type){var before=new HashSet<UUID>();for(var e:l.getEntitiesOfClass(Mob.class,AREA))before.add(e.getUUID());p.setItemInHand(InteractionHand.MAIN_HAND,egg);var at=new BlockPos(6,79,6);var result=egg.useOn(new UseOnContext(p,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(6.5,80,6.5),Direction.UP,at,false)));check(result.consumesAction()&&egg.isEmpty(),"Survival hatch consumes once");return l.getEntitiesOfClass(Mob.class,AREA).stream().filter(m->m.getType()==type&&!before.contains(m.getUUID())).findFirst().orElseThrow();}
|
||||
private static Entity spawn(ServerLevel l,EntityType<?> type){var e=type.create(l,EntitySpawnReason.BREEDING);e.snapTo(0,80,0,0,0);if(e instanceof Mob mob)mob.finalizeSpawn(l,l.getCurrentDifficultyAt(e.blockPosition()),EntitySpawnReason.BREEDING,null);if(e instanceof AgeableMob ageable)ageable.setAge(0);l.addFreshEntity(e);return e;}
|
||||
private static ItemStack oneEgg(ServerLevel l){var eggs=l.getEntitiesOfClass(ItemEntity.class,AREA);check(eggs.size()==1&&eggs.getFirst().getItem().getCount()==1,"Exactly one dropped egg "+eggs);return eggs.getFirst().getItem();}
|
||||
private static void clear(ServerLevel l){for(var e:l.getEntities(null,AREA))if(!(e instanceof net.minecraft.world.entity.player.Player))e.discard();}
|
||||
private static void invoke(Object o,String name){try{var m=o.getClass().getDeclaredMethod(name);m.setAccessible(true);m.invoke(o);}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.companion.CompanionWorld;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Server discovery snapshots, restricted catalogue, and actual idle movement plus combat regression. */
|
||||
public final class Exploration100ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Exploration100 "+UUID.randomUUID());s.setSeed("100");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
var table=new BlockPos(3,1,3);
|
||||
c.runOnClient(m->imports());
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");
|
||||
for(int x=-20;x<=20;x++)for(int z=-20;z<=20;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
CompanionService.save(p,CompanionService.record(p).note("seen_mobs",""));
|
||||
for(var q:fr.koka.sanctuary.multiblock.Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());
|
||||
check(fr.koka.sanctuary.multiblock.Metatable.assemble(p,table),"Assembled workshop");
|
||||
// Owning an egg is not an encounter.
|
||||
p.getInventory().setItem(0,new ItemStack(Items.PIG_SPAWN_EGG));
|
||||
check(!fr.koka.sanctuary.plans.StatueDiscoveries.known(p).contains("minecraft:pig"),"Egg does not unlock statue");
|
||||
var cow=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);cow.setNoAi(true);cow.setPos(.5,1,4.5);p.level().addFreshEntity(cow);
|
||||
p.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,cow.getBoundingBox().getCenter());
|
||||
fr.koka.sanctuary.companion.CompanionQueries.observeSpecies(p);
|
||||
check(fr.koka.sanctuary.plans.StatueDiscoveries.known(p).contains("minecraft:cow"),"Native aimed encounter unlocks cow");cow.discard();
|
||||
p.awardStat(net.minecraft.stats.Stats.ENTITY_KILLED.get(EntityTypes.ZOMBIE));
|
||||
p.awardStat(net.minecraft.stats.Stats.ENTITY_KILLED_BY.get(EntityTypes.SKELETON));
|
||||
check(fr.koka.sanctuary.plans.StatueDiscoveries.known(p).containsAll(List.of("minecraft:cow","minecraft:zombie","minecraft:skeleton")),"All three discovery sources");
|
||||
});
|
||||
c.waitFor(m->fr.koka.sanctuary.multiblock.Metatable.state(m.level,table,true)==1,100);
|
||||
server.runOnServer(s->fr.koka.sanctuary.multiblock.Metatable.open(player(s),table));c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->{check(MetatableClient.knows(EntityTypes.COW)&&MetatableClient.knows(EntityTypes.ZOMBIE)&&MetatableClient.knows(EntityTypes.SKELETON),"Server snapshot reaches client");check(!MetatableClient.knows(EntityTypes.PIG),"Unknown species hidden even creative");
|
||||
var before=PlanClient.plan;PlanClient.generate(EntityTypes.PIG.create(m.level,EntitySpawnReason.COMMAND),8,"mixed");check(!PlanClient.busy&&PlanClient.plan==before,"Unknown identifier cannot start generator");
|
||||
m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.plans.tab.statue")).findFirst().orElseThrow().onPress(enter());
|
||||
});c.takeScreenshot("exploration100-discovered-statues");
|
||||
c.runOnClient(m->{m.gui.screen().onClose();check(!MetatableClient.knows(EntityTypes.COW),"Session discovery snapshot cleared");});
|
||||
for(var item:List.of(Items.WOLF_SPAWN_EGG,Items.CHICKEN_SPAWN_EGG,Items.SLIME_SPAWN_EGG,Items.BAT_SPAWN_EGG)){
|
||||
for(String mood:List.of("calm","curious","playful")){
|
||||
var start=new AtomicReference<Vec3>();var distinct=new HashSet<BlockPos>();var moved=new AtomicBoolean();var paused=new AtomicInteger();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);equip(p,item,mood.equals("calm")?FamiliarPersonality.PROTECTIVE:FamiliarPersonality.PACIFIST,mood,1000);var pet=FamiliarService.active(p);pet.setPos(.5,pet.motion().flies()?2.6:1,1.5);pet.stopRoaming();start.set(pet.position());});
|
||||
for(int tick=0;tick<28;tick++){
|
||||
c.waitTicks(10);server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Pet remains active");
|
||||
distinct.add(pet.blockPosition());if(pet.position().distanceToSqr(start.get())>1)moved.set(true);
|
||||
if(pet.getNavigation().isDone())paused.incrementAndGet();
|
||||
check(pet.distanceToSqr(p)<100,"Idle stays near owner");check(FamiliarBattle.target(p)==null,"No unsolicited target");
|
||||
});
|
||||
}
|
||||
check(moved.get()&&distinct.size()>=3,"Real wandering: "+item+" "+mood+" cells="+distinct);check(paused.get()>0,"Idle has pauses: "+item+" "+mood);
|
||||
System.out.println("ROAM100_PASS "+item+" "+mood+" cells="+distinct.size()+" pauses="+paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
new Autonomy074ClientChecks().runTest(c);
|
||||
System.out.println("EXPLORATION100_PASS");
|
||||
}
|
||||
private static void imports(){
|
||||
for(int version:List.of(2,3)){
|
||||
var root=new CompoundTag();root.putInt("Version",version);root.putInt("DataVersion",2730);root.putShort("Width",(short)1);root.putShort("Height",(short)1);root.putShort("Length",(short)1);
|
||||
var blocks=version==3?new CompoundTag():root;if(version==3)root.put("Blocks",blocks);
|
||||
var palette=new CompoundTag();palette.putInt("minecraft:grass",0);blocks.put("Palette",palette);blocks.putByteArray(version==3?"Data":"BlockData",new byte[]{0});
|
||||
var file=PlanClient.directory().resolve("beta100-fixture-"+version+".schem");
|
||||
try{
|
||||
java.nio.file.Files.createDirectories(file.getParent());net.minecraft.nbt.NbtIo.writeCompressed(version==3?wrap(root):root,file);
|
||||
var before=java.nio.file.Files.readAllBytes(file);
|
||||
var plan=fr.koka.sanctuary.plans.SchematicIO.read(file);
|
||||
check(plan.cells().getFirst().state().is(Blocks.SHORT_GRASS),"Native old grass rename: Sponge "+version);
|
||||
check(Arrays.equals(before,java.nio.file.Files.readAllBytes(file)),"Import preserves original bytes");
|
||||
palette.remove("minecraft:grass");palette.putInt("missing_mod:unknown",0);
|
||||
try{fr.koka.sanctuary.plans.SchematicIO.decode(version==3?wrap(root):root,"Bad");throw new AssertionError("Missing mod rejected");}
|
||||
catch(java.io.IOException expected){check(expected.getMessage().contains("missing_mod:unknown"),"Useful unknown block diagnostic");}
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
finally{try{java.nio.file.Files.deleteIfExists(file);}catch(java.io.IOException ignored){}}
|
||||
}
|
||||
System.out.println("IMPORT100_PASS Sponge v2/v3, native data fixes, missing block diagnosis, original bytes preserved");
|
||||
}
|
||||
private static CompoundTag wrap(CompoundTag root){var outer=new CompoundTag();outer.put("Schematic",root);return outer;}
|
||||
private static void equip(ServerPlayer p,Item item,FamiliarPersonality personality,String mood,int size){
|
||||
p.stopRiding();CarryService.detach(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.removeAttached(FamiliarBattle.COMBAT_LOCK);p.removeAttached(FamiliarBattle.LAST_FAMILIAR);
|
||||
var egg=new ItemStack(item);var scale=new CompoundTag();scale.putInt("schema",1);scale.putInt("per_mille",size);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,scale));
|
||||
var profile=FamiliarBattle.world(p.level().getServer()).catalog.forEgg(egg);UUID id;do{id=UUID.randomUUID();}while(FamiliarPersonality.of(id,profile.id())!=personality||!FamiliarBond.create(id,profile,0,"test",p.getUUID()).temperament().equals(mood));
|
||||
FamiliarBattle.world(p.level().getServer()).store.put(FamiliarBond.create(id,profile,0,"test",p.getUUID()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,FamiliarIdentity.assign(egg,id));
|
||||
CompanionService.tick(p);p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);check(FamiliarService.active(p)!=null,"Familiar spawned: "+item);p.inventoryMenu.broadcastChanges();
|
||||
}
|
||||
private static Mob enemy(ServerPlayer p,double x,double y,double z){var e=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);e.setNoAi(true);e.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);e.setHealth(100);e.setPos(x,y,z);p.level().addFreshEntity(e);return e;}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+46
-2
@@ -67,8 +67,20 @@ public final class Fourneau085ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null&&FourneauModels.modelAt(ORIGIN).getPath().contains(facing.getName()),200);
|
||||
c.waitTicks(8);c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(15);
|
||||
c.runOnClient(m->{for(int x=0;x<3;x++)for(int y=0;y<3;y++)for(int z=0;z<3;z++)check(FourneauModels.modelAt(ORIGIN.offset(x,y,z))!=null,"All 27 components synchronized across four chunks");});
|
||||
c.takeScreenshot("fourneau085-"+facing.getName());
|
||||
server.runOnServer(s->{var l=s.getPlayerList().getPlayers().getFirst().level();
|
||||
var f=(FurnaceBlockEntity)l.getBlockEntity(ORIGIN.offset(1,1,1));
|
||||
f.setItem(0,new ItemStack(Items.RAW_IRON,64));f.setItem(1,new ItemStack(Items.COAL,64));});
|
||||
c.waitFor(m->oven.members().stream().allMatch(p->m.level.getBlockState(p).getValue(AbstractFurnaceBlock.LIT)),200);
|
||||
c.runOnClient(m->verifyFire(m,facing,true));
|
||||
c.waitTicks(15);c.takeScreenshot("fourneau094-lit-"+facing.getName());
|
||||
}
|
||||
server.runOnServer(s->{var l=s.getPlayerList().getPlayers().getFirst().level();
|
||||
for(var pos:oven.members()) {var f=(FurnaceBlockEntity)l.getBlockEntity(pos);
|
||||
f.setItem(0,ItemStack.EMPTY);f.setItem(1,ItemStack.EMPTY);((MultiblockFurnaceAccess)f).sanctuary$heat(0);}
|
||||
((FurnaceBlockEntity)l.getBlockEntity(ORIGIN)).setItem(0,new ItemStack(Items.DIAMOND,17));});
|
||||
c.waitFor(m->oven.members().stream().noneMatch(p->m.level.getBlockState(p).getValue(AbstractFurnaceBlock.LIT)),200);
|
||||
c.runOnClient(m->verifyFire(m,Direction.WEST,false));
|
||||
c.waitTicks(10);c.takeScreenshot("fourneau094-cold");
|
||||
// Resource reload must retain the synchronized appearance and rebuild native models.
|
||||
var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(10);
|
||||
c.takeScreenshot("fourneau085-reload");
|
||||
@@ -92,9 +104,41 @@ public final class Fourneau085ClientChecks implements FabricClientGameTest {
|
||||
c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(15);c.takeScreenshot("fourneau085-return");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.level().destroyBlock(ORIGIN.offset(2,2,2),true);});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null,200);c.waitTicks(10);c.takeScreenshot("fourneau085-broken");
|
||||
System.out.println("FOURNEAU085_PASS: four orientations, four chunks, native models, resource reload, unload/re-entry, stock preservation, dismantle and break");
|
||||
System.out.println("FOURNEAU094_PASS: lit and cold facade, particles inside textured apertures, vanilla furnace preserved; FOURNEAU085_PASS: four orientations, four chunks, native models, resource reload, unload/re-entry, stock preservation, dismantle and break");
|
||||
}
|
||||
}
|
||||
private static void verifyFire(net.minecraft.client.Minecraft m,Direction facing,boolean lit){
|
||||
var emissions=fr.koka.sanctuary.gametest.Fourneau094Probe.emissions;
|
||||
emissions.clear();fr.koka.sanctuary.gametest.Fourneau094Probe.armed=true;
|
||||
try {
|
||||
var random=net.minecraft.util.RandomSource.create(94);
|
||||
// Real block animation entry point must replace all 27 individual furnace sources.
|
||||
for(int n=0;n<50;n++)for(var pos:oven.members()){
|
||||
var state=m.level.getBlockState(pos);
|
||||
check(FourneauModels.modelAt(pos,state).getPath().endsWith("_lit")==lit,"Whole facade follows shared heat");
|
||||
state.getBlock().animateTick(state,m.level,pos,random);
|
||||
}
|
||||
check(emissions.size()==(lit?200:0),"Exactly two flame/smoke pairs per machine, none when cold: "+emissions.size());
|
||||
var texture=javax.imageio.ImageIO.read(Fourneau085ClientChecks.class.getResourceAsStream("/assets/sanctuary/textures/block/fourneau/front_on.png"));
|
||||
var original=javax.imageio.ImageIO.read(Fourneau085ClientChecks.class.getResourceAsStream("/assets/sanctuary/textures/block/fourneau/front.png"));
|
||||
for(var emission:emissions){
|
||||
var q=emission.point().subtract(ORIGIN.getX(),ORIGIN.getY(),ORIGIN.getZ());
|
||||
double u=switch(facing){case NORTH->3-q.x;case SOUTH->q.x;case EAST->3-q.z;case WEST->q.z;default->throw new AssertionError();};
|
||||
double normal=switch(facing){case NORTH->-q.z;case SOUTH->q.z-3;case EAST->q.x-3;case WEST->-q.x;default->0;};
|
||||
check(Math.abs(normal-.02)<.00001,"Emission is in front of facade, never a side or interior block");
|
||||
int x=(int)(u*16),y=(int)((3-q.y)*16);
|
||||
int rgb=texture.getRGB(x,y),old=original.getRGB(x,y);int r=(rgb>>16)&255,g=(rgb>>8)&255,b=rgb&255;
|
||||
check(r>100&&r>b*1.5,"Emission overlaps fire texel "+x+","+y);
|
||||
check((old&255)<80&&((old>>8)&255)<80&&((old>>16)&255)<80,"Emission overlaps original dark opening");
|
||||
check(y==16||y==37,"Both correct aperture heights");
|
||||
}
|
||||
// A non-assembled furnace must keep the native animation.
|
||||
emissions.clear();var pos=ORIGIN.offset(7,0,0);
|
||||
Blocks.FURNACE.defaultBlockState().setValue(AbstractFurnaceBlock.LIT,true).getBlock().animateTick(Blocks.FURNACE.defaultBlockState().setValue(AbstractFurnaceBlock.LIT,true),m.level,pos,random);
|
||||
check(emissions.size()==2,"Unassembled furnace retains native particles");
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
finally{fr.koka.sanctuary.gametest.Fourneau094Probe.armed=false;emissions.clear();}
|
||||
}
|
||||
private static net.minecraft.client.input.KeyEvent enter(){return new net.minecraft.client.input.KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
|
||||
/** Native quick-transfer, arbitrary slot placement, shared cooking and synchronized feedback. */
|
||||
public final class Furnace115ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos ORIGIN=new BlockPos(2,1,2);
|
||||
private static MultiblockData.Machine oven;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Furnace115 "+UUID.randomUUID());s.setSeed("115");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.getInventory().clearContent();p.teleportTo(1.5,1,1.5);
|
||||
for(int x=-2;x<8;x++)for(int z=-2;z<8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
oven=new MultiblockData.Machine(UUID.randomUUID(),ORIGIN,true,Direction.NORTH);for(var pos:oven.members())p.level().setBlockAndUpdate(pos,Blocks.FURNACE.defaultBlockState());check(Multiblocks.assemble(p,oven),"Assemble");
|
||||
for(int i=0;i<9;i++){var stack=new ItemStack(Items.RAW_IRON,8);stack.set(DataComponents.CUSTOM_NAME,Component.literal("Batch "+i));p.getInventory().setItem(i,stack);}Multiblocks.open(p,oven,ORIGIN,0,"",0);
|
||||
});
|
||||
c.waitFor(m->m.gui.screen() instanceof MultiblockScreen&&m.player.containerMenu.getSlot(54).hasItem(),200);
|
||||
// One native Shift-drag crosses nine hotbar stacks, with no mouse release between them.
|
||||
c.getInput().holdKey(o->o.keyShift);move(c,54);c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),1));
|
||||
for(int i=55;i<63;i++)move(c,i);
|
||||
c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),0));c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitTicks(8);c.takeScreenshot("furnace115-inputs");
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(8).hasItem()&&!m.player.containerMenu.getSlot(62).hasItem(),150);
|
||||
c.runOnClient(m->{for(int i=0;i<9;i++)check(m.player.containerMenu.getSlot(i).getItem().getHoverName().getString().equals("Batch "+i),"Shift-drag fills consecutive UI slots, no three-column gaps");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);for(int i=0;i<9;i++)check(stock.getItem(i*3).getCount()==8,"Physical inputs correspond to visible slots");});
|
||||
// Move an ingredient to a distant, arbitrary slot using real inventory packets.
|
||||
click(c,1,0,ContainerInput.PICKUP);click(c,23,0,ContainerInput.PICKUP);c.waitTicks(10);
|
||||
c.runOnClient(m->{check(m.player.containerMenu.getSlot(1).getItem().isEmpty()&&m.player.containerMenu.getSlot(23).getItem().getCount()==8,"Manual placement remains in chosen cell: "+m.player.containerMenu.slots.stream().filter(slot->slot.hasItem()).map(slot->slot.index+":"+slot.getItem()).toList()+", cursor="+m.player.containerMenu.getCarried());});
|
||||
click(c,23,0,ContainerInput.PICKUP);
|
||||
click(c,-999,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(0,0),ContainerInput.QUICK_CRAFT);
|
||||
for(int i:new int[]{1,9,10,13,14,22})click(c,i,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(1,0),ContainerInput.QUICK_CRAFT);
|
||||
click(c,-999,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(2,0),ContainerInput.QUICK_CRAFT);click(c,23,0,ContainerInput.PICKUP);c.waitTicks(6);
|
||||
c.runOnClient(m->{for(int i:new int[]{1,9,10,13,14,22})check(m.player.containerMenu.getSlot(i).getItem().getCount()==1,"Native drag distribution keeps every chosen column");check(m.player.containerMenu.getSlot(23).getItem().getCount()==2&&m.player.containerMenu.getCarried().isEmpty(),"Distributed count conserved");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);check(stock.getItem(69).getCount()==2&&stock.getItem(3).getCount()==1,"Manual distribution matches server");for(int i=0;i<27;i++)check(p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.RAW_IRON)),"Every visible input validates its physical input slot, not Slot.index");reset(stock);stock.setItem(0,new ItemStack(Items.RAW_IRON,4));stock.setItem(18,new ItemStack(Items.RAW_GOLD,4));
|
||||
var coal=new ItemStack(Items.COAL,5);coal.set(DataComponents.CUSTOM_NAME,Component.literal("Stationary fuel"));stock.setItem(79,coal);
|
||||
for(int tick=0;tick<205;tick++)MultiblockCooking.tick(p.level(),oven);
|
||||
check(stock.getItem(79).getCount()==4&&stock.getItem(79).getHoverName().getString().equals("Stationary fuel"),"Named unburnt fuel stays in original cell");
|
||||
for(int i=1;i<79;i+=3)check(stock.getItem(i).isEmpty(),"No displaced coal in other slots");
|
||||
check(stock.getItem(2).is(Items.IRON_INGOT)&&stock.getItem(20).is(Items.GOLD_INGOT),"Shared heat cooks both arbitrary lanes");
|
||||
Multiblocks.open(p,oven,ORIGIN,0,"",0);
|
||||
});
|
||||
c.waitTicks(6);c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.cooking==2&&menu.heat>0&&menu.laneState(6)==MultiblockMenu.COOKING,"Real per-lane feedback synchronizes");});c.takeScreenshot("furnace115-cooking");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);reset(stock);
|
||||
stock.setItem(0,new ItemStack(Items.RAW_IRON));stock.setItem(3,new ItemStack(Items.DIAMOND));stock.setItem(6,new ItemStack(Items.RAW_GOLD));stock.setItem(8,new ItemStack(Items.GOLD_INGOT,64));
|
||||
var a=(MultiblockFurnaceAccess)stock.cell(0);a.sanctuary$progress(80);a.sanctuary$total(200);p.containerMenu.broadcastChanges();
|
||||
});c.waitTicks(6);
|
||||
c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.laneState(0)==MultiblockMenu.NO_FUEL&&menu.laneProgress(0)==40,"Paused progress is visible and preserved");check(menu.laneState(1)==MultiblockMenu.NO_RECIPE,"Invalid recipe explained");check(menu.laneState(2)==MultiblockMenu.BLOCKED,"Blocked output explained");check(menu.cooking==0&&menu.heat==0,"Cold furnace clearly shown");});c.takeScreenshot("furnace115-paused");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();cookingRegressions(p);var stock=new MultiblockContainer(p.level(),oven);reset(stock);
|
||||
for(int i=0;i<9;i++){var stack=new ItemStack(Items.COAL,5);stack.set(DataComponents.CUSTOM_NAME,Component.literal("Fuel "+i));p.getInventory().setItem(i,stack);}Multiblocks.open(p,oven,ORIGIN,0,"",1);
|
||||
});c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.section()==1&&menu.getSlot(54).hasItem(),100);
|
||||
// No cooking in this fixture: compare the predicted UI in the same call, before any server reply.
|
||||
c.runOnClient(m->{var menu=m.player.containerMenu;for(int i=0;i<9;i++){
|
||||
m.gameMode.handleContainerInput(menu.containerId,54+i,0,ContainerInput.QUICK_MOVE,m.player);
|
||||
check(menu.getSlot(54+i).getItem().isEmpty()&&menu.getSlot(i).getItem().getCount()==5,"Immediate Shift prediction uses consecutive fuel cells");}
|
||||
check(!menu.getSlot(9).mayPlace(new ItemStack(Items.DIAMOND)),"Client rejects invalid fuel like server");});
|
||||
c.waitTicks(8);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);for(int i=0;i<9;i++)check(stock.getItem(i*3+1).getCount()==5,"Fuel server mapping matches prediction");for(int i=0;i<27;i++){check(p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.COAL)),"All 27 fuel cells accept fuel");check(!p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.DIAMOND)),"All 27 fuel cells refuse other items");}});c.takeScreenshot("furnace115-fuel");
|
||||
c.runOnClient(m->{var screen=(MultiblockScreen)m.gui.screen();screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(Multiblocks.text("how_to_cook").getString())).findFirst().orElseThrow().onPress(enter());check(screen.helpVisible(),"Help button opens its explanation");});c.waitTicks(5);c.takeScreenshot("furnace115-help");
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected("fr_fr");return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(3);c.takeScreenshot("furnace115-help-fr");
|
||||
System.out.println("FURNACE115_PASS native Shift-drag, consecutive input/fuel mapping, immediate prediction, arbitrary placement, stationary named fuel, heat/output/pause/recipe feedback, fuel budget, refuelling, buckets, save and result extraction");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void cookingRegressions(ServerPlayer p){
|
||||
var stock=new MultiblockContainer(p.level(),oven);reset(stock);stock.setItem(0,new ItemStack(Items.WET_SPONGE));stock.setItem(79,new ItemStack(Items.LAVA_BUCKET));for(int t=0;t<205;t++)MultiblockCooking.tick(p.level(),oven);
|
||||
check(stock.getItem(2).is(Items.SPONGE)&&stock.getItem(1).is(Items.WATER_BUCKET)&&stock.getItem(79).isEmpty(),"Native lava and wet sponge remainder");
|
||||
reset(stock);for(int i=0;i<81;i+=3)stock.setItem(i,new ItemStack(Items.RAW_IRON));stock.setItem(79,new ItemStack(Items.COAL));for(int t=0;t<90;t++)MultiblockCooking.tick(p.level(),oven);
|
||||
int partial=((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress();check(partial>0&&partial<200,"One coal cannot cook all 27 items");for(int t=0;t<30;t++)MultiblockCooking.tick(p.level(),oven);check(((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress()==partial,"No lost paid progress when out of fuel");
|
||||
stock.setItem(79,new ItemStack(Items.COAL,4));for(int t=0;t<205;t++)MultiblockCooking.tick(p.level(),oven);int produced=0;for(int i=2;i<81;i+=3)produced+=stock.getItem(i).getCount();check(produced==27,"Refuelling completes all lanes exactly once");
|
||||
var f=(AbstractFurnaceBlockEntity)stock.cell(0);var restored=(AbstractFurnaceBlockEntity)BlockEntity.loadStatic(f.getBlockPos(),f.getBlockState(),f.saveWithFullMetadata(p.level().registryAccess()),p.level().registryAccess());check(((MultiblockFurnaceAccess)restored).sanctuary$heat()==((MultiblockFurnaceAccess)f).sanctuary$heat(),"Native heat survives save");
|
||||
Multiblocks.open(p,oven,ORIGIN,0,"",2);var menu=(MultiblockMenu)p.containerMenu;check(!menu.getSlot(0).mayPlace(new ItemStack(Items.STONE)),"Results refuse input");check(!menu.quickMoveStack(p,0).isEmpty()&&stock.getItem(2).isEmpty(),"Native result slot can extract");
|
||||
}
|
||||
private static void reset(MultiblockContainer stock){stock.clearContent();for(int i=0;i<81;i+=3){var a=(MultiblockFurnaceAccess)stock.cell(i);a.sanctuary$heat(0);a.sanctuary$progress(0);}}
|
||||
private static void click(ClientGameTestContext c,int slot,int button,ContainerInput type){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,slot,button,type,m.player));}
|
||||
private static void move(ClientGameTestContext c,int index){c.runOnClient(m->{var a=(AbstractContainerScreenAccessor)m.gui.screen();var slot=m.player.containerMenu.getSlot(index);var w=m.getWindow();double x=a.sanctuary$getLeftPos()+slot.x+8,y=a.sanctuary$getTopPos()+slot.y+8;double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});c.waitTick();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.gems.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.*;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.item.enchantment.Enchantments;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
|
||||
/** Actual native registration, recipes, drops, tab order and naturally decorated Sanctuary chunks. */
|
||||
public final class Gems126ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);var enter=new KeyEvent(InputConstants.KEY_RETURN,0,0);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter);intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Gems126 registry "+UUID.randomUUID());s.setSeed("126");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();var pos=new BlockPos(0,4,0);var iron=new ItemStack(Items.IRON_PICKAXE);var silk=iron.copy();silk.enchant(l.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH),1);var fortune=iron.copy();fortune.enchant(l.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.FORTUNE),3);
|
||||
for(var f:Gems.FAMILIES){
|
||||
for(var b:List.of(f.block(),f.ore(),f.deepslateOre())){var state=b.defaultBlockState();check(state.is(BlockTags.MINEABLE_WITH_PICKAXE)&&state.is(BlockTags.NEEDS_IRON_TOOL),"Native mining tags "+b);check(iron.isCorrectToolForDrops(state)&&!new ItemStack(Items.STONE_PICKAXE).isCorrectToolForDrops(state),"Iron tier enforced "+b);}
|
||||
drop(Block.getDrops(f.block().defaultBlockState(),l,pos,null,p,iron),f.block().asItem(),1);
|
||||
for(var ore:List.of(f.ore(),f.deepslateOre())){
|
||||
drop(Block.getDrops(ore.defaultBlockState(),l,pos,null,p,iron),f.gem(),1);drop(Block.getDrops(ore.defaultBlockState(),l,pos,null,p,silk),ore.asItem(),1);
|
||||
int max=0;for(int i=0;i<48;i++){var drops=Block.getDrops(ore.defaultBlockState(),l,pos,null,p,fortune);check(drops.size()==1&&drops.getFirst().is(f.gem()),"Fortune gem type");int n=drops.getFirst().getCount();check(n>=1&&n<=4,"Native Fortune III range");max=Math.max(max,n);}check(max>1,"Fortune increases gem yield");
|
||||
var input=new SingleRecipeInput(new ItemStack(ore));for(var type:List.of(RecipeType.SMELTING,RecipeType.BLASTING)){var recipe=s.getRecipeManager().getRecipeFor(type,input,l).orElseThrow().value();check(recipe.assemble(input).is(f.gem()),"Native furnace recipe");}
|
||||
}
|
||||
var input=CraftingInput.of(3,3,Collections.nCopies(9,new ItemStack(f.gem())));drop(List.of(s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,l).orElseThrow().value().assemble(input)),f.block().asItem(),1);
|
||||
input=CraftingInput.of(1,1,List.of(new ItemStack(f.block())));drop(List.of(s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,l).orElseThrow().value().assemble(input)),f.gem(),9);
|
||||
check(f.block().defaultBlockState().is(BlockTags.BEACON_BASE_BLOCKS)&&new ItemStack(f.gem()).is(ItemTags.BEACON_PAYMENT_ITEMS),"Native beacon compatibility");
|
||||
var feature=l.registryAccess().lookupOrThrow(Registries.FEATURE).getOrThrow(ResourceKey.create(Registries.FEATURE,SanctuaryMod.id("ore_"+f.id()))).value();check(feature instanceof GemOreFeature,"Data feature decoded");
|
||||
check(!feature.place(l,l.getChunkSource().getGenerator(),RandomSource.create(126),pos),"Gem veins do not run in ordinary flat/vanilla worlds");
|
||||
fr.koka.sanctuary.recipes.RecipeService.discover(p,f.gem());fr.koka.sanctuary.recipes.RecipeService.refresh(p);check(fr.koka.sanctuary.recipes.RecipeService.knowledge(p).recipes.contains("sanctuary:"+f.id()+"_block"),"Native discovery unlocks storage recipe");
|
||||
}
|
||||
int x=0;for(var block:List.of(Blocks.EMERALD_BLOCK,Gems.RUBY.block(),Gems.SAPPHIRE.block(),Blocks.EMERALD_ORE,Gems.RUBY.ore(),Gems.SAPPHIRE.ore(),Blocks.DEEPSLATE_EMERALD_ORE,Gems.RUBY.deepslateOre(),Gems.SAPPHIRE.deepslateOre())){l.setBlockAndUpdate(new BlockPos(x%3,1,x/3),block.defaultBlockState());x++;}
|
||||
p.teleportTo(1.5,3,7);p.getAbilities().flying=true;p.onUpdateAbilities();p.getInventory().setItem(0,new ItemStack(Items.EMERALD));p.getInventory().setItem(1,new ItemStack(Gems.RUBY.gem()));p.getInventory().setItem(2,new ItemStack(Gems.SAPPHIRE.gem()));p.inventoryMenu.broadcastFullState();
|
||||
System.out.println("GEMS126_SERVER_PASS eight registered items, six blocks, native loot/Fortune/Silk Touch, twelve recipes, mining tiers, beacons, discoveries and vanilla generation exclusion");
|
||||
});
|
||||
c.runOnClient(m->{CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());order(CreativeModeTabs.INGREDIENTS,Items.EMERALD,Gems.RUBY.gem(),Gems.SAPPHIRE.gem());order(CreativeModeTabs.BUILDING_BLOCKS,Items.EMERALD_BLOCK,Gems.RUBY.block().asItem(),Gems.SAPPHIRE.block().asItem());order(CreativeModeTabs.NATURAL_BLOCKS,Items.EMERALD_ORE,Gems.RUBY.ore().asItem(),Gems.SAPPHIRE.ore().asItem());order(CreativeModeTabs.NATURAL_BLOCKS,Items.DEEPSLATE_EMERALD_ORE,Gems.RUBY.deepslateOre().asItem(),Gems.SAPPHIRE.deepslateOre().asItem());
|
||||
for(var f:Gems.FAMILIES)for(var block:List.of(f.block(),f.ore(),f.deepslateOre())){var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(block.defaultBlockState()).collectParts(RandomSource.create(0),parts);check(!parts.isEmpty(),"Baked gem block model");for(var part:parts)for(var face:Direction.values())for(var quad:part.getQuads(face))check(!quad.materialInfo().sprite().contents().name().toString().contains("missingno"),"Native texture atlas resolves supplied gem PNG");}
|
||||
System.out.println("GEMS126_CREATIVE_PASS emerald, ruby, sapphire adjacency in four groups; all six baked block models");
|
||||
});
|
||||
c.waitTicks(10);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new net.minecraft.world.phys.Vec3(1.5,1.5,1.5)));c.waitTicks(4);c.takeScreenshot("gems126-gallery");
|
||||
Clay126Checks.exercise(c,world.getServer());
|
||||
Discovery126Checks.exercise(c,world.getServer());
|
||||
}
|
||||
intro.set(true);net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;var found=new HashMap<BlockPos,BlockState>();
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Gems126 natural "+UUID.randomUUID());s.setSeed("126");s.setAllowCommands(true);s.setGenerateStructures(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),2400);
|
||||
world.getServer().runOnServer(s->{var l=s.overworld();check(l.getChunkSource().getGenerator() instanceof fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator,"Actual Sanctuary generator");
|
||||
for(int cx=-4;cx<=4;cx++)for(int cz=-4;cz<=4;cz++){var chunk=l.getChunk(cx,cz);var cursor=new BlockPos.MutableBlockPos();for(int x=0;x<16;x++)for(int z=0;z<16;z++)for(int y=0;y<=256;y++){cursor.set(cx*16+x,y,cz*16+z);var state=chunk.getBlockState(cursor);if(state.is(Gems.RUBY.ore())||state.is(Gems.RUBY.deepslateOre())||state.is(Gems.SAPPHIRE.ore())||state.is(Gems.SAPPHIRE.deepslateOre()))found.put(cursor.immutable(),state);}}
|
||||
for(var f:Gems.FAMILIES){long ordinary=found.values().stream().filter(b->b.is(f.ore())).count(),deep=found.values().stream().filter(b->b.is(f.deepslateOre())).count();System.out.println("GEMS126_NATURAL "+f.id()+" stone="+ordinary+" deepslate="+deep+" seed=126 chunks=81");check(ordinary+deep>0,"Both gems generate naturally without structure placement: "+f.id());}
|
||||
var biome=l.getBiome(found.keySet().iterator().next()).value();var features=biome.getGenerationSettings().features().get(GenerationStep.Decoration.UNDERGROUND_ORES.ordinal());for(var f:Gems.FAMILIES)check(features.stream().anyMatch(h->h.is(ResourceKey.create(Registries.PLACED_FEATURE,SanctuaryMod.id("ore_"+f.id())))),"Native biome includes configurable gem placement");
|
||||
Discovery126Checks.command(s.getPlayerList().getPlayers().getFirst(),"all");
|
||||
var mined=found.keySet().iterator().next();l.setBlockAndUpdate(mined,Blocks.AIR.defaultBlockState());found.put(mined,Blocks.AIR.defaultBlockState());
|
||||
});saved=world.getWorldSave();
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,2400);world.getServer().runOnServer(s->{Discovery126Checks.all(s.getPlayerList().getPlayers().getFirst());for(var e:found.entrySet())check(s.overworld().getBlockState(e.getKey())==e.getValue(),"Generated ores persist on reload without repopulation");});
|
||||
}
|
||||
System.out.println("GEMS126_PASS registry, recipes, drops, creative order, native models, natural Sanctuary generation seed 126 and save/reconnect");
|
||||
}
|
||||
private static void order(ResourceKey<CreativeModeTab> tab,Item anchor,Item ruby,Item sapphire){var items=new ArrayList<>(BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(tab).value().getDisplayItems());int i=0;while(i<items.size()&&!items.get(i).is(anchor))i++;check(i+2<items.size()&&items.get(i+1).is(ruby)&&items.get(i+2).is(sapphire),"Emerald/ruby/sapphire order in "+tab);check(items.stream().filter(v->v.is(ruby)||v.is(sapphire)).count()==2,"No duplicate creative entries");}
|
||||
private static void drop(List<ItemStack> drops,Item item,int n){check(drops.size()==1&&drops.getFirst().is(item)&&drops.getFirst().getCount()==n,"Native loot/craft expected "+n+" "+item+", got "+drops);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.companion.CompanionWorld;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native movement contacts, fall callbacks and real attack regression for every flying profile. */
|
||||
public final class Landing101ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
|
||||
|
||||
var descending=new AtomicReference<FamiliarEntity>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var pet=descending.get();if(pet!=null&&pet.isAlive())pet.move(MoverType.SELF,new Vec3(0,-.25,0));});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Landing101 "+UUID.randomUUID());s.setSeed("101");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());});
|
||||
var flyers=new ArrayList<Item>();c.runOnClient(m->{for(var item:net.minecraft.core.registries.BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem&&FamiliarMotion.of(item.getDefaultInstance()).flies())flyers.add(item);});
|
||||
for(var item:flyers){
|
||||
var health=new AtomicReference<Float>();var hurt=new AtomicInteger();
|
||||
server.runOnServer(s->{var p=player(s);equip(p,item,FamiliarPersonality.PACIFIST,"calm",1000);var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setPos(2.5,7,2.5);pet.setDeltaMovement(Vec3.ZERO);pet.resetFallDistance();health.set(pet.getHealth());hurt.set(FamiliarBattle.state(p).lastHurt);
|
||||
// Native fall callbacks can also arrive with a precomputed fall distance.
|
||||
check(!pet.causeFallDamage(7,1,pet.damageSources().fall()),"Flying familiar rejects native fall callback: "+item);
|
||||
check(pet.getHealth()==health.get()&&FamiliarBattle.state(p).lastHurt==hurt.get(),"No false hurt/combat from landing callback: "+item);
|
||||
descending.set(pet);
|
||||
});
|
||||
c.waitTicks(32);
|
||||
server.runOnServer(s->{var p=player(s);var pet=descending.getAndSet(null);check(pet.onGround(),"Native contact reached ground: "+item);check(pet.getHealth()==health.get()&&pet.fallDistance==0&&FamiliarBattle.state(p).lastHurt==hurt.get(),"Slow descent remains harmless: "+item);
|
||||
pet.setPos(2.5,7,2.5);pet.setOnGround(false);pet.move(MoverType.SELF,new Vec3(0,-8,0));check(pet.onGround()&&pet.getHealth()==health.get()&&FamiliarBattle.state(p).lastHurt==hurt.get(),"Fast descent also safe: "+item);
|
||||
var enemy=enemy(p,4,1,2.5);check(pet.hurtServer(p.level(),p.damageSources().mobAttack(enemy),1),"Genuine hit remains accepted: "+item);check(pet.getHealth()<health.get(),"Genuine hit removes health: "+item);enemy.discard();FamiliarBattle.cancel(p);
|
||||
System.out.println("LANDING101_SPECIES_PASS "+item);
|
||||
});
|
||||
}
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.WOLF_SPAWN_EGG,FamiliarPersonality.PACIFIST,"calm",1000);var pet=FamiliarService.active(p);float before=pet.getHealth();check(pet.causeFallDamage(7,1,pet.damageSources().fall())&&pet.getHealth()<before,"Terrestrial fall damage retained");});
|
||||
System.out.println("LANDING101_PASS profiles="+flyers.size());
|
||||
}finally{descending.set(null);}
|
||||
}
|
||||
private static void equip(ServerPlayer p,Item item,FamiliarPersonality personality,String mood,int size){
|
||||
p.stopRiding();CarryService.detach(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.removeAttached(FamiliarBattle.COMBAT_LOCK);p.removeAttached(FamiliarBattle.LAST_FAMILIAR);
|
||||
var egg=new ItemStack(item);var scale=new CompoundTag();scale.putInt("schema",1);scale.putInt("per_mille",size);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,scale));
|
||||
var profile=FamiliarBattle.world(p.level().getServer()).catalog.forEgg(egg);UUID id;do{id=UUID.randomUUID();}while(FamiliarPersonality.of(id,profile.id())!=personality||!FamiliarBond.create(id,profile,0,"test",p.getUUID()).temperament().equals(mood));
|
||||
FamiliarBattle.world(p.level().getServer()).store.put(FamiliarBond.create(id,profile,0,"test",p.getUUID()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,FamiliarIdentity.assign(egg,id));
|
||||
CompanionService.tick(p);p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);check(FamiliarService.active(p)!=null,"Familiar spawned: "+item);p.inventoryMenu.broadcastChanges();
|
||||
}
|
||||
private static Mob enemy(ServerPlayer p,double x,double y,double z){var e=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);e.setNoAi(true);e.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);e.setHealth(100);e.setPos(x,y,z);p.level().addFreshEntity(e);return e;}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.client.panorama.*;
|
||||
import fr.koka.sanctuary.client.shader.VanillaLight;
|
||||
import fr.koka.sanctuary.weather.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.nio.file.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.animal.Animal;
|
||||
import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
|
||||
public final class Living105ClientChecks implements FabricClientGameTest {
|
||||
private static int wearer,cow,pig;private static double cowStart,pigStart,flightStart;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen s){Starter039TestSupport.selectFirst(s);s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Living105 "+UUID.randomUUID());s.setSeed("105");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
for(int x=-12;x<=12;x++)for(int z=-12;z<=12;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.GRASS_BLOCK.defaultBlockState());p.teleportTo(0,80,-3);p.getInventory().clearContent();
|
||||
var m=spawn(l,EntityTypes.COW,0,80,0);m.setNoAi(true);wearer=m.getId();
|
||||
HeadEquipment.set(m,new ItemStack(Items.WRITABLE_BOOK));var original=new ArrayList<net.minecraft.server.network.Filterable<String>>();original.add(net.minecraft.server.network.Filterable.passThrough("My original page"));HeadEquipment.get(m).set(DataComponents.WRITABLE_BOOK_CONTENT,new WritableBookContent(original));
|
||||
check(HeadCosmeticService.interact(p,m),"Book responds to native head interaction");check(pages(m)==2,"Pet adds diary entry");check(m.hurtServer(l,p.damageSources().playerAttack(p),1),"Actual hurt event");check(pages(m)==3,"Hurt adds distinct diary entry");check(HeadEquipment.get(m).get(DataComponents.WRITABLE_BOOK_CONTENT).pages().getFirst().raw().equals("My original page"),"Original text preserved");
|
||||
var shown=HeadCosmeticService.appearance(m);check(shown.get(DataComponents.WRITABLE_BOOK_CONTENT).pages().isEmpty(),"Diary not broadcast in appearance");
|
||||
p.setShiftKeyDown(true);check(MobHeadService.equip(p,m),"Retrieve written diary");check(p.getInventory().countItem(Items.WRITABLE_BOOK)==1,"Book returned once");p.setShiftKeyDown(false);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);HeadEquipment.set(m,new ItemStack(Items.WHEAT_SEEDS));var head=HeadBlockService.session(m);check(head!=null,"Crop session");head.change(((CropBlock)Blocks.WHEAT).getStateForAge(7));check(HeadBlockService.use(p,m,false),"Harvest mature head crop");check(p.getInventory().countItem(Items.WHEAT)==1,"Actual native wheat loot");check(((CropBlock)Blocks.WHEAT).getAge(head.state)==0,"Replanted after harvest");
|
||||
head.change(((CropBlock)Blocks.WHEAT).getStateForAge(7));p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);p.setShiftKeyDown(true);check(MobHeadService.equip(p,m),"Mature crop retrieval");p.setShiftKeyDown(false);
|
||||
p.getInventory().clearContent();HeadEquipment.set(m,new ItemStack(Items.WHEAT));var a=spawn(l,EntityTypes.COW,7,80,0);cow=a.getId();cowStart=a.distanceToSqr(m);
|
||||
});
|
||||
c.waitTicks(100);
|
||||
server.runOnServer(s->{var m=mob(s,wearer);check(mob(s,cow).distanceToSqr(m)<cowStart/2,"Wheat hat attracts native cow");mob(s,cow).discard();HeadEquipment.set(m,new ItemStack(Items.CARROT_ON_A_STICK));var a=spawn(player(s).level(),EntityTypes.PIG,7,80,0);pig=a.getId();pigStart=a.distanceToSqr(m);});
|
||||
c.waitTicks(100);
|
||||
server.runOnServer(s->{var p=player(s);var m=mob(s,wearer);check(mob(s,pig).distanceToSqr(m)<pigStart/2,"Carrot stick guides native pig");mob(s,pig).discard();
|
||||
HeadEquipment.set(m,new ItemStack(Items.FLINT_AND_STEEL));m.setYHeadRot(0);snowball(p,m);check(p.level().getBlockState(m.blockPosition().south()).is(Blocks.FIRE),"Snowball ignites flint hat without native damage");check(HeadEquipment.get(m).getDamageValue()==1,"Flint durability spent once");p.level().removeBlock(m.blockPosition().south(),false);
|
||||
});c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);var m=mob(s,wearer);m.setNoAi(false);HeadEquipment.set(m,new ItemStack(Items.FIREWORK_ROCKET));flightStart=m.getY();snowball(p,m);check(HeadEquipment.get(m).isEmpty(),"Rocket consumed once");});c.waitTicks(18);
|
||||
server.runOnServer(s->{var p=player(s);var m=mob(s,wearer);check(m.getY()>flightStart+2,"Firework lifts mob");MobHeadActions.clear(s);m.setNoAi(true);m.setPos(0,80,0);m.setDeltaMovement(0,0,0);HeadEquipment.set(m,new ItemStack(Items.SPAWNER));var head=HeadBlockService.session(m);var be=(SpawnerBlockEntity)head.entity;be.getSpawner().setEntityId(EntityTypes.PIG,p.level(),p.level().getRandom(),head.pos());var tag=be.saveWithoutMetadata(p.level().registryAccess());tag.putShort("Delay",(short)500);tag.putShort("MinSpawnDelay",(short)200);tag.putShort("MaxSpawnDelay",(short)400);be.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,p.level().registryAccess(),tag));
|
||||
int before=p.level().getEntitiesOfClass(net.minecraft.world.entity.animal.pig.Pig.class,m.getBoundingBox().inflate(10)).size();MobHeadActions.activate(m,p.damageSources().playerAttack(p),true);int after=p.level().getEntitiesOfClass(net.minecraft.world.entity.animal.pig.Pig.class,m.getBoundingBox().inflate(10)).size();check(after>before,"Hit runs configured native spawner immediately");HeadEquipment.set(m,new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace("lightning_rod"))));
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather thunder");p.level().setThunderLevel(1);p.level().setRainLevel(1);
|
||||
var at=HeadBlockService.session(m).pos();check(MobHeadActions.lightningTarget(p.level(),at.offset(5,0,0),at.offset(5,0,0)).equals(at.above()),"Mobile rod attracts lightning");var bolt=EntityTypes.LIGHTNING_BOLT.create(p.level(),EntitySpawnReason.COMMAND);bolt.setVisualOnly(true);bolt.setPos(at.getX()+.5,at.getY()+1,at.getZ()+.5);p.level().addFreshEntity(bolt);
|
||||
});c.waitTicks(2);
|
||||
server.runOnServer(s->{check(HeadBlockService.session(mob(s,wearer)).emitsPower(),"Native lightning powers head rod");});c.waitTicks(60);
|
||||
server.runOnServer(s->{var p=player(s);var m=mob(s,wearer);check(!HeadBlockService.session(m).emitsPower(),"Lightning pulse resets");HeadEquipment.set(m,ItemStack.EMPTY);m.discard();s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather snow");p.level().setRainLevel(1);var at=new BlockPos(9,80,9);check(p.level().getBiome(at).value().shouldSnow(p.level(),at),"Temperate biome permits seasonal accumulation");
|
||||
try{var tick=ServerLevel.class.getDeclaredMethod("tickPrecipitation",BlockPos.class);tick.setAccessible(true);for(int i=0;i<200;i++)tick.invoke(p.level(),at);}catch(Exception e){throw new RuntimeException(e);}check(p.level().getBlockState(at).is(Blocks.SNOW),"Native precipitation accumulates snow");
|
||||
});c.waitFor(m->WeatherClient.state().kind()==DailyWeather.Kind.SNOW,200);
|
||||
c.runOnClient(m->check(m.level.getPrecipitationAt(new BlockPos(9,81,9))==net.minecraft.world.level.biome.Biome.Precipitation.SNOW,"Client snow render and audio query"));
|
||||
server.runOnServer(s->{var p=player(s);s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather clear");var origin=new BlockPos(3,80,1);for(int x=0;x<3;x++)for(int y=0;y<3;y++)for(int z=0;z<3;z++)p.level().setBlockAndUpdate(origin.offset(x,y,z),Blocks.BARREL.defaultBlockState());var f=new MultiblockData.Machine(UUID.randomUUID(),origin,false,Direction.NORTH);check(Multiblocks.assemble(p,f),"Assemble fût");Multiblocks.open(p,f,origin,0,"",0);});
|
||||
c.waitFor(m->m.gui.screen() instanceof MultiblockScreen,200);c.runOnClient(m->{var screen=(MultiblockScreen)m.gui.screen();var a=screen.extraAreas().getFirst();check(screen.mouseScrolled(a.getX()+5,a.getY()+5,0,-1),"Wheel handled");});c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.page()==1,200);
|
||||
c.runOnClient(m->{var screen=(MultiblockScreen)m.gui.screen();var a=screen.extraAreas().getFirst();screen.mouseScrolled(a.getX()+5,a.getY()+5,0,100);});c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.page()==0,200);
|
||||
server.runOnServer(s->player(s).closeContainer());c.waitFor(m->m.gui.screen()==null,200);
|
||||
c.waitTicks(30);var captures=new ArrayList<Path>();
|
||||
for(int saturation:new int[]{0,200}){c.runOnClient(m->{VanillaLight.setEnabled(true);VanillaLight.setSaturation(saturation);PanoramaOptions.get().resolution=256;});c.waitTicks(4);c.runOnClient(PanoramaCapture::request);c.waitFor(m->PanoramaCapture.lastCapture().isDone(),400);captures.add(c.computeOnClient(m->PanoramaCapture.lastCapture().join()));}
|
||||
verifyPanorama(captures);c.runOnClient(m->{VanillaLight.setSaturation(142);PanoramaOptions.get().resolution=1024;});
|
||||
System.out.println("LIVING105_NATIVE_PASS: diary, crops, lures, snowball, flint, firework, spawner, lightning, snow, barrel scroll, shader panoramas "+captures);
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void verifyPanorama(List<Path> captures){try{double[] chroma=new double[2];for(int n=0;n<2;n++){var meta=com.google.gson.JsonParser.parseString(Files.readString(captures.get(n).resolve("capture.json"))).getAsJsonObject();check(meta.getAsJsonArray("post_effects").toString().contains("vanilla_light"),"Active effect captured");for(int f=0;f<6;f++){double before=chroma[n];var image=javax.imageio.ImageIO.read(captures.get(n).resolve("panorama_"+f+".png").toFile());for(int y=0;y<256;y++)for(int x=0;x<256;x++){int rgb=image.getRGB(x,y),r=(rgb>>16)&255,g=(rgb>>8)&255,b=rgb&255;chroma[n]+=Math.max(r,Math.max(g,b))-Math.min(r,Math.min(g,b));}if(n==0)check(chroma[n]-before<100000,"Every face is desaturated: "+f+" "+(chroma[n]-before));}var raw=java.nio.ByteBuffer.wrap(Files.readAllBytes(captures.get(n).resolve("depth_5.f32"))).order(java.nio.ByteOrder.LITTLE_ENDIAN);int surfaces=0;while(raw.hasRemaining())if(raw.getFloat()>0)surfaces++;check(surfaces>30000,"Postprocessing preserves terrain depth");}check(chroma[1]>chroma[0]+100000,"Captured saturation changes actual pixels "+Arrays.toString(chroma));}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void snowball(ServerPlayer p,Mob m){var ball=new Snowball(p.level(),p,new ItemStack(Items.SNOWBALL));try{var hit=Snowball.class.getDeclaredMethod("onHitEntity",EntityHitResult.class);hit.setAccessible(true);hit.invoke(ball,new EntityHitResult(m));}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static int pages(Mob m){return HeadEquipment.get(m).get(DataComponents.WRITABLE_BOOK_CONTENT).pages().size();}
|
||||
private static <T extends Mob>T spawn(ServerLevel l,EntityType<T> type,double x,double y,double z){var m=type.create(l,EntitySpawnReason.COMMAND);m.setPos(x,y,z);l.addFreshEntity(m);return m;}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static Mob mob(net.minecraft.server.MinecraftServer s,int id){return (Mob)s.overworld().getEntity(id);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import fr.koka.sanctuary.realtime.RealtimeService;
|
||||
import fr.koka.sanctuary.weather.SeasonalSnowMelt;
|
||||
import fr.koka.sanctuary.weather.SeasonalWeather;
|
||||
import fr.koka.sanctuary.weather.WeatherService;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.SnowLayerBlock;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
|
||||
public final class Melt109ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos AT=new BlockPos(4,80,4), PERSIST=new BlockPos(5,80,12);
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
new Snow108ClientChecks().runTest(c);
|
||||
var intro=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}});
|
||||
TestWorldSave saved;
|
||||
try {
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Melt109 "+UUID.randomUUID());s.setSeed("109");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
s.getGameRules().set(SanctuaryGameplay.ENABLED,true,null);
|
||||
}).create()) {
|
||||
saved=world.getWorldSave();
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var l=p.level();p.teleportTo(0,80,-5);
|
||||
for(int x=-15;x<=15;x++)for(int z=-15;z<=15;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||
check(l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt enabled by default");
|
||||
check(SeasonalSnowMelt.REALTIME_SCALE==72,"Real-time snow melt scale is one real day per 72 native days");
|
||||
check(SeasonalSnowMelt.chance(SeasonalWeather.Season.SUMMER)==72
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.SPRING)==144
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.AUTUMN)==288
|
||||
&&SeasonalSnowMelt.chance(SeasonalWeather.Season.WINTER)==0,"Seasonal melt chances include the 72x real-time scale");
|
||||
deposit(l,AT,16);check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default two-block accumulation retained");
|
||||
clear(l);
|
||||
until(l,AT,5000,()->layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
||||
until(l,AT,20000,()->l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
|
||||
until(l,AT,20000,()->l.getBlockState(AT).isAir()&&!tracked(l,AT),"Natural column vanishes without water; receipt removed");
|
||||
|
||||
var built=new BlockPos(7,80,4);l.setBlockAndUpdate(built,Blocks.SNOW_BLOCK.defaultBlockState());step(l,built,64);check(layers(l,built)==8,"Untracked snow block preserved");
|
||||
var old=new BlockPos(8,80,4);snow(l,old,5);step(l,old,64);check(layers(l,old)==5,"Untracked old layers preserved");
|
||||
var base=new BlockPos(9,80,4);snow(l,base,3);deposit(l,base,5);clear(l);until(l,base,20000,()->layers(l,base)==3&&!tracked(l,base),"Weather above manual layers melts only back to its base");
|
||||
var atop=new BlockPos(9,80,6);l.setBlockAndUpdate(atop,Blocks.SNOW_BLOCK.defaultBlockState());deposit(l,atop,8);clear(l);until(l,atop,20000,()->layers(l,atop)==8&&l.getBlockState(atop.above()).isAir(),"Weather on a snow construction melts; construction stays");
|
||||
var replaced=new BlockPos(10,80,4);deposit(l,replaced,8);l.setBlockAndUpdate(replaced,Blocks.AIR.defaultBlockState());l.setBlockAndUpdate(replaced,Blocks.SNOW_BLOCK.defaultBlockState());clear(l);step(l,replaced,32);check(layers(l,replaced)==8&&!tracked(l,replaced),"Break and replacement cannot inherit melt permission");
|
||||
var thickened=new BlockPos(10,80,6);deposit(l,thickened,2);snow(l,thickened,6);clear(l);step(l,thickened,32);check(layers(l,thickened)==6&&!tracked(l,thickened),"Manual layer edit relinquishes natural provenance");
|
||||
var capped=new BlockPos(12,80,6);deposit(l,capped,8);snow(l,capped.above(),1);clear(l);step(l,capped,32);check(layers(l,capped)==8&&layers(l,capped.above())==1,"Manual thin snow cap protects its natural support");
|
||||
var roof=new BlockPos(11,80,4);deposit(l,roof,8);l.setBlockAndUpdate(roof.above(3),Blocks.STONE.defaultBlockState());clear(l);step(l,roof,32);check(layers(l,roof)==8,"Covered snow remains untouched");l.setBlockAndUpdate(roof.above(3),Blocks.AIR.defaultBlockState());until(l,roof,10000,()->layers(l,roof)==7,"Uncovered natural snow resumes melt");
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt false");step(l,roof,32);check(layers(l,roof)==7,"Operator disables melting");command(l,"gamerule sanctuary:seasonal_snow_melt true");
|
||||
clock(l,"2026-01-10T12:00:00Z",SeasonalWeather.Season.WINTER);clear(l);step(l,roof,64);check(layers(l,roof)==7,"Winter never melts even with clear weather");
|
||||
clock(l,"2026-04-10T12:00:00Z",SeasonalWeather.Season.SPRING);clear(l);until(l,roof,30000,()->l.getBlockState(roof).isAir(),"Spring progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);until(l,roof,50000,()->l.getBlockState(roof).isAir(),"Autumn progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);
|
||||
command(l,"sanctuary weather snow");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Forced summer snowfall blocks melting");
|
||||
command(l,"sanctuary weather snow_showers");l.getWeatherData().setRaining(false);l.setRainLevel(0);step(l,roof,64);check(layers(l,roof)==8,"Pauses in snow showers also block melting");
|
||||
clear(l);command(l,"sanctuary weather vanilla");step(l,roof,64);check(layers(l,roof)==8,"Vanilla mode does not apply seasonal melt");
|
||||
var cold=new BlockPos(-4,80,-4);deposit(l,cold,8);command(l,"fillbiome -8 76 -8 -1 88 -1 minecraft:snowy_plains");clear(l);step(l,cold,64);check(layers(l,cold)==8,"Permanently cold biome retains natural snow");
|
||||
deposit(l,PERSIST,16);clear(l);command(l,"gamerule sanctuary:seasonal_snow_melt false");
|
||||
check(tracked(l,PERSIST)&&tracked(l,PERSIST.above()),"Both persisted blocks have weather provenance");
|
||||
});
|
||||
}
|
||||
try(var world=saved.open()) {
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active(),1200);
|
||||
world.getConnection().waitForChunksDownload();c.waitTicks(20);
|
||||
world.getServer().runOnServer(server->{
|
||||
var l=server.overworld();clock(l,"2026-07-10T12:00:00Z",SeasonalWeather.Season.SUMMER);clear(l);
|
||||
check(!l.getGameRules().get(SeasonalSnowMelt.ENABLED),"Melt rule persists across restart");
|
||||
check(layers(l,PERSIST)==8&&layers(l,PERSIST.above())==8&&tracked(l,PERSIST)&&tracked(l,PERSIST.above()),"Chunk attachment survives native save/reload");
|
||||
command(l,"gamerule sanctuary:seasonal_snow_melt true");until(l,PERSIST,30000,()->l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST), "Loaded natural snow melts and clears provenance");
|
||||
check(l.getBlockState(PERSIST).isAir()&&l.getBlockState(PERSIST.above()).isAir()&&!tracked(l,PERSIST),"Loaded natural snow melts and clears provenance: base="+l.getBlockState(PERSIST)+", top="+l.getBlockState(PERSIST.above())+", sky="+l.canSeeSky(PERSIST.above(2))+", active="+WeatherService.active(l)+", biome="+l.getBiome(PERSIST)+", season="+WeatherService.season(server));
|
||||
check(layers(l,new BlockPos(7,80,4))==8&&layers(l,new BlockPos(9,80,4))==3,"Built and preexisting snow preserved across restart");
|
||||
});
|
||||
}
|
||||
System.out.println("MELT109_NATIVE_PASS: accumulation regression, top-down melt, seasons, profiles, cold biomes, constructions, edit invalidation, gamerule, native chunk save/reload");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void deposit(ServerLevel l,BlockPos pos,int count){l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,l.getServer());command(l,"sanctuary weather snow");l.getWeatherData().setRaining(true);l.setRainLevel(1);int target=depth(l,pos)+count;until(l,pos,Math.max(1000,count*200),()->depth(l,pos)>=target,"Weather snow reaches depth "+target+" at "+pos);l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,l.getServer());}
|
||||
private static void clear(ServerLevel l){command(l,"sanctuary weather clear");l.getWeatherData().setRaining(false);l.setRainLevel(0);}
|
||||
private static void clock(ServerLevel l,String time,SeasonalWeather.Season expected){try{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(l.getServer()),Clock.fixed(Instant.parse(time),ZoneOffset.UTC));check(WeatherService.season(l.getServer())==expected,"Test calendar: "+expected);}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
|
||||
private static boolean tracked(ServerLevel l,BlockPos p){var m=l.getChunkAt(p).getAttached(SeasonalSnowMelt.NATURAL);return m!=null&&m.containsKey(Long.toString(p.asLong()));}
|
||||
private static void snow(ServerLevel l,BlockPos p,int layers){l.setBlockAndUpdate(p,Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS,layers));}
|
||||
private static int layers(ServerLevel l,BlockPos p){return SeasonalSnowMelt.layers(l.getBlockState(p));}
|
||||
private static int depth(ServerLevel l,BlockPos p){int total=0;for(int i=0;i<32;i++){int layers=layers(l,p.above(i));if(layers==0)break;total+=layers;}return total;}
|
||||
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
|
||||
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+3
-3
@@ -59,10 +59,10 @@ public final class Menus083ClientChecks implements FabricClientGameTest {
|
||||
click(c,"sanctuary.arena.title");c.waitFor(m->m.gui.screen() instanceof ArenaScreen&&ArenaClient.view!=null,100);
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Arena returns to pause"));
|
||||
c.runOnClient(m->{ArenaClient.open(m.gui.screen());m.gui.screen().onClose();});c.waitTicks(8);c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Late arena response does not reopen it"));
|
||||
click(c,"sanctuary.progression.progress");c.runOnClient(m->{check(find(m.gui.screen(),"sanctuary.arena.title")==null,"No combat in progression");check(find(m.gui.screen(),"sanctuary.cycle.faction")==null,"No faction in progression");for(String tab:List.of("cycle","log"))button(m.gui.screen(),"sanctuary.cycle."+tab);button(m.gui.screen(),"gui.advancements");});
|
||||
click(c,"sanctuary.progression.progress");c.runOnClient(m->{check(find(m.gui.screen(),"sanctuary.arena.title")==null,"No combat in progression");check(find(m.gui.screen(),"sanctuary.cycle.faction")==null,"No faction in progression");for(String tab:List.of("cycle","log"))button(m.gui.screen(),"sanctuary.cycle."+tab);check(widgets(m.gui.screen()).anyMatch(InlineAdvancements.class::isInstance),"Embedded native tree");});
|
||||
c.takeScreenshot("menus083-progress-"+locale+"-"+scale);
|
||||
c.runOnClient(m->{var screen=(ProgressionScreen)m.gui.screen();check(button(screen,"sanctuary.cycle.cycle").getY()>=screen.contentTop+12,"XP has separate line");check(button(screen,"gui.advancements").getX()<screen.width/2,"Advancements above left skills");});
|
||||
click(c,"gui.advancements");trackDiet(c);c.runOnClient(m->check(m.gui.screen() instanceof net.minecraft.client.gui.screens.advancements.AdvancementsScreen,"Full native advancements"));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof ProgressionScreen,"Advancements returns to progression"));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
c.runOnClient(m->{var screen=(ProgressionScreen)m.gui.screen();check(button(screen,"sanctuary.cycle.cycle").getY()>=screen.contentTop+12,"XP has separate line");check(widgets(screen).filter(InlineAdvancements.class::isInstance).map(InlineAdvancements.class::cast).findFirst().orElseThrow().getX()<screen.width/2,"Advancements below left skills");});
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});
|
||||
click(c,"sanctuary.progression.blocks");click(c,"sanctuary.discovery.recipes");click(c,"sanctuary.recipes.buy");c.waitFor(m->RecipeClient.knowledge!=null&&RecipeClient.knowledge.learned(),100);
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Physical workshop, selection boundary, native paid construction and reversible nine-cell assembly. */
|
||||
public final class Metatable096ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos O=new BlockPos(15,80,15);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Metatable096 "+UUID.randomUUID());s.setSeed("96");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);fr.koka.sanctuary.companion.CompanionService.save(p,fr.koka.sanctuary.companion.CompanionService.record(p).note("seen_mobs","creeper"));var l=p.level();p.setGameMode(GameType.CREATIVE);p.setNoGravity(true);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
for(int x=9;x<27;x++)for(int z=9;z<27;z++)l.setBlockAndUpdate(new BlockPos(x,80,z),Blocks.STONE.defaultBlockState());
|
||||
for(var pos:Metatable.members(O))l.setBlockAndUpdate(pos,Blocks.CRAFTING_TABLE.defaultBlockState());
|
||||
p.getInventory().setItem(p.getInventory().getSelectedSlot(),ItemStack.EMPTY);home(p);
|
||||
});
|
||||
c.waitFor(m->m.level.getBlockState(O).is(Blocks.CRAFTING_TABLE)&&m.player.getY()>80,200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.options.bobView().set(false);m.getWindow().setWindowed(1280,800);});
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==null,"K cannot open catalogue without visiting workshop"));
|
||||
server.runOnServer(s->{var p=player(s);Metatable.wrench(p,O);check(Metatable.state(p.level(),O,false)==1,"First click only previews");Metatable.wrench(p,O.offset(1,0,1));check(Metatable.state(p.level(),O,true)==1,"Second click assembles nine parts");});
|
||||
c.waitFor(m->Metatable.state(m.level,O,true)==1,200);
|
||||
open(c);c.runOnClient(m->{check(((PlansScreen)m.gui.screen()).isWorkshop(),"Physical interaction opens workshop");for(String t:List.of("statue","machines","buildings","library"))button(m.gui.screen(),"sanctuary.plans.tab."+t);});
|
||||
c.takeScreenshot("metabli096-catalog-en");
|
||||
c.runOnClient(m->{for(String category:List.of("recent","infrastructure","decoration"))button(m.gui.screen(),"sanctuary.plans.tab."+category);});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.tab.decoration").onPress(enter()));c.takeScreenshot("catalog099-decoration-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.tab.statue").onPress(enter()));c.takeScreenshot("catalog099-statues-en");
|
||||
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.plans.tab.machines").onPress(enter());});c.takeScreenshot("metabli096-machines-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.catalog.piston").onPress(enter()));
|
||||
c.waitFor(m->m.gui.screen()==null&&PlanClient.plan!=null,100);
|
||||
c.runOnClient(m->check(PlanClient.plan.cells().size()==9&&!MetatableClient.available(),"Selection closes catalogue and releases selection access"));
|
||||
cockpit(c);
|
||||
var selected=new AtomicReference<BuildPlan>();
|
||||
c.runOnClient(m->{var screen=(PlansScreen)m.gui.screen();check(!screen.isWorkshop(),"K opens only field controls");
|
||||
check(widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).startsWith("sanctuary.plans.tab.")||Set.of("sanctuary.plans.clear","sanctuary.plans.generate").contains(key(b))),"No remote catalogue or abandon controls");
|
||||
selected.set(PlanClient.plan);var origin=PlanClient.origin;button(screen,"sanctuary.plans.move.east").onPress(enter());check(PlanClient.origin.equals(origin.east()),"Move by one block");
|
||||
button(screen,"sanctuary.plans.rotate").onPress(enter());check(PlanClient.plan.width()==1&&PlanClient.plan.depth()==3,"Rotation transforms footprint");
|
||||
for(int i=0;i<3;i++)PlanClient.rotate();check(PlanClient.plan.equals(selected.get()),"Four rotations restore plan");
|
||||
PlanClient.catalog(ConstructionCatalog.ENTRIES.getFirst());check(PlanClient.plan.equals(selected.get()),"Remote selection refused");
|
||||
screen.onFilesDrop(List.of(PlanClient.directory().resolve("missing.schem")));check(!PlanClient.busy,"Remote import refused before reading files");
|
||||
});
|
||||
c.takeScreenshot("metabli096-controls-en");close(c);
|
||||
// Tap K anchors at the actual 96-block ray hit, repeat rotates around that same anchor.
|
||||
c.getInput().lookAt(new BlockPos(23,80,23));c.waitTicks(3);
|
||||
var tapped=new AtomicReference<BlockPos>();
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitTicks(3);
|
||||
c.runOnClient(m->{check(m.gui.screen()==null,"K tap never opens cockpit");var hit=(BlockHitResult)m.player.pick(96,1,false);var target=hit.getBlockPos().relative(hit.getDirection());check(PlanClient.origin.equals(target.offset(-PlanClient.plan.width()/2,0,-PlanClient.plan.depth()/2)),"K positions at the looked-at face, not near the player");tapped.set(PlanClient.origin);selected.set(PlanClient.plan);});
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitTicks(3);
|
||||
c.runOnClient(m->check(PlanClient.plan.equals(selected.get().rotate()),"Second K on same point rotates exactly once"));
|
||||
c.getInput().lookAt(new BlockPos(16,250,13));c.waitTicks(3);c.runOnClient(m->tapped.set(PlanClient.origin));
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitTicks(3);c.runOnClient(m->check(PlanClient.origin.equals(tapped.get()),"Looking into the sky keeps the old placement"));
|
||||
cockpit(c);c.runOnClient(m->check(PlanClient.origin.equals(tapped.get()),"Holding K opens controls without moving or rotating"));close(c);
|
||||
|
||||
// Walking away from the workstation never grants catalogue access through K.
|
||||
server.runOnServer(s->player(s).teleportTo(24,82,24));c.waitFor(m->m.player.getX()>23,100);
|
||||
cockpit(c);c.runOnClient(m->check(!((PlansScreen)m.gui.screen()).isWorkshop(),"Field options work away from the table"));close(c);
|
||||
server.runOnServer(s->home(player(s)));c.waitFor(m->m.player.getZ()<15,100);open(c);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.tab.infrastructure").onPress(enter()));c.takeScreenshot("metabli096-buildings-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.catalog.footbridge").onPress(enter()));c.waitFor(m->m.gui.screen()==null&&PlanClient.plan.depth()==7,100);
|
||||
c.runOnClient(m->{PlanClient.origin=new BlockPos(20,81,15);PlanClient.refresh();check(PlanClient.completed==0,"Preview changes no world blocks");});
|
||||
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.SURVIVAL);p.setNoGravity(false);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Items.OAK_PLANKS,2));p.teleportTo(20.5,81,13.5);});
|
||||
c.waitFor(m->!m.player.isCreative()&&m.player.getX()>20&&m.player.getMainHandItem().is(Items.OAK_PLANKS),150);
|
||||
c.getInput().lookAt(new BlockPos(20,80,15));c.waitTicks(3);c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
|
||||
c.waitFor(m->PlanClient.completed==1,150);
|
||||
server.runOnServer(s->check(player(s).getMainHandItem().getCount()==1,"Native manual placement consumes one object"));
|
||||
c.takeScreenshot("metabli096-manual-preview");
|
||||
// A catalogue visit is required again. Closing while a statue computes cancels that selection.
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().setItem(p.getInventory().getSelectedSlot(),ItemStack.EMPTY);home(p);});
|
||||
c.waitFor(m->m.player.getX()<18&&m.player.getMainHandItem().isEmpty(),100);open(c);
|
||||
c.runOnClient(m->{selected.set(PlanClient.plan);configure(m.gui.screen());button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter());m.gui.screen().onClose();});
|
||||
c.waitFor(m->!PlanClient.busy,400);c.runOnClient(m->check(PlanClient.plan==selected.get(),"Closing workshop discards asynchronous new selection"));
|
||||
open(c);c.runOnClient(m->{configure(m.gui.screen());button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter());});
|
||||
c.waitFor(m->!PlanClient.busy&&PlanClient.plan!=selected.get()&&m.gui.screen()==null,400);
|
||||
c.runOnClient(m->check(PlanClient.plan.height()==32&&PlanClient.plan.cells().size()>100,"Statue generation is available at the workshop"));
|
||||
open(c);language(c,"fr_fr");c.takeScreenshot("metabli096-catalog-fr");
|
||||
close(c);
|
||||
server.runOnServer(s->{var p=player(s);var holder=s.getAdvancements().get(net.minecraft.resources.Identifier.withDefaultNamespace("story/mine_stone"));for(String criterion:holder.value().criteria().keySet())p.getAdvancements().award(holder,criterion);});
|
||||
c.waitTicks(10);c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(null)));c.waitTicks(6);c.takeScreenshot("catalog099-progression-fr");
|
||||
c.runOnClient(m->{var screen=m.gui.screen();check(widgets(screen).anyMatch(InlineAdvancements.class::isInstance),"Native advancements embedded under skills");check(widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).equals("gui.advancements")),"No separate advancements navigation button");});
|
||||
c.runOnClient(m->{try{
|
||||
var embedded=widgets(m.gui.screen()).filter(InlineAdvancements.class::isInstance).map(InlineAdvancements.class::cast).findFirst().orElseThrow();
|
||||
var method=InlineAdvancements.class.getDeclaredMethod("tab");method.setAccessible(true);var tab=(net.minecraft.client.gui.screens.advancements.AdvancementTab)method.invoke(embedded);
|
||||
var access=(fr.koka.sanctuary.mixin.client.NotificationAdvancementTabAccess)tab;
|
||||
var node=access.sanctuary$widgets().values().stream().filter(w->NotificationClient.canTrack(w.getAdvancement())).findFirst().orElseThrow();
|
||||
tab.scroll(100-node.getX()-access.sanctuary$scrollX(),45-node.getY()-access.sanctuary$scrollY());
|
||||
float scale=Math.min(1,(embedded.getWidth()-4)/234f);int x=embedded.getX()+(embedded.getWidth()-Math.round(234*scale))/2,y=embedded.getY()+27;
|
||||
var event=new net.minecraft.client.input.MouseButtonEvent(x+(Math.floor(access.sanctuary$scrollX())+node.getX()+12)*scale,y+(Math.floor(access.sanctuary$scrollY())+node.getY()+12)*scale,new net.minecraft.client.input.MouseButtonInfo(1,0));
|
||||
boolean before=NotificationClient.isPinned(node.getAdvancement().id());m.gui.screen().mouseClicked(event,false);m.gui.screen().mouseReleased(event);
|
||||
check(NotificationClient.isPinned(node.getAdvancement().id())!=before,"Embedded tree right-click toggles notification tracking");
|
||||
}catch(ReflectiveOperationException e){throw new AssertionError(e);}});
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(5);c.takeScreenshot("catalog099-progression-scale3-fr");
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});close(c);open(c);
|
||||
|
||||
// Export/import remains available, but file drops only select at the workshop.
|
||||
c.runOnClient(m->PlanClient.export());c.waitFor(m->!PlanClient.busy,200);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.plans.tab.library").onPress(enter());});c.takeScreenshot("metabli096-library-fr");
|
||||
c.runOnClient(m->{try(var files=java.nio.file.Files.list(PlanClient.directory())){
|
||||
var path=files.filter(p->p.toString().endsWith(".litematic")).findFirst().orElseThrow();((PlansScreen)m.gui.screen()).onFilesDrop(List.of(path));
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}});
|
||||
c.waitFor(m->!PlanClient.busy&&m.gui.screen()==null,300);
|
||||
cockpit(c);c.takeScreenshot("metabli096-controls-fr");close(c);
|
||||
// Breaking the workshop while its menu is open invalidates selection, but retains the player's plan.
|
||||
open(c);c.runOnClient(m->selected.set(PlanClient.plan));
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(O,true);
|
||||
check(Metatable.members(O).stream().filter(q->!q.equals(O)).allMatch(q->p.level().getBlockState(q).is(Blocks.CRAFTING_TABLE)),"Eight remaining parts restore without drops");
|
||||
int drops=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(O).inflate(4)).stream().filter(e->e.getItem().is(Items.CRAFTING_TABLE)).mapToInt(e->e.getItem().getCount()).sum();check(drops==1,"Broken component drops one crafting table");
|
||||
});
|
||||
c.waitFor(m->m.gui.screen()==null,100);c.runOnClient(m->check(PlanClient.plan==selected.get(),"Table destruction leaves existing preview intact"));
|
||||
server.runOnServer(s->{var p=player(s);p.level().setBlockAndUpdate(O,Blocks.CRAFTING_TABLE.defaultBlockState());check(Metatable.assemble(p,O),"Reassemble");});
|
||||
c.waitFor(m->Metatable.state(m.level,O,true)==1,100);
|
||||
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.CREATIVE);p.setNoGravity(true);p.getAbilities().flying=true;p.onUpdateAbilities();p.teleportTo(21.5,84,12.5);});
|
||||
c.waitTicks(8);c.getInput().lookAt(O.offset(1,0,1));c.waitTicks(10);c.runOnClient(m->{PlanClient.visible=false;if(!m.gui.hud.isHidden())m.gui.hud.toggle();});c.waitTicks(8);c.takeScreenshot("metabli096-textures");
|
||||
server.runOnServer(s->player(s).teleportTo(400,90,400));c.waitFor(m->m.level.getChunkSource().getChunkNow(O.getX()>>4,O.getZ()>>4)==null,600);
|
||||
server.runOnServer(s->home(player(s)));c.waitFor(m->Metatable.state(m.level,O,true)==1,500);
|
||||
server.waitFor(s->Metatable.state(player(s).level(),O,true)==1,200);
|
||||
server.runOnServer(s->{Metatable.dissolve(player(s).level(),O);check(Metatable.state(player(s).level(),O,false)==1,"Dissociation restores all nine original blocks");});
|
||||
server.runOnServer(s->{
|
||||
var l=player(s).level();
|
||||
for(boolean desert:new boolean[]{true,false}){
|
||||
var holder=l.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.STRUCTURE).getOrThrow(net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.STRUCTURE,net.minecraft.resources.Identifier.withDefaultNamespace(desert?"desert_pyramid":"jungle_pyramid")));
|
||||
net.minecraft.world.level.levelgen.structure.StructurePiece piece=desert?new net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece(net.minecraft.util.RandomSource.create(97),0,0):new net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece(net.minecraft.util.RandomSource.create(97),0,0);
|
||||
var start=new net.minecraft.world.level.levelgen.structure.StructureStart(holder.value(),new ChunkPos(0,0),0,new net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer(List.of(piece)));
|
||||
((fr.koka.sanctuary.expansion.NativeStructureMetadata23)(Object)start).sanctuary$native23(new fr.koka.sanctuary.expansion.NativeStructureMetadata23.Data(97,-64));
|
||||
Temple097ClientChecks.foundationChecks(l,start);
|
||||
}
|
||||
var p=player(s);p.teleportTo(18.5,84,8);p.setDeltaMovement(Vec3.ZERO);
|
||||
});
|
||||
c.waitTicks(10);
|
||||
c.runOnClient(m->{
|
||||
var states=List.of(Blocks.COBBLESTONE.defaultBlockState(),Blocks.OAK_PLANKS.defaultBlockState(),Blocks.BRICKS.defaultBlockState(),Blocks.OAK_LOG.defaultBlockState(),Blocks.FURNACE.defaultBlockState(),Blocks.CHEST.defaultBlockState(),Blocks.OAK_STAIRS.defaultBlockState(),Blocks.OAK_LEAVES.defaultBlockState());
|
||||
var cells=new ArrayList<BuildPlan.Cell>();for(int i=0;i<states.size();i++)cells.add(new BuildPlan.Cell(new BlockPos(i,0,0),states.get(i)));
|
||||
PlanClient.select(new BuildPlan("Textures",8,1,1,cells),Map.of());PlanClient.origin=new BlockPos(15,82,15);PlanClient.refresh();
|
||||
if(m.gui.hud.isHidden())m.gui.hud.toggle();check(PlanClient.preview.size()==8,"Every material has a textured preview cell");
|
||||
});
|
||||
c.getInput().lookAt(new BlockPos(19,82,15));c.waitTicks(10);c.takeScreenshot("preview097-materials-fr");
|
||||
c.runOnClient(m->{PlanClient.rotate();check(PlanClient.plan.width()==1&&PlanClient.plan.depth()==8,"Native textured block states rotate with the plan");PlanClient.visible=false;});
|
||||
c.waitTicks(5);c.takeScreenshot("preview097-hidden");
|
||||
System.out.println("PREVIEW097_PASS: native textured materials, stairs and container models, rotation, visibility; optional temple foundation clipping and NBT for desert and jungle");
|
||||
language(c,"en_us");
|
||||
System.out.println("METATABLE096_PASS: two-click assembly, four-chunk sync, physical catalogue, one selection, field controls, no remote import/switch, native paid placement, asynchronous cancellation, statue + import, FR/EN, destruction/drop/reassembly/unload/dissociation");
|
||||
}
|
||||
}
|
||||
private static void home(ServerPlayer p){p.teleportTo(16.5,81,13.8);p.setDeltaMovement(Vec3.ZERO);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void open(ClientGameTestContext c){
|
||||
c.runOnClient(m->{var at=O.offset(1,0,0);m.gameMode.useItemOn(m.player,net.minecraft.world.InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(at).add(0,.5,0),Direction.UP,at,false));});
|
||||
c.waitFor(m->m.gui.screen() instanceof PlansScreen screen&&screen.isWorkshop(),150);
|
||||
}
|
||||
private static void close(ClientGameTestContext c){c.runOnClient(m->{if(m.gui.screen()!=null)m.gui.screen().onClose();});}
|
||||
private static void cockpit(ClientGameTestContext c){c.getInput().holdKey(InputConstants.KEY_K);c.waitTicks(12);c.getInput().releaseKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);}
|
||||
private static void configure(Screen screen){button(screen,"sanctuary.plans.tab.statue").onPress(enter());button(screen,"sanctuary.plans.configure").onPress(enter());widgets(screen).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("minecraft:creeper");}
|
||||
private static void language(ClientGameTestContext c,String lang){var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(lang);return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();}
|
||||
private static Stream<GuiEventListener> widgets(GuiEventListener node){return Stream.concat(Stream.of(node),node instanceof ContainerEventHandler c?c.children().stream().flatMap(Metatable096ClientChecks::widgets):Stream.empty());}
|
||||
private static String key(Button b){return b.getMessage().getContents() instanceof TranslatableContents t?t.getKey():b.getMessage().getString();}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing button "+key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+50
-10
@@ -3,6 +3,7 @@ package fr.koka.sanctuary.client;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import fr.koka.sanctuary.multiblock.Metatable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.util.stream.Stream;
|
||||
@@ -56,13 +57,16 @@ public final class Plans072ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
require(supported.containsAll(List.of("minecraft:creeper","minecraft:cow","minecraft:pig","minecraft:chicken","minecraft:zombie","minecraft:sheep")),"Common native models are supported");
|
||||
System.out.println("PLANS072 models="+supported.size()+" refused="+refused+" capture_ms="+(System.nanoTime()-started)/1_000_000);
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var o=new BlockPos(0,9,2);for(var q:Metatable.members(o))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());fr.koka.sanctuary.companion.CompanionService.save(p,fr.koka.sanctuary.companion.CompanionService.record(p).note("seen_mobs","creeper"));require(Metatable.assemble(p,o),"Assemble the physical workshop");});
|
||||
c.waitFor(m->Metatable.state(m.level,new BlockPos(0,9,2),true)==1,150);
|
||||
server.runOnServer(s->Metatable.open(s.getPlayerList().getPlayers().getFirst(),new BlockPos(0,9,2)));c.waitForScreen(PlansScreen.class);
|
||||
c.takeScreenshot("plans072-create-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter()));
|
||||
c.runOnClient(m->{configure(m.gui.screen());button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter());});
|
||||
c.waitFor(m->PlanClient.plan!=null&&!PlanClient.busy,400);
|
||||
c.runOnClient(m->{require(PlanClient.plan.height()==32,"Requested model height");require(PlanClient.plan.cells().size()>100,"Detailed geometry becomes real material cells");
|
||||
PlanClient.origin=new BlockPos(10,2,10);PlanClient.refresh();
|
||||
});
|
||||
cockpit(c);
|
||||
c.takeScreenshot("plans072-plan-en");
|
||||
var expected=new AtomicReference<BuildPlan>();c.runOnClient(m->expected.set(PlanClient.plan));
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.rotate").onPress(enter()));
|
||||
@@ -74,19 +78,49 @@ public final class Plans072ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitFor(m->m.player.distanceToSqr(15.5,15,55.5)<2,100);
|
||||
c.getInput().lookAt(new BlockPos(15,18,17));c.waitTicks(15);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);c.takeScreenshot("plans072-ghost-creeper");c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.paste").onPress(enter()));c.waitForScreen(ConfirmScreen.class);
|
||||
c.clickScreenButton("gui.yes");
|
||||
cockpit(c);
|
||||
// Creative builds use the same native controls and confirmation as a player.
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.paste").onPress(enter()));
|
||||
c.waitForScreen(ConfirmScreen.class);c.takeScreenshot("plans098-confirm-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"gui.no").onPress(enter()));
|
||||
c.waitForScreen(PlansScreen.class);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(p.level().getBlockState(new BlockPos(10,2,10).offset(expected.get().cells().getFirst().pos())).isAir(),"Cancel leaves plan unbuilt");});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.paste").onPress(enter()));
|
||||
c.waitForScreen(ConfirmScreen.class);
|
||||
c.runOnClient(m->button(m.gui.screen(),"gui.yes").onPress(enter()));
|
||||
c.waitFor(m->PlanClient.completed==PlanClient.plan.cells().size(),400);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(var cell:expected.get().cells())require(p.level().getBlockState(new BlockPos(10,2,10).offset(cell.pos()))==cell.state(),"Server pasted every exact material");});
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.getInput().pressKey(InputConstants.KEY_F1);c.waitTicks(10);c.takeScreenshot("plans072-built-creeper");c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
// Select real machine/building catalogue entries, then build through K.
|
||||
for(String id:List.of("fourneau","shelter")){
|
||||
c.waitTicks(25); // Respect the server's one-second upload rate limit.
|
||||
var origin=new BlockPos(id.equals("fourneau")?30:40,4,10);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(.5,10,.5);});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,10,.5)<1,100);
|
||||
server.runOnServer(s->Metatable.open(s.getPlayerList().getPlayers().getFirst(),new BlockPos(0,9,2)));c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.plans.tab."+(id.equals("fourneau")?"machines":"buildings")).onPress(enter());button(m.gui.screen(),"sanctuary.plans.catalog."+id).onPress(enter());});
|
||||
c.waitFor(m->m.gui.screen()==null,100);
|
||||
c.runOnClient(m->{PlanClient.origin=origin;PlanClient.rotate();PlanClient.layer=0;PlanClient.refresh();expected.set(PlanClient.plan);});
|
||||
cockpit(c);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.paste").onPress(enter()));c.waitForScreen(ConfirmScreen.class);
|
||||
c.runOnClient(m->button(m.gui.screen(),"gui.yes").onPress(enter()));
|
||||
c.waitFor(m->PlanClient.completed==PlanClient.plan.cells().size(),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(var cell:expected.get().cells())require(p.level().getBlockState(origin.offset(cell.pos()))==cell.state(),"Full rotated catalogue plan built: "+id);});
|
||||
}
|
||||
// Losing creative while the confirmation is open must neither build nor keep its button.
|
||||
c.runOnClient(m->{PlanClient.select(new BuildPlan("Mode change",1,1,1,List.of(new BuildPlan.Cell(BlockPos.ZERO,Blocks.STONE.defaultBlockState()))),Map.of());PlanClient.origin=new BlockPos(4,2,4);PlanClient.refresh();});
|
||||
cockpit(c);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.paste").onPress(enter()));c.waitForScreen(ConfirmScreen.class);
|
||||
// A real survival connection must reject an upload, even when sent without the GUI.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(GameType.SURVIVAL);p.getAbilities().invulnerable=true;});
|
||||
c.waitFor(m->!m.player.isCreative(),100);
|
||||
c.runOnClient(m->button(m.gui.screen(),"gui.yes").onPress(enter()));c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->{require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).equals("sanctuary.plans.paste")),"Creative button disappears after mode change");m.gui.screen().onClose();});
|
||||
server.runOnServer(s->require(s.getPlayerList().getPlayers().getFirst().level().getBlockState(new BlockPos(4,2,4)).isAir(),"No build after losing creative"));
|
||||
c.runOnClient(m->{var tiny=new BuildPlan("Survival",1,1,1,List.of(new BuildPlan.Cell(BlockPos.ZERO,Blocks.STONE.defaultBlockState())));PlanClient.select(tiny,Map.of());PlanClient.origin=new BlockPos(4,2,4);PlanClient.refresh();
|
||||
ClientPlayNetworking.send(new PlanPayload(UUID.randomUUID(),PlanClient.origin,1,1,1,1,0,tiny.cells()));});
|
||||
c.waitTicks(30);server.runOnServer(s->require(s.getPlayerList().getPlayers().getFirst().level().getBlockState(new BlockPos(4,2,4)).isAir(),"Survival spoofed paste leaves the world untouched"));
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);
|
||||
cockpit(c);
|
||||
c.runOnClient(m->require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).equals("sanctuary.plans.paste")),"Survival UI only offers construction guidance"));
|
||||
language(c,"fr_fr");c.takeScreenshot("plans072-survie-fr");
|
||||
c.runOnClient(m->{PlanClient.layer=0;PlanClient.refresh();require(PlanClient.completed==0,"Free preview does not complete a block");});
|
||||
@@ -97,15 +131,21 @@ public final class Plans072ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
c.waitFor(m->m.player.distanceToSqr(4.5,1,3.5)<1&&m.player.getMainHandItem().is(Items.STONE),150);
|
||||
c.getInput().lookAt(new BlockPos(4,1,4));c.waitTicks(3);
|
||||
c.runOnClient(m->System.out.println("PLANS072 survival hit="+m.hitResult+" player="+m.player.position()+" origin="+PlanClient.origin+" held="+m.player.getMainHandItem()));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
|
||||
c.runOnClient(m->System.out.println("PLANS072 survival hit="+m.hitResult+" target="+(m.hitResult instanceof net.minecraft.world.phys.BlockHitResult h?h.getBlockPos()+" "+h.getDirection():"none")+" player="+m.player.position()+" origin="+PlanClient.origin+" held="+m.player.getMainHandItem()));
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,net.minecraft.world.InteractionHand.MAIN_HAND,
|
||||
new net.minecraft.world.phys.BlockHitResult(new Vec3(4.5,2,4.5),Direction.UP,new BlockPos(4,1,4),false)));
|
||||
c.waitFor(m->PlanClient.completed==1,150);
|
||||
server.runOnServer(s->require(s.getPlayerList().getPlayers().getFirst().getMainHandItem().getCount()==1,"Survival native placement consumes exactly one block"));
|
||||
c.runOnClient(m->{PlanClient.clear();m.gui.setScreen(new PlansScreen(null));});c.takeScreenshot("plans072-create-fr");
|
||||
c.runOnClient(m->PlanClient.clear());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();p.teleportTo(.5,10,.5);});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,10,.5)<1,100);
|
||||
server.runOnServer(s->Metatable.open(s.getPlayerList().getPlayers().getFirst(),new BlockPos(0,9,2)));c.waitForScreen(PlansScreen.class);c.takeScreenshot("plans072-create-fr");
|
||||
language(c,"en_us");
|
||||
System.out.println("PLANS072 passed: native models + UV textures + active palette, creation widgets, 32-block statue, rotations, materials, gzip export, colored preview, real creative network paste, survival spoof rejection and paid native placement, FR/EN.");
|
||||
System.out.println("PLANS072 passed: native models + UV textures + active palette, creation widgets, 32-block statue, rotations, materials, gzip export, colored preview, creative confirmation/cancel, statue/machine/building construction, mode change rejection, survival spoof rejection and paid native placement, FR/EN.");
|
||||
}
|
||||
}
|
||||
private static void cockpit(ClientGameTestContext c){c.getInput().holdKey(InputConstants.KEY_K);c.waitTicks(12);c.getInput().releaseKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);}
|
||||
private static void configure(Screen screen){button(screen,"sanctuary.plans.tab.statue").onPress(enter());button(screen,"sanctuary.plans.configure").onPress(enter());widgets(screen).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("minecraft:creeper");}
|
||||
private static void language(ClientGameTestContext c,String id){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(id);return m.reloadResourcePacks();});
|
||||
c.waitFor(m->reload.isDone(),600);reload.join();c.waitFor(m->m.gui.overlay()==null,400);c.waitTicks(8);
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.packs.PackSelectionScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.repository.PackCompatibility;
|
||||
|
||||
/** Native screen/export, complete snapshot, real resource precedence and preservation of edits. */
|
||||
public final class ResourceTemplate121ClientChecks implements FabricClientGameTest {
|
||||
private static final String BUILTIN = "sanctuary:textures";
|
||||
private static final String PERSONAL = "file/" + ResourcePackTemplate.FOLDER;
|
||||
private static final String WRENCH = "sanctuary:textures/item/golden_wrench.png";
|
||||
private static final String WRENCH_PATH = "assets/sanctuary/textures/item/golden_wrench.png";
|
||||
private static final String WRENCH_SHA = "a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af";
|
||||
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
try { run(c); } catch (IOException error) { throw new AssertionError(error); }
|
||||
}
|
||||
|
||||
private void run(ClientGameTestContext c) throws IOException {
|
||||
var previous = c.computeOnClient(m -> m.gui.screen());
|
||||
var packDirectory = c.computeOnClient(Minecraft::getResourcePackDirectory);
|
||||
var selected = c.computeOnClient(m -> List.copyOf(m.getResourcePackRepository().getSelectedIds()));
|
||||
var target = packDirectory.resolve(ResourcePackTemplate.FOLDER);
|
||||
check(!Files.exists(target), "Fresh disposable client directory");
|
||||
c.runOnClient(m -> {
|
||||
check(source(m).equals(BUILTIN), "Built-in pack active");
|
||||
check(hash(resource(m, WRENCH)).equals(WRENCH_SHA), "Exact creator PNG active");
|
||||
m.getWindow().setWindowed(1280, 800);
|
||||
});
|
||||
for (int scale : new int[]{2, 3}) for (String language : List.of("fr_fr", "en_us")) {
|
||||
c.runOnClient(m -> {
|
||||
m.options.guiScale().set(scale); m.resizeGui();
|
||||
m.getLanguageManager().setSelected(language);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
screen(m, packDirectory);
|
||||
var button = button(m, "customize");
|
||||
check(button.getX() >= 0 && button.getRight() <= m.gui.screen().width, "Template button fits width");
|
||||
check(button.getY() > 60 && button.getBottom() < m.gui.screen().height, "Template button fits height");
|
||||
var done = m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> key(b, "gui.done")).findFirst().orElseThrow();
|
||||
check(done.getY() >= button.getBottom() + 4, "Separate native footer row");
|
||||
check(m.font.width(button.getMessage()) < button.getWidth() - 8, "Localized label fits");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("template121-create-" + language + "-" + scale);
|
||||
}
|
||||
c.runOnClient(m -> button(m, "customize").onPress(new KeyEvent(InputConstants.KEY_RETURN, 0, 0)));
|
||||
c.waitFor(m -> m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.anyMatch(b -> b.active && key(b, "sanctuary.resource_pack.open_copy")), 1200);
|
||||
c.waitFor(m -> m.getResourcePackRepository().getAvailableIds().contains(PERSONAL), 1200);
|
||||
c.runOnClient(m -> {
|
||||
check(List.copyOf(m.getResourcePackRepository().getSelectedIds()).equals(selected), "Creating never changes selection");
|
||||
check(m.getResourcePackRepository().getPack(PERSONAL).getCompatibility() == PackCompatibility.COMPATIBLE,
|
||||
"Template format accepted by exact Minecraft version");
|
||||
});
|
||||
var mod = FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
|
||||
Map<String, String> expected = new TreeMap<>();
|
||||
merge(expected, mod.findPath("assets").orElseThrow(), "assets/");
|
||||
merge(expected, mod.findPath("resourcepacks/textures/assets").orElseThrow(), "assets/");
|
||||
merge(expected, mod.findPath("resourcepacks/template").orElseThrow(), "");
|
||||
expected.put("pack.png", hash(Files.readAllBytes(mod.findPath("resourcepacks/textures/pack.png").orElseThrow())));
|
||||
check(snapshot(target).equals(expected), "Complete byte-identical snapshot with built-in precedence");
|
||||
Files.writeString(packDirectory.getParent().resolve("template121-export-sha256.json"),
|
||||
new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(expected));
|
||||
check(expected.containsKey("README.md") && expected.containsKey("LICENSE_sanctuary")
|
||||
&& expected.containsKey("THIRD_PARTY_NOTICES.md"), "Editable copy includes guide and credits");
|
||||
check(expected.keySet().stream().noneMatch(p -> p.startsWith("data/") || p.endsWith(".class")), "Client assets only");
|
||||
c.waitTicks(4); c.takeScreenshot("template121-copy-ready");
|
||||
c.runOnClient(m -> m.gui.setScreen(new Screen(Component.literal("Golden wrench")) {
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g, int x, int y, float delta) {
|
||||
g.fill(0, 0, width, height, 0xff292929);
|
||||
g.centeredText(font, Component.translatable("item.sanctuary.golden_wrench"), width / 2, 55, 0xffffffff);
|
||||
g.blit(RenderPipelines.GUI_TEXTURED, Identifier.parse(WRENCH), width / 2 - 48, 85, 0, 0, 96, 96, 16, 16, 16, 16);
|
||||
}
|
||||
}));
|
||||
c.waitTicks(4); c.takeScreenshot("template121-wrench-exact-texture");
|
||||
|
||||
byte[] replacement = c.computeOnClient(m -> resource(m, "minecraft:textures/item/golden_pickaxe.png"));
|
||||
Files.write(target.resolve(WRENCH_PATH), replacement);
|
||||
Files.writeString(target.resolve("my-notes.txt"), "Personal edits must survive all subsequent exports.");
|
||||
var edited = snapshot(target);
|
||||
check(ResourcePackTemplate.create(packDirectory).equals(target.toAbsolutePath().normalize()), "Existing copy returned");
|
||||
check(snapshot(target).equals(edited), "Second export preserves every edited byte and extra file");
|
||||
c.runOnClient(m -> {
|
||||
var repo = m.getResourcePackRepository(); repo.addPack(PERSONAL); m.options.updateResourcePacks(repo);
|
||||
});
|
||||
c.waitFor(m -> source(m).equals(PERSONAL) && m.gui.overlay() == null, 1200);
|
||||
c.runOnClient(m -> {
|
||||
check(Arrays.equals(resource(m, WRENCH), replacement), "Personal edit overrides built-in texture");
|
||||
var repo = m.getResourcePackRepository(); repo.reload(); m.options.loadSelectedResourcePacks(repo);
|
||||
check(repo.getSelectedIds().contains(PERSONAL), "Personal activation persists after rediscovery");
|
||||
screen(m, packDirectory); button(m, "open_copy");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("template121-personal-active");
|
||||
var reload = c.computeOnClient(Minecraft::reloadResourcePacks);
|
||||
c.waitFor(m -> reload.isDone() && m.gui.overlay() == null, 1200); reload.join();
|
||||
check(snapshot(target).equals(edited), "Reload never writes personal assets");
|
||||
c.runOnClient(m -> {
|
||||
check(Arrays.equals(resource(m, WRENCH), replacement), "Reload still uses personal PNG");
|
||||
var repo = m.getResourcePackRepository(); repo.removePack(PERSONAL); m.options.updateResourcePacks(repo);
|
||||
});
|
||||
c.waitFor(m -> source(m).equals(BUILTIN) && m.gui.overlay() == null, 1200);
|
||||
c.runOnClient(m -> {
|
||||
check(hash(resource(m, WRENCH)).equals(WRENCH_SHA), "Disabling restores creator texture");
|
||||
var repo = m.getResourcePackRepository(); repo.reload(); m.options.loadSelectedResourcePacks(repo);
|
||||
check(!repo.getSelectedIds().contains(PERSONAL), "Explicit opt-out retained");
|
||||
});
|
||||
|
||||
Path collision = Files.createTempDirectory(packDirectory.getParent(), "template121-collision-");
|
||||
Files.writeString(collision.resolve(ResourcePackTemplate.FOLDER), "Do not replace this file");
|
||||
try { ResourcePackTemplate.create(collision); throw new AssertionError("Existing file must be refused"); }
|
||||
catch (IOException expectedError) { check(Files.readString(collision.resolve(ResourcePackTemplate.FOLDER)).equals("Do not replace this file"), "Collision leaves file intact"); }
|
||||
Path datapacks = Files.createTempDirectory(packDirectory.getParent(), "template121-datapacks-");
|
||||
c.runOnClient(m -> {
|
||||
screen(m, datapacks);
|
||||
check(m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.noneMatch(b -> key(b, "sanctuary.resource_pack.customize") || key(b, "sanctuary.resource_pack.open_copy")), "No button in datapack selector");
|
||||
m.gui.setScreen(previous);
|
||||
});
|
||||
System.out.println("TEMPLATE121_PASS: " + expected.size() + " exact files; native button FR/EN at scales 2/3; complete copy; edits and selection preserved; actual override, reload and opt-out; collision safe; datapacks untouched");
|
||||
}
|
||||
|
||||
private static void screen(Minecraft m, Path directory) {
|
||||
m.gui.setScreen(new PackSelectionScreen(m.getResourcePackRepository(), repo -> {}, directory, Component.translatable("resourcePack.title")));
|
||||
}
|
||||
private static Button button(Minecraft m, String suffix) {
|
||||
return m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> key(b, "sanctuary.resource_pack." + suffix)).findFirst().orElseThrow();
|
||||
}
|
||||
private static boolean key(Button b, String key) { return b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key); }
|
||||
private static String source(Minecraft m) { return m.getResourceManager().getResource(Identifier.parse(WRENCH)).orElseThrow().sourcePackId(); }
|
||||
private static byte[] resource(Minecraft m, String name) {
|
||||
try (var in = m.getResourceManager().getResource(Identifier.parse(name)).orElseThrow().open()) { return in.readAllBytes(); }
|
||||
catch (IOException error) { throw new AssertionError(error); }
|
||||
}
|
||||
private static String hash(byte[] bytes) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
|
||||
catch (java.security.NoSuchAlgorithmException error) { throw new AssertionError(error); }
|
||||
}
|
||||
private static Map<String, String> snapshot(Path root) throws IOException { Map<String, String> result = new TreeMap<>(); merge(result, root, ""); return result; }
|
||||
private static void merge(Map<String, String> result, Path root, String prefix) throws IOException {
|
||||
try (var files = Files.walk(root)) { for (Path file : files.filter(Files::isRegularFile).toList()) result.put(prefix + root.relativize(file).toString().replace('\\', '/'), hash(Files.readAllBytes(file))); }
|
||||
}
|
||||
private static void check(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.companion.CompanionWorld;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Imported files never become world data; rejected imports survive safe world transitions. */
|
||||
public final class Schematics100ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
|
||||
|
||||
var bad=PlanClient.directory().resolve("beta100-bad.schem");var good=PlanClient.directory().resolve("beta100-good.schem");
|
||||
var table=new BlockPos(3,1,3);
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try{
|
||||
java.nio.file.Files.createDirectories(bad.getParent());write(bad,"missing_mod:block");write(good,"minecraft:stone");
|
||||
byte[] original=java.nio.file.Files.readAllBytes(bad);
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Schematics100 "+UUID.randomUUID());s.setSeed("100");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(.5,1,.5);for(var q:fr.koka.sanctuary.multiblock.Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());check(fr.koka.sanctuary.multiblock.Metatable.assemble(p,table),"Workshop");});
|
||||
c.waitFor(m->fr.koka.sanctuary.multiblock.Metatable.state(m.level,table,true)==1,100);
|
||||
world.getServer().runOnServer(s->fr.koka.sanctuary.multiblock.Metatable.open(s.getPlayerList().getPlayers().getFirst(),table));c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->PlanClient.load(bad));c.waitFor(m->!PlanClient.busy,100);
|
||||
c.runOnClient(m->{check(m.gui.screen() instanceof PlansScreen&&PlanClient.plan==null,"Invalid import stays in workshop");check(PlanClient.status.getString().contains("missing_mod:block"),"Error names incompatible block");});
|
||||
c.takeScreenshot("schematics100-rejected-import");
|
||||
c.runOnClient(m->PlanClient.load(good));c.waitFor(m->!PlanClient.busy&&PlanClient.plan!=null&&m.gui.screen()==null,100);
|
||||
check(Arrays.equals(original,java.nio.file.Files.readAllBytes(bad)),"Rejected file unchanged");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);
|
||||
c.runOnClient(m->check(PlanClient.plan==null&&!PlanClient.busy,"No stale plan after reload"));
|
||||
world.getServer().runOnServer(s->check(fr.koka.sanctuary.multiblock.Metatable.state(s.overworld(),table,true)==1,"Same world and table survived reload"));
|
||||
}
|
||||
intro.set(true);
|
||||
try(var second=c.worldBuilder().adjustSettings(s->{s.setName("Schematics100 second "+UUID.randomUUID());s.setSeed("101");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
c.runOnClient(m->check(PlanClient.plan==null&&!MetatableClient.available(),"Independent second world opens without stale plan/session"));
|
||||
}
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
finally{try{java.nio.file.Files.deleteIfExists(bad);java.nio.file.Files.deleteIfExists(good);}catch(java.io.IOException ignored){}}
|
||||
System.out.println("SCHEMATICS100_PASS rejected asynchronous import, useful diagnostic, valid import, save/reopen, source bytes preserved");
|
||||
}
|
||||
private static void write(java.nio.file.Path path,String id)throws java.io.IOException{
|
||||
var root=new CompoundTag();root.putInt("Version",3);root.putInt("DataVersion",net.minecraft.SharedConstants.getCurrentVersion().dataVersion().version());root.putShort("Width",(short)1);root.putShort("Height",(short)1);root.putShort("Length",(short)1);
|
||||
var palette=new CompoundTag();palette.putInt(id,0);var blocks=new CompoundTag();blocks.put("Palette",palette);blocks.putByteArray("Data",new byte[]{0});root.put("Blocks",blocks);var outer=new CompoundTag();outer.put("Schematic",root);net.minecraft.nbt.NbtIo.writeCompressed(outer,path);
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Checks emitted render geometry, not a duplicate of the placement implementation. */
|
||||
final class Sculpture113ClientChecks {
|
||||
static void placementContexts(ServerPlayer player){
|
||||
for(var pos:List.of(new BlockPos(2,0,-2),new BlockPos(-2,0,2))){
|
||||
for(var face:List.of(Direction.UP,Direction.DOWN)){
|
||||
for(double x:new double[]{.25,.5,.75})for(double z:new double[]{.25,.5,.75}){
|
||||
var hit=new Vec3(pos.getX()+x,pos.getY()+(face==Direction.UP?1:0),pos.getZ()+z);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(hit,face,pos,false));
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Middle square including boundaries selects center on top/bottom faces");
|
||||
}
|
||||
}
|
||||
for(var edge:Direction.Plane.HORIZONTAL){
|
||||
for(double offset:new double[]{.25-1e-6,.25+1e-6}){
|
||||
var hit=new Vec3(pos.getX()+.5+edge.getStepX()*offset,pos.getY()+1,pos.getZ()+.5+edge.getStepZ()*offset);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(hit,Direction.UP,pos,false));
|
||||
var expected=offset<.25?SculptureAnchor.CENTER:SculptureAnchor.of(edge);
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==expected,"Center boundary switches to the aimed edge");
|
||||
}
|
||||
var top=new Vec3(pos.getX()+.5+edge.getStepX()*.4,pos.getY()+1,pos.getZ()+.5+edge.getStepZ()*.4);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(top,Direction.UP,pos,false));
|
||||
var state=Statuary.BLOCK.getStateForPlacement(context);
|
||||
check(context.getClickedPos().equals(pos.above())&&state.getValue(Statuary.ANCHOR)==SculptureAnchor.of(edge),"Top hit selects aimed edge at positive/negative coordinates");
|
||||
var side=Vec3.atCenterOf(pos).add(edge.getStepX()*.5,0,edge.getStepZ()*.5);
|
||||
context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(side,edge,pos,false));
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==SculptureAnchor.of(edge.getOpposite()),"Side hit anchors against supporting block");
|
||||
}
|
||||
}
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:List.of(SculptureAnchor.CENTER,SculptureAnchor.NORTH,SculptureAnchor.SOUTH,SculptureAnchor.EAST,SculptureAnchor.WEST)){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);
|
||||
for(var rotation:Rotation.values()){
|
||||
var actual=state.rotate(rotation);
|
||||
check(actual.getValue(Statuary.ANCHOR)==(anchor==SculptureAnchor.CENTER?anchor:SculptureAnchor.of(rotation.rotate(Direction.valueOf(anchor.name())))),"Structure rotates anchor independently of facing");
|
||||
}
|
||||
for(var mirror:Mirror.values()){
|
||||
var actual=state.mirror(mirror);
|
||||
check(actual.getValue(Statuary.ANCHOR)==(anchor==SculptureAnchor.CENTER?anchor:SculptureAnchor.of(mirror.mirror(Direction.valueOf(anchor.name())))),"Structure mirrors anchor independently of facing");
|
||||
}
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(state);legacy.getCompoundOrEmpty("properties").remove("anchor");
|
||||
var loaded=net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy);
|
||||
check(loaded.getValue(Statuary.FACING)==facing&&loaded.getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Beta.111/112 state keeps facing and defaults anchor");
|
||||
}
|
||||
}
|
||||
static void geometry(Minecraft client,String model){
|
||||
// Every footprint, odd and even, through all orientations and saved anchor states.
|
||||
for(int w=1;w<=16;w++)for(int d=1;d<=16;d++){
|
||||
var sculpture=new VoxelSculpture(model,"Grid fixture",w,2,d,List.of(new VoxelSculpture.Cell(0,0,0,0xFFFF0000),new VoxelSculpture.Cell(w-1,1,d-1,0xFF0000FF)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())rendered(entity(client,sculpture,facing,anchor));
|
||||
}
|
||||
// Transparent margins in imported models must not leave a gap at the aimed edge.
|
||||
var padded=new VoxelSculpture(model,"Padded fixture",13,2,15,List.of(new VoxelSculpture.Cell(2,0,3,-1),new VoxelSculpture.Cell(6,1,10,-1)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())rendered(entity(client,padded,facing,anchor));
|
||||
// Colored markers identify forward motion after rotation, independent of the origin shift.
|
||||
var sculpture=new VoxelSculpture(model,"Direction fixture",3,2,7,List.of(new VoxelSculpture.Cell(0,0,0,0xFFFF0000),new VoxelSculpture.Cell(0,0,6,0xFF0000FF),new VoxelSculpture.Cell(2,1,0,0xFF00FF00)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL){
|
||||
var mesh=mesh(entity(client,sculpture,facing,SculptureAnchor.CENTER));
|
||||
var red=mesh.stream().filter(q->q.color()==0xFFFF0000).findFirst().orElseThrow();var blue=mesh.stream().filter(q->q.color()==0xFF0000FF).findFirst().orElseThrow();
|
||||
check(blue.x()-red.x()==facing.getStepX()*6/16f&&blue.z()-red.z()==facing.getStepZ()*6/16f,"Colored front faces gaze");
|
||||
}
|
||||
var itemMesh=new StatuaryRenderer.ItemRenderer().extractArgument(StatuaryEntity.item(sculpture));
|
||||
check(itemMesh.equals(mesh(entity(client,sculpture,Direction.SOUTH,SculptureAnchor.CENTER))),"Item preview shares exact centered grid geometry");
|
||||
System.out.println("SCULPTURE113_GEOMETRY_PASS 9216 footprints/orientations/anchors and colored direction markers");
|
||||
}
|
||||
private static StatuaryEntity entity(Minecraft client,VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
|
||||
var entity=new StatuaryEntity(BlockPos.ZERO,Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor));entity.setLevel(client.level);
|
||||
var data=new CompoundTag();data.put("sculpture",StatuaryEntity.encode(sculpture));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,client.level.registryAccess(),data));return entity;
|
||||
}
|
||||
private static List<StatuaryRenderer.Quad> mesh(StatuaryEntity entity){
|
||||
var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(entity,state,0,Vec3.ZERO,null);return state.mesh;
|
||||
}
|
||||
static void rendered(StatuaryEntity entity){
|
||||
var mesh=mesh(entity);check(!mesh.isEmpty(),"Sculpture has visible geometry");
|
||||
float minX=1,minZ=1,maxX=0,maxZ=0;
|
||||
for(var q:mesh){
|
||||
check(q.x()*16==Math.rint(q.x()*16)&&q.y()*16==Math.rint(q.y()*16)&&q.z()*16==Math.rint(q.z()*16),"Every rendered voxel is on the exact 1/16 grid");
|
||||
check(q.x()>=0&&q.y()>=0&&q.z()>=0&&q.x()+1/16f<=1&&q.y()+1/16f<=1&&q.z()+1/16f<=1,"No voxel spills out of its block");
|
||||
minX=Math.min(minX,q.x());maxX=Math.max(maxX,q.x()+1/16f);minZ=Math.min(minZ,q.z());maxZ=Math.max(maxZ,q.z()+1/16f);
|
||||
}
|
||||
var anchor=entity.getBlockState().getValue(Statuary.ANCHOR);
|
||||
switch(anchor){
|
||||
case NORTH -> check(minZ==0,"North edge flush");case SOUTH -> check(maxZ==1,"South edge flush");
|
||||
case WEST -> check(minX==0,"West edge flush");case EAST -> check(maxX==1,"East edge flush");
|
||||
case NORTH_WEST -> check(minX==0&&minZ==0,"North-west corner flush");case NORTH_EAST -> check(maxX==1&&minZ==0,"North-east corner flush");
|
||||
case SOUTH_WEST -> check(minX==0&&maxZ==1,"South-west corner flush");case SOUTH_EAST -> check(maxX==1&&maxZ==1,"South-east corner flush");default -> {}
|
||||
}
|
||||
if(anchor==SculptureAnchor.CENTER||anchor==SculptureAnchor.NORTH||anchor==SculptureAnchor.SOUTH)check(Math.abs(minX-(1-maxX))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
if(anchor==SculptureAnchor.CENTER||anchor==SculptureAnchor.EAST||anchor==SculptureAnchor.WEST)check(Math.abs(minZ-(1-maxZ))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.ItemContainerContents;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** All registered block materials, native paid crafts, stale input rejection and pack reload. */
|
||||
public final class Sculpture116ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
Path file=PlanClient.directory().resolve("sculpture116-"+UUID.randomUUID()+".glb");
|
||||
try{
|
||||
Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture(true));var asset=GlbModel.load(file);var source=VoxelSculpture.of(asset.id(),asset.name(),StatueVoxelizer.miniature(asset.faces()));
|
||||
try{new SculpturePalette(List.of());throw new AssertionError("Empty palette accepted");}catch(IllegalArgumentException expected){}
|
||||
try{new SculpturePalette(Collections.nCopies(257,-1));throw new AssertionError("Oversized palette accepted");}catch(IllegalArgumentException expected){}
|
||||
try{new SculpturePalette(List.of(0));throw new AssertionError("Transparent color accepted");}catch(IllegalArgumentException expected){}
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;ItemStack hat;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture116 "+UUID.randomUUID());s.setSeed("116");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.getInventory().clearContent();p.teleportTo(.5,1,.5);for(int x=-4;x<8;x++)for(int z=-4;z<8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());Statuary.open(p,WORKSHOP);});
|
||||
c.waitForScreen(ClayWorkshopScreen.class);c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();((ClayWorkshopScreen)m.gui.screen()).load(file);});
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);
|
||||
c.runOnClient(m->{int count=0;for(var item:BuiltInRegistries.ITEM)if(item instanceof BlockItem){var stack=new ItemStack(item);check(ClayWorkshopMenu.material(stack),"Empty registered block accepted: "+item);var palette=SculptureMaterialSampler.sample(stack);check(palette!=null&&palette.colors().size()<=256&&palette.colors().stream().allMatch(v->(v>>>24)==255),"Bounded opaque palette: "+item);count++;}check(count>1000,"Whole block registry sampled");System.out.println("SCULPTURE116_PALETTES "+count+" registered block items");
|
||||
var dark=new ItemStack(Items.SHULKER_BOX);dark.set(DataComponents.CONTAINER,ItemContainerContents.fromItems(List.of(new ItemStack(Items.DIAMOND))));check(!ClayWorkshopMenu.material(dark),"Filled container cannot lose contents");check(!ClayWorkshopMenu.material(new ItemStack(Items.DIAMOND)),"Non-block item refused");
|
||||
check(source.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"Native paid crafts start from a uniform model");Sculpture117Checks.run(source);});
|
||||
var rendered=new HashSet<VoxelSculpture>();
|
||||
for(var item:List.of(Items.STONE,Items.OAK_PLANKS,Items.GOLD_BLOCK,Items.WOOL.pick(DyeColor.RED),Items.GLASS,Items.OAK_LEAVES,Items.CLAY,Items.DEEPSLATE)){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(item,2));check(menu.getSlot(1).getItem().isEmpty(),"Previous material result invalidates immediately");menu.broadcastChanges();});
|
||||
var expected=c.computeOnClient(m->ClayWorkshopMenu.appearance(source,item,SculptureMaterialSampler.sample(new ItemStack(item))));rendered.add(expected);
|
||||
c.waitFor(m->expected.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
|
||||
c.runOnClient(m->check(ItemStack.matches(((ClayWorkshopScreen)m.gui.screen()).preview(),m.player.containerMenu.getSlot(1).getItem()),"Preview equals server-owned result"));
|
||||
check(StatuaryEntity.decode(StatuaryEntity.encode(expected)).equals(expected),"Palette colors persist in unchanged schema 1");
|
||||
c.takeScreenshot("sculpture116-"+BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(0).getItem().getCount()==1,100);
|
||||
server.waitFor(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;return menu.getCarried().is(Statuary.ITEM)&&menu.getSlot(0).getItem().getCount()==1;},100);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)s.getPlayerList().getPlayers().getFirst().containerMenu;check(expected.equals(StatuaryEntity.sculpture(menu.getCarried()))&&menu.getSlot(0).getItem().getCount()==1,"Exactly one actual block consumed per sculpture");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.PICKUP,m.player));c.waitFor(m->m.player.containerMenu.getCarried().isEmpty(),100);server.waitFor(s->s.getPlayerList().getPlayers().getFirst().containerMenu.getCarried().isEmpty(),100);
|
||||
}
|
||||
check(rendered.size()>=7,"Distinct materials produce distinct sculptures");
|
||||
c.waitTicks(6);var stone=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.STONE)));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(Items.DIAMOND_BLOCK));menu.select(p,source,Items.STONE,stone);check(menu.getSlot(1).getItem().isEmpty(),"Stale material palette rejected by server");});
|
||||
var diamond=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.DIAMOND_BLOCK)).apply(source));c.waitFor(m->diamond.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
|
||||
c.runOnClient(m->{var menu=(ClayWorkshopMenu)m.player.containerMenu;ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,UUID.randomUUID(),source,Items.DIAMOND_BLOCK,stone));});c.waitTicks(6);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)s.getPlayerList().getPlayers().getFirst().containerMenu;check(diamond.equals(StatuaryEntity.sculpture(menu.getSlot(1).getItem()))&&menu.getSlot(0).getItem().getCount()==1,"Forged menu token cannot change recipe or spend input");});
|
||||
var before=c.computeOnClient(m->m.getModelManager().getBlockStateModelSet());var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(8);
|
||||
c.runOnClient(m->{check(before!=m.getModelManager().getBlockStateModelSet(),"Actual resource reload replaces sampling generation");check(diamond.equals(StatuaryEntity.sculpture(((ClayWorkshopScreen)m.gui.screen()).preview())),"Preview refreshes after pack reload");});
|
||||
hat=SculptureHat117Checks.exercise(c,server,diamond);
|
||||
System.out.println("SCULPTURE116_MATERIALS_PASS all block palettes, eight native paid materials, preview equality, shading, stale selections, input identity, token, palette bounds, containers and resource reload");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);c.waitTicks(12);
|
||||
SculptureHat117Checks.verifyEquipped(c,world.getServer(),hat);
|
||||
System.out.println("SCULPTURE117_HAT_RECONNECT_PASS equipped sculpture survives native save and fresh client reconnect");
|
||||
}
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}finally{intro.set(false);try{Files.deleteIfExists(file);}catch(java.io.IOException ignored){}}
|
||||
new Statuary106ClientChecks().runTest(c);
|
||||
System.out.println("SCULPTURE116_PASS materials plus full workshop/hotbar, placement, anchoring, drops, reconnect, FR/EN and Metabli regression");
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Regression for flat-color models, exact texture colors, deterministic grain and bounded CPU work. */
|
||||
final class Sculpture117Checks {
|
||||
static void run(VoxelSculpture source){
|
||||
var cells=new ArrayList<VoxelSculpture.Cell>();
|
||||
for(int z=0;z<16;z++)for(int y=0;y<16;y++)for(int x=0;x<16;x++)cells.add(new VoxelSculpture.Cell(x,y,z,0xFF808080));
|
||||
var flat=new VoxelSculpture(source.modelId(),"Flat",16,16,16,cells);
|
||||
var colors=new ArrayList<Integer>();colors.addAll(Collections.nCopies(64,0xFF223344));colors.addAll(Collections.nCopies(192,0xFFCCBBAA));
|
||||
var weighted=new SculpturePalette(colors);var out=weighted.apply(flat);
|
||||
long dark=out.cells().stream().filter(v->v.color()==0xFF223344).count();
|
||||
check(dark>900&&dark<1150,"Uniform source retains sampled 25% dark / 75% light texture distribution");
|
||||
check(out.cells().stream().allMatch(v->colors.contains(v.color())),"Never invent an averaged color");
|
||||
var reversed=new ArrayList<>(flat.cells());Collections.reverse(reversed);
|
||||
var shuffled=weighted.apply(new VoxelSculpture(flat.modelId(),flat.name(),16,16,16,reversed));
|
||||
check(Arrays.equals(out.packed(),shuffled.cells().reversed().stream().flatMapToInt(v->java.util.stream.IntStream.of(v.key(),v.color())).toArray()),"Sampling is independent of cell iteration order");
|
||||
check(out.equals(weighted.apply(flat))&&out.equals(StatuaryEntity.decode(StatuaryEntity.encode(out))),"Stable pattern and persisted final colors");
|
||||
var gradient=new ArrayList<Integer>();for(int i=0;i<256;i++)gradient.add(0xFF000000|i<<16|i<<8|i);
|
||||
var palette=new SculpturePalette(gradient);
|
||||
var shaded=new VoxelSculpture(flat.modelId(),flat.name(),16,16,16,cells.stream().map(v->new VoxelSculpture.Cell(v.x(),v.y(),v.z(),v.x()<8?0xFF000000:0xFFFFFFFF)).toList());
|
||||
var result=palette.apply(shaded);
|
||||
check(result.cells().stream().filter(v->v.x()<8).allMatch(v->SculpturePalette.lightness(v.color())<.5),"Dark regions sample dark shades");
|
||||
check(result.cells().stream().filter(v->v.x()>=8).allMatch(v->SculpturePalette.lightness(v.color())>=.5),"Light regions sample light shades");
|
||||
check(result.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>200,"Grain remains inside source shading regions");
|
||||
var single=new SculpturePalette(List.of(0xFF123456));check(single.apply(flat).cells().stream().allMatch(v->v.color()==0xFF123456),"Truly uniform block textures remain uniform");
|
||||
for(var item:List.of(Items.STONE,Items.OAK_PLANKS,Items.COAL_ORE,Items.OAK_LOG)){
|
||||
var sampled=SculptureMaterialSampler.sample(new ItemStack(item));var sculpture=sampled.apply(flat);
|
||||
check(sculpture.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>2,"Actual texture grain on flat model: "+item);
|
||||
check(sculpture.cells().stream().allMatch(v->sampled.colors().contains(v.color())),"Exact sampled pixels: "+item);
|
||||
}
|
||||
// Log tops and bark are separate native face materials; both must supply exact colors.
|
||||
var mc=Minecraft.getInstance();var state=Blocks.OAK_LOG.defaultBlockState();var parts=new ArrayList<BlockStateModelPart>();
|
||||
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(state.getSeed(BlockPos.ZERO)),parts);
|
||||
var bySprite=new HashMap<String,Set<Integer>>();
|
||||
for(var part:parts)for(var face:Direction.values())for(var quad:part.getQuads(face)){
|
||||
var sample=PlanMaterials.faceSample(state,quad.materialInfo());var set=bySprite.computeIfAbsent(quad.materialInfo().sprite().contents().name().toString(),k->new HashSet<>());
|
||||
for(int pixel:sample.pixels().argb()){int color=ARGB.multiply(pixel,sample.tint());if((color>>>24)>=128)set.add(color|0xFF000000);}
|
||||
}
|
||||
check(bySprite.size()>=2,"Native log has distinct face textures");var sampled=SculptureMaterialSampler.sample(new ItemStack(Items.OAK_LOG));
|
||||
var union=new HashSet<Integer>();bySprite.values().forEach(union::addAll);
|
||||
check(sampled.colors().stream().allMatch(union::contains),"All palette colors belong to baked face textures");
|
||||
for(var faceColors:bySprite.values())check(sampled.colors().stream().anyMatch(faceColors::contains),"Each log texture contributes pixels");
|
||||
for(int i=0;i<100;i++)palette.apply(flat);
|
||||
long[] times=new long[101];for(int i=0;i<times.length;i++){long start=System.nanoTime();var measured=palette.apply(flat);times[i]=System.nanoTime()-start;check(measured.cells().size()==4096,"Full bounded model");}
|
||||
Arrays.sort(times);
|
||||
System.out.printf(java.util.Locale.ROOT,"SCULPTURE117_PERF 4096 voxels median %.3f ms, p95 %.3f ms%n",times[50]/1e6,times[95]/1e6);
|
||||
System.out.println("SCULPTURE117_GRAIN_PASS flat model, weighted exact pixels, dark/light regions, deterministic cells, persistence and native face materials");
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.phys.shapes.*;
|
||||
|
||||
/** Single-box selection/collision bounds and light propagation compared with the real sculpture mesh. */
|
||||
public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
private static final String MODEL="sanctuary:model/"+"1".repeat(64);
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),SKY=new BlockPos(10,2,2),CONTROL=new BlockPos(18,2,2);
|
||||
private static final VoxelSculpture ARCH=arch();
|
||||
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture122 "+UUID.randomUUID());s.setSeed("122");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-14;x<=21;x++)for(int z=-6;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);p.setYRot(0);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));p.inventoryMenu.broadcastFullState();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(TARGET.below()).is(Blocks.STONE),150);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,.999,.5)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.below())&&hit.getDirection()==Direction.UP,"Cursor aims at placement support"));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(3);
|
||||
server.runOnServer(s->System.out.println("SCULPTURE122_PLACED "+player(s).level().getBlockState(TARGET)+" "+player(s).level().getBlockEntity(TARGET)));
|
||||
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),150);
|
||||
server.runOnServer(s->{physical(player(s).level(),TARGET);placementOverlap(player(s));});
|
||||
c.runOnClient(m->{physical(m.level,TARGET);geometry(m);Sculpture113ClientChecks.geometry(m,MODEL);});
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.0625,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects an occupied pillar"));
|
||||
c.takeScreenshot("sculpture124-single-outline");
|
||||
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET.north(),Blocks.REDSTONE_BLOCK.defaultBlockState()));
|
||||
c.waitFor(m->m.level.getBlockState(TARGET.north()).is(Blocks.REDSTONE_BLOCK),100);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects the sculpture bounds even through its visual hole"));
|
||||
c.takeScreenshot("sculpture124-select-volume");
|
||||
// A state edit and fresh entity payload must invalidate geometry on both sides.
|
||||
server.runOnServer(s->{var l=player(s).level();l.setBlockAndUpdate(TARGET,l.getBlockState(TARGET).setValue(Statuary.FACING,Direction.WEST).setValue(Statuary.ANCHOR,SculptureAnchor.EAST));});
|
||||
c.waitFor(m->m.level.getBlockState(TARGET).getValue(Statuary.FACING)==Direction.WEST,100);
|
||||
server.runOnServer(s->{var l=player(s).level();var bounds=l.getBlockState(TARGET).getShape(l,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Server shape rotates and moves to aimed edge");});
|
||||
c.runOnClient(m->{var bounds=m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Client shape follows changed blockstate");});
|
||||
var small=new VoxelSculpture(MODEL,"Small",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1)));
|
||||
server.runOnServer(s->place(player(s).level(),TARGET,small,Direction.WEST,SculptureAnchor.EAST));
|
||||
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&small.equals(e.sculpture()),100);
|
||||
c.runOnClient(m->check(m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds().getYsize()==1/16d,"Changed block entity payload invalidates cached shape"));
|
||||
server.runOnServer(s->{var l=player(s).level();place(l,TARGET,ARCH,Direction.SOUTH,SculptureAnchor.CENTER);l.setBlockAndUpdate(TARGET.north(),Blocks.AIR.defaultBlockState());
|
||||
for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)place(l,new BlockPos(x,y,z),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
for(int x=16;x<=20;x++)for(int z=0;z<=4;z++)l.setBlockAndUpdate(new BlockPos(x,3,z),Blocks.STONE.defaultBlockState());
|
||||
l.setBlockAndUpdate(new BlockPos(-8,2,0),Blocks.GLOWSTONE.defaultBlockState());place(l,new BlockPos(-7,2,0),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
});
|
||||
server.waitFor(s->{var l=player(s).level();return l.getBrightness(LightLayer.SKY,SKY)==15&&l.getBrightness(LightLayer.SKY,CONTROL)<15&&l.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13;},200);
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15&&m.level.getBrightness(LightLayer.SKY,CONTROL)<15&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13,200);
|
||||
server.runOnServer(s->{var l=player(s).level();for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)check(l.getBrightness(LightLayer.SKY,new BlockPos(x,y,z))==15,"All 75 sculptures keep full native skylight");});
|
||||
c.runOnClient(m->{var e=(StatuaryEntity)m.level.getBlockEntity(new BlockPos(10,4,2));var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(e,state,0,Vec3.ZERO,null);check(net.minecraft.util.LightCoordsUtil.sky(state.lightCoords)==15,"Actual renderer receives full skylight inside a group");});
|
||||
// Visual crowd at ground level: no whole-block AO squares between the models.
|
||||
server.runOnServer(s->{var p=player(s);for(int x=-3;x<=3;x++)for(int z=-5;z<=-2;z++)place(p.level(),new BlockPos(x,1,z),ARCH,Direction.from2DDataValue(Math.floorMod(x+z,4)),SculptureAnchor.CENTER);p.teleportTo(.5,3,5.5);p.getAbilities().flying=true;p.onUpdateAbilities();});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,3,5.5)<.1,100);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1,-3)));c.waitTicks(12);c.takeScreenshot("sculpture122-crowd-light");
|
||||
// Transparent-to-light is not emissive: in a closed room sculpture faces stay dark.
|
||||
server.runOnServer(s->{var l=player(s).level();for(int x=-13;x<=-9;x++)for(int y=1;y<=5;y++)for(int z=5;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,y,z),x==-13||x==-9||y==1||y==5||z==5||z==9?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());place(l,new BlockPos(-11,2,7),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);});
|
||||
c.waitFor(m->m.level.getBlockEntity(new BlockPos(-11,2,7)) instanceof StatuaryEntity&&m.level.getBrightness(LightLayer.SKY,new BlockPos(-11,2,7))==0&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-11,2,7))==0,200);
|
||||
c.runOnClient(m->{var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState((StatuaryEntity)m.level.getBlockEntity(new BlockPos(-11,2,7)),state,0,Vec3.ZERO,null);check(state.lightCoords==0,"No fullbright workaround: a dark room produces dark faces");});
|
||||
System.out.println("SCULPTURE122_LIGHT_PASS: 75 stacked models sky=15; opaque control shadow; block light=13 across sculpture; dark room=0; shade brightness=1");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),1200);
|
||||
world.getServer().runOnServer(s->physical(player(s).level(),TARGET));c.runOnClient(m->physical(m.level,TARGET));
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15,200);
|
||||
System.out.println("SCULPTURE122_PASS: native placement, one-box outline including holes, single-box physical collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
|
||||
}
|
||||
}
|
||||
private static void placementOverlap(ServerPlayer player){
|
||||
var pos=new BlockPos(3,1,0);var level=player.level();player.teleportTo(3.75,1,.5);player.setYRot(0);
|
||||
var filled=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)filled.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var dense=new VoxelSculpture(MODEL,"Solid",16,16,16,filled);var stack=StatuaryEntity.item(dense);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var hit=new BlockHitResult(new Vec3(3.01,1,.5),Direction.UP,pos.below(),false);
|
||||
var blocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(!blocked.consumesAction()&&level.getBlockState(pos).isAir()&&stack.getCount()==1,"Native placement refuses occupied voxels inside player without consuming sculpture");
|
||||
player.teleportTo(3.5,1,.5);var pillars=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x:new int[]{0,15})pillars.add(new VoxelSculpture.Cell(x,y,0,-1));
|
||||
var hollow=new VoxelSculpture(MODEL,"Two pillars",16,16,1,pillars);stack=StatuaryEntity.item(hollow);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var hollowBlocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(!hollowBlocked.consumesAction()&&level.getBlockState(pos).isAir(),"Placement also rejects a player inside the enclosing box between separated pillars");player.teleportTo(3.75,1,.5);
|
||||
var postCells=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x=0;x<2;x++)for(int z=0;z<2;z++)postCells.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var post=new VoxelSculpture(MODEL,"Post",2,16,2,postCells);stack=StatuaryEntity.item(post);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var allowed=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(allowed.consumesAction()&&level.getBlockState(pos).is(Statuary.BLOCK),"Native placement allows empty part of block beside player");
|
||||
check(level.noCollision(player,player.getBoundingBox()),"Accepted sculpture does not intersect player");
|
||||
level.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());player.teleportTo(.5,1,3.2);player.getInventory().setItem(player.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));player.inventoryMenu.broadcastFullState();
|
||||
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied bounds including visual holes, allows space outside the box, preserves stack on rejection");
|
||||
}
|
||||
private static void physical(Level level,BlockPos pos){
|
||||
var state=level.getBlockState(pos);var shape=state.getShape(level,pos);var collision=state.getCollisionShape(level,pos);
|
||||
check(shape==collision&&shape.toAabbs().size()==1,"Selection and collision share one cached bounding box");check(shape==state.getShape(level,pos),"Selection queries reuse the cached box");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Outline ray selects the whole sculpture volume");
|
||||
check(level.clip(new ClipContext(new Vec3(.0625,1.5,2),new Vec3(.0625,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Collision ray hits pillar");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Physical collision uses the enclosing volume, including visual holes");
|
||||
check(!level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query fills the inner volume instead of following tiny details");
|
||||
check(!level.noCollision(new AABB(.02,1.2,.4,.1,1.7,.6)),"Native collision query blocks occupied pillar");
|
||||
check(!state.isCollisionShapeFullBlock(level,pos)&&!state.isSolidRender()&&!state.canOcclude(),"Sculpture is not a solid occluding cube");
|
||||
check(state.getOcclusionShape().isEmpty()&&state.propagatesSkylightDown()&&state.getLightDampening()==0&&state.getShadeBrightness(level,pos)==1,"Light metadata never reintroduces full-block occlusion");
|
||||
check(!state.isSuffocating(level,pos)&&state.getLightEmission()==0,"Decorative shape neither suffocates nor emits light");
|
||||
}
|
||||
private static void geometry(Minecraft m){
|
||||
var padded=new VoxelSculpture(MODEL,"Padded",13,5,15,List.of(new VoxelSculpture.Cell(2,2,3,-1),new VoxelSculpture.Cell(6,4,10,-1)));
|
||||
var dense=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)dense.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var models=List.of(ARCH,padded,new VoxelSculpture(MODEL,"Dense",16,16,16,dense),new VoxelSculpture(MODEL,"One voxel",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1))));
|
||||
int cases=0;for(var model:models)for(var direction:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())for(var height:SculptureHeight.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor).setValue(Statuary.HEIGHT,height);var entity=new StatuaryEntity(BlockPos.ZERO,state);entity.setLevel(m.level);load(entity,model,m.level);
|
||||
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(boxes.size()==1&&volume>=model.cells().size()/4096d,"One box encloses all occupied cells");
|
||||
var outline=entity.shape(state);check(outline==entity.shape(state),"Repeated outline queries reuse the cached box");
|
||||
check(outline.toAabbs().size()==1&&outline.bounds().equals(shape.bounds()),"One outline box matches exact rendered extents for every orientation and anchor");
|
||||
int[] edges={0};outline.forAllEdges((x1,y1,z1,x2,y2,z2)->edges[0]++);check(edges[0]==12,"Native outline emits only the 12 enclosing edges, independent of voxel count");
|
||||
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
|
||||
double minX=1,minY=1,minZ=1,maxX=0,maxY=0,maxZ=0;
|
||||
for(var q:render.mesh){minX=Math.min(minX,q.x());minY=Math.min(minY,q.y());minZ=Math.min(minZ,q.z());maxX=Math.max(maxX,q.x()+1/16d);maxY=Math.max(maxY,q.y()+1/16d);maxZ=Math.max(maxZ,q.z()+1/16d);var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
|
||||
}
|
||||
check(shape.bounds().equals(new AABB(minX,minY,minZ,maxX,maxY,maxZ)),"Single collision box exactly matches visible mesh extents");
|
||||
cases++;
|
||||
}
|
||||
System.out.println("SCULPTURE124_BOUNDS_PASS "+cases+" cached single boxes agree with exact renderer extents; 12 outline edges each");
|
||||
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native bounding boxes agree with emitted mesh extents, plus cached 12-edge selection bounds for holes, padding, dense 4096 cells, single voxel, 4 directions, 9 anchors and 4 vertical modes");
|
||||
}
|
||||
private static void place(ServerLevel level,BlockPos pos,VoxelSculpture model,Direction facing,SculptureAnchor anchor){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);level.setBlockAndUpdate(pos,state);var entity=(StatuaryEntity)level.getBlockEntity(pos);load(entity,model,level);entity.setChanged();level.sendBlockUpdated(pos,state,state,3);
|
||||
}
|
||||
private static void load(StatuaryEntity entity,VoxelSculpture model,Level level){var tag=new CompoundTag();tag.put("sculpture",StatuaryEntity.encode(model));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,level.registryAccess(),tag));}
|
||||
private static VoxelSculpture arch(){var cells=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<4;z++)if(x<2||x>=14||y>=14)cells.add(new VoxelSculpture.Cell(x,y,z,0xffc99679));return new VoxelSculpture(MODEL,"Arch",16,16,4,cells);}
|
||||
private static ServerPlayer player(MinecraftServer server){return server.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native clicks on all six support faces; additive saved-state compatibility and real renderer bounds. */
|
||||
public final class Sculpture125ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos SUPPORT=new BlockPos(0,4,0);
|
||||
private static final String MODEL="sanctuary:model/"+"2".repeat(64);
|
||||
private static final VoxelSculpture STATUE=statue();
|
||||
private static final SculptureAnchor[][] ANCHORS={{SculptureAnchor.NORTH_WEST,SculptureAnchor.WEST,SculptureAnchor.SOUTH_WEST},{SculptureAnchor.NORTH,SculptureAnchor.CENTER,SculptureAnchor.SOUTH},{SculptureAnchor.NORTH_EAST,SculptureAnchor.EAST,SculptureAnchor.SOUTH_EAST}};
|
||||
private static final SculptureHeight[] HEIGHTS={SculptureHeight.BOTTOM,SculptureHeight.CENTER,SculptureHeight.TOP};
|
||||
private record Spot(Direction face,int u,int v){
|
||||
Vec3 hit(BlockPos p){double a=.1+.4*u,b=.1+.4*v;return switch(face){
|
||||
case UP,DOWN->new Vec3(p.getX()+a,p.getY()+(face==Direction.UP?1:0),p.getZ()+b);
|
||||
case NORTH,SOUTH->new Vec3(p.getX()+a,p.getY()+b,p.getZ()+(face==Direction.SOUTH?1:0));
|
||||
case WEST,EAST->new Vec3(p.getX()+(face==Direction.EAST?1:0),p.getY()+b,p.getZ()+a);
|
||||
};}
|
||||
SculptureAnchor anchor(){return switch(face){case UP,DOWN->ANCHORS[u][v];case NORTH->ANCHORS[u][2];case SOUTH->ANCHORS[u][0];case EAST->ANCHORS[0][u];case WEST->ANCHORS[2][u];};}
|
||||
SculptureHeight height(){return face==Direction.UP?SculptureHeight.BOTTOM:face==Direction.DOWN?SculptureHeight.TOP:HEIGHTS[v];}
|
||||
}
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);var enter=new KeyEvent(InputConstants.KEY_RETURN,0,0);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter);intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
var persistent=new HashMap<BlockPos,BlockState>();
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture125 "+UUID.randomUUID());s.setSeed("125");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-8;x<=8;x++)for(int z=-8;z<=8;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.getAbilities().flying=true;p.onUpdateAbilities();p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(STATUE));p.inventoryMenu.broadcastFullState();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");contexts(p);legacy(p);Sculpture113ClientChecks.placementContexts(p);l.setBlockAndUpdate(SUPPORT,Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(SUPPORT).is(Blocks.STONE),150);
|
||||
int clicks=0;
|
||||
for(var face:Direction.values()){
|
||||
Vec3 eye= switch(face){case UP->new Vec3(.5,6,3);case DOWN->new Vec3(.5,1.3,2.5);case NORTH->new Vec3(.5,3,-2.2);case SOUTH->new Vec3(.5,3,3.2);case EAST->new Vec3(3.2,3,.5);case WEST->new Vec3(-2.2,3,.5);};
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(eye.x,eye.y,eye.z);p.setDeltaMovement(Vec3.ZERO);p.getAbilities().flying=true;p.onUpdateAbilities();});c.waitFor(m->m.player.distanceToSqr(eye)<.05,100);c.runOnClient(m->{m.player.getAbilities().flying=true;m.player.setDeltaMovement(Vec3.ZERO);m.player.onUpdateAbilities();});c.waitTicks(10);
|
||||
for(int u=0;u<3;u++)for(int v=0;v<3;v++){
|
||||
var spot=new Spot(face,u,v);var target=SUPPORT.relative(face);
|
||||
server.runOnServer(s->{for(var direction:Direction.values())player(s).level().setBlockAndUpdate(SUPPORT.relative(direction),Blocks.AIR.defaultBlockState());});
|
||||
c.waitFor(m->m.level.getBlockState(target).isAir(),100);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,spot.hit(SUPPORT)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult h&&h.getBlockPos().equals(SUPPORT)&&h.getDirection()==face,"Native cursor hits requested support face "+spot+" instead of "+m.hitResult+" at "+m.hitResult.getLocation()+" eye "+m.player.getEyePosition()+" flying "+m.player.getAbilities().flying));
|
||||
var facing=c.computeOnClient(m->{System.out.println("SCULPTURE125_AIM "+spot+" actual "+m.hitResult.getLocation()+" eye "+m.player.getEyePosition());return m.player.getDirection();});c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(3);
|
||||
c.waitFor(m->m.level.getBlockEntity(target) instanceof StatuaryEntity e&&STATUE.equals(e.sculpture()),100);
|
||||
server.runOnServer(s->{var l=player(s).level();var state=l.getBlockState(target);check(state.getValue(Statuary.ANCHOR)==spot.anchor()&&state.getValue(Statuary.HEIGHT)==spot.height()&&state.getValue(Statuary.FACING)==facing,"Server uses aimed position and horizontal gaze for "+spot+" got "+state+" expected facing "+facing);});
|
||||
c.runOnClient(m->{var e=(StatuaryEntity)m.level.getBlockEntity(target);renderBounds(e);});
|
||||
if(u==0&&v==2)c.takeScreenshot("sculpture125-"+face.getSerializedName());clicks++;
|
||||
}
|
||||
}
|
||||
System.out.println("SCULPTURE125_CLICKS_PASS "+clicks+" real placements: all six faces, every corner/edge/center and supported heights");
|
||||
c.runOnClient(m->Sculpture125ParticleChecks.exercise(m,SUPPORT.relative(Direction.EAST)));
|
||||
Sculpture125WorkshopChecks.exercise(c,server);
|
||||
server.runOnServer(s->{var l=player(s).level();for(var direction:Direction.values())l.setBlockAndUpdate(SUPPORT.relative(direction),Blocks.AIR.defaultBlockState());for(int x=0;x<3;x++)for(int y=0;y<3;y++){
|
||||
var pos=new BlockPos(x*2-2,y+1,0);l.setBlockAndUpdate(pos.north(),Blocks.STONE.defaultBlockState());var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,Direction.SOUTH).setValue(Statuary.ANCHOR,ANCHORS[x][0]).setValue(Statuary.HEIGHT,HEIGHTS[y]);place(l,pos,state);persistent.put(pos,state);
|
||||
}player(s).teleportTo(.5,2,5);});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,2,5)<.05,100);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,2.5,.5)));c.waitTicks(8);c.takeScreenshot("sculpture125-wall-placements");saved=world.getWorldSave();
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);world.getServer().runOnServer(s->{for(var entry:persistent.entrySet()){var state=player(s).level().getBlockState(entry.getKey());check(state==entry.getValue(),"New anchors and heights persist exactly");check(STATUE.equals(((StatuaryEntity)player(s).level().getBlockEntity(entry.getKey())).sculpture()),"Model data persists unchanged");}});
|
||||
c.waitFor(m->persistent.keySet().stream().allMatch(p->m.level.getBlockEntity(p) instanceof StatuaryEntity e&&STATUE.equals(e.sculpture())),150);c.runOnClient(m->persistent.keySet().forEach(p->renderBounds((StatuaryEntity)m.level.getBlockEntity(p))));
|
||||
}
|
||||
new Sculpture122ClientChecks().runTest(c);
|
||||
System.out.println("SCULPTURE125_PASS new placement gestures, native picking, collision/render agreement, legacy states and saved new positions");
|
||||
}
|
||||
private static void contexts(ServerPlayer p){int count=0;
|
||||
for(var pos:List.of(new BlockPos(5,3,5),new BlockPos(-5,3,-5))){p.level().setBlockAndUpdate(pos,Blocks.STONE.defaultBlockState());
|
||||
for(int turn=0;turn<4;turn++){p.setYRot(turn*90);for(var face:Direction.values())for(int u=0;u<3;u++)for(int v=0;v<3;v++){
|
||||
var spot=new Spot(face,u,v);var context=new BlockPlaceContext(p,InteractionHand.MAIN_HAND,StatuaryEntity.item(STATUE),new BlockHitResult(spot.hit(pos),face,pos,false));var state=Statuary.BLOCK.getStateForPlacement(context);
|
||||
check(context.getClickedPos().equals(pos.relative(face))&&state.getValue(Statuary.ANCHOR)==spot.anchor()&&state.getValue(Statuary.HEIGHT)==spot.height()&&state.getValue(Statuary.FACING)==p.getDirection(),"Native placement context agrees at signed coordinates: "+spot);count++;
|
||||
}}p.level().setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
check(Statuary.BLOCK.getStateDefinition().getPossibleStates().size()==144,"108 placement states plus 36 legacy-height states");
|
||||
for(var anchor:SculptureAnchor.values())for(var height:SculptureHeight.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.ANCHOR,anchor).setValue(Statuary.HEIGHT,height);var box=SculptureGeometry.shape(STATUE,Direction.SOUTH,anchor,height).bounds();
|
||||
for(var rotation:Rotation.values()){var rotated=state.rotate(rotation);check(rotated.getValue(Statuary.HEIGHT)==height,"Yaw rotation retains height");var b=SculptureGeometry.shape(STATUE,rotated.getValue(Statuary.FACING),rotated.getValue(Statuary.ANCHOR),height).bounds();double ex=box.getCenter().x-.5,ez=box.getCenter().z-.5;double tx=switch(rotation){case NONE->ex;case CLOCKWISE_90->-ez;case CLOCKWISE_180->-ex;case COUNTERCLOCKWISE_90->ez;};double tz=switch(rotation){case NONE->ez;case CLOCKWISE_90->ex;case CLOCKWISE_180->-ez;case COUNTERCLOCKWISE_90->-ex;};check(Math.abs((b.getCenter().x-.5)-tx)<=1/16d&&Math.abs((b.getCenter().z-.5)-tz)<=1/16d&&b.minY==box.minY,"Rotated anchor follows native structure rotation without half voxels");}
|
||||
for(var mirror:Mirror.values()){var mirrored=state.mirror(mirror);var b=SculptureGeometry.shape(STATUE,mirrored.getValue(Statuary.FACING),mirrored.getValue(Statuary.ANCHOR),height).bounds();double x=mirror==Mirror.FRONT_BACK?1-box.getCenter().x:box.getCenter().x,z=mirror==Mirror.LEFT_RIGHT?1-box.getCenter().z:box.getCenter().z;check(Math.abs(b.getCenter().x-x)<=1/16d&&Math.abs(b.getCenter().z-z)<=1/16d&&b.minY==box.minY,"Mirrored corner and vertical placement follow native structure mirror");}
|
||||
}
|
||||
System.out.println("SCULPTURE125_CONTEXT_PASS "+count+" contexts, signed coordinates, four headings, rotations/mirrors and 144 valid states");
|
||||
}
|
||||
private static void legacy(ServerPlayer p){int n=0;
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:List.of(SculptureAnchor.CENTER,SculptureAnchor.NORTH,SculptureAnchor.SOUTH,SculptureAnchor.WEST,SculptureAnchor.EAST)){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor).setValue(Statuary.HEIGHT,SculptureHeight.TOP);var nbt=NbtUtils.writeBlockState(state);nbt.getCompoundOrEmpty("properties").remove("vertical_anchor");var restored=NbtUtils.readBlockState(BuiltInRegistries.BLOCK,nbt);
|
||||
check(restored.getValue(Statuary.HEIGHT)==SculptureHeight.ORIGINAL&&restored.getValue(Statuary.ANCHOR)==anchor&&restored.getValue(Statuary.FACING)==facing,"Old NBT keeps orientation and anchor and defaults original Y");var b=SculptureGeometry.shape(STATUE,facing,anchor,restored.getValue(Statuary.HEIGHT)).bounds();check(b.minY==2/16d&&b.maxY==10/16d,"Legacy vertical padding is preserved exactly");n++;
|
||||
}System.out.println("SCULPTURE125_LEGACY_PASS "+n+" old blockstates retain exact placements without rewriting payloads");
|
||||
}
|
||||
private static void renderBounds(StatuaryEntity entity){var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);double x0=1,y0=1,z0=1,x1=0,y1=0,z1=0;for(var q:render.mesh){x0=Math.min(x0,q.x());y0=Math.min(y0,q.y());z0=Math.min(z0,q.z());x1=Math.max(x1,q.x()+1/16d);y1=Math.max(y1,q.y()+1/16d);z1=Math.max(z1,q.z()+1/16d);}var bounds=entity.shape(entity.getBlockState()).bounds();check(bounds.equals(new AABB(x0,y0,z0,x1,y1,z1)),"Native single box follows actual rendered offsets on all three axes");Sculpture113ClientChecks.rendered(entity);}
|
||||
private static void place(ServerLevel level,BlockPos pos,BlockState state){level.setBlockAndUpdate(pos,state);var e=(StatuaryEntity)level.getBlockEntity(pos);var data=new CompoundTag();data.put("sculpture",StatuaryEntity.encode(STATUE));e.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,level.registryAccess(),data));e.setChanged();level.sendBlockUpdated(pos,state,state,3);}
|
||||
private static VoxelSculpture statue(){var cells=new ArrayList<VoxelSculpture.Cell>();for(int y=2;y<10;y++)for(int x=y<6?2:1;x<=(y<6?4:5);x++)for(int z=y<6?1:0;z<=(y<6?3:4);z++)cells.add(new VoxelSculpture.Cell(x,y,z,y<6?0xff44aabb:0xffddb88b));return new VoxelSculpture(MODEL,"Placement statue",7,10,5,cells);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean ok,String reason){if(!ok)throw new AssertionError(reason);}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.particle.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
final class Sculpture125ParticleChecks {
|
||||
static void exercise(Minecraft mc,BlockPos pos){
|
||||
var entity=(StatuaryEntity)mc.level.getBlockEntity(pos);var state=entity.getBlockState();
|
||||
var render=new StatuaryRenderer();var rs=render.createRenderState();render.extractRenderState(entity,rs,0,Vec3.ZERO,null);
|
||||
Set<Integer> colors=new HashSet<>();for(var q:rs.mesh)colors.add(q.color());
|
||||
var queue=queue(mc);queue.clear();var q=rs.mesh.getLast();var hit=Vec3.atLowerCornerOf(pos).add(SculptureParticles.center(q));
|
||||
mc.hitResult=new BlockHitResult(hit,Direction.UP,pos,false);mc.level.addBreakingBlockEffects(pos,Direction.UP,false);
|
||||
check(queue.size()==1&&queue.peek() instanceof SculptureParticles.Fragment,"Native hit emits one voxel chip, no clay debris");
|
||||
var chip=(SingleQuadParticle)queue.peek();check(color(chip)==q.color(),"Hit chip uses the exact targeted voxel color");
|
||||
queue.clear();mc.level.addDestroyBlockEffect(pos,state);verify(queue,rs.mesh,colors,pos);int count=queue.size();
|
||||
queue.clear();mc.level.setBlock(pos,Blocks.AIR.defaultBlockState(),3);check(mc.level.getBlockEntity(pos)==null,"Native block update removes sculpture before break effect");
|
||||
mc.level.addDestroyBlockEffect(pos,state);verify(queue,rs.mesh,colors,pos);check(queue.size()==count,"Late break event retains voxel colors and offsets");
|
||||
queue.clear();mc.level.addDestroyBlockEffect(pos,Blocks.STONE.defaultBlockState());check(!queue.isEmpty()&&queue.stream().noneMatch(SculptureParticles.Fragment.class::isInstance),"Other blocks retain native particles");queue.clear();
|
||||
System.out.println("SCULPTURE125_PARTICLES_PASS native hit/destroy hooks, exact voxel colors, real surface origins, 64-chip bound, late removal and ordinary block regression");
|
||||
}
|
||||
private static void verify(Queue<Particle> queue,List<StatuaryRenderer.Quad> mesh,Set<Integer> colors,BlockPos pos){
|
||||
check(!queue.isEmpty()&&queue.size()<=64,"Bounded destruction particles");var observed=new HashSet<Integer>();
|
||||
for(var p:queue){check(p instanceof SculptureParticles.Fragment,"No clay fallback particles");int color=color((SingleQuadParticle)p);observed.add(color);check(colors.contains(color),"Particle retains an actual model color");var point=new Vec3(number(p,"x"),number(p,"y"),number(p,"z")).subtract(Vec3.atLowerCornerOf(pos));check(mesh.stream().anyMatch(q->q.color()==color&&SculptureParticles.center(q).equals(point)),"Particle belongs to an actual colored voxel face after rotation/anchor");check(((SingleQuadParticle)p).getQuadSize(0)==1/32f,"One voxel diameter, no oversized clay chips");}
|
||||
check(observed.size()==colors.size(),"Both fixture material colors occur in destruction chips");
|
||||
}
|
||||
@SuppressWarnings("unchecked") private static Queue<Particle> queue(Minecraft mc){try{var f=ParticleEngine.class.getDeclaredField("particlesToAdd");f.setAccessible(true);return (Queue<Particle>)f.get(mc.particleEngine);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static double number(Object value,String name){try{Class<?> type=value.getClass();Field f=null;while(type!=null){try{f=type.getDeclaredField(name);break;}catch(NoSuchFieldException ignored){type=type.getSuperclass();}}f.setAccessible(true);return ((Number)f.get(value)).doubleValue();}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static int color(SingleQuadParticle p){return 0xff000000|Math.round((float)number(p,"rCol")*255)<<16|Math.round((float)number(p,"gCol")*255)<<8|Math.round((float)number(p,"bCol")*255);}
|
||||
private static void check(boolean ok,String reason){if(!ok)throw new AssertionError(reason);}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
|
||||
final class Sculpture125WorkshopChecks {
|
||||
static void exercise(ClientGameTestContext c,TestServerContext server){
|
||||
var pos=new BlockPos(4,1,4);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(3,1,3);p.setGameMode(GameType.SURVIVAL);p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.level().setBlockAndUpdate(pos,Statuary.WORKSHOP.defaultBlockState());
|
||||
CompanionService.save(p,CompanionService.record(p).note("seen_mobs",""));Statuary.open(p,pos);
|
||||
});
|
||||
c.waitForScreen(ClayWorkshopScreen.class);
|
||||
c.runOnClient(m->{var menu=(ClayWorkshopMenu)m.player.containerMenu;check(!menu.view.statues().contains("minecraft:cow"),"Unseen cow is absent without visiting Metabli");var screen=(ClayWorkshopScreen)m.gui.screen();screen.load(EntityTypes.COW);check(screen.selected()==null&&!screen.busy(),"Unseen animal cannot start local generation");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(Items.OAK_PLANKS,2));var locked=new VoxelSculpture(StatueDiscoveries.modelId("minecraft:cow"),"Cow",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1)));menu.select(p,locked,Items.OAK_PLANKS,new SculpturePalette(List.of(-1)));check(menu.getSlot(1).getItem().isEmpty(),"Server rejects a locked animal model despite valid material");p.closeContainer();
|
||||
CompanionService.save(p,CompanionService.record(p).note("seen_mobs","cow"));p.awardStat(Stats.ENTITY_KILLED.get(EntityTypes.SHEEP));p.awardStat(Stats.ENTITY_KILLED_BY.get(EntityTypes.PIG));
|
||||
});
|
||||
c.waitFor(m->m.gui.screen()==null,100);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();Statuary.open(p,pos);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.view.statues().equals(StatueDiscoveries.known(p)),"Workshop uses the exact statue catalogue discoveries");menu.getSlot(0).set(new ItemStack(Items.OAK_PLANKS,4));menu.broadcastChanges();});
|
||||
c.waitForScreen(ClayWorkshopScreen.class);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();var menu=(ClayWorkshopMenu)m.player.containerMenu;check(menu.view.statues().containsAll(List.of("minecraft:cow","minecraft:sheep","minecraft:pig")),"Encounter, kill and killed-by unlocks are transmitted");});
|
||||
for(var type:List.of(EntityTypes.COW,EntityTypes.SHEEP,EntityTypes.PIG)){
|
||||
String model=StatueDiscoveries.modelId(BuiltInRegistries.ENTITY_TYPE.getKey(type).toString());
|
||||
c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(type.getDescription().getString())).findFirst().orElseThrow().onPress(new KeyEvent(257,0,0));});
|
||||
c.waitTicks(10);c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();System.out.println("SCULPTURE125_WORKSHOP_DEBUG "+type+" busy "+screen.busy()+" source "+(screen.selected()==null?"null":screen.selected().modelId())+" input "+m.player.containerMenu.getSlot(0).getItem()+" output "+m.player.containerMenu.getSlot(1).getItem());});
|
||||
c.waitFor(m->{var screen=(ClayWorkshopScreen)m.gui.screen();return !screen.busy()&&screen.selected()!=null&&screen.selected().modelId().equals(model)&&m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM)&&StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem()).modelId().equals(model);},250);
|
||||
c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var source=screen.selected();check(source.cells().size()>20&&Math.max(source.width(),Math.max(source.height(),source.depth()))==16,"Native animal model voxelized at miniature scale");check(ItemStack.matches(screen.preview(),m.player.containerMenu.getSlot(1).getItem()),"Animal preview equals server-paid palette result");});
|
||||
c.takeScreenshot("sculpture125-workshop-"+BuiltInRegistries.ENTITY_TYPE.getKey(type).getPath());
|
||||
}
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(0).getItem().getCount()==3,100);
|
||||
server.waitFor(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;return menu.getCarried().is(Statuary.ITEM)&&menu.getSlot(0).getItem().getCount()==3;},100);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.containerMenu.getSlot(0).getItem().getCount()==3&&p.containerMenu.getCarried().is(Statuary.ITEM),"Exactly one block consumed for native animal sculpture in survival");});
|
||||
Path file=PlanClient.directory().resolve("sculpture125-"+UUID.randomUUID()+".glb");
|
||||
try{Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture(true));c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(ClayWorkshopScreen.text("imports").getString())).findFirst().orElseThrow().onPress(new KeyEvent(257,0,0));screen.load(file);});
|
||||
var id=GlbModel.load(file).id();c.waitFor(m->{var screen=(ClayWorkshopScreen)m.gui.screen();return !screen.busy()&&screen.selected()!=null&&screen.selected().modelId().equals(id)&&id.equals(Optional.ofNullable(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())).map(VoxelSculpture::modelId).orElse(""));},250);c.takeScreenshot("sculpture125-workshop-import");
|
||||
}catch(Exception e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(Exception ignored){}}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.closeContainer();p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();});c.waitFor(m->m.gui.screen()==null,100);
|
||||
System.out.println("SCULPTURE125_WORKSHOP_PASS encounter/kill/death unlocks, native menu sync, locked server rejection, three textured animals, paid survival craft and GLB import");
|
||||
}
|
||||
private static void check(boolean ok,String reason){if(!ok)throw new AssertionError(reason);}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Actual accessory-slot transfers and recovery from a saved beta.116 head session. */
|
||||
final class SculptureHat117Checks {
|
||||
static ItemStack exercise(ClientGameTestContext c,TestServerContext server,VoxelSculpture sculpture){
|
||||
var original=StatuaryEntity.item(sculpture);original.set(DataComponents.CUSTOM_NAME,Component.literal("Sculpture chapeau 117"));
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
server.waitFor(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.containerMenu==p.inventoryMenu;},100);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,original.copy());p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->ItemStack.matches(m.player.inventoryMenu.getSlot(36).getItem(),original),100);
|
||||
c.runOnClient(m->m.gui.setScreen(new InventoryScreen(m.player)));
|
||||
click(c,36);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Statuary.ITEM),100);
|
||||
click(c,66);c.waitTicks(12);verifyEquipped(c,server,original);
|
||||
c.takeScreenshot("sculpture117-hat-inventory");
|
||||
click(c,66);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Statuary.ITEM),100);
|
||||
click(c,36);
|
||||
server.waitFor(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.inventoryMenu.getCarried().isEmpty()&&HeadEquipment.get(p).isEmpty()&&ItemStack.matches(p.getInventory().getItem(0),original);},100);
|
||||
c.waitFor(m->ItemStack.matches(m.player.inventoryMenu.getSlot(36).getItem(),original)&&m.player.inventoryMenu.getSlot(66).getItem().isEmpty(),100);
|
||||
c.runOnClient(m->check(!new StatuaryRenderer.ItemRenderer().extractArgument(m.player.inventoryMenu.getSlot(36).getItem()).isEmpty(),"Returned sculpture still renders in hotbar"));
|
||||
click(c,36);click(c,66);c.waitTicks(8);verifyEquipped(c,server,original);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var saved=HeadBlockService.save(p);check(saved!=null,"Authoritative worn model is saved");
|
||||
var damaged=original.copy();damaged.remove(DataComponents.BLOCK_ENTITY_DATA);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,damaged);HeadEquipment.retainData(p,saved);
|
||||
HeadBlockService.session(p);p.inventoryMenu.broadcastChanges();
|
||||
check(ItemStack.matches(HeadEquipment.get(p),original),"Recover already stripped sculpture from its saved worn model");
|
||||
});
|
||||
c.waitTicks(8);verifyEquipped(c,server,original);
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
System.out.println("SCULPTURE117_HAT_PASS native equip/remove/re-equip, slot mesh, exact components and old worn-session recovery");
|
||||
return original;
|
||||
}
|
||||
static void verifyEquipped(ClientGameTestContext c,TestServerContext server,ItemStack original){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(ItemStack.matches(HeadEquipment.get(p),original),"Equipped stack retains its geometry and name");check(p.getInventory().getItem(0).isEmpty()&&p.inventoryMenu.getCarried().isEmpty(),"Exactly one sculpture moved into head slot");});
|
||||
c.waitFor(m->ItemStack.matches(m.player.inventoryMenu.getSlot(66).getItem(),original)&&StatuaryEntity.sculpture(original).equals(StatuaryEntity.sculpture(HeadCosmeticService.appearance(m.player))),150);
|
||||
c.runOnClient(m->check(!new StatuaryRenderer.ItemRenderer().extractArgument(m.player.inventoryMenu.getSlot(66).getItem()).isEmpty(),"Equipped slot retains visible item geometry"));
|
||||
}
|
||||
private static void click(ClientGameTestContext c,int slot){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.inventoryMenu.containerId,slot,0,ContainerInput.PICKUP,m.player));}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import fr.koka.sanctuary.weather.SnowAccumulation;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
|
||||
public final class Snow108ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos AT = new BlockPos(4,80,4);
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}});
|
||||
try {
|
||||
for(boolean enabled:new boolean[]{true,false})try(var world=c.worldBuilder().adjustSettings(s->{
|
||||
s.setName("Snow108 "+enabled+" "+UUID.randomUUID());s.setSeed("108");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
s.getGameRules().set(SanctuaryGameplay.ENABLED,enabled,null);
|
||||
}).create()) {
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&(!enabled||ProgressionClient.active()&&!IntroSpawnFlash.active()),1200);
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var l=p.level();p.teleportTo(0,80,-5);
|
||||
for(int x=-15;x<=15;x++)for(int z=-15;z<=15;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
if(enabled) {
|
||||
command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||
check(l.getGameRules().get(SnowAccumulation.MAX_HEIGHT)==2,"Default two blocks");
|
||||
check(SnowAccumulation.REALTIME_SCALE==12,"Snow accumulation uses the 12x real-time arrival scale");
|
||||
for(int i=1;i<=7;i++){int expected=i;until(l,AT,1000,()->layers(l,AT)==expected,"Layer "+i+" arrives through the native sampler");}
|
||||
var pig=EntityTypes.PIG.create(l,EntitySpawnReason.COMMAND);pig.setNoAi(true);pig.setNoGravity(true);pig.setPos(4.5,80.75,4.5);l.addFreshEntity(pig);
|
||||
until(l,AT,1000,()->l.getBlockState(AT).is(Blocks.SNOW_BLOCK),"Eighth layer becomes solid snow block");
|
||||
check(l.getBlockState(AT).getCollisionShape(l,AT,CollisionContext.empty()).bounds().maxY==1,"Full block collision");
|
||||
check(pig.getY()>=81,"Growth lifts a standing entity");pig.discard();
|
||||
until(l,AT,1000,()->layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||
until(l,AT,2000,()->l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default cap fills a second block");
|
||||
check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(2)).isAir(),"Default cap at two blocks");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 4");
|
||||
until(l,AT,5000,()->l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK),"Raised cap fills the fourth block");
|
||||
check(l.getBlockState(AT.above(3)).is(Blocks.SNOW_BLOCK)&&l.getBlockState(AT.above(4)).isAir(),"Operator raises cap to four blocks");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height -1");
|
||||
until(l,AT,3000,()->l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode fills beyond the cap");
|
||||
check(l.getBlockState(AT.above(5)).is(Blocks.SNOW_BLOCK),"Unlimited mode continues upward");
|
||||
var fresh=new BlockPos(-5,80,-5);command(l,"gamerule sanctuary:snow_accumulation_height 0");step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Sanctuary zero stops deposits");
|
||||
command(l,"gamerule sanctuary:snow_accumulation_height 2");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,0,server);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"Native zero remains authoritative");l.getGameRules().set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT,1,server);
|
||||
l.setRainLevel(0);l.getWeatherData().setRaining(false);step(l,fresh,200);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
||||
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);until(l,fresh,1000,()->layers(l,fresh)==1,"Snow showers accumulate while falling");
|
||||
var flower=new BlockPos(-6,80,-6);l.setBlockAndUpdate(flower.below(),Blocks.GRASS_BLOCK.defaultBlockState());l.setBlockAndUpdate(flower,Blocks.DANDELION.defaultBlockState());step(l,flower,200);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
||||
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());step(l,water,200);check(l.getBlockState(water).is(Blocks.WATER)&&l.getBlockState(water.above()).isAir(),"Open water not filled with snow");
|
||||
var roof=new BlockPos(7,83,-7);l.setBlockAndUpdate(roof,Blocks.STONE.defaultBlockState());until(l,roof,1000,()->layers(l,roof.above())==1,"Snow on roof arrives");check(layers(l,roof.above())==1&&l.getBlockState(roof.below()).isAir(),"Snow on roof, not indoors");
|
||||
var cauldron=new BlockPos(8,80,-8);l.setBlockAndUpdate(cauldron,Blocks.CAULDRON.defaultBlockState());for(int i=0;i<200;i++)l.tickPrecipitation(cauldron);check(l.getBlockState(cauldron).is(Blocks.POWDER_SNOW_CAULDRON),"Native cauldron snow filling preserved");
|
||||
command(l,"sanctuary weather rain");l.setRainLevel(1);var rainy=new BlockPos(10,80,0);l.tickPrecipitation(rainy);check(l.getBlockState(rainy).isAir(),"Temperate rain is not snow");
|
||||
command(l,"sanctuary weather vanilla");cold(l);until(l,new BlockPos(0,80,0),2000,()->l.getBlockState(new BlockPos(0,80,0)).is(Blocks.SNOW_BLOCK),"Cold biome also accumulates with vanilla weather");
|
||||
var light=new BlockPos(11,80,11);l.setBlockAndUpdate(light.below(),Blocks.GLOWSTONE.defaultBlockState());command(l,"sanctuary weather snow");l.setRainLevel(1);
|
||||
} else {
|
||||
cold(l);for(int i=0;i<40;i++)l.tickPrecipitation(AT);check(layers(l,AT)==1&&l.getBlockState(AT.above()).isAir(),"Non-Sanctuary world keeps native one-layer limit");
|
||||
}
|
||||
});
|
||||
if(enabled){c.waitTicks(8);world.getServer().runOnServer(s->{var l=s.overworld();var light=new BlockPos(11,80,11);check(l.getBrightness(LightLayer.BLOCK,light)>=10,"Test light propagated");step(l,light,400);check(l.getBlockState(light).isAir(),"Bright surfaces do not accumulate");});}
|
||||
}
|
||||
System.out.println("SNOW108_NATIVE_PASS: layers, solid blocks, vertical stacking, entity lift, caps and commands, weather, light, supports, cauldron and vanilla world");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void cold(ServerLevel l){command(l,"fillbiome -8 76 -8 8 88 8 minecraft:snowy_plains");l.getWeatherData().setRaining(true);l.setRainLevel(1);}
|
||||
private static int layers(ServerLevel l,BlockPos p){var s=l.getBlockState(p);return s.is(Blocks.SNOW)?s.getValue(SnowLayerBlock.LAYERS):0;}
|
||||
private static void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
private static void until(ServerLevel l,BlockPos p,int max,BooleanSupplier condition,String message){for(int i=0;i<max&&!condition.getAsBoolean();i++)l.tickPrecipitation(p);check(condition.getAsBoolean(),message);}
|
||||
private static void command(ServerLevel l,String s){l.getServer().getCommands().performPrefixedCommand(l.getServer().createCommandSourceStack(),s);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.building.ColoredBricks;
|
||||
import fr.koka.sanctuary.multiblock.Metatable;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.io.*;
|
||||
import java.nio.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Actual GLB texture/scene import, paid native crafting, placement, drops, reconnect and plan coexistence. */
|
||||
public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
private static final Map<Direction,BlockPos> DIRECTIONS=Map.of(Direction.NORTH,new BlockPos(0,1,4),Direction.SOUTH,new BlockPos(4,1,4),Direction.EAST,new BlockPos(-4,1,4),Direction.WEST,new BlockPos(4,1,-2));
|
||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2),SCULPTURE=new BlockPos(2,1,0),TABLE=new BlockPos(-3,1,2);
|
||||
private static final AtomicReference<VoxelSculpture> ORIGINAL=new AtomicReference<>();
|
||||
private static final AtomicReference<String> MODEL=new AtomicReference<>();
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());button(screen,"sanctuary.progression.enter").onPress(enter());intro.set(false);
|
||||
}});
|
||||
Path file=PlanClient.directory().resolve("stat106-cat-"+UUID.randomUUID()+".glb");
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try{
|
||||
System.out.println("STATUARY106_LIBRARY "+file);Files.createDirectories(file.getParent());byte[] fixture=fixture();Files.write(file,fixture);
|
||||
var asset=GlbModel.load(file);var pixels=StatueVoxelizer.miniature(asset.faces());var originalSource=VoxelSculpture.of(asset.id(),asset.name(),pixels);
|
||||
MODEL.set(originalSource.modelId());check(originalSource.height()==16&&originalSource.width()<16,"Proportions, 16 voxel longest axis");check(originalSource.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Embedded PNG retains multiple colors");
|
||||
check(CreationLibrary.files(CreationLibrary.Kind.MODEL).contains(file)&&!CreationLibrary.files(CreationLibrary.Kind.SCHEMATIC).contains(file),"One folder, different filters");
|
||||
var invalid=fixture.clone();ByteBuffer.wrap(invalid).order(ByteOrder.LITTLE_ENDIAN).putInt(8,1);reject(invalid,"GLB header length");
|
||||
var oversize=new byte[GlbModel.MAX_BYTES+1];reject(oversize,"File bound");
|
||||
reject(edit(fixture,j->j.getAsJsonArray("buffers").get(0).getAsJsonObject().addProperty("uri","https://example.invalid/mesh.bin")),"External buffer");
|
||||
reject(edit(fixture,j->j.getAsJsonArray("images").get(0).getAsJsonObject().addProperty("uri","../../texture.png")),"External texture");
|
||||
reject(edit(fixture,j->j.getAsJsonArray("accessors").get(0).getAsJsonObject().addProperty("byteOffset",Integer.MAX_VALUE)),"Accessor overflow");
|
||||
reject(edit(fixture,j->{var a=new JsonArray();a.add(0);j.getAsJsonArray("nodes").get(0).getAsJsonObject().add("children",a);}),"Cyclic scene");
|
||||
var interop=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("../../../../../build/statuary106-evidence/BoxTextured.glb").normalize();
|
||||
if(Files.exists(interop)){var box=GlbModel.read(interop);check(box.size()==12,"Independent Khronos indexed GLB");check(!StatueVoxelizer.miniature(box).isEmpty(),"Independent textured box voxelizes");System.out.println("STATUARY106_INTEROP_PASS Khronos BoxTextured GLB");}
|
||||
check(originalSource.rotate().rotate().rotate().rotate().equals(originalSource),"Four rotations preserve ID and data");
|
||||
try{new VoxelSculpture(originalSource.modelId(),"bad",16,16,16,List.of(new VoxelSculpture.Cell(0,0,0,-1),new VoxelSculpture.Cell(0,0,0,-1)));throw new AssertionError("Duplicate cell accepted");}catch(IllegalArgumentException expected){}
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Statuary106 "+UUID.randomUUID());s.setSeed("106");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var source=originalSource;
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-7;x<=9;x++)for(int z=-5;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties");
|
||||
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.FACING)==Direction.SOUTH,"Legacy state defaults to unchanged model orientation");
|
||||
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Legacy state keeps centered anchor");
|
||||
Sculpture113ClientChecks.placementContexts(p);
|
||||
for(var q:Metatable.members(TABLE))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());check(Metatable.assemble(p,TABLE),"Metabli");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
c.waitFor(m->m.level.getBlockState(WORKSHOP).is(Statuary.WORKSHOP)&&!m.player.isCreative(),200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();Sculpture113ClientChecks.geometry(m,source.modelId());});
|
||||
open(c);c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).onFilesDrop(List.of(file)));
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");
|
||||
menu.getSlot(ClayWorkshopMenu.HOTBAR_START).set(new ItemStack(Items.CLAY,3));
|
||||
p.getInventory().setItem(18,new ItemStack(Items.CLAY,64));p.getInventory().setItem(Inventory.SLOT_OFFHAND,new ItemStack(Items.CLAY,12));
|
||||
fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.CLAY,64));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(ClayWorkshopMenu.HOTBAR_START).getItem().getCount()==3,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.QUICK_MOVE,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);
|
||||
c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture"),"Crafted sculpture has translated type name EN"));
|
||||
c.takeScreenshot("statuary106-workshop-en");
|
||||
checkWorkshopLayout(c);
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
|
||||
// Hidden inventory, overflow and offhand must never join this menu or feed its input.
|
||||
c.runOnClient(m->check(!((SanctuaryContainerScreenGeometry)(Object)m.gui.screen()).sanctuary$hasIntegratedInventory(),"No expanding player-inventory panel"));
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;
|
||||
check(menu.slots.size()==11&&menu.getSlot(0).getItem().getCount()==3,"Two crafting slots plus exactly nine hotbar slots");
|
||||
check(!((fr.koka.sanctuary.inventory.SanctuaryExtendedInventoryMenu)(Object)menu).sanctuary$hasExtendedInventory(),"No hidden overflow destination");
|
||||
menu.clicked(0,Inventory.SLOT_OFFHAND,ContainerInput.SWAP,p);menu.clicked(11,0,ContainerInput.PICKUP,p);menu.clicked(-2,0,ContainerInput.PICKUP,p);
|
||||
check(menu.quickMoveStack(p,11).isEmpty()&&menu.getCarried().isEmpty(),"Out-of-menu slot rejected");
|
||||
check(menu.getSlot(0).getItem().getCount()==3&&p.getOffhandItem().getCount()==12,"Offhand swap refused");
|
||||
check(p.getInventory().getItem(18).getCount()==64&&fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).getItem(17).getCount()==64,"Reserve clay untouched");
|
||||
p.getInventory().setItem(18,ItemStack.EMPTY);p.getInventory().setItem(Inventory.SLOT_OFFHAND,ItemStack.EMPTY);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);
|
||||
});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay applies its sampled palette and preserves model ID");
|
||||
});
|
||||
c.runOnClient(m->{for(var family:ColoredBricks.families()){var palette=SculptureMaterialSampler.sample(new ItemStack(family.clay()));var colored=palette.apply(originalSource);check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().allMatch(cell->palette.colors().contains(cell.color())),"All 16 clays keep geometry and use actual texture shades");}});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM),100);
|
||||
c.waitTicks(6);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;check(menu.getSlot(0).getItem().getCount()==2,"Taking one statue costs exactly one clay");check(menu.getCarried().getCount()==1,"One portable item");});
|
||||
close(c);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);check(count(p,Items.CLAY)==2,"Closing returns unused clay");equip(p,Statuary.ITEM);p.inventoryMenu.broadcastFullState();check(p.getMainHandItem().is(Statuary.ITEM),"Crafted statue equipped on server");});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,SCULPTURE);c.waitTicks(6);
|
||||
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,100);
|
||||
server.runOnServer(s->check(((StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE)).sculpture().equals(source),"Native placement loads voxel data on server"));
|
||||
c.getInput().lookAt(SCULPTURE);c.waitTicks(5);c.takeScreenshot("statuary106-world-colors");
|
||||
// Native block loot preserves the entire model. Re-place that same dropped item.
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(SCULPTURE,true);var dropped=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(SCULPTURE).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||
check(StatuaryEntity.sculpture(dropped.getItem()).equals(source),"Native drop keeps name, ID and voxels");p.getInventory().setItem(p.getInventory().getSelectedSlot(),dropped.getItem().copy());dropped.discard();p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.level.getBlockState(SCULPTURE).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,SCULPTURE);server.waitFor(s->player(s).level().getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||
// Real use-key placements must follow the gaze on both sides of the network.
|
||||
for(var direction:Direction.Plane.HORIZONTAL){
|
||||
var pos=DIRECTIONS.get(direction);double px=pos.getX()+.5-direction.getStepX()*2,pz=pos.getZ()+.5-direction.getStepZ()*2;
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(px,1,pz);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(pos);check(state.getValue(Statuary.FACING)==direction&&state.getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Native center placement follows gaze on server");});
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture())&&e.getBlockState().getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,100);
|
||||
c.runOnClient(m->Sculpture113ClientChecks.rendered((StatuaryEntity)m.level.getBlockEntity(pos)));
|
||||
c.takeScreenshot("sculpture119-center-"+direction.getSerializedName());
|
||||
server.runOnServer(s->{var p=player(s);p.level().removeBlock(pos,false);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.level.getBlockState(pos).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,pos,direction.getOpposite());
|
||||
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(pos);check(state.getValue(Statuary.FACING)==direction,"Native placement faces gaze: "+direction);
|
||||
check(state.getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Aimed edge stored on server");
|
||||
check(state.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_90).getValue(Statuary.FACING)==direction.getClockWise(),"Structure rotation follows native directions");
|
||||
check(state.mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT).getValue(Statuary.FACING)==(direction.getAxis()==Direction.Axis.Z?direction.getOpposite():direction),"Structure mirror follows native directions");
|
||||
check(((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(source),"Placement never rewrites model voxels");});
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||
c.runOnClient(m->{var entity=(StatuaryEntity)m.level.getBlockEntity(pos);check(entity.getBlockState().getValue(Statuary.FACING)==direction,"Client receives direction");
|
||||
check(entity.getBlockState().getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Client receives aimed edge");
|
||||
Sculpture113ClientChecks.rendered(entity);});
|
||||
c.takeScreenshot("statuary111-facing-"+direction.getSerializedName());
|
||||
}
|
||||
// Re-place a dropped north-facing model while looking west: no baked or cumulative turn.
|
||||
var north=DIRECTIONS.get(Direction.NORTH);var west=DIRECTIONS.get(Direction.WEST);
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(north,true);p.level().removeBlock(west,false);
|
||||
var drop=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(north).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||
check(source.equals(StatuaryEntity.sculpture(drop.getItem())),"Direction does not alter dropped item");
|
||||
p.getInventory().setItem(p.getInventory().getSelectedSlot(),drop.getItem().copy());drop.discard();p.teleportTo(west.getX()+2.5,1,west.getZ()+.5);p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west,Direction.EAST);
|
||||
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(west).getValue(Statuary.FACING)==Direction.WEST,"Dropped statue takes new gaze"));
|
||||
server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
|
||||
// Reuse the file with colored clay; server applies the palette bound to that actual material.
|
||||
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c);
|
||||
c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture"),"Crafted sculpture has translated type name FR"));c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
checkWorkshopLayout(c);
|
||||
var expectedBlue=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow().clay())).apply(originalSource));
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.equals(expectedBlue),"Blue input uses sampled palette and model shading");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);check(p.containerMenu.getSlot(0).getItem().isEmpty(),"Shift crafting consumes stock");check(count(p,Statuary.ITEM)==2,"Shift crafting produces exactly paid quantity");});close(c);
|
||||
// At the Metabli, the same source creates a full-sized construction plan, with no placement in survival.
|
||||
server.runOnServer(s->Metatable.open(player(s),TABLE));c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->{((PlansScreen)m.gui.screen()).onFilesDrop(List.of(file));button(m.gui.screen(),"sanctuary.plans.palette.concrete").onPress(enter());});c.takeScreenshot("statuary106-metabli-fr");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter()));c.waitFor(m->!PlanClient.busy&&PlanClient.plan!=null&&m.gui.screen()==null,400);
|
||||
c.runOnClient(m->check(PlanClient.plan.height()==32&&PlanClient.plan.cells().size()>source.cells().size(),"Same source produces real-block statue at selected height"));
|
||||
server.runOnServer(s->check(((StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE)).sculpture().equals(source),"Plan generation leaves existing miniature intact"));
|
||||
// Saturated hotbar must not spill crafted statues into hidden inventory or consume unpaid clay.
|
||||
c.runOnClient(m->PlanClient.clear());open(c);c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
var savedHotbar=new AtomicReference<List<ItemStack>>();
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;savedHotbar.set(java.util.stream.IntStream.range(0,9).mapToObj(i->p.getInventory().getItem(i).copy()).toList());
|
||||
for(int i=0;i<9;i++)p.getInventory().setItem(i,new ItemStack(Items.STONE,64));menu.getSlot(0).set(new ItemStack(Items.CLAY,2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(2).getItem().is(Items.STONE),100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(0).getItem().getCount()==2&&count(p,Statuary.ITEM)==0,"Full hotbar consumes nothing and does not fill reserve");
|
||||
var nearlyFull=StatuaryEntity.item(source);nearlyFull.setCount(63);p.getInventory().setItem(8,nearlyFull);menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(10).getItem().getCount()==63,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(p.getInventory().getItem(8).getCount()==64&&count(p,Statuary.ITEM)==64&&menu.getSlot(0).getItem().getCount()==1,"Partial merge crafts only the one statue that fits");
|
||||
p.getInventory().setItem(0,ItemStack.EMPTY);menu.broadcastChanges();});
|
||||
close(c);server.waitFor(s->player(s).containerMenu==player(s).inventoryMenu,100);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getInventory().getItem(0).is(Items.CLAY)&&p.getInventory().getItem(0).getCount()==1,"Closing returns the unused clay");for(int i=0;i<9;i++)p.getInventory().setItem(i,savedHotbar.get().get(i));p.inventoryMenu.broadcastFullState();});
|
||||
// Cancel an import by closing the native menu; late worker completion cannot craft in the next session.
|
||||
open(c);c.runOnClient(m->{((ClayWorkshopScreen)m.gui.screen()).load(file);m.gui.screen().onClose();});c.waitTicks(12);
|
||||
server.runOnServer(s->check(player(s).containerMenu==player(s).inventoryMenu,"Closed workshop releases crafting session"));
|
||||
check(Arrays.equals(Files.readAllBytes(file),fixture),"Original GLB never rewritten");
|
||||
}
|
||||
Files.delete(file); // Placed models must survive loss of the source file.
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var entity=(StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE);check(entity.sculpture().equals(ORIGINAL.get()),"Server save and reload preserve source-independent voxels");check(count(player(s),Statuary.ITEM)==2,"Colored inventory statues survive reconnect");});
|
||||
for(var direction:List.of(Direction.SOUTH,Direction.EAST,Direction.WEST)){
|
||||
var pos=DIRECTIONS.get(direction);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.FACING)==direction&&((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(ORIGINAL.get()),"Direction and model survive server save/reload"));
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Anchor survives server save/reload"));
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&ORIGINAL.get().equals(e.sculpture())&&e.getBlockState().getValue(Statuary.FACING)==direction,150);
|
||||
}
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(SCULPTURE).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Centered placement survives server save/reload"));
|
||||
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->{check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file");check(m.level.getBlockState(SCULPTURE).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Fresh client receives centered anchor");});
|
||||
c.getInput().lookAt(SCULPTURE);c.takeScreenshot("statuary106-reload-no-source");
|
||||
}
|
||||
System.out.println("STATUARY106_PASS GLB textured scene, bounded import, shared folder filters, native paid crafting, 16 clay colors, model ID, shift-click, place/drop/re-place, Metabli plan, cancellation, server save/reconnect without source");
|
||||
System.out.println("STATUARY111_PASS nine-slice GUI FR/EN scales 2/3, four native gaze placements, renderer directions, structure rotation/mirror, legacy state default, drop/re-place and persisted orientation");
|
||||
System.out.println("STATUARY113_PASS clay sculpture FR/EN, native aimed edges, voxel grid across every footprint/orientation/anchor, side placement, legacy states, rotated/mirrored anchor, save/reconnect");
|
||||
System.out.println("SCULPTURE119_PASS native centered placements in four directions, center/edge boundaries, odd/even voxel grid, side anchoring and center save/reconnect");
|
||||
System.out.println("STATUARY112_PASS fixed compact hotbar-only menu, native input transfer, hidden slots/offhand rejected, full hotbar no consumption, partial merge exact cost, closing restitution");
|
||||
}catch(IOException e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(IOException ignored){}}
|
||||
}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void checkWorkshopLayout(ClientGameTestContext c){c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var refresh=button(screen,"sanctuary.statuary.refresh");int left=refresh.getX()-10,top=refresh.getY()-127;
|
||||
check(top>=0&&top+screen.panelHeight()<=screen.height,"Workshop frame fits GUI height");
|
||||
check(screen.panelHeight()==202&&screen.getMenu().slots.size()==11,"Compact height and nine player slots regardless of reserve capacity");
|
||||
for(int i=0;i<9;i++){var slot=screen.getMenu().getSlot(i+2);check(slot.container==m.player.getInventory()&&slot.getContainerSlot()==i,"Displayed player slot belongs to active native hotbar");}
|
||||
for(var slot:screen.getMenu().slots)if(slot.x>=0&&slot.y>=0)check(slot.x>3&&slot.x+17<310&&slot.y>3&&slot.y+17<screen.panelHeight(),"Every visible slot fits inside nine-slice frame");
|
||||
for(var widget:screen.children())if(widget instanceof Button b)check(b.getX()>=left&&b.getRight()<=left+310&&b.getY()>=top&&b.getBottom()<=top+145,"Every action stays clear of explanations and inventory");
|
||||
});}
|
||||
private static void close(ClientGameTestContext c){c.runOnClient(m->m.gui.screen().onClose());c.waitFor(m->m.gui.screen()==null,100);}
|
||||
private static void open(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(WORKSHOP),Direction.UP,WORKSHOP,false)));c.waitForScreen(ClayWorkshopScreen.class);}
|
||||
private static void place(ClientGameTestContext c,BlockPos pos){place(c,pos,null);}
|
||||
private static void place(ClientGameTestContext c,BlockPos pos,Direction edge){
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5+(edge==null?0:edge.getStepX()*.4),pos.getY()-.001,pos.getZ()+.5+(edge==null?0:edge.getStepZ()*.4))));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos.below())&&hit.getDirection()==Direction.UP,"Native cursor targets support top face"));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
|
||||
}
|
||||
private static int count(ServerPlayer p,Item item){int n=0;for(int i=0;i<p.getInventory().getContainerSize();i++)if(p.getInventory().getItem(i).is(item))n+=p.getInventory().getItem(i).getCount();return n;}
|
||||
private static void equip(ServerPlayer p,Item item){var inv=p.getInventory();for(int i=0;i<inv.getContainerSize();i++)if(inv.getItem(i).is(item)){var old=inv.getSelectedItem();var found=inv.getItem(i);inv.setItem(i,old);inv.setItem(inv.getSelectedSlot(),found);return;}throw new AssertionError("Missing crafted item");}
|
||||
private static void language(ClientGameTestContext c,String lang){var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(lang);return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(GuiEventListener root){return java.util.stream.Stream.concat(java.util.stream.Stream.of(root),root instanceof ContainerEventHandler container?container.children().stream().flatMap(Statuary106ClientChecks::widgets):java.util.stream.Stream.empty());}
|
||||
private static Button button(GuiEventListener root,String key){return widgets(root).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing button "+key));}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
private static void reject(byte[] bytes,String label)throws IOException{try{GlbModel.read(bytes);throw new AssertionError("Accepted "+label);}catch(IOException expected){}}
|
||||
private static byte[] edit(byte[] file,java.util.function.Consumer<JsonObject> edit){
|
||||
var input=ByteBuffer.wrap(file).order(ByteOrder.LITTLE_ENDIAN);int length=input.getInt(12);byte[] original=Arrays.copyOfRange(file,20,20+length);var json=JsonParser.parseString(new String(original,java.nio.charset.StandardCharsets.UTF_8)).getAsJsonObject();edit.accept(json);
|
||||
byte[] text=json.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);int padded=(text.length+3)/4*4;byte[] binary=Arrays.copyOfRange(file,20+length,file.length);var out=ByteBuffer.allocate(20+padded+binary.length).order(ByteOrder.LITTLE_ENDIAN);out.putInt(0x46546C67).putInt(2).putInt(out.capacity()).putInt(padded).putInt(0x4E4F534A).put(text);while(out.position()<20+padded)out.put((byte)' ');out.put(binary);return out.array();
|
||||
}
|
||||
/** A tiny authored cat built from triangle boxes, embedded 2×2 texture and a translated scene node. */
|
||||
static byte[] fixture()throws IOException{
|
||||
return fixture(false);
|
||||
}
|
||||
static byte[] fixture(boolean uniform)throws IOException{
|
||||
var xyz=new ArrayList<Float>();var uv=new ArrayList<Float>();
|
||||
for(float[] box:List.of(new float[]{.2f,0,.1f,.8f,1,.65f},new float[]{.1f,1,.05f,.9f,1.7f,.7f},new float[]{.1f,1.7f,.1f,.3f,2.1f,.5f},new float[]{.7f,1.7f,.1f,.9f,2.1f,.5f},new float[]{.75f,.1f,.2f,1.1f,.65f,.5f})){
|
||||
float x=box[0],y=box[1],z=box[2],X=box[3],Y=box[4],Z=box[5];float[][] v={{x,y,z},{X,y,z},{X,Y,z},{x,Y,z},{x,y,Z},{X,y,Z},{X,Y,Z},{x,Y,Z}};
|
||||
int[][] quads={{0,3,2,1},{5,6,7,4},{4,7,3,0},{1,2,6,5},{3,7,6,2},{4,0,1,5}};
|
||||
for(var q:quads)for(int i:new int[]{0,1,2,0,2,3}){for(float f:v[q[i]])xyz.add(f);uv.add(i==0||i==1?0f:1f);uv.add(i==0||i==3?1f:0f);}
|
||||
}
|
||||
var image=new java.awt.image.BufferedImage(2,2,java.awt.image.BufferedImage.TYPE_INT_ARGB);image.setRGB(0,0,0xFFFF9933);image.setRGB(1,0,0xFF44CCAA);image.setRGB(0,1,0xFF4477EE);image.setRGB(1,1,0xFFFFFFEE);
|
||||
if(uniform)for(int y=0;y<2;y++)for(int x=0;x<2;x++)image.setRGB(x,y,0xFF808080);
|
||||
var png=new ByteArrayOutputStream();javax.imageio.ImageIO.write(image,"png",png);
|
||||
int pbytes=xyz.size()*4,ubytes=uv.size()*4,ibytes=png.size(),total=pbytes+ubytes+ibytes;var binary=ByteBuffer.allocate((total+3)/4*4).order(ByteOrder.LITTLE_ENDIAN);for(float f:xyz)binary.putFloat(f);for(float f:uv)binary.putFloat(f);binary.put(png.toByteArray());
|
||||
String json="""
|
||||
{"asset":{"version":"2.0"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0,"translation":[3,2,-1]}],
|
||||
"meshes":[{"primitives":[{"attributes":{"POSITION":0,"TEXCOORD_0":1},"material":0}]}],
|
||||
"materials":[{"pbrMetallicRoughness":{"baseColorTexture":{"index":0}}}],"textures":[{"source":0}],"images":[{"mimeType":"image/png","bufferView":2}],
|
||||
"buffers":[{"byteLength":%d}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":%d},{"buffer":0,"byteOffset":%d,"byteLength":%d},{"buffer":0,"byteOffset":%d,"byteLength":%d}],
|
||||
"accessors":[{"bufferView":0,"componentType":5126,"count":%d,"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":%d,"type":"VEC2"}]}
|
||||
""".formatted(total,pbytes,pbytes,ubytes,pbytes+ubytes,ibytes,xyz.size()/3,uv.size()/2);
|
||||
byte[] text=json.getBytes(java.nio.charset.StandardCharsets.UTF_8);int jl=(text.length+3)/4*4;var out=ByteBuffer.allocate(28+jl+binary.capacity()).order(ByteOrder.LITTLE_ENDIAN);out.putInt(0x46546C67).putInt(2).putInt(out.capacity()).putInt(jl).putInt(0x4E4F534A).put(text);while(out.position()<20+jl)out.put((byte)' ');out.putInt(binary.capacity()).putInt(0x004E4942).put(binary.array());return out.array();
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.SanctuaryChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.structure.*;
|
||||
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
|
||||
|
||||
/** Real failing seed, native loot and traps, and cache/chunk save reload. */
|
||||
public final class Temple097ClientChecks implements FabricClientGameTest {
|
||||
private static final AtomicReference<BlockPos> HALL=new AtomicReference<>();
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
System.setProperty("sanctuary.expansion.structureDiagnostics","true");
|
||||
var lastHello=new AtomicReference<HelloWorldScreen>();
|
||||
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(m.gui.screen() instanceof HelloWorldScreen screen&&lastHello.getAndSet(screen)!=screen){
|
||||
IntroClient.setQuickTestIntro(true);fr.koka.sanctuary.gametest.Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(net.minecraft.client.gui.components.Button.class::isInstance).map(net.minecraft.client.gui.components.Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new net.minecraft.client.input.KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(net.minecraft.client.gui.components.Button.class::isInstance).map(net.minecraft.client.gui.components.Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new net.minecraft.client.input.KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0));
|
||||
}
|
||||
});
|
||||
c.runOnClient(m->{m.options.renderDistance().set(4);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
var baseline=new AtomicReference<List<String>>();TestWorldSave save;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Temple097 "+UUID.randomUUID());s.setSeed("-4700804240597771092");s.setAllowCommands(true);s.setGenerateStructures(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()) {
|
||||
save=world.getWorldSave();
|
||||
world.getServer().runOnServer(s->baseline.set(inspect(s.overworld())));
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var at=HALL.get();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);p.setNoGravity(true);p.getAbilities().flying=true;p.onUpdateAbilities();p.teleportTo(at.getX()+1.5,at.getY()+3,at.getZ()-5);});
|
||||
c.waitFor(m->m.player.distanceToSqr(HALL.get().getX()+1.5,HALL.get().getY()+3,HALL.get().getZ()-5)<2,200);
|
||||
c.getInput().lookAt(HALL.get().offset(1,0,1));c.waitTicks(25);c.takeScreenshot("catalog099-hall-metatable");
|
||||
}
|
||||
try(var world=save.open()) {
|
||||
world.getServer().runOnServer(s->{check(baseline.get().equals(inspect(s.overworld())),"Native structures and loot survive reload");var g=(SanctuaryChunkGenerator)s.overworld().getChunkSource().getGenerator();check(g.planCache().hits()>0,"Reopen restores native planning cache");});
|
||||
}
|
||||
System.out.println("TEMPLE097_PASS: supplied seed, two complete native temples, native starts, loot and traps, metadata round-trip, locate, chunk/cache reload");
|
||||
}
|
||||
private static void hall099(ServerLevel level,SanctuaryChunkGenerator host){
|
||||
var hall=host.undergroundHall22().networks().stream().flatMap(n->n.modules().stream()).filter(m->m.kind()==fr.koka.sanctuary.worldgen.city.LostCityUnderground16.Kind.EXPANSION_HALL).findFirst().orElseThrow();
|
||||
var origin=new BlockPos(hall.minX()+hall.width()/2-1,hall.minY()+3,hall.minZ()+hall.depth()/2-1);
|
||||
for(var pos:fr.koka.sanctuary.multiblock.Metatable.members(origin))level.getChunkAt(pos);
|
||||
HALL.set(origin);
|
||||
System.out.println("HALL099 states="+fr.koka.sanctuary.multiblock.Metatable.members(origin).stream().map(level::getBlockState).toList());
|
||||
check(fr.koka.sanctuary.multiblock.Metatable.state(level,origin,true)==1,"Generated hall contains an assembled central Metatable");
|
||||
for(var pos:fr.koka.sanctuary.multiblock.Metatable.members(origin))check(level.getBlockState(pos.below()).is(Blocks.SMOOTH_STONE),"Metatable rests on existing raised plinth");
|
||||
var codec=StructurePieceSerializationContext.fromLevel(level);
|
||||
var modern=new fr.koka.sanctuary.worldgen.city.LostCityUndergroundPiece20(hall,true);var tag=modern.createTag(codec);
|
||||
check(tag.getBooleanOr("SanctuaryMetatable099",false),"New hall piece persists opt-in");
|
||||
var restored=new fr.koka.sanctuary.worldgen.city.LostCityUndergroundPiece20(codec,tag);
|
||||
check(restored.createTag(codec).equals(tag),"New hall piece round-trips");
|
||||
tag.remove("SanctuaryMetatable099");var historical=new fr.koka.sanctuary.worldgen.city.LostCityUndergroundPiece20(codec,tag);
|
||||
check(!historical.createTag(codec).contains("SanctuaryMetatable099"),"Historical unmarked pieces never opt into new furniture");
|
||||
System.out.println("HALL099_PASS: assembled Metatable on central plinth at "+origin+", native saved opt-in, historical absence preserved");
|
||||
}
|
||||
private static List<String> inspect(ServerLevel level) {
|
||||
var host=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
||||
check(host.expeditionTemples002().size()==2,"Both expedition temples required");var evidence=new ArrayList<String>();
|
||||
hall099(level,host);
|
||||
for(var temple:host.expeditionTemples002()) {
|
||||
var planned=temple.start();var cp=planned.getChunkPos();var box=ExpeditionTemples002.footprint(planned);
|
||||
for(int x=box.minX()>>4;x<=box.maxX()>>4;x++)for(int z=box.minZ()>>4;z<=box.maxZ()>>4;z++)level.getChunk(x,z);
|
||||
var actual=level.structureManager().getStartForStructure(temple.structure().value(),level.getChunk(cp.x(),cp.z()));
|
||||
check(actual!=null&&actual.isValid(),"Native start exists");
|
||||
var codec=StructurePieceSerializationContext.fromLevel(level);var nbt=actual.createTag(codec,cp);var restored=StructureStart.loadStaticStart(codec,nbt,level.getSeed());
|
||||
check(restored!=null&&restored.createTag(codec,cp).equals(nbt),"NBT round trip includes optional foundation metadata");
|
||||
int chests=0,tnt=0,dispensers=0,masonry=0;
|
||||
for(var at:BlockPos.betweenClosed(box.minX(),box.minY(),box.minZ(),box.maxX(),box.maxY(),box.maxZ())) {
|
||||
var block=level.getBlockState(at);if(block.is(Blocks.CHEST))chests++;if(block.is(Blocks.TNT))tnt++;if(block.is(Blocks.DISPENSER))dispensers++;
|
||||
if(block.is(Blocks.SANDSTONE)||block.is(Blocks.COBBLESTONE)||block.is(Blocks.MOSSY_COBBLESTONE))masonry++;
|
||||
}
|
||||
foundationChecks(level,actual);
|
||||
var id=temple.structure().unwrapKey().orElseThrow().identifier().toString();boolean desert=id.endsWith("desert_pyramid");
|
||||
check(chests>=(desert?4:2)&&masonry>100,"Temple masonry and chests: "+id+" "+chests+" "+masonry);
|
||||
check(desert?tnt==9:dispensers==2,"Native traps: "+id+" TNT="+tnt+" dispensers="+dispensers);
|
||||
check(host.findNearestMapStructure(level,HolderSet.direct(temple.structure()),actual.getBoundingBox().getCenter(),1,false)!=null,"Locate finds temple");
|
||||
String row=id+" "+cp+" "+box+" chests="+chests+" TNT="+tnt+" dispensers="+dispensers+" masonry="+masonry;
|
||||
evidence.add(row);System.out.println("TEMPLE097 "+row+" metadata="+((NativeStructureMetadata23)(Object)actual).sanctuary$native23());
|
||||
}
|
||||
return List.copyOf(evidence);
|
||||
}
|
||||
static void foundationChecks(ServerLevel level,StructureStart original) {
|
||||
var codec=StructurePieceSerializationContext.fromLevel(level);var at=original.getChunkPos();
|
||||
var copy=StructureStart.loadStaticStart(codec,original.createTag(codec,at),level.getSeed());
|
||||
var old=((NativeStructureMetadata23)(Object)copy).sanctuary$native23();var box=copy.getPieces().getFirst().getBoundingBox();int bottom=box.minY()-16;
|
||||
((NativeStructureMetadata23)(Object)copy).sanctuary$native23(new NativeStructureMetadata23.Data(old.islandSeed(),old.seaLevel(),bottom));
|
||||
var encoded=copy.createTag(codec,at);var restored=StructureStart.loadStaticStart(codec,encoded,level.getSeed());
|
||||
check(((NativeStructureMetadata23)(Object)restored).sanctuary$native23().templeFoundationMinY()==bottom,"Prepared foundation survives native NBT");
|
||||
var writes=new HashMap<BlockPos,net.minecraft.world.level.block.state.BlockState>();var clipping=new AtomicReference<BoundingBox>();
|
||||
var region=(net.minecraft.world.level.WorldGenLevel)java.lang.reflect.Proxy.newProxyInstance(getClassLoader(),new Class[]{net.minecraft.world.level.WorldGenLevel.class},(proxy,method,args)->switch(method.getName()) {
|
||||
case "getMinY" -> level.getMinY();case "getMaxY" -> level.getMaxY();
|
||||
case "setBlock" -> {var pos=((BlockPos)args[0]).immutable();check(clipping.get().isInside(pos),"Foundation never writes outside current decorating chunk");writes.put(pos,(net.minecraft.world.level.block.state.BlockState)args[1]);yield true;}
|
||||
default -> throw new AssertionError("Unexpected world access: "+method.getName());
|
||||
});
|
||||
for(int x=box.minX()>>4;x<=box.maxX()>>4;x++)for(int z=box.minZ()>>4;z<=box.maxZ()>>4;z++) {
|
||||
var bounds=new BoundingBox(x*16,level.getMinY(),z*16,x*16+15,level.getMaxY()-1,z*16+15);clipping.set(bounds);ExpeditionTemples002.prepareFoundation(region,restored,bounds);
|
||||
}
|
||||
check(writes.size()==box.getXSpan()*box.getZSpan()*16,"Continuous foundation under the entire native footprint");
|
||||
check(writes.keySet().stream().allMatch(p->p.getY()>=bottom&&p.getY()<box.minY()),"Foundation leaves building and underground native room construction to vanilla");
|
||||
writes.clear();clipping.set(ExpeditionTemples002.footprint(original));ExpeditionTemples002.prepareFoundation(region,original,clipping.get());
|
||||
check(writes.isEmpty(),"Existing unmarked temples never receive terrain edits");
|
||||
}
|
||||
private static ClassLoader getClassLoader(){return Temple097ClientChecks.class.getClassLoader();}
|
||||
private static void check(boolean condition,String message){if(!condition)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.building.ColoredBricks;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.properties.WallSide;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Gameplay regressions using native placement, neighbor updates and recipe matching. */
|
||||
final class Walls118Checks {
|
||||
static void server(MinecraftServer server){
|
||||
var player=server.getPlayerList().getPlayers().getFirst();var level=player.level();
|
||||
for(int x=-1;x<=17;x++)for(int z=9;z<=13;z++)level.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
int index=0;
|
||||
for(var family:ColoredBricks.families()){
|
||||
var wall=family.wall();var state=wall.defaultBlockState();
|
||||
check(wall instanceof WallBlock&&state.is(BlockTags.WALLS)&&state.is(BlockTags.MINEABLE_WITH_PICKAXE),"Native wall type, connection and mining tags");
|
||||
check(new ItemStack(wall).is(ItemTags.WALLS),"Native wall item tag");
|
||||
var input=CraftingInput.of(3,2,Collections.nCopies(6,new ItemStack(family.bricks())));
|
||||
var recipe=server.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,level).orElseThrow().value();var result=recipe.assemble(input);
|
||||
check(result.is(wall.asItem())&&result.getCount()==6,"Six matching bricks craft six walls");
|
||||
var drops=Block.getDrops(state,level,new BlockPos(0,2,0),null,player,new ItemStack(Items.DIAMOND_PICKAXE));
|
||||
check(drops.size()==1&&drops.getFirst().is(wall.asItem())&&drops.getFirst().getCount()==1,"Wall drops its own color");
|
||||
for(int z:new int[]{10,12}){
|
||||
var pos=new BlockPos(index,1,z);
|
||||
player.teleportTo(index+.5,1,z+2.5);
|
||||
if(z==12)level.setBlockAndUpdate(pos,Blocks.WATER.defaultBlockState());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(wall));
|
||||
var hit=new BlockHitResult(Vec3.atBottomCenterOf(pos),Direction.UP,pos.below(),false);
|
||||
var placed=player.getMainHandItem().useOn(new UseOnContext(player,InteractionHand.MAIN_HAND,hit));
|
||||
check(placed.consumesAction(),"Native wall item placement: "+family.color()+" at "+pos+" = "+placed+", existing "+level.getBlockState(pos));
|
||||
check(level.getBlockState(pos).is(wall),"Placed wall keeps its color");
|
||||
check(level.getBlockState(pos).getValue(WallBlock.WATERLOGGED)==(z==12),"Native waterlogging on placement");
|
||||
}
|
||||
index++;
|
||||
}
|
||||
for(int i=1;i<15;i++){
|
||||
var pos=new BlockPos(i,1,10);var state=level.getBlockState(pos);
|
||||
check(state.getValue(WallBlock.EAST)==WallSide.LOW&&state.getValue(WallBlock.WEST)==WallSide.LOW&&!state.getValue(WallBlock.UP),"Mixed-color straight wall removes middle post");
|
||||
level.setBlockAndUpdate(pos.above(),Blocks.STONE.defaultBlockState());
|
||||
state=level.getBlockState(pos);check(state.getValue(WallBlock.EAST)==WallSide.TALL&&state.getValue(WallBlock.WEST)==WallSide.TALL,"Full block above raises connected sides");
|
||||
var vanilla=Blocks.BRICK_WALL.withPropertiesOf(state);
|
||||
check(state.getCollisionShape(level,pos).toAabbs().equals(vanilla.getCollisionShape(level,pos).toAabbs()),"Collision equals native brick wall");
|
||||
level.removeBlock(pos.above(),false);
|
||||
}
|
||||
var end=new BlockPos(16,1,10);level.setBlockAndUpdate(end,Blocks.BRICK_WALL.defaultBlockState());
|
||||
check(level.getBlockState(end.west()).getValue(WallBlock.EAST)==WallSide.LOW,"Connects to vanilla brick wall");
|
||||
saved(level);
|
||||
}
|
||||
static void creative(List<ItemStack> display){
|
||||
var colors=List.of(DyeColor.WHITE,DyeColor.LIGHT_GRAY,DyeColor.GRAY,DyeColor.BLACK,DyeColor.BROWN,DyeColor.RED,DyeColor.ORANGE,DyeColor.YELLOW,DyeColor.LIME,DyeColor.GREEN,DyeColor.CYAN,DyeColor.LIGHT_BLUE,DyeColor.BLUE,DyeColor.PURPLE,DyeColor.MAGENTA,DyeColor.PINK);
|
||||
var families=ColoredBricks.families().stream().sorted(Comparator.comparingInt(f->colors.indexOf(f.color()))).toList();
|
||||
int start=-1;for(int i=0;i<display.size();i++)if(display.get(i).is(families.getFirst().wall().asItem())){start=i;break;}
|
||||
check(start>0,"Wall series is visible in Colored Blocks");
|
||||
for(int i=0;i<16;i++)check(display.get(start+i).is(families.get(i).wall().asItem()),"Contiguous series of sixteen in native color order");
|
||||
check(display.get(start-1).is(families.getLast().slab().asItem())&&display.get(start+16).is(families.getFirst().clay().asItem()),"Wall series follows slabs and precedes clay");
|
||||
}
|
||||
static void saved(ServerLevel level){
|
||||
int i=0;for(var family:ColoredBricks.families()){
|
||||
check(level.getBlockState(new BlockPos(i,1,10)).is(family.wall()),"Persisted wall color");
|
||||
var wet=level.getBlockState(new BlockPos(i++,1,12));check(wet.is(family.wall())&&wet.getValue(WallBlock.WATERLOGGED)&&wet.getFluidState().isSource(),"Persisted waterlogged wall");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.renderer.v1.Renderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.level.block.piston.PistonBaseBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real client gestures and persisted server state; rendered UVs must change without moving geometry. */
|
||||
public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),CHEST=new BlockPos(2,1,0),DIRT=new BlockPos(-2,1,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench120 "+UUID.randomUUID());s.setSeed("120");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.STEVE_WRENCH));p.inventoryMenu.broadcastFullState();
|
||||
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
int tested=0;for(var block:BuiltInRegistries.BLOCK)for(var property:block.getStateDefinition().getProperties()){
|
||||
var before=block.defaultBlockState();var after=GoldenWrench.cycle(before,property,false);
|
||||
check(after.is(block)&&GoldenWrench.cycle(after,property,true)==before,"Every registered native property has reversible values");tested++;
|
||||
}
|
||||
check(tested>1000,"Broad block property coverage");System.out.println("WRENCH120_PROPERTIES "+tested);
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));click(c,TARGET,false,false);
|
||||
server.waitFor(s->player(s).level().getBlockState(TARGET)!=stairs,100);c.waitFor(m->m.level.getBlockState(TARGET)!=stairs,100);
|
||||
server.runOnServer(s->{var actual=player(s).level().getBlockState(TARGET);check(actual.getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING)&&actual.getValue(StairBlock.HALF)==stairs.getValue(StairBlock.HALF),"Right click changes facing only");});
|
||||
click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
|
||||
click(c,TARGET,true,false);server.runOnServer(s->check(GoldenWrench.selected(player(s),stairs).equals("half"),"Left click selects half without breaking creative block: "+GoldenWrench.selected(player(s),stairs)));
|
||||
click(c,TARGET,false,false);server.waitFor(s->player(s).level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,100);
|
||||
click(c,TARGET,true,false);server.runOnServer(s->check(GoldenWrench.selected(player(s),stairs).equals("shape"),"Next property is stair shape"));
|
||||
click(c,TARGET,false,false);server.waitFor(s->player(s).level().getBlockState(TARGET).getValue(StairBlock.SHAPE)!=StairsShape.STRAIGHT,100);
|
||||
c.takeScreenshot("wrench120-stairs");
|
||||
// A container rotates in place; inventory and block entity survive the full native interaction.
|
||||
click(c,CHEST,false,false);server.runOnServer(s->{var p=player(s);var chest=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(chest.getItem(0).is(Items.DIAMOND)&&chest.getItem(0).getCount()==23,"Chest contents retained");check(p.containerMenu==p.inventoryMenu,"Wrench does not open ordinary chest");check(chest.getBlockState().getValue(ChestBlock.FACING)!=Blocks.CHEST.defaultBlockState().getValue(ChestBlock.FACING),"Chest rotates");});
|
||||
// Ordinary survival without commands retains Steve’s full powers; dirt keeps its native ID, shape and loot.
|
||||
server.runOnServer(s->{s.getWorldData().setAllowCommands(false);player(s).setGameMode(GameType.SURVIVAL);});c.waitFor(m->!m.player.isCreative(),100);
|
||||
var original=c.computeOnClient(m->geometry(m,DIRT));c.takeScreenshot("wrench120-dirt-before");
|
||||
click(c,DIRT,false,false);server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
var rotated=c.computeOnClient(m->geometry(m,DIRT));checkUvs(original,rotated);c.takeScreenshot("wrench120-dirt-90");
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(DIRT)==Blocks.DIRT.defaultBlockState(),"Dirt type and native state unchanged"));
|
||||
click(c,DIRT,true,false);server.runOnServer(s->check(player(s).level().getBlockState(DIRT).is(Blocks.DIRT),"Left click never breaks dirt in survival"));
|
||||
click(c,DIRT,false,true);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==0,100);checkGeometry(original,c.computeOnClient(m->geometry(m,DIRT)),"Reverse restores exact UVs");
|
||||
for(int i=1;i<=4;i++){click(c,DIRT,false,false);int expected=i%4;c.waitFor(m->WrenchTextureModels.rotation(DIRT)==expected,100);}
|
||||
checkGeometry(original,c.computeOnClient(m->geometry(m,DIRT)),"Four turns restore exact original geometry and UVs");
|
||||
click(c,DIRT,false,false);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
var reload=c.computeOnClient(Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();checkGeometry(rotated,c.computeOnClient(m->geometry(m,DIRT)),"Resource reload retains texture rotation");
|
||||
// Replacing a block clears the visual receipt; blockstate edits retain it.
|
||||
server.runOnServer(s->{var l=player(s).level();var q=new BlockPos(-3,1,0);l.setBlockAndUpdate(q,Blocks.OAK_LOG.defaultBlockState());WrenchTextures.turn(l,q,1);l.setBlock(q,Blocks.OAK_LOG.defaultBlockState().setValue(RotatedPillarBlock.AXIS,Direction.Axis.X),18);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"State edit preserves texture rotation");l.removeBlock(q,false);l.setBlockAndUpdate(q,Blocks.OAK_LOG.defaultBlockState());check(WrenchTextures.rotation(l.getChunkAt(q),q)==0,"Break/replacement clears rotation");
|
||||
var p=player(s);p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure cannot edit");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.SURVIVAL);check(!GoldenWrench.allowed(p,TARGET.offset(100,0,0),InteractionHand.MAIN_HAND,Direction.UP),"Out-of-range edit refused");
|
||||
});
|
||||
// A full Metabli still assembles with two normal right clicks, then separates with sneak-use.
|
||||
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
|
||||
c.waitFor(m->m.player.distanceToSqr(7.5,1,4)<.1&&m.level.getBlockState(table).is(Blocks.CRAFTING_TABLE),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.waitFor(s->player(s).level().getBlockState(table).is(Metatable.BLOCK),100);c.waitFor(m->m.level.getBlockState(table).is(Metatable.BLOCK),100);
|
||||
click(c,table.offset(1,0,2),true,false);server.runOnServer(s->check(Metatable.state(player(s).level(),table,true)==1,"Left click cannot corrupt a multiblock's part states"));
|
||||
click(c,table.offset(1,0,2),false,true);server.waitFor(s->Metatable.state(player(s).level(),table,false)==1,100);
|
||||
// Both container assemblers and the piston assembler must win over generic state editing.
|
||||
for(var component:List.of(Blocks.BARREL,Blocks.FURNACE)){
|
||||
server.runOnServer(s->{var l=player(s).level();for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++)l.setBlockAndUpdate(table.offset(x,y,z),component.defaultBlockState());});
|
||||
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(component),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.runOnServer(s->check(MultiblockData.get(player(s).level()).at(table)!=null,"Complete container machine assembles before generic rotation"));
|
||||
click(c,table.offset(1,0,2),false,true);
|
||||
server.runOnServer(s->check(MultiblockData.get(player(s).level()).at(table)==null,"Container machine still separates"));
|
||||
}
|
||||
server.runOnServer(s->{var l=player(s).level();for(int y=2;y>=0;y--)for(int z=0;z<3;z++)for(int x=0;x<3;x++)l.setBlockAndUpdate(table.offset(x,y,z),y==0?Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING,Direction.UP):Blocks.AIR.defaultBlockState());});
|
||||
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(Blocks.PISTON),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.waitFor(s->SuperPistons.status(player(s).level(),table.offset(1,0,1),Direction.UP,false)==1,100);
|
||||
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(SuperPistons.NORMAL),100);
|
||||
click(c,table.offset(1,0,2),false,true);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(table.offset(1,0,2)).is(Blocks.PISTON),"Super piston still separates before editing its parts"));
|
||||
// An isolated barrel and piston now use generic states instead of an incomplete-structure error.
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,3.2);p.level().setBlockAndUpdate(TARGET,Blocks.BARREL.defaultBlockState());});c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.BARREL),100);
|
||||
click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(BarrelBlock.FACING)!=Blocks.BARREL.defaultBlockState().getValue(BarrelBlock.FACING),"Isolated barrel rotates"));
|
||||
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET,Blocks.PISTON.defaultBlockState()));c.waitFor(m->m.level.getBlockState(TARGET).is(Blocks.PISTON),100);
|
||||
click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(PistonBaseBlock.FACING)!=Blocks.PISTON.defaultBlockState().getValue(PistonBaseBlock.FACING),"Isolated piston rotates"));
|
||||
server.runOnServer(s->player(s).level().setBlock(TARGET,stairs.setValue(StairBlock.HALF,Half.TOP),18));saved=world.getWorldSave();
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);world.getServer().runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==1,"Server persisted dirt texture");check(l.getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Server persisted native edited state");check(((ChestBlockEntity)l.getBlockEntity(CHEST)).getItem(0).getCount()==23,"Saved chest keeps inventory");});
|
||||
c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,150);c.getInput().lookAt(DIRT);c.takeScreenshot("wrench120-reconnect");
|
||||
}
|
||||
System.out.println("WRENCH120_PASS native creative/survival clicks, reverse, properties, textures and resource reload, unchanged geometry, filled chest, multiblock assembly priority, permissions, cleanup and save/reconnect");
|
||||
}
|
||||
static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
|
||||
if(sneak){c.getInput().holdKey(o->o.keyShift);c.waitTicks(3);}
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()+.3,pos.getZ()+.5)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos),"Native cursor hits target "+pos+" instead of "+m.hitResult));
|
||||
if(left){c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);}else{c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);}
|
||||
c.waitTicks(5);if(sneak){c.getInput().releaseKey(o->o.keyShift);c.waitTicks(3);}
|
||||
}
|
||||
private static List<float[]> geometry(Minecraft m,BlockPos pos){
|
||||
var result=new ArrayList<float[]>();var state=m.level.getBlockState(pos);
|
||||
var emitter=Renderer.get().quadEmitter(q->{var data=new float[20];for(int i=0;i<4;i++){data[i*5]=q.x(i);data[i*5+1]=q.y(i);data[i*5+2]=q.z(i);data[i*5+3]=q.u(i);data[i*5+4]=q.v(i);}result.add(data);});
|
||||
m.getModelManager().getBlockStateModelSet().get(state).emitQuads(emitter,m.level,pos,state,RandomSource.create(state.getSeed(pos)),face->false);check(!result.isEmpty(),"Native model emits real textured geometry");return result;
|
||||
}
|
||||
private static void checkUvs(List<float[]> before,List<float[]> after){
|
||||
check(before.size()==after.size(),"Same face count");boolean changed=false;
|
||||
for(int q=0;q<before.size();q++)for(int v=0;v<4;v++){for(int axis=0;axis<3;axis++)check(before.get(q)[v*5+axis]==after.get(q)[v*5+axis],"Texture turn never moves geometry");changed|=before.get(q)[v*5+3]!=after.get(q)[v*5+3]||before.get(q)[v*5+4]!=after.get(q)[v*5+4];}
|
||||
check(changed,"Dirt UVs actually change");
|
||||
}
|
||||
private static void checkGeometry(List<float[]> a,List<float[]> b,String message){check(a.size()==b.size(),message);for(int i=0;i<a.size();i++)check(Arrays.equals(a.get(i),b.get(i)),message);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.piston.PistonBaseBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native golden-wrench clicks cannot create resources or split linked blocks. */
|
||||
public final class Wrench123ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),CHEST=new BlockPos(2,1,0),DIRT=new BlockPos(-2,1,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench123 "+UUID.randomUUID());s.setSeed("123");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-5;x<=8;x++)for(int z=-4;z<=6;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);equip(p,Multiblocks.WRENCH);
|
||||
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));
|
||||
Wrench120ClientChecks.click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING),"Golden wrench changes native stair facing"));
|
||||
Wrench120ClientChecks.click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
|
||||
for(int i=0;i<5;i++){Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);}
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(TARGET);check(!state.getValue(StairBlock.WATERLOGGED)&&state.getValue(StairBlock.HALF)==Half.BOTTOM&&state.getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT,"Repeated native clicks never select water, half or fake stair shapes");});
|
||||
for(int i=0;i<4;i++)Wrench120ClientChecks.click(c,CHEST,false,false);
|
||||
server.runOnServer(s->{var p=player(s);var be=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(be.getItem(0).getCount()==23&&be.getBlockState()==Blocks.CHEST.defaultBlockState(),"Single chest remains single, dry and filled after four rotations");check(p.containerMenu==p.inventoryMenu,"Ordinary rotation does not open chest");});
|
||||
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
|
||||
server.runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==0&&l.getBlockState(DIRT).is(Blocks.DIRT),"Golden wrench never adds a texture transform");regressions(player(s));});
|
||||
c.takeScreenshot("wrench123-golden-safe");
|
||||
// Finding Steve's wrench grants its full powers in ordinary survival, without operator status.
|
||||
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.SURVIVAL);s.getWorldData().setAllowCommands(false);equip(p,Multiblocks.STEVE_WRENCH);});
|
||||
c.waitFor(m->!m.player.isCreative()&&m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH),100);
|
||||
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
|
||||
server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
Wrench120ClientChecks.click(c,TARGET,false,false);Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Native Steve edit works for ordinary survival without commands");
|
||||
for(var hand:InteractionHand.values()){
|
||||
p.setItemInHand(hand,new ItemStack(Multiblocks.STEVE_WRENCH));check(GoldenWrench.allowed(p,TARGET,hand,Direction.UP),"Survival use allowed in both hands");
|
||||
GoldenWrench.change(p,DIRT,hand,Direction.UP);
|
||||
}
|
||||
check(WrenchTextures.rotation(p.level().getChunkAt(DIRT),DIRT)==3,"Both hands can freely edit textures in survival");
|
||||
p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);p.setGameMode(GameType.CREATIVE);check(GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Creative access without commands enabled");
|
||||
p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure restrictions remain");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.CREATIVE);
|
||||
var steve=new ItemStack(Multiblocks.STEVE_WRENCH);check(Boolean.TRUE.equals(steve.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE)),"Distinct enchanted Steve item");
|
||||
});
|
||||
c.waitFor(m->m.player.isCreative(),100);c.takeScreenshot("wrench123-steve-texture");
|
||||
System.out.println("WRENCH123_SAFE_PASS native clicks, water/inventory preservation, linked blocks, real neighbour shapes, support checks, ordinary survival and creative use, adventure/spectator restrictions and both hands");
|
||||
}
|
||||
// Keep the full former debug-stick contract on Steve, including UVs/save/reload, and assembly on gold.
|
||||
new Wrench120ClientChecks().runTest(c);
|
||||
System.out.println("WRENCH123_PASS safe golden wrench and Steve wrench with native regression coverage");
|
||||
}
|
||||
private static void regressions(ServerPlayer p){
|
||||
var l=p.level();
|
||||
// Water stays present when rotating a legitimately waterlogged block.
|
||||
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.WATERLOGGED,true),18);
|
||||
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Existing water is preserved");
|
||||
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING,Direction.WEST).setValue(StairBlock.SHAPE,StairsShape.INNER_LEFT),18);
|
||||
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT&&!l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Native shape recomputed against actual empty neighbours");
|
||||
var a=CHEST;var b=CHEST.east();var base=Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING,Direction.NORTH);
|
||||
l.setBlock(a,base.setValue(ChestBlock.TYPE,ChestType.LEFT),18);l.setBlock(b,base.setValue(ChestBlock.TYPE,ChestType.RIGHT),18);
|
||||
var first=(ChestBlockEntity)l.getBlockEntity(a);var second=(ChestBlockEntity)l.getBlockEntity(b);first.setItem(0,new ItemStack(Items.DIAMOND,23));second.setItem(0,new ItemStack(Items.EMERALD,17));
|
||||
var left=l.getBlockState(a);var right=l.getBlockState(b);use(p,a);use(p,b);
|
||||
check(l.getBlockState(a)==left&&l.getBlockState(b)==right&&l.getBlockEntity(a)==first&&l.getBlockEntity(b)==second&&first.getItem(0).getCount()==23&&second.getItem(0).getCount()==17,"Double chest remains linked with both original inventories");
|
||||
var q=TARGET;var door=Blocks.OAK_DOOR.defaultBlockState();l.setBlock(q,door.setValue(DoorBlock.HALF,DoubleBlockHalf.LOWER),18);l.setBlock(q.above(),door.setValue(DoorBlock.HALF,DoubleBlockHalf.UPPER),18);
|
||||
var lower=l.getBlockState(q);var upper=l.getBlockState(q.above());use(p,q);use(p,q.above());check(l.getBlockState(q)==lower&&l.getBlockState(q.above())==upper,"Both door halves protected");l.setBlock(q.above(),Blocks.AIR.defaultBlockState(),18);
|
||||
var bed=Blocks.BED.red().defaultBlockState();l.setBlock(q,bed,18);use(p,q);check(l.getBlockState(q)==bed,"Bed half protected even if malformed already");
|
||||
var extended=Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.EXTENDED,true);l.setBlock(q,extended,18);use(p,q);check(l.getBlockState(q)==extended,"Extended piston protected");
|
||||
for(var block:List.of(Blocks.PISTON_HEAD,Blocks.MOVING_PISTON)){l.setBlock(q,block.defaultBlockState(),18);use(p,q);check(l.getBlockState(q)==block.defaultBlockState(),"Attached/moving piston part protected");}
|
||||
l.setBlock(q,Blocks.OAK_LOG.defaultBlockState(),18);var axes=EnumSet.noneOf(Direction.Axis.class);for(int i=0;i<3;i++){use(p,q);axes.add(l.getBlockState(q).getValue(RotatedPillarBlock.AXIS));}check(axes.size()==3,"All three native log axes supported");
|
||||
l.setBlock(q,Blocks.PISTON.defaultBlockState(),18);var facings=EnumSet.noneOf(Direction.class);for(int i=0;i<6;i++){use(p,q);facings.add(l.getBlockState(q).getValue(PistonBaseBlock.FACING));}check(facings.size()==6,"Isolated piston supports all six native orientations");
|
||||
// Turning a wall torch toward empty air must not break it or its support.
|
||||
l.setBlock(q.north(),Blocks.STONE.defaultBlockState(),18);var torch=Blocks.WALL_TORCH.defaultBlockState().setValue(WallTorchBlock.FACING,Direction.SOUTH);l.setBlock(q,torch,18);use(p,q);check(l.getBlockState(q)==torch,"Unsupported rotation leaves wall-mounted block intact");l.setBlock(q.north(),Blocks.AIR.defaultBlockState(),18);
|
||||
l.setBlock(q,Blocks.OAK_STAIRS.defaultBlockState(),18);
|
||||
var top=GoldenWrench.cycle(l.getBlockState(q),StairBlock.HALF,false);check(top.getValue(StairBlock.HALF)==Half.TOP,"Steve still has native debug property cycle");
|
||||
// Existing visual edits are preserved; the ordinary key cannot create or reset them.
|
||||
WrenchTextures.turn(l,q,1);use(p,q);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"Golden rotation preserves saved Steve texture data");WrenchTextures.turn(l,q,-1);
|
||||
}
|
||||
private static void use(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
|
||||
private static void equip(ServerPlayer p,Item item){p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(item));p.inventoryMenu.broadcastFullState();}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Actual source texels, native GPU masks, occlusion and user controls in an isolated world. */
|
||||
public final class Bloom130ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){new PixelShadows122ClientChecks(true).runTest(c);}
|
||||
static final List<String> ORES=ores();
|
||||
private static List<String> ores(){var result=new ArrayList<String>();for(String name:List.of("iron","copper","redstone","lapis","diamond","emerald","ruby","sapphire","gold")){
|
||||
String ns=name.equals("ruby")||name.equals("sapphire")?"sanctuary:":"minecraft:";result.add(ns+name+"_ore");result.add(ns+"deepslate_"+name+"_ore");}
|
||||
result.add("minecraft:nether_quartz_ore");result.add("minecraft:nether_gold_ore");return List.copyOf(result);}
|
||||
static void run(ClientGameTestContext c,TestServerContext server){
|
||||
c.runOnClient(m->{preferences();VanillaLight.setSunshafts(false);VanillaLight.setLensFlare(false);VanillaLight.setPbr(false);VanillaLight.setSsgi(false);analyseTextures();VanillaLight.setSaturation(100);VanillaLight.setPixelShadows(false);VanillaLight.setEdgeHighlights(false);VanillaLight.setBloom(false);VanillaLight.setOreEmission(false);});
|
||||
server.runOnServer(s->{command(s,"time set 18000");
|
||||
for(int x=4;x<=20;x++)for(int z=4;z<=20;z++){
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,64,z),Blocks.CONCRETE.black().defaultBlockState());
|
||||
for(int y=65;y<=70;y++)s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
}
|
||||
for(int i=0;i<ORES.size();i++){var state=BuiltInRegistries.BLOCK.getValue(Identifier.parse(ORES.get(i))).defaultBlockState();check(state.getLightEmission()==0,"Ores do not acquire server-side light: "+ORES.get(i));s.overworld().setBlockAndUpdate(orePos(i),state);}
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());
|
||||
});
|
||||
camera(c,server,10.5,71,9.5,60);c.waitTicks(40);c.waitFor(m->m.levelRenderer.hasRenderedAllSections(),1200);
|
||||
var off=capture(c,"bloom-ores-off");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(false);VanillaLight.setBloom(true);});frames(c);
|
||||
check(luminous(emission(c))==0,"White/grey rock, ordinary surfaces and unlit lamps do not seed bloom");
|
||||
unchanged(off,capture(c,"bloom-ores-disabled"),"Disabling ore emission leaves the original scene intact");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(100);});frames(c);
|
||||
var source=emission(c);int orePixels=luminous(source);check(orePixels>100,"Actual exposed ore inclusions enter the GPU mask: "+orePixels);
|
||||
var on=capture(c,"bloom-ores-on");double oreGain=gain(off,on,null);check(oreGain>1000,"Ores visibly glow in the native night scene: "+oreGain);
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));frames(c);
|
||||
var coreOnly=capture(c,"emissive-bloom-off");
|
||||
check(gain(off,coreOnly,null)>1000,"Emissive cores survive bloom OFF");
|
||||
check(gain(off,coreOnly,source)<1,"No halo outside source texels with bloom OFF");
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated(),"Bloom OFF frees blur targets and uniforms"));
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});frames(c);
|
||||
unchanged(coreOnly,capture(c,"emissive-bloom-zero"),"Zero bloom matches OFF while retaining emissive cores");
|
||||
c.runOnClient(m->check(!Bloom.haloAllocated(),"Zero bloom frees blur resources"));
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));c.waitTicks(8);
|
||||
c.runOnClient(m->check(!Bloom.allocated()&&!Bloom.applied(),"Zero halo and disabled cores release everything"));
|
||||
unchanged(off,capture(c,null),"Disabling both restores the original scene");
|
||||
c.runOnClient(m->{VanillaLight.setOreEmission(true);VanillaLight.setBloomIntensity(35);});frames(c);
|
||||
unchanged(on,capture(c,null),"Re-enabling bloom restores its original image");
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(10));frames(c);double low=gain(off,capture(c,null),source);
|
||||
c.runOnClient(m->VanillaLight.setBloomIntensity(90));frames(c);double high=gain(off,capture(c,"bloom-strength-90"),source);
|
||||
check(high>Math.max(30,low*2),"Bloom strength changes the halo outside source pixels: low="+low+" high="+high);
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(0));frames(c);var tight=capture(c,"bloom-spread-0");
|
||||
c.runOnClient(m->VanillaLight.setBloomRadius(100));frames(c);var wide=capture(c,"bloom-spread-100");
|
||||
check(difference(tight,wide)>100,"Spread changes the real GPU halo");
|
||||
c.runOnClient(m->VanillaLight.setOreIntensity(0));frames(c);check(luminous(emission(c))==0,"Zero ore strength suppresses both cores and halos");
|
||||
unchanged(off,capture(c,null),"Zero ore strength restores the source image");
|
||||
c.runOnClient(m->{VanillaLight.setOreIntensity(100);VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);});frames(c);
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.STONE.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Covering veins with stone removes all emission: no through-wall ore glow");
|
||||
capture(c,"bloom-covered-ores");
|
||||
server.runOnServer(s->{for(int i=0;i<ORES.size();i++)s.overworld().setBlockAndUpdate(orePos(i).above(),Blocks.AIR.defaultBlockState());});c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>100,"Removing covers reveals the genuine visible inclusion masks again");
|
||||
var reload=c.computeOnClient(net.minecraft.client.Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();frames(c);
|
||||
check(luminous(emission(c))>100,"Resource reload rebuilds emitter meshes and masks");
|
||||
c.runOnClient(m->VanillaLight.setOreEmission(false));
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.TORCH.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
int torch=luminous(emission(c));check(torch>3,"A native torch seeds bloom: "+torch);capture(c,"bloom-torch");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.GLOWSTONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))>torch,"A full luminous block contributes its real textured surface");capture(c,"bloom-glowstone");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.STONE.defaultBlockState()));c.waitTicks(25);frames(c);
|
||||
check(luminous(emission(c))==0,"Replacing the light source removes its cached emission");
|
||||
server.runOnServer(s->{s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.REDSTONE_LAMP.defaultBlockState());s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.REDSTONE_BLOCK.defaultBlockState());});
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Powered lamp emits through its on-state texture");capture(c,"bloom-lamp-on");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,64,9),Blocks.CONCRETE.black().defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))==0,"Unpowered lamp loses its glow after its native state update");
|
||||
server.runOnServer(s->s.overworld().setBlockAndUpdate(new BlockPos(14,65,9),Blocks.LAVA.defaultBlockState()));
|
||||
c.waitTicks(25);frames(c);check(luminous(emission(c))>10,"Native visible lava seeds bloom");capture(c,"bloom-lava");
|
||||
server.runOnServer(s->command(s,"time set 3000"));camera(c,server,10.5,90,9.5,70);
|
||||
var direction=c.computeOnClient(m->PixelShadows.sunDirection(m.gameRenderer.gameRenderState().levelRenderState.skyRenderState.sunAngle));
|
||||
look(c,server,(float)Math.toDegrees(Math.atan2(-direction.x,direction.z)),-(float)Math.toDegrees(Math.asin(direction.y)));
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(8);var sunOff=capture(c,"bloom-sun-off");
|
||||
c.runOnClient(m->VanillaLight.setBloom(true));frames(c);var sunMask=emission(c);
|
||||
check(luminous(sunMask)>40,"The actual sun disc seeds bloom");
|
||||
double sunlight=gain(sunOff,capture(c,"bloom-sun-on"),sunMask);check(sunlight>100,"Sun halo extends beyond its source disc: "+sunlight);
|
||||
server.runOnServer(s->{var eye=s.getPlayerList().getPlayers().getFirst().getEyePosition();var center=BlockPos.containing(eye.x+direction.x*8,eye.y+direction.y*8,eye.z+direction.z*8);
|
||||
for(int x=-4;x<=4;x++)for(int y=-4;y<=4;y++)for(int z=-4;z<=4;z++)s.overworld().setBlockAndUpdate(center.offset(x,y,z),Blocks.STONE.defaultBlockState());});
|
||||
c.waitTicks(30);frames(c);check(luminous(emission(c))==0,"Solid terrain in front of the sun suppresses its bloom source");capture(c,"bloom-sun-occluded");
|
||||
c.runOnClient(m->VanillaLight.setBloom(false));c.waitTicks(6);var coveredOff=capture(c,null);
|
||||
c.runOnClient(m->{VanillaLight.setBloom(true);VanillaLight.setBloomIntensity(0);});c.waitTicks(6);c.runOnClient(m->check(!Bloom.applied()&&!Bloom.allocated(),"Zero bloom releases GPU resources"));
|
||||
unchanged(coveredOff,capture(c,null),"Zero bloom leaves no residual glow");
|
||||
c.runOnClient(m->{VanillaLight.setBloomIntensity(35);VanillaLight.setBloom(false);VanillaLight.setOreEmission(true);VanillaLight.setOreIntensity(50);});
|
||||
System.out.println("EMISSIVE131_PASS independent cores with halo OFF/zero; blur resources released; defaults; masks for 20 ore textures; selective night glow; halo intensity/spread; no through-wall glow; live edits; torch/glowstone/lamp/lava/sun; resource reload; defaults/migration; orePixels="+orePixels+" oreGain="+oreGain+" haloLow="+low+" haloHigh="+high+" sunHalo="+sunlight);
|
||||
LensFlare136ClientChecks.run(c,server);
|
||||
}
|
||||
private static BlockPos orePos(int i){return new BlockPos(8+i%5,65,8+i/5);}
|
||||
private static void preferences(){try{
|
||||
var path=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-shaders.json");Files.deleteIfExists(path);forgetPreferences();
|
||||
check(VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==128&&VanillaLight.shadowPixels()==16&&VanillaLight.edgeDistance()==32,"Fresh requested defaults: shadows ON, 128; highlights 32; shared precision 16");
|
||||
check(VanillaLight.bloom()&&VanillaLight.oreEmission()&&VanillaLight.edgeHighlights()&&VanillaLight.bloomIntensity()==20&&VanillaLight.bloomRadius()==50,"Bloom, ore emission and edge highlights default to ON");
|
||||
Files.writeString(path,"{\"bloom\":false,\"oreEmission\":false,\"edgeHighlights\":false,\"pixelShadows\":false,\"shadowDistance\":64,\"shadowPixels\":8,\"edgeDistance\":16}");forgetPreferences();
|
||||
check(!VanillaLight.bloom()&&!VanillaLight.oreEmission()&&!VanillaLight.edgeHighlights()&&!VanillaLight.pixelShadows()&&VanillaLight.shadowDistance()==64&&VanillaLight.shadowPixels()==8&&VanillaLight.edgeDistance()==16,"Existing user choices are retained");
|
||||
for(int n:new int[]{-10,0,35,100,400}){VanillaLight.setBloomIntensity(n);VanillaLight.setBloomRadius(n);VanillaLight.setOreIntensity(n);forgetPreferences();check(VanillaLight.bloomIntensity()==Math.clamp(n,0,100)&&VanillaLight.bloomRadius()==Math.clamp(n,0,100)&&VanillaLight.oreIntensity()==Math.clamp(n,0,100),"Persisted bloom controls are bounded");}
|
||||
VanillaLight.setBloomIntensity(35);VanillaLight.setBloomRadius(50);VanillaLight.setOreIntensity(50);VanillaLight.setShadowPixels(16);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}}
|
||||
private static void analyseTextures(){
|
||||
var report=new StringBuilder("texture,selected,total,neutral_rock_selected\n");
|
||||
try(var materials=new BloomMaterials();var sheet=new NativeImage(5*16*6,4*32*6,false)){
|
||||
sheet.fillRect(0,0,sheet.getWidth(),sheet.getHeight(),0xFF16191E);
|
||||
for(int index=0;index<ORES.size();index++){
|
||||
var block=Identifier.parse(ORES.get(index));var id=block.withPath("block/"+block.getPath());
|
||||
try(var image=BloomMaterials.read(id)){
|
||||
int w=image.getWidth(),h=image.getHeight();int[] pixels=new int[w*h];for(int y=0;y<h;y++)for(int x=0;x<w;x++)pixels[y*w+x]=image.getPixel(x,y);
|
||||
var mask=materials.mask(id,pixels,w,h,true,false);int count=0,grey=0;
|
||||
for(int i=0;i<mask.length;i++){if(mask[i]>0){count++;int c=pixels[i],r=c>>16&255,g=c>>8&255,b=c&255;if(r==g&&g==b&&r<215)grey++;}}
|
||||
check(count>=4&&count<w*h*.7,"Texture has a selective inclusion mask: "+id+" "+count+"/"+mask.length);
|
||||
if(!id.getPath().contains("quartz"))check(grey==0,"Neutral host rock never emits: "+id);
|
||||
report.append(id).append(',').append(count).append(',').append(mask.length).append(',').append(grey).append('\n');
|
||||
for(int y=0;y<32*6;y++)for(int x=0;x<16*6;x++){
|
||||
int source=(y/6%16)*w/16*w+(x/6)*w/16;int color=pixels[source];if(y>=16*6&&mask[source]==0)color=0xFF16191E;
|
||||
sheet.setPixel((index%5)*16*6+x,(index/5)*32*6+y,color);
|
||||
}
|
||||
}
|
||||
}
|
||||
var root=FabricLoader.getInstance().getGameDir();sheet.writeToFile(root.resolve("bloom130-ore-masks.png"));Files.writeString(root.resolve("bloom130-ore-masks.csv"),report.toString());
|
||||
System.out.println("BLOOM130_MASKS\n"+report);
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}
|
||||
}
|
||||
private static void frames(ClientGameTestContext c){int before=c.computeOnClient(m->Bloom.renderedFrames());c.waitFor(m->Bloom.applied()&&Bloom.ready()&&Bloom.renderedFrames()>before+10,1200);c.waitTicks(8);}
|
||||
private static Frame emission(ClientGameTestContext c){var result=new CompletableFuture<Frame>();c.runOnClient(m->Screenshot.takeScreenshot(Bloom.emissionTarget(),image->{try(image){result.complete(new Frame(image.getWidth(),image.getHeight(),image.getPixels()));}}));c.waitFor(m->result.isDone(),200);return result.join();}
|
||||
private static int luminous(Frame frame){int count=0;for(int color:frame.pixels())if((color&0xFFFFFF)!=0)count++;return count;}
|
||||
private static double gain(Frame off,Frame on,Frame source){double result=0;for(int i=0;i<off.pixels().length;i++)if(source==null||(source.pixels()[i]&0xFFFFFF)==0)result+=Math.max(0,brightness(on.pixels()[i])-brightness(off.pixels()[i]));return result;}
|
||||
private static int difference(Frame a,Frame b){int n=0;for(int i=0;i<a.pixels().length;i++)if(channelDifference(a.pixels()[i],b.pixels()[i])>3)n++;return n;}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user