Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,14 @@ 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.122** adapte la sélection et les collisions des sculptures
|
||||
à leurs voxels réels et supprime leurs ombres de blocs pleins. Les modèles
|
||||
restent alignés au centre ou au bord choisi, dans les quatre orientations.
|
||||
Le [ticket de livraison](docs/sculpture-shape-light-beta122.md) détaille la
|
||||
forme physique, les règles de lumière et les vérifications réussies.
|
||||
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 +35,252 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
## 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 +2146,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 +2175,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,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.
|
||||
@@ -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,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.
|
||||
+198
-1
@@ -8,11 +8,179 @@ 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.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 +1934,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,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,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,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,90 @@
|
||||
# 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
|
||||
|
||||
Artefacts prêts ; publication et synchronisation à effectuer.
|
||||
@@ -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,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.
|
||||
+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.122
|
||||
pack_version=beta.122
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,32 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
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 +149,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 +191,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 +452,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 +1148,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");
|
||||
}
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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 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");
|
||||
deposit(l,AT,16);check(l.getBlockState(AT.above()).is(Blocks.SNOW_BLOCK),"Default two-block accumulation retained");
|
||||
clear(l);step(l,AT,1);
|
||||
check(layers(l,AT.above())==7&&layers(l,AT)==8,"Summer melts only one top layer");
|
||||
step(l,AT,7);check(l.getBlockState(AT.above()).isAir()&&layers(l,AT)==8,"Top block melts completely before the lower block");
|
||||
step(l,AT,8);check(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);step(l,base,16);check(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);step(l,atop,16);check(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());step(l,roof,1);check(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);step(l,roof,64);check(l.getBlockState(roof).isAir(),"Spring progressively melts");
|
||||
deposit(l,roof,8);clock(l,"2026-10-10T12:00:00Z",SeasonalWeather.Season.AUTUMN);clear(l);step(l,roof,128);check(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");step(l,PERSIST,16);
|
||||
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);step(l,pos,count);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 void step(ServerLevel l,BlockPos p,int n){for(int i=0;i<n;i++)l.tickPrecipitation(p);}
|
||||
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);}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
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:SculptureAnchor.values()){
|
||||
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 5120 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");default -> {}
|
||||
}
|
||||
if(anchor!=SculptureAnchor.WEST&&anchor!=SculptureAnchor.EAST)check(Math.abs(minX-(1-maxX))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
if(anchor!=SculptureAnchor.NORTH&&anchor!=SculptureAnchor.SOUTH)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->SculptureMaterialSampler.sample(new ItemStack(item)).apply(source));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);}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
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.*;
|
||||
|
||||
/** Native picking/collision and light propagation, compared with the actual emitted 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("sculpture122-voxel-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.north()),"Native cursor passes through the sculpture hole to the block behind"));
|
||||
c.takeScreenshot("sculpture122-pick-through-hole");
|
||||
// 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, voxel outline and picking through holes, authoritative 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");
|
||||
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 voxels, allows empty space, 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,"Selection and native collision share cached shape");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.MISS,"Outline ray passes through empty arch center");
|
||||
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.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query leaves hole empty");
|
||||
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()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor);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(volume==model.cells().size()/4096d,"Shape contains exactly the occupied volume");
|
||||
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
|
||||
for(var q:render.mesh){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");
|
||||
var normal=switch(q.side()){case 0->Direction.NORTH;case 1->Direction.SOUTH;case 2->Direction.WEST;case 3->Direction.EAST;case 4->Direction.UP;default->Direction.DOWN;};var neighbor=center.add(normal.getStepX()/16d,normal.getStepY()/16d,normal.getStepZ()/16d);check(boxes.stream().noneMatch(b->b.contains(neighbor)),"Rendered exposed face is also exposed physically");}
|
||||
cases++;
|
||||
}
|
||||
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native voxel shapes agree with emitted meshes, including holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
|
||||
}
|
||||
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);}
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
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 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");
|
||||
for(int i=1;i<=7;i++){l.tickPrecipitation(AT);check(layers(l,AT)==i,"One layer per precipitation event: "+i);}
|
||||
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);
|
||||
l.tickPrecipitation(AT);check(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();
|
||||
l.tickPrecipitation(AT);check(layers(l,AT.above())==1,"New layer begins above solid snow");
|
||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
||||
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");
|
||||
for(int i=0;i<30;i++)l.tickPrecipitation(AT);
|
||||
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");
|
||||
for(int i=0;i<16;i++)l.tickPrecipitation(AT);
|
||||
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");l.tickPrecipitation(fresh);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);l.tickPrecipitation(fresh);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);l.tickPrecipitation(fresh);check(l.getBlockState(fresh).isAir(),"No accumulation between showers");
|
||||
command(l,"sanctuary weather snow_showers");l.setRainLevel(1);l.getWeatherData().setRaining(true);l.tickPrecipitation(fresh);check(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());l.tickPrecipitation(flower);check(l.getBlockState(flower).is(Blocks.DANDELION),"Plant preserved");
|
||||
var water=new BlockPos(-7,79,-7);l.setBlockAndUpdate(water,Blocks.WATER.defaultBlockState());l.tickPrecipitation(water);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());l.tickPrecipitation(roof);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);for(int i=0;i<8;i++)l.tickPrecipitation(new BlockPos(0,80,0));check(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");for(int i=0;i<16;i++)l.tickPrecipitation(light);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 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=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.CLAY)).apply(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.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.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");});
|
||||
// Survival uses exactly the same gestures; dirt keeps its native ID, shape and loot.
|
||||
server.runOnServer(s->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);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");
|
||||
}
|
||||
private 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);}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
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.*;
|
||||
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.multiplayer.SessionSearchTrees;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
|
||||
public final class Tooltip093ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(m.gui.screen() instanceof IntroScreen s)s.onClose();
|
||||
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("Tooltip093 "+UUID.randomUUID());s.setSeed("42");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()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->s.getPlayerList().getPlayers().getFirst().setGameMode(GameType.CREATIVE));
|
||||
var eggs=c.computeOnClient(m->BuiltInRegistries.ITEM.stream().filter(SpawnEggItem.class::isInstance).map(ItemStack::new).filter(e->CompanionTooltips.type(e)!=null).toList());
|
||||
check(eggs.size()==88,"All 88 familiar eggs covered");Tooltip093Probe.armed=true;
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(locale);m.options.languageCode=locale;return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();
|
||||
for(boolean details:new boolean[]{false,true}){
|
||||
c.runOnClient(m->{ProgressionClient.operator=false;CompanionClient.studied=details;});
|
||||
var context=c.computeOnClient(m->Item.TooltipContext.of(m.level));
|
||||
var player=c.computeOnClient(m->m.player);
|
||||
var async=CompletableFuture.supplyAsync(()->eggs.stream().map(e->e.getTooltipLines(context,player,TooltipFlag.NORMAL)).toList());
|
||||
c.waitFor(m->async.isDone(),300);var workerLines=async.join();
|
||||
c.runOnClient(m->{for(int i=0;i<eggs.size();i++){
|
||||
var visible=eggs.get(i).getTooltipLines(context,player,TooltipFlag.NORMAL);
|
||||
check(text(visible).equals(text(workerLines.get(i))),"Search retains the entire displayed description: "+locale+" / "+eggs.get(i));
|
||||
check(visible.size()>2,"Familiar description is present");
|
||||
check(visible.stream().noneMatch(t->t.getString().startsWith("sanctuary.")),"Descriptions remain translated");
|
||||
}});
|
||||
// Exactly the native asynchronous path from the supplied crash, without joining on the render thread.
|
||||
var search=c.computeOnClient(m->{var trees=new SessionSearchTrees();trees.updateCreativeTooltips(m.level.registryAccess(),eggs);return trees;});
|
||||
var indexed=CompletableFuture.supplyAsync(search::creativeNameSearch);c.waitFor(m->indexed.isDone(),300);var tree=indexed.join();
|
||||
String query=c.computeOnClient(m->eggs.getFirst().getHoverName().getString().toLowerCase(Locale.ROOT));
|
||||
check(!tree.search(query).isEmpty(),"Native creative search resolves the indexed egg");
|
||||
c.runOnClient(m->search.rebuildAfterLanguageChange());var again=CompletableFuture.supplyAsync(search::creativeNameSearch);c.waitFor(m->again.isDone(),300);check(!again.join().search(query).isEmpty(),"Language rebuild retains indexed results");
|
||||
System.out.println("TOOLTIP093_INDEX_PASS locale="+locale+" details="+details);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(new CreativeModeInventoryScreen(m.player,m.level.enabledFeatures(),false)));c.waitTicks(8);
|
||||
// Native creative inventory selects the search tab when its search key is pressed.
|
||||
c.getInput().pressKey(o->o.keyChat);c.waitTicks(4);
|
||||
c.getInput().typeChars("w");c.waitTicks(5);c.getInput().typeChars("olf");c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.gui.screen() instanceof CreativeModeInventoryScreen,"Typing in creative inventory remains usable"));
|
||||
c.takeScreenshot("tooltip093-creative-search-"+locale);c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
System.out.println("TOOLTIP093_PASS: worker tooltips and native search/rebuild, all 88 eggs, cryptic/detailed FR/EN, resource reload and creative typing");
|
||||
}finally{Tooltip093Probe.armed=false;intro.set(false);}
|
||||
}
|
||||
private static String text(List<Component> lines){return lines.stream().map(Component::getString).reduce("",String::concat).replaceAll("\\s+","");}
|
||||
private static void check(boolean v,String message){if(!v)throw new AssertionError(message);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Fourneau094Probe {
|
||||
public record Emission(ParticleOptions type,Vec3 point){}
|
||||
public static boolean armed;
|
||||
public static final List<Emission> emissions=new ArrayList<>();
|
||||
public static void record(ParticleOptions type,double x,double y,double z){
|
||||
if(armed)emissions.add(new Emission(type,new Vec3(x,y,z)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
/** Test-only guard: even a warm font cache must never conceal off-thread glyph access. */
|
||||
public final class Tooltip093Probe {
|
||||
public static volatile boolean armed;
|
||||
private Tooltip093Probe() {}
|
||||
public static void fontAccess(){
|
||||
if(armed&&!net.minecraft.client.Minecraft.getInstance().isSameThread())
|
||||
throw new AssertionError("TOOLTIP093_OFF_THREAD_FONT: tooltip indexing accessed the rendering font");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Fourneau094Probe;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ClientLevel.class)
|
||||
public abstract class Fourneau094ParticleProbeMixin {
|
||||
@Inject(method="addParticle(Lnet/minecraft/core/particles/ParticleOptions;DDDDDD)V",at=@At("HEAD"))
|
||||
private void sanctuary$particle(ParticleOptions type,double x,double y,double z,double vx,double vy,double vz,CallbackInfo ci){
|
||||
Fourneau094Probe.record(type,x,y,z);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package fr.koka.sanctuary.gametest.mixin;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Tooltip093Probe;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(Font.class)
|
||||
abstract class Tooltip093FontProbeMixin {
|
||||
@Inject(method={"getSplitter","width"},at=@At("HEAD"))
|
||||
private void verifyThread(CallbackInfoReturnable<?> ci){Tooltip093Probe.fontAccess();}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"client": [
|
||||
"Fourneau094ParticleProbeMixin",
|
||||
"Tooltip093FontProbeMixin",
|
||||
"ClientVsync046Mixin",
|
||||
"Inventory012RenderProbeMixin",
|
||||
"Atlas026ScreenProbeMixin",
|
||||
|
||||
@@ -43,6 +43,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
fr.koka.sanctuary.building.ColoredBricks.register();
|
||||
fr.koka.sanctuary.sky.VortexClouds.register();
|
||||
fr.koka.sanctuary.loading.PreloadProgress.register();
|
||||
fr.koka.sanctuary.progression.SanctuaryGameplay.register();
|
||||
@@ -70,6 +71,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
fr.koka.sanctuary.realtime.RealtimeService.register();
|
||||
fr.koka.sanctuary.weather.WeatherService.register();
|
||||
fr.koka.sanctuary.plans.PlanService.register();
|
||||
fr.koka.sanctuary.plans.Statuary.register();
|
||||
fr.koka.sanctuary.food.FoodSync.register();
|
||||
fr.koka.sanctuary.food.FoodKnowledgeService.register();
|
||||
fr.koka.sanctuary.grave.GraveService.register();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package fr.koka.sanctuary.building;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.*;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
|
||||
/** Native building blocks; no world-generation, ticking entities or custom inventory. */
|
||||
public final class ColoredBricks {
|
||||
public record Family(DyeColor color,Block bricks,Block slab,Block stairs,Block wall,Block clay,Item clayBall,Item brick){}
|
||||
private static final List<Family> FAMILIES=new ArrayList<>();
|
||||
// The order used by the native 26.3 Colored Blocks tab, rather than dye numeric IDs.
|
||||
private static final List<DyeColor> CREATIVE_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);
|
||||
public static List<Family> families(){return List.copyOf(FAMILIES);}
|
||||
public static void register(){
|
||||
for(var color:DyeColor.values()){
|
||||
String name=color.getName();
|
||||
var bricks=block(name+"_bricks",new Block(properties(name+"_bricks",Blocks.BRICKS,color)));
|
||||
var slab=block(name+"_brick_slab",new SlabBlock(properties(name+"_brick_slab",Blocks.BRICK_SLAB,color)));
|
||||
var stairs=block(name+"_brick_stairs",new Stairs(bricks,properties(name+"_brick_stairs",Blocks.BRICK_STAIRS,color)));
|
||||
var wall=block(name+"_brick_wall",new WallBlock(properties(name+"_brick_wall",Blocks.BRICK_WALL,color)));
|
||||
var clay=block(name+"_clay",new Block(properties(name+"_clay",Blocks.CLAY,color)));
|
||||
var clayBall=item(name+"_clay_ball");var brick=item(name+"_brick");
|
||||
FAMILIES.add(new Family(color,bricks,slab,stairs,wall,clay,clayBall,brick));
|
||||
}
|
||||
var display=FAMILIES.stream().sorted(Comparator.comparingInt(f->CREATIVE_COLORS.indexOf(f.color()))).toList();
|
||||
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.COLORED_BLOCKS).register(out->{
|
||||
for(var f:display)out.accept(f.bricks);
|
||||
for(var f:display)out.accept(f.stairs);
|
||||
for(var f:display)out.accept(f.slab);
|
||||
for(var f:display)out.accept(f.wall);
|
||||
for(var f:display)out.accept(f.clay);
|
||||
});
|
||||
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.INGREDIENTS).register(out->{
|
||||
for(var f:display)out.accept(f.clayBall);
|
||||
for(var f:display)out.accept(f.brick);
|
||||
});
|
||||
}
|
||||
private static BlockBehaviour.Properties properties(String id,Block source,DyeColor color){
|
||||
return BlockBehaviour.Properties.ofFullCopy(source).mapColor(color.getMapColor()).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));
|
||||
}
|
||||
private static Block block(String name,Block block){
|
||||
var id=SanctuaryMod.id(name);Registry.register(BuiltInRegistries.BLOCK,id,block);
|
||||
Registry.register(BuiltInRegistries.ITEM,id,new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,id)).useBlockDescriptionPrefix()));return block;
|
||||
}
|
||||
private static Item item(String name){var id=SanctuaryMod.id(name);return Registry.register(BuiltInRegistries.ITEM,id,new Item(new Item.Properties().setId(ResourceKey.create(Registries.ITEM,id))));}
|
||||
private static final class Stairs extends StairBlock {Stairs(Block base,BlockBehaviour.Properties properties){super(base.defaultBlockState(),properties);}}
|
||||
private ColoredBricks(){}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import com.mojang.blaze3d.Blaze3D;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */
|
||||
public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> {
|
||||
private static final Identifier FRAME=fr.koka.sanctuary.SanctuaryMod.id("container/clay_workshop/frame");
|
||||
private static final Identifier SLOT=Identifier.withDefaultNamespace("container/slot");
|
||||
private static final Identifier SELECTION=fr.koka.sanctuary.SanctuaryMod.id("hud/hotbar_selection");
|
||||
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary clay models");t.setDaemon(true);return t;});
|
||||
private List<Path> files=List.of();private int page,generation,ticks;private boolean busy,pending;
|
||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastMaterial=ItemStack.EMPTY;
|
||||
private SculpturePalette palette;private Object sampledResources;
|
||||
private Component status=text("choose_model");private Button rotate;
|
||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,202);}
|
||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.statuary."+key,args);}
|
||||
public static void register(){
|
||||
net.minecraft.client.gui.screens.MenuScreens.register(Statuary.MENU,ClayWorkshopScreen::new);StatuaryRenderer.register();
|
||||
net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{
|
||||
if(!stack.is(Statuary.ITEM))return;var sculpture=StatuaryEntity.sculpture(stack);if(sculpture!=null){
|
||||
if(!stack.getHoverName().getString().equals(Statuary.BLOCK.getName().getString()))lines.add(Statuary.BLOCK.getName());
|
||||
lines.add(text("model_name",sculpture.name()));if(flags.isAdvanced())lines.add(Component.literal(sculpture.modelId()));}
|
||||
});
|
||||
}
|
||||
@Override protected void init(){
|
||||
super.init();
|
||||
try{files=CreationLibrary.files(CreationLibrary.Kind.MODEL);}catch(Exception e){status=ModelImportErrors.message(e);}
|
||||
page=Math.min(page,Math.max(0,(files.size()-1)/4));
|
||||
for(int i=0;i<4;i++){int at=page*4+i;if(at>=files.size())break;var file=files.get(at);
|
||||
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+32+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
||||
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+106,22,18).build()).active=page>0;
|
||||
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+106,78,18).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+106,22,18).build()).active=(page+1)*4<files.size();
|
||||
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+127,128,18).build());
|
||||
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+148,topPos+127,152,18).build());rotate.active=source!=null&&!busy;
|
||||
rotate.setTooltip(Tooltip.create(text("placement")));
|
||||
}
|
||||
private void folder(){try{Files.createDirectories(CreationLibrary.directory());Blaze3D.openPath(CreationLibrary.directory());}catch(Exception e){status=ModelImportErrors.message(e);}}
|
||||
public void load(Path file){
|
||||
if(busy)return;busy=true;status=text("working");int token=++generation;
|
||||
CompletableFuture.supplyAsync(()->{try{var asset=GlbModel.load(file);return VoxelSculpture.of(asset.id(),asset.name(),StatueVoxelizer.miniature(asset.faces()));}catch(Exception e){throw new CompletionException(e);}},WORKER)
|
||||
.whenComplete((sculpture,error)->net.minecraft.client.Minecraft.getInstance().execute(()->{
|
||||
if(token!=generation||minecraft.gui.screen()!=this||minecraft.player==null||minecraft.player.containerMenu!=menu)return;
|
||||
busy=false;if(error!=null){status=ModelImportErrors.message(error.getCause());return;}
|
||||
source=sculpture;pending=true;status=text("ready",sculpture.name());
|
||||
}));
|
||||
}
|
||||
@Override protected void containerTick(){
|
||||
super.containerTick();rotate.active=source!=null&&!busy;
|
||||
var material=menu.getSlot(0).getItem();var resources=minecraft.getModelManager().getBlockStateModelSet();
|
||||
boolean changedMaterial=!ItemStack.isSameItemSameComponents(lastMaterial,material)||sampledResources!=resources;
|
||||
if(previewSource!=source||changedMaterial){
|
||||
previewSource=source;lastMaterial=material.copy();sampledResources=resources;
|
||||
if(changedMaterial)palette=SculptureMaterialSampler.sample(material);
|
||||
var colored=source==null?null:palette==null?source:palette.apply(source);
|
||||
preview=colored==null?ItemStack.EMPTY:StatuaryEntity.item(colored);pending=true;
|
||||
if(source!=null&&!busy)status=palette==null?text("choose_material"):text("material_ready",material.getHoverName());
|
||||
}
|
||||
// Retry a selection that raced the input packet or server rate limit. The result is the
|
||||
// acknowledgement; never predict consumption or manufacture an item on the client.
|
||||
boolean needsResult=source!=null&&palette!=null&&!ItemStack.matches(preview,menu.getSlot(1).getItem());
|
||||
if(++ticks>=5&&(pending||needsResult)){
|
||||
ticks=0;pending=false;
|
||||
if(source!=null&&palette!=null)ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,menu.view.token(),source,material.getItem(),palette));
|
||||
}
|
||||
|
||||
}
|
||||
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())load(files.getFirst());}
|
||||
@Override public void removed(){generation++;super.removed();}
|
||||
int panelHeight(){return imageHeight;}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float partial){
|
||||
g.blitSprite(RenderPipelines.GUI_TEXTURED,FRAME,leftPos,topPos,imageWidth,panelHeight());
|
||||
g.fill(leftPos+148,topPos+23,leftPos+300,topPos+92,0xFF8B8B8B);
|
||||
g.fill(leftPos+149,topPos+24,leftPos+299,topPos+91,0xFF373737);
|
||||
for(var slot:menu.slots)if(slot.x>=0&&slot.y>=0)g.blitSprite(RenderPipelines.GUI_TEXTURED,SLOT,leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
||||
if(minecraft.player!=null)g.blitSprite(RenderPipelines.GUI_TEXTURED,SELECTION,
|
||||
leftPos+ClayWorkshopMenu.HOTBAR_X-3+minecraft.player.getInventory().getSelectedSlot()*18,topPos+ClayWorkshopMenu.HOTBAR_Y-3,22,21);
|
||||
if(!preview.isEmpty()){
|
||||
g.pose().pushMatrix();g.pose().translate(leftPos+194,topPos+27);g.pose().scale(3.75f,3.75f);g.item(preview,0,0);g.pose().popMatrix();
|
||||
}
|
||||
g.text(font,Component.literal("→"),leftPos+254,topPos+111,0xFF404040,false);
|
||||
var detail=font.split(status,290);for(int i=0;i<Math.min(2,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFF404040,false);
|
||||
if(detail.size()>2&&mx>=leftPos+10&&mx<leftPos+300&&my>=topPos+148&&my<topPos+174)g.setTooltipForNextFrame(font,status,mx,my);
|
||||
if(files.isEmpty())g.text(font,text("empty"),leftPos+10,topPos+32,0xFF404040,false);
|
||||
}
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor g,int mx,int my){
|
||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("models"),10,22,0xFF404040,false);
|
||||
g.text(font,text("input"),230-font.width(text("input"))/2,95,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),95,0xFF303030,false);
|
||||
}
|
||||
ItemStack preview(){return preview;}
|
||||
public boolean busy(){return busy;}
|
||||
public VoxelSculpture selected(){return source;}
|
||||
}
|
||||
@@ -62,8 +62,7 @@ public final class CompanionTooltips {
|
||||
for(var technique:java.util.List.of(initiative,profile.signature())){
|
||||
var text=FamiliarClient.name(technique).copy();
|
||||
if(details)text.append(" — ").append(FamiliarClient.help(technique));
|
||||
var m=net.minecraft.client.Minecraft.getInstance();int width=Math.max(100,Math.min(280,m.getWindow().getGuiScaledWidth()-32));
|
||||
for(var part:m.font.getSplitter().splitLines(text,width,net.minecraft.network.chat.Style.EMPTY))extra.add(Component.literal(part.getString()).withStyle(ChatFormatting.GRAY));
|
||||
addWrapped(extra,text);
|
||||
}
|
||||
if(details)extra.add(CompanionClient.label("cooldown_detail",profile.signature().cooldown()/20).copy().withStyle(ChatFormatting.DARK_GRAY));
|
||||
lines.addAll(Math.min(1,lines.size()),extra);return;
|
||||
@@ -71,14 +70,24 @@ public final class CompanionTooltips {
|
||||
for(var power:new String[]{"passive","active"}){
|
||||
var description=Component.translatable("sanctuary.companion."+type.id()+"."+power+(details?"":"_name"));
|
||||
var line=CompanionClient.label("tooltip_"+power,description);
|
||||
if(details){
|
||||
var m=net.minecraft.client.Minecraft.getInstance();int width=Math.max(100,Math.min(280,m.getWindow().getGuiScaledWidth()-32));
|
||||
for(var part:m.font.getSplitter().splitLines(line,width,net.minecraft.network.chat.Style.EMPTY))
|
||||
extra.add(Component.literal(part.getString()).withStyle(ChatFormatting.GRAY));
|
||||
}else extra.add(line.copy().withStyle(ChatFormatting.GRAY));
|
||||
if(details)addWrapped(extra,line);
|
||||
else extra.add(line.copy().withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
if(details)extra.add(CompanionClient.label("cooldown_detail",CompanionClient.config.entry(type).cooldownSeconds()).copy().withStyle(ChatFormatting.DARK_GRAY));
|
||||
lines.addAll(Math.min(1,lines.size()),extra);
|
||||
});
|
||||
}
|
||||
private static void addWrapped(java.util.List<Component> lines,Component text){
|
||||
var m=net.minecraft.client.Minecraft.getInstance();
|
||||
// Creative search (and item viewers) build tooltips on workers. Font measurement
|
||||
// can upload a new glyph to the GPU, so even measuring text must stay on the client thread.
|
||||
// Never enqueue and wait here: the client may itself be waiting for the search index.
|
||||
if(!m.isSameThread()){
|
||||
lines.add(text.copy().withStyle(ChatFormatting.GRAY));
|
||||
return;
|
||||
}
|
||||
int width=Math.max(100,Math.min(280,m.getWindow().getGuiScaledWidth()-32));
|
||||
for(var part:m.font.getSplitter().splitLines(text,width,net.minecraft.network.chat.Style.EMPTY))
|
||||
lines.add(Component.literal(part.getString()).withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.AbstractFurnaceBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** One animation source per facade; texture coordinates are measured on the full 48px face. */
|
||||
public final class FourneauFire {
|
||||
public static Vec3 point(BlockPos origin,Direction facing,double pixelX,double pixelY){
|
||||
double u=pixelX/16.0,v=3-pixelY/16.0,edge=0.02;
|
||||
var local=switch(facing){
|
||||
case NORTH -> new Vec3(3-u,v,-edge);
|
||||
case SOUTH -> new Vec3(u,v,3+edge);
|
||||
case EAST -> new Vec3(3+edge,v,3-u);
|
||||
case WEST -> new Vec3(-edge,v,u);
|
||||
default -> throw new IllegalArgumentException("Horizontal furnace facade required");
|
||||
};
|
||||
return local.add(origin.getX(),origin.getY(),origin.getZ());
|
||||
}
|
||||
public static boolean animate(BlockState state,Level level,BlockPos pos,RandomSource random){
|
||||
var tile=FourneauModels.fireTile(pos);
|
||||
if(tile==null)return false;
|
||||
// Cancel all 27 native sources, even when cold; only the front middle cell emits.
|
||||
if(!state.getValue(AbstractFurnaceBlock.LIT)||!tile.emitter())return true;
|
||||
var origin=tile.origin(pos);
|
||||
if(random.nextDouble()<0.1){
|
||||
var sound=point(origin,tile.facing(),24,24);
|
||||
level.playLocalSound(sound.x,sound.y,sound.z,SoundEvents.FURNACE_FIRE_CRACKLE,SoundSource.BLOCKS,1,1,false);
|
||||
}
|
||||
for(int row:new int[]{16,37}){
|
||||
var p=point(origin,tile.facing(),12+random.nextDouble()*24,row+0.25+random.nextDouble()*0.5);
|
||||
level.addParticle(ParticleTypes.SMOKE,p.x,p.y,p.z,0,0,0);
|
||||
level.addParticle(ParticleTypes.FLAME,p.x,p.y,p.z,0,0,0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private FourneauFire(){}
|
||||
}
|
||||
@@ -25,7 +25,15 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
public final class FourneauModels {
|
||||
private static volatile Map<Long,Map<BlockPos,Identifier>> chunks=Map.of();
|
||||
private static final List<Identifier> MODELS=new ArrayList<>();
|
||||
static {for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++)MODELS.add(caskId(x,y,z));for(var d:Direction.Plane.HORIZONTAL)for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++)MODELS.add(id(d,x,y,z));}
|
||||
private static final Map<Identifier,FireTile> FIRE_TILES=new HashMap<>();
|
||||
public record FireTile(Direction facing,int x,int y,int z) {
|
||||
public BlockPos origin(BlockPos pos){return pos.offset(-x,-y,-z);}
|
||||
public boolean emitter(){return y==1 && switch(facing){case NORTH -> x==1&&z==0;case SOUTH -> x==1&&z==2;case EAST -> x==2&&z==1;case WEST -> x==0&&z==1;default -> false;};}
|
||||
}
|
||||
public static FireTile fireTile(BlockPos pos){return FIRE_TILES.get(modelAt(pos));}
|
||||
private static Identifier litId(Identifier id){return id.withPath(id.getPath()+"_lit");}
|
||||
public static Identifier modelAt(BlockPos pos,BlockState state){var id=modelAt(pos);return id!=null&&state.is(Blocks.FURNACE)&&state.getValue(net.minecraft.world.level.block.AbstractFurnaceBlock.LIT)&&FIRE_TILES.containsKey(id)?litId(id):id;}
|
||||
static {for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++)MODELS.add(caskId(x,y,z));for(var d:Direction.Plane.HORIZONTAL)for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++){var id=id(d,x,y,z);MODELS.add(id);MODELS.add(litId(id));FIRE_TILES.put(id,new FireTile(d,x,y,z));}}
|
||||
private static Identifier id(Direction d,int x,int y,int z){return SanctuaryMod.id("block/fourneau/"+d.getName()+"_"+x+"_"+y+"_"+z);}
|
||||
private static Identifier caskId(int x,int y,int z){return SanctuaryMod.id("block/fut/"+x+"_"+y+"_"+z);}
|
||||
public static Identifier modelAt(BlockPos pos){var cells=chunks.get(ChunkPos.pack(pos));return cells==null?null:cells.get(pos);}
|
||||
@@ -65,15 +73,15 @@ public final class FourneauModels {
|
||||
private static final class FacadeModel extends WrapperBlockStateModel {
|
||||
private final Map<Identifier,BlockStateModel> tiled;
|
||||
FacadeModel(BlockStateModel original,Map<Identifier,BlockStateModel> tiled){super(original);this.tiled=tiled;}
|
||||
private BlockStateModel select(BlockPos pos){var id=modelAt(pos);return id==null?wrapped:tiled.getOrDefault(id,wrapped);}
|
||||
private BlockStateModel select(BlockPos pos,BlockState state){var id=modelAt(pos,state);return id==null?wrapped:tiled.getOrDefault(id,wrapped);}
|
||||
@Override public void emitQuads(QuadEmitter emitter,BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random,Predicate<Direction> cull){
|
||||
select(pos).emitQuads(emitter,view,pos,state,random,cull);
|
||||
select(pos,state).emitQuads(emitter,view,pos,state,random,cull);
|
||||
}
|
||||
@Override public Object createGeometryKey(BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random){
|
||||
var model=select(pos);return model==wrapped?wrapped.createGeometryKey(view,pos,state,random):model;
|
||||
var model=select(pos,state);return model==wrapped?wrapped.createGeometryKey(view,pos,state,random):model;
|
||||
}
|
||||
@Override public Material.Baked particleMaterial(BlockAndTintGetter view,BlockPos pos,BlockState state){return select(pos).particleMaterial(view,pos,state);}
|
||||
@Override public int materialFlags(BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random){return select(pos).materialFlags(view,pos,state,random);}
|
||||
@Override public Material.Baked particleMaterial(BlockAndTintGetter view,BlockPos pos,BlockState state){return select(pos,state).particleMaterial(view,pos,state);}
|
||||
@Override public int materialFlags(BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random){return select(pos,state).materialFlags(view,pos,state,random);}
|
||||
}
|
||||
private FourneauModels(){}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ public final class HotbarClient {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// This workshop exposes only the current hotbar; Tab remains native focus navigation.
|
||||
if (screen instanceof ClayWorkshopScreen) return;
|
||||
ScreenKeyboardEvents.afterKeyRelease(screen).register((s,event) -> {if (key.matches(event)) held=false;});
|
||||
ScreenKeyboardEvents.allowKeyPress(screen).register((s,event) -> {
|
||||
if (!ProgressionClient.active() || !key.matches(event) || event.isEscape() || editing(s.getFocused())) return true;
|
||||
|
||||
+4
-1
@@ -14,12 +14,15 @@ import net.minecraft.world.item.SpawnEggItem;
|
||||
public final class InhabitantCompanionWidget extends AbstractWidget {
|
||||
private final Mob model;
|
||||
public InhabitantCompanionWidget(int width,int height,ItemStack egg) {
|
||||
this(width,height,egg,true);
|
||||
}
|
||||
public InhabitantCompanionWidget(int width,int height,ItemStack egg,boolean baby) {
|
||||
super(0,0,width,height,egg.getHoverName());
|
||||
var client=net.minecraft.client.Minecraft.getInstance();
|
||||
var entity=client.level!=null && egg.getItem() instanceof SpawnEggItem
|
||||
? SpawnEggItem.getType(egg).create(client.level,EntitySpawnReason.MOB_SUMMONED) : null;
|
||||
model=entity instanceof Mob mob ? mob : null;
|
||||
if(model!=null){model.setId(Integer.MIN_VALUE);model.setBaby(true);model.setPos(client.player.position());model.setCustomName(egg.getCustomName());}
|
||||
if(model!=null){model.setId(Integer.MIN_VALUE);model.setBaby(baby);model.setPos(client.player.position());model.setCustomName(egg.getCustomName());}
|
||||
}
|
||||
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mouseX,int mouseY,float partial) {
|
||||
if(model==null)return;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.*;
|
||||
import fr.koka.sanctuary.mixin.client.NotificationAdvancementTabAccess;
|
||||
import net.minecraft.advancements.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.gui.screens.advancements.*;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** The actual native advancement tree, embedded below skills without a second screen. */
|
||||
final class InlineAdvancements extends AbstractWidget {
|
||||
private final List<AdvancementTab> tabs=new ArrayList<>();
|
||||
private int selected,nodeCount;
|
||||
private AdvancementHolder pressed;
|
||||
private double dragged;
|
||||
InlineAdvancements(int width){
|
||||
super(0,0,width,155,Component.translatable("gui.advancements"));
|
||||
rebuildTree();
|
||||
}
|
||||
void copyPosition(InlineAdvancements previous){if(previous==null)return;selected=previous.selected;for(var t:tabs)for(var old:previous.tabs)if(t.getRootAdvancement().equals(old.getRootAdvancement()))t.copyPosition(old);}
|
||||
private void rebuildTree(){
|
||||
var old=new ArrayList<>(tabs);tabs.clear();
|
||||
var mc=Minecraft.getInstance();var manager=mc.player.connection.getAdvancements();nodeCount=manager.tree().nodes().size();
|
||||
var owner=new AdvancementsScreen(manager);owner.width=mc.getWindow().getGuiScaledWidth();
|
||||
for(var root:manager.tree().roots()){
|
||||
var tab=AdvancementTab.create(mc,owner,0,root);if(tab==null)continue;
|
||||
for(var node:manager.tree().tasks())if(node.root().equals(root))tab.addAdvancement(node);
|
||||
for(var prior:old)if(prior.getRootAdvancement().equals(tab.getRootAdvancement()))tab.copyPosition(prior);
|
||||
tabs.add(tab);
|
||||
}
|
||||
}
|
||||
private float scale(){return Math.min(1,(width-4)/234f);}
|
||||
private int treeX(){return getX()+(width-Math.round(234*scale()))/2;}
|
||||
private int treeY(){return getY()+27;}
|
||||
private AdvancementTab tab(){return tabs.isEmpty()?null:tabs.get(Math.floorMod(selected,tabs.size()));}
|
||||
private AdvancementHolder hit(double x,double y){
|
||||
var t=tab();if(t==null)return null;int rx=(int)((x-treeX())/scale()),ry=(int)((y-treeY())/scale());
|
||||
if(rx<0||rx>=234||ry<0||ry>=113)return null;
|
||||
var a=(NotificationAdvancementTabAccess)t;
|
||||
for(var w:a.sanctuary$widgets().values())if(w.isMouseOver((int)Math.floor(a.sanctuary$scrollX()),(int)Math.floor(a.sanctuary$scrollY()),rx,ry))return w.getAdvancement();
|
||||
return null;
|
||||
}
|
||||
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float dt){
|
||||
var mc=Minecraft.getInstance();if(nodeCount!=mc.player.connection.getAdvancements().tree().nodes().size())rebuildTree();g.text(mc.font,getMessage(),getX(),getY(),0xFFA9D7A0);var t=tab();
|
||||
if(t==null){g.text(mc.font,Component.translatable("advancements.empty"),getX(),getY()+30,0xFFCCCCCC);return;}
|
||||
g.text(mc.font,"‹",getX()+2,getY()+14,0xFFFFFF55);g.text(mc.font,"›",getRight()-8,getY()+14,0xFFFFFF55);
|
||||
g.centeredText(mc.font,t.getTitle(),getX()+width/2,getY()+14,0xFFFFFFFF);
|
||||
var manager=mc.player.connection.getAdvancements();for(var entry:manager.progress().entrySet()){var w=t.getWidget(entry.getKey());if(w!=null)w.setProgress(entry.getValue());}
|
||||
g.pose().pushMatrix();g.pose().translate(treeX(),treeY());g.pose().scale(scale(),scale());t.extractContents(g,0,0);g.pose().popMatrix();
|
||||
var holder=hit(mx,my);
|
||||
if(holder!=null&&isHovered()){
|
||||
var display=holder.value().display().orElseThrow();var progress=manager.progress().get(holder);
|
||||
var lines=new ArrayList<Component>();lines.add(display.title());lines.add(display.description());
|
||||
if(progress!=null&&progress.getProgressText()!=null)lines.add(progress.getProgressText());
|
||||
if(NotificationClient.canTrack(holder))lines.add(NotificationClient.label(NotificationClient.isPinned(holder.id())?"unpin_hint":"pin_hint"));
|
||||
g.setTooltipForNextFrame(mc.font,lines,Optional.empty(),mx,my);
|
||||
}
|
||||
}
|
||||
@Override protected boolean isValidClickButton(MouseButtonInfo b){return b.button()==0||b.button()==1;}
|
||||
@Override public void onClick(MouseButtonEvent e,boolean twice){
|
||||
if(e.y()<treeY()&&e.y()>=getY()+10){selected+=e.x()<getX()+width/2?-1:1;return;}
|
||||
pressed=hit(e.x(),e.y());dragged=0;
|
||||
}
|
||||
@Override protected void onDrag(MouseButtonEvent e,double dx,double dy){dragged+=Math.abs(dx)+Math.abs(dy);if(tab()!=null)tab().scroll(dx/scale(),dy/scale());}
|
||||
@Override public void onRelease(MouseButtonEvent e){if(e.button()==1&&dragged<3&&pressed!=null&&pressed.equals(hit(e.x(),e.y())))NotificationClient.toggleTracking(pressed);pressed=null;}
|
||||
@Override public boolean mouseScrolled(double x,double y,double dx,double dy){if(!isMouseOver(x,y)||tab()==null)return false;tab().scroll(dx*12,dy*12);return true;}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput out){defaultButtonNarrationText(out);}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.multiblock.Metatable;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** A physical workshop grants one local plan selection, never permission to place world blocks. */
|
||||
public final class MetatableClient {
|
||||
private static BlockPos origin;
|
||||
private static ClientLevel level;
|
||||
private static int session;
|
||||
private static java.util.Set<String> statues=java.util.Set.of();
|
||||
public static boolean knows(net.minecraft.world.entity.EntityType<?> type){return available()&&statues.contains(net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(type).toString());}
|
||||
public static void register(){
|
||||
ClientPlayNetworking.registerGlobalReceiver(Metatable.Open.TYPE,(p,c)->c.client().execute(()->{
|
||||
leave();origin=p.origin();level=c.client().level;statues=java.util.Set.copyOf(p.statues());
|
||||
if(available())c.client().gui.setScreen(PlansScreen.workshop());else leave();
|
||||
}));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((h,c)->leave());
|
||||
ClientConfigurationConnectionEvents.INIT.register((h,c)->leave());
|
||||
}
|
||||
public static boolean available(){
|
||||
var m=Minecraft.getInstance();
|
||||
return origin!=null&&m.level==level&&m.player!=null&&m.player.isAlive()&&!m.player.isSpectator()
|
||||
&&Metatable.members(origin).stream().anyMatch(p->m.player.getEyePosition().distanceToSqr(Vec3.atCenterOf(p))<64)
|
||||
&&Metatable.state(level,origin,true)==1;
|
||||
}
|
||||
public static int session(){return session;}
|
||||
public static boolean available(int expected){return expected==session&&available();}
|
||||
public static void leave(){session++;origin=null;level=null;statues=java.util.Set.of();}
|
||||
public static void selected(){leave();Minecraft.getInstance().gui.setScreen(null);}
|
||||
public static void needWorkshop(){var m=Minecraft.getInstance();if(m.player!=null)m.gui.hud.setOverlayMessage(PlanClient.label("workshop_required"),false);}
|
||||
private MetatableClient(){}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
final class ModelImportErrors {
|
||||
static Component message(Throwable error){
|
||||
String key=error==null?"":String.valueOf(error.getMessage());
|
||||
if(key.startsWith("glb_"))return Component.translatable("sanctuary.statuary.error."+key.substring(4));
|
||||
if(key.equals("model_limit")||key.equals("block_limit")||key.equals("size_limit"))return Component.translatable("sanctuary.statuary.error.model_limit");
|
||||
return Component.translatable("sanctuary.statuary.error.file");
|
||||
}
|
||||
private ModelImportErrors(){}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import net.minecraft.world.entity.player.Inventory;
|
||||
public final class MultiblockScreen extends ContainerScreen {
|
||||
private EditBox search;
|
||||
private Button previous,next,apply;
|
||||
private double wheel;
|
||||
private boolean showHelp;
|
||||
private final java.util.List<Button> sections=new java.util.ArrayList<>();
|
||||
public MultiblockScreen(MultiblockMenu menu,Inventory inv,Component title){super(menu,inv,title);}
|
||||
private MultiblockMenu machine(){return (MultiblockMenu)menu;}
|
||||
@@ -25,21 +27,64 @@ public final class MultiblockScreen extends ContainerScreen {
|
||||
apply=addRenderableWidget(Button.builder(Multiblocks.text("search"),b->change(0,v.section())).bounds(x,topPos+20,w,20).build());
|
||||
previous=addRenderableWidget(Button.builder(Component.literal("<"),b->change(v.page()-1,v.section())).bounds(x,topPos+44,w/2-1,20).build());
|
||||
next=addRenderableWidget(Button.builder(Component.literal(">"),b->change(v.page()+1,v.section())).bounds(x+w/2+1,topPos+44,w/2-1,20).build());
|
||||
if(v.furnace())for(int n=0;n<3;n++){int section=n;var b=addRenderableWidget(Button.builder(Multiblocks.text(new String[]{"inputs","fuel","outputs"}[n]),button->change(0,section)).bounds(x,topPos+84+n*22,w,20).build());sections.add(b);}
|
||||
if(v.furnace())for(int n=0;n<3;n++){int section=n;var b=addRenderableWidget(Button.builder(Multiblocks.text(new String[]{"inputs","fuel","outputs"}[n]),button->change(0,section)).bounds(x,topPos+44+n*22,w,20).build());sections.add(b);}
|
||||
if(v.furnace())addRenderableWidget(Button.builder(Multiblocks.text("how_to_cook"),b->showHelp=!showHelp).bounds(x,topPos+142,w,20)
|
||||
.tooltip(Tooltip.create(Multiblocks.text("cooking_help"))).build());
|
||||
if(v.furnace()){previous.visible=false;next.visible=false;}
|
||||
controls();
|
||||
}
|
||||
@Override public boolean mouseScrolled(double x,double y,double horizontal,double vertical){
|
||||
int right=search==null?leftPos+imageWidth:search.getRight();
|
||||
if(vertical==0||x<leftPos||x>right||y<topPos||y>topPos+imageHeight||!menu.getCarried().isEmpty())
|
||||
return super.mouseScrolled(x,y,horizontal,vertical);
|
||||
wheel+=vertical;int steps=(int)wheel;
|
||||
if(steps!=0){wheel-=steps;var v=machine().view;int page=Math.clamp(v.page()-steps,0,v.pages()-1);if(page!=v.page())change(page,v.section());}
|
||||
return true;
|
||||
}
|
||||
private void controls(){if(apply==null)return;var v=machine().view;boolean empty=menu.getCarried().isEmpty();apply.active=empty;previous.active=empty&&v.page()>0;next.active=empty&&v.page()+1<v.pages();for(int i=0;i<sections.size();i++)sections.get(i).active=empty&&i!=v.section();}
|
||||
@Override protected void containerTick(){super.containerTick();controls();}
|
||||
@Override protected void extractLabels(net.minecraft.client.gui.GuiGraphicsExtractor g,int mouseX,int mouseY){
|
||||
super.extractLabels(g,mouseX,mouseY);var m=machine();int x=imageWidth+4;
|
||||
g.text(font,Multiblocks.text("page",m.view.page()+1,m.view.pages()),x,68,0xFFFFFFFF,false);
|
||||
if(!m.view.furnace())g.text(font,Multiblocks.text("page",m.view.page()+1,m.view.pages()),x,68,0xFFFFFFFF,false);
|
||||
if(m.view.furnace()){
|
||||
g.text(font,Multiblocks.text("heat",m.heat),x,154,0xFFFFFFFF,false);
|
||||
g.text(font,Multiblocks.text("progress",m.progress),x,166,0xFFFFFFFF,false);
|
||||
g.text(font,Multiblocks.text("heat",m.heat),x,116,0xFFFFFFFF,false);
|
||||
g.text(font,Multiblocks.text("cooking_count",m.cooking),x,128,0xFFFFFFFF,false);
|
||||
}
|
||||
for(int i=m.view.indices().length;i<m.displaySlots();i++){var s=menu.getSlot(i);g.fill(s.x,s.y,s.x+16,s.y+16,0xB0181818);}
|
||||
}
|
||||
public java.util.List<net.minecraft.client.renderer.Rect2i> extraAreas(){return search==null?java.util.List.of():java.util.List.of(new net.minecraft.client.renderer.Rect2i(search.getX()-2,search.getY()-2,search.getWidth()+4,machine().view.furnace()?182:82));}
|
||||
@Override protected void extractSlot(net.minecraft.client.gui.GuiGraphicsExtractor g,net.minecraft.world.inventory.Slot slot,int x,int y){
|
||||
super.extractSlot(g,slot,x,y);var m=machine();
|
||||
if(!m.view.furnace()||m.view.section()!=0||slot.index>=m.view.indices().length)return;
|
||||
int lane=m.view.indices()[slot.index]/3,state=m.laneState(lane);if(state==MultiblockMenu.IDLE)return;
|
||||
int color=switch(state){case MultiblockMenu.COOKING->0xFFFFB23F;case MultiblockMenu.NO_FUEL->0xFFE1CE62;default->0xFFE06B5F;};
|
||||
g.fill(slot.x,slot.y+14,slot.x+16,slot.y+16,0xFF343434);
|
||||
g.fill(slot.x,slot.y+14,slot.x+Math.max(1,16*m.laneProgress(lane)/100),slot.y+16,color);
|
||||
}
|
||||
@Override protected java.util.List<Component> getTooltipFromContainerItem(net.minecraft.world.item.ItemStack stack){
|
||||
var lines=new java.util.ArrayList<>(super.getTooltipFromContainerItem(stack));var m=machine();
|
||||
if(m.view.furnace()&&hoveredSlot!=null&&hoveredSlot.index<m.view.indices().length){
|
||||
int lane=m.view.indices()[hoveredSlot.index]/3;
|
||||
if(m.view.section()==1)lines.add(Multiblocks.text("shared_fuel").copy().withStyle(net.minecraft.ChatFormatting.GRAY));
|
||||
else {lines.add(Multiblocks.text("lane",lane+1).copy().withStyle(net.minecraft.ChatFormatting.GRAY));
|
||||
lines.add(Multiblocks.text("state."+new String[]{"idle","cooking","no_fuel","blocked","no_recipe"}[m.laneState(lane)],m.laneProgress(lane)));}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@Override public void extractRenderState(net.minecraft.client.gui.GuiGraphicsExtractor g,int x,int y,float partial){
|
||||
super.extractRenderState(g,x,y,partial);
|
||||
if(showHelp){
|
||||
g.nextStratum();
|
||||
g.tooltip(font,font.split(Multiblocks.text("cooking_help"),Math.min(260,width-32)).stream()
|
||||
.map(net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent::create).toList(),
|
||||
8,24,net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE,null,false);
|
||||
}
|
||||
}
|
||||
@Override public boolean mouseClicked(net.minecraft.client.input.MouseButtonEvent event,boolean doubleClick){
|
||||
if(showHelp){showHelp=false;return true;}
|
||||
return super.mouseClicked(event,doubleClick);
|
||||
}
|
||||
boolean helpVisible(){return showHelp;}
|
||||
public java.util.List<net.minecraft.client.renderer.Rect2i> extraAreas(){return search==null?java.util.List.of():java.util.List.of(new net.minecraft.client.renderer.Rect2i(search.getX()-2,search.getY()-2,search.getWidth()+4,machine().view.furnace()?166:82));}
|
||||
public static void register(){
|
||||
net.minecraft.client.gui.screens.MenuScreens.<net.minecraft.world.inventory.ChestMenu,MultiblockScreen>register(Multiblocks.MENU,(m,i,t)->new MultiblockScreen((MultiblockMenu)m,i,t));
|
||||
net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{if(stack.is(Multiblocks.WRENCH))lines.add(Multiblocks.text("hint").copy().withStyle(net.minecraft.ChatFormatting.GRAY));});
|
||||
|
||||
@@ -23,7 +23,10 @@ import net.minecraft.world.phys.*;
|
||||
/** Local plans and cheap visible faces; the survival overlay never sends a placement packet. */
|
||||
public final class PlanClient {
|
||||
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{Thread t=new Thread(r,"Sanctuary plans");t.setDaemon(true);return t;});
|
||||
private static int generation, ticks;
|
||||
private static int generation, ticks, keyTicks;
|
||||
private static boolean keyConsumed, keyBlocked;
|
||||
private static BlockPos lastAnchor;
|
||||
private static boolean incompleteSeen, recorded;
|
||||
private static KeyMapping key;
|
||||
public static BuildPlan plan;
|
||||
public static BlockPos origin;
|
||||
@@ -33,6 +36,8 @@ public final class PlanClient {
|
||||
private static Map<BlockState,Integer> colors=Map.of();
|
||||
private static net.minecraft.client.multiplayer.ClientLevel planLevel;
|
||||
private static List<Face> faces=List.of();
|
||||
static List<PlanPreviewRenderer.Cell> preview=List.of();
|
||||
private static Map<BlockPos,BlockState> previewMaterials=Map.of();
|
||||
private static Upload upload;
|
||||
private record Face(BlockPos pos,AABB box,Direction direction,int color,boolean conflict) { }
|
||||
private static final class Upload {
|
||||
@@ -40,8 +45,10 @@ public final class PlanClient {
|
||||
Upload(BuildPlan plan,BlockPos origin){this.plan=plan;this.origin=origin;}
|
||||
}
|
||||
private PlanClient() { }
|
||||
public static Path directory(){return FabricLoader.getInstance().getGameDir().resolve("schematics").resolve("sanctuary");}
|
||||
public static Component controlKey(){return key.getTranslatedKeyMessage();}
|
||||
public static Path directory(){return CreationLibrary.directory();}
|
||||
public static void register(KeyMapping.Category category){
|
||||
PlanPreviewRenderer.register();PlanHistory.load();
|
||||
key=KeyMappingHelper.registerKeyMapping(new KeyMapping("sanctuary.plans.key",InputConstants.Type.KEYBOARD,InputConstants.KEY_K,category));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((h,c)->clear());
|
||||
ClientConfigurationConnectionEvents.INIT.register((h,c)->clear());
|
||||
@@ -49,11 +56,21 @@ public final class PlanClient {
|
||||
HudElementRegistry.addLast(SanctuaryMod.id("plan_overlay"),(g,d)->{
|
||||
var mc=Minecraft.getInstance();if(plan==null||!visible||mc.player==null||mc.gui.screen()!=null)return;
|
||||
var text=label("hud",plan.name(),completed,plan.cells().size(),conflicts,key.getTranslatedKeyMessage());
|
||||
int x=(g.guiWidth()-mc.font.width(text))/2,y=g.guiHeight()-65;
|
||||
g.fill(x-4,y-3,x+mc.font.width(text)+4,y+mc.font.lineHeight+3,0xB0101720);g.text(mc.font,text,x,y,0xFFFFFFFF);
|
||||
var lines=mc.font.split(text,Math.max(80,g.guiWidth()-24));int count=Math.min(3,lines.size());
|
||||
int y=g.guiHeight()-65-(count-1)*mc.font.lineHeight;
|
||||
for(int i=0;i<count;i++){var line=lines.get(i);int x=(g.guiWidth()-mc.font.width(line))/2,ly=y+i*mc.font.lineHeight;
|
||||
g.fill(x-4,ly-2,x+mc.font.width(line)+4,ly+mc.font.lineHeight+2,0xB0101720);g.text(mc.font,line,x,ly,0xFFFFFFFF);}
|
||||
|
||||
var eye=mc.player.getEyePosition();var direction=mc.player.getLookAngle();BlockState looked=null;
|
||||
for(int step=0;step<=96&&looked==null;step++)looked=previewMaterials.get(BlockPos.containing(eye.add(direction.scale(step/8.0))).subtract(origin));
|
||||
if(looked!=null){var name=label("target_material",looked.getBlock().getName());int nx=(g.guiWidth()-mc.font.width(name))/2,ny=y+count*mc.font.lineHeight+7;
|
||||
g.fill(nx-4,ny-2,nx+mc.font.width(name)+4,ny+mc.font.lineHeight+2,0xB0101720);g.text(mc.font,name,nx,ny,0xFFFFFFFF);}
|
||||
|
||||
});
|
||||
}
|
||||
public static void generate(Entity entity,int height,String family){
|
||||
if(!MetatableClient.knows(entity.getType())){status=label("undiscovered_statue");return;}
|
||||
if(!MetatableClient.available()){MetatableClient.needWorkshop();return;}
|
||||
if(busy)return;var mc=Minecraft.getInstance();
|
||||
try{
|
||||
var mesh=StatueModels.capture(entity);var palette=new StatuePalette(family);
|
||||
@@ -67,29 +84,73 @@ public final class PlanClient {
|
||||
}
|
||||
@FunctionalInterface private interface Work {BuildPlan run() throws Exception;}
|
||||
private static void task(Work work,Map<BlockState,Integer> palette){
|
||||
if(busy)return;busy=true;status=label("working");int token=++generation;var mc=Minecraft.getInstance();var world=mc.level;
|
||||
if(!MetatableClient.available()){MetatableClient.needWorkshop();return;}
|
||||
if(busy)return;busy=true;status=label("working");int token=++generation;int workshop=MetatableClient.session();var mc=Minecraft.getInstance();var world=mc.level;
|
||||
CompletableFuture.supplyAsync(()->{try{return work.run();}catch(Exception e){throw new CompletionException(e);}},WORKER)
|
||||
.whenComplete((result,error)->mc.execute(()->{
|
||||
if(token!=generation)return;
|
||||
busy=false;
|
||||
if(mc.level!=world)return;
|
||||
if(mc.level!=world||!MetatableClient.available(workshop)){status=label("workshop_required");return;}
|
||||
if(error!=null){fail(error.getCause()==null?error:error.getCause());return;}
|
||||
select(result,palette);if(mc.gui.screen() instanceof PlansScreen screen)screen.showPlan();
|
||||
select(result,palette);MetatableClient.selected();
|
||||
}));
|
||||
}
|
||||
public static void load(Path file){task(()->SchematicIO.read(file),Map.of());}
|
||||
public static void importModel(Path file,int height,String family){
|
||||
if(!MetatableClient.available()||busy)return;
|
||||
try{
|
||||
var palette=new StatuePalette(family);
|
||||
task(()->{
|
||||
var pixels=StatueVoxelizer.convert(GlbModel.read(file),height);var blocks=new HashMap<BlockPos,BlockState>();
|
||||
pixels.forEach((pos,color)->blocks.put(pos,palette.nearest(color)));
|
||||
return BuildPlan.normalized(GlbModel.name(file),blocks);
|
||||
},palette.colors());
|
||||
}catch(Exception e){fail(e);}
|
||||
}
|
||||
public static void catalog(ConstructionCatalog.Entry entry){
|
||||
if(!MetatableClient.available()){MetatableClient.needWorkshop();return;}
|
||||
if(busy)return;
|
||||
var result=entry.factory().get();
|
||||
select(new BuildPlan(label("catalog."+entry.id()).getString(),result.width(),result.height(),result.depth(),result.cells()),Map.of());
|
||||
MetatableClient.selected();
|
||||
}
|
||||
public static void select(BuildPlan result,Map<BlockState,Integer> palette){
|
||||
plan=result;colors=Map.copyOf(palette);layer=-1;visible=true;upload=null;
|
||||
plan=result;origin=null;lastAnchor=null;incompleteSeen=false;recorded=false;colors=Map.copyOf(palette);layer=-1;visible=true;upload=null;
|
||||
planLevel=Minecraft.getInstance().level;
|
||||
anchor();status=label("ready",result.cells().size());
|
||||
}
|
||||
public static void anchor(){
|
||||
var mc=Minecraft.getInstance();if(mc.player==null||plan==null)return;
|
||||
BlockPos target=mc.hitResult instanceof BlockHitResult h&&h.getType()!=HitResult.Type.MISS?h.getBlockPos().relative(h.getDirection()):mc.player.blockPosition().relative(mc.player.getDirection(),5);
|
||||
origin=target.offset(-plan.width()/2,0,-plan.depth()/2);refresh();
|
||||
private static BlockPos lookedAt(){
|
||||
var mc=Minecraft.getInstance();if(mc.player==null)return null;
|
||||
var hit=mc.player.pick(96,1,false);
|
||||
return hit instanceof BlockHitResult h&&h.getType()==HitResult.Type.BLOCK?h.getBlockPos().relative(h.getDirection()):null;
|
||||
}
|
||||
public static void move(int x,int y,int z){if(origin!=null){origin=origin.offset(x,y,z);refresh();}}
|
||||
public static void rotate(){if(plan==null)return;BlockPos before=origin;select(plan.rotate(),colors);origin=before;refresh();}
|
||||
public static void anchor(){
|
||||
if(plan==null)return;
|
||||
var target=lookedAt();
|
||||
if(target==null){if(origin==null&&Minecraft.getInstance().player!=null)origin=Minecraft.getInstance().player.blockPosition();status=label("aim_block");return;}
|
||||
origin=target.offset(-plan.width()/2,0,-plan.depth()/2);lastAnchor=null;incompleteSeen=false;recorded=false;refresh();
|
||||
}
|
||||
private static void quickPlace(){
|
||||
if(plan==null){MetatableClient.needWorkshop();return;}
|
||||
var target=lookedAt();if(target==null){status=label("aim_block");Minecraft.getInstance().gui.hud.setOverlayMessage(status,false);return;}
|
||||
if(target.equals(lastAnchor))plan=plan.rotate();
|
||||
origin=target.offset(-plan.width()/2,0,-plan.depth()/2);lastAnchor=target;
|
||||
incompleteSeen=false;recorded=false;visible=true;refresh();status=label("quick_placed");
|
||||
}
|
||||
private static void controls(Minecraft mc){
|
||||
boolean clicked=false;while(key.consumeClick())clicked=true;
|
||||
boolean down=key.isDown();
|
||||
if(mc.player==null||mc.gui.screen()!=null){keyTicks=0;keyConsumed=false;keyBlocked=down;return;}
|
||||
if(keyBlocked){if(!down)keyBlocked=false;return;}
|
||||
if(down){
|
||||
if(++keyTicks==10){keyConsumed=true;if(plan==null)MetatableClient.needWorkshop();else mc.gui.setScreen(new PlansScreen(null));}
|
||||
}else{
|
||||
if(!keyConsumed&&(keyTicks>0||clicked))quickPlace();
|
||||
keyTicks=0;keyConsumed=false;
|
||||
}
|
||||
}
|
||||
public static void move(int x,int y,int z){if(origin!=null){origin=origin.offset(x,y,z);lastAnchor=null;incompleteSeen=false;recorded=false;refresh();}}
|
||||
public static void rotate(){if(plan==null)return;plan=plan.rotate();lastAnchor=null;incompleteSeen=false;recorded=false;refresh();}
|
||||
public static void export(){
|
||||
if(plan==null||busy)return;BuildPlan current=plan;busy=true;int token=++generation;status=label("working");
|
||||
CompletableFuture.supplyAsync(()->{try{return SchematicIO.write(current,directory());}catch(IOException e){throw new CompletionException(e);}},WORKER)
|
||||
@@ -98,13 +159,14 @@ public final class PlanClient {
|
||||
}));
|
||||
}
|
||||
public static void paste(){
|
||||
if(uploading()||busy)return;
|
||||
var mc=Minecraft.getInstance();
|
||||
if(plan==null||origin==null||mc.player==null||!mc.player.isCreative()||!ClientPlayNetworking.canSend(PlanPayload.TYPE)){status=label("result.creative_only");return;}
|
||||
upload=new Upload(plan,origin);status=label("uploading");
|
||||
}
|
||||
public static boolean uploading(){return upload!=null;}
|
||||
private static void tick(Minecraft mc){
|
||||
while(key.consumeClick())if(mc.player!=null&&mc.gui.screen()==null)mc.gui.setScreen(new PlansScreen(null));
|
||||
controls(mc);
|
||||
if(mc.level==null||mc.player==null)return;
|
||||
if(planLevel!=null&&planLevel!=mc.level){clear();return;}
|
||||
if(upload!=null){
|
||||
@@ -115,8 +177,9 @@ public final class PlanClient {
|
||||
u.offset=end;if(end==u.plan.cells().size()){upload=null;status=label("sent");}
|
||||
}
|
||||
}
|
||||
if(plan==null||origin==null||!visible)return;
|
||||
if(plan==null||origin==null)return;
|
||||
if(++ticks%10==0)refresh();
|
||||
if(!visible)return;
|
||||
try(var ignored=mc.collectPerTickGizmos()){
|
||||
Gizmos.cuboid(new AABB(origin.getX(),origin.getY(),origin.getZ(),origin.getX()+plan.width(),origin.getY()+plan.height(),origin.getZ()+plan.depth()),GizmoStyle.stroke(0xFF80DAC4,1));
|
||||
for(Face face:faces){
|
||||
@@ -124,7 +187,7 @@ public final class PlanClient {
|
||||
if(mc.player.position().distanceToSqr(Vec3.atCenterOf(p))>96*96)continue;
|
||||
Vec3 a=Vec3.atLowerCornerOf(p).add(face.box.minX,face.box.minY,face.box.minZ);
|
||||
Vec3 b=Vec3.atLowerCornerOf(p).add(face.box.maxX,face.box.maxY,face.box.maxZ);
|
||||
Gizmos.rect(a,b,face.direction,GizmoStyle.fill((face.conflict?0x90FF5544:0x98000000|(face.color&0xFFFFFF))));
|
||||
if(face.conflict)Gizmos.rect(a,b,face.direction,GizmoStyle.stroke(0xD0FF5544,1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,11 +196,23 @@ public final class PlanClient {
|
||||
var remaining=new HashSet<BlockPos>();var occupied=new HashSet<BlockPos>();completed=0;conflicts=0;
|
||||
for(var c:plan.cells()){
|
||||
var actual=mc.level.getBlockState(origin.offset(c.pos()));
|
||||
if(actual==c.state()){completed++;continue;}
|
||||
if(matches(actual,c.state())){completed++;continue;}
|
||||
if(!actual.isAir()){conflicts++;occupied.add(c.pos());}
|
||||
if(layer<0||c.pos().getY()==layer)remaining.add(c.pos());
|
||||
}
|
||||
if(completed<plan.cells().size())incompleteSeen=true;
|
||||
else if(incompleteSeen&&!recorded){recorded=true;PlanHistory.completed(plan);}
|
||||
var next=new ArrayList<Face>();
|
||||
var ghosts=new ArrayList<PlanPreviewRenderer.Cell>();
|
||||
var occluders=new HashSet<BlockPos>();
|
||||
for(var cell:plan.cells())if(remaining.contains(cell.pos())&&cell.state().canOcclude()&&cell.state().isCollisionShapeFullBlock(mc.level,origin.offset(cell.pos())))occluders.add(cell.pos());
|
||||
for(var cell:plan.cells())if(remaining.contains(cell.pos())) {
|
||||
int hidden=0;for(var direction:Direction.values())if(occluders.contains(cell.pos().relative(direction)))hidden|=1<<direction.ordinal();
|
||||
if(hidden==63&&cell.state().canOcclude()&&cell.state().isCollisionShapeFullBlock(mc.level,origin.offset(cell.pos())))continue;
|
||||
ghosts.add(new PlanPreviewRenderer.Cell(cell.pos(),cell.state(),hidden,occupied.contains(cell.pos())));
|
||||
}
|
||||
preview=List.copyOf(ghosts);
|
||||
var materials=new HashMap<BlockPos,BlockState>();for(var cell:ghosts)materials.put(cell.pos(),cell.state());previewMaterials=Map.copyOf(materials);
|
||||
for(var c:plan.cells())if(remaining.contains(c.pos())){
|
||||
int color=colors.getOrDefault(c.state(),0xFF000000|c.state().getMapColor(mc.level,origin.offset(c.pos())).col);
|
||||
var boxes=c.state().getShape(mc.level,origin.offset(c.pos())).toAabbs();
|
||||
@@ -150,14 +225,28 @@ public final class PlanClient {
|
||||
}
|
||||
public static Map<BlockState,Integer> materials(boolean remainingOnly){
|
||||
var result=new LinkedHashMap<BlockState,Integer>();var mc=Minecraft.getInstance();if(plan==null)return result;
|
||||
for(var cell:plan.cells())if(!remainingOnly||mc.level==null||mc.level.getBlockState(origin.offset(cell.pos()))!=cell.state())result.merge(cell.state(),1,Integer::sum);
|
||||
for(var cell:plan.cells())if(!remainingOnly||mc.level==null||!matches(mc.level.getBlockState(origin.offset(cell.pos())),cell.state()))result.merge(cell.state(),1,Integer::sum);
|
||||
return result;
|
||||
}
|
||||
public static void clear(){generation++;busy=false;plan=null;origin=null;planLevel=null;faces=List.of();colors=Map.of();upload=null;completed=0;conflicts=0;status=Component.empty();}
|
||||
private static boolean matches(BlockState actual,BlockState wanted){
|
||||
if(actual==wanted)return true;
|
||||
// Assembling a planned piston changes its technical block, not its paid component.
|
||||
if(actual.getBlock() instanceof fr.koka.sanctuary.multiblock.SuperPistonBlocks.Base base){
|
||||
var source=base.sticky?net.minecraft.world.level.block.Blocks.STICKY_PISTON:net.minecraft.world.level.block.Blocks.PISTON;
|
||||
var facing=net.minecraft.world.level.block.piston.PistonBaseBlock.FACING;
|
||||
return wanted.is(source)&&wanted.getValue(facing)==actual.getValue(facing);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static void clear(){lastAnchor=null;keyTicks=0;keyConsumed=false;incompleteSeen=false;recorded=false;MetatableClient.leave();generation++;busy=false;plan=null;origin=null;planLevel=null;faces=List.of();preview=List.of();previewMaterials=Map.of();colors=Map.of();upload=null;completed=0;conflicts=0;status=Component.empty();}
|
||||
private static void fail(Throwable e){
|
||||
if(e.getMessage()!=null&&e.getMessage().startsWith("glb_")){busy=false;status=ModelImportErrors.message(e);return;}
|
||||
busy=false;String reason=e.getMessage()==null?"unknown":e.getMessage().split(":",2)[0];
|
||||
if(!net.minecraft.locale.Language.getInstance().has("sanctuary.plans.problem."+reason))reason="unknown";
|
||||
status=label("error",label("problem."+reason));SanctuaryMod.LOGGER.warn("Plan operation failed",e);
|
||||
status=label("error",label("problem."+reason));
|
||||
if((reason.equals("unknown_block")||reason.equals("block_state"))&&e.getMessage()!=null&&e.getMessage().contains(": "))
|
||||
status=status.copy().append(" · ").append(e.getMessage().substring(e.getMessage().indexOf(": ")+2));
|
||||
SanctuaryMod.LOGGER.warn("Plan operation failed",e);
|
||||
}
|
||||
public static Component label(String key,Object...args){return Component.translatable("sanctuary.plans."+key,args);}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.nbt.*;
|
||||
|
||||
/** Personal reusable plans, not world state or placement authority. At most three completed projects. */
|
||||
final class PlanHistory {
|
||||
private static final ExecutorService IO=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary plan history");t.setDaemon(true);return t;});
|
||||
private static List<BuildPlan> recent=List.of();
|
||||
private static int revision;
|
||||
private static Path file(){return PlanClient.directory().resolve("recent-completed.nbt");}
|
||||
static List<BuildPlan> entries(){return recent;}
|
||||
static int revision(){return revision;}
|
||||
static void load(){IO.execute(()->{
|
||||
try{
|
||||
if(!Files.isRegularFile(file()))return;
|
||||
if(Files.size(file())>SchematicIO.MAX_FILE)throw new IllegalArgumentException("history file size");
|
||||
var root=NbtIo.readCompressed(file(),NbtAccounter.create(SchematicIO.MAX_NBT));
|
||||
var list=root.getList("Completed").orElseThrow();if(list.size()>3)throw new IllegalArgumentException("history size");
|
||||
var plans=new ArrayList<BuildPlan>();
|
||||
for(var value:list){var entry=(CompoundTag)value;plans.add(SchematicIO.decode(entry,entry.getStringOr("PlanName","Plan")));}
|
||||
Minecraft.getInstance().execute(()->{if(revision==0){recent=List.copyOf(plans);revision++;}});
|
||||
}catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot read personal construction history",e);}
|
||||
});}
|
||||
static void completed(BuildPlan plan){
|
||||
var plans=new ArrayList<BuildPlan>();plans.add(plan);
|
||||
for(var old:recent)if(!old.cells().equals(plan.cells())&&plans.size()<3)plans.add(old);
|
||||
recent=List.copyOf(plans);revision++;
|
||||
var snapshot=recent;IO.execute(()->{
|
||||
try{
|
||||
Files.createDirectories(file().getParent());var root=new CompoundTag();var entries=new ListTag();
|
||||
for(var p:snapshot){var entry=SchematicIO.encode(p);entry.putString("PlanName",p.name());entries.add(entry);}
|
||||
root.put("Completed",entries);var tmp=file().resolveSibling("recent-completed.tmp");NbtIo.writeCompressed(root,tmp);
|
||||
try{Files.move(tmp,file(),StandardCopyOption.ATOMIC_MOVE,StandardCopyOption.REPLACE_EXISTING);}
|
||||
catch(AtomicMoveNotSupportedException e){Files.move(tmp,file(),StandardCopyOption.REPLACE_EXISTING);}
|
||||
}catch(Exception e){SanctuaryMod.LOGGER.warn("Cannot save personal construction history",e);}
|
||||
});
|
||||
}
|
||||
static void choose(BuildPlan plan){if(!MetatableClient.available()){MetatableClient.needWorkshop();return;}PlanClient.select(plan,Map.of());MetatableClient.selected();}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.*;
|
||||
import fr.koka.sanctuary.plans.StatueVoxelizer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Resource-pack samples shared by thumbnails and the unrestricted statue palette. */
|
||||
final class PlanMaterials {
|
||||
private static Object resources;
|
||||
private static final Map<BlockState,Sample> CACHE=new HashMap<>();
|
||||
private static final Map<Identifier,StatueVoxelizer.Texture> TEXTURES=new HashMap<>();
|
||||
record Sample(StatueVoxelizer.Texture pixels,int tint,int average){}
|
||||
static StatueVoxelizer.Texture texture(net.minecraft.client.renderer.texture.TextureAtlasSprite sprite) throws java.io.IOException {
|
||||
var content=sprite.contents();var id=content.name();var saved=TEXTURES.get(id);if(saved!=null)return saved;
|
||||
var raw=StatueModels.readTexture(Identifier.fromNamespaceAndPath(id.getNamespace(),"textures/"+id.getPath()+".png"));
|
||||
int frameWidth=Math.min(raw.width(),content.width()),frameHeight=Math.min(raw.height(),content.height());
|
||||
int width=Math.min(64,frameWidth),height=Math.min(64,frameHeight);var pixels=new int[width*height];
|
||||
for(int y=0;y<height;y++)for(int x=0;x<width;x++)pixels[y*width+x]=raw.argb()[(y*frameHeight/height)*raw.width()+x*frameWidth/width];
|
||||
var result=new StatueVoxelizer.Texture(width,height,pixels);TEXTURES.put(id,result);return result;
|
||||
}
|
||||
static Sample faceSample(BlockState state,net.minecraft.client.resources.model.geometry.BakedQuad.MaterialInfo material){
|
||||
var fallback=sample(state);
|
||||
try{
|
||||
int tint=0xFFFFFFFF;var mc=Minecraft.getInstance();var sources=mc.getBlockColors().getTintSources(state);
|
||||
if(material.tintIndex()>=0&&material.tintIndex()<sources.size())tint=sources.get(material.tintIndex()).colorInWorld(state,mc.level,mc.player.blockPosition());
|
||||
return new Sample(texture(material.sprite()),tint,fallback.average());
|
||||
}catch(Exception e){return fallback;}
|
||||
}
|
||||
static Sample sample(BlockState state){
|
||||
var mc=Minecraft.getInstance();var model=mc.getModelManager().getBlockStateModelSet();
|
||||
if(resources!=model){resources=model;CACHE.clear();TEXTURES.clear();}
|
||||
return CACHE.computeIfAbsent(state,s->{
|
||||
var material=model.getParticleMaterial(s).sprite().contents();var name=material.name();
|
||||
StatueVoxelizer.Texture pixels;
|
||||
try{pixels=texture(model.getParticleMaterial(s).sprite());}
|
||||
catch(Exception e){int color=0xFF000000|s.getMapColor(mc.level,mc.player.blockPosition()).col;pixels=new StatueVoxelizer.Texture(1,1,new int[]{color});}
|
||||
int tint=0xFFFFFFFF;var sources=mc.getBlockColors().getTintSources(s);
|
||||
if(!sources.isEmpty())tint=sources.getFirst().colorInWorld(s,mc.level,mc.player.blockPosition());
|
||||
long r=0,g=0,b=0,n=0;int limit=Math.min(pixels.argb().length,material.width()*material.height());
|
||||
for(int i=0;i<limit;i++){int c=net.minecraft.util.ARGB.multiply(pixels.argb()[i],tint);if((c>>>24)<128)continue;r+=(c>>16)&255;g+=(c>>8)&255;b+=c&255;n++;}
|
||||
int average=n==0?0xFF808080:0xFF000000|(int)(r/n)<<16|(int)(g/n)<<8|(int)(b/n);
|
||||
return new Sample(pixels,tint,average);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.*;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.client.resources.model.sprite.Material;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native resource-pack models and UVs, submitted as translucent, non-interactive construction ghosts. */
|
||||
final class PlanPreviewRenderer {
|
||||
record Cell(BlockPos pos,BlockState state,int hiddenFaces,boolean conflict) {}
|
||||
private record Draw(BlockPos pos,List<BlockStateModelPart> parts,int[] tints,int color,net.minecraft.client.renderer.block.BlockModelRenderState special) {}
|
||||
private static final RenderStateDataKey<List<Draw>> FRAME=RenderStateDataKey.create(()->"sanctuary:construction_preview");
|
||||
private static final int LIMIT=4096;
|
||||
private PlanPreviewRenderer(){}
|
||||
static void register(){
|
||||
LevelRenderEvents.END_EXTRACTION.register(context->{
|
||||
var render=(FabricRenderState)context.levelState();render.setData(FRAME,List.of());
|
||||
var mc=Minecraft.getInstance();
|
||||
if(!PlanClient.visible||PlanClient.plan==null||PlanClient.origin==null||mc.player==null)return;
|
||||
var camera=context.levelState().cameraRenderState;var origin=PlanClient.origin;
|
||||
var cells=PlanClient.preview.stream().filter(c->{var p=origin.offset(c.pos());return camera.pos.distanceToSqr(Vec3.atCenterOf(p))<=96*96&&camera.cullFrustum.isVisible(new AABB(p));})
|
||||
.sorted(Comparator.comparingDouble(c->camera.pos.distanceToSqr(Vec3.atCenterOf(origin.offset(c.pos()))))).limit(LIMIT).toList();
|
||||
var draws=new ArrayList<Draw>(cells.size());var models=mc.getModelManager().getBlockStateModelSet();
|
||||
var random=RandomSource.create();
|
||||
for(var cell:cells){
|
||||
var pos=origin.offset(cell.pos());var parts=new ArrayList<BlockStateModelPart>();
|
||||
random.setSeed(cell.state().getSeed(pos));models.get(cell.state()).collectParts(random,parts);
|
||||
var filtered=parts.stream().map(p->(BlockStateModelPart)new CulledPart(p,cell.hiddenFaces())).toList();
|
||||
var colors=mc.getBlockColors().getTintSources(cell.state());int[] tints=new int[colors.size()];
|
||||
for(int i=0;i<tints.length;i++)tints[i]=colors.get(i).colorInWorld(cell.state(),context.level(),pos);
|
||||
net.minecraft.client.renderer.block.BlockModelRenderState special=null;
|
||||
if(cell.state().hasBlockEntity()) {
|
||||
special=new net.minecraft.client.renderer.block.BlockModelRenderState();
|
||||
mc.getModelManager().getBlockModelSet().get(cell.state()).update(special,cell.state(),net.minecraft.client.renderer.block.model.BlockDisplayContext.create(),cell.state().getSeed(pos));
|
||||
}
|
||||
draws.add(new Draw(pos,filtered,tints,cell.conflict()?0xC8FF9090:0xC8FFFFFF,special));
|
||||
}
|
||||
render.setData(FRAME,List.copyOf(draws));
|
||||
});
|
||||
LevelRenderEvents.COLLECT_SUBMITS.register(context->{
|
||||
var draws=((FabricRenderState)context.levelState()).getDataOrDefault(FRAME,List.of());
|
||||
var camera=context.levelState().cameraRenderState.pos;var pose=context.poseStack();
|
||||
for(var draw:draws){
|
||||
pose.pushPose();pose.translate(draw.pos().getX()-camera.x,draw.pos().getY()-camera.y,draw.pos().getZ()-camera.z);
|
||||
if(draw.special()!=null&&!draw.special().isEmpty())draw.special().submit(pose,context.submitNodeCollector(),0xF000F0,OverlayTexture.NO_OVERLAY,0);
|
||||
else context.submitNodeCollector().submitCustomGeometry(pose,RenderTypes.translucentMovingBlock(),(matrix,vertices)->{
|
||||
var instance=new com.mojang.blaze3d.vertex.QuadInstance();instance.setLightCoords(0xF000F0);instance.setOverlayCoords(OverlayTexture.NO_OVERLAY);
|
||||
for(var part:draw.parts()){
|
||||
for(var face:Direction.values())for(var quad:part.getQuads(face))quad(matrix,vertices,instance,quad,draw);
|
||||
for(var quad:part.getQuads(null))quad(matrix,vertices,instance,quad,draw);
|
||||
}
|
||||
});
|
||||
pose.popPose();
|
||||
}
|
||||
});
|
||||
}
|
||||
private static void quad(com.mojang.blaze3d.vertex.PoseStack.Pose pose,com.mojang.blaze3d.vertex.VertexConsumer vertices,
|
||||
com.mojang.blaze3d.vertex.QuadInstance instance,BakedQuad quad,Draw draw){
|
||||
int index=quad.materialInfo().tintIndex();int color=index>=0&&index<draw.tints().length?net.minecraft.util.ARGB.multiply(draw.color(),draw.tints()[index]):draw.color();
|
||||
instance.setColor(color);vertices.putBakedQuad(pose,quad,instance);
|
||||
}
|
||||
private record CulledPart(BlockStateModelPart source,int hidden) implements BlockStateModelPart {
|
||||
public List<BakedQuad> getQuads(Direction face){return face!=null&&(hidden&(1<<face.ordinal()))!=0?List.of():source.getQuads(face);}
|
||||
public boolean useAmbientOcclusion(){return source.useAmbientOcclusion();}
|
||||
public Material.Baked particleMaterial(){return source.particleMaterial();}
|
||||
public int materialFlags(){return source.materialFlags();}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.*;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.plans.BuildPlan;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Small cached isometric silhouettes, using actual voxel shapes and active-pack material samples. */
|
||||
final class PlanThumbnail extends AbstractWidget {
|
||||
private static final int W=160,H=100;
|
||||
private static final Map<BuildPlan,Identifier> CACHE=new LinkedHashMap<>(32,.75f,true);
|
||||
private static Object resources;
|
||||
private static int serial;
|
||||
private final BuildPlan plan;
|
||||
PlanThumbnail(int width,int height,BuildPlan plan){super(0,0,width,height,Component.literal(plan.name()));this.plan=plan;}
|
||||
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float dt){
|
||||
var mc=Minecraft.getInstance();var models=mc.getModelManager().getBlockStateModelSet();
|
||||
if(resources!=models){for(var id:CACHE.values())mc.getTextureManager().release(id);CACHE.clear();resources=models;}
|
||||
var id=CACHE.get(plan);
|
||||
if(id==null){
|
||||
id=SanctuaryMod.id("plan_thumbnail/"+(serial++));var texture=new DynamicTexture("Sanctuary plan preview",W,H,false);
|
||||
raster(plan,texture.getPixels());texture.upload();mc.getTextureManager().register(id,texture);CACHE.put(plan,id);
|
||||
if(CACHE.size()>48){var first=CACHE.keySet().iterator().next();mc.getTextureManager().release(CACHE.remove(first));}
|
||||
}
|
||||
g.fill(getX(),getY(),getRight(),getBottom(),0x55202020);
|
||||
int w=Math.min(width,Math.min(128,height*W/H)),h=w*H/W;
|
||||
g.blit(RenderPipelines.GUI_TEXTURED,id,getX()+(width-w)/2,getY()+(height-h)/2,0,0,w,h,W,H,W,H);
|
||||
}
|
||||
private static void raster(BuildPlan plan,NativeImage out){
|
||||
out.fillRect(0,0,W,H,0);
|
||||
var mc=Minecraft.getInstance();double[] depth=new double[W*H];Arrays.fill(depth,Double.NEGATIVE_INFINITY);
|
||||
double scale=Math.min((W-12.)/(plan.width()+plan.depth()),(H-12.)/(plan.height()+.5*(plan.width()+plan.depth())));
|
||||
double ox=W/2.-(plan.width()+plan.depth())*.5*scale,oy=6+(plan.height()+.5*plan.depth())*scale;
|
||||
var occupied=new HashSet<net.minecraft.core.BlockPos>();for(var c:plan.cells())occupied.add(c.pos());
|
||||
for(var cell:plan.cells()){
|
||||
var p=cell.pos();if(occupied.contains(p.above())&&occupied.contains(p.east())&&occupied.contains(p.north())&&cell.state().canOcclude())continue;
|
||||
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();
|
||||
mc.getModelManager().getBlockStateModelSet().get(cell.state()).collectParts(net.minecraft.util.RandomSource.create(cell.state().getSeed(p)),parts);
|
||||
boolean drew=false;
|
||||
for(var part:parts)for(var direction:new net.minecraft.core.Direction[]{net.minecraft.core.Direction.UP,net.minecraft.core.Direction.EAST,net.minecraft.core.Direction.NORTH,null})for(var quad:part.getQuads(direction)){
|
||||
var face=quad.direction();if(face==net.minecraft.core.Direction.DOWN||face==net.minecraft.core.Direction.WEST||face==net.minecraft.core.Direction.SOUTH)continue;
|
||||
var sample=PlanMaterials.faceSample(cell.state(),quad.materialInfo());var sprite=quad.materialInfo().sprite();double[][] vertices=new double[4][3],uv=new double[4][2];
|
||||
for(int i=0;i<4;i++){var v=quad.position(i);vertices[i]=new double[]{p.getX()+v.x(),p.getY()+v.y(),p.getZ()+v.z()};long packed=quad.packedUV(i);
|
||||
uv[i][0]=(net.minecraft.client.model.geom.builders.UVPair.unpackU(packed)-sprite.getU0())/(sprite.getU1()-sprite.getU0());
|
||||
uv[i][1]=(net.minecraft.client.model.geom.builders.UVPair.unpackV(packed)-sprite.getV0())/(sprite.getV1()-sprite.getV0());}
|
||||
face(out,depth,sample,vertices,uv,face==net.minecraft.core.Direction.UP?1:face==net.minecraft.core.Direction.EAST?.78:.58,scale,ox,oy);drew=true;
|
||||
}
|
||||
if(drew)continue;
|
||||
var sample=PlanMaterials.sample(cell.state());var boxes=cell.state().getShape(mc.level,mc.player.blockPosition()).toAabbs();
|
||||
if(boxes.isEmpty())boxes=List.of(new AABB(0,0,0,1,1,1));
|
||||
for(var box:boxes){double x=p.getX()+box.minX,X=p.getX()+box.maxX,y=p.getY()+box.minY,Y=p.getY()+box.maxY,z=p.getZ()+box.minZ,Z=p.getZ()+box.maxZ;
|
||||
face(out,depth,sample,new double[][]{{x,Y,z},{X,Y,z},{X,Y,Z},{x,Y,Z}},null,1,scale,ox,oy);
|
||||
face(out,depth,sample,new double[][]{{X,Y,Z},{X,Y,z},{X,y,z},{X,y,Z}},null,.78,scale,ox,oy);
|
||||
face(out,depth,sample,new double[][]{{X,Y,z},{x,Y,z},{x,y,z},{X,y,z}},null,.58,scale,ox,oy);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void face(NativeImage out,double[] depths,PlanMaterials.Sample sample,double[][] v,double[][] uv,double shade,double scale,double ox,double oy){
|
||||
double[][] q=new double[4][3];for(int i=0;i<4;i++){q[i][0]=ox+(v[i][0]+v[i][2])*scale;q[i][1]=oy+(.5*(v[i][0]-v[i][2])-v[i][1])*scale;q[i][2]=v[i][0]-v[i][2]+2*v[i][1];}
|
||||
double ax=q[1][0]-q[0][0],ay=q[1][1]-q[0][1],bx=q[3][0]-q[0][0],by=q[3][1]-q[0][1],det=ax*by-ay*bx;if(Math.abs(det)<.0001)return;
|
||||
int minX=Math.max(0,(int)Math.floor(Arrays.stream(q).mapToDouble(a->a[0]).min().orElse(0))),maxX=Math.min(W-1,(int)Math.ceil(Arrays.stream(q).mapToDouble(a->a[0]).max().orElse(0)));
|
||||
int minY=Math.max(0,(int)Math.floor(Arrays.stream(q).mapToDouble(a->a[1]).min().orElse(0))),maxY=Math.min(H-1,(int)Math.ceil(Arrays.stream(q).mapToDouble(a->a[1]).max().orElse(0)));
|
||||
var tex=sample.pixels();
|
||||
for(int y=minY;y<=maxY;y++)for(int x=minX;x<=maxX;x++){
|
||||
double dx=x+.5-q[0][0],dy=y+.5-q[0][1],u=(dx*by-dy*bx)/det,t=(ax*dy-ay*dx)/det;
|
||||
if(u<0||u>1||t<0||t>1)continue;double d=q[0][2]+u*(q[1][2]-q[0][2])+t*(q[3][2]-q[0][2]);int at=y*W+x;if(d<depths[at])continue;
|
||||
double tu=uv==null?u:uv[0][0]+u*(uv[1][0]-uv[0][0])+t*(uv[3][0]-uv[0][0]);
|
||||
double tv=uv==null?t:uv[0][1]+u*(uv[1][1]-uv[0][1])+t*(uv[3][1]-uv[0][1]);
|
||||
int c=net.minecraft.util.ARGB.multiply(tex.argb()[Math.clamp((int)(tv*tex.height()),0,tex.height()-1)*tex.width()+Math.clamp((int)(tu*tex.width()),0,tex.width()-1)],sample.tint());if((c>>>24)<128)continue;
|
||||
c=0xFF000000|(int)(((c>>16)&255)*shade)<<16|(int)(((c>>8)&255)*shade)<<8|(int)((c&255)*shade);out.setPixel(x,y,c);depths[at]=d;
|
||||
}
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput out){defaultButtonNarrationText(out);}
|
||||
}
|
||||
@@ -15,51 +15,160 @@ import net.minecraft.world.item.SpawnEggItem;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import static fr.koka.sanctuary.client.PlanClient.label;
|
||||
|
||||
/** Three native panels: create, local library, and construction. */
|
||||
/** Physical workshop catalogue and separate in-field construction controls. */
|
||||
public final class PlansScreen extends Screen {
|
||||
private final Screen parent;
|
||||
private final boolean workshop;
|
||||
private final Entity target;
|
||||
private String tab,model="minecraft:creeper",heightText="32",palette="mixed";
|
||||
private int page;
|
||||
private String tab,model="minecraft:creeper",heightText="32",palette="all";
|
||||
private boolean materialList;
|
||||
public PlansScreen(Screen parent){
|
||||
super(label("title"));this.parent=parent;tab=PlanClient.plan==null?"statue":"plan";
|
||||
private int cataloguePage,historyRevision;
|
||||
private boolean statueSettings;
|
||||
private boolean externalModels;
|
||||
private Path importedModel;
|
||||
private boolean creativeControls;
|
||||
private Button buildButton;
|
||||
public PlansScreen(Screen parent){this(parent,false);}
|
||||
public static PlansScreen workshop(){return new PlansScreen(null,true);}
|
||||
public boolean isWorkshop(){return workshop;}
|
||||
private PlansScreen(Screen parent,boolean workshop){
|
||||
super(label(workshop?"workshop_title":"controls_title"));this.parent=parent;this.workshop=workshop;tab=workshop?"recent":"plan";
|
||||
var mc=net.minecraft.client.Minecraft.getInstance();target=mc.hitResult instanceof EntityHitResult hit?hit.getEntity():null;
|
||||
if(mc.player!=null&&mc.player.getMainHandItem().getItem() instanceof SpawnEggItem)
|
||||
model=BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(mc.player.getMainHandItem())).toString();
|
||||
}
|
||||
public void showPlan(){tab="plan";materialList=false;rebuildWidgets();}
|
||||
@Override protected void init(){
|
||||
creativeControls=minecraft.player!=null&&minecraft.player.isCreative();buildButton=null;
|
||||
if(PlanClient.plan!=null)PlanClient.refresh();
|
||||
int w=Math.min(450,width-24),x=(width-w)/2;
|
||||
var tabs=LinearLayout.horizontal().spacing(4);
|
||||
for(String name:List.of("statue","library","plan")){
|
||||
var b=tabs.addChild(Button.builder(label("tab."+name),button->{tab=name;page=0;materialList=false;rebuildWidgets();}).width((w-8)/3).build());
|
||||
b.active=!name.equals(tab)&&(!name.equals("plan")||PlanClient.plan!=null);
|
||||
int w=Math.min(workshop?650:450,width-24),x=(width-w)/2;
|
||||
if(workshop){
|
||||
var tabs=LinearLayout.vertical().spacing(3);
|
||||
var first=tabs.addChild(LinearLayout.horizontal().spacing(3));var second=tabs.addChild(LinearLayout.horizontal().spacing(3));
|
||||
int index=0;
|
||||
for(String name:List.of("recent","statue","machines","buildings","infrastructure","decoration","library")){
|
||||
var row=index++<4?first:second;
|
||||
var b=row.addChild(Button.builder(label("tab."+name),button->{tab=name;cataloguePage=0;statueSettings=false;materialList=false;rebuildWidgets();}).width((w-9)/4).build());b.active=!name.equals(tab);
|
||||
}
|
||||
tabs.setPosition(x,29);tabs.arrangeElements();tabs.visitWidgets(this::addRenderableWidget);
|
||||
}
|
||||
var layout=LinearLayout.vertical().spacing(6);
|
||||
if(tab.equals("statue"))statue(layout,w);else if(tab.equals("library"))library(layout,w);else plan(layout,w);
|
||||
var scroll=new ScrollableLayout(minecraft,layout,Math.max(32,height-110),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setPosition(x,57);scroll.arrangeElements();scroll.visitWidgets(this::addRenderableWidget);
|
||||
addRenderableWidget(Button.builder(label("return"),b->onClose()).bounds(x,height-27,w,20).build());
|
||||
if(!workshop)plan(layout,w);
|
||||
else if(tab.equals("statue")&&statueSettings)statue(layout,w);else catalogue(layout,w);
|
||||
if(workshop&&PlanClient.plan!=null){
|
||||
button(layout,"keep_project",w,this::onClose);
|
||||
button(layout,"clear",w,()->{PlanClient.clear();onClose();}).active=!PlanClient.busy;
|
||||
}
|
||||
var scroll=new ScrollableLayout(minecraft,layout,Math.max(32,height-(workshop?130:110)),ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setPosition(x,workshop?80:32);scroll.arrangeElements();scroll.visitWidgets(this::addRenderableWidget);
|
||||
addRenderableWidget(Button.builder(label(workshop?"close_workshop":"continue_building"),b->onClose()).bounds(x,height-27,w,20).build());
|
||||
}
|
||||
@Override public void tick(){
|
||||
if(workshop&&historyRevision!=PlanHistory.revision()){historyRevision=PlanHistory.revision();rebuildWidgets();}
|
||||
if(workshop&&!MetatableClient.available()){onClose();return;}
|
||||
if(!workshop&&PlanClient.plan==null){onClose();return;}
|
||||
if(!workshop&&creativeControls!=(minecraft.player!=null&&minecraft.player.isCreative()))rebuildWidgets();
|
||||
if(buildButton!=null)buildButton.active=canBuild();
|
||||
}
|
||||
private boolean canBuild(){
|
||||
return minecraft.player!=null&&minecraft.player.isCreative()&&PlanClient.plan!=null&&PlanClient.origin!=null
|
||||
&&!PlanClient.busy&&!PlanClient.uploading()
|
||||
&&net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.canSend(fr.koka.sanctuary.plans.PlanPayload.TYPE);
|
||||
}
|
||||
private void confirmBuild(){
|
||||
if(!canBuild())return;
|
||||
var selected=PlanClient.plan;var origin=PlanClient.origin;var level=minecraft.level;
|
||||
minecraft.gui.setScreen(new ConfirmScreen(confirmed->{
|
||||
if(confirmed&&canBuild()&&PlanClient.plan==selected&&origin.equals(PlanClient.origin)&&minecraft.level==level){
|
||||
PlanClient.paste();onClose();
|
||||
}else minecraft.gui.setScreen(this);
|
||||
},label("paste_confirm"),label("paste_detail",selected.cells().size(),origin.getX(),origin.getY(),origin.getZ())));
|
||||
}
|
||||
private record Card(Component name,Component detail,fr.koka.sanctuary.plans.BuildPlan plan,net.minecraft.world.item.ItemStack egg,String action,Runnable choose){}
|
||||
private void catalogue(LinearLayout l,int w){
|
||||
var libraryPaths=new HashMap<Card,Path>();
|
||||
var cards=new ArrayList<Card>();
|
||||
if(tab.equals("recent")){
|
||||
text(l,"recent_help",w);
|
||||
for(var p:PlanHistory.entries())cards.add(new Card(Component.literal(p.name()),label("completed_plan"),p,null,"choose_plan",()->PlanHistory.choose(p)));
|
||||
if(cards.isEmpty())text(l,"recent_empty",w);
|
||||
}else if(tab.equals("statue")){
|
||||
button(l,externalModels?"native_models":"imported_models",w,()->{externalModels=!externalModels;cataloguePage=0;rebuildWidgets();});
|
||||
if(externalModels){
|
||||
text(l,"model_files_help",w);
|
||||
button(l,"folder",w,()->{try{Files.createDirectories(PlanClient.directory());Blaze3D.openPath(PlanClient.directory());}catch(Exception e){PlanClient.status=label("error",e.getMessage());}});
|
||||
button(l,"refresh",w,this::rebuildWidgets);
|
||||
try{for(var file:fr.koka.sanctuary.plans.CreationLibrary.files(fr.koka.sanctuary.plans.CreationLibrary.Kind.MODEL))
|
||||
cards.add(new Card(Component.literal(file.getFileName().toString()),label("imported_model"),null,null,"configure",()->chooseModel(file)));
|
||||
}catch(Exception e){text(l,"error",w,e.getMessage());}
|
||||
if(cards.isEmpty())text(l,"empty_models",w);
|
||||
}else{
|
||||
text(l,"statue_catalogue_help",w);
|
||||
for(var item:BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem){var egg=item.getDefaultInstance();var type=SpawnEggItem.getType(egg);String id=BuiltInRegistries.ENTITY_TYPE.getKey(type).toString();if(!MetatableClient.knows(type))continue;
|
||||
cards.add(new Card(type.getDescription(),label("statue_card"),null,egg,"configure",()->{importedModel=null;model=id;statueSettings=true;rebuildWidgets();}));}
|
||||
cards.sort(Comparator.comparing(c->c.name().getString()));
|
||||
if(cards.isEmpty())text(l,"no_discovered_statues",w);
|
||||
}
|
||||
}else if(tab.equals("library")){
|
||||
text(l,"library_help",w);button(l,"folder",w,()->{try{Files.createDirectories(PlanClient.directory());Blaze3D.openPath(PlanClient.directory());}catch(Exception e){PlanClient.status=label("error",e.getMessage());}});
|
||||
button(l,"refresh",w,this::rebuildWidgets);
|
||||
try{
|
||||
var files=fr.koka.sanctuary.plans.CreationLibrary.files(fr.koka.sanctuary.plans.CreationLibrary.Kind.SCHEMATIC);
|
||||
for(var file:files){var card=new Card(Component.literal(file.getFileName().toString()),label("imported_plan"),null,null,"choose_plan",()->PlanClient.load(file));cards.add(card);libraryPaths.put(card,file);}
|
||||
if(cards.isEmpty())text(l,"empty_library",w);
|
||||
}catch(Exception e){text(l,"error",w,e.getMessage());}
|
||||
}else for(var entry:fr.koka.sanctuary.plans.ConstructionCatalog.ENTRIES)if(entry.category().equals(tab))
|
||||
cards.add(new Card(label("catalog."+entry.id()),label("catalog."+entry.id()+".detail"),entry.factory().get(),null,"catalog."+entry.id(),()->PlanClient.catalog(entry)));
|
||||
boolean compact=height<320;int thumbnailHeight=compact?48:80;
|
||||
int columns=w>=360?3:2,cardWidth=(w-14-(columns-1)*8)/columns,pageSize=columns*2;
|
||||
cataloguePage=Math.min(cataloguePage,Math.max(0,(cards.size()-1)/pageSize));
|
||||
for(int start=cataloguePage*pageSize;start<Math.min(cards.size(),(cataloguePage+1)*pageSize);start+=columns){
|
||||
var row=l.addChild(LinearLayout.horizontal().spacing(8));
|
||||
for(int i=start;i<Math.min(cards.size(),Math.min(start+columns,(cataloguePage+1)*pageSize));i++){
|
||||
var card=cards.get(i);var col=row.addChild(LinearLayout.vertical().spacing(4));
|
||||
var plan=card.plan();
|
||||
if(tab.equals("library"))try{plan=fr.koka.sanctuary.plans.SchematicIO.read(libraryPaths.get(card));}catch(Exception ignored){}
|
||||
if(plan!=null)col.addChild(new PlanThumbnail(cardWidth,thumbnailHeight,plan));
|
||||
else if(card.egg()!=null)col.addChild(new InhabitantCompanionWidget(cardWidth,thumbnailHeight,card.egg(),false));
|
||||
else col.addChild(new net.minecraft.client.gui.layouts.FrameLayout(cardWidth,thumbnailHeight)).addChild(new StringWidget(cardWidth,thumbnailHeight,label("preview_unavailable"),font));
|
||||
var title=col.addChild(new StringWidget(cardWidth,12,card.name(),font).setMaxWidth(cardWidth,StringWidget.TextOverflow.SCROLLING));
|
||||
var tip=card.detail().copy();if(plan!=null)tip.append("\n").append(label("dimensions_short",plan.width(),plan.height(),plan.depth()));
|
||||
title.setTooltip(Tooltip.create(tip));
|
||||
if(!compact){
|
||||
var detail=col.addChild(new net.minecraft.client.gui.layouts.FrameLayout(cardWidth,32));
|
||||
detail.addChild(new MultiLineTextWidget(card.detail(),font).setMaxWidth(cardWidth).setMaxRows(3));
|
||||
if(plan!=null)col.addChild(new StringWidget(cardWidth,10,label("dimensions_short",plan.width(),plan.height(),plan.depth()),font));
|
||||
else col.addChild(new net.minecraft.client.gui.layouts.FrameLayout(cardWidth,10));
|
||||
}
|
||||
var choice=col.addChild(Button.builder(label(card.action()),b->card.choose().run()).width(cardWidth).build());choice.active=!PlanClient.busy;choice.setTooltip(Tooltip.create(tip));
|
||||
}
|
||||
}
|
||||
if(cards.size()>pageSize){var pages=l.addChild(LinearLayout.horizontal().spacing(4));
|
||||
pages.addChild(Button.builder(Component.literal("←"),b->{cataloguePage--;rebuildWidgets();}).width(40).build()).active=cataloguePage>0;
|
||||
pages.addChild(new StringWidget(label("page",cataloguePage+1,(cards.size()+pageSize-1)/pageSize),font));
|
||||
pages.addChild(Button.builder(Component.literal("→"),b->{cataloguePage++;rebuildWidgets();}).width(40).build()).active=(cataloguePage+1)*pageSize<cards.size();
|
||||
}
|
||||
}
|
||||
private void text(LinearLayout l,String key,int width,Object...args){l.addChild(new MultiLineTextWidget(label(key,args),font).setMaxWidth(width-14));}
|
||||
private Button button(LinearLayout l,String key,int w,Runnable action){return l.addChild(Button.builder(label(key),b->action.run()).width(w-14).build());}
|
||||
private void statue(LinearLayout l,int w){
|
||||
button(l,"back_catalogue",w,()->{statueSettings=false;rebuildWidgets();});
|
||||
text(l,"statue_help",w);
|
||||
text(l,"model",w);
|
||||
if(importedModel!=null)l.addChild(new StringWidget(Component.literal(importedModel.getFileName().toString()),font));
|
||||
else{
|
||||
var input=l.addChild(new EditBox(font,w-14,20,label("model")));input.setMaxLength(100);input.setValue(model);input.setResponder(v->model=v);
|
||||
button(l,"models",w,()->{tab="models";modelPicker();});
|
||||
if(target!=null)button(l,"target",w,()->generate(target));
|
||||
if(target!=null&&MetatableClient.knows(target.getType()))button(l,"target",w,()->generate(target));
|
||||
}
|
||||
text(l,"height",w);
|
||||
var height=l.addChild(new EditBox(font,w-14,20,label("height")));height.setMaxLength(2);height.setValue(heightText);height.setResponder(v->heightText=v);
|
||||
var choices=l.addChild(LinearLayout.horizontal().spacing(4));
|
||||
for(String family:List.of("mixed","concrete","wool")){
|
||||
var b=choices.addChild(Button.builder(label("palette."+family),p->{palette=family;rebuildWidgets();}).width((w-22)/3).build());b.active=!palette.equals(family);
|
||||
for(String family:List.of("all","mixed","concrete","wool")){
|
||||
var b=choices.addChild(Button.builder(label("palette."+family),p->{palette=family;rebuildWidgets();}).width((w-26)/4).build());b.active=!palette.equals(family);
|
||||
}
|
||||
var generate=button(l,"generate",w,()->{
|
||||
try{
|
||||
if(importedModel!=null){int size=Integer.parseInt(heightText);if(size<4||size>96){PlanClient.status=label("height_error");return;}PlanClient.importModel(importedModel,size,palette);return;}
|
||||
var id=Identifier.tryParse(model.trim());var type=id==null?null:BuiltInRegistries.ENTITY_TYPE.getOptional(id).orElse(null);
|
||||
Entity e=type==null?null:type.create(minecraft.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
if(e==null){PlanClient.status=label("unknown_model");return;}
|
||||
@@ -69,8 +178,9 @@ public final class PlansScreen extends Screen {
|
||||
e.setId(Integer.MIN_VALUE+72);generate(e);
|
||||
}catch(Exception e){PlanClient.status=label("error",e.getMessage());}
|
||||
});generate.active=!PlanClient.busy;
|
||||
text(l,"model_note",w);
|
||||
text(l,importedModel==null?"model_note":"model_files_help",w);
|
||||
}
|
||||
private void chooseModel(Path file){importedModel=file;tab="statue";externalModels=true;statueSettings=true;rebuildWidgets();}
|
||||
private void generate(Entity e){
|
||||
try{int height=Integer.parseInt(heightText);if(height<4||height>96)throw new NumberFormatException();PlanClient.generate(e,height,palette);}
|
||||
catch(NumberFormatException ex){PlanClient.status=label("height_error");}
|
||||
@@ -79,25 +189,6 @@ public final class PlansScreen extends Screen {
|
||||
// A separate native list avoids reinitializing the text field while the player types.
|
||||
minecraft.gui.setScreen(new ModelPicker(this));
|
||||
}
|
||||
private void library(LinearLayout l,int w){
|
||||
text(l,"library_help",w);
|
||||
button(l,"folder",w,()->{
|
||||
try{Files.createDirectories(PlanClient.directory());Blaze3D.openPath(PlanClient.directory());}
|
||||
catch(Exception e){PlanClient.status=label("error",e.getMessage());}
|
||||
});
|
||||
button(l,"refresh",w,this::rebuildWidgets);
|
||||
text(l,"formats",w);
|
||||
try{
|
||||
var files=new ArrayList<Path>();
|
||||
// Include existing community downloads in the conventional schematics folder.
|
||||
for(Path dir:List.of(PlanClient.directory(),PlanClient.directory().getParent()))if(Files.isDirectory(dir)){
|
||||
try(var stream=Files.list(dir)){stream.filter(Files::isRegularFile).filter(p->p.getFileName().toString().matches("(?i).+\\.(litematic|schem|nbt|schematic)")).limit(1000).forEach(files::add);}
|
||||
}
|
||||
files.sort(Comparator.comparing(p->p.getFileName().toString().toLowerCase(Locale.ROOT)));
|
||||
if(files.isEmpty())text(l,"empty_library",w);
|
||||
for(Path file:files)l.addChild(Button.builder(Component.literal(file.getFileName().toString()),b->PlanClient.load(file)).width(w-14).build());
|
||||
}catch(Exception e){text(l,"error",w,e.getMessage());}
|
||||
}
|
||||
private void plan(LinearLayout l,int w){
|
||||
var plan=PlanClient.plan;if(plan==null){text(l,"empty_library",w);return;}
|
||||
l.addChild(new StringWidget(Component.literal(plan.name()),font));
|
||||
@@ -115,6 +206,7 @@ public final class PlansScreen extends Screen {
|
||||
if(materials.isEmpty())text(l,"complete",w);return;
|
||||
}
|
||||
text(l,"position",w,PlanClient.origin.getX(),PlanClient.origin.getY(),PlanClient.origin.getZ());
|
||||
if(creativeControls){buildButton=button(l,"paste",w,this::confirmBuild);buildButton.active=canBuild();}
|
||||
var moves=l.addChild(LinearLayout.horizontal().spacing(3));
|
||||
for(var d:net.minecraft.core.Direction.values())moves.addChild(Button.builder(label("move."+d.getName()),b->{PlanClient.move(d.getStepX(),d.getStepY(),d.getStepZ());rebuildWidgets();}).width((w-29)/6).build());
|
||||
button(l,"anchor",w,()->{PlanClient.anchor();rebuildWidgets();});
|
||||
@@ -126,22 +218,22 @@ public final class PlansScreen extends Screen {
|
||||
button(l,PlanClient.visible?"hide":"show",w,()->{PlanClient.visible=!PlanClient.visible;rebuildWidgets();});
|
||||
button(l,"materials",w,()->{materialList=true;rebuildWidgets();});
|
||||
button(l,"export",w,PlanClient::export).active=!PlanClient.busy;
|
||||
if(minecraft.player.isCreative()){
|
||||
button(l,"paste",w,()->minecraft.gui.setScreen(new ConfirmScreen(confirmed->{
|
||||
minecraft.gui.setScreen(this);if(confirmed)PlanClient.paste();
|
||||
},label("paste_confirm"),label("paste_detail",plan.cells().size(),PlanClient.origin.getX(),PlanClient.origin.getY(),PlanClient.origin.getZ())))).active=!PlanClient.uploading();
|
||||
}else text(l,"survival_help",w);
|
||||
text(l,"survival_help",w);
|
||||
text(l,"workshop_required",w);
|
||||
text(l,"blocks_only",w);
|
||||
button(l,"clear",w,()->{PlanClient.clear();tab="statue";rebuildWidgets();});
|
||||
}
|
||||
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())PlanClient.load(files.getFirst());}
|
||||
@Override public void onFilesDrop(List<Path> files){
|
||||
if(workshop&&MetatableClient.available()&&!files.isEmpty()){
|
||||
var file=files.getFirst();if(fr.koka.sanctuary.plans.CreationLibrary.accepts(file,fr.koka.sanctuary.plans.CreationLibrary.Kind.MODEL))chooseModel(file);else PlanClient.load(file);
|
||||
}else MetatableClient.needWorkshop();
|
||||
}
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g,int mouseX,int mouseY,float partial){
|
||||
super.extractRenderState(g,mouseX,mouseY,partial);g.centeredText(font,title,width/2,12,0xFFFFFFFF);
|
||||
var lines=font.split(PlanClient.status,Math.min(450,width-24));
|
||||
for(int i=0;i<Math.min(2,lines.size());i++)g.text(font,lines.get(i),(width-Math.min(450,width-24))/2,height-49+i*font.lineHeight,0xFFF3D68A);
|
||||
}
|
||||
@Override public boolean isPauseScreen(){return false;}
|
||||
@Override public void onClose(){minecraft.gui.setScreen(parent);}
|
||||
@Override public void onClose(){if(workshop)MetatableClient.leave();minecraft.gui.setScreen(parent);}
|
||||
private final class ModelPicker extends Screen {
|
||||
private final PlansScreen back;
|
||||
private String filter="";
|
||||
@@ -162,7 +254,7 @@ public final class PlansScreen extends Screen {
|
||||
update();
|
||||
}
|
||||
private void update(){
|
||||
var models=BuiltInRegistries.ENTITY_TYPE.stream().filter(t->BuiltInRegistries.ITEM.stream().anyMatch(i->i instanceof SpawnEggItem&&SpawnEggItem.getType(i.getDefaultInstance())==t))
|
||||
var models=BuiltInRegistries.ENTITY_TYPE.stream().filter(MetatableClient::knows).filter(t->BuiltInRegistries.ITEM.stream().anyMatch(i->i instanceof SpawnEggItem&&SpawnEggItem.getType(i.getDefaultInstance())==t))
|
||||
.filter(t->(t.getDescription().getString()+" "+BuiltInRegistries.ENTITY_TYPE.getKey(t)).toLowerCase(Locale.ROOT).contains(filter.toLowerCase(Locale.ROOT)))
|
||||
.sorted(Comparator.comparing(t->t.getDescription().getString())).toList();
|
||||
pickerPage=Math.min(pickerPage,Math.max(0,(models.size()-1)/entries.size()));
|
||||
@@ -175,6 +267,7 @@ public final class PlansScreen extends Screen {
|
||||
}
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g,int x,int y,float p){super.extractRenderState(g,x,y,p);g.centeredText(font,title,width/2,12,0xFFFFFFFF);}
|
||||
@Override public boolean isPauseScreen(){return false;}
|
||||
@Override public void onClose(){back.tab="statue";minecraft.gui.setScreen(back);}
|
||||
@Override public void tick(){if(!MetatableClient.available()){MetatableClient.leave();minecraft.gui.setScreen(null);}}
|
||||
@Override public void onClose(){if(MetatableClient.available()){back.tab="statue";minecraft.gui.setScreen(back);}else {MetatableClient.leave();minecraft.gui.setScreen(null);}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class ProgressionScreen extends BlocodexFrame {
|
||||
private int pendingTicks;
|
||||
private int panel, panelTop, listTop, listBottom;
|
||||
private ScrollableLayout aptitudes, skills;
|
||||
private InlineAdvancements advancements;
|
||||
private int shownXp=-1;
|
||||
private final List<Button> purchases=new ArrayList<>();
|
||||
|
||||
@@ -51,23 +52,12 @@ public final class ProgressionScreen extends BlocodexFrame {
|
||||
|
||||
private void initProgression() {
|
||||
var progress=ProgressionClient.progress;
|
||||
int navigationWidth=(contentWidth-4)/2;
|
||||
String[] pages={"cycle","log"};
|
||||
for(int i=0;i<pages.length;i++) {
|
||||
String page=pages[i];
|
||||
var button=addRenderableWidget(Button.builder(fr.koka.sanctuary.cycle.CycleService.label(page),
|
||||
b -> CycleClient.open(this,page)).bounds(contentX+i*(navigationWidth+4),contentTop+14,navigationWidth,20).build());
|
||||
if(page.equals("cycle"))button.setTooltip(Tooltip.create(fr.koka.sanctuary.cycle.CycleService.label("eligibility")));
|
||||
}
|
||||
int fullNameWidth=0;
|
||||
for (String skill:SKILLS) fullNameWidth=Math.max(fullNameWidth,font.width(ProgressionService.label(skill)));
|
||||
// Give the XP bar the remaining width; keep the aptitude column and inline controls.
|
||||
int fixedWidth=9+40+4*4;
|
||||
panel=Math.min(contentWidth-120,Math.max((contentWidth-10)*3/5,fixedWidth+fullNameWidth+96+24));
|
||||
panelTop=contentTop+40;listTop=panelTop+16;listBottom=contentBottom-14;
|
||||
addRenderableWidget(Button.builder(Component.translatable("gui.advancements"),b -> minecraft.gui.setScreen(
|
||||
new net.minecraft.client.gui.screens.advancements.AdvancementsScreen(minecraft.player.connection.getAdvancements(),this)))
|
||||
.bounds(contentX+6,panelTop,panel-12,20).build());
|
||||
panelTop=contentTop+14;listTop=panelTop+16;listBottom=contentBottom-14;
|
||||
shownXp=minecraft.player.experienceLevel;
|
||||
LinearLayout skillContents=LinearLayout.vertical().spacing(4);
|
||||
int rowWidth=panel-24;
|
||||
@@ -94,8 +84,12 @@ public final class ProgressionScreen extends BlocodexFrame {
|
||||
plus.active=plus.visible && pendingTicks==0;
|
||||
plus.setTooltip(Tooltip.create(ProgressionService.label("upgrade_cost",Math.max(0,cost)).copy().append("\n").append(detail)));
|
||||
}
|
||||
var tree=new InlineAdvancements(rowWidth);tree.copyPosition(advancements);advancements=tree;skillContents.addChild(tree);
|
||||
var links=skillContents.addChild(LinearLayout.horizontal().spacing(4));
|
||||
for(String page:List.of("cycle","log"))links.addChild(Button.builder(fr.koka.sanctuary.cycle.CycleService.label(page),
|
||||
b->CycleClient.open(this,page)).width((rowWidth-4)/2).build());
|
||||
double skillOffset=skills==null ? 0 : skills.getScrollAmount();
|
||||
skills=scroll(skillContents,contentX+6,listTop+24,Math.max(24,listBottom-listTop-24),skillOffset);
|
||||
skills=scroll(skillContents,contentX+6,listTop,Math.max(24,listBottom-listTop),skillOffset);
|
||||
int innerWidth=contentWidth-panel-32;
|
||||
LinearLayout contents=LinearLayout.vertical().spacing(12);
|
||||
for(String target:ProgressRecord.APTITUDES) {
|
||||
@@ -229,7 +223,7 @@ public final class ProgressionScreen extends BlocodexFrame {
|
||||
if (!ProgressionClient.active()) return;
|
||||
if (tab.equals("progress")) {
|
||||
graphics.text(font,ProgressionService.label("levels",minecraft.player.experienceLevel),contentX,contentTop,0xFFDDDDDD);
|
||||
graphics.text(font,ProgressionService.label(ProgressionClient.operator ? "skills_operator" : "skills"),contentX+6,panelTop+27,0xFFA9D7A0);
|
||||
graphics.text(font,ProgressionService.label(ProgressionClient.operator ? "skills_operator" : "skills"),contentX+6,panelTop+3,0xFFA9D7A0);
|
||||
graphics.text(font,ProgressionService.label("aptitudes"),contentX+panel+16,panelTop+3,0xFFA6CAE8);
|
||||
if (!message.isEmpty()) graphics.centeredText(font,ProgressionService.label(message),width/2,contentBottom-9,0xFFFFFFAA);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.Blaze3D;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Comparator;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Util;
|
||||
|
||||
/** Explicit, one-time export. The player's copy is never a managed mod resource. */
|
||||
public final class ResourcePackTemplate {
|
||||
public static final String FOLDER = "Sanctuary-Custom";
|
||||
|
||||
private ResourcePackTemplate() {}
|
||||
|
||||
public static Button button(Path packDirectory) {
|
||||
Path target = packDirectory.resolve(FOLDER);
|
||||
return Button.builder(label(Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)
|
||||
? "open_copy" : "customize"), button -> {
|
||||
if (Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Blaze3D.openPath(target);
|
||||
return;
|
||||
}
|
||||
button.active = false;
|
||||
button.setMessage(label("creating"));
|
||||
Util.ioPool().execute(() -> {
|
||||
try {
|
||||
create(packDirectory);
|
||||
Minecraft.getInstance().execute(() -> {
|
||||
button.active = true;
|
||||
button.setMessage(label("open_copy"));
|
||||
button.setTooltip(Tooltip.create(label("copy_ready")));
|
||||
});
|
||||
} catch (IOException | RuntimeException error) {
|
||||
SanctuaryMod.LOGGER.error("Could not create personal Sanctuary resource pack", error);
|
||||
Minecraft.getInstance().execute(() -> {
|
||||
button.active = true;
|
||||
button.setMessage(label("copy_failed"));
|
||||
button.setTooltip(Tooltip.create(label("copy_failed_help")));
|
||||
});
|
||||
}
|
||||
});
|
||||
}).width(308).tooltip(Tooltip.create(label("customize_help"))).build();
|
||||
}
|
||||
|
||||
/** Returns an existing personal directory untouched, including after mod upgrades. */
|
||||
public static synchronized Path create(Path packDirectory) throws IOException {
|
||||
Path directory = packDirectory.toAbsolutePath().normalize();
|
||||
Files.createDirectories(directory);
|
||||
Path target = directory.resolve(FOLDER);
|
||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) return existing(target);
|
||||
var mod = FabricLoader.getInstance().getModContainer(SanctuaryMod.ID).orElseThrow();
|
||||
// Keep incomplete packs outside the directory watched by the native pack screen.
|
||||
Path staging = Files.createTempDirectory(directory.getParent(), ".sanctuary-template-");
|
||||
try {
|
||||
copyTree(mod.findPath("assets").orElseThrow(), staging.resolve("assets"));
|
||||
copyTree(mod.findPath("resourcepacks/textures/assets").orElseThrow(), staging.resolve("assets"));
|
||||
copyTree(mod.findPath("resourcepacks/template").orElseThrow(), staging);
|
||||
Files.copy(mod.findPath("resourcepacks/textures/pack.png").orElseThrow(), staging.resolve("pack.png"));
|
||||
try {
|
||||
Files.move(staging, target);
|
||||
} catch (FileAlreadyExistsException race) {
|
||||
return existing(target);
|
||||
}
|
||||
return target;
|
||||
} finally {
|
||||
if (Files.exists(staging)) {
|
||||
try (var files = Files.walk(staging)) {
|
||||
for (Path file : files.sorted(Comparator.reverseOrder()).toList()) Files.delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path existing(Path target) throws IOException {
|
||||
if (!Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Personal pack path already exists and is not a directory: " + target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static void copyTree(Path source, Path destination) throws IOException {
|
||||
try (var files = Files.walk(source)) {
|
||||
for (Path file : files.toList()) {
|
||||
if (Files.isSymbolicLink(file)) throw new IOException("Unexpected asset symlink: " + file);
|
||||
// String paths also work when the mod is loaded from a ZIP filesystem.
|
||||
Path output = destination.resolve(source.relativize(file).toString()).normalize();
|
||||
if (!output.startsWith(destination)) throw new IOException("Invalid asset path: " + file);
|
||||
if (Files.isDirectory(file)) Files.createDirectories(output);
|
||||
else Files.copy(file, output, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Component label(String key) {
|
||||
return Component.translatable("sanctuary.resource_pack." + key);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
FoodKnowledgeClient.register();
|
||||
MultiblockScreen.register();
|
||||
FourneauModels.register();
|
||||
WrenchTextureModels.register();
|
||||
MetatableClient.register();
|
||||
ClayWorkshopScreen.register();
|
||||
GraveScreen.register();
|
||||
GraveInventoryScreen.register();
|
||||
TutorialClient.register();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** Bounded samples of the baked faces' actual textures, with their own tint from the active pack. */
|
||||
final class SculptureMaterialSampler {
|
||||
private static final int MAX_MATERIALS=32,SAMPLE_EDGE=16;
|
||||
static SculpturePalette sample(ItemStack stack){
|
||||
if(!ClayWorkshopMenu.material(stack))return null;
|
||||
var state=((BlockItem)stack.getItem()).getBlock().defaultBlockState();var sample=PlanMaterials.sample(state);
|
||||
var mc=net.minecraft.client.Minecraft.getInstance();
|
||||
var parts=new ArrayList<BlockStateModelPart>();
|
||||
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(state.getSeed(BlockPos.ZERO)),parts);
|
||||
var materials=new LinkedHashSet<BakedQuad.MaterialInfo>();
|
||||
for(var part:parts)for(int face=0;face<=6;face++)for(var quad:part.getQuads(face==6?null:Direction.values()[face])){
|
||||
if(materials.size()<MAX_MATERIALS)materials.add(quad.materialInfo());
|
||||
}
|
||||
var pixels=new ArrayList<Integer>();
|
||||
for(var material:materials)add(pixels,PlanMaterials.faceSample(state,material));
|
||||
if(pixels.isEmpty()){
|
||||
var tints=mc.getBlockColors().getTintSources(state);
|
||||
int tint=tints.isEmpty()?0xFFFFFFFF:tints.getFirst().colorInWorld(state,mc.level,mc.player.blockPosition());
|
||||
add(pixels,new PlanMaterials.Sample(sample.pixels(),tint,sample.average()));
|
||||
}
|
||||
if(pixels.isEmpty())pixels.add(sample.average()|0xFF000000);
|
||||
// Keep repeated pixels: they retain the sampled frequency of each shade.
|
||||
pixels.sort(Comparator.comparingDouble(SculpturePalette::lightness).thenComparingInt(Integer::intValue));
|
||||
var palette=new ArrayList<Integer>();int count=Math.min(SculpturePalette.MAX_COLORS,pixels.size());
|
||||
for(int i=0;i<count;i++)palette.add(pixels.get((int)((i+.5)*pixels.size()/count)));
|
||||
return new SculpturePalette(palette);
|
||||
}
|
||||
private static void add(List<Integer> pixels,PlanMaterials.Sample sample){
|
||||
var texture=sample.pixels();int width=Math.min(SAMPLE_EDGE,texture.width()),height=Math.min(SAMPLE_EDGE,texture.height());
|
||||
for(int y=0;y<height;y++)for(int x=0;x<width;x++){
|
||||
int sx=(2*x+1)*texture.width()/(2*width),sy=(2*y+1)*texture.height()/(2*height);
|
||||
int color=ARGB.multiply(texture.argb()[sy*texture.width()+sx],sample.tint());
|
||||
if((color>>>24)>=128)pixels.add(color|0xFF000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.client.renderer.*;
|
||||
import net.minecraft.client.renderer.blockentity.*;
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.TypedEntityData;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/** Shared block/item render geometry: opaque visible faces, generated once per cached sculpture. */
|
||||
public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntity,StatuaryRenderer.State> {
|
||||
record Quad(float x,float y,float z,int side,int color){}
|
||||
private record Placement(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){}
|
||||
private static final Map<Placement,List<Quad>> MESHES=new LinkedHashMap<>(32,.75f,true);
|
||||
private static final Map<TypedEntityData<?>,VoxelSculpture> ITEMS=new LinkedHashMap<>(32,.75f,true);
|
||||
private static final float[][] FACES={
|
||||
{0,0,0, 0,1,0, 1,1,0, 1,0,0, 0,0,-1},
|
||||
{1,0,1, 1,1,1, 0,1,1, 0,0,1, 0,0,1},
|
||||
{0,0,1, 0,1,1, 0,1,0, 0,0,0, -1,0,0},
|
||||
{1,0,0, 1,1,0, 1,1,1, 1,0,1, 1,0,0},
|
||||
{0,1,0, 0,1,1, 1,1,1, 1,1,0, 0,1,0},
|
||||
{0,0,1, 0,0,0, 1,0,0, 1,0,1, 0,-1,0}
|
||||
};
|
||||
private static final int[][] STEP={{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}};
|
||||
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();}
|
||||
public State createRenderState(){return new State();}
|
||||
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){
|
||||
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);
|
||||
state.mesh=mesh(entity.sculpture(),entity.getBlockState().getValue(Statuary.FACING),entity.getBlockState().getValue(Statuary.ANCHOR));
|
||||
}
|
||||
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){
|
||||
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);
|
||||
}
|
||||
private static List<Quad> mesh(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
|
||||
if(sculpture==null)return List.of();var key=new Placement(sculpture,facing,anchor);var cached=MESHES.get(key);if(cached!=null)return cached;
|
||||
var placed=SculptureGeometry.place(sculpture,facing,anchor);sculpture=placed.sculpture();
|
||||
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
||||
float ox=placed.offsetX()/16f,oz=placed.offsetZ()/16f;
|
||||
for(var cell:sculpture.cells())for(int side=0;side<6;side++){
|
||||
var s=STEP[side];int x=cell.x()+s[0],y=cell.y()+s[1],z=cell.z()+s[2];
|
||||
if(x>=0&&x<16&&y>=0&&y<16&&z>=0&&z<16&&occupied.get(x|y<<4|z<<8))continue;
|
||||
quads.add(new Quad(ox+cell.x()/16f,cell.y()/16f,oz+cell.z()/16f,side,cell.color()));
|
||||
}
|
||||
cached=List.copyOf(quads);MESHES.put(key,cached);if(MESHES.size()>32)MESHES.remove(MESHES.keySet().iterator().next());return cached;
|
||||
}
|
||||
private static void draw(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay){
|
||||
if(mesh.isEmpty())return;
|
||||
collector.submitCustomGeometry(pose,RenderTypes.entitySolid(SanctuaryMod.id("textures/misc/sculpture_white.png")),(matrix,vertices)->{
|
||||
for(var quad:mesh){var face=FACES[quad.side];for(int i=0;i<4;i++)vertices.addVertex(matrix,quad.x+face[i*3]/16f,quad.y+face[i*3+1]/16f,quad.z+face[i*3+2]/16f)
|
||||
.setColor(quad.color).setUv(i<2?0:1,i==0||i==3?1:0).setOverlay(overlay).setLight(light).setNormal(matrix,face[12],face[13],face[14]);}
|
||||
});
|
||||
}
|
||||
public static final class ItemRenderer implements SpecialModelRenderer<List<Quad>> {
|
||||
public List<Quad> extractArgument(ItemStack stack){
|
||||
var data=stack.get(DataComponents.BLOCK_ENTITY_DATA);if(data==null)return List.of();
|
||||
var sculpture=ITEMS.get(data);
|
||||
if(sculpture==null){sculpture=StatuaryEntity.sculpture(stack);if(sculpture==null)return List.of();ITEMS.put(data,sculpture);if(ITEMS.size()>64)ITEMS.remove(ITEMS.keySet().iterator().next());}
|
||||
return mesh(sculpture,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
}
|
||||
public void submit(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay,boolean foil,int outline){draw(mesh,pose,collector,light,overlay);}
|
||||
public void getExtents(java.util.function.Consumer<Vector3fc> out){out.accept(new Vector3f(0,0,0));out.accept(new Vector3f(1,1,1));}
|
||||
}
|
||||
public record Unbaked() implements SpecialModelRenderer.Unbaked<List<Quad>> {
|
||||
public static final MapCodec<Unbaked> CODEC=MapCodec.unit(new Unbaked());
|
||||
public MapCodec<Unbaked> type(){return CODEC;}
|
||||
public SpecialModelRenderer<List<Quad>> bake(SpecialModelRenderer.BakingContext context){return new ItemRenderer();}
|
||||
}
|
||||
public static void register(){
|
||||
net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry.register(Statuary.TYPE,c->new StatuaryRenderer());
|
||||
fr.koka.sanctuary.mixin.client.SpecialModelsAccess.sanctuary$types().put(SanctuaryMod.id("statuary"),Unbaked.CODEC);
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,30 @@ import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Small stable palettes of full blocks, sampled from the active resource pack. */
|
||||
/** Material colors from the active resource pack, including an unrestricted block-item palette. */
|
||||
public final class StatuePalette {
|
||||
public record Material(BlockState state, int color, double[] lab) { }
|
||||
public record Material(BlockState state, int color, double[] lab,double shapePenalty) {
|
||||
Material(BlockState state,int color,double[] lab){this(state,color,lab,0);}
|
||||
}
|
||||
private final List<Material> materials;
|
||||
private final Map<Integer, BlockState> matches = new HashMap<>();
|
||||
public StatuePalette(String family) throws IOException {
|
||||
if (!List.of("concrete", "wool", "mixed").contains(family)) throw new IllegalArgumentException("palette");
|
||||
if (!List.of("all", "concrete", "wool", "mixed").contains(family)) throw new IllegalArgumentException("palette");
|
||||
var result = new ArrayList<Material>();
|
||||
if(family.equals("all")){
|
||||
for(var block:BuiltInRegistries.BLOCK){
|
||||
if(block.asItem()==net.minecraft.world.item.Items.AIR||block instanceof net.minecraft.world.level.block.GameMasterBlock)continue;
|
||||
var state=block.defaultBlockState();if(state.isAir())continue;
|
||||
var persistent=net.minecraft.world.level.block.state.properties.BlockStateProperties.PERSISTENT;
|
||||
if(state.hasProperty(persistent))state=state.setValue(persistent,true);
|
||||
var mc=net.minecraft.client.Minecraft.getInstance();
|
||||
double volume=state.getShape(mc.level,mc.player.blockPosition()).toAabbs().stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();
|
||||
double penalty=.3*(1-Math.min(1,volume));
|
||||
if(block instanceof net.minecraft.world.level.block.FallingBlock)penalty+=.05;
|
||||
int color=PlanMaterials.sample(state).average();result.add(new Material(state,color,StatueVoxelizer.colorSpace(color),penalty));
|
||||
}
|
||||
materials=List.copyOf(result);return;
|
||||
}
|
||||
for (String set : family.equals("mixed") ? List.of("concrete","wool","terracotta") : List.of(family)) {
|
||||
for (DyeColor dye : DyeColor.values()) add(result,dye.getName()+"_"+set);
|
||||
}
|
||||
@@ -35,7 +51,7 @@ public final class StatuePalette {
|
||||
public BlockState nearest(int color) {
|
||||
return matches.computeIfAbsent(color, c->{
|
||||
double[] lab=StatueVoxelizer.colorSpace(c);double best=Double.POSITIVE_INFINITY;Material chosen=materials.getFirst();
|
||||
for(Material m:materials){double distance=0;for(int i=0;i<3;i++)distance+=(lab[i]-m.lab[i])*(lab[i]-m.lab[i]);if(distance<best){best=distance;chosen=m;}}
|
||||
for(Material m:materials){double distance=m.shapePenalty();for(int i=0;i<3;i++)distance+=(lab[i]-m.lab[i])*(lab[i]-m.lab[i]);if(distance<best){best=distance;chosen=m;}}
|
||||
return chosen.state;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.*;
|
||||
import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
|
||||
import net.fabricmc.fabric.api.client.model.loading.v1.wrapper.WrapperBlockStateModel;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.*;
|
||||
import net.fabricmc.fabric.api.client.renderer.v1.mesh.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.BlockAndTintGetter;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Immutable visual snapshots, usable from mesh workers and rebuilt after resource reloads. */
|
||||
public final class WrenchTextureModels {
|
||||
private static volatile Map<Long,Map<BlockPos,Integer>> chunks=Map.of();
|
||||
public static int rotation(BlockPos pos){return chunks.getOrDefault(ChunkPos.pack(pos),Map.of()).getOrDefault(pos,0);}
|
||||
public static void register(){
|
||||
ClientPlayNetworking.registerGlobalReceiver(WrenchTextures.Update.TYPE,(packet,context)->context.client().execute(()->apply(packet)));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler,client)->chunks=Map.of());
|
||||
ClientLevelEvents.AFTER_CLIENT_LEVEL_CHANGE.register((client,level)->chunks=Map.of());
|
||||
ClientChunkEvents.CHUNK_UNLOAD.register((level,chunk)->{var next=new HashMap<>(chunks);next.remove(chunk.getPos().pack());chunks=Map.copyOf(next);});
|
||||
ModelLoadingPlugin.register(context->context.modifyBlockModelAfterBake().register((model,bake)->new Rotated(model)));
|
||||
}
|
||||
private static void apply(WrenchTextures.Update packet){
|
||||
var client=Minecraft.getInstance();if(client.level==null||!client.level.dimension().identifier().equals(packet.dimension()))return;
|
||||
long key=packet.chunk().pack();var old=chunks.getOrDefault(key,Map.of());var dirty=new HashSet<BlockPos>();
|
||||
var entries=packet.clear()?new HashMap<BlockPos,Integer>():new HashMap<>(old);if(packet.clear())dirty.addAll(old.keySet());
|
||||
for(var entry:packet.entries()){
|
||||
var before=entry.rotation()==0?entries.remove(entry.pos()):entries.put(entry.pos(),entry.rotation());
|
||||
if(!Objects.equals(before,entry.rotation()))dirty.add(entry.pos());
|
||||
}
|
||||
var next=new HashMap<>(chunks);if(entries.isEmpty())next.remove(key);else next.put(key,Map.copyOf(entries));chunks=Map.copyOf(next);
|
||||
var sections=new HashSet<SectionPos>();for(var pos:dirty)sections.add(SectionPos.of(pos));
|
||||
for(var section:sections)client.levelExtractor.setSectionDirty(section.x(),section.y(),section.z());
|
||||
}
|
||||
static boolean rotate(MutableQuadView quad,int turns){
|
||||
// Rotate texture coordinates only: geometry, light, tints and materials stay native.
|
||||
for(int turn=0;turn<turns;turn++){
|
||||
float u=quad.u(0),v=quad.v(0);
|
||||
for(int vertex=0;vertex<3;vertex++)quad.uv(vertex,quad.u(vertex+1),quad.v(vertex+1));
|
||||
quad.uv(3,u,v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private record Geometry(Object original,int rotation){}
|
||||
private static final class Rotated extends WrapperBlockStateModel {
|
||||
Rotated(BlockStateModel original){super(original);}
|
||||
@Override public void emitQuads(QuadEmitter emitter,BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random,Predicate<Direction> cull){
|
||||
int turns=rotation(pos);if(turns==0){wrapped.emitQuads(emitter,view,pos,state,random,cull);return;}
|
||||
emitter.pushTransform(quad->rotate(quad,turns));
|
||||
try{wrapped.emitQuads(emitter,view,pos,state,random,cull);}finally{emitter.popTransform();}
|
||||
}
|
||||
@Override public Object createGeometryKey(BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random){
|
||||
var key=wrapped.createGeometryKey(view,pos,state,random);int turns=rotation(pos);
|
||||
return turns==0||key==null?key:new Geometry(key,turns);
|
||||
}
|
||||
}
|
||||
private WrenchTextureModels(){}
|
||||
}
|
||||
@@ -49,12 +49,18 @@ public final class PanoramaCapture {
|
||||
boolean fogEnabled=PanoramaFogAccess.sanctuary$enabled();
|
||||
int[] turns={0,90,180,-90,0,0};
|
||||
for(int face=0;face<6;face++){
|
||||
// Each cube face is a separate GPU submission: native post-pass ring buffers
|
||||
// must not wrap around fences belonging to this same unsubmitted batch.
|
||||
RenderSystem.getDevice().createCommandEncoder().submit();
|
||||
p.setYRot(yaw+turns[face]);p.setXRot(face==4?-90:face==5?90:0);p.yRotO=p.getYRot();p.xRotO=p.getXRot();
|
||||
m.gameRenderer.update(DeltaTracker.ONE);m.gameRenderer.extract(DeltaTracker.ONE,true);
|
||||
var state=m.gameRenderer.gameRenderState();var c=state.levelRenderState.cameraRenderState;
|
||||
state.optionsRenderState.bobView=false;state.optionsRenderState.damageTiltStrength=0;state.optionsRenderState.screenEffectScale=0;
|
||||
c.entityRenderState.isLiving=false;
|
||||
var renderer=(PanoramaRendererAccess)m.gameRenderer;
|
||||
renderer.sanctuary$prepareEffects(state.requestedPostEffects);
|
||||
m.gameRenderer.renderLevel();
|
||||
renderer.sanctuary$applyEffects();
|
||||
var matrix=new Matrix4f(c.projectionMatrix).mul(c.viewRotationMatrix);var inverse=new Matrix4f(matrix).invert();
|
||||
var fog=PanoramaDepth.Fog.of(c.fogData);float far=c.depthFar;
|
||||
var info=new LinkedHashMap<String,Object>();info.put("face",face);info.put("yaw",c.yRot);info.put("pitch",c.xRot);info.put("camera",new double[]{c.pos.x,c.pos.y,c.pos.z});info.put("view_projection_column_major",matrix.get(new float[16]));info.put("inverse_view_projection_column_major",inverse.get(new float[16]));info.put("depth_png_max_blocks",far);info.put("fog",fog);faces.add(info);
|
||||
@@ -74,7 +80,8 @@ public final class PanoramaCapture {
|
||||
}catch(Throwable e){depthDone.completeExceptionally(e);}
|
||||
},0);}catch(Throwable e){buffer.close();depthDone.completeExceptionally(e);throw e;}
|
||||
}
|
||||
var metadata=new LinkedHashMap<String,Object>();metadata.put("schema",1);metadata.put("minecraft",net.minecraft.SharedConstants.getCurrentVersion().name());metadata.put("size",size);metadata.put("fov_degrees",90);metadata.put("depth_reversed",true);metadata.put("clip_z_zero_to_one",zeroToOne);metadata.put("fog_enabled",fogEnabled);metadata.put("pixel_origin","top_left");metadata.put("depth_f32","Raw reversed GPU depth; little-endian float32, row-major, zero = no surface");metadata.put("depth_png","16-bit grayscale spherical distance / per-face depth_png_max_blocks; sky white");metadata.put("fog_png","Reconstructed native terrain fog opacity, 16-bit grayscale; no-surface sky white. Transparent surfaces, clouds, particles and custom shaders may differ.");metadata.put("faces",faces);
|
||||
RenderSystem.getDevice().createCommandEncoder().submit();
|
||||
var metadata=new LinkedHashMap<String,Object>();metadata.put("schema",1);metadata.put("minecraft",net.minecraft.SharedConstants.getCurrentVersion().name());metadata.put("size",size);metadata.put("fov_degrees",90);metadata.put("depth_reversed",true);metadata.put("clip_z_zero_to_one",zeroToOne);metadata.put("fog_enabled",fogEnabled);metadata.put("pixel_origin","top_left");metadata.put("depth_f32","Raw reversed GPU depth; little-endian float32, row-major, zero = no surface");metadata.put("depth_png","16-bit grayscale spherical distance / per-face depth_png_max_blocks; sky white");metadata.put("fog_png","Reconstructed native terrain fog opacity, 16-bit grayscale; no-surface sky white. Transparent surfaces, clouds, particles and custom shaders may differ.");metadata.put("post_effects",m.gameRenderer.getAppliedPostEffects().stream().map(Object::toString).toList());metadata.put("faces",faces);
|
||||
final Path output=folder;writes.add(CompletableFuture.runAsync(()->{try{Files.writeString(output.resolve("capture.json"),new GsonBuilder().setPrettyPrinting().create().toJson(metadata),StandardOpenOption.CREATE_NEW);}catch(Exception e){throw new CompletionException(e);}},Util.ioPool()));
|
||||
}catch(Throwable e){failure=e;}
|
||||
finally{
|
||||
|
||||
@@ -30,7 +30,9 @@ public final class CompanionQueries {
|
||||
public static void observeSpecies(ServerPlayer p){
|
||||
var record=CompanionService.record(p);if(record==null)return;
|
||||
var entity=CompanionWorld.aimed(p,12,e->e instanceof Mob);if(entity==null)return;
|
||||
var id=BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).getPath();
|
||||
var type=entity instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity familiar
|
||||
&&familiar.egg().getItem() instanceof net.minecraft.world.item.SpawnEggItem?net.minecraft.world.item.SpawnEggItem.getType(familiar.egg()):entity.getType();
|
||||
var id=BuiltInRegistries.ENTITY_TYPE.getKey(type).getPath();
|
||||
if(CompanionType.find(id)==null)return;
|
||||
var seen=new TreeSet<>(Arrays.asList(record.notes().getOrDefault("seen_mobs","").split(",")));seen.remove("");
|
||||
if(seen.add(id))record=record.note("seen_mobs",String.join(",",seen));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user