Compare commits

..
Author SHA1 Message Date
koka de6e11848a Prépare les notes de publication beta.318
Build Sanctuary / build (push) Canceled after 0s
2026-10-30 06:52:51 +01:00
koka 303210124e Consigne l’installation locale de beta.318 dans Prism 2026-10-30 06:44:48 +01:00
koka 980544e279 Ajoute les 18 recettes séchées et les réactions épicées beta.318 2026-10-30 06:26:53 +01:00
koka 7ef4d51a59 Ajoute les provisions séchées et le guide culinaire beta.317 2026-10-30 05:38:54 +01:00
koka 7b56c3cd78 Add climate-based survival foraging and the nine-island food wiki 2026-10-30 01:01:52 +01:00
koka bb61547b07 Align current beta.315 summary with published status
Build Sanctuary / build (push) Canceled after 0s
2026-10-29 10:09:41 +01:00
koka ebd35184b7 Record verified beta.315 rollout and editable texture export
Build Sanctuary / build (push) Canceled after 0s
2026-10-29 10:07:46 +01:00
koka 359f49792b Prepare It's Alive texture editing kit and beta.315 release notes
Build Sanctuary / build (push) Canceled after 0s
2026-10-29 09:49:29 +01:00
koka 4cbf350e01 Add placed cured sausage with four shared slices (beta.315) 2026-10-29 09:05:46 +01:00
koka 03935e9c89 Add shared cheese wheels and real-time wine maturation (beta.314) 2026-10-29 08:25:50 +01:00
koka 72c709ebc7 Add native sausage preparation and four-hook drying rack (beta.313) 2026-10-29 05:33:48 +01:00
koka addc4bb4db Repeat the chosen cauldron recipe across supply and serving (beta.312) 2026-10-29 04:38:03 +01:00
koka 228a953475 Start cauldron cooking by stirring and preserve ingredient containers (beta.311) 2026-10-29 04:00:59 +01:00
koka 91db17450a Stack frying pans with one paid recipe per pile (beta.310) 2026-10-28 21:15:25 +01:00
koka 36941f611f Keep one oven output and manual ingredient loading (beta.309) 2026-10-28 19:56:50 +01:00
koka 9dd63f683a Refine oven preparation and automatic cooking (beta.308) 2026-10-28 18:59:08 +01:00
koka 60bc773b80 Replace mill recipe redstone with a manual handle (beta.307) 2026-10-28 18:32:48 +01:00
koka 9fd380b782 Make the mill manual like native stonecutting (beta.306) 2026-10-28 18:07:35 +01:00
koka ca5e350e4a Replace food press with layered fruit crushing (beta.305) 2026-10-28 15:02:40 +01:00
koka feaf9ca62c Port culinary appliances and timed recipes for beta.304 2026-10-28 14:13:50 +01:00
koka a60937f77d Import 26.2 food recipes with native crafting and cooking (beta.303) 2026-10-28 09:20:23 +01:00
koka 930719d75e Rework vanilla growth textures at 16x16 (beta.302) 2026-10-28 08:01:19 +01:00
koka 394e22d805 Add upward tree-climbing vanilla with integrated sprites (beta.301) 2026-10-28 07:49:39 +01:00
koka ed11cd77a5 Port It's Alive crops, fruit trees and coffee to 26.3 (beta.300) 2026-10-28 07:11:42 +01:00
koka d78d39cd1b Import It's Alive agriculture and cooking textures for 26.3 (beta.299) 2026-10-28 06:17:33 +01:00
koka 77fdabfb4d docs: record complete beta.298 deployment
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 06:02:41 +01:00
koka fb8705369e docs: align beta.298 server kit instructions
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:58:46 +01:00
koka c3d1443732 docs: prepare beta.298 release notes
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:57:03 +01:00
koka 1967342247 fix: exclude invisible media anchors from PBR normals
Build Sanctuary / build (push) Canceled after 0s
2026-10-27 05:36:47 +01:00
577 changed files with 28405 additions and 190 deletions
+234 -4
View File
@@ -1,7 +1,7 @@
# Sanctuary
[Dernière release : beta.297](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.297) ·
[Reçu de livraison](docs/release-beta297.md) ·
[Dernière release : beta.315](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.315) ·
[Reçu de livraison](docs/release-beta315.md) ·
[Installation et mises à jour](docs/packwiz.md).
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
@@ -10,6 +10,235 @@ Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.318 — cuisiner les produits séchés (préparation locale)
[Les 18 nouvelles recettes et leur fonctionnement](docs/dried-cuisine-beta318.md) :
pains, biscuits, sandwichs, plats au chaudron, salade, dessert, provisions et épices.
Le saucisson pimenté se prépare, sèche puis se pose pour partager quatre tranches.
Manger épicé produit deux petits coups visuels et sonores sans dégât.
[Ingrédients, quantités, appareils et durées](docs/wiki/alimentation/sechoir.md).
Même branche locale. [Installée dans Prism pour les essais](docs/local-beta318.md),
sans publication. Build et packs vérifiés, **189 tests serveur** et parcours
**Vulkan avec Sodium** réussis.
## beta.317 — produits séchés (préparation locale)
[Viande, poissons, fruits, herbes et vanille au séchoir](docs/drying-foods-beta317.md) :
15 recettes avec leurs propres durées, 12 objets supplémentaires, quatre crochets
indépendants. Les herbes et piments séchés rejoignent la cuisine existante.
[Recettes et 18 propositions de combinaisons](docs/wiki/alimentation/sechoir.md).
Même branche locale, sans publication ni installation. Build et packs vérifiés,
**179 tests serveur** et parcours **Vulkan avec Sodium** réussis.
## beta.316 — découverte alimentaire en survie (préparation locale)
[IA-09 : récolter et rapporter les premières semences](docs/foraging-beta316.md).
Plantes sauvages, fruitiers, café et vanille répartis par habitat sur les neuf îles ;
solutions de découverte dans les terrains déjà explorés, sans repeuplement.
[Guide et pages des îles](docs/wiki/alimentation/README.md) ·
[Planche illustrée](docs/wiki/alimentation/planche.html). Même branche locale ;
aucune publication ou installation par ce ticket.
Build et packs vérifiés, **85 tests serveur** et parcours **Vulkan avec Sodium**
réussis ; premières récoltes et sauvegarde sur le preset Sanctuary actuel.
## beta.315 — cuisine et agriculture livrées
[Livraison complète vérifiée](docs/release-beta315.md) : les incréments locaux
beta.299 à beta.315 sont publiés et installés sur la Beta dédiée, le site et
la même instance Prism. **187 tests serveur** et artefacts vérifiés ;
sauvegardes et réglages conservés. [Textures modifiables](docs/itsalive-textures-export-beta315.md).
### Saucisson posé en tranches
[Poser le saucisson séché et partager quatre tranches](docs/sausage-slices-beta315.md).
Même objet après le séchoir, quatre orientations, modèle qui raccourcit et
chair visible sur la coupe. Deux points de nourriture par tranche, sons natifs
et parts restantes conservées après sauvegarde. Préparation locale vérifiée avant cette publication. Build et packwiz, **134 tests serveur** et deux
parcours **Vulkan avec Sodium** réussis : séchoir, pose payée, quatre prises,
sauvegarde orientée, seize états de rendu et sons natifs.
## beta.314 — fromage à partager et vins de garde (préparation locale)
[Poser et partager le fromage, choisir la qualité du vin](docs/cheese-wine-beta314.md).
Le fromage garde son objet historique et se mange en sept parts posées,
comme un gâteau. Le vin jeune garde ses trois minutes de fermentation ;
le laisser dans le tonneau donne du vin de table après **un jour réel**,
de réserve après **trois jours** et un grand cru après **sept jours**,
même jeu fermé. Quatre objets/qualités, HUD FR/EN, recettes JEI et sortie
compatible avec les entonnoirs. Même branche locale, sans publication ou
installation. Build et packwiz, **126 tests serveur** et parcours **Vulkan
avec Sodium** réussis : sept parts, face de coupe, sauvegarde, jours réels,
qualités, collecte et consommation.
## beta.313 — saucisson au séchoir (préparation locale)
[Préparer le saucisson, puis le suspendre cinq minutes](docs/drying-rack-beta313.md).
Séchoir en bois sans combustible ni menu, quatre crochets indépendants,
pluie et immersion qui suspendent le séchage, toit protecteur et reprise
après sauvegarde. Recettes à l’établi, modèles cubiques aux textures vanilla,
HUD FR/EN et découverte dans JEI. Même branche locale, sans publication ou
installation. Build et packwiz, **112 tests serveur** et parcours
**Vulkan avec Sodium** réussis : fabrication, quatre crochets, humidité,
sauvegarde, collecte et consommation.
## beta.312 — recettes répétées au même chaudron (préparation locale)
[Un premier coup de bâton mémorise la recette](docs/cauldron-repeat-beta312.md).
Réapprovisionner avec les mêmes ingrédients et servir les plats ne demandent
plus de recommencer. Les ajouts en cours attendent leur lot complet ; la
sortie s’accumule et une sortie pleine attend de la place. Maj-clic à main
vide arrête la série. Même branche locale, sans publication ou installation.
Build et packwiz, **100 tests serveur** et parcours **Vulkan avec Sodium**
réussis : quatre lots après un geste, réassort, service, rechargement et arrêt.
## beta.311 — chaudron préparé puis remué (préparation locale)
[Ajouter les ingrédients, puis remuer avec un bâton](docs/cauldron-beta311.md).
Le chaudron attend le geste, conserve les mélanges incomplets et rend le seau
du lait seulement à la cuisson terminée. Toute modification demande de
remuer à nouveau ; les restes ne lancent pas une nouvelle recette. Sons
natifs discrets à l’ajout, au geste, pendant la cuisson et au service.
Même branche locale, sans publication ou installation. Build et packwiz,
**89 tests serveur** et parcours **Vulkan avec Sodium et contrôle audio**
réussis.
## beta.310 — poêles en ligne et piles de quatre (préparation locale)
[Une recette commune par pile de une à quatre poêles](docs/pan-stacks-beta310.md).
Chaque portion paie ses ingrédients, y compris les seaux de lait conservés
dans des réserves natives. Des piles voisines peuvent cuire des plats
différents, chacune avec sa chaleur par-dessous. Contenu visible, HUD FR/EN,
casse et continuité des anciennes cases documentés. Même branche locale,
sans publication ou installation. Build et packwiz, **76 tests serveur**
et parcours **Vulkan avec Sodium** réussis.
## beta.309 — une seule sortie de cuisinière (préparation locale)
[La cuisinière garde une seule sortie, comme un four](docs/oven-single-output-beta309.md).
La case d'aperçu disparaît. La grille se remplit à la main ; un entonnoir
dessous peut récupérer les plats cuits. Consommation en fin de cuisson,
annulation des modifications et poursuite avec le menu fermé conservées.
Même branche locale, sans publication ou installation.
Build et packwiz, **63 tests serveur** et parcours **Vulkan avec Sodium**
réussis ; sortie unique, entonnoirs et décalage du feu vérifiés.
## beta.308 — cuisinière automatique (préparation locale)
[Grille de préparation et cuisson comme un four](docs/oven-beta308.md) :
aperçu séparé, ingrédients consommés uniquement en fin de cuisson, annulation
si la grille change et lots successifs avec le menu fermé. Treize recettes de
cuisine, combustible natif et sortie réservée aux plats terminés.
Même branche locale, sans publication ou installation.
Build et packwiz, **63 tests serveur** et parcours **Vulkan avec Sodium**
réussis : grille modifiable, annulation, lots avec le menu fermé, menus FR/EN,
sortie et rechargement réel.
## beta.307 — recette du moulin manuel (préparation locale)
[La recette remplace la redstone par un bâton](docs/mill-recipe-beta307.md)
pour représenter la poignée, avec les trois dalles de pierre lisse, deux
lingots de cuivre et un tailleur de pierre. Elle rejoint la catégorie Divers
du livre de fabrication natif. Même branche locale, sans publication ou
installation.
Build et packwiz, **52 tests serveur** et comparaison des archives réussis.
## beta.306 — moulin manuel (préparation locale)
[Le moulin fonctionne comme le tailleur de pierre](docs/mill-beta306.md) :
entrée et résultat immédiat, sans combustible. Prendre le résultat consomme
l'ingrédient ; Maj-clic traite les lots disponibles. Le feu disparaît du menu,
avec un bref mouvement mécanique à chaque utilisation. Nom **Moulin / Mill**,
neuf recettes conservées et anciens objets stockés récupérables.
Même branche locale, sans publication ou installation.
Build et packwiz, **52 tests serveur** et parcours **Vulkan avec Sodium**
réussis : prises natives, clic droit, lots, menus FR/EN et rechargement.
## beta.305 — écrasement direct des fruits (préparation locale)
[Les blocs de tomates, raisins et olives s'écrasent en sautant dessus](docs/crushing-beta305.md) :
huit paliers descendants, résidus progressifs et neuf portions au total.
Un piston libère immédiatement le reste ; le super piston traite jusqu'à
27 blocs sur sa course complète. La presse quitte fabrication, créatif et JEI.
Même branche locale, sans publication ou installation.
Build et packwiz, **78 tests serveur**, parcours d'écrasement et régression
complète du super piston sous **Vulkan avec Sodium** réussis.
## beta.304 — appareils culinaires (préparation locale)
[Six appareils et 80 recettes physiques portés en 26.3](docs/kitchen-beta304.md) :
poêle, chaudron vanilla qui devient une marmite, cuisinière 3 × 3, tonneau de
fermentation, moulin et pressoir à piston. Récipients conservés, lots limités,
contenus visibles et progression sauvegardée. La fermentation instantanée de
beta.303 cède la place au tonneau. Même branche locale, sans publication.
Les dix nouvelles entrées rejoignent Nourriture et boissons, sans nouvel onglet.
Build et packwiz, **72 tests serveur** et parcours **Vulkan avec Sodium**
réussis : appareils, JEI, menus FR/EN GUI 2/3/4, sauvegardes et régressions
cuisine/agriculture.
## beta.303 — recettes de cuisine (préparation locale)
[Les 92 recettes alimentaires de la 26.2 utilisent les nouvelles récoltes](docs/cuisine-beta303.md).
Salades, soupes, plats, pains, gâteaux, boissons et ingrédients intermédiaires.
Préparation à la table, cuisson au four/fumoir ; transformations de fermentation
adaptées en fabrication manuelle immédiate. 129 objets dans Nourriture et
boissons, sans nouvel onglet. Même branche locale, sans publication.
Les appareils culinaires restent dans leurs tickets.
Build, packwiz et **55 tests serveur** réussis. Parcours **Vulkan avec Sodium**
validé : fabrication, fumoir, 196 étapes dans JEI, 129 modèles, onglet natif
FR/EN GUI 2/3/4 et rechargement ; régression agricole réussie.
## beta.302 — vanille en 16 × 16 (préparation locale)
[Les quatre stades de vanille passent en 16 × 16 pixels](docs/vanilla-16px-beta302.md).
Dessin simplifié pour cette résolution, feuillage, fleurs et gousses sur un
seul plan contre le tronc. Même branche `codex/itsalive-26-3`, sans publication.
Dimensions vérifiées dans le JAR et dans l'atlas chargé par Minecraft.
Build et packwiz, 46 tests serveur et parcours Vulkan avec Sodium réussis.
## beta.301 — vanille grimpante (préparation locale)
[La vanille s'attache aux troncs comme les lianes et pousse vers le haut](docs/vanilla-beta301.md).
Faces fines contre l'écorce, nouvelle texture réunissant feuilles et gousses
sur un seul plan, appui requis pour chaque segment, maturité et poudre d'os.
Elle s'arrête aux obstacles et à la fin du tronc.
Bouture dans Naturel, gousses dans Nourriture et boissons, sans nouvel onglet.
Même branche locale `codex/itsalive-26-3`, sans publication ou installation.
Build et packwiz, **46 tests serveur** et parcours **Vulkan avec Sodium**
réussis, avec contrôle du plan unique et rechargement réel de 24 segments.
## beta.300 — cultures, arbres fruitiers et caféier (préparation locale)
[Les 20 cultures d'It's Alive ! deviennent jouables en 26.3](docs/agriculture-beta300.md),
avec le citronnier, l'olivier et le caféier. Les 45 objets rejoignent les
onglets vanilla Naturel et Nourriture et boissons, sans nouvelle page créative.
Semences, croissance, récoltes pérennes, plants hauts, riz immergé, fruits et
grains de café. Les textures fournies sont conservées, les libellés sont FR/EN.
Quinze cultures, dont l'oignon et l'ail, utilisent deux plans en croix pour
mieux montrer le pied ; les PNG restent intacts.
Même branche locale `codex/itsalive-26-3` ; apparition naturelle, vanille
grimpante et appareils culinaires restent dans leurs prochains tickets.
Aucune publication ni mise à jour d'installation.
Build, **34 tests serveur**, packwiz et parcours **Vulkan avec Sodium**
réussis : modèles, onglets vanilla FR/EN GUI 2/3/4 et rechargement réel.
## beta.299 — textures d'It's Alive ! (préparation locale)
[Premier lot de la refonte cuisine et agriculture pour 26.3](docs/itsalive-26-3.md) :
343 textures importées, dont les 38 PNG fournis prioritaires et conservés
sans retouche. Cultures, citronnier, olivier, caféier, ingrédients et appareils
disposent de leurs sources graphiques ; leur fonctionnement reste à porter.
Le suivi conserve les tickets chaudron, plats ratés, séchoir, poêle, vanille
grimpante et cuisinière. Branche locale `codex/itsalive-26-3`, sans publication.
Build, 13 tests serveur ciblés, contenu du JAR et assemblage packwiz vérifiés.
## beta.298 — normales de l'ancre corrigées
[Retrait du petit cadre résiduel dans la passe PBR](docs/media-anchor-normals-beta298.md).
Le modèle invisible de l'ancre ne laisse plus de reflet sur la vidéo
avec les shaders Sanctuary activés.
[Livraison vérifiée](docs/release-beta298.md) : serveur Beta, site et même
instance Prism à jour ; écrans existants et réglages conservés.
## beta.297 — correctifs des cadres disponibles
[Menu de taille à la pose, lecteur au Maj + clic droit et cadre en volume](docs/media-frame-fixes-beta297.md).
@@ -3870,8 +4099,9 @@ sauvegarde 26.2 : aucun outil de migration de ses chunks n'est livré ici.
| `.gitea/ISSUE_TEMPLATE/` | Modèles de tickets fonctionnalités et bugs |
| `.gitea/workflows/` | Vérification sur un runner Gitea compatible |
It's Alive !, Only Fun et Master Key restent des modules autonomes prévus par la
vision. Leurs sources 26.2 ne sont pas copiées dans ce socle. Fabric API est
La [refonte d'It's Alive !](docs/itsalive-26-3.md) reprend les textures et
l'agriculture dans Sanctuary ; ses appareils et sa cuisine restent à porter. Only Fun
et Master Key restent prévus par la vision. Fabric API est
requise ; Demeure appartient au code interne de Sanctuary. Depuis beta.280,
le port source de JEI pour 26.3 est embarqué dans le JAR Sanctuary pour le
client et le serveur. Sound Physics et Sodium restent facultatifs côté client.
+177
View File
@@ -1,5 +1,182 @@
# Provenance et crédits
## Saucisson posé en tranches, beta.315 locale
Le bloc de saucisson, ses quatre longueurs et orientations sont développés
dans Sanctuary 26.3. Les modèles cubiques originaux reprennent les références
natives du modèle d’objet beta.313 : terre cuite rouge, laine blanche et bloc
de champignon rouge pour la coupe. Aucun PNG ni fichier audio Minecraft
n’est copié ; le son natif de mastication est référencé. Les modèles d’objets
cru/séché et leur affichage au séchoir sont conservés. Voir
[comportement et continuité](docs/sausage-slices-beta315.md).
## Fromage à partager et élevage du vin, beta.314 locale
Le nouveau bloc de fromage réutilise les règles du gâteau de Minecraft 26.3
par héritage, avec des modèles cubiques originaux Sanctuary pour ses sept
états et des références aux textures natives de terre cuite jaune et pierre
de l’End. Les icônes historiques de fromage et de vin sont conservées ; les
trois qualités ajoutées héritent du modèle d’objet du vin et distinguent
leurs noms, raretés et saturation. Aucun PNG, son ou source Minecraft n’est
copié. Le calcul des jours réels et les recettes d’élevage sont développés
dans Sanctuary. Voir [comportement et continuité](docs/cheese-wine-beta314.md).
## Séchoir et saucisson, beta.313 locale
Le séchoir, ses modèles cubiques, les deux états du saucisson, la préparation
à l’établi et le séchage sont développés dans Sanctuary 26.3. Ils réutilisent
les ingrédients du catalogue culinaire historique et des textures Minecraft
référencées : chêne écorcé, terre cuite rose/rouge, laine blanche et bloc de
champignon rouge. Aucun fichier de texture ni son Minecraft n’est copié.
Les événements sonores natifs d’ajout/retrait de cadre et de récupération
d’objet sont référencés sans nouveau fichier audio. Voir
[comportement, provenance et continuité](docs/drying-rack-beta313.md).
## Répétition au chaudron, beta.312 locale
Sanctuary mémorise la recette autorisée au bâton, enchaîne ses lots et conserve
les ingrédients réapprovisionnés pour leurs cycles complets. Le chaudron, ses
19 recettes et ses ressources restent issus du port de `26.2/itsalive`
(commit `26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later).
Aucune recette, texture ou piste audio n’est modifiée ; les sons natifs de
beta.311 restent utilisés une fois par événement. Voir
[comportement et continuité](docs/cauldron-repeat-beta312.md).
## Chaudron remué, beta.311 locale
Le chaudron et ses 19 recettes proviennent du port de `26.2/itsalive`
(commit `26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later).
Sanctuary remplace le délai d’échec par une préparation lancée au bâton,
conserve les récipients réels jusqu’au paiement final et précise la capture
dans l’ouverture. Les sons référencent les événements natifs de Minecraft
26.3, sans copie de fichier audio ni modification de texture. Voir
[fonctionnement et continuité](docs/cauldron-beta311.md).
## Piles de poêles, beta.310 locale
La poêle, sa texture et ses 26 recettes restent issues du port de
`26.2/itsalive` (commit `26f2c2a0061619df1f146f9081d6740c560a7497`,
GPL-3.0-or-later). Sanctuary ajoute une pose native par couches de quatre
pixels, une préparation commune et des réserves réelles pour payer chaque
portion. Les trois modèles supérieurs translatent la géométrie historique
avec ses UV conservés ; aucun PNG ne change. Voir
[fonctionnement et contrat de continuité](docs/pan-stacks-beta310.md).
## Sortie unique de cuisinière, beta.309 locale
La correction Sanctuary retire la case d'aperçu et l'alimentation de la grille
par entonnoir, tout en conservant l'extraction des plats et le cycle culinaire
porté de `26.2/itsalive` (GPL-3.0-or-later, même reçu beta.304).
L'interface utilise les panneaux et sprites vanilla existants, sans retouche
de PNG. Voir [comportement et continuité](docs/oven-single-output-beta309.md).
## Cycle de cuisinière et aperçu, beta.308 locale
La cuisinière et ses treize recettes restent issues du port de `26.2/itsalive`
(commit `26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later).
Sanctuary renforce l'annulation sur modification de grille, sépare l'aperçu
virtuel des plats cuits et conserve l'inventaire natif et ses compteurs.
L'interface réutilise les panneaux et sprites Minecraft de l'établi et du four ;
aucun PNG n'est retouché. Voir [cycle et continuité](docs/oven-beta308.md).
## Moulin manuel, beta.306 locale
Le moulin et ses neuf recettes proviennent du port culinaire de
`26.2/itsalive`, commit `26f2c2a0061619df1f146f9081d6740c560a7497`,
GPL-3.0-or-later. Sanctuary remplace le traitement au combustible par un poste
manuel inspiré du tailleur de pierre vanilla 26.3. L'interface réutilise le
panneau natif Minecraft et les deux sprites mécaniques historiques importés ;
aucun PNG n'est modifié. Les anciennes cases de stockage restent lisibles.
Voir [comportement et continuité](docs/mill-beta306.md).
La [recette adaptée en beta.307](docs/mill-recipe-beta307.md) remplace la
redstone historique par un bâton et utilise la catégorie native Divers.
## Écrasement direct des fruits, beta.305 locale
Le comportement de piétinement à huit hauteurs et son intégration aux pistons
sont développés dans Sanctuary 26.3. Ils utilisent les trois textures de blocs
et les trois recettes de transformation déjà importées de `26.2/itsalive`
(commit `26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later).
Aucun PNG n'est retouché ; les modèles partiels réutilisent les mêmes textures.
La presse beta.304 devient une compatibilité de lecture d'inventaire inactive.
Voir [comportements, continuité et vérifications](docs/crushing-beta305.md).
## Appareils culinaires, beta.304 locale
Les six appareils, leurs 36 sources Java, leurs modèles et recettes adaptent
`26.2/itsalive` au commit `26f2c2a0061619df1f146f9081d6740c560a7497`,
GPL-3.0-or-later. Le [reçu source](docs/kitchen-beta304-sources.json) conserve
les empreintes historiques. Les recettes d'équipement adaptées sont dans
`scripts/data/kitchen-crafting.json` ; celles des procédés restent dans le
reçu de cuisine beta.303. Le port conserve les PNG déjà importés, adapte
explicitement les API Minecraft 26.3 et ajoute les catégories JEI, les retours
de récipients et le suivi exact de l'eau vanilla. La fermentation instantanée
de l'étape beta.303 est remplacée par le tonneau et ses délais historiques.
Voir [comportements, limites et vérifications](docs/kitchen-beta304.md).
## Cuisine, beta.303 locale
Les 92 recettes alimentaires, noms FR/EN et profils alimentaires adaptent
`26.2/itsalive` au commit `26f2c2a0061619df1f146f9081d6740c560a7497`,
GPL-3.0-or-later. Le reçu source `scripts/data/cuisine.json` conserve les
recettes originales et leurs SHA-256. Les mécanismes sont adaptés à la table,
au four et au fumoir de Minecraft 26.3 ; les transformations de fermentation
deviennent des fabrications manuelles immédiates. Les appareils historiques
ne sont pas portés dans ce lot. Les modèles de préparations réutilisent les
sprites importés, avec une teinte native, sans modifier les PNG. Voir
[le contrat, les adaptations et contrôles](docs/cuisine-beta303.md).
## Vanille 16 × 16, beta.302 locale
Les quatre sprites grimpants de beta.301 sont repris avec l'outil intégré
imagegen pour leur lecture en 16 × 16 pixels. L'atlas de production contient
quatre cellules de cette taille ; les PNG de jeu sont exportés à cette
résolution exacte, sans interpolation. Les sources, prompt et empreintes
restent dans `ressources-pack/sources/vanilla-beta301/`. Les 343 PNG de l'import
historique beta.299 restent intacts. Voir [le correctif](docs/vanilla-16px-beta302.md).
## Vanille grimpante, beta.301 locale
La maturité et la récolte adaptent le `VanillaVineBlock` de Sanctuary 26.2
(`26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later). L'attachement
à plusieurs faces, la croissance ascendante et les modèles plats sont adaptés
pour Sanctuary 26.3. Les quatre PNG historiques restent intacts. Quatre
nouveaux sprites intégrant feuilles, fleurs et gousses sont créés avec l'outil
imagegen, à partir de la liane Minecraft comme référence de structure et de
style et des anciens PNG comme références de vanille. Un seul plan est rendu
par face ; aucune texture native n'est copiée dans le pack. L'atlas final,
les prompts et la préparation des sprites sont conservés dans
`ressources-pack/sources/vanilla-beta301/`.
Voir [le contrat et les contrôles](docs/vanilla-beta301.md).
## Agriculture d'It's Alive ! pour 26.3, beta.300 locale
Les classes `fr.koka.sanctuary.agriculture` adaptent le code agricole et les
registres du module `26.2/itsalive`, commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later. Leur portée
exclut créatures, Canaplia et appareils culinaires. Les définitions des deux
arbres plantés adaptent les paramètres `oak` et `acacia` de Minecraft 26.3
finale ; les butins utilisent les règles natives de cette version.
Les textures du [reçu beta.299](docs/itsalive-textures-beta299.json) sont
conservées sans retouche. Voir le [contrat et les vérifications](docs/agriculture-beta300.md).
## Textures cuisine et agriculture d'It's Alive !, beta.299 locale
Le premier lot de la refonte reprend 343 PNG du module `itsalive` du projet
Sanctuary 26.2 (GPL-3.0-or-later), commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, sous
`itsalive/src/main/resources/assets/itsalive/textures/`. Les 38 images du dossier
`food` fournies par le créateur le 28 octobre 2026 remplacent leurs homologues
dans la nouvelle copie ; elles sont conservées octet pour octet. Les 305 autres
PNG restent strictement identiques aux sources historiques. Les textures de
créatures et les œufs ne font pas partie de cet import.
Le [reçu par fichier](docs/itsalive-textures-beta299.json) conserve origine,
empreintes anciennes/nouvelles et dimensions. Le
[ticket de portage](docs/itsalive-26-3.md) distingue la présence des textures
des mécaniques encore à développer. Aucun nouveau code 26.2 ni modèle tiers
n'est inclus par ce lot ; les notices des portages antérieurs restent valables.
## Mods facultatifs beta.292
Les binaires officiels de [Simple Voice Chat 2.6.24+26.3](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy)
+7
View File
@@ -75,6 +75,13 @@ tasks.register('verifyResourcePack', Exec) {
}
tasks.named('check') { dependsOn('verifyResourcePack') }
tasks.register('verifyItsAliveTextures', Exec) {
group = 'verification'
description = 'Verify the first agriculture and cooking texture import and its source hashes.'
commandLine('python3', 'scripts/verify_itsalive_textures.py')
}
tasks.named('check') { dependsOn('verifyItsAliveTextures') }
tasks.register('assembleResourcePack', Zip) {
group = 'distribution'
description = 'Export the same textures as the built-in resource pack for standalone use.'
+181
View File
@@ -0,0 +1,181 @@
# IA-02 — Cultures, arbres fruitiers et caféier en 26.3
Préparation **locale** du 28 octobre 2026, `beta.300`, sur la même branche
`codex/itsalive-26-3`. Socle : import graphique beta.299, commit `d78d39cd`.
Minecraft 26.3, Fabric Loader 0.19.5, Fabric API 0.160.5+26.3 et Java 25
restent inchangés. Aucun push, tag, publication, serveur, site ou Prism modifié.
## Contenu jouable
Le créatif utilise exclusivement les onglets vanilla existants :
| Onglet | Ajouts |
| --- | --- |
| **Naturel / Natural Blocks** | 23 semences, propagules et pousses, plus les deux feuillages fruitiers |
| **Nourriture et boissons / Food & Drinks** | 26 récoltes et ingrédients, plus la gousse d'ail |
Ces **45 objets distincts** comprennent sept récoltes replantables ou grains
présents dans les deux catégories selon leur usage. Aucun onglet ou page
créative supplémentaire n'est enregistré.
Tous portent des identifiants `sanctuary:*` et des noms FR/EN. Les blocs et
objets sont ceux de Sanctuary ; aucun deuxième JAR It's Alive n'est nécessaire.
Les **20 cultures** sont : fraise, tomate, ananas, maïs, concombre, poivron,
aubergine, laitue, oignon, ail, chou, raisin, riz, soja, haricot, pois chiche,
piment, basilic, menthe et coriandre. Elles ont huit âges, croissent par les
ticks aléatoires et acceptent la poudre d'os. La croissance garde les règles
vanilla de lumière et de sol ; aucun verrou climatique nouveau n'est ajouté.
Les modèles suivent la silhouette des plantes : **15 cultures en croix**
(fraise, ananas, poivron, aubergine, laitue, oignon, ail, chou, raisin, soja,
pois chiche, piment, basilic, menthe et coriandre). Le riz, le maïs, la tomate,
le concombre et le haricot gardent les plans de culture existants, avec les
parties hautes pour les quatre derniers. La croix comprend deux plans
diagonaux au lieu des quatre plans formant un cadre. Son pied est abaissé
d'un pixel, comme le modèle de culture vanilla, pour toucher la terre
labourée. Ce choix s'applique aux huit âges, sans retoucher les PNG.
- Les semences se plantent sur la terre labourée. L'oignon, l'ail, le riz,
le soja, les haricots et les pois chiches sont directement replantables.
- Le **riz** se plante exclusivement dans une vraie source d'eau au-dessus
de boue. L'eau courante et la plantation à sec sont refusées sans consommer
de riz. Le plant contient sa source ; le casser restitue l'eau.
- Fraise, tomate, concombre, poivron, aubergine, raisin, piment, basilic,
menthe et coriandre se récoltent au clic droit à maturité. Ils donnent
une à quatre unités selon la plante et reviennent à l'âge 3.
- Le **haricot** donne des haricots verts aux âges 5–6 et des haricots secs
à l'âge 7, puis revient à l'âge 3. Les autres cultures se récoltent par casse.
- Tomate, maïs, concombre et haricot occupent **deux blocs dès l'âge 4**.
La partie haute suit l'âge du pied ; son clic transmet la récolte ou la
poudre d'os au pied. La casse ne produit qu'un seul butin. Un plafond bloque
le passage au stade haut et la poudre d'os n'est pas consommée en vain.
- Manger un **ananas** rend exactement une couronne en survie, pour replanter.
Les fruits, légumes et herbes comestibles conservent leurs valeurs 26.2.
Les grains secs et ingrédients de transformation ne deviennent pas tous
des aliments crus par cet import.
Le **caféier** se plante avec un grain vert sur un sol admis par les buissons
vanilla. Il croît sur quatre âges, donne des cerises aux âges 2–3 et revient
à l'âge 1 après récolte. Il n'a ni épines, ni ralentissement du buisson à baies.
Les cerises donnent deux grains ; le fumoir torréfie les grains verts.
Les boissons et les appareils culinaires restent dans les tickets suivants.
Le **citronnier** et l'**olivier** poussent depuis leurs propres pousses :
forme de chêne pour le premier, forme d'acacia pour le second, avec bois
vanilla. Les feuilles fruitières sont créées directement par les arbres,
sans scanner ou convertir les feuilles d'un arbre voisin. Une feuille sur
douze environ porte un fruit à la croissance ; les fruits reviennent ensuite
rarement par tick aléatoire, comme dans l'intention historique. Le clic droit
récolte un fruit sans arracher les feuilles ; cisailles et Toucher de soie
récupèrent les feuilles, sinon elles peuvent donner une pousse.
Les recettes natives de récupération des semences et de torréfaction sont
disponibles pour la fabrication manuelle. Le compostage utilise le composant
`minecraft:compostable` et les probabilités natives 26.3, au lieu de l'ancienne
table statique Java. Les tags de cultures, maintien de terre labourée,
feuilles, pousses et nourriture des poules sont additifs.
## Accès pour les essais
Créer un **nouveau monde de développement** en créatif, prendre les semences
dans **Naturel** et les récoltes dans **Nourriture et boissons**. Sur un monde de test où les
commandes sont autorisées :
```mcfunction
/give @s sanctuary:tomato_seeds 16
/give @s sanctuary:rice 16
/give @s sanctuary:coffee_beans 16
/give @s sanctuary:lemon_sapling 4
/give @s sanctuary:olive_sapling 4
```
Les infobulles FR/EN expliquent la plantation du riz et du caféier.
Le bloc d'ananas sauvage est porté pour le futur peuplement, sans apparition
naturelle activée dans ce lot.
## Génération et sauvegardes
Les deux nouvelles définitions `sanctuary:lemon_tree` et
`sanctuary:olive_tree` servent aux **pousses plantées**, avec leurs fournisseurs
de feuillage `sanctuary:lemon_foliage` et `sanctuary:olive_foliage`. Leurs formes
reprennent les paramètres exacts des arbres `oak` et `acacia` de Minecraft
26.3 finale, adaptés au feuillage Sanctuary. Ce premier contrat de pousses
fruitières est **IA-02 / beta.300** ; graine de validation : **300**.
Aucune nouvelle feature n'est ajoutée aux biomes ou aux étapes de décoration.
L'apparition naturelle et la découverte des premières semences en survie
restent un lot séparé. Aucun ancien chunk n'est repeuplé ou régénéré. Le client
de validation utilise un monde plat Sanctuary Test neuf, jamais une sauvegarde
personnelle. Les 30 nouveaux blocs utilisent les états natifs de Minecraft :
âge, partie haute, distance au tronc, persistance, eau et fructification.
Aucun format de sauvegarde existant n'est réécrit ; aucun monde 26.2 n'est migré.
La **vanille grimpante** reste dans IA-03. Le chaudron, les plats ratés,
le séchoir/saucisson, la poêle et la cuisinière restent dans IA-04 à IA-08.
Leurs textures déjà importées ne signifient pas que leurs comportements
sont activés par ce lot.
## Sources et reproductibilité
Le port adapte les classes agricoles de `26.2/itsalive`, commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later, en lecture seule.
Les registres sont séparés des monstres, de Canaplia et des appareils culinaires.
Les changements d'API 26.3 sont vérifiés dans le JAR exact de Minecraft utilisé
par Loom : végétation, prédiction d'inventaire, compostage, butin, arbres et
fournisseurs d'états. Aucun mod agricole tiers n'est ajouté.
`scripts/data/agriculture.json` décrit le catalogue et les formes des arbres.
`scripts/agriculture_resources.py` produit modèles, objets, états de blocs,
butins, recettes, tags et définitions des arbres dans le répertoire de build,
par `generateAgricultureResources`. Les 343 PNG et leurs empreintes beta.299
restent inchangés ; les 38 fichiers du créateur gardent la priorité.
## Validation
Le parcours graphique utilise **Vulkan / Apple M1 / Sodium 0.9.2+mc26.3**,
un monde de développement neuf de graine **300** et un client de heap 4 Gio :
- plantation en survie par les vrais paquets client/serveur, consommation
d'une semence, poudre d'os et casse de la partie haute ;
- **313 états de blocs** avec géométrie et textures réellement chargées,
**45 modèles d'objets** avec leurs sprites fournis ;
- vérification des deux plans croisés de l'oignon et de l'ail et des quatre
plans conservés pour le maïs et le riz ;
- onglets vanilla Naturel et Nourriture et boissons : présence effective des
ajouts, absence de nouvel onglet, captures **FR/EN et GUI 2/3/4** ;
- croissance des deux arbres et fermeture/réouverture réelle du monde :
**173 états agricoles** conservés, dont les âges et la fructification.
La construction finale `check build assemblePack` réussit en **3 min 6 s**,
159 tâches, avec **34 GameTests serveur requis réussis** : 17 scénarios
agricoles et 17 régressions des recettes, de la progression et de la
connaissance alimentaire. Le parcours client final réussit en **53 s**.
Le JAR contient les modèles croisés attendus pour les huit âges des 15
cultures et les classes agricoles identiques aux classes vérifiées. Les
343 textures correspondent au reçu beta.299. Les JAR des profils packwiz
client et serveur sont strictement identiques au binaire contrôlé ; le
template personnel contient les mêmes PNG. Version et index des packs vérifiés.
SHA-256 de `sanctuary-beta.300.jar` :
`d432015abbb52154af8ec2a2b9e31a73374133e6909b954b0fc54a15397661d7`.
Reçu local complémentaire : `build/agriculture300-validation.json`.
Commandes de validation :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=agriculture300,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAgriculture300ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=agriculture300 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Les journaux restent ignorés dans `build/agriculture300-*.log` et les captures
dans `mods/sanctuary/build/run/clientGameTest/screenshots/`. Aucun essai
OpenGL n'est lancé ou revendiqué. Les autres moteurs graphiques, un serveur
multijoueur distant et une partie de survie depuis zéro sans commandes ne
sont pas validés par ce lot ; la découverte naturelle des semences est différée.
+55
View File
@@ -1,5 +1,60 @@
# Backlog Sanctuary
## IA-01 à IA-10 — Refonte cuisine et agriculture d'It's Alive !
**IA-10, préparation locale beta.315 :** [saucisson séché posable et mangeable
en quatre tranches](sausage-slices-beta315.md), huit points de nourriture au
total, quatre orientations et sauvegarde native. Même branche locale,
sans publication.
**IA-09, préparation locale beta.314 :** [fromage en sept parts posées et quatre
qualités de vin, après 3 minutes de fermentation puis 1/3/7 jours réels
au tonneau](cheese-wine-beta314.md). Le vieillissement continue jeu fermé.
Même branche locale, sans publication.
**IA-06, préparation locale beta.313 :** [saucisson préparé puis séché cinq
minutes sur quatre crochets indépendants, protection de la pluie et sauvegarde](drying-rack-beta313.md).
Même branche locale, sans publication.
**CUI-305, préparation locale beta.305 :** [écrasement direct des fruits](crushing-beta305.md).
La presse est retirée des recettes, du créatif et de JEI. Huit paliers à piétiner,
neuf portions par bloc, écrasement au piston et course complète de 27 blocs
pour le super piston. Les autres appareils restent en place.
**IA-04/05/07/08, préparation locale beta.304 :** [six appareils culinaires et
80 procédés physiques](kitchen-beta304.md). Chaudron depuis l'eau vanilla,
poêle native avec contenu visible, plats ratés, cuisinière 3 × 3, fermentation
temporisée, moulin et pressoir à piston. À cette étape, les six appareils étaient portés ;
la refonte de pose/orientation de la poêle, le séchoir et les nouveaux plats
restaient ouverts. Même branche locale, sans publication.
**IA-05, préparation locale beta.303 :** [92 recettes alimentaires historiques,
ingrédients et plats jouables](cuisine-beta303.md), avec table et four/fumoir.
La suite beta.304 remplace la fermentation instantanée et porte les appareils
et les plats ratés. Même branche locale, sans publication.
**Premier lot graphique importé en beta.299 locale**, branche
`codex/itsalive-26-3`, à la demande du 28 octobre 2026 :
343 textures, dont les 38 fichiers `food/` prioritaires, avec reçu de provenance
et vérification des empreintes. Aucun bloc, objet ou système agricole ajouté
au registre par ce lot. [Périmètre, catalogue et tickets](itsalive-26-3.md) :
cultures/arbres/caféier, vanille grimpante, chaudron, plats ratés et recettes,
saucisson au séchoir, poêle et cuisinière four + table de fabrication.
Suite par incréments locaux ; aucune publication ou mise à jour d'installation.
**IA-02, préparation locale beta.300 :** [20 cultures, deux arbres fruitiers
et caféier jouables](agriculture-beta300.md), 45 objets créatifs, modèles,
butins et recettes natifs 26.3. Apparition naturelle, vanille grimpante et
cuisine restent dans les prochains lots. Même branche `codex/itsalive-26-3`.
**IA-03, préparation locale beta.301 :** [vanille attachée aux bûches et
croissance vers le haut](vanilla-beta301.md), sans propagation descendante,
avec gousses et pose à plusieurs faces. Les appareils culinaires et le
peuplement naturel restent à traiter. Même branche, sans publication.
**Correction locale beta.302 :** [les quatre textures de vanille sont
reprises en 16 × 16 pixels](vanilla-16px-beta302.md), sur le même plan unique.
## HELLO-264 — Menu central et sol commun — livré localement
Sur `codex/hello-world-menu`, socle beta.263. Colonne centrale de 182 pixels,
+56
View File
@@ -0,0 +1,56 @@
{
"version": "beta.311",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.311.jar",
"sha256": "3fa69ebc452ad2bbc3e7d6992305e9349d2f34f04f666a3688453fbd2fe74409",
"previous_verified_sha256": "4a2278d987b031b343a6d66694da50db0aeb408bb77785ce8c02ae0ed9174b26",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/culinary/block/CookingCauldronBlock.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/CookingCauldronBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/CookingCauldronHud.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [],
"removed_jar_entries": [],
"generated_json_unchanged": {
"cuisine": 432,
"kitchen": 195,
"agriculture": 376
},
"culinary_compiled_classes_verified": 61,
"cauldron_recipes_unchanged": 19,
"manual_start": "stick",
"failed_dish_from_cauldron": false,
"milk_bucket_paid_at_completion": true,
"unchanged_native_inventory_indices": [
0,
1,
2,
3,
4,
5,
6
],
"preparation_storage_marker": 311,
"unchanged_pngs": 1108,
"server_required": 89,
"cauldron_scenarios": 13,
"client_vulkan": {
"log": "build/cauldron311-client.log",
"screenshots": "build/cauldron311-screenshots",
"screenshot_count": 11,
"world_seed": 311,
"sodium": "0.9.2+mc26.3",
"audio_engine_listener": true
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"build_log": "build/cauldron311-build.log"
}
+130
View File
@@ -0,0 +1,130 @@
# CUI-311 / IA-04 — chaudron préparé puis remué
Suite locale beta.312 : [la recette se répète après son premier geste et
reste mémorisée après la collecte des plats](cauldron-repeat-beta312.md).
Le comportement à lot unique décrit ci-dessous est celui de beta.311.
Choix du créateur le 29 octobre 2026 : préparer les ingrédients, puis **remuer
avec un bâton** pour lancer la cuisson. Les mélanges incomplets ne deviennent
plus des plats ratés. Le lait reste un ingrédient non payé jusqu'à la fin ;
le seau vide est éjecté à la préparation terminée. Des sons Minecraft discrets
accompagnent les gestes et la cuisson. Même branche `codex/itsalive-26-3`,
préparation locale **beta.311**, sans publication ou installation.
## Contrat de continuité avant modification
L'identifiant `sanctuary:cooking_cauldron`, les six cases d'ingrédients 0–5,
la sortie 6 et `WaterUnits` restent inchangés. Les objets natifs gardent leurs
composants et tailles maximales ; les récipients non empilables sont stockés
séparément. Une vue agrégée sert uniquement à reconnaître la recette et ses
quantités, sans déplacer les objets stockés.
Le champ `AssemblyComplete` devient l'autorisation donnée par le geste de
remuer. `CauldronPreparationVersion=311` distingue ce sens de l'ancien départ
automatique. À la lecture d'une sauvegarde antérieure sans ce marqueur, seuls
l'autorisation et l'avancement de cuisson non payé sont remis à zéro ; les
six cases, la sortie déjà terminée et l'eau sont conservées. La préparation
attend alors un nouveau geste. Les anciennes dates restent lisibles mais ne
créent plus de délai d'échec. Une sauvegarde beta.311 conserve une cuisson
explicitement lancée, qui reprend sur ses ticks restants avec de la chaleur.
Changer un ingrédient ou l'eau annule la cuisson non payée et demande de
remuer à nouveau. Une cuisson produit un lot de la recette choisie ; elle ne
relance pas automatiquement une recette depuis les restes. Le bâton n'est
pas consommé. Le chaudron ordinaire vide ou rempli d'eau devient la variante
culinaire seulement lorsqu'un véritable ingrédient tombe dans son ouverture.
Les annulations rendent le lait plein, la casse rend les objets encore
stockés et la collecte finale restaure le chaudron vanilla avec son eau exacte.
Revenir à beta.310 exige d'abord de récupérer les préparations non terminées :
l'ancien code reprend son départ automatique. Aucun monde personnel n'est
modifié ; les validations utilisent des fixtures et mondes neufs ignorés.
## Gestes et recettes
Jeter le premier ingrédient dans l'ouverture d'un chaudron vanilla vide ou
rempli d'eau. Ajouter les autres ingrédients par clic, jet dans l'ouverture
ou entonnoir. Un objet posé à côté ou sur l'extérieur du rebord n'est pas
capturé. La recette reste modifiable sans compte à rebours d'échec.
Un clic avec un bâton démarre une recette complète, avec une source de
chaleur dessous. Le bâton reste entier. Un mélange incomplet, un chaudron
froid ou une préparation déjà en cours donnent une indication FR/EN, sans
consommer d'ingrédient. Une modification annule l'avancement, sans paiement,
et demande un nouveau geste. La perte de chaleur suspend simplement les ticks
restants d'une recette déjà lancée.
Les **19 recettes historiques** et leurs durées restent inchangées. Les lots
regroupent les ingrédients identiques à la reconnaissance, tout en conservant
les vrais seaux non empilables dans les six cases natives. Trois seaux de lait
produisent six crèmes et rendent trois seaux, après la durée entière. Une
préparation ne poursuit pas une autre recette depuis ses restes. La sortie
et les ingrédients peuvent se récupérer, et l'eau restante retrouve son
niveau vanilla exact lorsque la variante culinaire se vide.
Les sons sont envoyés côté serveur, une fois par événement : ajout (volume
0,18 ; 0,35 pour le lait, limité à un son tous les quatre ticks), geste (0,30),
mijotage (0,12 tous les 60 ticks réellement cuits), fin (0,35), service (0,25).
Événements Minecraft : `entity.generic.splash`, `item.bucket.empty`,
`item.bottle.empty`, `block.bubble_column.bubble_pop`,
`block.brewing_stand.brew`, `entity.item.pickup`. Aucun fichier audio n'est
importé. Un chaudron froid ou non lancé ne joue pas le son de mijotage.
## Vérifications
Treize cas serveur natifs couvrent le jet de lait dans un chaudron vide,
le vrai geste au bâton, l'absence d'échec et de paiement prématuré, les
**19 recettes complètes**, les lots de seaux séparés, l'absence de recette
suivante automatique, les changements de préparation/eau, le manque de chaleur,
la pause, la sauvegarde et l'ancien avancement désarmé, les objets hors du
bol, les récipients jamais réabsorbés, la casse et l'entonnoir natif.
Les vérifications culinaires historiques ont été adaptées au geste manuel ;
les fixtures successives éliminent leurs anciens objets avant de préparer
la recette suivante, pour ne pas introduire d'ingrédients parasites.
Le parcours client réussi sous **Vulkan / Apple M1**, avec
**Sodium 0.9.2+mc26.3**, dure **1 min 15 s** sur un monde plat neuf de graine
**311**. Il couvre une vraie touche Q de jet de lait, la capture par les ticks
natifs, **650 ticks d'attente sans recette ni échec**, les HUD FR/EN aux
échelles 2, 3 et 4, les vrais paquets de geste et de service, la cuisson réelle,
le seau éjecté après le paiement, la fermeture/réouverture du monde et un
mélange invalide qui conserve le lait. Les 19 recettes sont visibles dans JEI.
Le jet Q est replacé au-dessus de l'ouverture par la fixture ; la chute,
la capture et la suite n'utilisent aucun raccourci de cuisson.
Un écouteur du moteur audio client a observé `item.bucket.empty`,
`item.bottle.empty`, deux `block.bubble_column.bubble_pop`,
`block.brewing_stand.brew` et `entity.item.pickup`. Ce contrôle vérifie l'envoi
et la réception des événements ; les volumes restent réglables dans la
catégorie native Blocs. Onze captures sont archivées dans
`build/cauldron311-screenshots/`, avec les journaux ignorés
`build/cauldron311-client.log` et `build/cauldron311-build.log`.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryCauldron311ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=cauldron311 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
`check build assemblePack` a réussi en **2 min 52 s**, avec **89 tests serveur
obligatoires**. L’audit strict à beta.310 ne trouve aucun fichier ajouté ou
retiré du JAR : seuls les trois fichiers de classes du chaudron, les cinq
libellés FR/EN, la version et les crédits changent. Les 19 recettes, les
432 JSON de cuisine, 195 d’appareils et 376 d’agriculture ainsi que les
**1 108 PNG** restent identiques. Les classes compilées sont comparées au
JAR et les copies client/serveur sont identiques. Le modèle de pack de
ressources ne change que ses deux fichiers de langue et les crédits.
[Reçu de vérification](cauldron-beta311-verification.json). JAR local :
`mods/sanctuary/build/libs/sanctuary-beta.311.jar`
SHA-256 :
`3fa69ebc452ad2bbc3e7d6992305e9349d2f34f04f666a3688453fbd2fe74409`
Aucun push, tag, publication, avancement du canal packwiz, installation
Prism ou déploiement sur un serveur personnel n’a été effectué.
@@ -0,0 +1,67 @@
{
"version": "beta.312",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.312.jar",
"sha256": "90349a998970030ec0bfd880b307b9162e76fd0f460d70f1c9ac8ad2c115c8bd",
"previous_verified_sha256": "3fa69ebc452ad2bbc3e7d6992305e9349d2f34f04f666a3688453fbd2fe74409",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/culinary/block/CookingCauldronBlock.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/AbstractTimedCulinaryBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/CookingCauldronBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/CookingCauldronHud.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [],
"removed_jar_entries": [],
"generated_json_unchanged": {
"cuisine": 432,
"kitchen": 195,
"agriculture": 376
},
"culinary_compiled_classes_verified": 61,
"cauldron_recipes_unchanged": 19,
"manual_start": "stick",
"failed_dish_from_cauldron": false,
"milk_bucket_paid_at_completion": true,
"unchanged_native_inventory_indices": [
0,
1,
2,
3,
4,
5,
6
],
"preparation_storage_marker": 311,
"optional_storage_fields": [
"CauldronRepeatRecipe",
"CauldronCycleBatches"
],
"unchanged_pngs": 1108,
"server_required": 100,
"cauldron_scenarios": 11,
"repeat_recipe": true,
"cycle_quantity_fixed": true,
"completed_client_batches": 4,
"client_paid_milk_buckets": 5,
"client_returned_buckets": 5,
"client_total_cream": 10,
"client_vulkan": {
"log": "build/cauldron312-client.log",
"screenshots": "build/cauldron312-screenshots",
"screenshot_count": 10,
"world_seed": 312,
"sodium": "0.9.2+mc26.3",
"audio_engine_listener": true
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"build_log": "build/cauldron312-build.log"
}
+129
View File
@@ -0,0 +1,129 @@
# CUI-312 / IA-04 — répéter une recette dans le même chaudron
Demande du créateur du 29 octobre 2026 : refaire la même recette plusieurs
fois sans recommencer toute la préparation. Après le premier geste au bâton,
le chaudron garde la recette choisie et peut reprendre ses lots avec les mêmes
ingrédients, y compris après avoir servi les plats. Préparation locale
**beta.312**, même branche `codex/itsalive-26-3`, sans publication ni installation.
## Contrat de continuité avant modification
Les identifiants, les six entrées 0–5, la sortie 6, l'eau, les composants
natifs et les quantités maximales restent inchangés. La cuisson garde ses
lots historiques et ne paie les ingrédients et récipients qu'en fin de lot.
Le champ optionnel `CauldronCycleBatches` conserve la quantité fixée au début
du lot non payé. Les ajouts en cours de cuisson restent pour le lot suivant ;
ils ne terminent pas leur cuisson sur le dernier tick d’un lot déjà lancé.
Une ancienne cuisson sans ce champ garde sa quantité disponible à la reprise.
Le champ optionnel `CauldronRepeatRecipe` enregistre l'identifiant exact de la
recette autorisée au bâton. `AssemblyComplete` garde l'autorisation manuelle,
y compris en attente d'ingrédients ou de place. Le marqueur de préparation
manuelle **311** reste valide ; il ne désigne pas la version du mod.
Une ancienne préparation beta.311 lancée sans ce nouveau champ retrouve sa
recette au premier tick chargé. Une préparation ancienne non lancée ne
s'autorise pas toute seule. Les sauvegardes antérieures au démarrage manuel
restent désarmées, avec ingrédients, eau et sortie conservés. Une recette
mémorisée disparue du datapack arrête l'autorisation sans perte d'objets.
Une sauvegarde beta.312 garde la recette exacte, les ticks non payés et les
objets ; le rechargement ne termine pas de cuisson hors ligne.
Ajouter un ingrédient de la recette mémorisée ou remettre son eau alimente
la même série sans annuler le travail en cours. Retirer un ingrédient,
diminuer l'eau ou ajouter un ingrédient d'une autre recette annule le travail
non payé et demande un nouveau geste. Servir la sortie laisse la série en
place ; le chaudron attend la même recette et ne transforme pas ses restes
en un autre produit. Une sortie pleine ou la perte de chaleur suspendent la
série ; récupérer les plats ou remettre de la chaleur permet de reprendre.
Maj-clic à main vide arrête explicitement la série en conservant les objets.
Un chaudron vide retrouve alors sa variante vanilla et son eau exacte.
Le retour à beta.311 conserve les sept cases et les plats, mais perd la
répétition : récupérer les préparations en cours avant un tel retour.
Aucun monde personnel n'est ouvert ou modifié par cette préparation locale.
## Utilisation
1. Préparer les ingrédients et remuer une première fois avec un bâton.
2. Ajouter les mêmes ingrédients quand nécessaire, par clic, jet dans
l'ouverture ou entonnoir. Les quantités ajoutées pendant un lot attendent
le lot suivant, avec toute sa durée de cuisson.
3. Prendre les plats à main vide laisse la série en place, même si un lot
suivant est déjà en train de cuire. Une sortie pleine conserve ses objets
et attend leur collecte ; un manque d'ingrédients attend le réassort.
4. Maj-clic à main vide arrête la série. Les objets restent récupérables et
le chaudron vide retrouve sa variante vanilla. Un nouveau plat différent
demande de préparer ses ingrédients et de remuer à nouveau.
Les quantités historiques par lot sont conservées : plusieurs recettes
complètes déjà présentes au début d'un lot peuvent cuire ensemble. Un apport
pendant ce lot n'augmente pas rétroactivement sa quantité. Un lot paie toujours
ses vrais ingrédients en fin de cuisson, avec ses récipients. Les sons de fin
restent limités à un par lot terminé, même lorsqu'une ancienne sortie est
encore stockée. Les 19 recettes et leurs durées restent inchangées.
Le HUD FR/EN montre les plats stockés avec l'état de la série : cuisson,
ingrédients manquants, chaleur manquante ou sortie pleine.
## Vérifications locales
Onze nouveaux cas serveur vérifient : lots qui s'accumulent, service natif en
cours de cuisson, réapprovisionnement compatible sans annulation, ingrédients
nouveaux conservés pour leur cycle entier, changement de préparation,
sortie pleine puis reprise, recette exacte conservée malgré l'eau restante,
réassort d'eau et pause sans chaleur, entonnoir natif, sauvegarde d'un lot avec
sa quantité fixée et un autre lot en attente, reprise beta.311 autorisée,
recette supprimée sans remplacement implicite et arrêt explicite.
Le parcours client **Vulkan / Apple M1**, avec **Sodium 0.9.2+mc26.3**, a
réussi en **1 min 58 s**, monde plat neuf de graine **312** :
- une vraie touche Q pour jeter le premier lait, puis un seul coup de bâton ;
- des ajouts natifs pendant le premier lot, avec leur propre lot suivant ;
- quatre lots réels donnant **dix crèmes pour cinq seaux de lait**, avec
**cinq seaux vides** rendus et un seul son de fin par lot ;
- ajout d'un lait alors que la sortie contient encore les plats précédents ;
- service natif de six crèmes pendant la cuisson suivante, sans annulation ;
- sauvegarde/réouverture conservant recette, lot non payé et plats servis,
puis reprise avec la chaleur remise, sans nouveau geste ;
- HUD FR/EN aux échelles 2, 3 et 4, avec contrôle de largeur et captures ;
- vrai Maj-clic à main vide pour arrêter, puis collecte des quatre crèmes
restantes et retour au chaudron vanilla.
La fixture replace le jet Q au-dessus de l'ouverture et retire la chaleur
avant la fermeture du monde pour observer le lot non payé au rechargement.
Les ingrédients, chutes, paiements, ticks de cuisson, interactions et quatre
sons de fin sont réels. Dix captures sont archivées dans
`build/cauldron312-screenshots/` ; les journaux de contrôle restent ignorés.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=cauldron312,cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryCauldron312ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=cauldron312 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
`check build assemblePack` a réussi en **2 min 51 s**, avec **100 tests serveur
obligatoires**. L’audit strict à beta.311 ne trouve aucun fichier ajouté ou
retiré du JAR : seuls les quatre fichiers de classes prévus (chaudron, HUD et
point de sélection de recette partagé), les quatre libellés FR/EN, la version
et les crédits changent. Les 19 recettes, les 432 JSON de cuisine, 195
d’appareils et 376 d’agriculture, ainsi que les **1 108 PNG**, sont identiques.
Les classes culinaires compilées sont comparées au JAR ; les copies
client/serveur sont identiques. Le modèle de pack de ressources ne change
que ses deux fichiers de langue et les crédits.
[Reçu de vérification](cauldron-repeat-beta312-verification.json). JAR local :
`mods/sanctuary/build/libs/sanctuary-beta.312.jar`
SHA-256 :
`90349a998970030ec0bfd880b307b9162e76fd0f460d70f1c9ac8ad2c115c8bd`
Aucun push, tag, publication, canal packwiz public, installation Prism ou
serveur personnel n’a été modifié.
+113
View File
@@ -0,0 +1,113 @@
{
"version": "beta.314",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.314.jar",
"sha256": "82a2a6d8d4e2ad9d0aab754037995609722bbebac28db89a6e641c851ccb3ceb",
"previous_verified_sha256": "54ea931d2f54eec8dac2d03e5ad1df8fd5b389db235a536bdd98d90a531fbe55",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"data/sanctuary/recipe/ferment_vinegar.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/Cuisine.class",
"fr/koka/sanctuary/cuisine/KitchenAppliances.class",
"fr/koka/sanctuary/cuisine/KitchenClient$1.class",
"fr/koka/sanctuary/cuisine/KitchenClient.class",
"fr/koka/sanctuary/cuisine/KitchenJei$Category.class",
"fr/koka/sanctuary/cuisine/KitchenJei.class",
"fr/koka/sanctuary/cuisine/culinary/block/FermentationBarrelBlock.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/FermentationBarrelBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/FermentationBarrelHud.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [
"assets/sanctuary/blockstates/cheese.json",
"assets/sanctuary/items/grand_cru_wine.json",
"assets/sanctuary/items/reserve_wine.json",
"assets/sanctuary/items/table_wine.json",
"assets/sanctuary/models/block/cheese.json",
"assets/sanctuary/models/block/cheese_slice_1.json",
"assets/sanctuary/models/block/cheese_slice_2.json",
"assets/sanctuary/models/block/cheese_slice_3.json",
"assets/sanctuary/models/block/cheese_slice_4.json",
"assets/sanctuary/models/block/cheese_slice_5.json",
"assets/sanctuary/models/block/cheese_slice_6.json",
"assets/sanctuary/models/item/grand_cru_wine.json",
"assets/sanctuary/models/item/reserve_wine.json",
"assets/sanctuary/models/item/table_wine.json",
"data/sanctuary/loot_table/blocks/cheese.json",
"data/sanctuary/recipe/age_grand_cru_wine.json",
"data/sanctuary/recipe/age_reserve_wine.json",
"data/sanctuary/recipe/age_table_wine.json",
"data/sanctuary/sanctuary_recipe_collections/wine_maturation.json",
"data/sanctuary/tags/item/wines.json",
"fr/koka/sanctuary/cuisine/culinary/block/CheeseBlock.class",
"fr/koka/sanctuary/cuisine/recipe/WineMaturationRecipe$1.class",
"fr/koka/sanctuary/cuisine/recipe/WineMaturationRecipe.class"
],
"removed_jar_entries": [],
"generated_json": {
"cuisine": 432,
"kitchen": 228,
"agriculture": 376
},
"additive_cheese_wine_json": 20,
"culinary_source_classes": 52,
"culinary_compiled_classes_verified": 72,
"cheese_slices": 7,
"cheese_top_rind_yellow": true,
"cheese_eaten_cut_holes_only": true,
"young_wine_game_ticks": 3600,
"wine_age_clock": "server UTC epoch milliseconds, including server shutdown",
"wine_quality_threshold_real_days": [
1,
3,
7
],
"wine_quality_ids": [
"sanctuary:wine",
"sanctuary:table_wine",
"sanctuary:reserve_wine",
"sanctuary:grand_cru_wine"
],
"old_pngs_unchanged": 1108,
"new_pngs": 0,
"server_required": 126,
"cheese_wine_scenarios": 14,
"client_vulkan": {
"log": "build/cheese-wine314-client.log",
"screenshots": "build/cheese-wine314-screenshots",
"screenshot_count": 15,
"world_seed": 314,
"sodium": "0.9.2+mc26.3",
"native_paid_fermentation": true,
"native_paid_cheese_placement": true,
"native_seven_slices": true,
"actual_disk_partial_cheese_and_wine_reopen": true,
"native_grand_cru_collection_and_drinking": true,
"fixture_simulates_fermentation_and_real_age": true
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"template_added_entries": [
"assets/sanctuary/blockstates/cheese.json",
"assets/sanctuary/items/grand_cru_wine.json",
"assets/sanctuary/items/reserve_wine.json",
"assets/sanctuary/items/table_wine.json",
"assets/sanctuary/models/block/cheese.json",
"assets/sanctuary/models/block/cheese_slice_1.json",
"assets/sanctuary/models/block/cheese_slice_2.json",
"assets/sanctuary/models/block/cheese_slice_3.json",
"assets/sanctuary/models/block/cheese_slice_4.json",
"assets/sanctuary/models/block/cheese_slice_5.json",
"assets/sanctuary/models/block/cheese_slice_6.json",
"assets/sanctuary/models/item/grand_cru_wine.json",
"assets/sanctuary/models/item/reserve_wine.json",
"assets/sanctuary/models/item/table_wine.json"
],
"build_log": "build/cheese-wine314-build.log"
}
+131
View File
@@ -0,0 +1,131 @@
# IA-09 — fromage à partager et quatre qualités de vin — beta.314 locale
Demande du créateur : poser le fromage comme un gâteau, en manger les parts,
et distinguer quatre qualités de vin. Choix confirmé le 29 octobre 2026 :
vin jeune après les trois minutes de fermentation existantes, puis vin de table
après un jour réel, réserve après trois jours et grand cru après sept jours.
Un jour réel vaut 24 heures ; le serveur arrêté compte dans ce vieillissement.
Même branche `codex/itsalive-26-3`, sans publication ni installation.
## Contrat de continuité avant modification
L'objet `sanctuary:cheese`, ses recettes et son icône conservent leur identifiant.
Il devient un BlockItem du nouveau bloc `sanctuary:cheese`, avec la propriété
native de gâteau `bites=0..6`. Les objets des inventaires existants deviennent
posables sans conversion. Le fromage posé n'a pas de block entity. Les mondes
et la génération ne sont pas modifiés. Comme le gâteau, casser une meule ne
rend pas une meule neuve et perdre son support la détruit. Aucune part mangée
ne peut être récupérée par le minage. Les gâteaux et bougies vanilla restent
inchangés ; une bougie ne transforme pas le fromage en gâteau.
Le tonneau garde son identifiant, ses cinq cases, ses entrées, sa sortie et
les champs historiques de fermentation. Vin jeune, bière, fromage, vinaigre
et les autres procédés gardent leurs durées de fermentation en temps de jeu.
Les nouveaux objets sont `sanctuary:table_wine`, `sanctuary:reserve_wine` et
`sanctuary:grand_cru_wine` ; `sanctuary:wine` reste le vin jeune.
Seule une bouteille terminée qui reste dans la sortie du tonneau vieillit.
L'horodatage UTC côté serveur commence à sa transformation effective en vin
jeune. De nouveaux champs optionnels de version 1 mémorisent cet instant ;
un ancien vin déjà en sortie commence son vieillissement lors du premier
tick sous beta.314, sans âge inventé. Une fermentation ancienne en cours
continue avec ses champs historiques, puis commence son vieillissement.
Retirer le produit libère la sortie et efface uniquement son horodatage.
Le vin dans un inventaire garde la qualité obtenue, sans vieillissement caché.
Le vieillissement se calcule avec l'heure réelle du serveur, même après une
fermeture ou un déchargement. Dormir ou modifier l'heure Minecraft ne l'accélère
pas. Un recul de l'horloge ne dégrade jamais un vin déjà amélioré. La qualité
est matérialisée par l'objet lui-même, ce qui empêche les lots de qualités
différentes de fusionner. La collecte et la casse rendent seulement ce produit.
Un entonnoir actif récupère le jeune vin dès qu'il est prêt ; pour le laisser
vieillir, verrouiller cet entonnoir avec de la redstone ou le retirer.
Avant un éventuel retour à beta.313, consommer les nouvelles bouteilles ou les
transformer en vinaigre et manger les meules posées ; les identifiants ajoutés
n'existent pas dans l'ancienne version.
Aucun monde personnel n'est ouvert ou migré pour les essais.
## Aspect du fromage
Ajustement demandé pendant cette préparation : le dessus et la croûte extérieure
restent jaunes. Dès la première part, seule la face de coupe expose la texture
claire à trous, auparavant visible sur le dessus. La forme continue de diminuer
sur les sept états natifs du gâteau. Les icônes historiques de fromage et de vin
restent intactes ; les trois vins ajoutés reprennent le modèle de la bouteille
avec des noms, raretés et saturation croissants. Les quatre vins restent
compatibles avec la recette de vinaigre.
## Vérifications locales
Quatorze cas serveur natifs couvrent :
- paiement de la pose depuis l'objet historique, sept vraies prises de parts,
forme qui diminue, faim pleine, absence de conversion par bougie, casse et
perte de support sans restitution d'une meule neuve ;
- recette de moût toujours prête à 3 600 ticks, origine UTC créée seulement
lors de la production effective du vin jeune ;
- frontière à une milliseconde près pour 1, 3 et 7 jours, une bouteille unique,
qualité maximale stable, rechargement NBT, rattrapage d'un arrêt et recul de
l'horloge sans déclassement ;
- ancien vin sans date initialisé sans âge inventé, temps Minecraft indépendant,
retrait puis nouveau lot sans âge hérité, casse avec la bouteille réellement
obtenue, quatre qualités buvables et utilisables pour le vinaigre ;
- trois recettes en secondes réelles, découverte depuis le moût et extraction
par un vrai entonnoir qui libère correctement l'horodatage.
Le parcours graphique **Vulkan / Apple M1 / Sodium 0.9.2+mc26.3** utilise un
monde plat neuf de graine **314**. Il vérifie les vrais paquets de dépôt de
crème et sel, l'obtention du fromage, sa pose payée puis les sept prises de
parts nourrissantes. Une fermeture/réouverture du monde conserve la meule avec
trois parts déjà mangées et le vin de table avec son horodatage exact.
Les quatre prises restantes épuisent cette même meule après rechargement.
Les sept modèles vérifient leurs six faces : dessus et croûte jaunes,
face ouest de coupe trouée seulement après la première part. Une capture est
prise depuis cette face. Les cinq objets fromage/vins sont dans Nourriture et
boissons, avec des modèles valides et les libellés FR/EN aux échelles GUI 2/3/4.
Le HUD affiche les jours réels. Le grand cru issu du même moût se récupère et
se boit par les interactions natives, et trois étapes de garde sont synchronisées
dans leur catégorie JEI. Quinze captures sont conservées dans
`build/cheese-wine314-screenshots/` (ignoré).
Les tests **n'attendent pas réellement sept jours** et ne changent pas l'horloge
du poste : les cas serveur fournissent explicitement des instants de référence,
et la fixture graphique antidate seulement le champ UTC des bouteilles dans
son monde jetable. La fermentation initiale est rapprochée de son échéance
historique, puis se termine par les ticks natifs. Le retrait, la pose,
la sauvegarde/relecture, les sept parts et la consommation du vin restent réels.
Le parcours graphique final a réussi en **1 min 28 s**.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=cheesewine314,drying313,cauldron312,cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryCheeseWine314ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=cheesewine314 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
`check build assemblePack` a réussi en **3 min 24 s**, avec **126 tests serveur
obligatoires**. L'audit strict à beta.313 autorise seulement les deux nouvelles
classes de production, les 20 JSON ajoutés, la compatibilité vinaigre et les
modifications de catalogue d'objets, registre, interaction du tonneau,
horodatage, HUD, JEI, langues et crédits prévues pour ce ticket. Les
**1 108 PNG** historiques sont identiques et aucun PNG n'est ajouté.
Les 432 JSON de cuisine et 376 JSON d'agriculture restent identiques ; les
228 JSON d'appareils sont comparés aux générateurs. Les modèles de coupe
sont vérifiés sur les sept états. Les 52 classes sources culinaires et leurs
72 classes compilées sont contrôlées dans le JAR. Les copies de ce JAR dans
les assemblages client et serveur sont identiques. Le modèle du pack de
ressources contient les quatorze nouveaux JSON visuels.
[Reçu d'audit](cheese-wine-beta314-verification.json).
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.314.jar`.
SHA-256 :
`82a2a6d8d4e2ad9d0aab754037995609722bbebac28db89a6e641c851ccb3ceb`
Aucun push, tag, publication, canal packwiz public, installation Prism ou
serveur personnel n'a été modifié.
+19
View File
@@ -0,0 +1,19 @@
{
"version": "beta.305",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.305.jar",
"sha256": "8c9b358c5c3caac3b9aa9a94230c7f6c6761d32f26002522bd8eba59c7f1cc2b",
"generated_json": {
"cuisine": 432,
"kitchen": 192,
"agriculture": 376
},
"culinary_classes": 44,
"server_required": 78,
"crushing_scenarios": 8,
"physical_recipes": 80,
"device_blueprints": 90,
"client_vulkan": {
"crushing": "build/crushing305-client.log",
"super_piston_regression": "build/crushing305-piston-client.log"
}
}
+149
View File
@@ -0,0 +1,149 @@
# CUI-305 — écraser les fruits sans presse
Demande du créateur du 28 octobre 2026, précisée pour le super piston et
confirmée : **huit paliers et neuf portions**. Préparation **beta.305** sur
`codex/itsalive-26-3`, après beta.304. Travail local, sans publication,
installation dans Prism ou déploiement serveur.
## Comportement retenu
La presse alimentaire quitte les recettes, les onglets créatifs et JEI.
Les blocs de tomates, raisins et olives s'écrasent directement dans le monde.
Leurs identifiants restent `sanctuary:tomato_block`, `grape_block` et `olive_block`.
Les textures fournies restent intactes.
Chaque bloc possède **huit hauteurs**, de 16 à 2 pixels. Un véritable saut
suivi d'un atterrissage retire un palier et fait apparaître les résidus en
objets dans le monde : pulpe de tomate, moût de raisin ou huile d'olive.
Les sept premiers sauts libèrent une portion chacun ; le huitième libère les
deux dernières et supprime le bloc. Le joueur descend sur la nouvelle surface.
Marcher, rester immobile et faire tomber un objet ne déclenchent rien.
La petite chute de deux pixels provoquée par l'écrasement n'est pas un
nouveau saut. Seuls les joueurs piétinent ces blocs.
Un **piston normal ou collant**, dans les six directions, écrase immédiatement
le bloc rencontré lors de sa sortie. Il libère uniquement le rendement restant.
Aucune presse, aucun réservoir ou récipient n'est requis : les portions sont
libres et peuvent être ramassées ou collectées par les moyens vanilla.
Aucune bouteille ou aucun bol n'est créé comme retour d'une portion libre.
Les portions déjà conditionnées de beta.304 gardent leurs composants de retour.
Le **super piston** conserve sa face 3 × 3, sa course de trois blocs et sa
capacité habituelle de déplacement. Une activation complète peut écraser les
**27 blocs** situés sur ses trois profondeurs, soit **243 portions** pour un
lot intact. Les neuf blocs rencontrés à chaque étape sont traités par le plan
commun du super piston ; un obstacle qui empêche une étape ne consomme aucun
fruit de cette étape. Les matériaux ordinaires conservent leurs règles.
Les blocs de fruits gardent leur gravité historique. Leur palier est porté par
l'état de bloc, y compris pendant une chute et après sauvegarde.
## Rendement, casse et recettes
Un bloc intact coûte neuf fruits et peut encore se décompacter en neuf fruits.
Après un ou plusieurs sauts, le casser rend **uniquement les fruits restants**,
sans redonner un bloc entier. Cela empêche de récupérer neuf fruits en plus
des résidus déjà obtenus. Par exemple :
| Situation | Déjà obtenu | Ce qui reste dans le bloc |
| --- | ---: | ---: |
| Bloc intact, 8 paliers | 0 | 9 |
| Après trois sauts, 5 paliers | 3 | 6 |
| Après six sauts, 2 paliers | 6 | 3 |
| Dernier palier | 7 | 2 |
Un piston appliqué à un bloc de deux paliers donne donc trois portions,
y compris après fermeture et réouverture du monde. La casse ordinaire utilise
les tables de butin natives 26.3 avec leurs conditions d'état et quantités ;
la destruction par piston remplace seulement ces butins par le produit écrasé.
Les trois identifiants de recettes `sanctuary:press_*` et le sérialiseur
`sanctuary:food_pressing` sont conservés pour les données et la progression.
Dans JEI, leur catégorie devient **Écrasement des fruits / Fruit Crushing**,
avec les deux gestes expliqués en FR/EN. Le total des recettes physiques reste
80 ; la recette de la presse disparaît, laissant dix fabrications d'équipement
et de stockage. Les recettes des autres appareils restent en place.
## Contrat de continuité locale
Aucun monde existant n'est modifié ou converti par ce lot. Un ancien état de
fruit sans propriété `layers` se charge comme un bloc plein de huit paliers.
Les nouveaux paliers utilisent le format natif des états de blocs ; les
identifiants et les recettes de compactage restent stables.
L'identifiant de presse introduit dans la préparation locale beta.304 reste
**inactif**, uniquement pour lire ses anciens inventaires : aucun traitement,
aucune recette, aucun catalogue créatif ou JEI. Un inventaire d'essai peut être
récupéré à main vide ou à la casse. Ses quatre anciennes cases utilisent le
même encodage natif. Il n'y a pas de conversion automatique en appareil ou en
fruits, ni de suppression silencieuse d'une sauvegarde. Aucun nouveau monde,
serveur ou chargement d'instance personnelle n'est engagé pour cette continuité.
## Implémentation et validation
`CrushableProduceBlock` gère la hauteur, les atterrissages et le rendement.
Le dernier palier de deux pixels nécessite de prendre le support réel du
joueur : le repère historique de chute de Minecraft regarde 0,2 bloc plus bas
et désigne sinon le sol sous le fruit. L'adaptation reste limitée aux
atterrissages des joueurs sur ces blocs.
Les pistons vanilla gardent leur résolution native des mouvements, avec la
réaction `POPPED` et un remplacement ciblé du butin lors de l'écrasement.
Le super piston utilise ce même rendement dans son plan commun déjà validé,
avant la création des têtes mobiles. Les 21 modèles de hauteur partielle sont
générés à partir des trois textures existantes, sans nouveaux PNG.
`check build assemblePack` passe en **2 min 20 s**, avec **78 tests serveur
requis réussis**, dont huit nouveaux scénarios pour l'écrasement. Ils couvrent
les trois fruits, les huit paliers, le rendement, la casse, la lecture d'anciens
états sans `layers`, les six directions du piston normal, la disparition de la
presse active, la course de 27 blocs du super piston et le refus atomique d'une
face bloquée. Le scénario vertical du super piston est isolé des autres tests
et compte aussi les résidus projetés en hauteur.
Le parcours **Vulkan avec Sodium 0.9.2** passe en **55 s** : huit vrais sauts
au clavier, neuf portions obtenues, disparition du dernier palier, 24 modèles
avec hauteur et collisions correspondantes, 27 blocs / 243 portions sur une
activation réelle du super piston. La presse est absente du créatif et des
ingrédients JEI, y compris du catalogue opérateur. La catégorie d'écrasement
et ses trois recettes s'affichent en FR/EN aux échelles GUI 2/3/4. Une
fermeture/réouverture réelle conserve les 24 états partiels ; le piston
récupère ensuite exactement les trois portions d'un bloc resté à deux paliers.
La régression **Piston087 sous Vulkan avec Sodium** passe en **1 min 8 s** :
six directions × deux variantes, charges ordinaires de 108/109 blocs, obstacles,
inventaires, adhérence, collisions, interruption, sérialisation, rechargement
de chunks et démontage. Son ancien appel de clé hors de portée est corrigé
dans la fixture de test, sans modifier les règles de portée du jeu.
Le JAR contient les **192 JSON d'appareils/écrasement**, 432 JSON culinaires et
376 JSON agricoles générés, identiques aux sources, les 44 classes culinaires
et les deux adaptations de pistons/atterrissages. La recette de presse est
absente de l'archive. Les 343 PNG importés restent identiques aux originaux,
dont les 38 fournis, et les 13 PNG de vanille sont toujours en 16 × 16.
Les copies packwiz client/serveur sont identiques au binaire contrôlé ; le
template conserve les mêmes textures culinaires, agricoles et de vanille.
SHA-256 de `sanctuary-beta.305.jar` :
```text
8c9b358c5c3caac3b9aa9a94230c7f6c6761d32f26002522bd8eba59c7f1cc2b
```
[Reçu de validation](crushing-beta305-verification.json). Journaux dans
`build/crushing305-*`, captures archivées dans
`build/crushing305-crushing-screenshots/`, monde client neuf de graine **305**.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=crushing305,kitchen304,cuisine303,vanilla301,agriculture300,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryCrushing305ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=crushing305 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Minecraft 26.3, Fabric API 0.160.5+26.3, Java 25. Les parcours graphiques
ciblent exclusivement Vulkan et leurs mondes sont neufs et ignorés par Git.
+241
View File
@@ -0,0 +1,241 @@
# IA-05 — Recettes de la 26.2 jouables, beta.303 locale
> Cette page décrit l'étape beta.303. [La préparation beta.304](kitchen-beta304.md)
> porte ensuite les six appareils : fermentation temporisée et pressage physique
> remplacent leurs adaptations manuelles. Les préparations crues restent valides.
Préparation du 28 octobre 2026 sur **la même branche `codex/itsalive-26-3`**,
à partir de beta.302 (`930719d7`). Minecraft 26.3, Java 25 et dépendances
inchangés. Aucun push, tag, publication, site, serveur ou Prism modifié.
Les essais utilisent seulement des mondes de développement neufs.
## Catalogue importé et accès
Les **92 recettes alimentaires de la 26.2** sont reprises : 12 fabrications
à froid, 19 préparations de chaudron, 26 de poêle, 13 de four, 10 recettes
de fermentation, 9 de moulin et 3 de presse. Les ingrédients intermédiaires
complètent les récoltes : farines, huile, pulpe, beurre, crème, fromage,+bouillon, sauces, tofu, riz cuit, nori, café moulu et vanille en poudre.
Le catalogue apporte **83 ingrédients et plats**, dont 70 comestibles, et
**46 préparations à cuire**, soit **129 objets**, tous dans l'onglet natif
**Nourriture et boissons**. Les semences gardent l'onglet Naturel ; aucune
nouvelle page créative n'est créée.
Les recettes utilisent la table de fabrication, le four et le fumoir
existants. Une salade se fabrique directement ; les plats chauds composés
passent par une préparation non comestible, puis une cuisson avec combustible.
Les préparations ont le dessin du plat, teinté, un nom distinct et une aide
FR/EN. Les recettes à un ingrédient sans récipient passent directement en
cuisson, notamment le maïs grillé et l'ananas grillé.
Le parcours utilise **196 recettes natives** : **180 recettes ajoutées**
(fabrications des préparations et variantes four/fumoir) et **16 recettes
vanilla réutilisées** pour les viandes, poissons et pommes de terre.
La découverte d'une récolte enseigne
la collection Cuisine au catalogue existant ; sa consultation conserve les
règles Sanctuary. La fabrication manuelle reste disponible sans cet achat.
JEI utilise les catégories natives de fabrication et de cuisson.
## Adaptations explicites
Ce lot porte le catalogue alimentaire sur les outils déjà jouables. Il
n'implémente pas les appareils historiques, leurs inventaires ou leurs gestes.
Les 10 transformations issues de la fermentation sont **des recettes manuelles
immédiates**, sans maturation en tonneau ni reprise du délai historique.
Le travail du chaudron, de la poêle, de la cuisinière et du séchoir reste
dans ses tickets ; aucun séchage ni mécanisme de plat raté n'est ajouté.
- Le pressage devient une fabrication avec **neuf fruits pour neuf portions**
d'huile, de pulpe ou de moût, conservant le bilan de l'ancien bloc pressé.
Les cinq recettes d'appareils et six recettes de compression/décompression
ne sont pas importées ; elles ne sont pas nécessaires à cette chaîne.
- Le moulin est adapté en fabrication manuelle. **Deux maïs donnent quatre
semoules**, pour conserver le rendement sans entrer en conflit avec la
récupération des grains depuis un maïs.
Deux fèves de cacao donnent quatre poudres, pour éviter le conflit avec
la teinture marron vanilla obtenue depuis une seule fève.
- L'ancien curry et le ragoût de pois chiches avaient les mêmes ingrédients
et se distinguaient par l'appareil. Le ragoût demande désormais une fiole
d'eau supplémentaire ; le curry garde ses ingrédients. La table donne un
résultat unique, sans dépendre de l'ordre de chargement des recettes.
- `minecraft:potion` signifie exclusivement **une fiole d'eau sans effet**.
Les potions de soin, poison, maladroites et eaux avec effets personnalisés
ne sont pas des ingrédients. La cuisson ne consomme jamais une bouteille
ou un seau : le récipient est récupéré dès la préparation à la table.
- La fabrication rend exactement les bouteilles d'eau/miel, les seaux de
lait et les couronnes d'ananas concernés. Les portions finies conservent
le modèle d'objet historique, sans ajout de bols ou bouteilles gratuits.
- Le tag historique manquant `herbs` est défini avec basilic, menthe et
coriandre. Les tags de baies, poissons, champignons et viandes cuites sont
repris. Les quantités finales et les profils alimentaires historiques
sont conservés, y compris les effets des plats et du café ; les boissons
utilisent l'animation native de boisson.
- Les huit cuissons simples déjà disponibles dans le jeu réutilisent leurs
recettes four/fumoir exactes de Minecraft 26.3, afin de conserver leurs
durées, combustible et expérience, sans ajouter de recettes concurrentes.
## Provenance et limites
`scripts/data/cuisine.json` conserve les 92 recettes sources et leurs
empreintes SHA-256, le catalogue alimentaire, les tags et les 11 exclusions.
Source historique en lecture seule : `26.2/itsalive`, commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later.
`scripts/cuisine_resources.py` produit les données, modèles et collection.
Les identifiants de destination utilisent `sanctuary:*` ; aucun ancien
identifiant agricole n'est renommé. Les 343 PNG importés, les images fournies
et les textures 16 × 16 de vanille sont conservés sans retouche.
`CulinaryCraftingRecipe` reste une recette sans forme native, avec contrôle
de l'eau et retours de récipients. Ses codecs 26.3 conservent les templates
sans matérialiser d'objets avant la liaison des composants du registre.
Les inventaires et sauvegardes utilisent les formats natifs ; aucune
migration, génération naturelle ou conversion d'appareil 26.2 n'est effectuée.
## Validation
`check build assemblePack` passe en **2 min 49 s**, avec **55 tests serveur
requis réussis**, dont neuf scénarios culinaires. Ils couvrent les 92 recettes
et leurs rendements, les chemins de production des 83 ingrédients/plats,
l'absence de résultats concurrents, l'eau et les potions refusées, les
récipients/couronnes, la fabrication en série, une vraie cuisson au fumoir,
les propriétés alimentaires, les effets du café et la découverte des recettes.
Le parcours **Vulkan avec Sodium 0.9.2** passe en **53 s** : paquets natifs
de fabrication d'une salade et d'une préparation de café, consommation exacte,
bouteille récupérée et cuisson par les ticks du fumoir. Les **196 étapes sont
présentes dans JEI** ; les ingrédients d'eau s'y affichent comme de l'eau.
Les 129 modèles d'objets, le seul onglet natif Nourriture et boissons, les
noms FR/EN et les échelles GUI 2/3/4 sont contrôlés. Un plat, une préparation
et un ingrédient gardent leurs quantités après fermeture/réouverture du monde.
La régression agricole Vulkan passe en **36 s** : 313 états de cultures,
48 objets, arbres/caféier, croix de l'oignon et de l'ail, onglets natifs FR/EN
GUI 2/3/4 et rechargement des 173 états agricoles. Les ajouts culinaires
n'empêchent pas l'accès aux récoltes dans le même onglet.
Les **445 ressources culinaires générées** et 376 JSON agricoles sont
vérifiés dans le JAR, avec les classes compilées et notices. Les 92 sources
de recette correspondent aux SHA-256 du reçu ; les 343 PNG importés restent
identiques, dont les 38 fournis. Les 13 textures de vanille sont toujours en
16 × 16. Les copies JAR packwiz client/serveur sont identiques au binaire
contrôlé ; le template contient les mêmes textures culinaires et notices.
SHA-256 de `sanctuary-beta.303.jar` :
```text
91809157a1f720192d5558682219840e6f70d13007a384973fa526ec4e9b2f66
```
Profils de vérification :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=cuisine303,vanilla301,agriculture300,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryCuisine303ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=cuisine303 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Journaux et reçu dans `build/cuisine303-*`, captures et monde neuf de graine
**303** dans le dossier de tests client ignoré. Validation graphique Vulkan
exclusivement.
## Catalogue vérifiable
| Recette historique | Ingrédients à la table ou en cuisson directe | Résultat | Usage 26.3 |
| --- | --- | --- | --- |
| `cauldron_bean_chili` | Haricots secs + Sauce tomate + Piment | Chili de haricots | Préparation → four/fumoir |
| `cauldron_broth` | Fiole d'eau + Carotte + Pomme de terre | 2 × Bouillon | Préparation → four/fumoir |
| `cauldron_cabbage_soup` | Chou + Carotte + Bouillon | Soupe au chou | Préparation → four/fumoir |
| `cauldron_chicken_rice_soup` | Poulet cru + Bouillon + Riz | Soupe poulet-riz | Préparation → four/fumoir |
| `cauldron_chickpea_stew` | Pois chiches + Sauce tomate + Coriandre + Piment + Fiole d'eau | Ragoût de pois chiches | Préparation → four/fumoir |
| `cauldron_coffee` | Café moulu + Fiole d'eau | Café | Préparation → four/fumoir |
| `cauldron_cooked_rice` | Riz + Fiole d'eau | 2 × Riz cuit | Préparation → four/fumoir |
| `cauldron_corn_chowder` | Maïs + Crème + Pomme de terre | Soupe de maïs | Préparation → four/fumoir |
| `cauldron_cream` | Seau de lait | 2 × Crème | Préparation → four/fumoir |
| `cauldron_lemon_honey_tea` | Citron + Fiole d'eau + Fiole de miel | Infusion citron-miel | Préparation → four/fumoir |
| `cauldron_mint_tea` | Menthe + Fiole d'eau | Infusion de menthe | Préparation → four/fumoir |
| `cauldron_mushroom_veloute` | Champignons de cuisine + Bouillon + Crème | Velouté de champignons | Préparation → four/fumoir |
| `cauldron_potato_soup` | Pomme de terre + Crème + Oignon | Soupe de pommes de terre | Préparation → four/fumoir |
| `cauldron_pumpkin_veloute` | Citrouille + Crème + Sel | Velouté de citrouille | Préparation → four/fumoir |
| `cauldron_salt` | Fiole d'eau | 2 × Sel | Préparation → four/fumoir |
| `cauldron_soy_milk` | Graines de soja + Fiole d'eau | 2 × Lait de soja | Préparation → four/fumoir |
| `cauldron_tofu` | Lait de soja + Sel | 2 × Tofu | Préparation → four/fumoir |
| `cauldron_tomato_sauce` | Pulpe de tomate + Sel + Basilic | 2 × Sauce tomate | Préparation → four/fumoir |
| `cauldron_vegetable_soup` | Carotte + Bouillon + Oignon | Soupe de légumes | Préparation → four/fumoir |
| `cold_burger` | Pain + Bœuf cuit + Fromage + Laitue + Tomate | Burger | Table |
| `cold_cucumber_maki` | Riz cuit + Nori + Concombre | Maki concombre | Table |
| `cold_fruit_salad` | Ananas + Fraise + Raisin + Pomme | Salade de fruits | Table |
| `cold_green_salad` | Laitue + Concombre + Tomate | Salade verte | Table |
| `cold_hummus` | Pois chiches + Huile d'olive + Citron + Ail | Houmous | Table |
| `cold_kebab` | Pain + Mouton cuit + Oignon + Tomate | Kebab | Table |
| `cold_mediterranean_salad` | Laitue + Concombre + Tomate + Olives | Salade méditerranéenne | Table |
| `cold_sandwich` | Pain + Viandes cuites + Laitue + Tomate | Sandwich | Table |
| `cold_sushi` | Riz cuit + Nori + Poissons de cuisine | Sushi | Table |
| `cold_tofu_maki` | Riz cuit + Nori + Tofu | Maki tofu | Table |
| `cold_vegetarian_sandwich` | Pain + Fromage + Tomate + Laitue | Sandwich végétarien | Table |
| `ferment_beer` | Blé + Fiole d'eau + Levure | Bière | Table |
| `ferment_cheese` | Crème + Sel | Fromage | Table |
| `ferment_hot_sauce` | Pulpe de tomate + Piment + Vinaigre | Sauce piquante | Table |
| `ferment_kimchi` | Chou + Sel + Piment | Kimchi | Table |
| `ferment_pickles` | Concombre + Sel + Vinaigre | Cornichons | Table |
| `ferment_soy_sauce` | Graines de soja + Sel | Sauce soja | Table |
| `ferment_vinegar` | Vin | Vinaigre | Table |
| `ferment_wine` | Moût de raisin | Vin | Table |
| `ferment_yeast` | Blé + Sucre | 2 × Levure | Table |
| `ferment_yogurt` | Seau de lait + Sucre | Yaourt | Table |
| `mill_chickpeas` | Pois chiches | 2 × Farine de pois chiche | Table |
| `mill_cocoa_powder` | 2 × Fèves de cacao | 4 × Poudre de cacao | Table |
| `mill_cornmeal` | 2 × Maïs | 4 × Semoule de maïs | Table |
| `mill_cream` | Crème | Beurre | Table |
| `mill_dried_kelp` | Algue séchée | Nori | Table |
| `mill_ground_coffee` | Grains de café torréfiés | 2 × Café moulu | Table |
| `mill_rice` | Riz | 2 × Farine de riz | Table |
| `mill_vanilla_powder` | Gousse de vanille séchée | 2 × Poudre de vanille | Table |
| `mill_wheat` | Blé | 2 × Farine de blé | Table |
| `oven_apple_pie` | Pâte crue + Pomme + Sucre + Beurre | Tarte aux pommes | Préparation → four/fumoir |
| `oven_berry_pie` | Pâte crue + Baies de cuisine + Sucre + Beurre | Tarte aux baies | Préparation → four/fumoir |
| `oven_bread` | Pâte crue | Pain | Four/fumoir |
| `oven_carrot_cake` | Pâte crue + Carotte + Sucre + Œuf | Gâteau aux carottes | Préparation → four/fumoir |
| `oven_chickpea_flatbread` | Farine de pois chiche + Fiole d'eau + Huile d'olive | Galette de pois chiche | Préparation → four/fumoir |
| `oven_chocolate_cake` | Pâte crue + Poudre de cacao + Sucre + Œuf | Gâteau au chocolat | Préparation → four/fumoir |
| `oven_cornbread` | Semoule de maïs + Levure + Seau de lait | Pain de maïs | Préparation → four/fumoir |
| `oven_flatbread` | Farine de blé + Fiole d'eau + Sel | Pain plat | Préparation → four/fumoir |
| `oven_margherita_pizza` | Pâte crue + Sauce tomate + Fromage + Basilic | Pizza margherita | Préparation → four/fumoir |
| `oven_pumpkin_pie` | Pâte crue + Citrouille + Sucre + Œuf | Tarte à la citrouille | Préparation → four/fumoir |
| `oven_rice_bread` | Farine de riz + Levure + Fiole d'eau | Pain de riz | Préparation → four/fumoir |
| `oven_stuffed_pepper` | Poivron + Riz cuit + Sauce tomate + Fromage | Poivron farci | Préparation → four/fumoir |
| `oven_vanilla_cake` | Pâte crue + Œuf + Sucre + Poudre de vanille | Gâteau à la vanille | Préparation → four/fumoir |
| `pan_beef` | Bœuf cru | Bœuf cuit | Four/fumoir |
| `pan_beef_stir_fry` | Bœuf cru + Poivron + Oignon + Sauce soja | Bœuf sauté | Préparation → four/fumoir |
| `pan_chicken` | Poulet cru | cooked_chicken | Four/fumoir |
| `pan_chickpea_curry` | Pois chiches + Sauce tomate + Piment + Coriandre | Curry de pois chiches | Préparation → four/fumoir |
| `pan_cod` | Morue crue | cooked_cod | Four/fumoir |
| `pan_fish_meuniere` | Poissons de cuisine + Citron + Beurre | Poisson meunière | Préparation → four/fumoir |
| `pan_fried_cabbage` | Chou + Beurre + Oignon | Chou poêlé | Préparation → four/fumoir |
| `pan_fried_egg` | Œuf | Œuf au plat | Four/fumoir |
| `pan_fried_rice` | Riz cuit + Œuf + Graines de soja + Sauce soja | Riz sauté | Préparation → four/fumoir |
| `pan_garlic_green_beans` | Haricots verts + Ail + Beurre | Haricots verts sautés | Préparation → four/fumoir |
| `pan_grilled_corn` | Maïs | Maïs grillé | Four/fumoir |
| `pan_grilled_eggplant` | Aubergine + Huile d'olive | Aubergine grillée | Préparation → four/fumoir |
| `pan_grilled_pineapple` | Ananas | Ananas grillé | Four/fumoir |
| `pan_herb_omelette` | Œuf + Seau de lait + Herbes aromatiques | Omelette aux herbes | Préparation → four/fumoir |
| `pan_mushroom_saute` | Champignons de cuisine + Beurre + Ail | Poêlée de champignons | Préparation → four/fumoir |
| `pan_mutton` | Mouton cru | Mouton cuit | Four/fumoir |
| `pan_omelette` | Œuf + Seau de lait | Omelette | Préparation → four/fumoir |
| `pan_porkchop` | Porc cru | cooked_porkchop | Four/fumoir |
| `pan_potato` | Pomme de terre | baked_potato | Four/fumoir |
| `pan_rabbit` | Lapin cru | cooked_rabbit | Four/fumoir |
| `pan_ratatouille` | Aubergine + Tomate + Poivron + Oignon | Ratatouille | Préparation → four/fumoir |
| `pan_salmon` | Saumon cru | cooked_salmon | Four/fumoir |
| `pan_sauteed_potatoes` | Pomme de terre + Oignon + Beurre | Pommes de terre sautées | Préparation → four/fumoir |
| `pan_spiced_mutton` | Mouton cru + Oignon + Piment + Coriandre | Émincé de mouton épicé | Préparation → four/fumoir |
| `pan_tofu_stir_fry` | Tofu + Poivron + Graines de soja + Sauce soja | Wok de tofu | Préparation → four/fumoir |
| `pan_tomato_basil_chicken` | Poulet cru + Tomate + Basilic | Poulet tomate-basilic | Préparation → four/fumoir |
| `press_grape_block` | 9 × Raisin | 9 × Moût de raisin | Table |
| `press_olive_block` | 9 × Olives | 9 × Huile d'olive | Table |
| `press_tomato_block` | 9 × Tomate | 9 × Pulpe de tomate | Table |
| `raw_dough` | Farine de blé + Levure + Fiole d'eau | 2 × Pâte crue | Table |
@@ -0,0 +1,49 @@
{
"version": "beta.318",
"date": "2026-10-30",
"branch": "codex/itsalive-26-3",
"base_commit": "7ef4d51a",
"local_only": true,
"published_or_installed": false,
"new_results": 18,
"new_items_including_preparation": 19,
"new_recipes": 19,
"active_drying_recipes": 16,
"active_oven_recipes": 17,
"active_cauldron_recipes": 24,
"active_milling_recipes": 10,
"generated_json_verified": 1268,
"unchanged_original_pngs": 1108,
"jar_sha256": "5b8d6fff72c3128ceac085a9d71b493bbda13a0dbbe42ffdd3e8dc2fda3a632b",
"client_server_staged_jar_sha256_match": true,
"template_zip_sha256": "a5b6199779c818e5134d6b9eef10f257dc4412f1bcd0dc53589121eff793ada3",
"server": {
"command": "./gradlew check build assemblePack -PsanctuaryFocusedTests=driedcuisine318,drying317,drying313,foraging316,agriculture300,vanilla301,sausage315,cheesewine314,cauldron312,cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge -PsanctuaryAtlasOnly=true",
"required_tests_passed": 189,
"duration": "2m 48s",
"log": "build/dried-cuisine318-check-build-pack.log"
},
"client": {
"command": "./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryDriedCuisine318ClientTests=true -PsanctuarySodiumClientTests=true -PsanctuaryFocusedTests=driedcuisine318,drying317 -PsanctuaryQuickTests=true -PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 -PsanctuaryClientNoVsync=true",
"backend": "Vulkan / Apple M1",
"sodium": true,
"duration": "1m 10s",
"result": "pass",
"models": 19,
"screenshots": 16,
"screenshots_directory": "build/dried-cuisine318-screenshots",
"log": "build/dried-cuisine318-client.log",
"spicy_visual_pulses_and_unchanged_health_verified": true,
"only_graphical_drying_fixture_accelerates_after_actual_save": true,
"final_low_impact_change": "Raw spicy sausage tooltip points to the drying rack; compiled with the full final build."
},
"spicy_effect": {
"visual_packet": "ClientboundHurtAnimationPacket",
"pulses": 2,
"delay_ticks": 8,
"damage_event_sent": false,
"health_absorption_velocity_and_server_hurt_state_unchanged": true,
"saved_state_added": false
},
"graphics_limit": "Existing sprites with native tint and reused cuboid sausage geometry."
}
+116
View File
@@ -0,0 +1,116 @@
# IA-11 — Les 18 nouvelles recettes aux produits séchés
Préparation locale **beta.318**, 30 octobre 2026, branche `codex/itsalive-26-3`,
socle `7ef4d51a` / beta.317. Demande : rendre jouables les dix-huit combinaisons
du guide, puis ajouter de petits coups sans dégâts lorsqu'on mange épicé.
Aucune publication, installation Prism, mise à jour de site ou de serveur.
## Résultat jouable
[Liste complète : ingrédients, appareils, quantités et durées](wiki/alimentation/sechoir.md) ·
[Fiche autonome HTML](wiki/alimentation/sechoir.html).
- Cuisinière : pain aux raisins, brioche vanillée aux raisins, biscuits aux fruits
secs et pain aux tomates et basilic. Cuisson de 12 à 16 secondes, ingrédients
payés en fin de cycle, combustible et sortie unique habituels. Quatre biscuits
par lot ; un pain ou une brioche pour les autres préparations.
- Établi : les deux sandwichs, salade de pois chiches aux tomates séchées,
tomates marinées, mélange de fruits secs, dessert ananas-vanille et
assaisonnement aux herbes. Un résultat par lot, ou deux portions d'assaisonnement.
- Chaudron : riz au saumon séché, ragoût du voyageur, soupe de poisson séché,
haricots à la viande séchée et infusion pomme-menthe. Une dose d'eau par portion,
chaleur et geste au bâton ; cuisson de 11 à 18 secondes. Le réassort et la recette
mémorisée gardent le fonctionnement existant. Ajouter le bol, ou une fiole vide
pour l'infusion, avec les ingrédients. Le récipient revient une fois à la consommation.
- Moulin manuel : un piment séché donne deux portions de poudre, sans combustible.
La poudre remplace aussi le piment dans les anciennes recettes. L'assaisonnement
aux herbes fonctionne dans l'omelette existante.
- Saucisson pimenté : à l'établi, une préparation de saucisson, un piment séché
ou une poudre de piment et une coriandre séchée donnent une préparation pimentée.
Elle sèche cinq minutes puis se pose pour manger quatre tranches de deux points
de nourriture chacune. Les modèles montrent la coupe et la longueur restante.
Cela ajoute **18 résultats, 19 objets en comptant la préparation de saucisson,
et 19 transformations** : le saucisson possède deux étapes. Le séchoir passe de
15 à **16 recettes**. Aucun nouvel onglet créatif ; tout rejoint Nourriture et boissons.
Libellés FR/EN, recettes natives synchronisées dans JEI et découverte par les
ingrédients séchés ou les préparations correspondantes.
Les bols et fioles sont des composants natifs `USE_REMAINDER`, après paiement
effectif de leur récipient d'origine. L'infusion consomme aussi une fiole de miel :
sa bouteille est éjectée à la cuisson, tandis que la fiole de service revient
quand on boit. L'eau peut venir de la réserve du chaudron ou d'une vraie fiole
d'eau ; les potions à effets sont refusées. L'ananas séché ne rend aucune seconde
couronne et les nouveaux plats ne créent aucune semence.
## Réaction épicée
Après la consommation effective d'un aliment du tag `sanctuary:spicy_foods`,
un paquet natif `ClientboundHurtAnimationPacket` et un petit son de coup reçu
produisent la réaction ; un second coup suit huit ticks plus tard, soit 0,4 seconde.
Une nouvelle bouchée remplace l'attente précédente, sans empiler une file sans limite.
Le piment cru et les anciens chilis, ragoût de pois chiches, curry, mouton épicé
et kimchi sont concernés, ainsi que les nouveaux haricots à la viande séchée et
le saucisson pimenté posé. Les ingrédients non comestibles, dont la sauce piquante,
ne déclenchent aucune consommation artificielle. Les plats doux ne réagissent pas.
Il n'y a aucun appel de dégâts, aucune modification de santé, absorption, armure,
invulnérabilité ou vitesse. Aucun événement de dégâts n'est envoyé. La réaction
reste transitoire, ne crée ni effet de statut ni donnée de sauvegarde, et s'arrête
à la mort, à la déconnexion ou à l'arrêt du serveur. Une tranche refusée parce
que le joueur est rassasié ne déclenche pas la réaction.
## Compatibilité et graphismes
Ajouts uniquement pour les objets, bloc de saucisson pimenté et recettes. Les
identifiants existants et le stockage des appareils, dont `DryingVersion=1`,
restent inchangés. Aucun changement de génération, aucun monde personnel ouvert,
aucune conversion de sauvegarde. Les anciennes préparations et le séchage nature
gardent leurs recettes et leurs durées.
Les nouvelles recettes réutilisent les types natifs de fabrication culinaire,
cuisson, moulin et séchoir déjà synchronisés. Le catalogue historique 26.2
n'est pas modifié. Le nouveau catalogue `scripts/data/dried-cuisine.json` contient
les valeurs alimentaires, quantités, récipients, durées et sources des icônes.
Les propositions du catalogue précédent sont marquées comme implémentées en beta.318.
Les icônes utilisent des sprites existants avec des teintes natives. Le saucisson
pimenté réutilise les géométries cubiques du saucisson avec une teinte orange,
y compris les seize orientations et découpes. Les PNG d'origine restent intacts ;
il ne s'agit pas de dix-neuf nouvelles textures peintes sur mesure.
## Vérification
`./gradlew check build assemblePack` a réussi en **2 min 48 s**. Les **189 tests
serveur natifs** passent : dix nouveaux cas couvrent les dix-huit résultats et
leurs dix-neuf transformations, les fabrications payées, toutes les échéances
complètes, la cuisson répétée, les bols/fioles, la découverte, le saucisson
sauvegardé puis partagé et les paquets épicés sans dégâts. Les régressions couvrent
les 24 recettes du chaudron, les autres appareils, l'agriculture, les récoltes,
la vanille, les recettes et la progression.
Le parcours **Vulkan / Apple M1 avec Sodium** a réussi en **1 min 10 s** :
les dix-neuf modèles et leurs noms FR/EN, nom du bloc placé, onglet Nourriture et
boissons, menus aux échelles 2/3/4, vrais paquets de fabrication d'un sandwich,
cuisson du pain aux raisins, riz au saumon au chaudron après remuage, mouture de
quatre piments en huit poudres puis paiement d'une poudre pour le saucisson,
accrochage et récupération, sauvegarde réelle, quatre tranches, deux animations
épicées par tranche avec santé inchangée, retour d'un bol et recettes JEI.
Seuls les derniers ticks du séchage graphique sont rapprochés de leur échéance,
après la sauvegarde ; les 6 000 ticks complets passent côté serveur. Seize
captures sont conservées dans `build/dried-cuisine318-screenshots/`.
L'infobulle finale de la préparation pimentée indique le séchoir et ses 300 secondes,
puis est compilée avec le build complet. Les contrôles du JAR confirment
**1 268 ressources JSON**, les **19 recettes ajoutées** sans suppression d'ancien
identifiant et **1 108 PNG strictement identiques à beta.317**. Les JAR du mod et
des packs client/serveur préparés ont le même SHA-256 ; le ZIP du template est valide.
Aucun monde personnel ni installation de jeu n'a été touché.
[Commandes, empreintes et reçu](dried-cuisine-beta318-verification.json).
## Installation locale ultérieure
Le 30 octobre 2026, ce binaire vérifié est installé dans la même instance Prism
pour les essais, sans publication ni lancement d’un monde personnel.
[Profil local, conservation et reçu](local-beta318.md).
@@ -0,0 +1,40 @@
{
"version": "beta.317",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.317.jar",
"jar_sha256": "a1ddd0b1701d2144a93f1368a4e1f862869e2170ee03c7c842908578c81a60a6",
"generated_json_verified": 1182,
"unchanged_original_pngs": 1108,
"new_items": 12,
"active_drying_recipes": 15,
"new_drying_recipes": 14,
"future_combinations_proposed": 18,
"legacy_smoking_xp_recipe_preserved": true,
"date": "2026-10-30",
"branch": "codex/itsalive-26-3",
"base_commit": "7b56c3cd",
"local_only": true,
"published_or_installed": false,
"world_generation_changed": false,
"drying_storage_version": 1,
"server": {
"command": "./gradlew check build assemblePack -PsanctuaryFocusedTests=drying317,drying313,foraging316,agriculture300,vanilla301,sausage315,cheesewine314,cauldron312,cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge -PsanctuaryAtlasOnly=true",
"required_tests_passed": 179,
"build_duration": "2m 40s",
"log": "build/drying317-check-build-pack.log"
},
"client": {
"command": "./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryDrying317ClientTests=true -PsanctuarySodiumClientTests=true -PsanctuaryFocusedTests=drying317,drying313 -PsanctuaryQuickTests=true -PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 -PsanctuaryClientNoVsync=true",
"backend": "Vulkan",
"renderer": "Apple M1",
"sodium": true,
"duration": "51s",
"result": "pass",
"screenshots": "build/drying317-final-screenshots",
"screenshots_count": 9,
"log": "build/drying317-client.log",
"accelerated_near_completion_only_in_graphical_fixture": true
},
"template_zip_sha256": "bd8f959ce3d835c1ffff7f7d769cf125700c599bee2f049ed9b04cb1e9412b94",
"staged_client_and_server_jar_sha256_match": true,
"graphics_limit": "Existing sprites with native constant tint; no twelve separately painted PNGs."
}
+114
View File
@@ -0,0 +1,114 @@
# IA-10 — Viandes, poissons, fruits et herbes au séchoir
Préparation locale **beta.317**, 30 octobre 2026, même branche
`codex/itsalive-26-3`, socle `7b56c3cd` / beta.316 locale. Demande : ajouter les
produits séchés et proposer des combinaisons culinaires. Aucune publication,
installation Prism, modification du site ou du serveur personnel.
## Résultat jouable
[Recettes détaillées, usages et 18 propositions de plats](wiki/alimentation/sechoir.md) ·
[Copie autonome sous forme de tableaux](wiki/alimentation/sechoir.html).
Le séchoir possède **15 recettes**, dont le saucisson de beta.313 inchangé :
| Durée de jeu | Produits |
| --- | --- |
| 1 minute | Basilic, menthe, coriandre et fraises séchés |
| 1 min 30 s | Raisins secs, piment séché, pomme séchée |
| 2 minutes | Cabillaud séché, saumon séché, tomates et ananas séchés |
| 3 minutes | Gousse de vanille séchée |
| 4 minutes | Viande séchée depuis du bœuf cru ou du porc cru |
| 5 minutes | Saucisson séché depuis sa préparation |
**Douze nouveaux objets** dans l'onglet vanilla Nourriture et boissons,
avec libellés FR/EN, modèles et recettes JEI. Cabillaud et saumon crus utilisent
les objets Minecraft existants. Bœuf et porc donnent le même produit séché ;
les deux poissons gardent des produits distincts. Les fruits ajoutés sont
pomme, fraise et ananas, plus les raisins secs.
Les nouvelles provisions sont comestibles. Viande : 6 points et coefficient
de saturation 0,8 ; cabillaud : 5/0,6 ; saumon : 6/0,7 ; raisins et fraises :
3/0,5 ; tomate : 2/0,6 ; pomme : 4/0,5 ; ananas : 4/0,7. Piment et herbes
séchés restent des ingrédients. La couronne d'ananas est rendue exactement
une fois **à la fin du séchage**, par le mécanisme de restes déjà présent.
Le produit séché n'en rend pas à sa consommation.
Les quatre crochets restent indépendants : aliments différents, départs
différents et durées différentes sur un même cadre. Aucun combustible.
Ticks chargés à 20 ticks/s ; pluie et immersion suspendent le travail,
un toit protège. Alimentation par entonnoir et extraction des produits prêts
seulement conservées. Le retrait d'un aliment annule uniquement son propre
travail. Les ingrédients prêts ne sont pas séchés à nouveau.
## Recettes existantes et pistes futures
Le piment et chaque herbe séchée remplacent leur équivalent frais, un pour
un, dans les recettes de cuisine portées, aussi bien à la préparation manuelle
que dans les appareils. Les tags sont additifs : `cooking_chilies`,
`cooking_basil`, `cooking_mint`, `cooking_coriander`, et le tag `herbs` inclut
aussi les trois herbes sèches. Cela rend immédiatement utilisables les
omelettes, infusions de menthe, sauces tomate, pizzas, chilis, currys, mouton
épicé, kimchi et sauce piquante concernés. Les récoltes sèches ne donnent
pas de nouvelles graines via les recettes agricoles.
Les tags `dried_fish` et `dried_fruits` décrivent les produits disponibles.
Les **18 autres combinaisons du guide sont des propositions**, avec des
ingrédients présents dans le jeu ; aucun nouveau pain, sandwich, ragoût,
mélange de fruits ou assaisonnement n'est annoncé comme implémenté.
Les quantités, eau, récipients et valeurs nutritionnelles de ces futurs plats
restent à décider lors d'un prochain lot.
## Contrat de compatibilité et graphismes
Les identifiants existants, le format `DryingVersion=1`, les quatre cases
et leurs compteurs sont conservés. Aucun changement de génération ni de
sauvegarde n'est ajouté. Pas de repeuplement ou de conversion d'un monde.
Les préparations de saucisson gardent leur recette et leurs 6 000 ticks.
La production active de vanille utilise désormais `sanctuary:dry_vanilla_pod`,
`sanctuary:food_drying`, 3 600 ticks, et garde le résultat `sanctuary:vanilla_pod`.
L'ancien identifiant `sanctuary:dried_vanilla_pod` reste enregistré comme recette
native de fumoir avec sa valeur d'XP historique, mais son tag d'ingrédients de
compatibilité `legacy_vanilla_smoking` est vide : aucune gousse ne s'y traite.
Cette conservation évite le cast natif vers `AbstractCookingRecipe` lors du
paiement d'une ancienne réserve d'XP de fumoir. Le test recharge cette histoire
native, récupère les recettes/XP et vérifie que gousses vertes et charbon restent
intacts. Aucun inventaire ou format de sauvegarde n'est réécrit.
La poudre de vanille au moulin et les recettes utilisant la gousse restent
compatibles. Aucune modification de monde existant n'est réalisée pour tester
ce changement.
Les nouvelles icônes utilisent le modèle natif `item/generated`, les sprites
existants et une teinte constante, comme les préparations déjà portées.
Tous les PNG d'origine restent intacts. Ce sont des icônes réutilisant les
dessins actuels, pas douze nouvelles textures peintes sur mesure.
## Sources et vérifications
`scripts/data/drying-foods.json` pilote les objets, 14 nouvelles transformations,
durées et 18 propositions. `scripts/dried_food_resources.py` génère les
modèles, recettes, tags et fiches de découverte ; `DriedFoods` enregistre les
objets. `scripts/build_drying_guide.py` régénère le guide Markdown et sa copie
autonome HTML. Le catalogue historique 26.2 n'est pas réécrit.
`./gradlew check build assemblePack` a réussi en **2 min 40 s**, avec
**179 tests serveur natifs réussis** : nouvelles transformations, séchoir historique,
récoltes, agriculture et vanille, fromage/vin/saucisson, appareils, cuisine,
recettes et progression. Les 14 nouvelles transformations exécutent chaque
échéance complète, avec contrôle au tick précédent puis au tick exact.
Le parcours **Vulkan / Apple M1 avec Sodium** a réussi en **51 s** : pose et
accrochage payés de quatre aliments différents, douze modèles sans texture
manquante, onglet Nourriture et boissons, FR/EN aux échelles 2/3/4, immersion
et vidage avec de vrais seaux, fermeture/réouverture du monde de test, reprise
des compteurs, quatre récupérations, consommation native et **15 recettes JEI**.
Les compteurs sont rapprochés de leur fin uniquement dans la fixture graphique,
après sauvegarde et vidage ; les durées complètes sont testées côté serveur.
Neuf captures sont conservées dans `build/drying317-final-screenshots/`.
L'audit du JAR vérifie **1 182 ressources JSON** contre les générateurs,
**1 108 PNG strictement identiques à beta.316**, et les modèles/libellés FR/EN.
Les JAR du mod et des packs client/serveur préparés ont le même SHA-256 ;
le ZIP du template beta.317 est valide. Aucune installation ni publication.
[Commandes, empreintes et reçu](drying-foods-beta317-verification.json).
@@ -0,0 +1,87 @@
{
"version": "beta.313",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.313.jar",
"sha256": "54ea931d2f54eec8dac2d03e5ad1df8fd5b389db235a536bdd98d90a531fbe55",
"previous_verified_sha256": "90349a998970030ec0bfd880b307b9162e76fd0f460d70f1c9ac8ad2c115c8bd",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"data/minecraft/tags/block/mineable/axe.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/KitchenAppliances.class",
"fr/koka/sanctuary/cuisine/KitchenClient$1.class",
"fr/koka/sanctuary/cuisine/KitchenClient.class",
"fr/koka/sanctuary/cuisine/KitchenJei$Category.class",
"fr/koka/sanctuary/cuisine/KitchenJei.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [
"assets/sanctuary/blockstates/drying_rack.json",
"assets/sanctuary/items/cured_sausage.json",
"assets/sanctuary/items/drying_rack.json",
"assets/sanctuary/items/raw_sausage.json",
"assets/sanctuary/models/block/drying_rack.json",
"assets/sanctuary/models/item/cured_sausage.json",
"assets/sanctuary/models/item/drying_rack.json",
"assets/sanctuary/models/item/raw_sausage.json",
"data/sanctuary/loot_table/blocks/drying_rack.json",
"data/sanctuary/recipe/dry_sausage.json",
"data/sanctuary/recipe/drying_rack.json",
"data/sanctuary/recipe/prepare_sausage.json",
"data/sanctuary/sanctuary_recipe_collections/drying.json",
"fr/koka/sanctuary/cuisine/culinary/block/DryingRackBlock.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/DryingRackBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/DryingRackHud.class",
"fr/koka/sanctuary/cuisine/culinary/client/renderer/DryingRackBlockEntityRenderer$1.class",
"fr/koka/sanctuary/cuisine/culinary/client/renderer/DryingRackBlockEntityRenderer.class",
"fr/koka/sanctuary/cuisine/culinary/client/renderer/state/DryingRackRenderState.class",
"fr/koka/sanctuary/cuisine/recipe/FoodDryingRecipe$1.class",
"fr/koka/sanctuary/cuisine/recipe/FoodDryingRecipe.class"
],
"removed_jar_entries": [],
"generated_json": {
"cuisine": 432,
"kitchen": 208,
"agriculture": 376
},
"additive_drying_json": 13,
"culinary_source_classes": 50,
"culinary_compiled_classes_verified": 69,
"drying_slots": 4,
"items_per_slot": 1,
"drying_ticks": 6000,
"native_minecraft_textures_only": true,
"old_pngs_unchanged": 1108,
"new_pngs": 0,
"server_required": 112,
"drying_scenarios": 12,
"client_vulkan": {
"log": "build/drying313-client.log",
"screenshots": "build/drying313-screenshots",
"screenshot_count": 11,
"world_seed": 313,
"sodium": "0.9.2+mc26.3",
"native_crafting": true,
"native_eating": true,
"actual_disk_reopen": true,
"actual_bucket_pause_drain": true
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"template_added_entries": [
"assets/sanctuary/blockstates/drying_rack.json",
"assets/sanctuary/items/cured_sausage.json",
"assets/sanctuary/items/drying_rack.json",
"assets/sanctuary/items/raw_sausage.json",
"assets/sanctuary/models/block/drying_rack.json",
"assets/sanctuary/models/item/cured_sausage.json",
"assets/sanctuary/models/item/drying_rack.json",
"assets/sanctuary/models/item/raw_sausage.json"
],
"build_log": "build/drying313-build.log"
}
+113
View File
@@ -0,0 +1,113 @@
# IA-06 — préparation de saucisson et séchoir, beta.313 locale
Demande du créateur du 29 octobre 2026 : préparer le saucisson puis le sécher
avec des gestes vanilla. Durée confirmée : **5 minutes de jeu**. Un séchoir
en bois porte quatre préparations visibles, sans combustible ni menu. La
pluie suspend le séchage exposé ; un toit le protège. Même branche locale
`codex/itsalive-26-3`, sans publication, installation ou déploiement.
## Contrat de stockage avant développement
Ajouts uniquement : bloc/objet/bloc entité `sanctuary:drying_rack`, objets
`sanctuary:raw_sausage` et `sanctuary:cured_sausage`, type et sérialiseur de
recette `sanctuary:food_drying`. Aucun identifiant existant ni format des
anciens appareils ne change. Aucune génération de monde n'est ajoutée.
Le séchoir possède quatre cases natives 0–3, **un objet par crochet**. La
préparation et le produit séché utilisent la même case : une conversion
remplace exactement un objet par un autre. Chaque crochet conserve ses
propres ticks, durée, identifiant de recette et état terminé, sous marqueur
`DryingVersion=1`. Les composants des objets restent natifs.
La progression est autoritaire côté serveur, uniquement sur les ticks du
bloc chargé et sec. Pluie et immersion suspendent sans payer ni perdre le
travail ; un toit protège de la pluie. Le rechargement reprend les ticks
stockés, sans rattrapage hors ligne. Remplacer/retirer une préparation annule
son avancement ; les autres crochets continuent. La casse rend le séchoir et
chaque objet présent une seule fois, cru ou séché selon son état réel.
Les pistons ne déplacent pas le bloc entité.
Une ancienne version ne connaît pas ces nouveaux identifiants : récupérer
les objets et retirer les séchoirs avant un retour à une version antérieure.
Aucun monde personnel n'est converti ; les tests ciblent des mondes neufs
et des fixtures de développement ignorés.
## Préparation et gestes retenus
À l'établi : deux côtelettes de porc crues, du sel, une gousse d'ail et une
ficelle donnent **deux préparations de saucisson**. La préparation crue ne se
mange pas. Le séchoir se fabrique avec trois bâtons, deux ficelles et deux
planches ; son modèle et ceux des saucissons utilisent des cuboïdes et des
textures vanilla référencées, sans nouveaux PNG.
Un clic place une préparation sur un crochet libre. À main vide, récupérer
un saucisson prêt ; sinon reprendre une préparation en cours. L'eau se gère
avec le comportement vanilla des blocs pouvant contenir de l'eau. L'orientation
est horizontale et les contenus tournent avec le cadre. Les entonnoirs peuvent
alimenter les crochets libres et ne retirer que les produits prêts dessous.
Les trois nouveaux objets rejoignent Nourriture et boissons, sans nouvelle
page créative. Les recettes sont intégrées au système de découverte et à JEI.
## Vérifications locales
Douze cas serveur natifs vérifient le vrai paiement à l'établi, l'absence de
recette concurrente, les **6 000 ticks complets** sur quatre crochets, les
interruptions indépendantes, la pluie et le toit, l'immersion et le seau,
les quatre progressions sauvegardées sans rattrapage hors ligne, la collecte
sélective, la casse sans duplication, l'alimentation/extraction par de vrais
entonnoirs, la sauvegarde d'un résultat fini et la découverte des trois recettes.
La fixture de pluie est placée dans un biome de plaine dans le monde de test.
Le parcours **Vulkan / Apple M1**, avec **Sodium 0.9.2+mc26.3**, a réussi en
**1 min 16 s**, monde plat neuf de graine **313** :
- vrais paquets de l'établi, deux fabrications payées donnant quatre
préparations depuis quatre porcs crus, deux sels, deux ails et deux ficelles ;
- pose du séchoir avec un objet réellement consommé, puis quatre accrochages ;
- retrait/raccrochage d'une préparation, sans annuler les trois autres ;
- huit états de modèle chargés, cinq cuboïdes de bois par état, trois modèles
d'objets sans texture manquante, objets dans Nourriture et boissons ;
- HUD français/anglais aux échelles 2, 3 et 4 ;
- vrai seau d'eau puis seau vide pour suspendre/reprendre le séchage ;
- fermeture/réouverture conservant les quatre préparations et leur progression
exacte pendant l'immersion ;
- transformation, quatre récupérations natives puis consommation réelle d'un
saucisson qui rend de la nourriture ;
- recette de cinq minutes synchronisée dans sa catégorie JEI.
Les 6 000 ticks sont exécutés intégralement dans les cas serveur. Pour ne pas
attendre cinq minutes dans le parcours graphique, la fixture rapproche les
quatre compteurs de leur fin **après** le rechargement et le vidage au seau ;
les 40 à 100 derniers ticks, les transformations, clics et paiements restent
réels. Onze captures sont archivées dans `build/drying313-screenshots/` ;
les journaux de build, serveur et client sont ignorés.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=drying313,cauldron312,cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryDrying313ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=drying313 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
`check build assemblePack` a réussi en **3 min 9 s**, avec **112 tests serveur
obligatoires**. L'audit strict à beta.312 vérifie les ajouts du séchoir et les
modifications prévues de registre, rendu, JEI, langues et crédits. Les
**1 108 PNG** et les 195 JSON culinaires d'appareils existants sont inchangés ;
13 JSON de modèles/recettes/découverte sont ajoutés. Les 432 JSON de cuisine,
208 d'appareils et 376 d'agriculture sont comparés à leurs générateurs.
Le tag de minage à la hache garde ses entrées historiques. Les classes
culinaires compilées et les copies client/serveur du JAR sont vérifiées.
Le modèle de pack de ressources emporte les huit nouveaux JSON visuels.
[Reçu de vérification](drying-rack-beta313-verification.json). JAR local :
`mods/sanctuary/build/libs/sanctuary-beta.313.jar`
SHA-256 :
`54ea931d2f54eec8dac2d03e5ad1df8fd5b389db235a536bdd98d90a531fbe55`
Aucun push, tag, publication, canal packwiz public, installation Prism ou
serveur personnel n'a été modifié.
+40
View File
@@ -0,0 +1,40 @@
{
"version": "beta.316",
"status": "local-only",
"date": "2026-10-30",
"jar": {
"path": "mods/sanctuary/build/libs/sanctuary-beta.316.jar",
"sha256": "27ec0a8f492517131766a8c026cd0406189db3e78595a59ab605bbacd4303889",
"bytes": 19070080
},
"textures_unchanged_from_beta315": 1108,
"server_required_tests": 85,
"build": "check build assemblePack",
"build_seconds": 300,
"client": {
"backend": "Vulkan",
"device": "Apple M1",
"sodium": "0.9.2+mc26.3",
"preset": "sanctuary:sanctuary",
"profile": "231 Medium",
"seed": 316,
"natural_plants": 66,
"types": 16,
"models": 24,
"locales": [
"fr_fr",
"en_us"
],
"reload_preserved": 65,
"harvested_stays_empty": true,
"seconds": 140,
"scope": "starter island native generation, native survival packets, models and disk reload; wild fruit tree plans validated separately by server tests on seeds 0/42/316"
},
"wiki": {
"islands": 9,
"crops_and_products": 28,
"biome_aliases": 111,
"missing_images": 0
},
"deployment": "none; server and Prism remain beta.315"
}
+146
View File
@@ -0,0 +1,146 @@
# IA-09 — Découverte alimentaire en survie, beta.316
Demande du 30 octobre 2026. Même branche `codex/itsalive-26-3`, socle public
`beta.315` / branche `bb61547b`. Préparation locale : pas de publication,
d'installation personnelle ou de modification de la Beta dédiée par ce ticket.
## Résultat
La première semence ne nécessite plus le créatif. Les 20 cultures importées,
le café, la vanille, les citronniers et les oliviers ont une voie de découverte
renouvelable en survie. Le blé, la carotte, la pomme de terre et la betterave
disposent aussi de plantes sauvages sur l'île tempérée.
Le [guide des récoltes](wiki/alimentation/README.md), ses neuf pages d'îles
et sa [planche illustrée](wiki/alimentation/planche.html) viennent du même
catalogue que le jeu : `scripts/data/foraging.json`. Ils décrivent les habitats,
les gestes et les semences à rapporter, ainsi que les ressources vanilla.
Ils restent des pages locales ; aucun wiki externe n'est modifié.
## Répartition
| Habitat | Principales zones | Récoltes |
| --- | --- | --- |
| Tempéré | Sanctuary ; certaines prairies du Nord-Ouest | Blé, carotte, pomme de terre, betterave, fraise, tomate, maïs, concombre, poivron, laitue, oignon, ail, chou, haricot, basilic, menthe |
| Frais | Vallées et clairières du Nord, prairies/taïgas du Nord-Ouest | Blé, carotte, pomme de terre, betterave, fraise, laitue, oignon, ail, chou, menthe |
| Frais humide | Marais et jardin pâle du Nord-Est | Concombre, laitue, chou, haricot, menthe |
| Chaud sec | Bosquets/berges du Sud, savanes et sols vivants du Sud-Ouest | Maïs, raisin, pois chiche, piment, coriandre, olive |
| Doux ensoleillé | Oasis/berges vivantes de l'Ouest | Tomate, maïs, poivron, aubergine, raisin, basilic, citron, olive |
| Tropical | Jungles de l'Est et du Sud-Est, mangroves | Ananas, riz, soja, piment, café, vanille |
La règle lit le **biome réel à la position**, avec des tags explicites pour
les biomes vanilla et les alias Sanctuary versionnés. Elle ne déduit pas
le climat de coordonnées cardinales : les expansions ne sont pas fixées
à des coordonnées universelles. Le bambou tempéré de l'île principale
reste dans le groupe tempéré ; les grottes de roche nue, zones de soufre, cratères, pics de glace et
biomes de vide ne sont pas des lieux de cueillette agricole.
La découverte suit le climat. La culture après transplantation conserve les
règles de beta.300/301 : lumière et sol vanilla, riz dans une source sur boue,
vanille appuyée contre les troncs. Aucun nouveau verrou climatique de croissance.
## Gestes et renouvellement
- Casser une plante sauvage donne une unité de récolte et une semence distincte,
ou une seule unité si le produit se replante lui-même. Ananas : fruit + couronne.
Aucun labourage automatique du terrain naturel.
- Caféier sauvage mûr : clic droit pour les cerises ; leur recette donne les
grains verts replantables. Le casser donne aussi un grain.
- Arbre fruitier : clic droit sur les feuilles portant un fruit. Les feuilles
cassées/décomposées peuvent donner une pousse, selon les règles déjà portées.
- Vanille mûre sur un tronc : clic droit pour les gousses, casse pour une bouture.
- Dans un terrain déjà exploré, les petites herbes et fougères cassées sans
cisailles ni Toucher de soie donnent parfois une récolte du climat : **1/8**,
puis tirage uniforme dans la liste admissible. La recette de récupération
de semences ou la plantation directe permet de la multiplier.
- Riz : herbes marines simples de jungle/mangrove, **1/8**. Pas les algues de
pleine mer ; le riz sauvage visible se trouve sur les berges proches d'eau.
- Bouture de vanille : lianes vanilla cassées en jungle/mangrove, **1/8**.
- Pousses fruitières : feuilles de chêne/acacia dans les habitats chauds
correspondants, **1/40**, puis choix parmi les pousses autorisées.
Les tables natives sont **étendues**, sans remplacement de leurs graines,
pommes, bâtons ou pousses. Les variantes à deux blocs ne reçoivent aucun butin
ajouté, pour éviter le double paiement des moitiés. Cisailles et Toucher de soie
conservent la végétation sans créer simultanément une récolte. Les remplacements
de tables par un datapack restent prioritaires. Fortune n'augmente pas les
quantités de ces nouvelles trouvailles.
## Contrat de génération et sauvegardes
**Décoration agricole IA-09 / révision 316**, graine du parcours natif **316**.
Le relief, les identifiants des profils, les biomes et les journaux des expansions
ne changent pas. La décoration s'ajoute à la fin de `applyBiomeDecoration`, pour
le générateur Sanctuary qui possède les neuf îles. Elle ne s'applique pas aux
mondes vanilla ou au monde plat de test.
Huit candidats déterministes par chunk, sel `0x316F00D`, graine du monde et
coordonnées du chunk. Lecture de surfaces déjà présentes ; aucun sol, bassin,
source d'eau ou couche de neige n'est créé. Petites herbes/fougères remplaçables,
autres végétaux, roches, fluides, constructions et réservations conservés.
Les petits arbres fruitiers sauvages utilisent les bois vanilla et un volume
entièrement contenu dans leur chunk ; leurs pousses plantées conservent les
arbres natifs de beta.300. La vanille ne se pose que contre un tronc accepté.
**Aucun ancien chunk n'est repassé en décoration**. Les chunks sauvegardés
gardent leurs blocs. Aucun chargement, tick de repeuplement, changement de
format de sauvegarde ou activation d'expansion n'est ajouté. Un monde existant
peut découvrir les semences par les nouveaux butins puis les cultiver ; les
nouveaux chunks obtiennent la décoration agricole à leur première génération.
Cette évolution additive peut donc être visible à une ancienne frontière
d'exploration, sans retouche rétroactive.
## Vérification
Le contrôle final `check build assemblePack` valide **85 GameTests serveur requis**,
dont huit scénarios IA-09 : toutes les cultures, les climats, les terrasses
vivantes du preset public, les premières propagules, les outils natifs,
la plantation en survie, les obstacles et les deux arbres sauvages (graines
0, 42 et 316). Log : `build/foraging316-build.log`.
Le parcours client final réussit sous **Vulkan / Apple M1 / Sodium 0.9.2+mc26.3**,
avec le preset public `sanctuary:sanctuary`, profil **231 Medium**, graine **316**,
dans un monde de développement neuf :
- **66 plantes sauvages**, **16 types**, relevés dans 25 chunks autour du départ ;
- vraie casse en survie, attente de l'état serveur et contrôle des objets attendus,
au sol ou ramassés dans l'inventaire ;
- **24 modèles sauvages** réellement chargés, quatre faces pour deux plans en croix,
sans sprite manquant ; noms FR/EN vérifiés ;
- fermeture et réouverture sur disque : **65 plantes conservées**, emplacement
récolté toujours vide, aucun repeuplement au chargement.
Log : `build/foraging316-client-final.log`, réussite en **2 min 20 s**.
Les captures restent ignorées dans `build/foraging316-screenshots/` ; le contrôle
valide le chargement des modèles et les interactions, pas une visite esthétique
exhaustive des huit expansions.
La planche est vérifiée dans le navigateur : **9 îles, 28 lignes de produits,
aucune image manquante ni débordement horizontal** au format du panneau.
Ses 111 alias de biomes et les 80 ressources ajoutées sont issus du catalogue.
Les commandes reproductibles sont :
```sh
python3 scripts/build_foraging_wiki.py
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=foraging316,agriculture300,vanilla301,sausage315,cheesewine314,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryForaging316ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=foraging316 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Le module Test sert au lanceur de contrôle ; les classes de génération et les
ressources agricoles sont dans le JAR Sanctuary de production. Aucun test
OpenGL, déploiement, visite d'un monde personnel ou activation d'expansion sur
le serveur existant n'est effectué. Le serveur et Prism restent en beta.315.
Les durées/quantités ci-dessus décrivent le parcours local, pas une garantie
uniforme du nombre de plantes de chaque île.
Assemblage final réussi en **5 min**, 161 tâches. JAR client/serveur et
ressources générées identiques ; les **1108 PNG** restent strictement
identiques à beta.315. [Reçu binaire et vérifications](foraging-beta316-verification.json).
SHA-256 du JAR beta.316 : `27ec0a8f492517131766a8c026cd0406189db3e78595a59ab605bbacd4303889`.
+295
View File
@@ -0,0 +1,295 @@
# IA — Refonte d'It's Alive ! pour Minecraft 26.3
**Livraison du 29 octobre 2026 :** [beta.315 publiée, serveur/site/Prism mis à jour](release-beta315.md). Les mentions « locale » ci-dessous conservent l'historique du développement avant cette publication.
Demande du créateur du **28 octobre 2026**, branche locale
`codex/itsalive-26-3`, socle `77fdabfb` (dernière livraison beta.298).
Le checkout historique 26.2 et les changements locaux du checkout principal
restent intacts. Aucun push, tag, canal, serveur, site ou Prism mis à jour.
Le premier lot retenu par le créateur est **textures et ressources d'abord**.
Le même jour, il demande ensuite le port jouable des cultures, arbres
fruitiers et caféier : [IA-02, préparation beta.300](agriculture-beta300.md).
Les statuts ci-dessous distinguent le port agricole des prochains tickets ;
leur inscription seule ne vaut pas implémentation. Les incréments continueront avec le compteur normal
`beta.xxx`, sans réserver ni publier à l'avance une série de versions 300.
L'intégration reste dans Sanctuary, conformément au
[cadrage de fusion](multiblocs-itsalive-contrat.md).
## IA-01 — Import graphique, beta.299 locale
**Import réalisé.** Les textures sont copiées dans
`mods/sanctuary/src/main/resources/assets/sanctuary/textures/`, avec les noms
de fichiers historiques, sans conversion, redimensionnement ou retouche.
Aucune ressource Sanctuary préexistante n'est écrasée. Le namespace de
destination est celui de Sanctuary ; aucun identifiant de registre n'est
ajouté par la présence d'un PNG.
| Ressources | Nombre | Contenu |
| --- | ---: | --- |
| Textures de blocs | 212 | Croissance, feuillages, pousses, appareils, blocs d'ingrédients |
| Textures d'objets | 129 | Semences, récoltes, ingrédients et plats |
| Sprites d'interface | 2 | Rail et tête du moulin historique |
| **Total** | **343** | Cuisine et agriculture uniquement |
Les **38 PNG fournis dans `food/`** sont prioritaires sur leurs homologues
26.2 et conservés octet pour octet, transparence comprise. Ils existent tous
déjà sous le même nom dans l'historique : ce sont de nouvelles versions des
dessins, pas 38 nouveaux objets. Comparaison avec la 26.2 :
- **32** modifient les pixels ;
- **2**, `corn.png` et `lettuce.png`, modifient seulement l'encodage PNG ;
- **4** sont strictement identiques : `bell_pepper.png`, `eggplant.png`,
`strawberry_seeds.png` et `tomato.png`.
Les **305 autres PNG** proviennent sans modification du module historique
`26.2/itsalive`, commit `26f2c2a0061619df1f146f9081d6740c560a7497`.
Le [reçu complet](itsalive-textures-beta299.json) conserve pour chaque texture
son origine, son SHA-256 final, le SHA-256 26.2 et ses dimensions. Les monstres,
œufs, archives ZIP, fichiers système et textures Canaplia restent hors de
ce lot culinaire. Les portages déjà présents dans Sanctuary sont conservés.
Les textures comprennent les huit stades des **20 cultures historiques** :
fraise, tomate, ananas, maïs, concombre, poivron, aubergine, laitue, oignon,
ail, chou, raisin, riz, soja, haricot, pois chiche, piment, basilic, menthe
et coriandre. S'ajoutent les parties hautes de la tomate, du maïs, du
concombre et du haricot, les quatre stades du caféier et de la vanille,
ainsi que les pousses et feuillages avec/sans fruits du citronnier et de
l'olivier. Les deux arbres historiques réutilisent les bois vanilla :
aucune nouvelle famille de bois n'est sous-entendue.
Les textures sont présentes dans les ressources du mod, mais **aucun de ces
nouveaux objets, blocs, arbres, appareils ou recettes n'est encore enregistré
en jeu**. Le Java, les recettes, les modèles et les règles de génération de
26.2 ne sont pas copiés automatiquement. Les images seules ne suffisent pas
à porter les comportements ni à valider leur compatibilité 26.3.
## Évolution beta.307 — recette du moulin manuel
[La redstone de la recette cède la place à un bâton](mill-recipe-beta307.md)
pour la poignée. Les dalles de pierre lisse, le cuivre et le tailleur de pierre
restent la base de l'appareil. La recette rejoint Divers dans le livre natif ;
son identifiant et le fonctionnement manuel de beta.306 restent stables.
## Évolution beta.306 — moulin manuel
[Le moulin devient un poste manuel comme le tailleur de pierre](mill-beta306.md),
sans combustible ni attente. Le résultat est un aperçu : seule sa récupération
paie un ingrédient. Le nom devient Moulin / Mill ; le menu conserve le
mouvement mécanique et retire le feu. Les neuf recettes et les identifiants
restent stables ; les anciens objets stockés sont récupérables.
## Évolution beta.305 — écrasement direct
À la demande du créateur, la presse alimentaire est retirée du jeu courant.
[Les blocs de fruits s'écrasent directement en huit paliers](crushing-beta305.md),
avec résidus progressifs, piston immédiat et super piston sur 27 blocs.
Les identifiants de fruits restent stables ; l'ancien ID de presse n'est gardé
que pour récupérer un inventaire d'essai local, sans traitement ni catalogue.
## IA-09 — Découverte des premières semences en survie
[Répartition climatique, récoltes sauvages et guide des neuf îles](foraging-beta316.md),
préparation locale beta.316 du 30 octobre 2026. Ce lot remplace le report de
la découverte naturelle annoncé dans IA-02/IA-03, sans repeupler les chunks sauvegardés.
## IA-10 — Extension du séchoir, beta.317 locale
[15 recettes de séchage, 12 nouveaux produits et propositions culinaires](drying-foods-beta317.md).
Les quatre crochets et les sauvegardes gardent leur fonctionnement ; la vanille
se sèche désormais sur le cadre et les herbes/piments secs servent aussi en cuisine.
## IA-11 — Les 18 recettes aux produits séchés, beta.318 locale
[Implémentation et vérifications](dried-cuisine-beta318.md). Les propositions de
beta.317 deviennent des recettes jouables avec les appareils prévus ; le saucisson
pimenté garde les gestes de préparation, séchage et partage en tranches. Les plats
épicés déclenchent de petits coups cosmétiques sans perte de vie.
## Suivi des mécaniques
### IA-02 — Cultures, arbres fruitiers et caféier jouables
**Port implémenté dans la préparation locale beta.300**, sur cette même
branche. [Comportements, accès et vérifications](agriculture-beta300.md).
Le texte ci-dessous conserve le périmètre du ticket.
Porter les 20 cultures, leurs propagules, les arbres et le caféier vers les
API 26.3. Reprendre explicitement les récoltes pérennes et annuelles, les
parties hautes et la plantation particulière du riz, après examen des règles
historiques. Ajouter les noms et aides FR/EN. Tester plantation, croissance,
récolte, casse, rechargement et absence de duplication côté serveur.
La répartition climatique et l'apparition naturelle feront l'objet d'un
contrat de génération séparé, documenté avec graine et version. Aucune
injection rétrospective dans les chunks existants n'est autorisée par ce lot.
### IA-03 — Vanille grimpante
**Implémentée dans la préparation locale beta.301**, sur la même branche :
[attachement aux troncs, croissance ascendante et vérifications](vanilla-beta301.md).
[Correction graphique beta.302 : quatre stades en 16 × 16](vanilla-16px-beta302.md).
Le texte ci-dessous conserve le périmètre initial du ticket.
Obtenir une plante plaquée contre son support, visuellement comparable aux
lianes, qui **progresse vers le haut** plutôt que de former un grand totem.
L'ancien `VanillaVineBlock` possède déjà une méthode `growUpward` et exige
une bûche de jungle ; il faut donc examiner aussi le modèle, les faces et les
supports, sans présenter un simple changement de direction comme une refonte.
Vérifier la pose, le support de chaque segment, la croissance supérieure,
les obstacles, la récolte, la casse du support et la poudre d'os. Choisir les
supports admis et la forme exacte dans ce ticket. Les quatre anciens PNG
sont conservés comme références, sans promettre qu'ils suffiront au modèle final.
### IA-04 — Cuisiner avec le chaudron
**Suite locale beta.312 :** [le premier geste mémorise la recette, les lots
se répètent et le réapprovisionnement garde le travail en cours](cauldron-repeat-beta312.md).
La sortie s’accumule, sa collecte conserve la série et Maj-clic à main vide
permet de l’arrêter.
**Refonte locale beta.311 :** [préparation libre, démarrage au bâton, aucun
plat raté au chaudron, récipients payés en fin de cuisson et sons natifs](cauldron-beta311.md).
Le geste manuel est confirmé par le créateur ; les 19 recettes restent les
recettes portées, avec une seule préparation lancée par geste.
**Port jouable en beta.304 locale :** [chaudron vanilla, eau exacte, chauffe, lots et récipients](kitchen-beta304.md).
Repartir des usages du chaudron vanilla pour l'eau, la chauffe, les ingrédients
et les récipients, puis définir comment préparer et servir un plat. Examiner
l'ancien `CookingCauldronBlock` avant de choisir l'extension du bloc vanilla
ou un appareil spécialisé. Les gestes, niveaux d'eau, portions et retours de
bols/seaux doivent former une boucle vérifiable, sans pertes ni duplication.
### IA-05 — Plats ratés et nouvelles recettes
**Suite beta.304 locale :** [appareils et plats ratés historiques portés](kitchen-beta304.md). La fermentation est désormais temporisée dans le tonneau et le pressage exige le pressoir.
**Recettes portées en beta.303 locale à la demande du créateur :**
[92 recettes alimentaires sur table et four/fumoir](cuisine-beta303.md),
avec les ingrédients intermédiaires. La fabrication manuelle instantanée de
fermentation de cette étape est remplacée par le tonneau en beta.304. Les plats
ratés historiques sont également portés dans cette suite.
Le texte ci-dessous conserve le cadrage initial ; la demande d'import du
28 octobre autorise le port du catalogue avec les adaptations documentées.
Revoir les « fake dishes » cités dans la demande. L'historique contient
`failed_dish`, produit par la poêle et le chaudron : c'est la correspondance
probable, à confirmer au cadrage de ce ticket. Définir ce qui constitue une
recette, un mélange incomplet et un échec, puis les ingrédients consommés et
le résultat obtenu. Ne pas importer en bloc l'ancien catalogue de recettes
avant cette décision. Conserver les idées de nouveaux plats dans ce même suivi.
### IA-06 — Saucisson et séchoir
**Implémentation locale beta.313 :** [préparation à l’établi, quatre crochets
visibles, cinq minutes de jeu, protection de la pluie et conservation des
objets/progressions](drying-rack-beta313.md). Les modèles utilisent les
textures natives ; le fonctionnement ci-dessous décrit le cadrage initial.
Créer une préparation qui devient du saucisson après séchage dans un séchoir.
Définir la recette initiale, la durée, les conditions et les gestes de pose
et de récupération. Vérifier interruption, reprise et rechargement, avec
progression autoritaire côté serveur et résultat unique. Le séchoir et le
saucisson n'ont pas de nouvelle texture fournie dans `food/` ; leur dessin
et leur fonctionnement restent à réaliser.
### IA-07 — Poêle native et pose souple
**Refonte locale beta.310 :** [une recette commune par pile de une à quatre
poêles, pose en ligne, paiement réel, réserves natives et continuité](pan-stacks-beta310.md).
Le choix confirmé concerne les piles ; les orientations supplémentaires et
les ancrages particuliers restent ouverts.
**Port de la poêle en beta.304 locale :** [bloc entité, contenu visible, cuisson, retraits et sauvegarde](kitchen-beta304.md). La forme historique est conservée ; la refonte de pose/orientation reste ouverte.
Revoir les bugs, la pose et l'intégration de la poêle, avec la souplesse de
placement des coussins comme référence exprimée par le créateur. L'ancienne
`FryingPanBlock` étend déjà `BaseEntityBlock` et possède une
`FryingPanBlockEntity` : l'absence de bloc entité n'est donc pas un diagnostic
établi. Reproduire les défauts puis choisir le placement, l'orientation et
l'ancrage adéquats pour 26.3.
Vérifier les surfaces prévues, le contenu visible, la source de chaleur,
l'ajout/retrait d'ingrédients, la casse et le rechargement. Un ingrédient ou
plat doit toujours être récupéré une seule fois. Ne pas reprendre l'ancien
modèle et son placement comme s'ils satisfaisaient déjà cette demande.
### IA-08 — Cuisinière : four et table de fabrication
**Précision beta.309 :** [une seule sortie, sans aperçu supplémentaire,
grille manuelle et extraction des plats par entonnoir](oven-single-output-beta309.md).
**Refonte locale beta.308 :** [cuisson automatique, aperçu, paiement en fin de
cycle, annulation des modifications et poursuite avec le menu fermé](oven-beta308.md).
Le créateur a retenu le démarrage comme un four et les recettes de cuisine
uniquement. La fabrication de l'appareil garde sa table et son four.
**Port culinaire en beta.304 locale :** [grille 3 × 3, combustible, résultat temporisé et recette table + four + briques + fer](kitchen-beta304.md). Le Fourneau multibloc existant est conservé.
Reprendre la cuisinière comme un assemblage cohérent du four et de la table
de fabrication. Examiner ses usages historiques et le Fourneau multibloc
déjà présent pour éviter deux appareils redondants. Définir ensuite les
gestes et une recette plus élaborée, mais lisible dans Minecraft.
La table de fabrication et les quatre pierres évoquées sont une **piste**,
pas une recette arrêtée. Le remplacement par le Fourneau reste à décider
dans ce ticket. Aucun inventaire historique ni travail de cuisson en cours
n'est converti sans contrat explicite.
## Vérifications du premier lot
Le contrôle permanent des 343 empreintes et dimensions est intégré à
`./gradlew check` via `verifyItsAliveTextures`. Il peut aussi comparer les
fichiers originaux et le JAR construit :
```sh
python3 scripts/verify_itsalive_textures.py
python3 scripts/verify_itsalive_textures.py --jar mods/sanctuary/build/libs/sanctuary-beta.299.jar
./gradlew check build assemblePack -PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true
```
Import : les 343 PNG ont été décodés intégralement ; comparaison des pixels
des 38 images fournies, contrôle visuel de leur planche avant/après et
vérification des fichiers originaux réussis.
- `check build assemblePack` avec le profil ci-dessus : **réussi en 3 min 2 s**,
158 tâches, **13 GameTests serveur requis réussis** et contrôles du dépôt.
Le profil cible les recettes et la progression existantes ; il ne teste
aucune culture nouvelle, puisqu'elles ne sont pas encore enregistrées.
- JAR final : les 343 textures correspondent exactement au reçu. Comparé
au JAR beta.298, aucune classe, recette, modèle ou dépendance embarquée
ne change. Seuls les 343 PNG, la version Fabric et les crédits (dont leur
copie dans le template) diffèrent ; aucune entrée n'est supprimée.
- Template de ressources et JAR rafraîchis après finalisation des crédits ;
profils packwiz client/serveur assemblés, versions et index vérifiés.
Le JAR du pack est strictement identique au JAR contrôlé.
- SHA-256 final de `sanctuary-beta.299.jar` :
`e219b13b2e211908bfff171615ad7c3f923d4e4c2df1bf031689930f3efd489b`.
- Logs et reçu local dans `build/itsalive-beta299-build.log`,
`build/itsalive-beta299-refresh.log` et
`build/itsalive-beta299-validation.json`, hors de Git.
Aucun essai graphique en jeu n'est revendiqué pour cet import sans contenu
enregistré. Les futurs essais graphiques cibleront Vulkan exclusivement.
Aucun monde personnel n'est ouvert ou modifié, aucun format de sauvegarde
ne change. Les versions mod/pack sont alignées en **beta.299**, préparée
localement ; la dernière release publiée reste beta.298.
### IA-09 — Fromage à partager et qualité du vin
Demande du 29 octobre 2026 : fromage posable et mangeable par parts comme un
gâteau ; quatre qualités de vin. Confirmation : le jeune vin reste prêt après
trois minutes de jeu, puis table/réserve/grand cru après un/trois/sept jours
réels de garde dans le tonneau, y compris jeu fermé.
[Implémentation et contrat local beta.314](cheese-wine-beta314.md).
Même branche locale, aucune publication.
### IA-10 — Saucisson posé en tranches
Demande du 29 octobre 2026 : poser le saucisson séché et manger ses tranches,
comme le fromage. [Implémentation locale beta.315](sausage-slices-beta315.md) :
le même objet de séchage devient posable, quatre tranches rendent huit points
de nourriture au total, quatre orientations et parts sauvegardées nativement.
Même branche locale, sans publication ou installation.
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
# Export modifiable It's Alive — beta.315
Demande du 29 octobre 2026 : fournir toutes les textures de pousse et tout le
contenu graphique ajouté depuis le début de la refonte, puis livrer le pack.
Le script `scripts/export_itsalive_textures.py` extrait les **347 PNG Sanctuary**
ajoutés depuis le socle beta.298, y compris les quatre nouveaux stades de
vanille grimpante. Il fournit **368 images modifiables** et **696 JSON visuels**
avec les matériaux vanilla réutilisés et les fonds/sprites d'interface.
L'export local est dans `Downloads/ItsAlive-Textures-beta.315/`, avec son ZIP.
`a-modifier/` est un pack de ressources au format 97.1, prêt pour Minecraft
26.3. `originaux/`, les notices et les inventaires CSV/JSON conservent les
références et SHA-256. Aucun dessin n'est réencodé ou redimensionné.
Les matériaux partagés par les modèles Minecraft sont recopiés sous
`sanctuary:itsalive_editable/<aliment>/...`, et les modèles de cet export sont
redirigés vers ces images. Fromage, saucissons cru/séché et séchoir se modifient
ainsi séparément, sans recolorer les blocs vanilla correspondants. Toutes les
portions d'un même aliment partagent ses matériaux. Chaque qualité de vin a
également son propre PNG, même si le jeu emploie l'icône héritée du vin jeune.
Les quatre images d'interface sous `assets/minecraft/` sont une exception
signalée dans l'inventaire : les écrans natifs partagent ces fonds/sprites ;
leur modification touche aussi ces écrans vanilla. Les 343 images d'import
initial, y compris les anciennes références conservées, restent présentes.
Le script vérifie les références, dimensions, copies, empreintes et intégrité
ZIP, refuse d'écraser un export existant et n'installe/active aucun pack.
Pour tester des dessins, copier `a-modifier/` dans les packs de ressources de
l'instance choisie, l'activer au-dessus de Textures Sanctuary et recharger.
Les règles de jeu et recettes restent celles du mod.
```sh
python3.14 scripts/export_itsalive_textures.py \
--output /chemin/neuf/ItsAlive-Textures-beta.315 \
--native-jar /chemin/du/client/Minecraft-26.3.jar
```
+297
View File
@@ -0,0 +1,297 @@
{
"source_commit": "26f2c2a0061619df1f146f9081d6740c560a7497",
"license": "GPL-3.0-or-later",
"java": [
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/CulinaryAppliance.java",
"sha256": "aabb476edecbe0c0f7accd936853a625681b924c47b7800995dfd5da0035358c",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/CulinaryAppliance.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/CulinaryExperience.java",
"sha256": "7f658276c4918af0ffac5f4bb9682b5844f520fa70f301999ad97fa115e32367",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/CulinaryExperience.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/CulinaryHeat.java",
"sha256": "b9294332154a1110d94f5110d693109e7c6c35ad6cdb5332c1af833d0c91a2fb",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/CulinaryHeat.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/CookingCauldronBlock.java",
"sha256": "334d22224387c7da4ce28bfd0e642bb5e7adb6697ddebd884e21df5d2a84ea27",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/CookingCauldronBlock.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java",
"sha256": "770364f134b8c9ee04be674b1dbe38956f5fa17c91a81ca258d7ea6a8d187dc5",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/FermentationBarrelBlock.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FryingPanBlock.java",
"sha256": "d6ec6cbf4ab52230bcdd6a64c97ceb8a4df33f2b2d717ce7346fe3c79370cffd",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/FryingPanBlock.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/KitchenOvenBlock.java",
"sha256": "d9cd55b9e002aab5c2d14d95d59ba46a7731a70d0bbb3d0959f1bf7e3eee1a19",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/KitchenOvenBlock.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java",
"sha256": "25ec974def75ab1891ddcd3eb150bbacf2a2b7e1c8b2ac30081698fe7afa94d7",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java",
"sha256": "99f217bc615aa414f41ffeeccc85d68655da48b794b82f8289fe86f93004c8a3",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java",
"sha256": "9c7f8145973f570f70e758650f99e174b5625b91e844bbcd1bf417eff1322951",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/entity/CookingCauldronBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java",
"sha256": "feb866fca3848c80e9d06aa9fa38e9f49b8a5b22aa9c7bb5b5f4837ad4abee9a",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/entity/FermentationBarrelBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FryingPanBlockEntity.java",
"sha256": "af102fc1c80600208778d6d2047781b63fa2ebaae2890b71acfc6dfa2af600fe",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/entity/FryingPanBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/KitchenOvenBlockEntity.java",
"sha256": "0f7c3f6612dee3dbb1b264be3e8a94c55e4f129155f351ea0eb8c3a1639ac463",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/CookingCauldronHud.java",
"sha256": "ec67f5180d01e5aa51abf229d1aa35aac325c50e93c3f2386ca194f95cf2e99a",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/CookingCauldronHud.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/FermentationBarrelHud.java",
"sha256": "ada21e2c3a268aa5b6b27bf9a16ffe42aeb1e20fd4305e07900e88a1a376d5e2",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/FermentationBarrelHud.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/FryingPanHud.java",
"sha256": "e37a353fde43eba54e4e60a98aa8a6ed87a06921f4040faca92dbd694d491304",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/FryingPanHud.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/renderer/FermentationBarrelBlockEntityRenderer.java",
"sha256": "adba55ed2154e1f0243bae3bf78247dac24fa79f9cc3de7fbb6e522f700f5109",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/renderer/FermentationBarrelBlockEntityRenderer.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/renderer/FryingPanBlockEntityRenderer.java",
"sha256": "13a30fee522d9b2efda13a763198b285f555bf3d011758780d27c19d9627249d",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/renderer/FryingPanBlockEntityRenderer.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/renderer/state/FermentationBarrelRenderState.java",
"sha256": "1adc65fe528cc9ea1da8e43350f8062c4bec0bb31cf03ee54914776acd9d9647",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/renderer/state/FermentationBarrelRenderState.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/renderer/state/FryingPanRenderState.java",
"sha256": "26eb76e9d6776d34ac9abb0769315d41f5a9574bf3415c2efd16b7b2c33c1b82",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/renderer/state/FryingPanRenderState.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/screen/KitchenOvenScreen.java",
"sha256": "f619f2b95a76193e21db4e2cbed4fa64ac2848aedaed042bee4fcfac54ea5842",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/screen/KitchenOvenScreen.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/client/screen/RedstoneMillScreen.java",
"sha256": "5ebc44972d34ea18b7982592b47a02ccd01fadd550bbe616e970b80cba1f9086",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/client/screen/RedstoneMillScreen.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryBatching.java",
"sha256": "9532bf15306e21e09da4e90f19563284c0d3889f8d4382ad46e23b6daf6274a1",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/recipe/CulinaryBatching.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java",
"sha256": "8cf3e2d74e24b9d83b8e9bdb36a216ba2745e1ab07bd2cbd55fb442b1a0f2824",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/recipe/CulinaryPartialMatching.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java",
"sha256": "57a51a52d4732fd8f7df38b02ea6bafecf3290c88f3146fe8c44892688736c11",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/recipe/CulinaryProcessRecipe.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryRecipeInput.java",
"sha256": "d733004754aa377752118d8dd965a9025daafff2174bab76678940c4cf9fd79a",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/recipe/CulinaryRecipeInput.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryRecipeTypes.java",
"sha256": "5227e6e3314502298ccba4cad95060b83604041eb0df53aac4769de1fe4fbe3c",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/culinary/recipe/CulinaryRecipeTypes.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java",
"sha256": "aeb6210329b66323de012616fcb8f4e360dcd990a1b930347a6fb20110c6eeda",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/machine/FoodPressBlock.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java",
"sha256": "1d0c93c1982fa1197fe9c56fd2a8e94c2109a29b2efc3bd5d6ab1e5e22a3d34e",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/machine/FoodPressBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressContent.java",
"sha256": "2aab91dd3b96d1fd2e759cb984bea6593e99a205ee68d0c7c00912329130e9e7",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/machine/FoodPressContent.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlock.java",
"sha256": "8dbd9e15609b4c07a27af0997b5c2e15f7bce49e32eedf3cc3e266f9685c09ef",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/machine/RedstoneMillBlock.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java",
"sha256": "31c750f9727430d00a80e1a65558ecc0471300bed5f9fbd4716a500d5d2274aa",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/machine/RedstoneMillBlockEntity.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/menu/KitchenOvenMenu.java",
"sha256": "4f222eb436ea88785e82c927682588530b44db7b5f86dca341c8cfb88ddd19ba",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/menu/RedstoneMillMenu.java",
"sha256": "7ca7c4d77c48121f9c81fecacf9b09f14964e7ac33a65d0c0688852a73425a2a",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/menu/RedstoneMillMenu.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/recipe/FoodPressingRecipe.java",
"sha256": "fbc8cf4b452e54662c4fc469aa16e14880f86110339d9388b5dba27469ba5c5d",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/recipe/FoodPressingRecipe.java"
},
{
"source": "src/main/java/fr/koka99cab/sanctuary26/itsalive/recipe/RedstoneMillingRecipe.java",
"sha256": "a0d011d89f46f38e8fa384299f73a7cab44fd3189600e041fb9aa20ea2ff612d",
"destination": "mods/sanctuary/src/main/java/fr/koka/sanctuary/cuisine/recipe/RedstoneMillingRecipe.java"
}
],
"assets": {
"src/main/resources/assets/itsalive/blockstates/olive_block.json": "998c5fa53bb09b44ce0cff864ca0a2ea7717b79efbd08637d820449a99e7ed69",
"src/main/resources/assets/itsalive/blockstates/food_press.json": "1f15baab1f3b678b2e3650b9c6af4bcc3404b9af0a3ade61eaf3a66049469f91",
"src/main/resources/assets/itsalive/blockstates/grape_block.json": "ed05b5c4e237b61ca0716a90bd7c57bcfa60aa2e40230481d86345862dd03622",
"src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json": "47db1631c9bad0b690f04e7cd78f1c1d219ad1a2c4aff7d535dea8c6ae7e06a1",
"src/main/resources/assets/itsalive/blockstates/cooking_cauldron.json": "c79479d63942c3ae6ad360996c0e3e020575565dbc69c6e5c7f110c7225535a5",
"src/main/resources/assets/itsalive/blockstates/frying_pan.json": "c2de84cf795042a16faad51e61d4afe900571227cd0585624e8a994d2deaffdf",
"src/main/resources/assets/itsalive/blockstates/redstone_mill.json": "9cf9223ea7fb6fa2fa8df8a3fec63a60cc5849425b32ab3aa97c08b5a106eeb0",
"src/main/resources/assets/itsalive/blockstates/kitchen_oven.json": "d9ef62ee1e5fa7abec636166528628fc265de2241a0f3234e919b2bea5bbe0a8",
"src/main/resources/assets/itsalive/blockstates/tomato_block.json": "2acb0a87767691a0247788bdbfd744d7d06c03bf817e4dcaea48d6f86e3a6f6d",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_7.json": "773e1389d1dfd67e9bfa1eb0dc21052e126aeb3bee29a7ec4be886867b95e17f",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_3.json": "b7becfd2e2691d6b36d2a3cf2a315c96fa6db87709e0d2741e535da17f10051e",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_7.json": "3eac8754b3426624da4be9f67d201619231491702611a2f037490c8b48a18e6d",
"src/main/resources/assets/itsalive/models/block/olive_block.json": "de9fa7bb7246ff9aec219f9ab9de43a219c9d6ee684bfc7fae66f6038c3fb983",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_6.json": "5d864c423dd19ac779634899650c53562a22bb1ab175b93c28c8987329a22660",
"src/main/resources/assets/itsalive/models/block/food_press.json": "615b598729b5462a7e4cc654f79ebce901b6d4e82d89b1895b03945f30ca2d35",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_2.json": "23d6ec4aa8e21eb63561b8132d441fb76a5f6dce152a3ee5ce2289c077d4023f",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive.json": "b8df7bb819163a91ab6f7911743126c1583f8aa453b0b396cd6f6ce2abd3b668",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_6.json": "869b096b5ae751bb6020ed3ef42ff998b28f79d8f44fac0a05c40561db57b4a5",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_5.json": "0b803d06608cfa324bb6e35ac301a59ec86b701e96f2fff9a585835924ddc0e4",
"src/main/resources/assets/itsalive/models/block/grape_block.json": "195114f18de907448c9797aa539c4c5792c48efd17fff0f0a2779358eafcee73",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_1.json": "9376fecd6122b37ff4c6aebfe09ed798326fa932a9c28f41b83cc6dd28d0b89a",
"src/main/resources/assets/itsalive/models/block/redstone_mill_on.json": "50009556a95a7db5bdce15622208a2931ed4a2a6e1fd3457c0be2754b495b78c",
"src/main/resources/assets/itsalive/models/block/fermentation_barrel.json": "0ccb53d3f86eb2b7b4932e29149a5e210b54324c07c517c0aa79d73e6417a8dd",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_1.json": "7c5d3b41782ad57688a1209e83d15e58d145980423acc44d93e29baec0c3a961",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_9.json": "dfbbc138ee4c0851acc220ab7c1fc63498b12e39f1eb3a75c3c2d2b582b1574d",
"src/main/resources/assets/itsalive/models/block/kitchen_oven_on.json": "326567713bae7ed35dad107838b81b1e2b00b9a3a67a644782eb0a1323d2fa1e",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_8.json": "ee4b305779c34e5743728ef78dd964d3aa8302526a2bbb4ffe05c6efaa29c8f5",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_4.json": "e7f95e5ab13f1b98c18fbd2563925bc9c9adbd793d129c7752fe5d2b66c2a33c",
"src/main/resources/assets/itsalive/models/block/cooking_cauldron.json": "f9fcfdbb09a63ee6437b5494009a3d3f727291cf356f502a1e8a5aa30f972968",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_3.json": "7fd398a8089e5fa5f7d0b12f2799c6e960d03abefd6e186d5ff7f11e7f50a3f6",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_7.json": "c1042823e4fa94c5635e12f446a6bf7eb48541edce0bfd1661aa441a46d74c23",
"src/main/resources/assets/itsalive/models/block/frying_pan.json": "9f9b5b1c7ae19b350e1d4c8f973b9987c8efb0a0f218fcf31d6d035957e1d88c",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_3.json": "eb3189f1fa68a3f9cda9f801c83ec55fc1c7a81c4369c139e1218e88d38b62dd",
"src/main/resources/assets/itsalive/models/block/redstone_mill.json": "3d0be0d018982089aa3b9214abd1cc2c1b89ccc6f021f5eef57560feb1cc9a0f",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_2.json": "7cfe14676d665c671cebc443d4d5e17db441526ff76db1a2740abdebe77259a4",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape.json": "e5b07ea4d0c7c0303c2a38bae068a1dff16293df894a59d09b3cf57fdbe434a7",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_6.json": "8d6019eb3a441d91a34e6ceeb6e7d482431e48f6d42536b015fc42281e1f6764",
"src/main/resources/assets/itsalive/models/block/kitchen_oven.json": "e9b8327dd6022313935edee4c8daa1af0a0071c7d8436f96fa94c46de9eee14d",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_2.json": "7db27db21ebedce3f5cf5a3801ec9ecb908ca590a3e23e2375243e8195988c95",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_9.json": "a29d412b4e576d37962194aa61b1d676399741500d9e29d171536ef352cc2079",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_5.json": "d2627140a4f52fdc59e7773e0b5f524ed91e141b7026805a78f21009fbff73ed",
"src/main/resources/assets/itsalive/models/block/cooking_cauldron_coffee.json": "818a1e10296a651845621cf9c44647fe5fac1b37ba358c88dde445714dc98119",
"src/main/resources/assets/itsalive/models/block/tomato_block.json": "bec799c63bd80b6c69a3ddff317d688c717fb89cb25d43783194f9f2426ff2d3",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_5.json": "953f2e19c4997927d524aa5e768fd02b36170baa43ffa52572a889fefe431417",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato_1.json": "3a6f5d0cf5747a3441ed8171ea845240bd272f46a1ffe634a2058699b1d661aa",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_9.json": "767e70ea72263ffbb6f1812cf84ec90bcd2001254e5ae3e88c2280fd9fdef62d",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_8.json": "9d1d33f89591e75353fcbb083164328e0961c2d97f678724813d3e5015334397",
"src/main/resources/assets/itsalive/models/block/food_press_fill_tomato.json": "a590f64f1042ca751cd81717cc184d53a761c5daf2667004b1a0a6d8e0bc5b21",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_4.json": "5730a951afc7bf294a082908e9cdb6617c48159fed64d2d79f400cf65c9769a9",
"src/main/resources/assets/itsalive/models/block/food_press_fill_grape_4.json": "140e7b9e9703c56f897a469e70e6c082ce5697e0c1a13210184c2980e8e0d325",
"src/main/resources/assets/itsalive/models/block/food_press_fill_olive_8.json": "a751e6738d5a4ae00f2a9c7b9d0035ecf6bb5c989a1733e5fc3471ba742330dc",
"src/main/resources/assets/itsalive/models/item/olive_block.json": "84b2fad0a5e2ce33e1798786a79a436246a75836acd98828e52f1bd393193314",
"src/main/resources/assets/itsalive/models/item/food_press.json": "a4a57208fd428939b7b3d042335dcec6c45f8e9de190c9cabd8bbc831a011ce1",
"src/main/resources/assets/itsalive/models/item/grape_block.json": "92d78de7a02a42fbe015e7aee1338052ff29701d37fe6c7906d6de611c4508f1",
"src/main/resources/assets/itsalive/models/item/fermentation_barrel.json": "c9e3db73daee99a2a3efca454541e8dd5f2f6d9e1c40b3d0eb5f222914082533",
"src/main/resources/assets/itsalive/models/item/frying_pan.json": "5477410d6dd022b72ea0ebe7306dd0bf86ba6bf6357bec67ec265c654801d3d8",
"src/main/resources/assets/itsalive/models/item/redstone_mill.json": "b19fb1874697283774f774ca1cb57c817a55ce55b6ae3ed032d61d2b949e6151",
"src/main/resources/assets/itsalive/models/item/kitchen_oven.json": "f9dd37260005e466276b4c9002b8093f8446df854369bb7b0310417ad50881ed",
"src/main/resources/assets/itsalive/models/item/tomato_block.json": "5956b5a5a866fade827ad6088dda8cd6db341d702896cea1c425809abe424c9b",
"src/main/resources/assets/itsalive/items/olive_block.json": "a88c83d92ff0b115dc6bd8c37fb1a46a93d1722520989c62b39f3809279f9987",
"src/main/resources/assets/itsalive/items/food_press.json": "5fcd04addfe5cf076340f639c78cf63c77bc04988357328dd2877695b09f89dc",
"src/main/resources/assets/itsalive/items/grape_block.json": "068e0859a395f620d10ae8df4c42871ffe0e70b94d2a383adb61d717843edbb1",
"src/main/resources/assets/itsalive/items/fermentation_barrel.json": "dcc8d9be395cb93c4545481e675f8159e48facca2eccdc8f34de6a012e5ff719",
"src/main/resources/assets/itsalive/items/frying_pan.json": "de51f0130cb1107c4d57db273ba581519ed35fef1c911f80793746cac2b11717",
"src/main/resources/assets/itsalive/items/redstone_mill.json": "3c22f1b4e085d4c0742caae943a8cbf90ce6b5523cb27bcf346ca262a8726f6b",
"src/main/resources/assets/itsalive/items/kitchen_oven.json": "a20e4385c41fcc3045fcc4ce11d4749980860a5221458df2ca550e7535169610",
"src/main/resources/assets/itsalive/items/tomato_block.json": "bd7e4e2e8cf433d5bf6180134ae81539f1ee72f75f7f60b373cae98e37f9b839",
"src/main/resources/assets/itsalive/items/failed_dish.json": "f0331dab307785acc01ce34922ef0e8bc6751245f356c9d19f1442908894ae72",
"src/main/resources/assets/itsalive/models/item/failed_dish.json": "9ea5d1fa8a9de8b94fdc14436b0dd33da34a0051672aa42f759f091b3f6075e3"
},
"recipes": "scripts/data/cuisine.json; scripts/data/kitchen-crafting.json",
"crafting": {
"src/main/resources/data/itsalive/recipe/fermentation_barrel.json": "bce931d76ef4c465c757f16ad7ee0db71838a77b39d3540cec327b7bfd72cde9",
"src/main/resources/data/itsalive/recipe/food_press.json": "431bbd78c482085ebde27a5501db97d4c999bf6b91524fba22793e9f4085caa3",
"src/main/resources/data/itsalive/recipe/frying_pan.json": "e99e9a0b514642897fa8e82f12847fca618350d86359fd8786c6f1561c772cac",
"src/main/resources/data/itsalive/recipe/grape_block.json": "625b20deab5f4debfa1c08febf5f752e705ee015f2c796accf95e21945b0b89b",
"src/main/resources/data/itsalive/recipe/grapes_from_block.json": "870393e4a147f99440cf2742c8cd852b403caac85dcd9ac4bfef1ccbbfddec6c",
"src/main/resources/data/itsalive/recipe/kitchen_oven.json": "9f2ccb182f907f2d0932fb53b21226afb9c61faf280089703b6a04b6fe66f3be",
"src/main/resources/data/itsalive/recipe/olive_block.json": "35a5c6f87f4483837242598c58367d986c475f8fa44d6bca513383e4b58ee60c",
"src/main/resources/data/itsalive/recipe/olives_from_block.json": "23d7c22b40bda59e717b98254cfbf73c32c1cb8e03f9a605235c680b061122d0",
"src/main/resources/data/itsalive/recipe/redstone_mill.json": "cedd2025ff9961084f1e0818c2d45b4696e6c0108fda25847b07df8fc62a9425",
"src/main/resources/data/itsalive/recipe/tomato_block.json": "994e0f68beb5ea3b8811912d3eef46a6b7f0a7dc48af2e05c2a33e4fb87fe598",
"src/main/resources/data/itsalive/recipe/tomatoes_from_block.json": "eb7fa7e229187b4439e88f1e95f143ed999533f00415db06768f537c1f01e5ee"
},
"adaptations": [
"namespace sanctuary; Minecraft 26.3 APIs and exact cooking-fuel components",
"native cauldron liquid accounting; ingredient containers; timed fermentation",
"JEI six physical categories; native cauldron item icon added",
"equipment crafts: iron/copper replace unavailable silver; oven uses iron/bricks/crafting table/furnace"
],
"verification": {
"version": "beta.304",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.304.jar",
"sha256": "d24bac38e1177d785cd23a0ebe702c20e6a36dda160a0b4f366d23e9e7f4fa09",
"generated_json": {
"cuisine": 432,
"kitchen": 172,
"agriculture": 376
},
"culinary_classes": 43,
"server_required": 72,
"kitchen_scenarios": 17,
"physical_recipes": 80,
"device_blueprints": 91,
"client_vulkan": {
"kitchen": "build/kitchen304-client.log",
"cuisine": "build/kitchen304-cuisine303-client.log",
"agriculture": "build/kitchen304-agriculture300-client.log"
}
}
}
+190
View File
@@ -0,0 +1,190 @@
# IA-04 / IA-05 / IA-07 / IA-08 — appareils culinaires, beta.304 locale
Précision locale beta.311 : le chaudron attend désormais un geste au bâton et
ne produit plus de plat raté. Voir [fonctionnement et continuité](cauldron-beta311.md).
Précision locale beta.310 : la poêle se pose désormais en piles de une à
quatre, avec une recette commune et les ingrédients de chaque portion. Voir
[comportement actuel et continuité](pan-stacks-beta310.md). Le reçu ci-dessous
décrit les vérifications historiques de beta.304.
> Étape historique : [beta.305 remplace la presse par le piétinement et les
> pistons directement sur les fruits](crushing-beta305.md), puis
> [beta.306 rend le moulin manuel, sans combustible](mill-beta306.md).
> [beta.308 précise le cycle de la cuisinière et sépare aperçu et plats cuits](oven-beta308.md).
> Cette page conserve le résultat vérifié de beta.304.
Préparation du 28 octobre 2026 sur **`codex/itsalive-26-3`**, après beta.303.
Le créateur a choisi de porter **recettes et appareils culinaires ensemble**.
Aucune publication, installation, mise à jour de canal, de Prism ou de serveur.
Le checkout historique 26.2 reste une référence en lecture seule.
## Résultat jouable
Les six appareils sont enregistrés pour Minecraft **26.3**, Fabric API
**0.160.5+26.3**. Ils utilisent les textures déjà importées, des inventaires
serveur, les sauvegardes natives, les comparateurs et les entonnoirs.
Aucun onglet créatif supplémentaire : les dix nouvelles entrées d'objets
rejoignent **Nourriture et boissons**. Les semences restent dans Naturel.
| Appareil | Geste et fonctionnement | Recettes physiques |
| --- | --- | ---: |
| Poêle | Poser, ajouter un ingrédient par clic ; Maj + clic pour retirer le plat ou le dernier ingrédient. Contenu visible dans le bloc entité. Chauffe par dessous. | 26 |
| Chaudron | Jeter un aliment reconnu dans un chaudron vanilla rempli d'eau pour commencer ; ajouter ensuite par clic ou jet. Clic pour récupérer. | 19 |
| Cuisinière / four de cuisine | Ouvrir la grille 3 × 3, disposer les ingrédients sans forme, ajouter du combustible et attendre le résultat. | 13 |
| Tonneau de fermentation | Ajouter par clic, retirer à main vide. Délai historique conservé et affiché ; le temps du monde continue de compter lorsque le chunk est déchargé. | 10 |
| Moulin à redstone | Interface entrée / combustible / sortie, durées historiques ; le combustible suit les composants natifs du four 26.3. | 9 |
| Pressoir | Compacter neuf fruits, placer le bloc contre le pressoir et pousser avec un piston horizontal alimenté, depuis l'un des quatre côtés. Récupérer les portions avec le bon récipient. | 3 |
| **Total** | Catégories dédiées dans JEI, avec ingrédients, résultat et durée pour les procédés temporisés. | **80** |
La poêle est un bloc entité bas qui peut être posé sur les surfaces usuelles,
sans condition d'ancrage supplémentaire. Ce port conserve sa forme historique ;
il n'ajoute pas une pose murale, une orientation libre ou un nouveau modèle de
poignée. Ces améliorations éventuelles de IA-07 restent ouvertes.
Poêle et chaudron laissent **30 secondes de chauffe** pour assembler un mélange.
Une poêle avec quatre positions d'ingrédients distinctes peut commencer après
3 secondes de stabilité. Un gros tas d'œufs ne compte pas comme quatre
positions : le joueur peut encore ajouter du lait pour une omelette.
Sans chaleur, le délai d'assemblage et la cuisson sont suspendus.
Un mélange invalide à l'échéance donne un **plat raté**, en consommant une unité
de chaque ingrédient présent et en conservant ses récipients.
Les sources de chaleur sont le feu, la lave, un feu de camp allumé, un four
vanilla allumé ou une cuisinière allumée. Une lampe de redstone ou une propriété
`lit` quelconque ne suffit pas. Le moulin brûle du combustible ; son nom
historique ne signifie pas qu'un signal redstone remplace ce combustible.
## Eau, portions et récipients
Le chaudron conserve séparément ses **1 à 3 niveaux d'eau vanilla**. Ils servent
aux recettes comme le café, sans créer de bouteilles. Un niveau paie une dose
d'eau ; trois niveaux et trois cafés moulus donnent trois cafés. Les recettes
sont traitées en lots limités par les ingrédients et la place en sortie.
Récupérer le dernier plat ou annuler le mélange rétablit le chaudron vanilla,
avec exactement l'eau restante. Les potions à effets et les objets étrangers
ne sont pas des ingrédients admis.
Les bouteilles et seaux réellement utilisés sont rendus par la transformation.
L'ananas rend sa couronne. Les mélanges ratés rendent aussi ces récipients.
Les retours du chaudron sont éjectés avec une courte protection contre leur
réabsorption. Le pressoir donne neuf portions par bloc : bol pour la pulpe de
tomate, bouteille pour le moût de raisin et l'huile d'olive. Une portion
conditionnée conserve le récipient payé dans son composant natif de retour.
Une recette qui consomme cette portion rend ce même récipient.
Pour automatiser le pressoir, placer un inventaire/entonnoir dessous et fournir
les récipients correspondants par dessus. Seules les portions conditionnées
sortent par le bas ; un réservoir plein préserve le bloc de fruits devant lui.
Poêle, chaudron et tonneau acceptent les ingrédients par dessus/côtés et sortent
les résultats par dessous. Cuisinière et moulin prennent leur combustible par
les côtés et les ingrédients par dessus. Les récipients restés dans leurs
cases sont récupérables dans l'interface ou en vidant l'appareil.
## Recettes et continuité avec beta.303
Les **92 recettes historiques** restent couvertes. Les 46 préparations crues
et les identifiants d'objets déjà ajoutés restent utilisables au four/fumoir.
Les alternatives manuelles de cuisson et de mouture sont conservées.
Les dix identifiants `sanctuary:ferment_*` désignent désormais les procédés du
tonneau : leur ancienne fabrication instantanée disparaît. Les trois
`sanctuary:press_*` deviennent des recettes physiques de pressoir ; neuf fruits
se compactent en bloc, et ce bloc peut aussi se décompacter à la table. Il n'y
a plus deux résultats concurrents pour la même grille de neuf fruits.
Les 67 autres procédés physiques portent le préfixe `sanctuary:physical_*`.
Onze recettes donnent les cinq appareils fabriqués, les trois blocs de fruits
et leurs trois décompactages. Le chaudron de cuisine commence depuis le bloc
vanilla et rend un chaudron vanilla à la casse. La cuisinière coûte **trois
lingots de fer, quatre blocs de briques, une table de fabrication et un four** :
```text
Fer Fer Fer
Briques Table Briques
Briques Four Briques
```
Le pressoir utilise du fer à la place de l'ancien argent indisponible ; le
moulin utilise du cuivre à sa place. Les noms et aides d'interface sont FR/EN.
La pâte d'olive historique sans recette n'est pas ajoutée comme objet orphelin.
Les cultures enseignent également les recettes d'appareils et les procédés
physiques dans la progression et JEI. Les nouvelles entités sont additives ;
aucun identifiant agricole n'est renommé, aucun monde existant n'est modifié,
aucune génération n'est activée et aucune sauvegarde 26.2 n'est convertie.
Le saucisson, le séchoir, les nouvelles recettes inventées et la génération
naturelle restent dans leurs tickets. La cuisinière possède une grille de
recettes culinaires ; le Fourneau multibloc existant garde ses propres usages.
## Provenance et vérifications
Le port adapte **36 fichiers Java** de `26.2/itsalive`, commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, GPL-3.0-or-later.
[Reçu des sources](kitchen-beta304-sources.json), modèles historiques dans
`scripts/data/kitchen-assets.json`, recettes d'équipement adaptées dans
`scripts/data/kitchen-crafting.json`. Les 92 recettes originales et leurs
empreintes restent dans `scripts/data/cuisine.json`.
Les adaptations 26.3 concernent notamment les codecs de templates d'objets,
les combustibles à composants, les menus, le rendu de blocs entités, la
synchronisation Fabric des recettes et les six catégories JEI. Aucun PNG
agricole ou culinaire n'est retouché.
`check build assemblePack` passe en **2 min 51 s**, avec **72 tests serveur
requis réussis**, dont **17 scénarios pour les appareils**. Ils chargent les
80 recettes, font réellement cuire les 58 procédés chauds et tourner les neuf
recettes du moulin, vérifient le délai de fermentation et son rechargement,
l'eau vanilla, les récipients, les mélanges ratés, les quatre côtés du pressoir,
la capacité de sortie, le conditionnement, les 11 fabrications et la découverte
des 91 plans d'appareils/procédés. La casse répétée rend une seule fois les
inventaires des six appareils. Les régressions cuisine, cultures, vanille et
progression sont incluses.
Le parcours **Vulkan avec Sodium 0.9.2** passe en **1 min 8 s**. Il vérifie un
vrai paquet d'insertion dans la poêle, les ticks de cuisson, les contenus
synchronisés, les **233 états de blocs et dix modèles d'objets**, les menus
FR/EN aux échelles GUI 2/3/4 et les **six catégories / 80 recettes dans JEI**.
Une fermeture/réouverture réelle conserve les six appareils et leurs contenus,
y compris le délai du tonneau ; le vin se termine une fois après le temps
requis. Les échecs graphiques repérés pendant le port — icône de marmite,
libellé et références des sprites de moulin — sont corrigés.
La régression cuisine Vulkan passe en **37 s** : les 196 étapes du catalogue
restent indexées dans JEI, avec l'eau correcte ; 129 objets, table, fumoir et
rechargement sont vérifiés. La régression agricole Vulkan passe en **32 s** :
313 états, 48 objets, 20 cultures, les arbres et le caféier, onglets FR/EN
GUI 2/3/4 et rechargement réel de 173 états agricoles.
Le JAR contient les **432 JSON de cuisine**, **172 JSON d'appareils** et
**376 JSON agricoles** générés, identiques aux générateurs, ainsi que les
43 classes culinaires et les notices courantes. Les **343 PNG importés**
restent identiques, dont les 38 fichiers fournis ; les 13 PNG de vanille sont
toujours en 16 × 16. Les copies du JAR packwiz client et serveur sont
identiques au binaire vérifié. Le template conserve les mêmes textures
culinaires/agricoles/vanille et notices.
SHA-256 de `sanctuary-beta.304.jar` :
```text
d24bac38e1177d785cd23a0ebe702c20e6a36dda160a0b4f366d23e9e7f4fa09
```
Journaux et reçu de vérification dans `build/kitchen304-*`. Les captures du
dernier parcours restent dans le dossier de test client ignoré.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=kitchen304,cuisine303,vanilla301,agriculture300,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryKitchen304ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=kitchen304,cuisine303 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Les tests client utilisent un monde de développement neuf, graine **304**.
La validation graphique cible uniquement Vulkan. Les tests des longues durées
avancent explicitement l'horloge de leur monde de test ; ils ne prétendent pas
avoir attendu plusieurs jours de jeu en temps réel.
+29
View File
@@ -0,0 +1,29 @@
{
"version": "beta.318",
"instance": "/Users/koka/Library/Application Support/PrismLauncher/instances/Sanctuary-0.1.0-alpha.1",
"profile": "/Users/koka/Library/Application Support/PrismLauncher/instances/Sanctuary-0.1.0-alpha.1/sanctuary-local-packs/beta.318",
"source_commit": "980544e27947b490684fb9dd1b0cb9a1eac4e3b1",
"jar_sha256": "5b8d6fff72c3128ceac085a9d71b493bbda13a0dbbe42ffdd3e8dc2fda3a632b",
"isolated_passes": 2,
"personal_passes": 2,
"passes_identical": true,
"protected_files_unchanged": 1018,
"optional_choices_preserved": true,
"launcher_changes": "Only the two PreLaunchCommand URL arguments; all other settings preserved.",
"installer_unchanged": "9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875",
"bootstrap_unchanged": "a8fbb24dc604278e97f4688e82d3d91a318b98efc08d5dbfcbcbcab6443d116c",
"backup": {
"instance": "/Users/koka/Library/Application Support/PrismLauncher/instances/Sanctuary-0.1.0-alpha.1",
"backup": "/Users/koka/Library/Application Support/PrismLauncher/instances/Sanctuary-0.1.0-alpha.1/sanctuary-backups/before-local-beta318-20261030T053736Z",
"protected_files": 1018,
"backup_sha256": "56158b90e8b6b8ec9040129b1ee8077b0764f73aed1e7ba6afe946294b7f9327",
"backup_bytes": 272635407
},
"personal_world_opened": false,
"published": false,
"public_channel_modified": false,
"remote_server_modified": false,
"web_modified": false,
"date": "2026-10-30",
"verification": "Actual packwiz installer, two isolated passes then two personal passes; no game or personal world launched."
}
+73
View File
@@ -0,0 +1,73 @@
# beta.318 — installation locale pour les essais
Installation du **30 octobre 2026**, même branche `codex/itsalive-26-3`.
La demande autorise l’instance locale et exclut toute publication.
## Installation vérifiée
L’instance Prism existante **Sanctuary Beta**, dossier historique
`Sanctuary-0.1.0-alpha.1`, passe de beta.315 à **beta.318**. Aucune nouvelle
instance n’est créée ou importée. Un seul JAR Sanctuary parmi les neuf mods ;
les huit autres JAR, les choix des mods facultatifs, Minecraft 26.3, Fabric
0.19.5, Java, l’installateur au logo et le lien FFmpeg sont conservés.
Le JAR installé correspond exactement au binaire de
[la préparation beta.318](dried-cuisine-beta318.md), source `980544e27947b490684fb9dd1b0cb9a1eac4e3b1` :
```text
5b8d6fff72c3128ceac085a9d71b493bbda13a0dbbe42ffdd3e8dc2fda3a632b
```
Cette préparation avait réussi 189 tests serveur et le parcours Vulkan/Sodium.
La présente installation vérifie les fichiers et la commande de pré-lancement ;
aucun jeu ni monde personnel n’est ouvert pour la prouver. Pour essayer les
nouvelles recettes et les coups épicés, lancer **Sanctuary Beta dans Prism en solo**.
Les appareils restent autoritaires côté serveur ; aucun serveur distant n’est mis à jour.
## Profil de test local
Un profil autonome est conservé dans le dossier de l’instance :
```text
sanctuary-local-packs/beta.318/pack.toml
```
Il contient le manifeste, l’index vérifié, les métadonnées inchangées des
huit dépendances et le JAR Sanctuary local. Le vrai installateur packwiz lit
ce profil depuis le disque ; aucun serveur HTTP ni accès au worktree n’est
nécessaire au lancement. Le JAR est une entrée directe de l’index local.
Seuls les deux arguments d’URL `PreLaunchCommand` de `instance.cfg` sont
remplacés par le chemin cité `"$INST_DIR/sanctuary-local-packs/beta.318/pack.toml"`.
Le bootstrap, `--bootstrap-no-update`, les autres arguments et toutes les
lignes de configuration restent inchangés. Les lancements restent donc sur
beta.318 pour ces essais, au lieu de reprendre la version du canal public.
La commande publique d’origine est sauvegardée ; son retour sera effectué
lors d’une mise à jour publique compatible demandée.
## Sauvegarde et contrôles
Sauvegarde complète, hors de `mods/`, avant toute modification :
```text
/Users/koka/Library/Application Support/PrismLauncher/instances/Sanctuary-0.1.0-alpha.1/sanctuary-backups/before-local-beta318-20261030T053736Z/instance.tar.gz
```
Archive de **272635407 octets**, SHA-256 :
```text
56158b90e8b6b8ec9040129b1ee8077b0764f73aed1e7ba6afe946294b7f9327
```
Le véritable installateur réussit **deux passages dans une copie isolée**,
puis **deux dans l’instance personnelle**. Le second passage est à jour,
les neuf JAR gardent leurs hashes et aucun doublon Sanctuary ne reste.
**1018 fichiers personnels** sont contrôlés identiques avant/après : sauvegardes,
réglages, captures, favoris, packs et dépendances. Les métadonnées Minecraft/Fabric,
les préférences des mods facultatifs et les outils de lancement restent identiques.
Aucune régénération, conversion ou ouverture de monde.
Aucun push, tag, release, canal public, site, serveur Beta ou autre instance
n’est modifié. Les journaux privés sont dans `build/local318/` ; les scripts
ponctuels y sont un historique de cette intervention, pas une procédure à rejouer.
[Reçu de vérification](local-beta318-verification.json).
+70
View File
@@ -0,0 +1,70 @@
# MEDIA-07 — normales résiduelles de l'ancre
Retour du 27 octobre 2026 : une bordure légèrement brillante reste sur la
vidéo à l'emplacement de l'ancre, dans le coin inférieur gauche.
Branche `codex/media-anchor-model`, **beta.298**, Media **0.3.2**.
[Livraison complète du 27 octobre 2026](release-beta298.md).
## Cause et correction
Le bloc est invisible pour le rendu Minecraft, mais conserve un ancien modèle
de cadre 1 × 1 avec **30 faces**. La passe PBR de Sanctuary collectait aussi
les modèles des blocs invisibles. Elle dessinait donc les normales et le
matériau de ce petit cadre, alors que sa couleur n'était pas rendue.
Les essais graphiques de MEDIA-06 avaient les shaders Sanctuary désactivés ;
ils ne couvraient pas cette passe.
La collecte PBR des modèles opaques et du verre respecte maintenant
`RenderShape.MODEL`. Les fluides gardent leur chemin dédié, y compris l'eau
contenue dans un bloc. Le modèle de bloc de l'ancre est également vidé ;
sa texture de particules reste disponible. L'objet utilise déjà un modèle
indépendant et garde son apparence dans l'inventaire et dans la main.
Le grand cadre reste dessiné par son maillage aux dimensions de l'écran.
Aucun identifiant, paquet, format de données ou règle d'interaction ne change.
Les ancres existantes bénéficient du correctif au chargement du client.
La sauvegarde et les dimensions ne nécessitent aucune migration.
## Validation
Reproduction avec le JAR beta.297 publié : deux clients Vulkan, Sodium,
Sound Physics et **shaders Sanctuary/PBR actifs**. Le diagnostic constate
`INVISIBLE`, 30 faces et admission de l'ancre dans la passe PBR opaque.
Le bois et le verre visibles sont admis dans leurs passes respectives, l'eau
dans la passe transparente.
Après correction, les deux clients constatent **zéro face**, ancre refusée
par les deux passes de matériau, PBR réellement appliqué et vidéo en mouvement.
Les chemins bois, verre et eau restent admis. Pose, taille 9 × 5 et ouverture
du lecteur testées dans le laboratoire ; un clic de pose a dû être rejoué
après la réception de l'inventaire du test.
Captures finales du client A inspectées, en gros plan du coin inférieur gauche,
de face et en vue rasante : aucune empreinte du cadre 1 × 1 sur la vidéo.
La sortie OpenAL est active sur les deux clients. Le JAR Sanctuary testé,
avec un seul Media embarqué et sans sonde, a pour SHA-256
`e6fdc36a212dcfcc89d31fbb29bc8eeec9d002cc9223ea6dcbfb31b7df17ec23`.
`check build assemblePack assembleServerPack :sanctuary-test:exportDuoLaunch`
réussis en **3 min 6 s**, 162 tâches, **18 GameTests requis** au profil
`property270,claims272,claims277,claims278`, atlas ciblé. Contrats Media inclus.
Compilation des vérifications client PBR, y compris les nouveaux contrôles
des blocs invisibles et des modèles visibles, réussie en **37 s**. La suite
graphique PBR historique complète n'est pas rejouée ; ce parcours ciblé
exerce la sélection réelle des matériaux du JAR construit.
Un lancement de client a automatiquement basculé hors Vulkan après une course
d'extraction MoltenVK entre les deux processus : il a été rejeté, arrêté et
relancé sous Vulkan. Seuls les relevés Vulkan sont retenus. Après les captures
et les relevés valides, le client B s'est arrêté sur l'écriture du journal de
la sonde (`No space left on device`). Le disque local ne disposait plus que
de 136 Mio au contrôle suivant ; ce parcours ne vaut donc pas essai d'endurance.
Ce défaut d'espace appartient au laboratoire, la sonde n'est pas distribuée.
[Relevé structuré](media-anchor-normals-results.json). Pas de nouvelle mesure
de synchronisation vidéo/live ou de source publique. Mur graphique orienté sud.
Laboratoires isolés sous `build/media-prototype/anchor-model-*`, captures finales
dans `anchor-model-after/client-a/screenshots/`. Aucun monde personnel utilisé.
Le correctif est maintenant publié et installé sur le canal, la Beta et la
même instance Prism. Le [reçu](release-beta298.md) distingue ces essais
graphiques des vérifications effectuées pendant la livraison.
+111
View File
@@ -0,0 +1,111 @@
{
"ticket": "MEDIA-07",
"state": "published and installed on Beta server, Web distribution and existing Prism instance",
"artifact": {
"version": "beta.298",
"bytes": 18301295,
"sha256": "e6fdc36a212dcfcc89d31fbb29bc8eeec9d002cc9223ea6dcbfb31b7df17ec23",
"media_version": "0.3.2",
"nested_media_count": 1,
"anchor_block_faces": 0,
"probe_embedded": false
},
"backend": "Vulkan",
"clients": [
{
"client": "client-a",
"model": {
"anchor_model_quads": 0,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"pbr_applied": true,
"anchor_pbr_opaque": false,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
},
"vulkan_video_uploads": 1625
},
{
"client": "client-b",
"model": {
"anchor_model_quads": 0,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"pbr_applied": true,
"anchor_pbr_opaque": false,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
},
"vulkan_video_uploads": 1448
}
],
"before": {
"version": "beta.297",
"clients": [
{
"client": "client-a",
"anchor_model_quads": 30,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"anchor_pbr_opaque": true,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
},
{
"client": "client-b",
"anchor_model_quads": 30,
"anchor_render_shape": "INVISIBLE",
"shaders_active": true,
"pbr_enabled": true,
"anchor_pbr_opaque": true,
"water_pbr_opaque": false,
"wood_pbr_opaque": true,
"anchor_pbr_transparent": false,
"water_pbr_transparent": true,
"glass_pbr_transparent": true
}
]
},
"build": {
"check_build_assemble": "passed",
"duration": "3m06s",
"tasks": 162,
"required_gametests": 18,
"client_checks_compile": "passed, 37s, 6 tasks; historical PBR graphic suite not rerun"
},
"limits": [
"No new live synchronization or public-source validation",
"Graphical wall faces south",
"Graphical validation used isolated instances; deployment to Beta and existing Prism is recorded in the delivery receipt",
"One automatic fallback after concurrent MoltenVK extraction was rejected and the client relaunched in Vulkan",
"Client B stopped after valid samples and captures because the probe could not write its log: No space left on device; not an endurance test"
],
"graphics_checks": {
"lower_left_front_video": "inspected, no 1x1 frame imprint",
"lower_left_grazing_video": "inspected, no 1x1 frame imprint",
"placement_resize_reader": "passed; placement replayed after inventory arrival",
"final_screenshots": [
"anchor-model-shaders-front-settled.png",
"anchor-model-shaders-side-settled.png"
]
},
"delivery": {
"date": "2026-10-27",
"receipt": "release-beta298.md",
"source_tag": "fb8705369e3834c66d965108d609ec704b584ec2",
"packwiz_commit": "7fa08625a4fa89391d64b4b4fc0859611ecf0952",
"fresh_check_build_assemble": "passed, 2m50s, 158 tasks, 18 required GameTests; identical JAR to final Vulkan validation"
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"version": "beta.306",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.306.jar",
"sha256": "a58ddb731719b5bcd975ac86acb95941f0f1bfe49cbefab429b01d47e847af60",
"generated_json": {
"cuisine": 432,
"kitchen": 192,
"agriculture": 376
},
"culinary_classes": 44,
"server_required": 52,
"manual_mill_scenarios": 10,
"manual_mill_recipes": 9,
"physical_recipes": 80,
"device_blueprints": 90,
"client_vulkan": {
"manual_mill": "build/mill306-client.log",
"screenshots": "build/mill306-screenshots",
"backend": "Vulkan",
"sodium": "0.9.2+mc26.3",
"world_seed": 306
}
}
+118
View File
@@ -0,0 +1,118 @@
# CUI-306 — moulin manuel, sans combustible
> Suite locale : [beta.307 adapte aussi la recette de fabrication, sans
> redstone](mill-recipe-beta307.md). Cette page conserve le résultat de beta.306.
Choix du créateur du 28 octobre 2026 : **manuel, comme le tailleur de pierre**.
Préparation **beta.306** sur la même branche `codex/itsalive-26-3`, après
beta.305. Travail local, sans publication, installation dans Prism, mise à
jour de canal ou déploiement serveur.
## Résultat jouable
Le bloc, l'objet, le titre de menu et la catégorie JEI s'appellent désormais
**Moulin / Mill**. L'interface propose une entrée et un résultat immédiat.
Chaque joueur possède sa propre surface de travail, comme dans un menu de
fabrication vanilla.
L'aperçu ne consomme rien. Prendre le résultat paie **un ingrédient** et donne
le rendement prévu par la recette. Par exemple, un blé donne deux farines de
blé, y compris au clic droit qui prend le lot entier. Le Maj-clic traite les
lots disponibles dans la limite de l'inventaire.
Si seul un morceau du dernier lot tient, le surplus payé tombe au sol, selon
le comportement natif du tailleur de pierre. Un inventaire plein ne consomme
pas d'ingrédient. Les récipients réellement payés sont rendus une seule fois.
Fermer le menu ou casser le moulin rend les ingrédients inutilisés ; aucun
aperçu n'est sauvegardé ou lâché comme objet.
Le moulin ne consomme **aucun combustible**, n'attend pas un délai, ne produit
pas passivement et n'accorde pas d'XP de cuisson. Il ne demande aucun signal
redstone. Les entonnoirs n'ont pas accès à cette fabrication manuelle.
Le feu et les commandes de combustible disparaissent du menu. Les deux
sprites historiques conservent un bref mouvement mécanique descendant puis
ascendant, de **12 ticks**, après une prise de résultat. Le son réutilise
celui du tailleur de pierre. L'ancien état `lit` sert uniquement à afficher
brièvement la face active du bloc, sans chaleur ni combustion.
Les **neuf recettes**, leurs rendements, les ingrédients reconnus et la
recette de fabrication du moulin sont conservés. JEI affiche « Mouture
manuelle / Manual milling » au lieu d'une durée. Le champ historique
`processing_time` reste accepté dans les données pour garder les recettes
compatibles ; il ne pilote plus le moulin.
## Continuité des objets déjà stockés
Les identifiants `sanctuary:redstone_mill` du bloc, de l'objet, du menu et du
bloc entité restent stables. Les trois anciennes cases gardent leur encodage
natif et leur version de stockage 2. La lecture historique de version 1 est
conservée. Aucun monde existant n'est ouvert, modifié ou converti par ce lot.
À la première ouverture d'un ancien moulin, son entrée stockée rejoint la
surface de travail du joueur. L'ancien combustible et l'ancien résultat
restent visibles dans deux cases **de récupération uniquement**, affichées
tant qu'elles contiennent un objet. Ils peuvent être retirés par clic ou
Maj-clic. Ils sont aussi conservés à la casse du bloc. Ces cases n'acceptent
aucun nouvel apport et disparaissent de l'affichage une fois vidées.
Les anciens compteurs de chauffe et de progression sont ignorés. Ils ne
consomment pas le combustible restant, ne créent pas de résultat et ne
remboursent pas un combustible déjà brûlé. Les objets réellement stockés
restent récupérables, sans conversion automatique en farine.
## Vérifications
Minecraft **26.3**, Fabric API **0.160.5+26.3**, Java 25. Les tests graphiques
ciblent uniquement **Vulkan avec Sodium 0.9.2**, dans un monde de développement
neuf de graine **306**, ignoré par Git.
`check build assemblePack` passe en **3 min 11 s**, avec **52 tests serveur
requis réussis**, dont les dix scénarios du moulin et les régressions
appareils, cuisine, recettes, progression et connaissances alimentaires.
Les dix nouveaux tests serveur couvrent l'aperçu sans paiement, le clic droit,
les prises et Maj-clics natifs, l'inventaire plein ou presque plein, la
fermeture/casse, deux joueurs indépendants, les anciens stocks et compteurs, l'absence de
combustible et d'accès par entonnoir, ainsi que les récipients payés. La
régression des appareils exécute les neuf recettes réelles du moulin.
Le parcours client Vulkan passe en **1 min 8 s** : vrais paquets
d'insertion, prise, Maj-clic et fermeture, mouvement mécanique sans feu,
deux cases de travail, noms FR/EN aux échelles GUI 2/3/4 et neuf recettes
dans JEI. Une fermeture/réouverture réelle du monde conserve les anciens
stocks ; leur extraction rend exactement sept charbons, cinq blés et trois
farines, en plus des objets déjà possédés. Les dix captures sont archivées
dans `build/mill306-screenshots/` ; journal `build/mill306-client.log`.
Le JAR contient les **432 JSON culinaires**, **192 JSON d'appareils** et
**376 JSON agricoles**, identiques aux générateurs, ainsi que les 44 classes
culinaires et les libellés FR/EN. Les trois classes du moulin sont identiques
aux fichiers compilés ; les neuf recettes sont présentes. Les **343 PNG
importés** restent identiques, dont les 38 fournis ; les 13 PNG de vanille
restent en 16 × 16. Les copies packwiz client et serveur sont identiques au
JAR contrôlé. Le template conserve les mêmes textures et les notices à jour.
SHA-256 de `sanctuary-beta.306.jar` :
```text
a58ddb731719b5bcd975ac86acb95941f0f1bfe49cbefab429b01d47e847af60
```
[Reçu de validation](mill-beta306-verification.json), journaux de build et
d'archives dans `build/mill306-build.log` et `build/mill306-artifacts.log`.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryMill306ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=mill306 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
La recette de fabrication du moulin et les autres appareils culinaires
sont conservés. L'automatisation ne fait pas partie du fonctionnement manuel
retenu. Les textures importées, les catégories créatives et les cultures
sont conservées.
@@ -0,0 +1,41 @@
{
"version": "beta.307",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.307.jar",
"sha256": "e510871bd515e5b52f2e1b218b593b49ff74ffae320e9b6b665ef1629c53d3d0",
"previous_verified_sha256": "a58ddb731719b5bcd975ac86acb95941f0f1bfe49cbefab429b01d47e847af60",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"data/sanctuary/recipe/redstone_mill.json",
"fabric.mod.json",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"generated_json": {
"cuisine": 432,
"kitchen": 192,
"agriculture": 376
},
"server_required": 52,
"recipe_id": "sanctuary:redstone_mill",
"pattern": [
"SSS",
"GCG",
" B "
],
"ingredients": {
"S": "minecraft:smooth_stone_slab",
"G": "minecraft:copper_ingot",
"C": "minecraft:stonecutter",
"B": "minecraft:stick"
},
"result": {
"id": "sanctuary:redstone_mill"
},
"crafting_category": "misc",
"runtime_classes_unchanged": 2745,
"pngs_unchanged": 973,
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md"
],
"build_log": "build/mill307-build.log"
}
+69
View File
@@ -0,0 +1,69 @@
# CUI-307 — recette du moulin manuel
Demande du créateur du 28 octobre 2026 : adapter la fabrication au
fonctionnement manuel du moulin, **sans redstone**. Préparation **beta.307**
sur la même branche `codex/itsalive-26-3`, après beta.306. Travail local,
sans publication, installation, mise à jour de canal ou déploiement serveur.
## Fabrication
La table de fabrication donne **un moulin** avec :
| Dalle de pierre lisse | Dalle de pierre lisse | Dalle de pierre lisse |
| --- | --- | --- |
| Lingot de cuivre | Tailleur de pierre | Lingot de cuivre |
| Vide | Bâton | Vide |
Le bâton représente la poignée du mécanisme manuel. Il remplace la poudre
de redstone de la recette précédente ; les autres ingrédients et leurs
quantités sont conservés. Aucun de ces composants ne demande de redstone.
La catégorie du livre de fabrication devient **Divers / Miscellaneous** au
lieu de Redstone ; les objets et leurs noms FR/EN restent ceux du jeu.
Les onglets créatifs ne changent pas.
La recette garde l'identifiant **`sanctuary:redstone_mill`** et sa découverte
dans la collection des appareils culinaires. Les moulins déjà placés, les
inventaires et les neuf recettes de mouture gardent le comportement manuel
de beta.306. Ce lot change uniquement les données de fabrication, sans
nouveau Java de jeu, nouveau PNG ou modification de sauvegarde.
La source adaptée est `scripts/data/kitchen-crafting.json`, distribuée par
`scripts/kitchen_resources.py`. Les reçus historiques 26.2 et beta.304 restent
inchangés ; cette page documente l'évolution de la copie Sanctuary.
## Vérifications
`check build assemblePack` passe en **2 min 53 s**, avec **52 tests serveur
requis réussis**. La nouvelle grille est chargée et produit un moulin sans
résultat concurrent ; la découverte du plan et les transformations manuelles
restent vérifiées.
Minecraft **26.3**, Fabric API **0.160.5+26.3**, Java 25. Les tests serveur
existants chargent les dix fabrications d'appareils/stockage, font correspondre
leurs grilles réelles et vérifient l'absence de résultat concurrent. Ils
couvrent également la découverte du plan et les neuf recettes du moulin
manuel. Le code de menu et les textures sont inchangés ; le parcours Vulkan
de beta.306 reste le reçu graphique de référence et n'est pas présenté comme
un nouvel essai de beta.307.
Les **1 000 JSON générés** correspondent aux sources : 432 culinaires,
192 d'appareils et 376 agricoles. Comparé au JAR vérifié de beta.306, seuls
la recette du moulin, la version du manifeste et les deux copies des notices
changent. Les **2 745 classes et 973 PNG** de l'archive sont identiques octet
pour octet. Les copies du JAR packwiz client et serveur sont identiques au
binaire contrôlé ; le template ne change que par sa notice.
SHA-256 de `sanctuary-beta.307.jar` :
```text
e510871bd515e5b52f2e1b218b593b49ff74ffae320e9b6b665ef1629c53d3d0
```
[Reçu de validation](mill-recipe-beta307-verification.json). Journaux dans
`build/mill307-build.log` et `build/mill307-artifacts.log`.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
```
+6
View File
@@ -3,6 +3,12 @@
16 septembre 2026. Cadrage corrigé après clarification du créateur.
Aucun portage, adaptateur ou changement de sauvegarde livré ici.
**Suite du 28 octobre 2026 :** le créateur ouvre la
[refonte 26.3 d'It's Alive !](itsalive-26-3.md). Le premier lot importe les
textures ; les mécaniques restent en tickets. La cuisinière four + table de
fabrication et son lien avec le Fourneau seront examinés dans IA-08. Le texte
ci-dessous conserve les décisions et constats du cadrage du 16 septembre.
## Direction retenue
Le créateur souhaite intégrer **tout It's Alive dans Sanctuary**, mais
+15
View File
@@ -0,0 +1,15 @@
# Sanctuary beta.298
Le petit cadre brillant dans le coin inférieur gauche de la vidéo est corrigé
avec les shaders Sanctuary activés. Le modèle invisible de l'ancre ne fournit
plus de faces à la passe des normales PBR. Le grand cadre conserve sa taille
et son épaisseur ; le lecteur et le redimensionnement gardent leurs gestes.
Les écrans déjà posés conservent leur source, leurs dimensions et leurs données.
Aucun bloc à remplacer ni conversion de sauvegarde. Media passe en 0.3.2 ;
Minecraft 26.3, Fabric et Dream Displays 1.10.4 restent inchangés.
Correctif vérifié à deux clients Vulkan avec PBR actif, vidéo en mouvement et
gros plans de face et en vue rasante. Le détail des contrôles et leurs limites
est dans `docs/media-anchor-normals-beta298.md` ; pas de nouvelle validation
des sources publiques ni de l'endurance du lecteur.
+30
View File
@@ -0,0 +1,30 @@
# Sanctuary beta.315 — cuisine et agriculture It's Alive
La refonte locale beta.299 à beta.315 rejoint le pack Sanctuary 26.3.
- Vingt cultures, citronnier, olivier, caféier et vanille grimpante vers le haut.
- Recettes pour les nouvelles récoltes, poêle et piles de quatre, cuisinière
automatique, chaudron remué au bâton et recettes répétées, fermentation.
- Moulin manuel, sans combustible. Les blocs de tomates, raisins et olives
se piétinent en huit paliers ou s'écrasent au piston ; super piston 27 blocs.
- Préparation de saucisson à l'établi puis séchage sur quatre crochets,
cinq minutes de jeu au sec ; pluie et immersion suspendent la progression.
- Fromage posé en sept parts et saucisson posé en quatre tranches.
- Vin jeune après trois minutes de fermentation ; vin de table, réserve et
grand cru après un, trois et sept jours réels de garde au tonneau, jeu fermé.
- Nourriture dans Nourriture et boissons, semences dans Naturel, sans nouvel onglet.
Les identifiants historiques de cette refonte restent stables. Les nouveaux
blocs conservent leur état par les mécanismes natifs ; le vin a un horodatage
serveur. Aucune régénération ou expansion de monde n'est effectuée.
Les cultures sont jouables ; leur apparition naturelle et la répartition
climatique ne sont pas introduites par cette livraison. Chercher les objets
dans les onglets vanilla et leurs recettes dans JEI pour les essais.
Le fromage et le saucisson se mangent après la pose ; casser un aliment entamé
ne rend pas un nouvel aliment entier. Une sortie de tonneau extraite par un
entonnoir garde sa qualité : le verrouiller pour laisser vieillir le vin.
Minecraft 26.3, Fabric Loader 0.19.5 et dépendances du pack conservés.
Les contrôles graphiques ciblent Vulkan avec Sodium. Les preuves détaillées
et limites figurent dans les tickets beta.299–315 et le reçu de livraison.
+29
View File
@@ -0,0 +1,29 @@
# Sanctuary beta.318
Minecraft 26.3 · Fabric Loader 0.19.5 · Java 25.
Cette mise à jour réunit les ajouts beta.316 à beta.318 :
- Récoltes sauvages et premières semences en survie selon les biomes des neuf
îles : cultures, arbres fruitiers, café et vanille. Les terrains déjà explorés
gardent leurs blocs et disposent aussi de trouvailles adaptées au climat.
- Séchoir : viandes, cabillaud et saumon crus, raisins, tomates, piment, herbes,
fruits et vanille. Durées propres à chaque aliment, quatre crochets indépendants,
pause sous la pluie et protection par un toit.
- Dix-huit nouvelles recettes aux produits séchés : pains, brioche, biscuits,
sandwichs, plats au chaudron, salade, dessert, mélange de fruits et épices.
Le saucisson pimenté se prépare, sèche cinq minutes et se partage en quatre tranches.
- Manger épicé provoque deux petits coups visuels et sonores, sans dégâts
ni recul. La réaction fonctionne aussi sur les tranches de saucisson pimenté.
Les nouveaux objets restent dans Nourriture et boissons. Les herbes et le piment
séchés, la poudre de piment et l'assaisonnement servent aussi dans la cuisine
existante. Les bols et fioles des nouveaux plats reviennent à la consommation.
Les anciennes recettes, appareils et sauvegardes restent compatibles. Aucun
repeuplement des anciens chunks ni changement de format de sauvegarde : les
plantes sauvages apparaissent à la première décoration des nouveaux chunks.
Les icônes supplémentaires réutilisent les sprites existants recolorés.
[Guide des récoltes](https://git.botsu.net/koka/sanctuary-beta/src/tag/beta.318/docs/wiki/alimentation/README.md) ·
[Recettes, quantités et durées](https://git.botsu.net/koka/sanctuary-beta/src/tag/beta.318/docs/wiki/alimentation/sechoir.md).
+49
View File
@@ -0,0 +1,49 @@
{
"version": "beta.308",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.308.jar",
"sha256": "103e08039063633829b4f2059f25e32920cdb7e4df6d1e02512f40a35c0390eb",
"previous_verified_sha256": "e510871bd515e5b52f2e1b218b593b49ff74ffae320e9b6b665ef1629c53d3d0",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity$1.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity$2.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/screen/KitchenOvenScreen.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$FuelSlot.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$IngredientSlot.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$OutputSlot.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$1.class"
],
"generated_json": {
"cuisine": 432,
"kitchen": 192,
"agriculture": 376
},
"culinary_source_classes": 44,
"culinary_compiled_classes_verified": 62,
"oven_recipes": 13,
"unchanged_pngs": 1108,
"server_required": 63,
"oven_scenarios": 11,
"client_vulkan": {
"log": "build/oven308-client.log",
"screenshots": "build/oven308-screenshots",
"screenshot_count": 12,
"world_seed": 308,
"sodium": "0.9.2+mc26.3"
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"build_log": "build/oven308-build.log"
}
+128
View File
@@ -0,0 +1,128 @@
# CUI-308 / IA-08 — cuisinière, grille et cuisson automatique
> Correction locale suivante : [beta.309 retire l'aperçu, garde une seule sortie
> et bloque le remplissage de la grille par entonnoir](oven-single-output-beta309.md).
> Cette page conserve le résultat historique de beta.308.
Demande du créateur du 28 octobre 2026, précisée : **démarrage automatique
comme un four**, et **recettes de cuisine uniquement**. Préparation locale
**beta.308**, après beta.307, sur la même branche `codex/itsalive-26-3`.
Aucune publication, installation dans Prism, mise à jour de canal ou de serveur.
## Cycle jouable
La cuisinière réunit une **grille persistante 3 × 3**, un combustible natif et
une sortie pour les plats terminés. L'interface montre aussi un **aperçu
« À cuire / To cook »**, séparé de cette sortie. Cet aperçu indique la recette
reconnue mais ne peut être retiré, déplacé, cloné ou collecté par double-clic.
Ses aides FR/EN expliquent le paiement en fin de cuisson.
La cuisson commence automatiquement lorsqu'une recette valide, de la place
en sortie et une source de combustible sont présents. Les ingrédients restent
visibles et disponibles dans la grille pendant **toute la durée de cuisson**.
À l'échéance seulement, la cuisinière consomme les quantités d'une recette,
rend les récipients réellement payés et range le plat dans la sortie.
Les ingrédients restants permettent un nouveau cycle complet, sans bouton.
Trois pâtes donnent donc trois pains sur trois durées de 240 ticks.
Fermer le menu laisse la grille dans le bloc et la cuisson continue tant que
le chunk est chargé. Retirer, remplacer, déplacer ou modifier la quantité ou
les composants d'un ingrédient **annule l'avancement en cours**. Cela couvre
les Maj-clics partiels, les autres joueurs, les entonnoirs et les changements
de pile effectués directement. Si la nouvelle grille reste une recette valide,
un nouveau cycle démarre automatiquement à zéro ; sinon elle attend.
Remettre immédiatement le même ingrédient ne rend pas l'ancien avancement.
Prendre un plat déjà cuit ou ajouter du combustible n'édite pas la grille et
ne remet pas sa cuisson à zéro. Une sortie pleine ou incompatible empêche
l'allumage d'un nouveau combustible et remet le cycle bloqué à zéro, sans
consommer les ingrédients. Libérer la sortie permet un nouveau cycle complet.
Le Maj-clic utilise les mêmes règles de combustible côté client et serveur.
Le combustible suit les composants et durées du four Minecraft **26.3**.
Un charbon est payé à l'allumage, comme dans un four ; retirer un ingrédient
ne rembourse pas le combustible déjà brûlé. La chaleur restante continue de
se dépenser. Les combustibles encore en pile restent disponibles. Une pause
par manque de combustible conserve l'avancement tant que la grille est intacte.
Les **treize recettes de cuisinière**, leurs ingrédients, résultats, identifiants
et durées de 240 à 380 ticks restent en place. La cuisinière ne fabrique pas
les outils et objets de l'établi ordinaire et ne fond pas les minerais.
Les 92 recettes du catalogue culinaire et les autres appareils restent dans
leurs circuits existants. La fabrication de la cuisinière conserve trois
lingots de fer, quatre blocs de briques, une table de fabrication et un four.
## Continuité des sauvegardes
L'identifiant `sanctuary:kitchen_oven` reste stable pour le bloc, l'objet, le
menu et le bloc entité. Les **onze cases sauvegardées** gardent leur encodage :
0–8 pour la grille, 9 pour le combustible, 10 pour les plats cuits. L'aperçu
est une case virtuelle du menu, jamais une douzième case de stockage.
Les champs natifs existants `CookingProgress`, `CookingDuration`,
`ActiveRecipe`, `BurnTime` et `BurnDuration` sont conservés. La surveillance de
la grille est reconstruite depuis les objets chargés, sans nouveau champ
de sauvegarde ni conversion automatique. Une cuisson en cours reprend avec
ses ingrédients et son avancement. Le temps écoulé hors chargement ne produit
aucun repas supplémentaire, comme pour une cuisson fondée sur les ticks du bloc.
La casse rend uniquement le vrai inventaire restant, une seule fois. Elle
ne transforme pas l'aperçu en plat. Aucun monde personnel n'est ouvert ou
modifié par cette préparation ; les tests utilisent de nouveaux mondes ignorés.
## Vérifications
`check build assemblePack` passe en **3 min 6 s**, avec **63 tests serveur
requis réussis**, dont onze nouveaux scénarios de cuisinière. Les régressions
appareils, cuisine, moulin, recettes, progression et connaissances alimentaires
sont incluses.
Minecraft **26.3**, Fabric API **0.160.5+26.3**, Java 25. Les onze nouveaux tests
serveur couvrent les échéances et lots successifs, la fermeture, l'annulation
près de la fin, le Maj-clic partiel, les changements directs de pile, la sortie
bloquée, l'aperçu non retirable, le rechargement natif d'une cuisson en cours,
les récipients, le périmètre culinaire et la casse sans duplication.
Les régressions des appareils exécutent également les treize recettes réelles.
Le parcours **Vulkan avec Sodium 0.9.2** passe en **1 min 41 s**. Il utilise
de vrais paquets d'insertion et de retrait, vérifie la prédiction du Maj-clic
du combustible, l'aperçu non retirable et les écrans FR/EN aux échelles GUI
2/3/4. Retirer la pâte annule une cuisson aux trois quarts sans la consommer.
Après remise en place, les trois cycles se terminent avec le menu fermé et
produisent exactement trois pains. Une fermeture/réouverture réelle du monde
conserve ces trois pains, récupérés ensuite par un vrai Maj-clic. Les treize
recettes de cuisinière restent présentes dans JEI. Les douze captures sont
archivées dans `build/oven308-screenshots/`, journal `build/oven308-client.log`.
Les **1 000 JSON générés** sont identiques aux sources et au JAR beta.307 :
432 culinaires, 192 d'appareils et 376 agricoles. Les 44 sources culinaires
et leurs 62 classes compilées sont présentes et contrôlées. L'archive ne
change que les classes de cuisinière, les libellés FR/EN, la version et les
notices ; le nouveau slot d'aperçu ajoute une classe interne de menu.
Les **1 108 PNG** de l'archive sont identiques à beta.307. Les copies du JAR
packwiz client et serveur sont identiques au binaire vérifié ; le template
ne change que par les notices et les deux fichiers de libellés.
SHA-256 de `sanctuary-beta.308.jar` :
```text
103e08039063633829b4f2059f25e32920cdb7e4df6d1e02512f40a35c0390eb
```
[Reçu de validation](oven-beta308-verification.json). Journaux complémentaires
dans `build/oven308-build.log` et `build/oven308-artifacts.log`.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryOven308ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=oven308 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Le parcours graphique cible uniquement **Vulkan avec Sodium 0.9.2**, dans un
monde de développement neuf de graine **308**. Aucun PNG n'est modifié :
l'interface assemble les panneaux et sprites vanilla de l'établi et du four.
@@ -0,0 +1,54 @@
{
"version": "beta.309",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.309.jar",
"sha256": "32ca2d855f71ac29af0a62ba9f4e463e3d11f172fefe7ecdbb1afad086b63ff9",
"previous_verified_sha256": "103e08039063633829b4f2059f25e32920cdb7e4df6d1e02512f40a35c0390eb",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity$1.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity$2.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/KitchenOvenBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/screen/KitchenOvenScreen.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$FuelSlot.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$IngredientSlot.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$OutputSlot.class",
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [],
"removed_jar_entries": [
"fr/koka/sanctuary/cuisine/menu/KitchenOvenMenu$1.class"
],
"generated_json": {
"cuisine": 432,
"kitchen": 192,
"agriculture": 376
},
"culinary_source_classes": 44,
"culinary_compiled_classes_verified": 61,
"oven_recipes": 13,
"machine_slots": 11,
"cooked_output_slots": 1,
"ingredient_hopper_insertion": false,
"cooked_hopper_extraction": true,
"unchanged_pngs": 1108,
"server_required": 63,
"oven_scenarios": 11,
"client_vulkan": {
"log": "build/oven309-client.log",
"screenshots": "build/oven309-screenshots",
"screenshot_count": 11,
"world_seed": 309,
"sodium": "0.9.2+mc26.3"
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"build_log": "build/oven309-build.log"
}
+93
View File
@@ -0,0 +1,93 @@
# CUI-309 — une seule sortie pour la cuisinière
Correction demandée par le créateur le 28 octobre 2026 : **une seule sortie,
comme un four Minecraft**, et abandon du remplissage automatique de la grille.
Préparation locale **beta.309**, après beta.308, sur `codex/itsalive-26-3`.
Aucune publication, installation dans Prism ou mise à jour de serveur/canal.
## Résultat
La cuisinière présente sa grille 3 × 3, le combustible et **une seule case de
sortie**, centrée face à la flèche de cuisson. La case d'aperçu « À cuire »
ajoutée en beta.308 est retirée, ainsi que ses aides et sa flèche supplémentaire.
La sortie reste vide jusqu'à la fin d'une cuisson réelle.
À la demande suivante du créateur, l'icône de feu (vide et animée) est décalée
de **2 pixels GUI vers la gauche et de 2 pixels vers le haut**, sans déplacer
la case de combustible.
Le cycle confirmé précédemment reste en place : démarrage automatique avec
une recette de cuisine et du combustible, ingrédients conservés dans la grille
jusqu'à l'échéance, paiement d'un lot en fin de cuisson, puis enchaînement des
lots disponibles. Modifier la grille annule l'avancement ; fermer le menu
laisse la cuisson continuer tant que le chunk est chargé. Les treize recettes
de cuisinière et leurs durées sont conservées.
La grille se remplit **à la main**. Aucun ingrédient n'est insérable par
entonnoir, quelle que soit la face. Un entonnoir placé dessous récupère
uniquement les plats déjà cuits dans l'unique sortie ; il ne prélève ni les
ingrédients ni le combustible. L'accès latéral au combustible déjà présent
reste disponible. Aucune grille d'automatisation spéciale n'est ajoutée.
Les identifiants et les onze cases sauvegardées restent inchangés : neuf
ingrédients, combustible en case 9 et plats cuits en case 10. L'ancienne case
d'aperçu n'était pas sauvegardée. Retirer cette case virtuelle ne convertit ni
ne supprime aucun objet. Les compteurs de cuisson et de combustion gardent
leur encodage. Aucun monde personnel n'est ouvert ou modifié.
## Vérifications
`check build assemblePack` passe en **3 min 32 s**, avec **63 tests serveur
requis réussis**, dont les onze scénarios de cuisinière adaptés. Le parcours
Vulkan repasse après le déplacement du feu ; les contrôles portent sur le
binaire final de cette préparation.
Minecraft **26.3**, Fabric API **0.160.5+26.3**, Java 25. La régression serveur
de la cuisinière vérifie maintenant un véritable entonnoir d'alimentation
bloqué au-dessus et un véritable entonnoir d'extraction dessous : aucune sortie
avant l'échéance, puis exactement un pain extrait et deux pâtes encore en
grille après le premier cycle. Les autres scénarios de beta.308 restent actifs.
Le parcours client est adapté à l'unique sortie, aux onze cases réelles et
aux indices natifs d'inventaire retrouvés. Les tests graphiques restent
exclusivement **Vulkan avec Sodium 0.9.2**, dans un nouveau monde de graine
**309**. Aucun PNG n'est modifié ; l'interface utilise les panneaux vanilla.
Les classes de régression et leurs options gardent leur nom `Oven308`.
Le parcours Vulkan passe en **1 min 47 s**, après le décalage du feu. Les
écrans FR/EN aux échelles GUI 2/3/4 présentent une seule sortie et aucune case
virtuelle. De vrais paquets d'inventaire vérifient le remplissage manuel, le
combustible, l'annulation d'une cuisson aux trois quarts et sa reprise depuis
zéro. Trois cycles se terminent avec le menu fermé, donnant exactement trois
pains dans l'unique sortie. Une fermeture/réouverture réelle conserve ces
trois pains et leur retrait par Maj-clic. Les treize recettes restent dans JEI.
Les onze captures sont dans `build/oven309-screenshots/`, avec l'icône vide
et la flamme animée décalées ensemble. Journal `build/oven309-client.log`.
Les **1 000 JSON générés** et les treize recettes de cuisinière sont inchangés.
Les 44 sources culinaires et leurs 61 classes compilées sont contrôlées dans
le JAR. La classe interne de l'ancien aperçu a disparu ; seules les classes
de cuisinière, la version, les notices et le retrait des trois libellés
d'aperçu diffèrent de beta.308. Les **1 108 PNG** sont identiques octet pour
octet. Les copies packwiz client et serveur sont identiques au JAR vérifié ;
le template ne change que par ses notices et fichiers de libellés FR/EN.
SHA-256 de `sanctuary-beta.309.jar` :
```text
32ca2d855f71ac29af0a62ba9f4e463e3d11f172fefe7ecdbb1afad086b63ff9
```
[Reçu de validation](oven-single-output-beta309-verification.json).
Journaux de build et d'archives dans `build/oven309-build.log` et
`build/oven309-artifacts.log`.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryOven308ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=oven308 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
+8
View File
@@ -8,6 +8,14 @@ La [procédure de reprise et de publication complète](updates.md) relie aussi
le serveur dédié, le téléchargement du site et l'instance locale. Les reçus
des anciennes versions ci-dessous restent historiques.
## beta.315 — cuisine et agriculture publiées
[Reçu de livraison](release-beta315.md) : refonte It's Alive beta.299–315,
release et canal client/serveur, CT 350, téléchargement Web et même instance
Prism mis à jour. 187 tests serveur, neuf JAR clients/cinq serveur, deux passages
isolés et personnels identiques. 211 fichiers serveur et 936 fichiers personnels
protégés conservés. [Export de textures](itsalive-textures-export-beta315.md).
## beta.293 — livraison complète publiée
[Reçu de livraison](release-beta293.md) : Happy Creepers de la branche dédiée,
+80
View File
@@ -0,0 +1,80 @@
{
"version": "beta.310",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.310.jar",
"sha256": "4a2278d987b031b343a6d66694da50db0aeb408bb77785ce8c02ae0ed9174b26",
"previous_verified_sha256": "32ca2d855f71ac29af0a62ba9f4e463e3d11f172fefe7ecdbb1afad086b63ff9",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/blockstates/frying_pan.json",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"data/sanctuary/loot_table/blocks/frying_pan.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/culinary/block/FryingPanBlock.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/AbstractTimedCulinaryBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/block/entity/FryingPanBlockEntity.class",
"fr/koka/sanctuary/cuisine/culinary/client/FryingPanHud.class",
"fr/koka/sanctuary/cuisine/culinary/client/renderer/FryingPanBlockEntityRenderer.class",
"fr/koka/sanctuary/cuisine/culinary/client/renderer/state/FryingPanRenderState.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [
"assets/sanctuary/models/block/frying_pan_layer_2.json",
"assets/sanctuary/models/block/frying_pan_layer_3.json",
"assets/sanctuary/models/block/frying_pan_layer_4.json"
],
"removed_jar_entries": [],
"generated_json": {
"cuisine": 432,
"kitchen": 195,
"agriculture": 376
},
"culinary_source_classes": 44,
"culinary_compiled_classes_verified": 61,
"pan_recipes_unchanged": 26,
"physical_pan_counts": [
1,
2,
3,
4
],
"heights_pixels": [
4,
8,
12,
16
],
"pan_output_slots": 1,
"shared_recipe_per_stack": true,
"native_inventory_size": 17,
"original_indices_preserved": [
0,
1,
2,
3,
4
],
"unchanged_pngs": 1108,
"server_required": 76,
"pan_scenarios": 13,
"client_vulkan": {
"log": "build/pan310-client.log",
"screenshots": "build/pan310-screenshots",
"screenshot_count": 13,
"world_seed": 310,
"sodium": "0.9.2+mc26.3"
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/blockstates/frying_pan.json",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"template_added_entries": [
"assets/sanctuary/models/block/frying_pan_layer_2.json",
"assets/sanctuary/models/block/frying_pan_layer_3.json",
"assets/sanctuary/models/block/frying_pan_layer_4.json"
],
"build_log": "build/pan310-build.log"
}
+147
View File
@@ -0,0 +1,147 @@
# CUI-310 / IA-07 — poêles en ligne et piles de quatre
Choix du créateur le 28 octobre 2026 : **une recette commune par pile**.
Le modèle actuel fait quatre pixels de haut ; quatre poêles tiennent donc
dans un bloc. Chaque emplacement reste chauffé par-dessous. Des piles côte
à côte peuvent préparer des plats différents. Préparation locale **beta.310**
sur `codex/itsalive-26-3`, sans publication, installation ou déploiement.
## Contrat de stockage et de continuité
Ce lot ajoute une capacité réelle pour les ingrédients non empilables :
quatre omelettes doivent réellement payer quatre seaux de lait, et non créer
quatre plats depuis un seul seau. Les quatre positions de préparation restent
communes à toute la pile, avec une seule recette et une seule sortie.
L'identifiant `sanctuary:frying_pan` du bloc, de l'objet et du bloc entité
reste stable. Le nouvel état natif `pans` vaut 1 à 4 ; une ancienne sauvegarde
sans cette propriété se charge avec **une poêle**. Aucune ancienne colonne
n'est supprimée ou regroupée automatiquement.
Les indices d'inventaire **0 à 4 restent inchangés** : ingrédients 0–3,
résultat 4. Douze indices de réserve sont ajoutés au même inventaire natif :
| Position commune | Case historique | Réserves pour les poêles 2–4 |
| --- | ---: | --- |
| Ingrédient 1 | 0 | 5, 6, 7 |
| Ingrédient 2 | 1 | 8, 9, 10 |
| Ingrédient 3 | 2 | 11, 12, 13 |
| Ingrédient 4 | 3 | 14, 15, 16 |
| Plat terminé | 4 | Aucune autre sortie |
La lecture des anciennes cinq cases conserve chaque objet à son indice,
avec douze réserves vides. Les champs de cuisson et d'assemblage sont gardés.
Le champ additionnel `CookingPanCount` mémorise le nombre de poêles associé
à l'avancement, avec 1 par défaut pour l'ancien format. Un ajout de poêle
pendant la cuisson remet l'assemblage et l'avancement à zéro, y compris si
la pile est sauvegardée avant le tick suivant. L'indication des récipients à
récupérer est dérivée des objets et synchronisée sans nouvelle case de sortie.
Les piles physiques respectent toujours la taille maximale native des objets :
quatre seaux de lait restent quatre piles d'un seau, sans pile illégale.
Les quantités agrégées servent uniquement à la résolution de la recette.
La quantité réellement consommée est prélevée dans les cases physiques et
les récipients payés sont rendus une seule fois.
Ce contrat décrit une évolution **vers beta.310**. Pour revenir à une version
antérieure, il faut d'abord vider les réserves et récupérer les poêles de la
pile : une ancienne version ne connaît ni les nouveaux indices ni leur compte.
Aucun monde personnel n'est ouvert ou converti par cette préparation ; la
continuité est vérifiée dans des sauvegardes et mondes de développement neufs.
## Utilisation
- Poser une poêle, puis utiliser d'autres poêles sur le même bloc pour atteindre
deux, trois ou quatre couches. Chaque ajout consomme un objet ; la casse
rend toutes les poêles et chaque pile réelle d'ingrédients une seule fois.
- Chauffer chaque emplacement par-dessous avec une source reconnue. Une poêle
ne transmet pas la chaleur à un autre bloc de poêle au-dessus. Les anciennes
colonnes restent présentes ; la pose neuve se prolonge horizontalement.
- Ajouter les ingrédients par clic droit, un objet à la fois. Les quatre
positions de recette et leurs réserves sont partagées. Quatre seaux de lait
pour quatre omelettes occupent quatre cases natives distinctes.
- L'assemblage conserve sa fenêtre de 30 secondes, ou son départ après trois
secondes de stabilité si les quatre positions d'une recette sont remplies.
Les réserves d'un même ingrédient ne comptent pas comme quatre positions.
- Une cuisson traite au plus autant de recettes complètes que de poêles,
pendant la durée historique du plat. Deux œufs dans quatre poêles donnent
deux œufs au plat. Une préparation invalide paie aussi ses plats ratés.
- Les ingrédients restent présents jusqu'au paiement final. Modifier une case
annule l'avancement. Ajouter une poêle relance aussi l'assemblage.
- Maj-clic récupère d'abord la sortie commune, puis les ingrédients ou
récipients. Les récipients revenus dans une position vidée doivent être
collectés avant de commencer une nouvelle préparation. Si des ingrédients
restent dans cette position, les récipients sont déposés dans le monde.
- Un entonnoir peut alimenter les réserves par les côtés ou le dessus selon
les limites natives. La face inférieure expose seulement la sortie ; une
chaleur réelle doit toujours être présente juste sous la poêle pour cuire.
Les 26 recettes, leurs durées, les textures et les catégories créatives sont
conservées. Les piles voisines peuvent cuire des plats différents. Le contenu
commun apparaît dans la poêle supérieure, et le HUD FR/EN affiche le nombre
de poêles, le temps, la quantité terminée ou les récipients à récupérer.
## Validation locale
Le parcours client sur un monde plat de développement neuf, graine **310**,
a réussi sous **Vulkan / Apple M1**, avec **Sodium 0.9.2+mc26.3** :
- vrais paquets de pose en survie, paiement des quatre objets, plafond à
quatre et prolongement horizontal ;
- vrais clics d'ajout pour quatre œufs et quatre seaux de lait non empilables ;
- huit états de modèle chargés, 30 faces par couche, hauteurs visibles exactes
et texture historique résolue sur chaque face ;
- HUD français/anglais aux échelles 2, 3 et 4, assemblage, cuisson et quantité
prête ; les autres piles cuisent chacune leur propre bœuf ;
- ticks réels pour la cuisson, quatre omelettes payées, quatre seaux rendus ;
- fermeture et réouverture réelle du monde, récupération par Maj-clic des
quatre plats puis des quatre seaux, conservés ensemble dans l'inventaire ;
- 26 recettes historiques présentes dans JEI.
La fenêtre d'assemblage est avancée par une fixture après les interactions
réelles, pour ne pas attendre 30 secondes avant la cuisson ; le paiement,
la progression et les durées de cuisson restent réels. Treize captures sont
archivées dans `build/pan310-screenshots/` (ignoré). Le parcours client dure
47 secondes et son journal est `build/pan310-client.log` (ignoré).
Les treize nouveaux cas serveur vérifient la pose native, formes et loot,
les quatre capacités, les **26 recettes à quatre portions**, le lait et ses
récipients, l'ajout d'une poêle avant un paiement et avant sauvegarde,
les recettes voisines et l'absence de chauffe transmise verticalement,
la casse, les 17 indices, les anciennes cinq cases sans propriété `pans`,
les quantités insuffisantes et préparations ratées, le retrait d'une réserve
juste avant la fin et l'insertion par un entonnoir natif.
Commandes de vérification :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryPan310ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=pan310 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Les orientations supplémentaires et les ancrages particuliers du ticket IA-07
restent ouverts. Ce lot réalise le choix confirmé de piles partageant une
recette, avec des ingrédients réellement stockés et payés.
`check build assemblePack` a réussi en **3 min 27 s**, avec **76 tests serveur
obligatoires**. La comparaison stricte à beta.309 confirme les six classes
prévues, le modèle par couches, le loot, les trois libellés FR/EN, la version
et les crédits. Les 26 recettes de poêle et les **1 108 PNG** de l’archive
sont identiques. Les 432 JSON de cuisine, 195 d’appareils et 376 d’agriculture
sont comparés à leurs générateurs. Les copies client/serveur du JAR sont
identiques ; le modèle de pack de ressources emporte les trois nouveaux
modèles et leur état multipart.
[Reçu de vérification](pan-stacks-beta310-verification.json). JAR local :
`mods/sanctuary/build/libs/sanctuary-beta.310.jar`
SHA-256 :
`4a2278d987b031b343a6d66694da50db0aeb408bb77785ce8c02ae0ed9174b26`
Aucun push, tag, canal packwiz public, installation Prism ou serveur
personnel n’a été modifié.
+97
View File
@@ -0,0 +1,97 @@
# beta.298 — normales de l'ancre corrigées et installées
Livraison complète du **27 octobre 2026**, branche `codex/media-anchor-model`.
[Correctif et preuve graphique](media-anchor-normals-beta298.md),
[notes de version](notes-beta298.md).
Avec les shaders Sanctuary activés, le petit cadre invisible ne laisse plus
de normales PBR sur la vidéo dans le coin inférieur gauche. Son modèle de
bloc est vidé et la collecte des matériaux ignore les modèles invisibles.
Le grand cadre, le lecteur et les interactions sont conservés. Les écrans
existants gardent leur source, leurs dimensions et leur identité ; aucun
bloc à reposer ni conversion de données.
## Cibles vérifiées
| Cible | État |
| --- | --- |
| Source et tag `beta.298` | `fb8705369e3834c66d965108d609ec704b584ec2` |
| Canal client/serveur | `7fa08625a4fa89391d64b4b4fc0859611ecf0952` |
| Release | [beta.298](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.298), quatre artefacts publics vérifiés |
| CT 350 | Service actif, Sanctuary beta.298, Media 0.3.2 embarqué et Dream Displays 1.10.4 chargé |
| CT 320 | Distribution `releases/beta.298/prism`, ancienne beta.297 conservée |
| Prism historique | Même instance Sanctuary Beta, neuf JAR, deux synchronisations identiques |
Minecraft 26.3, Fabric 0.19.5, Java 25 et les dépendances restent inchangés.
Un JAR Sanctuary contient JEI et Media ; Dream est distinct et obligatoire
des deux côtés. Le serveur compte cinq JAR. Le présent reçu est ajouté après
la publication sans déplacer le tag immuable.
## Vérifications
- Exécution fraîche `check build assemblePack assembleServerPack` réussie
en **2 min 50 s**, 158 tâches, **18 GameTests requis**, profil
`property270,claims272,claims277,claims278`, atlas ciblé. Contrats Media inclus.
- JAR reconstruit strictement identique au binaire vérifié dans MEDIA-07 :
deux clients Vulkan avec PBR actif et vidéo en mouvement, zéro face
résiduelle, ancre exclue des matériaux, gros plans de face et en vue rasante.
Ces essais précèdent la publication ; aucune nouvelle séance graphique ou
mesure de synchronisation n'est revendiquée pendant ce déploiement.
- Archives, profils et hashes vérifiés. Deux passages du véritable
installateur en instance isolée, puis deux dans Prism personnel : second
passage identique, aucun doublon Sanctuary, sélections facultatives conservées.
- Démarrage CT confirmé par `Done` et versions chargées dans le journal.
Ping DNS/SRV depuis le LAN : Minecraft 26.3, 0/20 lors du contrôle, icône
serveur inchangée. Cela ne prouve pas un accès depuis un autre réseau.
- ZIP et MRpack publics en HTTP 200, hashes attendus ; nom reçu
`Sanctuary-Beta.zip`, version beta.298 et lien direct dans le HTML public.
- Faux code d'admission refusé (`invalid`, HTTP 200), aucun redeem.
Métriques Web fraîches, serveur en marche, même UUID communautaire
`cc6f4f5a-e58d-409a-879f-81daecf59461` côté jeu et site.
## Artefacts
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.298.jar` | 18301295 | `e6fdc36a212dcfcc89d31fbb29bc8eeec9d002cc9223ea6dcbfb31b7df17ec23` |
| `Sanctuary-beta.298.mrpack` | 16890840 | `29f88803b0abc88443a32830ca0b8a1e0490de1543f8164bed1ce86c26aff10e` |
| `Sanctuary-Prism-beta.298.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
| `Sanctuary-Server-beta.298.zip` | 4036721 | `c33ac4c08bb8b99598c71d44e1e5047f0810c4938f68d9d393507b950e865d43` |
Le ZIP Prism reste identique à beta.297 : mêmes outils et plateforme, canal
stable maintenant en beta.298. Le kit serveur ne change que son README pour
aligner le nom du kit courant ; ses outils sont inchangés. Cette documentation
a été assemblée par `scripts/server_pack.py` après le build complet réussi.
## Conservation
Une connexion était active avant l'arrêt de maintenance. Sauvegarde complète
privée après arrêt propre :
`/root/sanctuary-beta-backups/before-beta298-20261027T045721Z/instance.tar.gz`,
SHA-256 `1310c975169f1b33ecb3b3494dbb0a12a8f9815b45e2240d25c509c19c5dc755`.
Les **177 fichiers protégés** sont identiques après synchronisation, avant
redémarrage. Environnement privé et clés/valeurs de `server.properties`
identiques après démarrage. Aucune migration ni régénération de monde.
Prism : sauvegarde sous la même instance,
`sanctuary-backups/before-beta298-20261027T045406Z/instance.tar.gz`, SHA-256
`692b1989f5dac8c2bc28a2fb2f7842937b5e28eeefeb4e30d428169ecc175d0e`.
Les **933 fichiers personnels contrôlés**, `instance.cfg` et l'installateur
habillé Sanctuary sont inchangés. Le lien FFmpeg existant est conservé.
Aucun monde personnel ouvert. L'autre instance importée n'est pas modifiée
manuellement ; elle suivra le canal au prochain lancement.
Web : HEAD `eadc30e8c7b6dda50bc810584af057235e7fdba8` et **73 changements
préexistants** conservés, empreintes Git avant/après identiques. Distribution
basculée atomiquement ; pas de remplacement de code ou de cache. Alpha,
réseau/NAT, comptes, permissions et chantier du checkout principal préservés.
## Limites
Les limites du parcours graphique MEDIA-07 restent valables : mur orienté sud,
pas de nouvelle validation de source publique, arrêt tardif d'un client par
manque d'espace pour les journaux de la sonde. Ce n'est pas un essai d'endurance.
La sonde n'est pas distribuée. Aucun nouveau problème d'espace n'est survenu
pendant cette livraison ; environ 2,5 Gio restaient au contrôle final local.
Reçus et journaux opérationnels : `build/release298/`, hors Git.
+95
View File
@@ -0,0 +1,95 @@
# beta.315 — cuisine et agriculture publiées
Livraison complète du **29 octobre 2026**, branche `codex/itsalive-26-3`.
[Notes de version](notes-beta315.md), [export de textures](itsalive-textures-export-beta315.md).
Les incréments beta.299 à beta.315, jusque-là locaux, sont réunis : cultures,
vanille grimpante, cuisine et appareils, écrasement, séchoir et saucisson,
fromage à partager et quatre qualités de vin.
## Cibles vérifiées
| Cible | État |
| --- | --- |
| Source/tag `beta.315` | `359f49792bbe401acb53da121babaca451744b9c` |
| Canal client/serveur | `a5df95c2d43a2e80d1215a1bca39528f4ce55957` |
| Release publique | [beta.315](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.315), quatre artefacts anonymes vérifiés |
| CT 350 | Service actif, Sanctuary beta.315, Media 0.3.2 et Dream Displays 1.10.4 chargés |
| CT 320 | Distribution `releases/beta.315/prism`, ancienne beta.298 conservée |
| Prism historique | Même instance Sanctuary Beta, neuf JAR, deux passages identiques |
Minecraft 26.3, Fabric Loader 0.19.5, Java 25 et les dépendances sont conservés.
Le serveur a cinq JAR, JEI et Media sont embarqués dans Sanctuary.
Ce reçu est committé après publication sans déplacer le tag immuable.
## Contrôles
- Build frais `check build assemblePack assembleServerPack`, **2 min 38 s**,
162 tâches et **187 GameTests requis** : cuisine complète, agriculture,
vanille, écrasement/super piston, progression/recettes et contrôles de
propriété/admission. Les contrats smoke du dépôt sont inclus.
- Le JAR reste strictement identique au binaire final audité beta.315,
[134 tests ciblés et contrôles de la préparation](sausage-slices-beta315.md).
Les deux parcours **Vulkan / Sodium** de cette préparation couvrent le
séchoir et les parts posées. Aucun jeu personnel n'est ouvert au déploiement.
- MRpack : huit dépendances distantes et un override Sanctuary vérifié.
ZIP Prism et kit serveur identiques à beta.298 : outils, plateforme et canal
stable inchangés ; le canal fournit maintenant le nouveau mod.
- Deux vrais passages de l'installateur en instance isolée puis deux dans
l'instance personnelle : neuf JAR, aucun doublon, second passage identique.
- CT 350 : versions chargées et `Done` dans le journal, propriétés identiques
par clés/valeurs, environnement privé et UUID communautaire conservés.
Vérification authentifiée d'un code fictif : HTTP 200, `invalid`, aucun redeem.
- Ping par DNS/SRV depuis le LAN : Minecraft 26.3, **0/20**, port public 25567,
icône inchangée. Cette preuve ne couvre pas un réseau extérieur.
- Site public : ZIP et MRpack en HTTP 200, hashes attendus, nom du ZIP reçu
`Sanctuary-Beta.zip`, bouton direct, survol et footer beta.315.
Métriques `running=true`, `fresh=true`, même UUID communautaire.
## Artefacts
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.315.jar` | 19031856 | `3767fe61832e1193367a09fa9e8c9c1df1a2f5ccf4802bc7280d2cfbe665ce78` |
| `Sanctuary-beta.315.mrpack` | 17335224 | `d1811f1e892ab41147b55280976fc8fc24bd20b867ff8b17d94217d98f72b1e1` |
| `Sanctuary-Prism-beta.315.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
| `Sanctuary-Server-beta.315.zip` | 4036721 | `c33ac4c08bb8b99598c71d44e1e5047f0810c4938f68d9d393507b950e865d43` |
## Conservation
CT : arrêt avant avance du canal ; le wrapper retourne 143 après SIGTERM et
aucun Java Minecraft ne reste actif avant la sauvegarde.
Sauvegarde privée `/root/sanctuary-beta-backups/before-beta315-20261029T085359Z/instance.tar.gz`, SHA-256
`58993afceff58adfa8b705a727ca19d5be24269065d8c23547088847daf9c77e` ; environnement privé copié à part en accès root.
**211 fichiers protégés** identiques après mise à jour,
avant redémarrage. Aucune régénération ni conversion explicite de monde.
Prism : sauvegarde `/Users/koka/Library/Application Support/PrismLauncher/instances/Sanctuary-0.1.0-alpha.1/sanctuary-backups/before-beta315-20261029T085045Z/instance.tar.gz`, SHA-256
`f418c47484ecb67a8301f4ae6010b60f7c5c34980400e8fea44baf6f6b8c69a7`. **936 fichiers personnels**
contrôlés inchangés, avec `instance.cfg`, réglages, sauvegardes, captures,
packs personnels, installateur au logo et lien FFmpeg. Aucune instance créée
ou réimportée dans Prism ; l'instance isolée de vérification reste dans build.
Web : publication limitée à la distribution, sans pull/reset, déploiement de
code, migration DB ou changement de cache. HEAD observé
`eadc30e8c7b6dda50bc810584af057235e7fdba8`, **73 changements préexistants**.
Le premier relevé Git préalable a été refusé par le contrôle de propriétaire ;
le relevé final utilise une exception de confiance limitée à la commande,
sans modifier la configuration Git. Aucun fichier applicatif n'est écrit
par le script de publication. Alpha, NAT, comptes et permissions hors cible.
## Textures éditables et limites
Export dans `Downloads/ItsAlive-Textures-beta.315/`, ZIP de même nom :
**347 PNG de la refonte**, **368 images modifiables** avec les matériaux
réutilisés et **696 JSON visuels**. Les 347 PNG correspondent exactement au
JAR. Copies séparées pour les matériaux du fromage, des saucissons et du
séchoir, et un PNG propre à chaque vin de garde. Le pack éditable n'est pas
activé automatiquement. [Mode d'emploi](itsalive-textures-export-beta315.md).
Les cultures sont jouables ; leur apparition naturelle/répartition climatique
reste hors de cette livraison. Le créateur prévoit maintenant ses essais de
saucisson et de cuisine : les contrôles techniques ne valent pas validation de
ses préférences d'équilibrage ou de présentation. Les limites Media/voix et
réseau des livraisons précédentes restent inchangées.
Reçus et journaux opérationnels : `build/release315/`, hors Git.
@@ -0,0 +1,77 @@
{
"version": "beta.315",
"jar": "mods/sanctuary/build/libs/sanctuary-beta.315.jar",
"sha256": "3767fe61832e1193367a09fa9e8c9c1df1a2f5ccf4802bc7280d2cfbe665ce78",
"previous_verified_sha256": "82a2a6d8d4e2ad9d0aab754037995609722bbebac28db89a6e641c851ccb3ceb",
"changed_jar_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json",
"fabric.mod.json",
"fr/koka/sanctuary/cuisine/KitchenAppliances.class",
"fr/koka/sanctuary/cuisine/KitchenClient$1.class",
"fr/koka/sanctuary/cuisine/KitchenClient.class",
"resourcepacks/template/THIRD_PARTY_NOTICES.md"
],
"added_jar_entries": [
"assets/sanctuary/blockstates/cured_sausage.json",
"assets/sanctuary/models/block/cured_sausage.json",
"assets/sanctuary/models/block/cured_sausage_slice_1.json",
"assets/sanctuary/models/block/cured_sausage_slice_2.json",
"assets/sanctuary/models/block/cured_sausage_slice_3.json",
"data/sanctuary/loot_table/blocks/cured_sausage.json",
"fr/koka/sanctuary/cuisine/culinary/block/CuredSausageBlock.class"
],
"removed_jar_entries": [],
"generated_json": {
"cuisine": 432,
"kitchen": 234,
"agriculture": 376
},
"additive_sausage_json": 6,
"culinary_source_classes": 53,
"culinary_compiled_classes_verified": 73,
"sausage_slices": 4,
"food_per_slice": 2,
"total_food": 8,
"saturation_modifier": 1.2,
"horizontal_orientations": 4,
"block_states": 16,
"existing_drying_recipe_and_raw_cured_item_models_unchanged": true,
"old_pngs_unchanged": 1108,
"new_pngs": 0,
"server_required": 134,
"sausage_scenarios": 8,
"client_vulkan": {
"log": "build/sausage315-client.log",
"screenshots": "build/sausage315-screenshots",
"screenshot_count": 12,
"world_seed": 315,
"sodium": "0.9.2+mc26.3",
"native_paid_rack_insertion_and_collection": true,
"native_crosshair_key_placement_and_eating": true,
"actual_disk_orientation_and_partial_sausage_reopen": true,
"four_native_eating_sounds": true,
"fixture_advances_only_drying_progress": true
},
"drying_regression_vulkan": {
"log": "build/sausage315-drying-regression.log",
"native_four_preparations_and_hooks": true,
"actual_bucket_and_disk": true,
"placed_slice_tasting": true
},
"client_server_copies_identical": true,
"template_changed_entries": [
"THIRD_PARTY_NOTICES.md",
"assets/sanctuary/lang/en_us.json",
"assets/sanctuary/lang/fr_fr.json"
],
"template_added_entries": [
"assets/sanctuary/blockstates/cured_sausage.json",
"assets/sanctuary/models/block/cured_sausage.json",
"assets/sanctuary/models/block/cured_sausage_slice_1.json",
"assets/sanctuary/models/block/cured_sausage_slice_2.json",
"assets/sanctuary/models/block/cured_sausage_slice_3.json"
],
"build_log": "build/sausage315-build.log"
}
+111
View File
@@ -0,0 +1,111 @@
# IA-10 — saucisson posé à partager — beta.315 locale
Demande du 29 octobre 2026 : poser le saucisson séché puis en manger des tranches,
comme le fromage. Même branche `codex/itsalive-26-3`, sans publication ni
installation. Premier réglage : quatre tranches de deux points de nourriture,
pour conserver les huit points du saucisson entier de beta.313.
## Contrat de continuité avant modification
L'objet `sanctuary:cured_sausage`, sa recette et son modèle restent stables.
Il devient le BlockItem du nouveau bloc du même identifiant. Comme le fromage,
il se mange désormais après la pose plutôt qu'entier dans la main. Les objets
existants deviennent posables sans conversion. Le cru, les quatre crochets,
les cinq minutes et les règles d'humidité du séchoir restent inchangés.
Le bloc n'a pas de block entity. Son état natif conserve `bites=0..3` et les
quatre orientations horizontales `facing`. Ces données additives se sauvegardent
par les mécanismes standard des blocs. Le modèle et la forme raccourcissent à
chaque tranche. Aucune migration de chunk, génération ou monde personnel n'est
nécessaire ou exécutée. Comme le gâteau et le fromage, casser le saucisson ou
lui enlever son support ne rend pas un saucisson entier ; les tranches mangées
ne peuvent pas être reconstituées gratuitement. Avant un retour à beta.314,
manger les saucissons posés ; les objets non posés gardent leur identifiant.
## Geste et apparence
Poser sur un bloc solide consomme un saucisson séché. Clic droit pour manger
une tranche si le joueur a faim ; la quatrième enlève le bloc. Aucun couteau,
menu, combustible ou nouvel objet « tranche » n'est ajouté. Chaque tranche
conserve le modificateur de saturation de 1,2 ; la faim totale reste de huit.
Un comparateur lit 8/6/4/2 selon les tranches restantes.
Les nouvelles géométries cubiques reprennent les références du modèle d'objet :
terre cuite rouge, laine blanche et bloc de champignon rouge pour la coupe.
La ficelle du bout coupé disparaît à la première tranche ; la ficelle et les
bandes encore présentes restent à leur vraie position sur le morceau restant.
Les modèles d'objet cru/séché et leurs rendus sur le séchoir restent intacts.
Aucun PNG ou son n'est ajouté. Les sons natifs de mastication sont référencés.
## Vérifications
Huit cas serveur natifs vérifient le paiement de la pose, les quatre vraies
prises de tranches et leurs huit points de nourriture, la faim pleine,
les bougies, les seize états de forme et de comparateur, la rotation et le
miroir, le codec standard de sauvegarde d'un saucisson entamé et orienté,
la casse de chacun des quatre états, la perte de support et la pose refusée
sans consommation. Le dernier cas exécute **6 000 ticks complets** du séchoir
sur une vraie préparation, puis pose le résultat payé du même identifiant.
Le parcours **Vulkan / Apple M1 / Sodium 0.9.2+mc26.3** utilise un monde plat
jetable de graine **315**, une préparation crue fournie en fixture et de vraies
touches d'utilisation pour l'accrocher, récupérer le résultat, poser le
saucisson puis manger ses quatre tranches. Le viseur doit réellement toucher
la forme du bloc avant chaque action. Le serveur confirme le paiement de
chaque étape et les deux points de nourriture de chaque tranche.
Les seize modèles sont chargés : huit cuboïdes pour l'entier, cinq pour chaque
portion raccourcie, références de textures natives valides et tous les sommets
à l'intérieur de la forme orientée correspondante. L'objet reste dans l'onglet
Nourriture et boissons et son modèle historique reste valide. FR/EN et GUI
2/3/4 sont contrôlés. Une fermeture/réouverture réelle conserve le bloc avec
**deux tranches déjà mangées** et la même orientation ; les deux autres se
mangent sur ce même morceau. Les quatre sons natifs de mastication sont captés.
Douze images sont conservées dans `build/sausage315-screenshots/` (ignoré).
Ce parcours graphique rapproche seulement la progression du séchage à dix
ticks de sa fin pour ne pas attendre cinq minutes ; les derniers ticks et
toutes les interactions restent natifs. Les cas serveur exécutent la durée
complète. Le parcours graphique a réussi en **1 min 14 s**. Le contrôle
historique beta.313 est adapté pour goûter le résultat après sa pose, et son
assertion d'objet comestible devient celle d'un BlockItem à partager.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=sausage315,cheesewine314,drying313,cauldron312,cauldron311,pan310,oven308,mill306,kitchen304,cuisine303,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuarySausage315ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=sausage315 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Le parcours historique complet du séchoir est également rejoué sous Vulkan
avec Sodium et réussit en **1 min 16 s** : fabrication payée des quatre
préparations, quatre crochets, retrait/raccrochage, seau d'eau, sauvegarde et
vidage au seau, quatre saucissons récupérés puis pose et dégustation d'une
tranche du même résultat. Sa table de dégustation est surélevée pour rester
au sec après l'essai d'écoulement. Les miroirs de structure vérifient aussi
la réflexion réelle de la forme restante sur X et Z pour les seize états,
et pas seulement la conservation du nombre de tranches.
`check build assemblePack` a réussi en **3 min 19 s**, avec **134 tests serveur
obligatoires**, dont les assertions de réflexion de coupe. L'audit strict à
beta.314 vérifie le nouveau bloc, ses six JSON ajoutés et les modifications
prévues de registre, libellés et crédits. Les **1 108 PNG** et tous les anciens
JSON de recettes/modèles sont inchangés. Les générateurs sont comparés au JAR :
432 JSON de cuisine, 234 d'appareils et 376 d'agriculture. Les 53 sources et
73 classes compilées culinaires sont vérifiées. Les assemblages client/serveur
embarquent exactement le même JAR et le modèle de pack de ressources reprend
les cinq nouveaux JSON visuels. Aucun fichier d'objet ou de modèle de séchoir
historique n'a changé.
[Reçu d'audit](sausage-slices-beta315-verification.json).
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.315.jar`.
SHA-256 :
`3767fe61832e1193367a09fa9e8c9c1df1a2f5ccf4802bc7280d2cfbe665ce78`
Aucun push, tag, publication, canal packwiz public, installation Prism ou
serveur personnel n'a été modifié.
+1 -1
View File
@@ -8,7 +8,7 @@ Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
## Préparer le kit
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
Le kit de la livraison courante est `build/Sanctuary-Server-beta.297.zip` ; les manifestes locaux sont
Le kit de la livraison courante est `build/Sanctuary-Server-beta.298.zip` ; les manifestes locaux sont
dans `build/packwiz-server/`. Le ZIP est un amorçage : il télécharge les mods
depuis le canal au premier démarrage, il n'est pas une copie hors ligne du pack.
Il n'est utilisable sur le canal public qu'après publication de cette version.
+21
View File
@@ -242,6 +242,27 @@ gérés au second passage, mêmes données personnelles avant/après. Conserver
`saves/`, captures, favoris, réglages, packs graphiques et configurations tierces.
Ne pas ouvrir un monde personnel pour prouver une simple synchronisation.
### Essai local explicitement demandé sans publication
Pour ce périmètre, ne pas publier le canal et ne pas remplacer seulement le JAR
alors que le pré-lancement suit une ancienne version publique. La beta.318 a
validé un profil packwiz autonome copié dans l’instance existante sous
`sanctuary-local-packs/beta.318/`. L’installateur v0.5.14 accepte un chemin local
pour le pack ; le JAR Sanctuary est une entrée directe de cet index local.
Les huit métadonnées des dépendances et leurs choix restent inchangés.
Après les essais isolés et la sauvegarde, remplacer seulement l’argument du
pack dans les deux `PreLaunchCommand` par le chemin cité
`"$INST_DIR/sanctuary-local-packs/beta.318/pack.toml"`. Garder le bootstrap au logo,
`--bootstrap-no-update` et tous les autres réglages. Vérifier deux passages réels,
l’unicité du JAR et les hashes personnels ; ne pas lancer un monde personnel.
Le profil doit être hors du worktree pour rester disponible aux lancements suivants.
Le pré-lancement demeure sur ce profil pendant l’essai. Le rétablissement de la
commande publique est une étape explicite d’une future livraison compatible,
après lecture de la configuration courante et de sa sauvegarde. Ne pas réutiliser
les scripts ponctuels d’une ancienne intervention. [Reçu beta.318](local-beta318.md).
## Reprise et reçu de livraison
Pour un échec, conserver l'état/les logs, revenir au dernier artefact immuable
+57
View File
@@ -0,0 +1,57 @@
# IA-03 — Vanille en 16 × 16, beta.302 locale
Correction du 28 octobre 2026 sur `codex/itsalive-26-3`, après le retour du
créateur sur les textures 32 × 32 de beta.301. Les quatre stades de vanille
doivent être des **PNG de 16 × 16 pixels réels**, comme les textures vanilla.
Le dessin est simplifié pour cette grille : jeune tige, feuillage, fleurs,
puis gousses vertes. Feuilles et gousses restent dans un même sprite sur le
plan unique contre le tronc. Les fichiers conservent leurs identifiants
`sanctuary:block/vanilla_climbing_stage0` à `3` ; la pousse ascendante, la
récolte, les supports et les onglets créatifs de beta.301 sont conservés.
L'[atlas de production et le prompt](../ressources-pack/sources/vanilla-beta301/README.md)
documentent le travail avec l'outil intégré imagegen. L'atlas contient quatre
cellules 16 × 16 ; les PNG de jeu sont extraits de ces cellules. L'ancien
atlas conceptuel reste une référence historique. Les 343 PNG du reçu beta.299
ne sont pas modifiés. Aucune génération, migration ou installation modifiée.
Le parcours client vérifie désormais la taille **16 × 16 de chaque sprite
effectivement chargé dans l'atlas Minecraft**, pour tous les modèles de
vanille, en plus du plan unique, de la culture en survie et du rechargement.
Le parcours **Vulkan avec Sodium 0.9.2** passe en **1 min 16 s** : les
64 états sont contrôlés, avec les quatre sprites chargés en 16 × 16 et un
plan recto verso par face. Après fermeture et réouverture du monde de test,
les 24 segments conservent exactement leurs faces et leur maturité.
Commandes de vérification :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=vanilla301,agriculture300,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryVanilla301ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=vanilla301 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
`check build assemblePack` passe en **3 min 18 s**, avec **46 tests serveur
requis** réussis. Le JAR, ses copies packwiz client/serveur et le template
contiennent les quatre nouveaux PNG 16 × 16 identiques aux sources. Les
**13 PNG de vanille présents dans le mod** sont tous contrôlés en 16 × 16.
Les 376 JSON agricoles et les notices sont vérifiés dans les archives ; les
343 PNG historiques restent identiques, dont les 38 images fournies.
SHA-256 de `sanctuary-beta.302.jar` :
```text
5959fa5e2fab808c55e011fc578171bc8cadca9d89d7170795051fc19fa01c0c
```
Journaux et reçu dans `build/vanilla302-*`, captures `vanilla302-16px-*`
dans le dossier de test client ignoré. Minecraft 26.3, graine de test 301,
Vulkan exclusivement.
Même branche en local, aucun push, tag, publication, serveur ou Prism mis à jour.
+146
View File
@@ -0,0 +1,146 @@
# IA-03 — Vanille grimpante, beta.301 locale
**Suite en beta.302 :** [les quatre textures sont reprises en 16 × 16](vanilla-16px-beta302.md).
Le reçu ci-dessous conserve la préparation initiale beta.301 en 32 × 32.
Préparation du **28 octobre 2026**, même branche `codex/itsalive-26-3`, sur
le port agricole beta.300 (`ed11cd77`). Minecraft 26.3, Java 25 et les
dépendances sont inchangés. Aucun push, tag, publication, serveur ou Prism
mis à jour ; seuls les mondes de développement neufs servent aux essais.
## Plante attachée aux arbres
La bouture se pose **contre une face latérale d'une bûche**, à n'importe
quelle hauteur, sans exiger de terre au pied. Les supports par défaut sont
les bûches et bois des arbres de l'Overworld, y compris les variantes
écorcées : tag `sanctuary:vanilla_supports`, qui reprend
`#minecraft:logs_that_burn`. La pierre, le feuillage, le sol sous le plant
et les tiges du Nether ne sont pas des supports. La plantation sous l'eau
est refusée sans consommer de bouture.
Chaque segment peut porter plusieurs faces attachées, à la manière des
lianes. Poser une autre bouture ajoute une face disposant de sa propre bûche
sans réinitialiser l'âge du segment. Aucun plafond ou segment flottant ne
sert d'appui. La plante n'a pas de collision centrale et le tag vanilla
`minecraft:climbable` permet l'escalade native.
Le rendu reste **plaqué dans le premier seizième de bloc contre l'écorce**.
À la demande du créateur, feuilles, fleurs et gousses sont réunies dans
**une seule texture sur un seul plan**, sans couches décalées. Quatre nouveaux
sprites RGBA de 32 × 32 pixels suivent la maturité : jeune plant, feuillage,
fleurs, gousses vertes. Ils prennent la liane Minecraft comme référence de
structure et de style. Leurs couleurs sont intégrées, sans teinte de biome.
Aucun modèle en croix, cadre ou colonne au centre de la cellule.
L'[atlas source et les prompts](../ressources-pack/sources/vanilla-beta301/README.md)
sont conservés ; les quatre anciens PNG de vanille restent intacts.
## Croissance et récolte
Les quatre âges sont **0 à 3**. Sous une lumière d'au moins 9, les ticks
aléatoires font mûrir le segment. À maturité, la croissance crée uniquement
un **nouveau segment au-dessus**, âge 0, sur les faces qui trouvent encore
une bûche à cette hauteur. Il n'y a ni propagation vers le bas ni propagation
latérale automatique.
La pousse s'arrête à la fin du tronc, devant un bloc, une autre plante, de
l'eau ou à la limite de hauteur du monde. Elle ne remplace que l'air.
La règle native `minecraft:spread_vines` contrôle l'extension naturelle ;
la maturation reste possible lorsqu'elle est désactivée.
La poudre d'os ajoute un âge, puis fait pousser le segment supérieur lorsque
celui-ci est possible. Une tentative d'extension bloquée ne consomme rien.
Cette action volontaire reste disponible lorsque la propagation naturelle
des lianes est désactivée.
Un clic droit à maturité donne **une à trois gousses vertes**, conserve les
faces et remet le segment à l'âge 1. Même une interaction obsolète ne peut
récompenser deux fois la même récolte. Une bouture en main sert à ajouter
une face ; la poudre d'os garde son usage de croissance. La perte d'une
bûche retire seulement les faces qu'elle portait. Si plus aucune face n'a
d'appui, le bloc disparaît et rend **une bouture par segment**, indépendamment
du nombre de faces. Les autres segments soutenus par leur propre tronc restent.
Le fumoir transforme une gousse verte en une gousse de vanille préparée,
comme dans le catalogue historique. Aucun nouvel appareil culinaire n'est
introduit. Le séchoir/saucisson et les autres machines restent
dans leurs tickets respectifs.
## Objets et sauvegardes
- `sanctuary:vanilla_cutting` : bouture, dans **Naturel**, avec aide FR/EN.
- `sanctuary:green_vanilla_pod` et `sanctuary:vanilla_pod` : ingrédients dans
**Nourriture et boissons**.
- `sanctuary:vanilla_vine` : bloc, états `north`, `east`, `south`, `west` et `age`.
Le catalogue agricole contient désormais **48 objets** distincts, sans
nouvel onglet créatif. La vanille n'était pas enregistrée en beta.300 : ces
identifiants et états sont additifs, sans conversion de données existantes.
Les états sont sauvegardés par Minecraft ; aucune donnée persistante annexe.
Le modèle prend en charge les rotations et miroirs des structures natives.
Les 343 PNG du reçu beta.299 restent inchangés ; les quatre nouveaux sprites
`vanilla_climbing_stage0` à `3` s'y ajoutent. Aucune génération de biome,
apparition naturelle, expansion, réécriture de chunk ou migration du monde
26.2 n'est ajoutée. L'obtention des premières boutures en survie naturelle
reste à traiter avec le peuplement agricole.
```mcfunction
/give @s sanctuary:vanilla_cutting 16
```
## Implémentation et vérification
`VanillaVineBlock` reprend le cycle de maturité et de récolte de l'historique
26.2, avec un nouvel attachement à plusieurs faces et une croissance strictement
ascendante. Les modèles multipart, le butin, la recette et les tags sont
produits par `generateAgricultureResources` pour les formats exacts 26.3.
La session ciblée passe **13 tests serveur**, dont **12 scénarios vanille**.
Ils couvrent la pose sur les quatre côtés,
les refus de plantation, plusieurs faces et leur conservation, poudre d'os,
obstacles, appuis en hauteur, ticks naturels et règle des lianes, récolte
unique, perte du support, formes fines, escalade native, NBT, rotations,
miroirs et recette au fumoir.
Le parcours **Vulkan avec Sodium** vérifie la pose réelle en survie,
la consommation de bouture et de poudre d'os, la pousse ascendante et
la récolte par les interactions client/serveur natives. Les 64 états de modèle
sont compilés et contrôlés, dont 60 avec au moins une face ; leurs sommets
restent contre le tronc, avec un seul plan recto verso par face et le nouveau
sprite correspondant à l'âge. Les onglets créatifs et noms FR/EN sont vérifiés.
Le monde est fermé puis rouvert pour comparer les faces et la maturité des
segments réellement cultivés.
**Validation finale avec les nouvelles textures :** `check build assemblePack`
passe en **2 min 43 s**, avec **46 tests serveur requis** réussis : vanille,
agriculture beta.300, recettes, progression et connaissances alimentaires.
Le parcours Vulkan avec Sodium 0.9.2 passe en **50 s**, avec les 64 états,
un seul plan par face et **24 segments identiques après rechargement réel**.
Les captures finales montrent la galerie de troncs et un gros plan des gousses.
Le JAR et les copies packwiz client/serveur sont identiques. Les **376 JSON
agricoles générés**, classes agricoles compilées, quatre nouveaux sprites et
notices sont vérifiés dans l'archive ; le template contient les mêmes sprites.
Les 343 PNG historiques et les 38 sources fournies sont revérifiés octet pour
octet, y compris dans le JAR. SHA-256 de `sanctuary-beta.301.jar` :
```text
14057e91d6d0fa776ffc67543f8c9cd137e71d2c7616311053dd7db3b1a69043
```
Commandes de vérification :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=vanilla301,agriculture300,recipes,progression,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryVanilla301ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=vanilla301 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=4 \
-PsanctuaryClientNoVsync=true
```
Journaux et reçus ignorés dans `build/vanilla301-*`, captures et monde de test
dans `mods/sanctuary/build/run/clientGameTest/`, graine **301**. Les tests
graphiques ciblent Vulkan exclusivement.
+75
View File
@@ -0,0 +1,75 @@
# Alimentation — trouver, récolter, replanter
Catalogue **IA-09 / beta.316**, préparation locale du 30 octobre 2026.
[Planche illustrée des neuf îles](planche.html) · [Contrat de génération](../../foraging-beta316.md).
[Produits du séchoir et 18 recettes culinaires, beta.318](sechoir.md).
Les emplacements indiquent des **habitats**, pas des coffres garantis à des coordonnées fixes.
Une île peut contenir plusieurs biomes : regarder le sol et la végétation, pas seulement la direction.
Les plantes sauvages apparaissent uniquement à la décoration des **nouveaux chunks Sanctuary**.
Dans les zones déjà explorées, casser à la main les petites herbes/fougères du bon biome
donne parfois une récolte à transformer en semences (1 chance sur 8, puis choix parmi les cultures du climat).
Le blé garde ses graines vanilla. Les cisailles et Toucher de soie préservent le bloc et n'ajoutent pas de récolte.
Les grandes herbes/fougères à deux blocs ne donnent pas ces nouveaux butins : éviter un double paiement.
Pour le riz : herbes marines de jungle/mangrove (1 sur 8). Pour les boutures de vanille : lianes tropicales (1 sur 8).
Pour les arbres : feuilles de chêne/acacia des habitats chauds admissibles (1 sur 40, puis choix de la pousse).
Des chênes/acacias plantés sur place fonctionnent aussi.
Ces chemins sont renouvelables avec les mécanismes vanilla de végétation ; aucun chunk n'est repeuplé.
**Le climat détermine la découverte.** Une semence rapportée peut être cultivée chez soi,
sans verrou climatique supplémentaire. Le riz garde sa condition eau/boue, la vanille son support en bois.
Les cuissons, fermentations et séchages utilisent les appareils existants ; leurs recettes sont dans JEI.
Le chaudron doit être remué avec un bâton, le moulin est manuel, la cuisinière démarre avec son combustible.
## Les neuf îles
| Île | Habitat | Cultures et produits |
| --- | --- | --- |
| [Nord-Ouest](northwest.md) | Prairies et taïgas | Blé, Carotte, Pomme de terre, Betterave, Fraise, Laitue, Oignon, Ail, Chou, Menthe, Tomate, Maïs, Concombre, Poivron, Haricots secs, Basilic |
| [Nord](north.md) | Vallées boréales et clairières | Blé, Carotte, Pomme de terre, Betterave, Fraise, Laitue, Oignon, Ail, Chou, Menthe |
| [Nord-Est](northeast.md) | Marais frais et jardin pâle | Concombre, Laitue, Chou, Haricots secs, Menthe |
| [Ouest](west.md) | Oasis de l'archipel | Tomate, Maïs, Poivron, Aubergine, Raisin, Basilic, Citron, Olives |
| [Sanctuary](center.md) | Forêts, prairies et sous-bois tempérés | Blé, Carotte, Pomme de terre, Betterave, Fraise, Tomate, Maïs, Concombre, Poivron, Laitue, Oignon, Ail, Chou, Haricots secs, Basilic, Menthe |
| [Est](east.md) | Jungles autour du volcan | Ananas, Riz, Graines de soja, Piment, Cerises de café, Gousse de vanille verte |
| [Sud-Ouest](southwest.md) | Savane, prairie sèche et désert | Maïs, Raisin, Pois chiches, Piment, Coriandre, Olives |
| [Sud](south.md) | Bosquets et berges des canyons | Maïs, Raisin, Pois chiches, Piment, Coriandre, Olives |
| [Sud-Est](southeast.md) | Jungle, mangrove et bambou | Ananas, Riz, Graines de soja, Piment, Cerises de café, Gousse de vanille verte |
## Toutes les cultures et leurs semences
| Récolte | Où chercher | À replanter |
| --- | --- | --- |
| Fraise | Nord-Ouest, Nord, Sanctuary | Graines de fraise |
| Tomate | Nord-Ouest, Ouest, Sanctuary | Graines de tomate |
| Ananas | Est, Sud-Est | Couronne d'ananas |
| Maïs | Nord-Ouest, Ouest, Sanctuary, Sud-Ouest, Sud | Grains de maïs |
| Concombre | Nord-Ouest, Nord-Est, Sanctuary | Graines de concombre |
| Poivron | Nord-Ouest, Ouest, Sanctuary | Graines de poivron |
| Aubergine | Ouest | Graines d'aubergine |
| Laitue | Nord-Ouest, Nord, Nord-Est, Sanctuary | Graines de laitue |
| Oignon | Nord-Ouest, Nord, Sanctuary | Oignon |
| Ail | Nord-Ouest, Nord, Sanctuary | Ail |
| Chou | Nord-Ouest, Nord, Nord-Est, Sanctuary | Graines de chou |
| Raisin | Ouest, Sud-Ouest, Sud | Pépins de raisin |
| Riz | Est, Sud-Est | Riz |
| Graines de soja | Est, Sud-Est | Graines de soja |
| Haricots secs | Nord-Ouest, Nord-Est, Sanctuary | Haricots secs |
| Pois chiches | Sud-Ouest, Sud | Pois chiches |
| Piment | Est, Sud-Ouest, Sud, Sud-Est | Graines de piment |
| Basilic | Nord-Ouest, Ouest, Sanctuary | Graines de basilic |
| Menthe | Nord-Ouest, Nord, Nord-Est, Sanctuary | Bouture de menthe |
| Coriandre | Sud-Ouest, Sud | Graines de coriandre |
| Blé | Nord-Ouest, Nord, Sanctuary | Graines de blé |
| Carotte | Nord-Ouest, Nord, Sanctuary | Carotte |
| Pomme de terre | Nord-Ouest, Nord, Sanctuary | Pomme de terre |
| Betterave | Nord-Ouest, Nord, Sanctuary | Graines de betterave |
| Cerises de café | Est, Sud-Est | Grains de café verts |
| Gousse de vanille verte | Est, Sud-Est | Bouture de vanille |
| Citron | Ouest | Pousse de citronnier |
| Olives | Ouest, Sud-Ouest, Sud | Pousse d'olivier |
## Ingrédients vanilla et transformations
Pommes : feuilles de chêne. Baies sucrées : buissons des taïgas. Baies lumineuses : lianes des cavernes luxuriantes. Champignons : sous-bois et grottes admissibles. Canne à sucre : berges, puis sucre à l’établi. Cacao : cabosses des troncs de jungle. Miel : ruches avec une fiole, feu de camp conseillé. Œufs : poules ; lait : seau sur une vache ; viandes : élevage ou chasse ; poissons : pêche ou capture dans les eaux peuplées. Ces règles natives ne sont pas remplacées.
La farine et la levure alimentent les pâtes ; les récoltes servent aux recettes portées de la 26.2. Fromage, saucisson et vin sont des produits de préparation, pas des plantes à découvrir. Aucun nouveau thé, navet ou fruit absent du catalogue n’est annoncé comme jouable par cette répartition.
+125
View File
@@ -0,0 +1,125 @@
{
"temperate": [
"minecraft:plains",
"minecraft:sunflower_plains",
"minecraft:forest",
"minecraft:birch_forest",
"minecraft:old_growth_birch_forest",
"minecraft:flower_forest",
"minecraft:dark_forest",
"minecraft:dappled_forest",
"minecraft:cherry_grove",
"sanctuary_test:adventure_forest",
"sanctuary_test:adventure_flowers",
"sanctuary_test:adventure_autumn",
"sanctuary_test:adventure_cherry",
"sanctuary_test:adventure_jungle",
"sanctuary_test:adventure_swamp",
"sanctuary_test:adventure_grove",
"sanctuary_test:adventure_lush",
"sanctuary_test:adventure_marsh",
"sanctuary:cavern_bamboo_grove",
"sanctuary:cavern_birch_forest",
"sanctuary:cavern_clearing",
"sanctuary:cavern_dark_grove",
"sanctuary:cavern_dry_woodland",
"sanctuary:cavern_oak_forest",
"sanctuary:cavern_rocky_heath",
"sanctuary:layered_flower_meadow",
"sanctuary:layered_lower_grove",
"sanctuary:layered_meadow",
"sanctuary:layered_mossy_terraces",
"sanctuary:layered_rocky_terraces",
"sanctuary:population_bamboo_grove",
"sanctuary:population_birch_forest",
"sanctuary:population_clearing",
"sanctuary:population_dark_grove",
"sanctuary:population_dry_woodland",
"sanctuary:population_oak_forest",
"sanctuary:population_rocky_heath",
"sanctuary:rift_bamboo_grove",
"sanctuary:rift_birch_forest",
"sanctuary:rift_clearing",
"sanctuary:rift_dark_grove",
"sanctuary:rift_dry_woodland",
"sanctuary:rift_oak_forest",
"sanctuary:rift_rocky_heath",
"sanctuary:starter_forest",
"sanctuary:starter_forest_hydrology",
"sanctuary:temperate_birch_forest",
"sanctuary:temperate_dappled_forest",
"sanctuary:temperate_flower_forest",
"sanctuary:temperate_forest",
"sanctuary:temperate_plains",
"sanctuary:woodland_bamboo_grove",
"sanctuary:woodland_birch_forest",
"sanctuary:woodland_clearing",
"sanctuary:woodland_dark_grove",
"sanctuary:woodland_dry_woodland",
"sanctuary:woodland_oak_forest",
"sanctuary:woodland_rocky_heath"
],
"cool": [
"minecraft:meadow",
"minecraft:taiga",
"minecraft:old_growth_pine_taiga",
"minecraft:old_growth_spruce_taiga",
"minecraft:snowy_taiga",
"minecraft:snowy_plains",
"minecraft:grove",
"minecraft:windswept_hills",
"minecraft:windswept_gravelly_hills",
"minecraft:windswept_forest",
"sanctuary:north_meadow206",
"sanctuary:north_meadow207",
"sanctuary:north_old_growth206",
"sanctuary:north_old_growth207",
"sanctuary:north_snowy_old_growth207",
"sanctuary:north_snowy_plains206",
"sanctuary:north_snowy_plains207",
"sanctuary:north_snowy_taiga207",
"sanctuary:north_taiga206",
"sanctuary:north_taiga207"
],
"wet_cool": [
"minecraft:swamp",
"minecraft:pale_garden",
"sanctuary:diagonal_pale229",
"sanctuary:diagonal_pale230",
"sanctuary:diagonal_swamp226",
"sanctuary:diagonal_swamp229"
],
"warm_dry": [
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:windswept_savanna",
"minecraft:desert",
"minecraft:badlands",
"minecraft:eroded_badlands",
"minecraft:wooded_badlands",
"sanctuary:south_badlands209",
"sanctuary:south_badlands210",
"sanctuary:south_badlands211",
"sanctuary:south_badlands212",
"sanctuary:south_badlands213",
"sanctuary:south_badlands216"
],
"warm": [
"minecraft:beach",
"sanctuary:west_beach218",
"sanctuary:west_oasis218"
],
"tropical": [
"minecraft:jungle",
"minecraft:sparse_jungle",
"minecraft:bamboo_jungle",
"minecraft:mangrove_swamp",
"sanctuary:diagonal_mangrove226",
"sanctuary:east_bamboo223",
"sanctuary:east_bamboo224",
"sanctuary:east_giant_jungle223",
"sanctuary:east_giant_jungle224",
"sanctuary:east_sparse_jungle223",
"sanctuary:east_sparse_jungle224"
]
}
+31
View File
@@ -0,0 +1,31 @@
# Sanctuary — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Forêts, prairies et sous-bois tempérés.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Blé | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de blé. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Carotte | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Carotte. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Pomme de terre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pomme de terre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Betterave | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de betterave. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Fraise | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de fraise. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Tomate | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de tomate. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Maïs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Grains de maïs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Concombre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de concombre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Poivron | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de poivron. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Laitue | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de laitue. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Oignon | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Oignon. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Ail | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Ail. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Chou | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de chou. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Haricots secs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Haricots secs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Basilic | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de basilic. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Menthe | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Bouture de menthe. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
## Ressources vanilla
Pommes des chênes, champignons, miel, œufs, lait, viandes et canne à sucre selon les habitats. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+21
View File
@@ -0,0 +1,21 @@
# Est — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Jungles autour du volcan.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Ananas | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Couronne d'ananas. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Riz | Casser le riz sauvage sur les berges humides ; ou les herbes marines (seagrass) de jungle/mangrove, à la main. | Riz. Planter le grain dans une source d'eau au-dessus de boue. |
| Graines de soja | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de soja. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Piment | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de piment. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Cerises de café | Clic droit sur un caféier mûr ; transformer ses cerises en grains verts. Les herbes tropicales peuvent aussi donner un grain. | Grains de café verts. Planter un grain vert sur terre/herbe ; clic droit à maturité. |
| Gousse de vanille verte | Clic droit pour les gousses mûres ; casser un segment pour une bouture. Les lianes tropicales cassées à la main peuvent aussi donner une bouture. | Bouture de vanille. Poser la bouture contre un tronc ; elle grimpe vers le haut. |
## Ressources vanilla
Cacao sur les troncs de jungle, bambou ; rien dans le cratère. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+25
View File
@@ -0,0 +1,25 @@
# Nord — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Vallées boréales et clairières.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Blé | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de blé. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Carotte | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Carotte. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Pomme de terre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pomme de terre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Betterave | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de betterave. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Fraise | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de fraise. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Laitue | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de laitue. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Oignon | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Oignon. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Ail | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Ail. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Chou | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de chou. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Menthe | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Bouture de menthe. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
## Ressources vanilla
Baies sucrées, lapins, saumon dans les eaux peuplées. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+20
View File
@@ -0,0 +1,20 @@
# Nord-Est — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Marais frais et jardin pâle.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Concombre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de concombre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Laitue | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de laitue. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Chou | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de chou. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Haricots secs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Haricots secs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Menthe | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Bouture de menthe. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
## Ressources vanilla
Champignons, poissons dans les eaux peuplées. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+31
View File
@@ -0,0 +1,31 @@
# Nord-Ouest — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Prairies et taïgas.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Blé | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de blé. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Carotte | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Carotte. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Pomme de terre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pomme de terre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Betterave | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de betterave. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Fraise | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de fraise. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Laitue | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de laitue. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Oignon | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Oignon. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Ail | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Ail. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Chou | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de chou. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Menthe | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Bouture de menthe. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Tomate | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de tomate. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Maïs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Grains de maïs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Concombre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de concombre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Poivron | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de poivron. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Haricots secs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Haricots secs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Basilic | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de basilic. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
## Ressources vanilla
Baies sucrées, champignons, lapins. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+116
View File
@@ -0,0 +1,116 @@
# Séchoir — provisions, ingrédients et nouvelles combinaisons
**Beta.318 locale — 30 octobre 2026.** [Guide des récoltes](README.md) · [Contrat et vérifications](../../dried-cuisine-beta318.md).
## Recettes implémentées
Quatre crochets indépendants, un objet par crochet : on peut mélanger quatre aliments différents.
Un ingrédient donne un produit séché. Les durées sont des minutes de jeu à 20 ticks/s,
avec le bloc chargé ; la pluie et l’immersion suspendent le travail. Un toit protège de la pluie.
Les entonnoirs alimentent les crochets libres et retirent uniquement les produits prêts par-dessous.
Les aliments prêts se récupèrent à main vide. Les nouveaux produits rejoignent Nourriture et boissons,
sans nouvel onglet. Les recettes et leurs durées sont visibles dans JEI.
| À suspendre | Résultat | Durée |
| --- | --- | --- |
| Préparation de saucisson | Saucisson séché | 5 min |
| Bœuf cru | Viande séchée | 4 min |
| Côtelette de porc crue | Viande séchée | 4 min |
| Cabillaud cru | Cabillaud séché | 2 min |
| Saumon cru | Saumon séché | 2 min |
| Raisin | Raisins secs | 1 min 30 s |
| Tomate | Tomates séchées | 2 min |
| Piment | Piment séché | 1 min 30 s |
| Basilic | Basilic séché | 1 min |
| Menthe | Menthe séchée | 1 min |
| Coriandre | Coriandre séchée | 1 min |
| Pomme | Pomme séchée | 1 min 30 s |
| Fraise | Fraises séchées | 1 min |
| Ananas | Ananas séché | 2 min |
| Gousse de vanille verte | Gousse de vanille | 3 min |
| Préparation de saucisson pimenté | Saucisson pimenté | 5 min |
Le bœuf et le porc donnent le même objet **Viande séchée**. Cabillaud et saumon donnent
deux produits distincts. L’ananas rend exactement une couronne à la fin du séchage,
pour être replanté. Le produit séché ne rend pas une seconde couronne à sa consommation.
La gousse de vanille garde son identifiant : sa production passe du fumoir au séchoir.
Un ancien fumoir peut donc garder sa gousse verte sans poursuivre ce traitement après la mise à jour.
Les viandes, poissons, raisins secs, tomates et fruits séchés sont comestibles.
Le piment et les trois herbes séchés sont des ingrédients. Le séchage n’ajoute pas de
péremption, de combustible ou de bonification hors ligne.
## Combinaisons déjà utilisables dans la cuisine existante
Les herbes et piments séchés remplacent leur équivalent frais, un pour un,
dans les préparations de cuisine. Les anciennes recettes aux produits frais restent disponibles.
La fabrication des semences à partir des récoltes fraîches reste distincte.
| Plat existant | Ingrédients possibles | Appareil |
| --- | --- | --- |
| Chili de haricots | Haricots secs + Sauce tomate + Piment frais ou séché | Chaudron |
| Ragoût de pois chiches | Pois chiches + Sauce tomate + Coriandre fraîche ou séchée + Piment frais ou séché | Chaudron |
| Infusion de menthe | Menthe fraîche ou séchée + Fiole d’eau | Chaudron |
| Sauce tomate | Pulpe de tomate + Sel + Basilic frais ou séché | Chaudron |
| Sauce piquante | Pulpe de tomate + Piment frais ou séché + Vinaigre | Tonneau |
| Kimchi | Chou + Sel + Piment frais ou séché | Tonneau |
| Pizza margherita | Pâte crue + Sauce tomate + Fromage + Basilic frais ou séché | Cuisinière |
| Curry de pois chiches | Pois chiches + Sauce tomate + Piment frais ou séché + Coriandre fraîche ou séchée | Poêle |
| Omelette aux herbes | Œuf + Seau de lait + Basilic, menthe ou coriandre, frais ou séchés | Poêle |
| Émincé de mouton épicé | Mouton cru + Oignon + Piment frais ou séché + Coriandre fraîche ou séchée | Poêle |
| Poulet tomate-basilic | Poulet cru + Tomate + Basilic frais ou séché | Poêle |
La vanille séchée conserve aussi sa transformation existante au moulin en poudre de vanille.
## Les 18 nouvelles recettes implémentées
Toutes ces recettes sont jouables dans la beta.318 locale. Chaque ingrédient indiqué
est payé une fois par lot. Les récipients figurent dans la liste ; ils reviennent une fois
à la consommation. Le chaudron demande une dose d’eau par portion, de la chaleur et
un coup de bâton pour choisir la préparation ; il conserve ensuite sa recette pour le réassort.
Le moulin reste manuel, sans combustible. La cuisinière utilise son combustible habituel.
| Recette | Ingrédients complets | Appareil | Durée | Quantité |
| --- | --- | --- | --- | --- |
| Pain aux raisins | Pâte crue + Raisins secs | Cuisinière | 12 s | 1 |
| Brioche vanillée aux raisins | Pâte crue + Raisins secs + Poudre de vanille + Beurre | Cuisinière | 16 s | 1 |
| Biscuits aux fruits secs | Farine de blé + Pomme séchée + Fraises séchées + Beurre | Cuisinière | 12 s | 4 |
| Pain aux tomates et basilic | Pâte crue + Tomates séchées + Basilic séché | Cuisinière | 14 s | 1 |
| Sandwich du voyageur | Pain + Viande séchée + Tomates séchées + Laitue | Établi | Immédiat, manuel | 1 |
| Sandwich au cabillaud séché | Pain + Cabillaud séché + Concombre + Citron | Établi | Immédiat, manuel | 1 |
| Riz au saumon séché | Riz + Saumon séché + Coriandre séchée + Bol + Fiole d’eau | Chaudron | 14 s | 1 |
| Ragoût du voyageur | Viande séchée + Pomme de terre + Carotte + Basilic séché + Bol + Fiole d’eau | Chaudron | 18 s | 1 |
| Soupe de poisson séché | Cabillaud séché + Saumon séché + Oignon + Coriandre séchée + Bol + Fiole d’eau | Chaudron | 16 s | 1 |
| Haricots à la viande séchée | Haricots secs + Viande séchée + Tomates séchées + Piment séché ou poudre de piment + Bol + Fiole d’eau | Chaudron | 18 s | 1 |
| Salade de pois chiches aux tomates séchées | Pois chiches + Tomates séchées + Huile d'olive + Basilic séché + Bol | Établi | Immédiat, manuel | 1 |
| Tomates marinées aux herbes | Tomates séchées + Huile d'olive + Ail + Basilic séché + Bol | Établi | Immédiat, manuel | 1 |
| Mélange de fruits secs | Raisins secs + Pomme séchée + Fraises séchées + Ananas séché | Établi | Immédiat, manuel | 1 |
| Infusion pomme-menthe | Pomme séchée + Menthe séchée + Fiole de miel + Fiole vide + Fiole d’eau | Chaudron | 11 s | 1 |
| Dessert ananas-vanille | Ananas séché + Poudre de vanille + Crème + Bol | Établi | Immédiat, manuel | 1 |
| Saucisson pimenté | Préparation de saucisson pimenté | Séchoir | 5 min | 1 |
| Poudre de piment | Piment séché | Moulin | Immédiat, manuel | 2 |
| Assaisonnement aux herbes | Basilic séché + Coriandre séchée + Sel | Établi | Immédiat, manuel | 2 |
**Préparation supplémentaire du saucisson pimenté :** à l’établi, une préparation de
saucisson + un piment séché (ou une poudre de piment) + une coriandre séchée donnent
une préparation pimentée. Elle sèche ensuite cinq minutes. Le résultat se pose et
se partage en quatre tranches, comme le saucisson nature. Ce sont donc **19 transformations
pour les 18 nouveaux résultats**.
La poudre de piment remplace aussi le piment dans les anciennes recettes de cuisine
et l’assaisonnement remplace les herbes dans l’omelette. L’eau du chaudron peut venir
d’une fiole d’eau ou de sa réserve ; les potions à effets sont refusées.
## Quand ça pique
Manger un plat épicé donne deux petites réactions visuelles et sonores de coup reçu,
espacées de huit ticks (0,4 seconde). Cela ne retire aucun cœur, ne consomme aucune
absorption et ne déplace pas le joueur. Les plats doux ne déclenchent pas cet effet.
Le saucisson pimenté le déclenche aussi à chaque tranche réellement mangée.
## Graphismes
Les nouveaux plats utilisent les sprites existants avec une teinte native ; les saucissons
réutilisent leurs modèles cubiques avec des couleurs distinctes
pour distinguer les préparations. Les PNG d’origine restent intacts ; ces dessins
peuvent recevoir des textures dédiées dans un prochain travail graphique.
+21
View File
@@ -0,0 +1,21 @@
# Sud — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Bosquets et berges des canyons.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Maïs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Grains de maïs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Raisin | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pépins de raisin. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Pois chiches | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pois chiches. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Piment | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de piment. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Coriandre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de coriandre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Olives | Clic droit sur les feuilles avec fruits. Casser les feuilles pour une pousse ; les feuilles de chêne/acacia du climat correspondant peuvent aussi donner une pousse. | Pousse d'olivier. Planter la pousse sur terre/herbe ; attendre l'arbre. |
## Ressources vanilla
Lapins ; les cultures ne poussent pas sur la terre cuite nue. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+21
View File
@@ -0,0 +1,21 @@
# Sud-Est — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Jungle, mangrove et bambou.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Ananas | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Couronne d'ananas. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Riz | Casser le riz sauvage sur les berges humides ; ou les herbes marines (seagrass) de jungle/mangrove, à la main. | Riz. Planter le grain dans une source d'eau au-dessus de boue. |
| Graines de soja | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de soja. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Piment | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de piment. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Cerises de café | Clic droit sur un caféier mûr ; transformer ses cerises en grains verts. Les herbes tropicales peuvent aussi donner un grain. | Grains de café verts. Planter un grain vert sur terre/herbe ; clic droit à maturité. |
| Gousse de vanille verte | Clic droit pour les gousses mûres ; casser un segment pour une bouture. Les lianes tropicales cassées à la main peuvent aussi donner une bouture. | Bouture de vanille. Poser la bouture contre un tronc ; elle grimpe vers le haut. |
## Ressources vanilla
Cacao, pastèques et bambou selon la végétation native. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+21
View File
@@ -0,0 +1,21 @@
# Sud-Ouest — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Savane, prairie sèche et désert.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Maïs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Grains de maïs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Raisin | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pépins de raisin. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Pois chiches | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pois chiches. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Piment | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de piment. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Coriandre | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de coriandre. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Olives | Clic droit sur les feuilles avec fruits. Casser les feuilles pour une pousse ; les feuilles de chêne/acacia du climat correspondant peuvent aussi donner une pousse. | Pousse d'olivier. Planter la pousse sur terre/herbe ; attendre l'arbre. |
## Ressources vanilla
Lapins du désert ; chercher les sols vivants et les berges. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+23
View File
@@ -0,0 +1,23 @@
# Ouest — cueillette et agriculture
[Retour au guide](README.md) · [Planche](planche.html)
Oasis de l'archipel.
| Aliment | Première récolte | À rapporter / replanter |
| --- | --- | --- |
| Tomate | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de tomate. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Maïs | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Grains de maïs. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Poivron | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de poivron. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Aubergine | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines d'aubergine. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Raisin | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Pépins de raisin. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Basilic | Casser la plante sauvage ; rapporter sa récolte et sa semence. | Graines de basilic. Planter sur terre labourée. Les récoltes se multiplient selon les règles de leur culture. |
| Citron | Clic droit sur les feuilles avec fruits. Casser les feuilles pour une pousse ; les feuilles de chêne/acacia du climat correspondant peuvent aussi donner une pousse. | Pousse de citronnier. Planter la pousse sur terre/herbe ; attendre l'arbre. |
| Olives | Clic droit sur les feuilles avec fruits. Casser les feuilles pour une pousse ; les feuilles de chêne/acacia du climat correspondant peuvent aussi donner une pousse. | Pousse d'olivier. Planter la pousse sur terre/herbe ; attendre l'arbre. |
## Ressources vanilla
Poissons de mer, algues, canne à sucre sur les berges. Présence selon les habitats et la génération native, sans garantie sur chaque île.
Les plantes sauvages donnent des ingrédients et des semences, jamais directement des plats cuisinés.
Les herbes de ce climat servent aussi de solution de découverte dans les zones déjà explorées : voir les chances et outils dans le [guide](README.md).
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.297
pack_version=beta.297
mod_version=beta.318
pack_version=beta.318
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
@@ -20,6 +20,10 @@ public final class AnchorProbeClient {
public static void tick(Minecraft mc){
if(mc.player==null||mc.level==null)return;
var ctl=Probe.control();String who=System.getProperty("sanctuary.mediaClient");
if(ctl.has("shaders_enabled")&&fr.koka.sanctuary.client.shader.VanillaLight.enabled()!=ctl.get("shaders_enabled").getAsBoolean())
fr.koka.sanctuary.client.shader.VanillaLight.setEnabled(ctl.get("shaders_enabled").getAsBoolean());
if(ctl.has("pbr_enabled")&&fr.koka.sanctuary.client.shader.VanillaLight.pbr()!=ctl.get("pbr_enabled").getAsBoolean())
fr.koka.sanctuary.client.shader.VanillaLight.setPbr(ctl.get("pbr_enabled").getAsBoolean());
if(ctl.has("hide_gui")&&mc.gui.hud.isHidden()!=ctl.get("hide_gui").getAsBoolean())mc.gui.hud.toggle();
boolean a=who.endsWith("a");
Vec3 aim=a?new Vec3(-3.5,81.5,.5):new Vec3(0,83,1);
@@ -33,6 +37,7 @@ public final class AnchorProbeClient {
net.minecraft.client.Screenshot.grab(mc.gameDirectory,"anchor-"+Probe.phase+".png",mc.gameRenderer.mainRenderTarget(),1,m->{});captured=Probe.phase;
}
var row=new JsonObject();row.addProperty("step",done);row.addProperty("screen",mc.gui.screen()==null?"":mc.gui.screen().getClass().getSimpleName());
if(ctl.has("inspect_anchor_model"))inspectModel(mc,row);
var target=Anchors.target(mc.level,mc.player);row.addProperty("target",target!=null);if(target!=null)row.addProperty("distance",target.distance());
if(mc.gui.screen() instanceof AnchorScreen screen){var v=screen.snapshot();row.addProperty("settings",v.settings());row.addProperty("editable",v.editable());row.addProperty("message",v.message());row.addProperty("width",v.width());row.addProperty("height",v.height());}
else if(mc.gui.screen()!=null&&mc.gui.screen().getClass().getSimpleName().equals("DisplayMenu")){
@@ -40,6 +45,27 @@ public final class AnchorProbeClient {
}
Probe.write(Probe.LAB.resolve("anchor-"+who+".jsonl"),row);
}
private static void inspectModel(Minecraft mc,JsonObject row){
var state=mc.level.getBlockState(AnchorProbe.POS);
if(!state.is(Anchors.BLOCK))return;
var parts=new java.util.ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);
int quads=0;for(var part:parts)for(int f=0;f<7;f++)quads+=part.getQuads(f==6?null:Direction.values()[f]).size();
row.addProperty("anchor_model_quads",quads);row.addProperty("anchor_render_shape",state.getRenderShape().name());
row.addProperty("shaders_active",fr.koka.sanctuary.client.shader.VanillaLight.active());
row.addProperty("pbr_enabled",fr.koka.sanctuary.client.shader.VanillaLight.pbr());
try{
var pbr=fr.koka.sanctuary.client.shader.ProceduralPbr.class.getDeclaredMethod("applied");pbr.setAccessible(true);
row.addProperty("pbr_applied",(boolean)pbr.invoke(null));
var type=Class.forName("fr.koka.sanctuary.client.shader.PbrTerrain");var ctor=type.getDeclaredConstructor(boolean.class);ctor.setAccessible(true);
var candidate=type.getDeclaredMethod("candidate",net.minecraft.world.level.block.state.BlockState.class);candidate.setAccessible(true);
for(boolean transparent:new boolean[]{false,true})try(var terrain=(AutoCloseable)ctor.newInstance(transparent)){
row.addProperty(transparent?"anchor_pbr_transparent":"anchor_pbr_opaque",(boolean)candidate.invoke(terrain,state));
row.addProperty(transparent?"water_pbr_transparent":"water_pbr_opaque",(boolean)candidate.invoke(terrain,net.minecraft.world.level.block.Blocks.WATER.defaultBlockState()));
row.addProperty(transparent?"glass_pbr_transparent":"wood_pbr_opaque",(boolean)candidate.invoke(terrain,(transparent?net.minecraft.world.level.block.Blocks.GLASS:net.minecraft.world.level.block.Blocks.OAK_PLANKS).defaultBlockState()));
}
}catch(Exception e){throw new IllegalStateException(e);}
}
private static boolean perform(Minecraft mc,String step,JsonObject ctl){
if(step.equals("place")){
mc.gameMode.useItemOn(mc.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(-3.5,81.5,1),Direction.SOUTH,new BlockPos(-4,81,0),false));return true;
+1 -1
View File
@@ -1,7 +1,7 @@
plugins { id 'java-library' }
group = rootProject.maven_group
version = '0.3.1'
version = '0.3.2'
base { archivesName = 'sanctuary-media' }
evaluationDependsOn(':sanctuary')
@@ -1,170 +1,7 @@
{
"ambientocclusion": false,
"textures": {
"particle": "minecraft:block/oak_planks",
"wood": "minecraft:block/oak_planks",
"redstone": "minecraft:block/redstone_block"
"particle": "minecraft:block/oak_planks"
},
"elements": [
{
"from": [
0,
0,
0
],
"to": [
16,
1,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
0,
15,
0
],
"to": [
16,
16,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
0,
1,
0
],
"to": [
1,
15,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
15,
1,
0
],
"to": [
16,
15,
1
],
"faces": {
"north": {
"texture": "#wood"
},
"south": {
"texture": "#wood"
},
"east": {
"texture": "#wood"
},
"west": {
"texture": "#wood"
},
"up": {
"texture": "#wood"
},
"down": {
"texture": "#wood"
}
}
},
{
"from": [
1,
1,
0
],
"to": [
3,
3,
2
],
"faces": {
"north": {
"texture": "#redstone"
},
"south": {
"texture": "#redstone"
},
"east": {
"texture": "#redstone"
},
"west": {
"texture": "#redstone"
},
"up": {
"texture": "#redstone"
},
"down": {
"texture": "#redstone"
}
}
}
]
"elements": []
}
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"id": "sanctuary_media",
"version": "0.3.1",
"version": "0.3.2",
"name": "Sanctuary Media",
"license": "GPL-3.0-or-later",
"environment": "*",
+55 -1
View File
@@ -7,6 +7,7 @@ version = rootProject.mod_version
base { archivesName = 'sanctuary' }
apply from: 'gradle/origin-resources.gradle'
apply from: 'gradle/agriculture-resources.gradle'
configurations { soundPhysicsArtifact { transitive = false } }
def soundPhysics = "maven.modrinth:qyVF9oeo:${rootProject.sound_physics_artifact}"
@@ -292,7 +293,7 @@ tasks.named('processGametestResources') {
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
def focusClasses = [happycreeper: 'HappyCreeperGameTests', whale290: 'FlyingWhale290GameTests', hunter286: 'Hunter286GameTests', smoker285: 'Smoker285GameTests', boomer284: 'Boomer284GameTests', spitter283: 'Spitter283GameTests', charger282: 'Charger282GameTests', infected280: 'Infected280GameTests', claims278: 'ClaimBattle278GameTests', claims277: 'ClaimDemeure277GameTests', menu275: 'Menu275GameTests', claims273: 'ClaimMap273GameTests', claims272: 'ChunkClaims272GameTests', overview271: 'Inhabitant271GameTests', property270: 'Property270GameTests', providence269: 'Providence269GameTests', providence268: 'Providence268GameTests', providence265: 'Providence265GameTests', familiar260: 'Familiar260GameTests', census: 'CensusGameTestsTerrain', insects245: 'Insect245GameTests', map244: 'Atlas244GameTests', ziplines242: 'Zipline242GameTests', waystones239: 'Waystone239GameTests', waystonesLarge239: 'WaystoneLarge239GameTests', forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', 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']
def focusClasses = [driedcuisine318: 'DriedCuisine318GameTests', drying317: 'Drying317GameTests', foraging316: 'Foraging316GameTests', sausage315: 'Sausage315GameTests', cheesewine314: 'CheeseWine314GameTests', drying313: 'Drying313GameTests', cauldron312: 'Cauldron312GameTests', cauldron311: 'Cauldron311GameTests', pan310: 'Pan310GameTests', oven308: 'Oven308GameTests', mill306: 'Mill306GameTests', crushing305: 'Crushing305GameTests', kitchen304: 'Kitchen304GameTests', cuisine303: 'Cuisine303GameTests', vanilla301: 'Vanilla301GameTests', agriculture300: 'Agriculture300GameTests', happycreeper: 'HappyCreeperGameTests', whale290: 'FlyingWhale290GameTests', hunter286: 'Hunter286GameTests', smoker285: 'Smoker285GameTests', boomer284: 'Boomer284GameTests', spitter283: 'Spitter283GameTests', charger282: 'Charger282GameTests', infected280: 'Infected280GameTests', claims278: 'ClaimBattle278GameTests', claims277: 'ClaimDemeure277GameTests', menu275: 'Menu275GameTests', claims273: 'ClaimMap273GameTests', claims272: 'ChunkClaims272GameTests', overview271: 'Inhabitant271GameTests', property270: 'Property270GameTests', providence269: 'Providence269GameTests', providence268: 'Providence268GameTests', providence265: 'Providence265GameTests', familiar260: 'Familiar260GameTests', census: 'CensusGameTestsTerrain', insects245: 'Insect245GameTests', map244: 'Atlas244GameTests', ziplines242: 'Zipline242GameTests', waystones239: 'Waystone239GameTests', waystonesLarge239: 'WaystoneLarge239GameTests', forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', 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']
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
inputs.property('focusedNative', focused)
def yellowRock222Client = providers.gradleProperty('sanctuaryYellowRock222ClientTests').map { it.toBoolean() }.getOrElse(false)
@@ -417,6 +418,59 @@ tasks.named('processGametestResources') {
def zombie287Client = providers.gradleProperty('sanctuaryZombie287ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('zombie287Client', zombie287Client)
if (zombie287Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Zombie287ClientChecks') }
def sausage315Client = providers.gradleProperty('sanctuarySausage315ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sausage315Client', sausage315Client)
if (sausage315Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sausage315ClientChecks') }
def cheeseWine314Client = providers.gradleProperty('sanctuaryCheeseWine314ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('cheeseWine314Client', cheeseWine314Client)
if (cheeseWine314Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.CheeseWine314ClientChecks') }
def driedCuisine318Client = providers.gradleProperty('sanctuaryDriedCuisine318ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('driedCuisine318Client', driedCuisine318Client)
if (driedCuisine318Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.DriedCuisine318ClientChecks') }
def drying317Client = providers.gradleProperty('sanctuaryDrying317ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('drying317Client', drying317Client)
if (drying317Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Drying317ClientChecks') }
def drying313Client = providers.gradleProperty('sanctuaryDrying313ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('drying313Client', drying313Client)
if (drying313Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Drying313ClientChecks') }
def cauldron312Client = providers.gradleProperty('sanctuaryCauldron312ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('cauldron312Client', cauldron312Client)
if (cauldron312Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Cauldron312ClientChecks') }
def cauldron311Client = providers.gradleProperty('sanctuaryCauldron311ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('cauldron311Client', cauldron311Client)
if (cauldron311Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Cauldron311ClientChecks') }
def pan310Client = providers.gradleProperty('sanctuaryPan310ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('pan310Client', pan310Client)
if (pan310Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Pan310ClientChecks') }
def oven308Client = providers.gradleProperty('sanctuaryOven308ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('oven308Client', oven308Client)
if (oven308Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Oven308ClientChecks') }
def mill306Client = providers.gradleProperty('sanctuaryMill306ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('mill306Client', mill306Client)
if (mill306Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Mill306ClientChecks') }
def crushing305Client = providers.gradleProperty('sanctuaryCrushing305ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('crushing305Client', crushing305Client)
if (crushing305Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Crushing305ClientChecks') }
def kitchen304Client = providers.gradleProperty('sanctuaryKitchen304ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('kitchen304Client', kitchen304Client)
if (kitchen304Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Kitchen304ClientChecks') }
def cuisine303Client = providers.gradleProperty('sanctuaryCuisine303ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('cuisine303Client', cuisine303Client)
if (cuisine303Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Cuisine303ClientChecks') }
def vanilla301Client = providers.gradleProperty('sanctuaryVanilla301ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('vanilla301Client', vanilla301Client)
if (vanilla301Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Vanilla301ClientChecks') }
def foraging316Client = providers.gradleProperty('sanctuaryForaging316ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('foraging316Client', foraging316Client)
if (foraging316Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Foraging316ClientChecks') }
def agriculture300Client = providers.gradleProperty('sanctuaryAgriculture300ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('agriculture300Client', agriculture300Client)
if (agriculture300Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Agriculture300ClientChecks') }
def whale290Client = providers.gradleProperty('sanctuaryWhale290ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('whale290Client', whale290Client)
if (whale290Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.FlyingWhale290ClientChecks') }
@@ -0,0 +1,47 @@
def agricultureResources = layout.buildDirectory.dir('generated/agricultureResources')
sourceSets.main.resources.srcDir(agricultureResources)
tasks.register('generateAgricultureResources', Exec) {
inputs.file(rootProject.file('scripts/agriculture_resources.py'))
inputs.file(rootProject.file('scripts/data/agriculture.json'))
inputs.file(rootProject.file('scripts/foraging_resources.py'))
inputs.file(rootProject.file('scripts/data/foraging.json'))
inputs.dir(file('src/main/resources/data/sanctuary/worldgen/biome'))
outputs.dir(agricultureResources)
doFirst { delete(agricultureResources) }
commandLine('python3', rootProject.file('scripts/agriculture_resources.py'), '--output', agricultureResources.get().asFile)
}
tasks.named('processResources') { dependsOn('generateAgricultureResources') }
tasks.matching { it.name == 'sourcesJar' }.configureEach { dependsOn('generateAgricultureResources') }
def cuisineResources = layout.buildDirectory.dir('generated/cuisineResources')
sourceSets.main.resources.srcDir(cuisineResources)
tasks.register('generateCuisineResources', Exec) {
inputs.file(rootProject.file('scripts/dried_food_resources.py'))
inputs.file(rootProject.file('scripts/cuisine_resources.py'))
inputs.file(rootProject.file('scripts/data/cuisine.json'))
inputs.file(rootProject.file('scripts/data/agriculture.json'))
outputs.dir(cuisineResources)
doFirst { delete(cuisineResources) }
commandLine('python3', rootProject.file('scripts/cuisine_resources.py'), '--output', cuisineResources.get().asFile)
}
tasks.named('processResources') { dependsOn('generateCuisineResources') }
tasks.matching { it.name == 'sourcesJar' }.configureEach { dependsOn('generateCuisineResources') }
def kitchenResources = layout.buildDirectory.dir('generated/kitchenResources')
sourceSets.main.resources.srcDir(kitchenResources)
tasks.register('generateKitchenResources', Exec) {
inputs.file(rootProject.file('scripts/kitchen_resources.py'))
inputs.file(rootProject.file('scripts/drying_resources.py'))
inputs.file(rootProject.file('scripts/dried_food_resources.py'))
inputs.file(rootProject.file('scripts/data/drying-foods.json'))
inputs.file(rootProject.file('scripts/dried_cuisine_resources.py'))
inputs.file(rootProject.file('scripts/data/dried-cuisine.json'))
inputs.file(rootProject.file('scripts/cheese_wine_resources.py'))
inputs.file(rootProject.file('scripts/sausage_serving_resources.py'))
inputs.files(['kitchen-assets.json', 'kitchen-crafting.json', 'cuisine.json'].collect { rootProject.file('scripts/data/' + it) })
outputs.dir(kitchenResources)
doFirst { delete(kitchenResources) }
commandLine('python3', rootProject.file('scripts/kitchen_resources.py'), '--output', kitchenResources.get().asFile)
}
tasks.named('processResources') { dependsOn('generateKitchenResources') }
tasks.matching { it.name == 'sourcesJar' }.configureEach { dependsOn('generateKitchenResources') }
@@ -0,0 +1,244 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.agriculture.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
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.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
import net.minecraft.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
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.state.BlockState;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.agriculture.AgricultureBlocks.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Vulkan/Sodium, actual planting packets, all baked states/items, FR/EN and an actual disk reload. */
public final class Agriculture300ClientChecks implements FabricClientGameTest {
private static final BlockPos INTERACTION = new BlockPos(8, 1, 15);
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable();
var intro = new AtomicBoolean(true);
var snapshot = new AtomicReference<Map<BlockPos, BlockState>>();
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Agriculture validation requires Vulkan");
System.out.println("AGRICULTURE300_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Agriculture300-" + UUID.randomUUID()); s.setSeed("300"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server = world.getServer();
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst(); var level = player.level();
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");
level.getGameRules().set(GameRules.SPAWN_MOBS, false, s);
level.getGameRules().set(GameRules.RANDOM_TICK_SPEED, 0, s);
gallery(level);
level.setBlockAndUpdate(INTERACTION.below(), Blocks.FARMLAND.defaultBlockState().setValue(FarmlandBlock.MOISTURE, 7));
player.teleportTo(8.5, 1, 17.5); player.setYRot(180); player.setXRot(20);
player.setGameMode(GameType.SURVIVAL); player.getInventory().clearContent();
player.getInventory().setItem(0, new ItemStack(AgricultureItems.CORN_KERNELS, 2)); player.inventoryMenu.broadcastChanges();
});
c.waitFor(m -> m.player.getMainHandItem().is(AgricultureItems.CORN_KERNELS), 400);
c.runOnClient(m -> m.gameMode.useItemOn(m.player, InteractionHand.MAIN_HAND, hit(INTERACTION.below())));
c.waitFor(m -> m.level.getBlockState(INTERACTION).is(CORN_CROP), 200);
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst();
check(player.level().getBlockState(INTERACTION).is(CORN_CROP) && player.getMainHandItem().getCount() == 1,
"Real survival planting packet consumes one seed on the authoritative server");
player.getInventory().setItem(0, new ItemStack(Items.BONE_MEAL, 16)); player.inventoryMenu.broadcastChanges();
});
c.waitFor(m -> m.player.getMainHandItem().is(Items.BONE_MEAL), 200);
for (int i = 0; i < 5; i++) {
c.runOnClient(m -> m.gameMode.useItemOn(m.player, InteractionHand.MAIN_HAND, hit(INTERACTION)));
c.waitTicks(5);
}
server.runOnServer(s -> check(s.overworld().getBlockState(INTERACTION.above()).is(CORN_CROP_UPPER), "Real bonemeal packets create the upper half"));
c.runOnClient(m -> m.gameMode.startDestroyBlock(INTERACTION.above(), Direction.UP));
c.waitFor(m -> m.level.getBlockState(INTERACTION).isAir() && m.level.getBlockState(INTERACTION.above()).isAir(), 200);
c.runOnClient(m -> {
m.getWindow().setWindowed(1280, 960);
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(), true, m.level.registryAccess());
check(allItems().size() == 48, "All 48 planting/harvest items are registered");
check(BuiltInRegistries.CREATIVE_MODE_TAB.getOptional(SanctuaryMod.id("agriculture")).isEmpty(), "No new creative page is registered");
var natural = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.NATURAL_BLOCKS).value();
var food = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();
for (var item : AgricultureItems.PLANTING_ITEMS) check(natural.getDisplayItems().stream().anyMatch(stack -> stack.is(item)), "Planting item is in vanilla Natural Blocks: " + item);
for (var item : AgricultureItems.PRODUCE_ITEMS) check(food.getDisplayItems().stream().anyMatch(stack -> stack.is(item)), "Harvest is in vanilla Food & Drinks: " + item);
check(food.getDisplayItems().stream().anyMatch(stack -> stack.is(AgricultureItems.GARLIC_CLOVE)), "Garlic clove is in Food & Drinks");
int states = 0;
for (var block : blocks()) for (var state : block.getStateDefinition().getPossibleStates()) {
var parts = new ArrayList<BlockStateModelPart>();
m.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(300), parts);
int quads = 0;
for (var part : parts) for (var direction : new Direction[]{null, Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST})
for (var quad : part.getQuads(direction)) {
check(quad.materialInfo().sprite().contents().name().getNamespace().equals("sanctuary"), "Actual supplied crop/tree texture is baked: " + state);
quads++;
}
check(quads > 0, "Every registered plant state has visible baked geometry: " + state); states++;
if (state.is(ONION_CROP) || state.is(GARLIC_CROP))
check(quads == 4, "Onion and garlic use two crossing planes, not a four-plane crop frame");
if (state.is(CORN_CROP) || state.is(RICE_CROP))
check(quads == 8, "Cereals keep their native field geometry");
}
for (var stack : allItems()) {
var item = new net.minecraft.client.renderer.item.ItemStackRenderState();
m.getItemModelResolver().updateForLiving(item, stack, ItemDisplayContext.GUI, m.player);
var material = item.pickParticleMaterial(RandomSource.create(300));
check(!item.isEmpty() && material != null && material.sprite().contents().name().getNamespace().equals("sanctuary"), "Actual item model resolves its supplied sprite: " + stack);
}
System.out.println("AGRICULTURE300_MODELS states=" + states + " items=48");
});
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst(); player.setGameMode(GameType.CREATIVE);
player.teleportTo(10, 9, 23); player.setYRot(180); player.setXRot(28);
player.getAbilities().flying = true; player.onUpdateAbilities();
});
c.waitTicks(25);
c.runOnClient(m -> { m.player.setYRot(180); m.player.setXRot(28); }); c.waitTicks(5);
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1); c.waitTicks(4);
c.takeScreenshot("agriculture300-farm-and-orchards");
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst(); player.teleportTo(7.5, 3.2, 25.5); player.setYRot(180); player.setXRot(12);
});
c.waitTicks(8); c.runOnClient(m -> {m.player.setYRot(180); m.player.setXRot(12);}); c.waitTicks(3);
c.takeScreenshot("agriculture300-cross-onion-garlic-and-herbs");
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
for (String locale : List.of("fr_fr", "en_us")) for (int scale : new int[]{2, 3, 4})
for (var tabKey : List.of(CreativeModeTabs.NATURAL_BLOCKS, CreativeModeTabs.FOOD_AND_DRINKS)) {
c.runOnClient(m -> {
m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
m.options.guiScale().set(scale); m.resizeGui();
for (var stack : allItems()) check(!stack.getHoverName().getString().startsWith("item.sanctuary"), "Translated agricultural item: " + locale + " " + stack);
for (var block : blocks()) check(!block.getName().getString().startsWith("block.sanctuary"), "Translated agricultural block: " + locale + " " + block);
m.gui.setScreen(new CreativeModeInventoryScreen(m.player, m.level.enabledFeatures(), true));
});
c.waitTicks(4);
c.runOnClient(m -> {
var screen = (CreativeModeInventoryScreen)m.gui.screen();
var tabs = (net.fabricmc.fabric.api.client.creativetab.v1.FabricCreativeModeInventoryScreen)screen;
var tab = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(tabKey).value();
tabs.switchToPage(tabs.getPage(tab)); tabs.setSelectedTab(tab);
check(tabs.getSelectedTab() == tab, "The visible tab is the requested vanilla category");
screen.mouseScrolled(screen.width / 2.0, screen.height / 2.0, 0, -1000);
if (tabKey.equals(CreativeModeTabs.FOOD_AND_DRINKS))
screen.mouseScrolled(screen.width / 2.0, screen.height / 2.0, 0, (fr.koka.sanctuary.cuisine.Cuisine.items().size() + fr.koka.sanctuary.cuisine.KitchenAppliances.items().size()) / 9);
var visible = screen.getMenu().slots.stream().limit(45).map(slot -> slot.getItem().getItem()).toList();
check(visible.contains(tabKey.equals(CreativeModeTabs.NATURAL_BLOCKS) ? AgricultureItems.LEMON_LEAVES : AgricultureItems.GARLIC_CLOVE), "New items are present in the visible native slots");
});
c.waitTicks(5);
c.runOnClient(m -> {
var menu = ((CreativeModeInventoryScreen)m.gui.screen()).getMenu();
if (tabKey.equals(CreativeModeTabs.FOOD_AND_DRINKS)) {
for (var item : List.of(AgricultureItems.BEANS, AgricultureItems.CHICKPEAS))
check(menu.slots.stream().limit(45).anyMatch(slot -> !slot.getItem().isEmpty() && slot.getItem().is(item)), "Harvest remains available in actual Food & Drinks slots after synchronization: " + item);
}
});
c.takeScreenshot("agriculture300-" + tabKey.identifier().getPath() + "-" + locale + "-gui" + scale);
c.runOnClient(m -> m.gui.setScreen(null));
}
server.runOnServer(s -> {
var level = s.overworld(); var map = new LinkedHashMap<BlockPos, BlockState>();
for (var pos : BlockPos.betweenClosed(new BlockPos(-10, 1, -12), new BlockPos(27, 15, 20))) {
var state = level.getBlockState(pos);
if (BuiltInRegistries.BLOCK.getKey(state.getBlock()).getNamespace().equals("sanctuary")) map.put(pos.immutable(), state);
}
check(map.values().stream().anyMatch(v -> v.is(LEMON_LEAVES)) && map.values().stream().anyMatch(v -> v.is(OLIVE_LEAVES)), "Both real fruit trees grew");
snapshot.set(map);
});
saved = world.getWorldSave();
}
try (var world = saved.open()) {
c.waitFor(m -> m.player != null && m.gui.screen() == null, 1200);
world.getServer().runOnServer(s -> {
for (var entry : snapshot.get().entrySet()) check(s.overworld().getBlockState(entry.getKey()).equals(entry.getValue()), "Actual disk reload retains crop/leaf state: " + entry);
System.out.println("AGRICULTURE300_DISK_RELOAD states=" + snapshot.get().size());
});
c.waitTicks(8); c.takeScreenshot("agriculture300-reloaded");
}
System.out.println("AGRICULTURE300_CLIENT_PASS Vulkan, 20 crops, two trees, coffee, native packets, models, FR/EN, disk reload");
}
private static void gallery(ServerLevel level) {
var crops = new ArrayList<>(AgricultureBlocks.crops().entrySet()); crops.sort(Map.Entry.comparingByKey());
int index = 0;
for (var entry : crops) {
var pos = new BlockPos(index % 5 * 3, 1, index / 5 * 3); index++;
boolean rice = entry.getValue() == RICE_CROP;
level.setBlockAndUpdate(pos.below(), rice ? Blocks.MUD.defaultBlockState() : Blocks.FARMLAND.defaultBlockState().setValue(FarmlandBlock.MOISTURE, 7));
if (rice) for (var direction : Direction.Plane.HORIZONTAL) level.setBlockAndUpdate(pos.relative(direction), Blocks.STONE.defaultBlockState());
level.setBlockAndUpdate(pos, rice ? Blocks.WATER.defaultBlockState() : entry.getValue().defaultBlockState());
if (rice) level.setBlockAndUpdate(pos, RICE_CROP.defaultBlockState());
var crop = (AgriculturalCropBlock)entry.getValue();
for (int i = 0; i < 10 && !crop.isMaxAge(level.getBlockState(pos)); i++) BoneMealItem.growCrop(new ItemStack(Items.BONE_MEAL), level, pos);
}
var coffee = new BlockPos(-3, 1, 7);
level.setBlockAndUpdate(coffee.below(), Blocks.GRASS_BLOCK.defaultBlockState());
level.setBlockAndUpdate(coffee, COFFEE_BUSH.defaultBlockState().setValue(SweetBerryBushBlock.AGE, 3));
int specimen = 0;
for (var crop : List.of(ONION_CROP, GARLIC_CROP, BASIL_CROP, MINT_CROP, CORIANDER_CROP, LETTUCE_CROP, CABBAGE_CROP, PINEAPPLE_CROP)) {
var pos = new BlockPos(specimen++ * 2, 1, 19);
level.setBlockAndUpdate(pos.below(), Blocks.FARMLAND.defaultBlockState().setValue(FarmlandBlock.MOISTURE, 7));
level.setBlockAndUpdate(pos, crop.getStateForAge(7));
}
for (int i = 0; i < 2; i++) {
var pos = new BlockPos(i == 0 ? -5 : 20, 1, -6);
for (var ground : BlockPos.betweenClosed(pos.offset(-3, -1, -3), pos.offset(3, -1, 3))) level.setBlockAndUpdate(ground, Blocks.DIRT.defaultBlockState());
var sapling = i == 0 ? LEMON_SAPLING : OLIVE_SAPLING;
level.setBlockAndUpdate(pos, sapling.defaultBlockState());
for (int n = 0; n < 10 && level.getBlockState(pos).is(sapling); n++) sapling.advanceTree(level, pos, level.getBlockState(pos), RandomSource.create(300 + n));
}
}
private static List<ItemStack> allItems() {
var items = new LinkedHashSet<Item>(AgricultureItems.PLANTING_ITEMS);
items.addAll(AgricultureItems.PRODUCE_ITEMS);
items.addAll(List.of(AgricultureItems.GARLIC_CLOVE, AgricultureItems.OLIVE_LEAVES, AgricultureItems.LEMON_LEAVES));
return items.stream().map(ItemStack::new).toList();
}
private static List<Block> blocks() {
var result = new ArrayList<Block>(AgricultureBlocks.crops().values());
result.addAll(List.of(TOMATO_CROP_UPPER, CORN_CROP_UPPER, CUCUMBER_CROP_UPPER, BEAN_CROP_UPPER,
COFFEE_BUSH, OLIVE_LEAVES, LEMON_LEAVES, OLIVE_SAPLING, LEMON_SAPLING, WILD_PINEAPPLE));
return result;
}
private static BlockHitResult hit(BlockPos pos) { return new BlockHitResult(Vec3.atCenterOf(pos).add(0, .5, 0), Direction.UP, pos, false); }
}
@@ -0,0 +1,70 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.CookingCauldronBlockEntity;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.sounds.SoundEventListener;
import net.minecraft.core.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Real dropped milk/stirring/serving packets, audio dispatch, Vulkan HUD and disk reopen. */
public final class Cauldron311ClientChecks implements FabricClientGameTest {
private static final BlockPos POT=new BlockPos(0,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void use(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(POT),Direction.UP,POT,false)));c.waitTicks(6);}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);var audio=new CopyOnWriteArrayList<String>();
SoundEventListener listener=(sound,event,distance)->{if(sound.getX()>=0&&sound.getX()<=1&&sound.getZ()>=0&&sound.getZ()<=1)audio.add(sound.getIdentifier().toString());};
c.runOnClient(m->{var device=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(device.backendName().equalsIgnoreCase("vulkan"),"Cauldron validation requires Vulkan");System.out.println("CAULDRON311_BACKEND "+device.backendName()+" / "+device.name());m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;m.getSoundManager().addListener(listener);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
TestWorldSave saved;
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Cauldron311-"+UUID.randomUUID());s.setSeed("311");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.setBlockAndUpdate(new BlockPos(0,0,2),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(POT.below(),Blocks.CAMPFIRE.defaultBlockState());l.setBlockAndUpdate(POT,Blocks.CAULDRON.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.MILK_BUCKET));});c.waitTicks(8);
// Real Q input creates the player's item entity; only position it over the opening.
c.getInput().pressKey(o->o.keyDrop);c.waitTicks(5);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var milk=p.level().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(4),e->e.getItem().is(Items.MILK_BUCKET)).getFirst();milk.setPos(.5,1.6,.5);milk.setDeltaMovement(0,0,0);milk.setPickUpDelay(200);});
c.waitFor(m->m.level.getBlockEntity(POT) instanceof CookingCauldronBlockEntity pot&&pot.getItem(0).is(Items.MILK_BUCKET),200);c.waitTicks(650);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(pot.progress()==0&&!pot.assemblyComplete()&&pot.getItem(0).is(Items.MILK_BUCKET)&&pot.getItem(6).isEmpty(),"Real 30-second wait cannot cook or fail the dropped milk");p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.STICK));});c.waitTicks(6);c.getInput().lookAt(POT);c.waitTicks(5);c.runOnClient(m->m.getWindow().setWindowed(1280,960));
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(POT),"Actual cauldron HUD targeted: "+m.hitResult+" eye="+m.player.getEyePosition());});c.waitTicks(3);c.takeScreenshot("cauldron311-stir-"+locale+"-gui"+scale);}
use(c);c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).progress()>35,150);c.takeScreenshot("cauldron311-simmering");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(pot.getItem(0).is(Items.MILK_BUCKET)&&pot.getItem(6).isEmpty()&&p.getMainHandItem().is(Items.STICK)&&p.getMainHandItem().getCount()==1,"The real gesture consumes neither the stick nor the milk before completion");});
c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).getItem(6).is(Cuisine.item("cream")),250);c.takeScreenshot("cauldron311-cream-and-ejected-bucket");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(pot.getItem(6).getCount()==2&&pot.getItem(0).isEmpty(),"Two paid creams finish one recipe");int buckets=p.getInventory().countItem(Items.BUCKET)+p.level().getEntitiesOfClass(ItemEntity.class,new AABB(POT).inflate(4),e->e.getItem().is(Items.BUCKET)).stream().mapToInt(e->e.getItem().getCount()).sum();check(buckets==1,"Exactly one actual empty bucket is ejected");});
for(String id:List.of("minecraft:item.bucket.empty","minecraft:item.bottle.empty","minecraft:block.bubble_column.bubble_pop","minecraft:block.brewing_stand.brew"))check(audio.contains(id),"Native audio engine received the preparation sound: "+id+" heard="+audio);
saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(pot.getItem(6).is(Cuisine.item("cream"))&&pot.getItem(6).getCount()==2,"Real disk reopening preserves the paid preparation");p.teleportTo(.5,1,2.5);});c.waitTicks(8);c.getInput().lookAt(POT);c.waitTicks(4);c.takeScreenshot("cauldron311-ready-after-disk");use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Cuisine.item("cream"))==2&&p.level().getBlockEntity(POT) instanceof CookingCauldronBlockEntity kept&&kept.repeatsRecipe()&&p.getInventory().countItem(Items.STICK)==1,"Actual serving packet returns exactly two creams and retains the authorized cauldron recipe");
p.teleportTo(.5,1,2.5);p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.STICK));var milk=new ItemEntity(p.level(),.5,1.5,.5,new ItemStack(Items.MILK_BUCKET));milk.setPickUpDelay(300);p.level().addFreshEntity(milk);
});c.waitFor(m->m.level.getBlockEntity(POT) instanceof CookingCauldronBlockEntity,150);
server.runOnServer(s->((CookingCauldronBlockEntity)s.overworld().getBlockEntity(POT)).insertIngredient(new ItemStack(Items.CARROT),1));use(c);c.takeScreenshot("cauldron311-incomplete-keeps-milk");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(!pot.assemblyComplete()&&pot.getItem(0).is(Items.MILK_BUCKET)&&pot.getItem(6).isEmpty(),"Invalid real stirring never creates a failed dish or consumes milk");p.giveExperienceLevels(4);RecipeService.discover(p,AgricultureItems.TOMATO);RecipeService.buy(p);RecipeService.refresh(p);});
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,300);c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("cooking_cauldron")).findFirst().orElseThrow();check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==19,"Nineteen original cauldron recipes remain in JEI");runtime.getRecipesGui().showTypes(List.of(type));});c.waitTicks(5);c.takeScreenshot("cauldron311-jei-nineteen");c.getInput().pressKey(InputConstants.KEY_ESCAPE);
}
check(audio.contains("minecraft:entity.item.pickup"),"Serving sound reached the actual audio engine");c.runOnClient(m->m.getSoundManager().removeListener(listener));
System.out.println("CAULDRON311_AUDIO "+audio);
System.out.println("CAULDRON311_CLIENT_PASS Vulkan, Q-drop native milk into empty cauldron, actual 30-second wait without failure, FR/EN HUD GUI2/3/4, actual stick/cooking/serving packets, delayed bucket, native audio dispatch, disk reopen, incomplete mixture preserved and JEI19");
}
}
@@ -0,0 +1,72 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.CookingCauldronBlockEntity;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.sounds.SoundEventListener;
import net.minecraft.core.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** One real stir, repeated native top-ups and serving, hot work across disk, audio and explicit stop. */
public final class Cauldron312ClientChecks implements FabricClientGameTest {
private static final BlockPos POT=new BlockPos(0,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void use(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(POT),Direction.UP,POT,false)));c.waitTicks(6);}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);var audio=new CopyOnWriteArrayList<String>();
SoundEventListener listener=(sound,event,distance)->{if(sound.getX()>=0&&sound.getX()<=1&&sound.getZ()>=0&&sound.getZ()<=1)audio.add(sound.getIdentifier().toString());};
c.runOnClient(m->{var device=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(device.backendName().equalsIgnoreCase("vulkan"),"Cauldron validation requires Vulkan");System.out.println("CAULDRON312_BACKEND "+device.backendName()+" / "+device.name());m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;m.getSoundManager().addListener(listener);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
TestWorldSave saved;
var beforeTopUp=new java.util.concurrent.atomic.AtomicInteger();
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Cauldron312-"+UUID.randomUUID());s.setSeed("312");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.setBlockAndUpdate(new BlockPos(0,0,2),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(POT.below(),Blocks.CAMPFIRE.defaultBlockState());l.setBlockAndUpdate(POT,Blocks.CAULDRON.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(7,new ItemStack(Items.MILK_BUCKET));p.getInventory().setItem(8,new ItemStack(Items.STICK));});c.waitTicks(8);select(c,7);
c.getInput().pressKey(o->o.keyDrop);c.waitTicks(5);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var milk=p.level().getEntitiesOfClass(ItemEntity.class,p.getBoundingBox().inflate(4),e->e.getItem().is(Items.MILK_BUCKET)).getFirst();milk.setPos(.5,1.6,.5);milk.setDeltaMovement(0,0,0);milk.setPickUpDelay(200);});
c.waitFor(m->m.level.getBlockEntity(POT) instanceof CookingCauldronBlockEntity pot&&pot.getItem(0).is(Items.MILK_BUCKET),200);c.waitTicks(40);select(c,8);use(c);
c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).progress()>60,150);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);beforeTopUp.set(pot.progress());check(p.getMainHandItem().is(Items.STICK)&&p.getMainHandItem().getCount()==1,"The one actual starting gesture preserves its stick");p.getInventory().setItem(7,new ItemStack(Items.MILK_BUCKET));});select(c,7);use(c);
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getInventory().setItem(7,new ItemStack(Items.MILK_BUCKET)));c.waitTicks(4);use(c);
server.runOnServer(s->{var pot=(CookingCauldronBlockEntity)s.overworld().getBlockEntity(POT);check(pot.progress()>=beforeTopUp.get()&&pot.getItem(6).isEmpty()&&pot.repeatsRecipe(),"Two actual compatible additions preserve the unpaid first cycle");});
c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).getItem(6).getCount()==6,440);c.waitTicks(22);c.getInput().lookAt(POT);c.waitTicks(5);c.runOnClient(m->m.getWindow().setWindowed(1280,960));
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();var pot=(CookingCauldronBlockEntity)m.level.getBlockEntity(POT);check(pot.repeatsRecipe()&&pot.repeatState()==CookingCauldronBlockEntity.NEEDS_INPUT&&pot.getItem(6).getCount()==6,"Actual waiting-for-supply HUD synchronized");var text=net.minecraft.network.chat.Component.translatable("hud.sanctuary.cooking_cauldron.stored_status",6,pot.getItem(6).getHoverName(),net.minecraft.network.chat.Component.translatable("hud.sanctuary.cooking_cauldron.resupply"));check(m.font.width(text)+8<=m.getWindow().getGuiScaledWidth(),"Stored result and repeat state fit every tested GUI scale");});c.waitTicks(3);c.takeScreenshot("cauldron312-resupply-"+locale+"-gui"+scale);}
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getInventory().setItem(7,new ItemStack(Items.MILK_BUCKET)));c.waitTicks(4);use(c);
c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).progress()>60,150);c.takeScreenshot("cauldron312-second-cycle-with-first-output");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(((CookingCauldronBlockEntity)p.level().getBlockEntity(POT)).getItem(6).getCount()==6&&p.getInventory().countItem(Cuisine.item("cream"))==0,"Milk input with existing output feeds the next cycle without serving or restarting");});use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(p.getInventory().countItem(Cuisine.item("cream"))==6&&pot.getItem(6).isEmpty()&&pot.progress()>60&&pot.repeatsRecipe(),"Actual serving preserves active work for the second cycle");p.level().setBlockAndUpdate(POT.below(),Blocks.STONE.defaultBlockState());});c.waitTicks(4);saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);check(pot.repeatsRecipe()&&pot.repeatedRecipe().getPath().equals("physical_cauldron_cream")&&pot.progress()>60&&pot.progress()<180&&pot.getItem(6).isEmpty()&&pot.getItem(0).is(Items.MILK_BUCKET)&&p.getInventory().countItem(Cuisine.item("cream"))==6,"Actual disk reopen keeps the exact recipe, paid food and unpaid pending cycle");p.teleportTo(.5,1,2.5);p.level().setBlockAndUpdate(POT.below(),Blocks.CAMPFIRE.defaultBlockState());});c.waitTicks(6);c.getInput().lookAt(POT);c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).getItem(6).getCount()==2,250);c.takeScreenshot("cauldron312-second-cycle-after-disk");c.waitTicks(22);
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getInventory().setItem(7,new ItemStack(Items.MILK_BUCKET)));select(c,7);use(c);c.waitFor(m->((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).getItem(6).getCount()==4,260);c.takeScreenshot("cauldron312-third-cycle-without-another-stir");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();int buckets=p.getInventory().countItem(Items.BUCKET)+p.level().getEntitiesOfClass(ItemEntity.class,new AABB(POT).inflate(12),e->e.isAlive()&&e.getItem().is(Items.BUCKET)).stream().mapToInt(e->e.getItem().getCount()).sum();check(buckets==5&&p.getInventory().countItem(Cuisine.item("cream"))==6,"Exactly five milk payments return exactly five native buckets across the four cycles");});
c.getInput().holdKey(o->o.keyShift);c.waitTicks(4);use(c);c.getInput().releaseKey(o->o.keyShift);c.waitTicks(4);
server.runOnServer(s->{var pot=(CookingCauldronBlockEntity)s.overworld().getBlockEntity(POT);check(!pot.repeatsRecipe()&&pot.getItem(6).getCount()==4,"Actual empty-handed sneak interaction stops the series while preserving its output");});c.takeScreenshot("cauldron312-explicitly-stopped");use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Cuisine.item("cream"))==10&&p.getInventory().countItem(Items.STICK)==1&&p.level().getBlockState(POT).is(Blocks.CAULDRON),"Final native serving returns all ten paid creams and restores the stopped vanilla block");});
}
long completed=audio.stream().filter(id->id.equals("minecraft:block.brewing_stand.brew")).count();check(completed==4,"Exactly one completion cue per paid cycle, with existing output and disk reopen: "+audio);c.runOnClient(m->m.getSoundManager().removeListener(listener));
System.out.println("CAULDRON312_AUDIO completed_batches="+completed);
System.out.println("CAULDRON312_CLIENT_PASS Vulkan, one actual stir, native Q milk drop and compatible top-ups, FR/EN GUI2/3/4 resupply HUD, accumulated output, actual serving without cancellation, disk pending work, four real cycles, five paid milk/returned buckets, ten creams, one audio cue per batch and native explicit stop");
}
private static void select(ClientGameTestContext c,int slot){c.runOnClient(m->{m.player.getInventory().setSelectedSlot(slot);m.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(slot));});c.waitTicks(4);}
}
@@ -0,0 +1,63 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.FermentationBarrelBlockEntity;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.*;
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.*;
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.core.*;
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.gamerules.GameRules;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native fermentation, placed cheese slices, actual disk save and real-clock aged drinks. */
public final class CheeseWine314ClientChecks implements FabricClientGameTest {
private static final BlockPos CHEESE=new BlockPos(0,1,0),BARREL=new BlockPos(-2,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void select(ClientGameTestContext c,int slot){c.runOnClient(m->{m.player.getInventory().setSelectedSlot(slot);m.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(slot));});c.waitTicks(4);}
private static void use(ClientGameTestContext c,BlockPos target){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(target.getX()+.5,target.getY()+1,target.getZ()+.5),Direction.UP,target,false)));c.waitTicks(6);}
private static void accelerateFermentation(net.minecraft.server.MinecraftServer s){var p=s.getPlayerList().getPlayers().getFirst();var b=(FermentationBarrelBlockEntity)p.level().getBlockEntity(BARREL);check(b.startedAtGameTime()>=0,"A paid native recipe has actually started");var tag=b.saveCustomOnly(p.level().registryAccess());tag.putLong("FermentationStartedAt",p.level().getGameTime()-b.activeDuration()+10);b.loadWithComponents(TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,p.level().registryAccess(),tag));}
private static void setAge(net.minecraft.server.MinecraftServer s,int days){var p=s.getPlayerList().getPlayers().getFirst();var b=(FermentationBarrelBlockEntity)p.level().getBlockEntity(BARREL);var tag=b.saveCustomOnly(p.level().registryAccess());tag.putLong("WineStartedAtEpochMillis",System.currentTimeMillis()-days*86_400_000L-1000);b.loadWithComponents(TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,p.level().registryAccess(),tag));}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);var origin=new AtomicLong();TestWorldSave saved;
c.runOnClient(m->{var d=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(d.backendName().equalsIgnoreCase("vulkan"),"Only Vulkan is validated");System.out.println("CHEESEWINE314_BACKEND "+d.backendName()+" / "+d.name());m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("CheeseWine314-"+UUID.randomUUID());s.setSeed("314");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);fr.koka.sanctuary.weather.WeatherService.setMode(s,false);}catch(Exception e){throw new IllegalStateException(e);}s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.getWeatherData().setRaining(false);l.setRainLevel(0);l.setBlockAndUpdate(BARREL,KitchenAppliances.FERMENTATION_BARREL.defaultBlockState());l.setBlockAndUpdate(CHEESE.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(new BlockPos(0,0,2),Blocks.STONE.defaultBlockState());((net.minecraft.world.level.storage.ServerLevelData)l.getLevelData()).setGameTime(10000);p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Cuisine.item("cream")));p.getInventory().setItem(1,new ItemStack(Cuisine.item("salt")));p.getInventory().setItem(2,new ItemStack(Cuisine.item("grape_must")));p.giveExperienceLevels(4);RecipeService.discover(p,Cuisine.item("grape_must"));RecipeService.buy(p);RecipeService.refresh(p);});c.waitTicks(8);
select(c,0);use(c,BARREL);select(c,1);use(c,BARREL);server.runOnServer(CheeseWine314ClientChecks::accelerateFermentation);c.waitFor(m->((FermentationBarrelBlockEntity)m.level.getBlockEntity(BARREL)).visibleItem().is(Cuisine.item("cheese")),100);select(c,0);use(c,BARREL);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Cuisine.item("cheese"))==1&&p.getInventory().countItem(Cuisine.item("cream"))==0&&p.getInventory().countItem(Cuisine.item("salt"))==0,"Native cream and salt interactions paid one cheese");});
use(c,CHEESE.below());c.waitFor(m->m.level.getBlockState(CHEESE).getBlock() instanceof fr.koka.sanctuary.cuisine.culinary.block.CheeseBlock,100);c.getInput().lookAt(CHEESE);c.waitTicks(4);c.takeScreenshot("cheesewine314-whole-wheel");
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);int states=0;var block=((BlockItem)Cuisine.item("cheese")).getBlock();for(var state:block.getStateDefinition().getPossibleStates()){var model=m.getModelManager().getBlockStateModelSet().get(state);var quads=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();model.collectParts(net.minecraft.util.RandomSource.create(314),quads);check(!quads.isEmpty(),"Every native bite state has a valid model");int count=0;for(var part:quads)for(var direction:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})for(var q:part.getQuads(direction)){String expected=state.getValue(CakeBlock.BITES)>0&&q.direction()==Direction.WEST?"block/end_stone":"block/yellow_terracotta";check(q.materialInfo().sprite().contents().name().toString().equals("minecraft:"+expected),"Yellow rind/top remain outside; only an eaten cross-section exposes the hole texture");count++;}check(count==6,"Every cheese state has all six visible faces");states++;}check(states==7,"Exactly seven cake slice states");net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());var food=net.minecraft.core.registries.BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();for(var item:List.of(Cuisine.item("cheese"),Cuisine.item("wine"),KitchenAppliances.TABLE_WINE,KitchenAppliances.RESERVE_WINE,KitchenAppliances.GRAND_CRU_WINE)){var render=new net.minecraft.client.renderer.item.ItemStackRenderState();m.getItemModelResolver().updateForTopItem(render,new ItemStack(item),ItemDisplayContext.GUI,m.level,m.player,314);check(!render.isEmpty()&&!render.pickParticleMaterial(net.minecraft.util.RandomSource.create(314)).sprite().contents().name().getPath().equals("missingno")&&food.getDisplayItems().stream().anyMatch(stack->stack.is(item)),"Native models and existing food category");}System.out.println("CHEESEWINE314_MODELS states=7 items=5 native_food_tab=true");});
for(int i=0;i<3;i++){server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getFoodData().setFoodLevel(0));c.waitFor(m->m.player.getFoodData().getFoodLevel()==0,100);use(c,CHEESE);final int eaten=i+1;server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var state=p.level().getBlockState(CHEESE);check(p.getFoodData().getFoodLevel()==2&&(eaten==7?state.isAir():state.getValue(CakeBlock.BITES)==eaten),"Each real native bite pays a visible slice and restores food");});if(i==2||i==5)c.takeScreenshot("cheesewine314-slices-remaining-"+(6-i));}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.level().setBlockAndUpdate(new BlockPos(-1,0,2),Blocks.STONE.defaultBlockState());p.teleportTo(-.5,1,2.5);});c.waitTicks(5);c.getInput().lookAt(CHEESE);c.waitTicks(4);c.takeScreenshot("cheesewine314-cut-face-and-yellow-rind");
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(.5,1,2.5));c.waitTicks(5);
select(c,2);use(c,BARREL);server.runOnServer(CheeseWine314ClientChecks::accelerateFermentation);c.waitFor(m->((FermentationBarrelBlockEntity)m.level.getBlockEntity(BARREL)).hasMaturingWine(),100);select(c,0);c.getInput().lookAt(BARREL);c.waitTicks(5);
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();for(var item:List.of(Cuisine.item("cheese"),Cuisine.item("wine"),KitchenAppliances.TABLE_WINE,KitchenAppliances.RESERVE_WINE,KitchenAppliances.GRAND_CRU_WINE))check(!new ItemStack(item).getHoverName().getString().matches("(item|block)\\.sanctuary.*"),"Every new quality is localized");});c.waitTicks(3);c.takeScreenshot("cheesewine314-hud-"+locale+"-gui"+scale);}
server.runOnServer(s->setAge(s,1));c.waitFor(m->((FermentationBarrelBlockEntity)m.level.getBlockEntity(BARREL)).visibleItem().is(KitchenAppliances.TABLE_WINE),100);c.takeScreenshot("cheesewine314-table-wine-one-real-day");server.runOnServer(s->origin.set(((FermentationBarrelBlockEntity)s.getPlayerList().getPlayers().getFirst().level().getBlockEntity(BARREL)).wineStartedAtEpochMillis()));saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var b=(FermentationBarrelBlockEntity)p.level().getBlockEntity(BARREL);check(b.getItem(4).is(KitchenAppliances.TABLE_WINE)&&b.wineStartedAtEpochMillis()==origin.get(),"Actual disk reopen preserves the quality and UTC origin");});server.runOnServer(s->check(s.getPlayerList().getPlayers().getFirst().level().getBlockState(CHEESE).getValue(CakeBlock.BITES)==3,"Actual disk reopen preserves the three paid eaten slices"));
for(int i=3;i<7;i++){server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getFoodData().setFoodLevel(0));c.waitFor(m->m.player.getFoodData().getFoodLevel()==0,100);use(c,CHEESE);final int eaten=i+1;server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var state=p.level().getBlockState(CHEESE);check(p.getFoodData().getFoodLevel()==2&&(eaten==7?state.isAir():state.getValue(CakeBlock.BITES)==eaten),"Each real native bite pays a visible slice and restores food");});if(i==2||i==5)c.takeScreenshot("cheesewine314-slices-remaining-"+(6-i));}
server.runOnServer(s->check(s.getPlayerList().getPlayers().getFirst().getInventory().countItem(Cuisine.item("cheese"))==0,"No whole cheese remains after native placement and seven bites"));c.getInput().lookAt(CHEESE);c.takeScreenshot("cheesewine314-last-slice-consumed");
c.getInput().lookAt(BARREL);server.runOnServer(s->setAge(s,3));c.waitFor(m->((FermentationBarrelBlockEntity)m.level.getBlockEntity(BARREL)).visibleItem().is(KitchenAppliances.RESERVE_WINE),100);c.takeScreenshot("cheesewine314-reserve-three-real-days");server.runOnServer(s->setAge(s,7));c.waitFor(m->((FermentationBarrelBlockEntity)m.level.getBlockEntity(BARREL)).visibleItem().is(KitchenAppliances.GRAND_CRU_WINE),100);c.takeScreenshot("cheesewine314-grand-cru-seven-real-days");select(c,0);use(c,BARREL);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.GRAND_CRU_WINE)==1&&((FermentationBarrelBlockEntity)p.level().getBlockEntity(BARREL)).isEmpty(),"Native collection yields just one grand cru from the one paid must");p.getFoodData().setFoodLevel(0);});c.waitFor(m->m.player.getFoodData().getFoodLevel()==0,100);c.getInput().lookAt(0,-40);c.getInput().holdKey(o->o.keyUse);c.waitFor(m->m.player.getMainHandItem().isEmpty(),200);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(4);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.GRAND_CRU_WINE)==0&&p.getFoodData().getFoodLevel()>0,"The actual native drink consumes the grand cru and restores food");});
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,200);c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("wine_maturation")).findFirst().orElseThrow();check(KitchenJei.types().size()==8&&runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==3,"JEI synchronizes all three real-day maturation stages");runtime.getRecipesGui().showTypes(List.of(type));});c.waitTicks(5);c.takeScreenshot("cheesewine314-jei-real-days");c.getInput().pressKey(InputConstants.KEY_ESCAPE);
}
System.out.println("CHEESEWINE314_CLIENT_PASS Vulkan, paid native fermentation/cheese placement, seven native slices, models, existing food tab, FR/EN GUI2/3/4, real UTC ageing, disk origin/quality, native grand cru collection/drinking, three JEI real-day stages");
}
}
@@ -0,0 +1,164 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.multiblock.*;
import net.minecraft.world.entity.item.ItemEntity;
import static fr.koka.sanctuary.cuisine.machine.CrushableProduceBlock.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.*;
import fr.koka.sanctuary.cuisine.culinary.block.*;
import fr.koka.sanctuary.cuisine.culinary.client.screen.*;
import fr.koka.sanctuary.cuisine.culinary.recipe.*;
import fr.koka.sanctuary.cuisine.machine.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.core.Direction;
import net.minecraft.world.phys.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
import java.util.concurrent.atomic.AtomicBoolean;
import mezz.jei.api.constants.RecipeTypes;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.util.RandomSource;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.item.alchemy.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native table packets, real smoker ticks, food catalogue/JEI and actual save reopening. */
public final class Crushing305ClientChecks implements FabricClientGameTest {
private static final BlockPos FRUIT=new BlockPos(0,1,0), PARTIAL=new BlockPos(6,1,0), SUPER=new BlockPos(14,3,0);
private static final java.util.Map<BlockPos,net.minecraft.world.level.block.state.BlockState> gallery=new LinkedHashMap<>();
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Cuisine validation requires Vulkan");
System.out.println("CRUSHING305_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Crushing305-" + UUID.randomUUID()); s.setSeed("305"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server=world.getServer();
server.runOnServer(s->{
var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);
l.setBlockAndUpdate(FRUIT.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(FRUIT,KitchenAppliances.TOMATO_BLOCK.defaultBlockState());
l.setBlockAndUpdate(PARTIAL.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(PARTIAL,KitchenAppliances.OLIVE_BLOCK.defaultBlockState().setValue(LAYERS,2));
p.setGameMode(GameType.SURVIVAL);p.getFoodData().setFoodLevel(20);p.getInventory().clearContent();p.teleportTo(.5,2,.5);
});c.waitFor(m->m.player.onGround()&&m.level.getBlockState(FRUIT).is(KitchenAppliances.TOMATO_BLOCK),200);
for(int jump=1;jump<=8;jump++){
int left=8-jump;
c.getInput().holdKey(o->o.keyJump);c.waitTicks(2);c.getInput().releaseKey(o->o.keyJump);
c.waitTicks(25);
c.runOnClient(m->System.out.println("CRUSHING305_JUMP_TRACE client left="+left+" state="+m.level.getBlockState(FRUIT)+" y="+m.player.getY()+" fall="+m.player.fallDistance+" ground="+m.player.onGround()));
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();System.out.println("CRUSHING305_JUMP_TRACE server left="+left+" state="+p.level().getBlockState(FRUIT)+" y="+p.getY()+" fall="+p.fallDistance+" ground="+p.onGround());});
c.waitFor(m->m.player.onGround() && (left==0?m.level.getBlockState(FRUIT).isAir():m.level.getBlockState(FRUIT).is(KitchenAppliances.TOMATO_BLOCK)&&m.level.getBlockState(FRUIT).getValue(LAYERS)==left),200);
c.waitTicks(4);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();
int portions=p.getInventory().countItem(Cuisine.item("tomato_pulp"))+count(l,new AABB(FRUIT).inflate(4),Cuisine.item("tomato_pulp"));
check(portions==(8-left)*9/8,"Native jump pays exact cumulative yield: "+left+" -> "+portions);
check(left==0?l.getBlockState(FRUIT).isAir():l.getBlockState(FRUIT).getValue(LAYERS)==left,"No extra fall caused by lowering the block");
});
}
System.out.println("CRUSHING305_NATIVE_JUMPS count=8 layers_removed=8 portions=9");
server.runOnServer(s->{
var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();gallery.clear();int row=0;
for(var block:List.of(KitchenAppliances.TOMATO_BLOCK,KitchenAppliances.GRAPE_BLOCK,KitchenAppliances.OLIVE_BLOCK)){
for(int layer=1;layer<=8;layer++){
var pos=new BlockPos(layer-1,1,4+row*3);l.setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());
var state=block.defaultBlockState().setValue(LAYERS,layer);l.setBlockAndUpdate(pos,state);gallery.put(pos,state);
}row++;
}
p.teleportTo(4,5,16);p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();
});c.waitFor(m->Math.abs(m.player.getZ()-16)<.1,200);c.getInput().lookAt(new BlockPos(4,1,7));c.waitTicks(8);
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);int models=0;
for(var entry:gallery.entrySet()){
var state=entry.getValue();var parts=new ArrayList<BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(305),parts);
int quads=0;float maxY=0;
for(var part:parts)for(var direction:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})for(var quad:part.getQuads(direction)){
check(quad.materialInfo().sprite().contents().name().getNamespace().equals("sanctuary"),"Supplied fruit texture resolves at every height");
for(int v=0;v<4;v++)maxY=Math.max(maxY,quad.position(v).y());quads++;
}
check(quads>0 && Math.abs(maxY-state.getValue(LAYERS)/8F)<.0001,"Baked model and collision height agree: "+state+" maxY="+maxY);models++;
}
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());
for(var tab:BuiltInRegistries.CREATIVE_MODE_TAB)check(tab.getDisplayItems().stream().noneMatch(i->i.is(KitchenAppliances.FOOD_PRESS.asItem())),"Retired press absent from every creative tab");
System.out.println("CRUSHING305_MODELS states="+models+" heights=8 retired_creative=false");
});c.waitTicks(5);c.takeScreenshot("crushing305-eight-heights-three-fruits");
server.runOnServer(s->{
var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();var f=Direction.NORTH;
for(int i=0;i<9;i++)l.setBlockAndUpdate(SuperPistons.tile(SUPER,f,i),Blocks.PISTON.defaultBlockState().setValue(net.minecraft.world.level.block.piston.PistonBaseBlock.FACING,f));
check(SuperPistons.assemble(l,SUPER,f,false),"Real 3x3 super piston assembly");
for(int depth=1;depth<=3;depth++)for(int i=0;i<9;i++){
var pos=SuperPistons.tile(SUPER,f,i).relative(f,depth);if(pos.getY()==SUPER.getY()-1)l.setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());
var fruit=List.of(KitchenAppliances.TOMATO_BLOCK,KitchenAppliances.GRAPE_BLOCK,KitchenAppliances.OLIVE_BLOCK).get(i%3);l.setBlockAndUpdate(pos,fruit.defaultBlockState());
}
p.teleportTo(21,8,-10);
});c.waitFor(m->Math.abs(m.player.getX()-21)<.1&&Math.abs(m.player.getZ()+10)<.1,200);c.getInput().lookAt(SUPER);c.waitTicks(5);c.takeScreenshot("crushing305-super-piston-before-27-blocks");
server.runOnServer(s->s.overworld().setBlockAndUpdate(SuperPistons.tile(SUPER,Direction.NORTH,0).south(),Blocks.REDSTONE_BLOCK.defaultBlockState()));c.waitTicks(35);
server.runOnServer(s->{var l=s.overworld();check(((SuperPistonController)l.getBlockEntity(SUPER)).stroke==3,"Full native powered course");
for(int depth=1;depth<=3;depth++)for(var pos:SuperPistons.tiles(SUPER,Direction.NORTH,depth))check(!(l.getBlockState(pos).getBlock() instanceof CrushableProduceBlock),"All 27 blocks crushed");
for(String item:List.of("tomato_pulp","grape_must","olive_oil"))check(count(l,new AABB(SUPER).inflate(7),Cuisine.item(item))==81,"Nine blocks give 81 portions each: "+item);
System.out.println("CRUSHING305_SUPER_PISTON blocks=27 portions=243 activation=1");
});c.takeScreenshot("crushing305-super-piston-after-243-portions");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(4);RecipeService.discover(p,AgricultureItems.TOMATO);RecipeService.buy(p);RecipeService.refresh(p);});
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,300);c.waitTicks(8);
var type=c.computeOnClient(m->{
var runtime=SanctuaryJeiPlugin.runtime();var crushing=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("fruit_crushing")).findFirst().orElseThrow();
check(runtime.getRecipeManager().createRecipeLookup(crushing).includeHidden().get().count()==3,"Three direct crushing recipes indexed");
check(!SanctuaryJeiPlugin.knows(new ItemStack(KitchenAppliances.FOOD_PRESS)),"Compatibility press cannot return through operator catalogue");
check(runtime.getIngredientManager().getAllItemStacks().stream().noneMatch(i->i.is(KitchenAppliances.FOOD_PRESS.asItem())),"Press absent from actual JEI ingredients");
return crushing;
});
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();SanctuaryJeiPlugin.runtime().getRecipesGui().showTypes(List.of(type));});
c.waitTicks(5);c.takeScreenshot("crushing305-jei-"+locale+"-gui"+scale);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(2);
}
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=s.overworld();
for(var e:gallery.entrySet())check(l.getBlockState(e.getKey()).equals(e.getValue()),"Disk reload preserves every partial state");
check(l.getBlockState(PARTIAL).getValue(LAYERS)==2,"Partly crushed olive block persists");
var base=PARTIAL.west();l.setBlockAndUpdate(base,Blocks.PISTON.defaultBlockState().setValue(net.minecraft.world.level.block.piston.PistonBaseBlock.FACING,Direction.EAST));l.setBlockAndUpdate(base.west(),Blocks.REDSTONE_BLOCK.defaultBlockState());
});c.waitTicks(6);
world.getServer().runOnServer(s->{check(count(s.overworld(),new AABB(PARTIAL).inflate(3),Cuisine.item("olive_oil"))==3,"Native piston after reload pays only three remaining portions");System.out.println("CRUSHING305_DISK_RELOAD partial_states=24 native_piston_remaining=3");});
}
System.out.println("CRUSHING305_CLIENT_PASS Vulkan, eight keyboard jumps, 24 models, 27-block super piston, retired press, JEI FR/EN GUI 2/3/4, disk reload");
}
private static int count(net.minecraft.server.level.ServerLevel l,AABB box,Item item){return l.getEntitiesOfClass(ItemEntity.class,box,e->e.isAlive()&&e.getItem().is(item)).stream().mapToInt(e->e.getItem().getCount()).sum();}
}
@@ -0,0 +1,187 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.Cuisine;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import mezz.jei.api.constants.RecipeTypes;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.util.RandomSource;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.item.alchemy.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native table packets, real smoker ticks, food catalogue/JEI and actual save reopening. */
public final class Cuisine303ClientChecks implements FabricClientGameTest {
private static final BlockPos TABLE = new BlockPos(0, 1, 0), SMOKER = new BlockPos(2, 1, 0), CHEST = new BlockPos(4, 1, 0);
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Cuisine validation requires Vulkan");
System.out.println("CUISINE303_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Cuisine303-" + UUID.randomUUID()); s.setSeed("303"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server = world.getServer();
server.runOnServer(s -> {
var p = s.getPlayerList().getPlayers().getFirst(); var level = p.level();
level.getGameRules().set(GameRules.SPAWN_MOBS, false, s);
level.setBlockAndUpdate(TABLE, Blocks.CRAFTING_TABLE.defaultBlockState());
level.setBlockAndUpdate(SMOKER, Blocks.SMOKER.defaultBlockState());
level.setBlockAndUpdate(CHEST, Blocks.CHEST.defaultBlockState());
p.teleportTo(.5, 1, 2.5); p.setGameMode(GameType.SURVIVAL); p.getInventory().clearContent();
p.openMenu(new SimpleMenuProvider((id, inventory, player) -> new CraftingMenu(id, inventory, ContainerLevelAccess.create(level, TABLE)), Component.literal("Cuisine")));
var menu = p.containerMenu;
menu.getSlot(1).set(new ItemStack(AgricultureItems.LETTUCE));
menu.getSlot(2).set(new ItemStack(AgricultureItems.CUCUMBER));
menu.getSlot(3).set(new ItemStack(AgricultureItems.TOMATO)); menu.broadcastChanges();
});
c.waitFor(m -> m.gui.screen() instanceof CraftingScreen && m.player.containerMenu.getSlot(0).getItem().is(Cuisine.item("green_salad")), 300);
c.takeScreenshot("cuisine303-native-salad-crafting");
c.runOnClient(m -> m.gameMode.handleContainerInput(m.player.containerMenu.containerId, 0, 0, ContainerInput.QUICK_MOVE, m.player)); c.waitTicks(6);
server.runOnServer(s -> {
var p = s.getPlayerList().getPlayers().getFirst();
check(p.getInventory().countItem(Cuisine.item("green_salad")) == 1, "Actual native packet crafts one salad");
for (int i = 1; i <= 3; i++) check(p.containerMenu.getSlot(i).getItem().isEmpty(), "Actual ingredients consumed once");
p.containerMenu.getSlot(1).set(new ItemStack(Cuisine.item("ground_coffee"), 3));
p.containerMenu.getSlot(2).set(PotionContents.createItemStack(Items.POTION, Potions.WATER)); p.containerMenu.broadcastChanges();
});
c.waitFor(m -> m.player.containerMenu.getSlot(0).getItem().is(Cuisine.item("raw_cauldron_coffee")), 200);
c.runOnClient(m -> m.gameMode.handleContainerInput(m.player.containerMenu.containerId, 0, 0, ContainerInput.QUICK_MOVE, m.player)); c.waitTicks(6);
server.runOnServer(s -> {
var p = s.getPlayerList().getPlayers().getFirst();
check(p.getInventory().countItem(Cuisine.item("raw_cauldron_coffee")) == 1 && p.containerMenu.getSlot(2).getItem().is(Items.GLASS_BOTTLE), "Actual native crafting returns one water bottle");
check(p.containerMenu.getSlot(1).getItem().getCount() == 2, "Exactly one ground coffee consumed");
});
c.getInput().pressKey(InputConstants.KEY_ESCAPE); c.waitFor(m -> m.gui.screen() == null, 200);
server.runOnServer(s -> {
var p = s.getPlayerList().getPlayers().getFirst(); var level = p.level();
var slot = p.getInventory().findSlotMatchingItem(new ItemStack(Cuisine.item("raw_cauldron_coffee")));
var removed = p.getInventory().removeItem(slot, 1);
check(removed.getCount() == 1, "The crafted batch moves to the smoker");
var smoker = (AbstractFurnaceBlockEntity) level.getBlockEntity(SMOKER);
smoker.setItem(0, new ItemStack(Cuisine.item("raw_cauldron_coffee"))); smoker.setItem(1, new ItemStack(Items.COAL));
});
c.waitTicks(225);
server.runOnServer(s -> {
var smoker = (AbstractFurnaceBlockEntity) s.overworld().getBlockEntity(SMOKER);
check(smoker.getItem(0).isEmpty() && smoker.getItem(2).is(Cuisine.item("coffee")) && smoker.getItem(2).getCount() == 1, "Real native smoker ticks cook the paid batch");
var p = s.getPlayerList().getPlayers().getFirst();
p.giveExperienceLevels(4); RecipeService.discover(p, AgricultureItems.TOMATO); check(RecipeService.buy(p), "Recipe catalogue unlocked normally"); RecipeService.refresh(p);
});
c.waitFor(m -> RecipeClient.allowed() && RecipeClient.knowledge.recipes.contains("sanctuary:cold_green_salad") && SanctuaryJeiPlugin.runtime() != null, 300);
c.waitTicks(15);
c.runOnClient(m -> {
var runtime = SanctuaryJeiPlugin.runtime(); var manager = runtime.getRecipeManager(); var actual = new HashSet<String>();
manager.createRecipeLookup(RecipeTypes.CRAFTING).includeHidden().get().forEach(r -> actual.add(r.id().identifier().toString()));
manager.createRecipeLookup(RecipeTypes.SMELTING).includeHidden().get().forEach(r -> actual.add(r.id().identifier().toString()));
manager.createRecipeLookup(RecipeTypes.SMOKING).includeHidden().get().forEach(r -> actual.add(r.id().identifier().toString()));
for (var type : fr.koka.sanctuary.cuisine.KitchenJei.types())
manager.createRecipeLookup(type).includeHidden().get().forEach(r -> {
if (r instanceof net.minecraft.world.item.crafting.RecipeHolder<?> holder) actual.add(holder.id().identifier().toString());
});
int count = 0;
for (var port : Cuisine.CATALOG.getAsJsonArray("ports")) for (var id : port.getAsJsonObject().getAsJsonArray("recipes")) {
check(actual.contains(id.getAsString()), "Every new recipe is indexed by JEI: " + id); count++;
}
var coffeeRecipe = manager.createRecipeLookup(RecipeTypes.CRAFTING).includeHidden().get()
.filter(r -> r.id().identifier().toString().equals("sanctuary:prepare_cauldron_coffee")).findFirst().orElseThrow().value();
var water = coffeeRecipe.placementInfo().ingredients().get(1).display()
.resolveForFirstStack(net.minecraft.world.item.crafting.display.SlotDisplayContext.fromLevel(m.level));
check(water.is(Items.POTION) && water.get(net.minecraft.core.component.DataComponents.POTION_CONTENTS).is(Potions.WATER), "JEI displays plain water, not an unspecified potion");
runtime.getRecipesGui().show(runtime.getJeiHelpers().getFocusFactory().createFocus(
mezz.jei.api.recipe.RecipeIngredientRole.OUTPUT, mezz.jei.api.constants.VanillaTypes.ITEM_STACK, new ItemStack(Cuisine.item("green_salad"))));
System.out.println("CUISINE303_JEI recipes=" + count + " plain_water=true");
}); c.waitTicks(6); c.takeScreenshot("cuisine303-jei-crafting-recipes");
c.runOnClient(m -> {
var runtime = SanctuaryJeiPlugin.runtime();
runtime.getRecipesGui().show(runtime.getJeiHelpers().getFocusFactory().createFocus(
mezz.jei.api.recipe.RecipeIngredientRole.OUTPUT, mezz.jei.api.constants.VanillaTypes.ITEM_STACK, new ItemStack(Cuisine.item("raw_cauldron_coffee"))));
}); c.waitTicks(6); c.takeScreenshot("cuisine303-jei-water-and-coffee");
c.getInput().pressKey(InputConstants.KEY_ESCAPE); c.waitTicks(4);
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().setGameMode(GameType.CREATIVE)); c.waitTicks(6);
c.runOnClient(m -> {
check(Cuisine.items().size() == 129, "All 83 ingredients/meals and 46 uncooked preparations registered");
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(), true, m.level.registryAccess());
var food = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();
for (var item : Cuisine.items()) {
var stack = new ItemStack(item); check(food.getDisplayItems().stream().anyMatch(s -> s.is(item)), "Every new item belongs to native Food & Drinks");
var state = new net.minecraft.client.renderer.item.ItemStackRenderState();
m.getItemModelResolver().updateForTopItem(state, stack, ItemDisplayContext.GUI, m.level, m.player, 0);
check(!state.isEmpty() && state.pickParticleMaterial(RandomSource.create(303)).sprite().contents().name().getNamespace().equals("sanctuary"), "Supplied food texture resolves: " + stack);
}
m.getWindow().setWindowed(1280, 960);
System.out.println("CUISINE303_MODELS items=129 native_food_tab=true");
});
for (String locale : List.of("fr_fr", "en_us")) for (int scale : new int[]{2, 3, 4}) {
c.runOnClient(m -> {
m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
m.options.guiScale().set(scale); m.resizeGui();
for (var item : Cuisine.items()) check(!new ItemStack(item).getHoverName().getString().startsWith("item.sanctuary"), "Translated food: " + locale);
m.gui.setScreen(new CreativeModeInventoryScreen(m.player, m.level.enabledFeatures(), true));
var screen = (CreativeModeInventoryScreen) m.gui.screen();
var tabs = (net.fabricmc.fabric.api.client.creativetab.v1.FabricCreativeModeInventoryScreen) screen;
var food = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();
tabs.switchToPage(tabs.getPage(food)); tabs.setSelectedTab(food);
screen.mouseScrolled(screen.width / 2.0, screen.height / 2.0, 0, -1000);
screen.mouseScrolled(screen.width / 2.0, screen.height / 2.0, 0, 5);
}); c.waitTicks(5); c.takeScreenshot("cuisine303-food-" + locale + "-gui" + scale);
c.runOnClient(m -> m.gui.setScreen(null));
}
server.runOnServer(s -> {
var chest = (ChestBlockEntity) s.overworld().getBlockEntity(CHEST);
chest.setItem(0, new ItemStack(Cuisine.item("ratatouille"), 3));
chest.setItem(1, new ItemStack(Cuisine.item("raw_oven_vanilla_cake"), 2));
chest.setItem(2, new ItemStack(Cuisine.item("olive_oil"), 9));
}); saved = world.getWorldSave();
}
try (var world = saved.open()) {
c.waitFor(m -> m.player != null && m.gui.screen() == null, 1200);
world.getServer().runOnServer(s -> {
var chest = (ChestBlockEntity) s.overworld().getBlockEntity(CHEST);
check(chest.getItem(0).is(Cuisine.item("ratatouille")) && chest.getItem(0).getCount() == 3, "Finished dish saved exactly");
check(chest.getItem(1).is(Cuisine.item("raw_oven_vanilla_cake")) && chest.getItem(1).getCount() == 2, "Uncooked preparation saved exactly");
check(chest.getItem(2).is(Cuisine.item("olive_oil")) && chest.getItem(2).getCount() == 9, "Ingredient saved exactly");
System.out.println("CUISINE303_DISK_RELOAD meals_preparations_ingredients=true");
});
}
System.out.println("CUISINE303_CLIENT_PASS Vulkan, native table packets, real smoker, JEI, 129 item models, native food tab, FR/EN GUI 2/3/4, disk reload");
}
}
@@ -0,0 +1,92 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.*;
import fr.koka.sanctuary.cuisine.culinary.client.screen.KitchenOvenScreen;
import fr.koka.sanctuary.cuisine.culinary.block.CuredSausageBlock;
import fr.koka.sanctuary.cuisine.machine.RedstoneMillBlockEntity;
import fr.koka.sanctuary.cuisine.menu.RedstoneMillMenu;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.*;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Vulkan: native table/pot/oven/mill/rack payments, saved foods, spicy slices and JEI. */
public final class DriedCuisine318ClientChecks implements FabricClientGameTest {
private static final BlockPos OVEN=new BlockPos(0,1,0),TABLE=new BlockPos(2,1,0),POT=new BlockPos(4,1,0),MILL=new BlockPos(-2,1,0),RACK=new BlockPos(-4,1,0),DISPLAY=new BlockPos(0,1,4);
private static void check(boolean ok,String text){if(!ok)throw new AssertionError(text);}
private static void close(ClientGameTestContext c){c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE);c.waitFor(m->m.gui.screen()==null,100);}
private static void select(ClientGameTestContext c,int slot){c.runOnClient(m->{m.player.getInventory().setSelectedSlot(slot);m.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(slot));});c.waitTicks(3);}
private static void use(ClientGameTestContext c,BlockPos pos){c.getInput().lookAt(pos);c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false)));c.waitTicks(5);}
private static void click(ClientGameTestContext c,int slot,int button,ContainerInput action){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,slot,button,action,m.player));c.waitTicks(3);}
private static void moveOne(ClientGameTestContext c,Item item,int target){
int index=c.computeOnClient(m->m.player.containerMenu.slots.stream().filter(s->s.container==m.player.getInventory()&&s.getItem().is(item)).findFirst().orElseThrow().index);
click(c,index,0,ContainerInput.PICKUP);click(c,target,1,ContainerInput.PICKUP);
if(c.computeOnClient(m->!m.player.containerMenu.getCarried().isEmpty()))click(c,index,0,ContainerInput.PICKUP);
}
private static void bite(ClientGameTestContext c,net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext server,int eaten){
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getFoodData().setFoodLevel(0));c.waitFor(m->m.player.getFoodData().getFoodLevel()==0,100);
var angles=new java.util.concurrent.atomic.AtomicReference<float[]>();c.runOnClient(m->{var point=m.level.getBlockState(DISPLAY).getShape(m.level,DISPLAY).bounds().getCenter().add(Vec3.atLowerCornerOf(DISPLAY));var d=point.subtract(m.player.getEyePosition());angles.set(new float[]{(float)(Math.toDegrees(Math.atan2(d.z,d.x))-90),(float)-Math.toDegrees(Math.atan2(d.y,Math.sqrt(d.x*d.x+d.z*d.z)))});});
float health=c.computeOnClient(m->m.player.getHealth());c.getInput().lookAt(angles.get()[0],angles.get()[1]);c.waitTicks(4);c.getInput().pressKey(o->o.keyUse);c.waitTicks(4);c.runOnClient(m->check(m.player.hurtTime>0,"Spicy slice receives its first cosmetic hit"));
c.waitTicks(8);c.runOnClient(m->check(m.player.hurtTime>0&&m.player.getHealth()==health,"A second cosmetic hit arrives with zero health loss"));
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var state=p.level().getBlockState(DISPLAY);check(p.getFoodData().getFoodLevel()==2&&(eaten==4?state.isAir():state.getValue(CuredSausageBlock.BITES)==eaten),"One real spicy slice restores two food points");});
}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
c.runOnClient(m->{var d=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(d.backendName().equalsIgnoreCase("vulkan"),"Vulkan required");System.out.println("DRIED318_BACKEND "+d.backendName()+" / "+d.name());m.options.renderDistance().set(6);m.options.simulationDistance().set(6);m.options.pauseOnLostFocus=false;});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
TestWorldSave save;
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("DriedCuisine318-"+UUID.randomUUID());s.setSeed("318");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);fr.koka.sanctuary.weather.WeatherService.setMode(s,false);}catch(Exception e){throw new IllegalStateException(e);}s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.getWeatherData().setRaining(false);l.setRainLevel(0);
l.setBlockAndUpdate(TABLE,Blocks.CRAFTING_TABLE.defaultBlockState());l.setBlockAndUpdate(OVEN,KitchenAppliances.KITCHEN_OVEN.defaultBlockState());l.setBlockAndUpdate(POT.below(),Blocks.CAMPFIRE.defaultBlockState());l.setBlockAndUpdate(POT,KitchenAppliances.COOKING_CAULDRON.defaultBlockState());l.setBlockAndUpdate(MILL,KitchenAppliances.REDSTONE_MILL.defaultBlockState());l.setBlockAndUpdate(RACK.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(RACK,KitchenAppliances.DRYING_RACK.defaultBlockState());l.setBlockAndUpdate(RACK.above(2),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(DISPLAY.below(),Blocks.STONE.defaultBlockState());
p.setGameMode(GameType.SURVIVAL);p.teleportTo(2.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.BREAD));p.getInventory().setItem(1,new ItemStack(DriedFoods.item("dried_meat")));p.getInventory().setItem(2,new ItemStack(DriedFoods.item("dried_tomato")));p.getInventory().setItem(3,new ItemStack(AgricultureItems.LETTUCE));p.giveExperienceLevels(8);RecipeService.buy(p);for(Item i:DriedCuisine.items())RecipeService.discover(p,i);for(Item i:DriedFoods.items())RecipeService.discover(p,i);RecipeService.refresh(p);
});select(c,8);use(c,TABLE);c.waitFor(m->m.player.containerMenu instanceof CraftingMenu,150);
moveOne(c,Items.BREAD,1);moveOne(c,DriedFoods.item("dried_meat"),2);moveOne(c,DriedFoods.item("dried_tomato"),3);moveOne(c,AgricultureItems.LETTUCE,4);
c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().is(DriedCuisine.item("traveler_sandwich")),150);c.takeScreenshot("dried318-native-sandwich-craft");click(c,0,0,ContainerInput.QUICK_MOVE);close(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("traveler_sandwich"))==1&&p.getInventory().countItem(Items.BREAD)==0&&p.getInventory().countItem(DriedFoods.item("dried_meat"))==0,"Native table pays all sandwich ingredients");p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(AgricultureItems.RICE));p.getInventory().setItem(1,new ItemStack(DriedFoods.item("dried_salmon")));p.getInventory().setItem(2,new ItemStack(DriedFoods.item("dried_coriander")));p.getInventory().setItem(3,new ItemStack(Items.BOWL));p.getInventory().setItem(8,new ItemStack(Items.STICK));p.teleportTo(4.5,1,2.5);((CookingCauldronBlockEntity)p.level().getBlockEntity(POT)).setWaterUnits(1);});
for(int i=0;i<4;i++){select(c,i);use(c,POT);}server.runOnServer(s->{var pot=(CookingCauldronBlockEntity)s.overworld().getBlockEntity(POT);check(pot.progress()==0&&!pot.assemblyComplete(),"Pot waits for the explicit stir");});select(c,8);use(c,POT);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Cuisine.item("raw_dough")));p.getInventory().setItem(1,new ItemStack(DriedFoods.item("raisins")));p.getInventory().setItem(2,new ItemStack(Items.COAL));p.teleportTo(.5,1,2.5);p.openMenu((KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN));});
c.waitFor(m->m.gui.screen() instanceof KitchenOvenScreen&&m.player.containerMenu.getSlot(38).getItem().is(Cuisine.item("raw_dough")),150);moveOne(c,Cuisine.item("raw_dough"),0);moveOne(c,DriedFoods.item("raisins"),1);moveOne(c,Items.COAL,9);
c.runOnClient(m->{CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());var tab=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();for(Item i:DriedCuisine.items()){var render=new net.minecraft.client.renderer.item.ItemStackRenderState();m.getItemModelResolver().updateForTopItem(render,new ItemStack(i),ItemDisplayContext.GUI,m.level,m.player,318);check(!render.isEmpty()&&!render.pickParticleMaterial(net.minecraft.util.RandomSource.create(318)).sprite().contents().name().getPath().equals("missingno"),"Every new icon resolves");check(tab.getDisplayItems().stream().anyMatch(stack->stack.is(i)),"Food & Drinks contains every new result and preparation");}System.out.println("DRIED318_MODELS items=19 missing=0");});
for(String lang:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();for(Item i:DriedCuisine.items())check(!new ItemStack(i).getHoverName().getString().startsWith("item.sanctuary"),"Translated new food name");check(!DriedCuisine.SPICED_SAUSAGE.getName().getString().startsWith("block.sanctuary"),"Translated placed sausage name");});c.waitTicks(3);c.takeScreenshot("dried318-oven-"+lang+"-gui"+scale);}close(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(-1.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(DriedFoods.item("dried_chili_pepper"),4));p.openMenu((RedstoneMillBlockEntity)p.level().getBlockEntity(MILL));});
c.waitFor(m->m.player.containerMenu instanceof RedstoneMillMenu&&m.player.containerMenu.getSlot(31).getItem().is(DriedFoods.item("dried_chili_pepper")),150);click(c,31,0,ContainerInput.QUICK_MOVE);c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(DriedCuisine.item("chili_powder")),150);c.takeScreenshot("dried318-manual-chili-powder");click(c,1,0,ContainerInput.QUICK_MOVE);close(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("chili_powder"))==8&&p.getInventory().countItem(DriedFoods.item("dried_chili_pepper"))==0,"Native mill pays four peppers for eight powder portions");p.getInventory().setItem(1,new ItemStack(KitchenAppliances.RAW_SAUSAGE));p.getInventory().setItem(2,new ItemStack(DriedFoods.item("dried_coriander")));p.teleportTo(2.5,1,2.5);});select(c,7);use(c,TABLE);c.waitFor(m->m.player.containerMenu instanceof CraftingMenu,150);
moveOne(c,KitchenAppliances.RAW_SAUSAGE,1);moveOne(c,DriedCuisine.item("chili_powder"),2);moveOne(c,DriedFoods.item("dried_coriander"),3);c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().is(DriedCuisine.item("raw_spiced_sausage")),150);click(c,0,0,ContainerInput.QUICK_MOVE);close(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("chili_powder"))==7&&p.getInventory().countItem(DriedCuisine.item("raw_spiced_sausage"))==1,"Spicy preparation consumes one of the actual milled powder portions");for(int i=0;i<36;i++)if(p.getInventory().getItem(i).is(DriedCuisine.item("raw_spiced_sausage"))){p.getInventory().setItem(8,p.getInventory().removeItem(i,1));break;}p.teleportTo(-3.5,1,2.5);});select(c,8);use(c,RACK);c.runOnClient(m->check(((DryingRackBlockEntity)m.level.getBlockEntity(RACK)).getItem(0).is(DriedCuisine.item("raw_spiced_sausage")),"Actual raw spicy sausage enters its native hook"));c.takeScreenshot("dried318-raw-spicy-sausage");
c.waitFor(m->((KitchenOvenBlockEntity)m.level.getBlockEntity(OVEN)).getItem(10).is(DriedCuisine.item("raisin_bread"))&&((CookingCauldronBlockEntity)m.level.getBlockEntity(POT)).getItem(6).is(DriedCuisine.item("dried_salmon_rice")),600);save=world.getWorldSave();
}
try(var world=save.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();var rack=(DryingRackBlockEntity)l.getBlockEntity(RACK);check(rack.getItem(0).is(DriedCuisine.item("raw_spiced_sausage"))&&rack.progress(0)>0,"Actual disk save keeps a new spicy preparation and its rack timer");check(((KitchenOvenBlockEntity)l.getBlockEntity(OVEN)).getItem(10).is(DriedCuisine.item("raisin_bread"))&&((CookingCauldronBlockEntity)l.getBlockEntity(POT)).getItem(6).is(DriedCuisine.item("dried_salmon_rice")),"Actual disk save keeps both newly cooked foods");var tag=rack.saveCustomOnly(s.registryAccess());tag.putInt("DryingProgress0",rack.duration(0)-30);rack.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,s.registryAccess(),tag));p.teleportTo(-3.5,1,2.5);});select(c,8);c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(RACK)).readyCount()==1,150);c.getInput().lookAt(RACK);c.takeScreenshot("dried318-finished-spicy-sausage");use(c,RACK);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("spiced_sausage"))==1,"Native collection gives exactly one spicy sausage");for(int i=0;i<36;i++)if(p.getInventory().getItem(i).is(DriedCuisine.item("spiced_sausage"))){p.getInventory().setItem(8,p.getInventory().removeItem(i,1));break;}p.teleportTo(.5,1,6.5);p.getFoodData().setFoodLevel(0);});select(c,8);use(c,DISPLAY.below());c.waitFor(m->m.level.getBlockState(DISPLAY).is(DriedCuisine.SPICED_SAUSAGE),150);c.getInput().lookAt(DISPLAY);c.takeScreenshot("dried318-placed-spicy-sausage");bite(c,server,1);bite(c,server,2);c.takeScreenshot("dried318-two-spicy-slices-left");bite(c,server,3);bite(c,server,4);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(s.overworld().getBlockState(DISPLAY).isAir()&&p.getFoodData().getFoodLevel()==2,"Four native clicks pay four spicy slices, two food each");p.teleportTo(.5,1,2.5);p.openMenu((KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN));});c.waitFor(m->m.gui.screen() instanceof KitchenOvenScreen,150);click(c,10,0,ContainerInput.QUICK_MOVE);close(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("raisin_bread"))==1,"Collect actual oven result after reload");p.teleportTo(4.5,1,2.5);});select(c,8);use(c,POT);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("dried_salmon_rice"))==1&&p.getInventory().countItem(Items.BOWL)==0,"One served pot meal contains its paid bowl");for(int i=0;i<36;i++)if(p.getInventory().getItem(i).is(DriedCuisine.item("dried_salmon_rice"))){p.getInventory().setItem(6,p.getInventory().removeItem(i,1));break;}p.getFoodData().setFoodLevel(0);});select(c,6);c.getInput().holdKey(o->o.keyUse);c.waitFor(m->m.player.getFoodData().getFoodLevel()>0,150);c.getInput().releaseKey(o->o.keyUse);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedCuisine.item("dried_salmon_rice"))==0&&p.getInventory().countItem(Items.BOWL)==1,"Eating returns exactly the paid bowl");});
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,200);
for(String[] pair:new String[][]{{"kitchen_oven","cook_raisin_bread"},{"cooking_cauldron","cook_dried_salmon_rice"},{"redstone_mill","mill_chili_powder"},{"drying_rack","dry_spiced_sausage"}}){c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith(pair[0])).findFirst().orElseThrow();check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().anyMatch(v->v instanceof RecipeHolder<?> holder&&holder.id().identifier().getPath().equals(pair[1])),"New recipe synchronized in JEI: "+pair[1]);runtime.getRecipesGui().showTypes(List.of(type));});c.waitTicks(4);c.takeScreenshot("dried318-jei-"+pair[0]);close(c);}
}
System.out.println("DRIED318_CLIENT_PASS Vulkan, 19 icons FR/EN GUI2/3/4, paid table/oven/pot/mill/rack, actual disk save, four spicy slices with two harmless hits each, one bowl returned, synchronized JEI");
}
}
@@ -0,0 +1,85 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.DryingRackBlockEntity;
import fr.koka.sanctuary.cuisine.culinary.block.DryingRackBlock;
import net.minecraft.world.inventory.*;
import net.minecraft.client.gui.screens.inventory.CraftingScreen;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.sounds.SoundEventListener;
import net.minecraft.core.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native crafting packets, four physical hooks, bucket weather pause, cuboid models and disk. */
public final class Drying313ClientChecks implements FabricClientGameTest {
private static final BlockPos POT=new BlockPos(0,1,0),TABLE=new BlockPos(-3,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void use(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(POT),Direction.UP,POT,false)));c.waitTicks(6);}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);var audio=new CopyOnWriteArrayList<String>();
SoundEventListener listener=(sound,event,distance)->{if(sound.getX()>=0&&sound.getX()<=1&&sound.getZ()>=0&&sound.getZ()<=1)audio.add(sound.getIdentifier().toString());};
c.runOnClient(m->{var device=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(device.backendName().equalsIgnoreCase("vulkan"),"Cauldron validation requires Vulkan");System.out.println("DRYING313_BACKEND "+device.backendName()+" / "+device.name());m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;m.getSoundManager().addListener(listener);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
TestWorldSave saved;var savedProgress=new java.util.concurrent.atomic.AtomicInteger();
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Drying313-"+UUID.randomUUID());s.setSeed("313");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);fr.koka.sanctuary.weather.WeatherService.setMode(s,false);}catch(Exception e){throw new IllegalStateException(e);}s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.getWeatherData().setRaining(false);l.setRainLevel(0);
l.setBlockAndUpdate(TABLE,Blocks.CRAFTING_TABLE.defaultBlockState());l.setBlockAndUpdate(new BlockPos(-3,0,2),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(new BlockPos(0,0,2),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(POT.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(POT.above(),Blocks.STONE.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(-2.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.PORKCHOP,4));p.getInventory().setItem(1,new ItemStack(Cuisine.item("salt"),2));p.getInventory().setItem(2,new ItemStack(AgricultureItems.GARLIC,2));p.getInventory().setItem(3,new ItemStack(Items.STRING,2));p.giveExperienceLevels(4);RecipeService.discover(p,Items.PORKCHOP);RecipeService.buy(p);RecipeService.refresh(p);p.openMenu(l.getBlockState(TABLE).getMenuProvider(l,TABLE));
});c.waitFor(m->m.gui.screen() instanceof CraftingScreen,200);
click(c,37,0,ContainerInput.PICKUP);click(c,1,1,ContainerInput.PICKUP);click(c,1,1,ContainerInput.PICKUP);click(c,2,1,ContainerInput.PICKUP);click(c,2,1,ContainerInput.PICKUP);
for(int i=0;i<3;i++){click(c,38+i,0,ContainerInput.PICKUP);click(c,3+i,0,ContainerInput.PICKUP);}
c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().is(KitchenAppliances.RAW_SAUSAGE),200);c.takeScreenshot("drying313-preparation-at-native-table");click(c,0,0,ContainerInput.QUICK_MOVE);c.waitTicks(6);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==4&&p.getInventory().countItem(Items.PORKCHOP)==0&&p.getInventory().countItem(Items.STRING)==0&&p.getInventory().countItem(Cuisine.item("salt"))==0&&p.getInventory().countItem(AgricultureItems.GARLIC)==0,"Native shift crafting pays four pork, two salt/garlic/thread for four preparations");});c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(5);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(int i=0;i<36;i++)if(p.getInventory().getItem(i).is(KitchenAppliances.RAW_SAUSAGE)){var raw=p.getInventory().removeItemNoUpdate(i);p.getInventory().setItem(0,raw);break;}p.getInventory().setItem(8,new ItemStack(KitchenAppliances.DRYING_RACK));p.teleportTo(.5,1,2.5);});c.waitTicks(5);select(c,8);c.getInput().lookAt(POT);c.waitTicks(3);
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,1,.5),Direction.UP,POT.below(),false)));c.waitFor(m->m.level.getBlockEntity(POT) instanceof DryingRackBlockEntity,200);select(c,0);
for(int i=0;i<4;i++)use(c);c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(POT)).dryingCount()==4,200);c.takeScreenshot("drying313-four-hanging-preparations");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==0&&rack.progress(0)>rack.progress(3),"Four real native insertions pay four preparations and keep separate ages");});use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(rack.getItem(3).isEmpty()&&rack.progress(0)>0&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==1,"Taking one unfinished preparation leaves the others drying");});use(c);c.waitTicks(5);
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);int models=0;for(var state:KitchenAppliances.DRYING_RACK.getStateDefinition().getPossibleStates()){
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(313),parts);int quads=0;float maxY=0;
for(var part:parts)for(var direction:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})for(var q:part.getQuads(direction)){check(q.materialInfo().sprite().contents().name().getNamespace().equals("minecraft"),"Rack resolves native wood textures");for(int v=0;v<4;v++)maxY=Math.max(maxY,q.position(v).y());quads++;}
check(quads==30&&Math.abs(maxY-1)<.0001,"Five frame cuboids resolve at full block height");models++;
}check(models==8,"Four orientations and both native water states bake correctly");
net.minecraft.world.item.CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());var food=net.minecraft.core.registries.BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(net.minecraft.world.item.CreativeModeTabs.FOOD_AND_DRINKS).value();for(var item:List.of(KitchenAppliances.RAW_SAUSAGE,KitchenAppliances.CURED_SAUSAGE,KitchenAppliances.DRYING_RACK.asItem())){var render=new net.minecraft.client.renderer.item.ItemStackRenderState();m.getItemModelResolver().updateForTopItem(render,new ItemStack(item),ItemDisplayContext.GUI,m.level,m.player,313);check(!render.isEmpty()&&!render.pickParticleMaterial(net.minecraft.util.RandomSource.create(313)).sprite().contents().name().getPath().equals("missingno")&&food.getDisplayItems().stream().anyMatch(s->s.is(item)),"Native cuboid item model and existing food category: "+item);}
System.out.println("DRYING313_MODELS states="+models+" items=3 native_textures=true");
});c.getInput().lookAt(POT);c.waitTicks(4);
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();check(m.hitResult instanceof BlockHitResult h&&h.getBlockPos().equals(POT),"Physical rack HUD is targeted");});c.waitTicks(3);c.takeScreenshot("drying313-hud-"+locale+"-gui"+scale);}
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getInventory().setItem(8,new ItemStack(Items.WATER_BUCKET)));select(c,8);c.getInput().pressKey(o->o.keyUse);c.waitFor(m->m.level.getBlockState(POT).getValue(DryingRackBlock.WATERLOGGED),150);c.waitTicks(4);c.takeScreenshot("drying313-native-water-pause");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(rack.paused()&&rack.readyCount()==0&&p.getMainHandItem().is(Items.BUCKET),"Actual bucket interaction waterlogs the rack and preserves all raw food");savedProgress.set(rack.progress(0));});saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(rack.dryingCount()==4&&rack.progress(0)==savedProgress.get()&&rack.getBlockState().getValue(DryingRackBlock.WATERLOGGED),"Actual disk reopen preserves all four items and paused progress exactly");p.teleportTo(.5,1,2.5);});c.waitTicks(8);c.getInput().lookAt(POT);select(c,8);c.getInput().pressKey(o->o.keyUse);c.waitFor(m->!m.level.getBlockState(POT).getValue(DryingRackBlock.WATERLOGGED),150);c.waitTicks(8);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(rack.progress(0)>savedProgress.get()&&p.getMainHandItem().is(Items.WATER_BUCKET),"Native bucket drain resumes loaded drying without replacing any preparation");var tag=rack.saveCustomOnly(p.level().registryAccess());for(int i=0;i<4;i++)tag.putInt("DryingProgress"+i,6000-40-i*20);rack.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,p.level().registryAccess(),tag));p.level().sendBlockUpdated(POT,rack.getBlockState(),rack.getBlockState(),net.minecraft.world.level.block.Block.UPDATE_CLIENTS);});select(c,0);
c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(POT)).readyCount()==4,220);c.takeScreenshot("drying313-four-finished-saucissons");for(int i=0;i<4;i++)use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(rack.isEmpty()&&p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==4&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==0,"Four real collection interactions give exactly four cured products");p.level().setBlockAndUpdate(new BlockPos(2,2,0),Blocks.STONE.defaultBlockState());p.getFoodData().setFoodLevel(1);});c.waitFor(m->m.player.getFoodData().getFoodLevel()==1,100);
// Since beta.315, share the drying result after native placement, like cheese.
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(2.5,3,.5),Direction.UP,new BlockPos(2,2,0),false)));c.waitFor(m->m.level.getBlockState(new BlockPos(2,3,0)).is(KitchenAppliances.CURED_SAUSAGE_BLOCK),100);
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(2.5,3.125,.5),Direction.UP,new BlockPos(2,3,0),false)));c.waitTicks(6);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==3&&p.getFoodData().getFoodLevel()>1&&p.level().getBlockState(new BlockPos(2,3,0)).getValue(fr.koka.sanctuary.cuisine.culinary.block.CuredSausageBlock.BITES)==1,"The actual drying result is placed and one slice restores hunger without a whole-item refund");});
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,200);c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("drying_rack")).findFirst().orElseThrow();check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==16,"All drying recipes are synchronized in JEI");runtime.getRecipesGui().showTypes(List.of(type));});c.waitTicks(5);c.takeScreenshot("drying313-jei-five-minute-recipe");c.getInput().pressKey(InputConstants.KEY_ESCAPE);
}
c.runOnClient(m->m.getSoundManager().removeListener(listener));System.out.println("DRYING313_CLIENT_PASS Vulkan, native table shift craft/payout, paid rack placement, four native independent hooks, raw cancellation, 8 baked states/3 native-textured item models, existing food category, FR/EN GUI2/3/4, real bucket pause/drain, disk progress, timed finish/collection, native placed-slice eating and JEI");
}
private static void click(ClientGameTestContext c,int slot,int button,ContainerInput input){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,slot,button,input,m.player));c.waitTicks(3);}
private static void select(ClientGameTestContext c,int slot){c.runOnClient(m->{m.player.getInventory().setSelectedSlot(slot);m.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(slot));});c.waitTicks(4);}
}
@@ -0,0 +1,63 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.DryingRackBlock;
import fr.koka.sanctuary.cuisine.culinary.block.entity.DryingRackBlockEntity;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.*;
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.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.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
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.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** New dried foods with real native placement, four mixed hooks, bucket pause, disk and consumption. */
public final class Drying317ClientChecks implements FabricClientGameTest {
private static final BlockPos POT=new BlockPos(0,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void select(ClientGameTestContext c,int slot){c.runOnClient(m->{m.player.getInventory().setSelectedSlot(slot);m.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(slot));});c.waitTicks(3);}
private static void use(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(POT),Direction.UP,POT,false)));c.waitTicks(5);}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);var progress=new AtomicInteger();
c.runOnClient(m->{var d=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(d.backendName().equalsIgnoreCase("vulkan"),"Vulkan required");System.out.println("DRYING317_BACKEND "+d.backendName()+" / "+d.name());m.options.renderDistance().set(6);m.options.simulationDistance().set(6);m.options.pauseOnLostFocus=false;});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
TestWorldSave save;
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Drying317-"+UUID.randomUUID());s.setSeed("317");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);fr.koka.sanctuary.weather.WeatherService.setMode(s,false);}catch(Exception e){throw new IllegalStateException(e);}s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.getWeatherData().setRaining(false);l.setRainLevel(0);l.setBlockAndUpdate(POT.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(new BlockPos(0,0,2),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(POT.above(),Blocks.STONE.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(8,new ItemStack(KitchenAppliances.DRYING_RACK));p.getInventory().setItem(0,new ItemStack(Items.BEEF,2));p.getInventory().setItem(1,new ItemStack(Items.COD));p.getInventory().setItem(2,new ItemStack(AgricultureItems.BASIL));p.getInventory().setItem(3,new ItemStack(AgricultureItems.GRAPES));p.giveExperienceLevels(4);RecipeService.buy(p);for(var row:DriedFoods.CATALOG.getAsJsonArray("recipes"))RecipeService.discover(p,BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.parse(row.getAsJsonObject().get("input").getAsString())));RecipeService.refresh(p);});
select(c,8);c.getInput().lookAt(POT);c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,1,.5),Direction.UP,POT.below(),false)));c.waitFor(m->m.level.getBlockEntity(POT) instanceof DryingRackBlockEntity,200);
for(int i=0;i<4;i++){select(c,i);use(c);}server.waitFor(s->((DryingRackBlockEntity)s.overworld().getBlockEntity(POT)).dryingCount()==4,150);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Items.BEEF)==1&&p.getInventory().countItem(Items.COD)==0&&p.getInventory().countItem(AgricultureItems.BASIL)==0&&p.getInventory().countItem(AgricultureItems.GRAPES)==0&&p.getInventory().countItem(KitchenAppliances.DRYING_RACK.asItem())==0,"Real rack placement and four insertions each consume one ingredient");});
c.getInput().lookAt(POT);c.waitTicks(6);c.takeScreenshot("drying317-four-different-foods");
c.runOnClient(m->{CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());var tab=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();for(Item item:DriedFoods.items()){var render=new net.minecraft.client.renderer.item.ItemStackRenderState();m.getItemModelResolver().updateForTopItem(render,new ItemStack(item),ItemDisplayContext.GUI,m.level,m.player,317);check(!render.isEmpty()&&!render.pickParticleMaterial(net.minecraft.util.RandomSource.create(317)).sprite().contents().name().getPath().equals("missingno"),"Dried food icon resolves its source texture and tint");check(tab.getDisplayItems().stream().anyMatch(stack->stack.is(item)),"Existing Food & Drinks tab contains "+item);}System.out.println("DRYING317_MODELS items="+DriedFoods.items().size()+" missing=0");});
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();for(Item i:DriedFoods.items())check(!new ItemStack(i).getHoverName().getString().startsWith("item.sanctuary"),"Translated dried food name");});c.getInput().lookAt(POT);c.waitTicks(3);c.takeScreenshot("drying317-hud-"+locale+"-gui"+scale);}
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getInventory().setItem(8,new ItemStack(Items.WATER_BUCKET)));select(c,8);c.getInput().pressKey(o->o.keyUse);c.waitFor(m->m.level.getBlockState(POT).getValue(DryingRackBlock.WATERLOGGED),150);c.waitTicks(4);
server.runOnServer(s->{var r=(DryingRackBlockEntity)s.overworld().getBlockEntity(POT);check(r.paused()&&r.readyCount()==0,"Native bucket pauses every mixed hook");progress.set(r.progress(0));});save=world.getWorldSave();
}
try(var world=save.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var r=(DryingRackBlockEntity)s.overworld().getBlockEntity(POT);check(r.progress(0)==progress.get()&&r.dryingCount()==4&&r.getBlockState().getValue(DryingRackBlock.WATERLOGGED),"Four different foods and paused work persist on actual disk reload");s.getPlayerList().getPlayers().getFirst().teleportTo(.5,1,2.5);});
c.getInput().lookAt(POT);select(c,8);c.getInput().pressKey(o->o.keyUse);c.waitFor(m->!m.level.getBlockState(POT).getValue(DryingRackBlock.WATERLOGGED),150);c.waitTicks(3);
server.runOnServer(s->{var r=(DryingRackBlockEntity)s.overworld().getBlockEntity(POT);var tag=r.saveCustomOnly(s.registryAccess());for(int i=0;i<4;i++)tag.putInt("DryingProgress"+i,r.duration(i)-30-i*5);r.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,s.registryAccess(),tag));s.overworld().sendBlockUpdated(POT,r.getBlockState(),r.getBlockState(),Block.UPDATE_CLIENTS);});
select(c,7);c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(POT)).readyCount()==4,200);c.getInput().lookAt(POT);c.takeScreenshot("drying317-four-dried-results");for(int i=0;i<4;i++)use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(String id:List.of("dried_meat","dried_cod","dried_basil","raisins"))check(p.getInventory().countItem(DriedFoods.item(id))==1,"Actual native collection returns one "+id);check(((DryingRackBlockEntity)s.overworld().getBlockEntity(POT)).isEmpty(),"No residual copied output");for(int i=0;i<36;i++)if(p.getInventory().getItem(i).is(DriedFoods.item("dried_meat"))){p.getInventory().setItem(6,p.getInventory().removeItemNoUpdate(i));break;}p.getFoodData().setFoodLevel(0);});
select(c,6);c.getInput().holdKey(o->o.keyUse);c.waitFor(m->m.player.getFoodData().getFoodLevel()>0,150);c.getInput().releaseKey(o->o.keyUse);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(DriedFoods.item("dried_meat"))==0&&p.getFoodData().getFoodLevel()>0,"Native eating consumes the real dried meat");});
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,200);c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("drying_rack")).findFirst().orElseThrow();check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==16,"All sixteen rack recipes synchronize to JEI");runtime.getRecipesGui().showTypes(List.of(type));});c.waitTicks(5);c.takeScreenshot("drying317-jei-drying-recipes");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE);
}
System.out.println("DRYING317_CLIENT_PASS Vulkan, paid mixed inputs, 12 models FR/EN GUI2/3/4, bucket pause/drain, saved timers, native finished collection/eating, 16 JEI recipes");
}
}
@@ -0,0 +1,123 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.agriculture.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
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.core.*;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** A fresh real Sanctuary island, native generated wild crops, paid survival harvest and all wild models. */
public final class Foraging316ClientChecks implements FabricClientGameTest {
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable();
var intro=new AtomicBoolean(true);
var target=new AtomicReference<BlockPos>();
var snapshot=new AtomicReference<Map<BlockPos,BlockState>>();
var expectedLoot=new AtomicReference<Set<net.minecraft.world.item.Item>>();
c.runOnClient(m->{
var info=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(info.backendName().equalsIgnoreCase("vulkan"),"Vulkan required");
System.out.println("FORAGING316_BACKEND "+info.backendName()+" / "+info.name());
m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;
});
ClientTickEvents.END_CLIENT_TICK.register(m->{
if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
IntroClient.setQuickTestIntro(true);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());
press(screen,"sanctuary.progression.enter");intro.set(false);
}
});
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave save;
try(var world=c.worldBuilder().adjustSettings(s->{
s.setName("Foraging316-"+UUID.randomUUID());s.setSeed("316");s.setAllowCommands(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()) {
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),2400);
var server=world.getServer();
server.runOnServer(s->{
var player=s.getPlayerList().getPlayers().getFirst();var level=player.level();
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
level.getGameRules().set(GameRules.SPAWN_MOBS,false,s);level.getGameRules().set(GameRules.RANDOM_TICK_SPEED,0,s);
var found=new LinkedHashMap<BlockPos,BlockState>();
var types=new HashSet<Block>();
var habitats=new java.util.TreeMap<String,Integer>();
int cx=player.blockPosition().getX()>>4,cz=player.blockPosition().getZ()>>4;
for(int dx=-2;dx<=2;dx++)for(int dz=-2;dz<=2;dz++){
var chunk=level.getChunk(cx+dx,cz+dz);
for(int x=0;x<16;x++)for(int z=0;z<16;z++)for(int y=33;y<384;y++){
var p=chunk.getPos().getBlockAt(x,y,z);var state=chunk.getBlockState(p);
if(Foraging.wildPlants().containsValue(state.getBlock())){found.put(p,state);types.add(state.getBlock());}
if(WildFoodGeneration.vacant(state)&&WildFoodGeneration.soil(chunk.getBlockState(p.below())))
habitats.merge(level.getBiome(p).unwrapKey().orElseThrow().identifier()+"/"+Foraging.habitat(level.getBiome(p)),1,Integer::sum);
}
}
System.out.println("FORAGING316_HABITATS "+habitats);
check(found.size()>=8&&types.size()>=3,"Real Sanctuary decoration produces several visible wild crops: "+found.size()+" / "+types.size());
snapshot.set(found);target.set(found.keySet().stream().max(Comparator.comparingInt(BlockPos::getY)).orElseThrow());
var p=target.get();
expectedLoot.set(Block.getDrops(level.getBlockState(p),level,p,null).stream().map(net.minecraft.world.item.ItemStack::getItem).collect(java.util.stream.Collectors.toSet()));
level.setBlockAndUpdate(p.south(2).below(),Blocks.STONE.defaultBlockState());player.teleportTo(p.getX()+.5,p.getY()+1,p.getZ()+2.5);player.setYRot(180);player.setXRot(35);
player.getInventory().clearContent();player.setGameMode(GameType.SURVIVAL);player.inventoryMenu.broadcastChanges();
System.out.println("FORAGING316_NATIVE crops="+found.size()+" types="+types.size()+" target="+p);
});
c.waitTicks(15);c.takeScreenshot("foraging316-native-temperate-food");
c.runOnClient(m->m.gameMode.startDestroyBlock(target.get(),Direction.UP));
server.waitFor(s->s.overworld().getBlockState(target.get()).isAir(),200);
server.runOnServer(s->{
var level=s.overworld();var drops=level.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,
new net.minecraft.world.phys.AABB(target.get()).inflate(2));
var player=s.getPlayerList().getPlayers().getFirst();
check(drops.stream().anyMatch(e->expectedLoot.get().contains(e.getItem().getItem()))
||expectedLoot.get().stream().anyMatch(item->player.getInventory().countItem(item)>0),
"Actual survival packet awards the expected native loot from a generated wild plant");
snapshot.get().remove(target.get());
var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(GameType.CREATIVE);
p.getAbilities().flying=true;p.onUpdateAbilities();
});
c.runOnClient(m->{
for(var block:Foraging.wildPlants().values()){
var state=block.defaultBlockState();var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();
m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(316),parts);
int quads=0;
for(var part:parts)for(Direction side:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})
for(var q:part.getQuads(side)){
check(!q.materialInfo().sprite().contents().name().getPath().contains("missing"),"Resolved wild crop texture");quads++;
}
check(quads==4,"Every wild plant has two visible crossed planes: "+state+" "+quads);
}
for(String lang:List.of("fr_fr","en_us")){
m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
for(var block:Foraging.wildPlants().values())check(!block.getName().getString().startsWith("block.sanctuary"),"Wild names translated: "+lang);
}
System.out.println("FORAGING316_MODELS wild=24 FR_EN=pass");
});
c.takeScreenshot("foraging316-harvested-wild-plant");
save=world.getWorldSave();
}
try(var world=save.open()){
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
world.getServer().runOnServer(s->{
check(s.overworld().getBlockState(target.get()).isAir(),"Harvested existing chunk is never repopulated on load");
snapshot.get().forEach((p,state)->check(s.overworld().getBlockState(p).equals(state),"Existing wild plant survives actual disk reload"));
System.out.println("FORAGING316_RELOAD preserved="+snapshot.get().size()+" harvested_stays_empty=true");
});
c.takeScreenshot("foraging316-reloaded-no-repopulation");
}
}
}
@@ -0,0 +1,170 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.*;
import fr.koka.sanctuary.cuisine.culinary.block.*;
import fr.koka.sanctuary.cuisine.culinary.client.screen.*;
import fr.koka.sanctuary.cuisine.culinary.recipe.*;
import fr.koka.sanctuary.cuisine.machine.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.core.Direction;
import net.minecraft.world.phys.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
import java.util.concurrent.atomic.AtomicBoolean;
import mezz.jei.api.constants.RecipeTypes;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.util.RandomSource;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.item.alchemy.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native table packets, real smoker ticks, food catalogue/JEI and actual save reopening. */
public final class Kitchen304ClientChecks implements FabricClientGameTest {
private static final BlockPos PAN=new BlockPos(-4,1,0), POT=new BlockPos(-2,1,0), OVEN=new BlockPos(0,1,0), BARREL=new BlockPos(2,1,0), MILL=new BlockPos(4,1,0), PRESS=new BlockPos(6,1,0);
private static long barrelStart;
private static int barrelDuration;
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Cuisine validation requires Vulkan");
System.out.println("KITCHEN304_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Kitchen304-" + UUID.randomUUID()); s.setSeed("304"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server=world.getServer();
server.runOnServer(s->{
var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();
l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);
((net.minecraft.world.level.storage.ServerLevelData)l.getLevelData()).setGameTime(l.getGameTime()+2000);
l.setBlockAndUpdate(PAN,KitchenAppliances.FRYING_PAN.defaultBlockState());
l.setBlockAndUpdate(POT,KitchenAppliances.COOKING_CAULDRON.defaultBlockState());
l.setBlockAndUpdate(OVEN,KitchenAppliances.KITCHEN_OVEN.defaultBlockState());
l.setBlockAndUpdate(BARREL,KitchenAppliances.FERMENTATION_BARREL.defaultBlockState());
l.setBlockAndUpdate(MILL,KitchenAppliances.REDSTONE_MILL.defaultBlockState());
l.setBlockAndUpdate(PRESS.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(PRESS,KitchenAppliances.TOMATO_BLOCK.defaultBlockState());
l.setBlockAndUpdate(PAN.below(),Blocks.CAMPFIRE.defaultBlockState());
l.setBlockAndUpdate(POT.below(),Blocks.CAMPFIRE.defaultBlockState());
p.teleportTo(-3.5,1,2.5);p.setYRot(180);p.setXRot(25);p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.EGG,2));
});c.waitTicks(8);
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(PAN),Direction.UP,PAN,false)));c.waitTicks(6);
server.runOnServer(s->{
var p=s.getPlayerList().getPlayers().getFirst();var pan=(FryingPanBlockEntity)p.level().getBlockEntity(PAN);
check(pan.getItem(0).is(Items.EGG) && pan.getItem(0).getCount()==1 && p.getMainHandItem().getCount()==1,"Native interaction packet inserts one paid egg");
var tag=pan.saveCustomOnly(p.level().registryAccess());tag.putLong("AssemblyStartedAt",p.level().getGameTime()-601);tag.putLong("LastIngredientChangedAt",p.level().getGameTime()-601);
pan.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,p.level().registryAccess(),tag));
var pot=(CookingCauldronBlockEntity)p.level().getBlockEntity(POT);pot.setWaterUnits(3);pot.insertIngredient(new ItemStack(Cuisine.item("ground_coffee")),1);pot.stir();
var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);oven.setItem(0,new ItemStack(Cuisine.item("raw_dough")));oven.setItem(9,new ItemStack(Items.COAL));
var mill=(RedstoneMillBlockEntity)p.level().getBlockEntity(MILL);mill.setItem(2,new ItemStack(Cuisine.item("wheat_flour"),2));
var b=(FermentationBarrelBlockEntity)p.level().getBlockEntity(BARREL);b.insertIngredient(new ItemStack(Cuisine.item("grape_must")),1);
p.teleportTo(.5,2,5.5);p.setYRot(180);p.setXRot(22);
});c.getInput().lookAt(180,22);c.waitTicks(300);
server.runOnServer(s->{var l=s.overworld();
check(((FryingPanBlockEntity)l.getBlockEntity(PAN)).getItem(4).is(Cuisine.item("fried_egg")),"Real pan ticks cook egg");
check(((KitchenOvenBlockEntity)l.getBlockEntity(OVEN)).getItem(10).is(Items.BREAD),"Real oven ticks cook dough");
check(((RedstoneMillBlockEntity)l.getBlockEntity(MILL)).getItem(2).is(Cuisine.item("wheat_flour")),"Old stored mill output remains recoverable");
});c.takeScreenshot("kitchen304-six-appliances-in-world");
c.runOnClient(m->{
var blocks=List.of(KitchenAppliances.TOMATO_BLOCK,KitchenAppliances.GRAPE_BLOCK,KitchenAppliances.OLIVE_BLOCK,KitchenAppliances.FRYING_PAN,KitchenAppliances.COOKING_CAULDRON,KitchenAppliances.KITCHEN_OVEN,KitchenAppliances.FERMENTATION_BARREL,KitchenAppliances.REDSTONE_MILL,KitchenAppliances.FOOD_PRESS);
int states=0;
for(var block:blocks) for(var state:block.getStateDefinition().getPossibleStates()){
var parts=new ArrayList<BlockStateModelPart>();
m.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(304),parts);
int quads=0;
for(var part:parts)for(var direction:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})for(var quad:part.getQuads(direction)){
check(!quad.materialInfo().sprite().contents().name().getPath().equals("missingno"),"Kitchen block texture resolves: "+state);quads++;
}
check(quads>0,"Kitchen block has visible geometry: "+state);states++;
}
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());
var food=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();
for(var item:KitchenAppliances.items()){
check(food.getDisplayItems().stream().anyMatch(i->i.is(item)),"Appliance in existing food tab");
var render=new net.minecraft.client.renderer.item.ItemStackRenderState();
m.getItemModelResolver().updateForTopItem(render,new ItemStack(item),ItemDisplayContext.GUI,m.level,m.player,0);check(!render.isEmpty() && !render.pickParticleMaterial(RandomSource.create(304)).sprite().contents().name().getPath().equals("missingno"),"Kitchen item renders "+item);
}
check(KitchenAppliances.items().size()==15,"Fifteen item entries, no new tab");
check(m.level.getBlockEntity(PAN) instanceof FryingPanBlockEntity pan && pan.visibleItems().getFirst().is(Cuisine.item("fried_egg")),"Pan content synchronized");
check(m.level.getBlockEntity(BARREL) instanceof FermentationBarrelBlockEntity b && b.visibleItem().is(Cuisine.item("grape_must")) && b.remainingFermentationTicks()>0,"Fermentation HUD and lid synchronized");
check(m.level.getBlockState(POT).getValue(CookingCauldronBlock.MIXTURE)==3,"Coffee mixture tint synchronized");
System.out.println("KITCHEN304_MODELS states="+states+" items=15 renderers_and_mixture_synced=true");
m.getWindow().setWindowed(1280,960);
});
for(String locale:List.of("fr_fr","en_us")) for(int scale:new int[]{2,3,4}){
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
for(var item:KitchenAppliances.items())check(!new ItemStack(item).getHoverName().getString().matches("(item|block)\\.sanctuary.*"),"FR/EN kitchen label: "+item);});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(.5,1,2.5);p.openMenu((KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN));});
c.waitFor(m->m.gui.screen() instanceof KitchenOvenScreen,200);c.waitTicks(4);c.takeScreenshot("kitchen304-oven-"+locale+"-gui"+scale);
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(4.5,1,2.5);p.openMenu((RedstoneMillBlockEntity)p.level().getBlockEntity(MILL));});
c.waitFor(m->m.gui.screen() instanceof RedstoneMillScreen,200);c.waitTicks(4);c.takeScreenshot("kitchen304-mill-"+locale+"-gui"+scale);
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(4);RecipeService.discover(p,AgricultureItems.TOMATO);RecipeService.buy(p);RecipeService.refresh(p);});
c.waitFor(m->RecipeClient.allowed() && SanctuaryJeiPlugin.runtime()!=null && RecipeClient.knowledge.recipes.contains("sanctuary:physical_pan_fried_egg"),300);c.waitTicks(10);
c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();int count=0;
check(KitchenJei.types().size()==8,"Eight actual JEI categories");
for(var type:KitchenJei.types()){
var values=runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().toList();check(!values.isEmpty(),"Physical category synchronized "+type);count+=values.size();
}check(count==109,"All 109 current appliance recipes indexed in JEI: "+count);System.out.println("KITCHEN304_JEI categories=8 recipes="+count);
});
for(var type: c.computeOnClient(m->KitchenJei.types())){
c.runOnClient(m->SanctuaryJeiPlugin.runtime().getRecipesGui().showTypes(List.of(type)));c.waitTicks(5);
c.takeScreenshot("kitchen304-jei-"+type.getUid().getPath().replace('/','-'));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
}
server.runOnServer(s->{var b=(FermentationBarrelBlockEntity)s.overworld().getBlockEntity(BARREL);barrelStart=b.startedAtGameTime();barrelDuration=b.activeDuration();check(barrelStart>=0,"Wine still fermenting before real save");});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=s.overworld();var b=(FermentationBarrelBlockEntity)l.getBlockEntity(BARREL);
check(b.startedAtGameTime()==barrelStart && b.activeDuration()==barrelDuration && b.getItem(0).is(Cuisine.item("grape_must")),"Disk reload keeps fermentation deadline and ingredient");
check(((FryingPanBlockEntity)l.getBlockEntity(PAN)).getItem(4).is(Cuisine.item("fried_egg")),"Pan meal persists");
check(((KitchenOvenBlockEntity)l.getBlockEntity(OVEN)).getItem(10).is(Items.BREAD),"Oven output persists");
check(((RedstoneMillBlockEntity)l.getBlockEntity(MILL)).getItem(2).is(Cuisine.item("wheat_flour")),"Mill output persists");
check(l.getBlockState(PRESS).is(KitchenAppliances.TOMATO_BLOCK),"Fruit block persists");
((net.minecraft.world.level.storage.ServerLevelData)l.getLevelData()).setGameTime(barrelStart+barrelDuration);
});c.waitTicks(5);
world.getServer().runOnServer(s->{var b=(FermentationBarrelBlockEntity)s.overworld().getBlockEntity(BARREL);check(b.getItem(4).is(Cuisine.item("wine")) && b.getItem(4).getCount()==1 && b.getItem(0).isEmpty(),"Fermentation finishes exactly once after reload and elapsed world time");System.out.println("KITCHEN304_DISK_RELOAD five_devices_and_fruit=true fermentation_completion=true");});
}
System.out.println("KITCHEN304_CLIENT_PASS Vulkan, native pan packet, real cooking, six appliances, JEI 109 recipes, FR/EN GUI 2/3/4, disk reload");
}
}
@@ -0,0 +1,103 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.menu.RedstoneMillMenu;
import fr.koka.sanctuary.cuisine.culinary.client.screen.*;
import fr.koka.sanctuary.cuisine.machine.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native manual milling packets, localized menus, JEI and actual save reopening. */
public final class Mill306ClientChecks implements FabricClientGameTest {
private static final BlockPos MILL=new BlockPos(0,1,0);
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Mill validation requires Vulkan");
System.out.println("MILL306_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Mill306-" + UUID.randomUUID()); s.setSeed("306"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);
l.setBlockAndUpdate(MILL,KitchenAppliances.REDSTONE_MILL.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);
p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.WHEAT,4));p.openMenu((RedstoneMillBlockEntity)l.getBlockEntity(MILL));
});c.waitFor(m->m.gui.screen() instanceof RedstoneMillScreen && m.player.containerMenu.getSlot(31).getItem().is(Items.WHEAT),200);
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,31,0,ContainerInput.QUICK_MOVE,m.player));
c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().getCount()==4 && m.player.containerMenu.getSlot(1).getItem().is(Cuisine.item("wheat_flour")),200);c.waitTicks(25);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.containerMenu.getSlot(0).getItem().getCount()==4 && ((RedstoneMillBlockEntity)p.level().getBlockEntity(MILL)).isEmpty(),"Native insertion previews without fuel, time or consumption");});
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,1,ContainerInput.PICKUP,m.player));c.waitTicks(2);c.takeScreenshot("mill306-mechanical-stroke-down");
c.waitTicks(4);c.takeScreenshot("mill306-mechanical-stroke-return");c.waitTicks(8);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.containerMenu.getCarried().is(Cuisine.item("wheat_flour"))&&p.containerMenu.getCarried().getCount()==2&&p.containerMenu.getSlot(0).getItem().getCount()==3,"Native right-click pays one grain, gives the full batch and keeps three");});
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,31,0,ContainerInput.PICKUP,m.player));c.waitTicks(4);
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(8);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Cuisine.item("wheat_flour"))==8&&p.containerMenu.getSlot(0).getItem().isEmpty()&&p.containerMenu.getSlot(1).getItem().isEmpty(),"Native shift crafts the three remaining paid batches");p.containerMenu.getSlot(0).set(new ItemStack(Items.WHEAT,2));p.containerMenu.broadcastChanges();});
c.runOnClient(m->m.getWindow().setWindowed(1280,960));
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
String expected=locale.equals("fr_fr")?"Moulin":"Mill";check(m.gui.screen().getTitle().getString().equals(expected),"Localized menu name");
check(new ItemStack(KitchenAppliances.REDSTONE_MILL).getHoverName().getString().equals(expected),"Localized item name");
check(Component.translatable("block.sanctuary.redstone_mill").getString().equals(expected),"Localized block name");
var menu=(RedstoneMillMenu)m.player.containerMenu;
check(menu.getSlot(0).isActive()&&menu.getSlot(1).isActive()&&!menu.getSlot(2).isActive()&&!menu.getSlot(3).isActive(),"Only two working slots, no fuel controls");
});c.waitTicks(5);c.takeScreenshot("mill306-manual-"+locale+"-gui"+scale);
}
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(6);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Items.WHEAT)==2&&p.getInventory().countItem(Cuisine.item("wheat_flour"))==8,"Closing native screen returns unused grain only");
p.giveExperienceLevels(4);RecipeService.discover(p,AgricultureItems.TOMATO);RecipeService.buy(p);RecipeService.refresh(p);
});c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,300);c.waitTicks(8);
c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("redstone_mill")).findFirst().orElseThrow();
check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==10,"Ten manual recipes remain synchronized in JEI");runtime.getRecipesGui().showTypes(List.of(type));
});c.waitTicks(5);c.takeScreenshot("mill306-jei-manual-recipes");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(4);
server.runOnServer(s->{var mill=(RedstoneMillBlockEntity)s.overworld().getBlockEntity(MILL);mill.setItem(0,new ItemStack(Items.WHEAT,5));mill.setItem(1,new ItemStack(Items.COAL,7));mill.setItem(2,new ItemStack(Cuisine.item("wheat_flour"),3));});saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var mill=(RedstoneMillBlockEntity)p.level().getBlockEntity(MILL);
check(mill.getItem(0).getCount()==5&&mill.getItem(1).getCount()==7&&mill.getItem(2).getCount()==3,"Legacy inventory round trip");p.teleportTo(.5,1,2.5);p.openMenu(mill);
});c.waitFor(m->m.gui.screen() instanceof RedstoneMillScreen&&m.player.containerMenu.getSlot(2).getItem().is(Items.COAL),200);c.waitTicks(20);c.takeScreenshot("mill306-stored-items-recovery");
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,2,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(4);
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,3,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(4);
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(6);
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Items.COAL)==7&&p.getInventory().countItem(Items.WHEAT)==7&&p.getInventory().countItem(Cuisine.item("wheat_flour"))==11,"Native recovery returns old items and unspent input, without creating the preview");});
}
System.out.println("MILL306_CLIENT_PASS Vulkan, native insertion/pickup/shift/close, two working slots, mechanical stroke without fire, names FR/EN GUI 2/3/4, JEI ten recipes, disk and old inventory recovery");
}
}
@@ -0,0 +1,113 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.menu.KitchenOvenMenu;
import fr.koka.sanctuary.cuisine.culinary.block.entity.KitchenOvenBlockEntity;
import fr.koka.sanctuary.cuisine.culinary.client.screen.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Real cooking packets, cancellation, closed-menu continuation, single cooked output and disk reload. */
public final class Oven308ClientChecks implements FabricClientGameTest {
private static final BlockPos OVEN=new BlockPos(0,1,0);
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Oven validation requires Vulkan");
System.out.println("OVEN309_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Oven309-" + UUID.randomUUID()); s.setSeed("309"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);
l.setBlockAndUpdate(OVEN,KitchenAppliances.KITCHEN_OVEN.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);
p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Cuisine.item("raw_dough"),3));p.getInventory().setItem(1,new ItemStack(Items.COAL));
p.openMenu((KitchenOvenBlockEntity)l.getBlockEntity(OVEN));
});c.waitFor(m->m.gui.screen() instanceof KitchenOvenScreen && m.player.containerMenu.getSlot(38).getItem().getCount()==3,200);
click(c,38,ContainerInput.QUICK_MOVE);
c.waitFor(m->m.player.containerMenu.getSlot(0).getItem().getCount()==3 && m.player.containerMenu.getSlot(10).getItem().isEmpty(),200);c.waitTicks(25);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);
check(oven.progress()==0 && oven.getItem(0).getCount()==3 && oven.getItem(10).isEmpty() && p.containerMenu.getCarried().isEmpty(),"No dish or consumed dough before fuel and a full cooking cycle");
});c.runOnClient(m->m.getWindow().setWindowed(1280,960));
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
check(m.gui.screen().getTitle().getString().equals(locale.equals("fr_fr")?"Four de cuisine":"Kitchen Oven"),"Localized oven title");
var menu=m.player.containerMenu;
check(menu.getSlot(11).container==m.player.getInventory() && menu.slots.subList(0,11).stream().noneMatch(Slot::isFake),"Exactly eleven real machine slots and no preview");
check(menu.getSlot(10).x==124 && menu.getSlot(10).y==35 && !menu.getSlot(10).mayPlace(new ItemStack(Items.BREAD)),"One centred, extraction-only cooked output");
check(!menu.getSlot(9).mayPlace(new ItemStack(Items.STONE)) && menu.getSlot(9).mayPlace(new ItemStack(Items.COAL)),"Client fuel validation follows native cooking components");
});c.waitTicks(5);c.takeScreenshot("oven309-grid-"+locale+"-gui"+scale);
}
c.runOnClient(m->{var menu=m.player.containerMenu;m.gameMode.handleContainerInput(menu.containerId,39,0,ContainerInput.QUICK_MOVE,m.player);
check(menu.getSlot(39).getItem().isEmpty() && menu.getSlot(9).getItem().is(Items.COAL),"Immediate client Shift prediction places coal in fuel, not the recipe grid");
});c.waitFor(m->((KitchenOvenMenu)m.player.containerMenu).cookingProgress()>=.75F,300);
c.takeScreenshot("oven309-cooking-before-cancellation");click(c,0,ContainerInput.PICKUP);c.waitTicks(6);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);
check(oven.progress()==0 && oven.getItem(0).isEmpty() && oven.getItem(10).isEmpty() && p.containerMenu.getCarried().getCount()==3,"Native extraction cancels nearly completed cooking without reserving ingredients");
});c.takeScreenshot("oven309-cancelled");click(c,0,ContainerInput.PICKUP);c.waitTicks(20);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);
check(oven.progress()>0 && oven.progress()<100 && oven.getItem(0).getCount()==3 && oven.getItem(10).isEmpty(),"Replacing the same dough starts a fresh full cooking cycle");
});c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitFor(m->m.gui.screen()==null,100);c.getInput().lookAt(180,25);
c.waitFor(m->m.level.getBlockEntity(OVEN) instanceof KitchenOvenBlockEntity oven && oven.getItem(10).is(Items.BREAD) && oven.getItem(10).getCount()==3,900);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);
check(oven.getItem(0).isEmpty() && oven.getItem(10).getCount()==3 && p.getInventory().countItem(Cuisine.item("raw_dough"))==0,"Three paid cycles continue with the inventory closed");
p.openMenu(oven);
});c.waitFor(m->m.gui.screen() instanceof KitchenOvenScreen && m.player.containerMenu.getSlot(10).getItem().getCount()==3,200);c.takeScreenshot("oven309-three-cooked-meals");
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(4);saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null && m.gui.screen()==null,1200);
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);
check(oven.getItem(0).isEmpty() && oven.getItem(10).is(Items.BREAD) && oven.getItem(10).getCount()==3,"Real disk reload preserves exactly the cooked storage");p.teleportTo(.5,1,2.5);p.openMenu(oven);
});c.waitFor(m->m.gui.screen() instanceof KitchenOvenScreen && m.player.containerMenu.getSlot(10).getItem().getCount()==3,200);c.takeScreenshot("oven309-finished-after-disk");
click(c,10,ContainerInput.QUICK_MOVE);c.waitTicks(5);
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var oven=(KitchenOvenBlockEntity)p.level().getBlockEntity(OVEN);
check(p.getInventory().countItem(Items.BREAD)==3 && oven.getItem(10).isEmpty(),"Native result Shift extracts exactly three paid meals after reload");
p.giveExperienceLevels(4);RecipeService.discover(p,AgricultureItems.TOMATO);RecipeService.buy(p);RecipeService.refresh(p);
});c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(4);
c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,300);
c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("kitchen_oven")).findFirst().orElseThrow();
check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==17,"All seventeen oven recipes remain synchronized in JEI");runtime.getRecipesGui().showTypes(List.of(type));
});c.waitTicks(5);c.takeScreenshot("oven309-jei-seventeen-recipes");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
}
System.out.println("OVEN309_CLIENT_PASS Vulkan, native grid/fuel Shift prediction, single cooked output FR/EN GUI 2/3/4, cancellation, three paid cycles with menu closed, disk reload, result extraction and JEI seventeen recipes");
}
private static void click(ClientGameTestContext c,int slot,ContainerInput action){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,slot,0,action,m.player));}
}
@@ -0,0 +1,82 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.FryingPanBlock;
import fr.koka.sanctuary.cuisine.culinary.block.entity.FryingPanBlockEntity;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.RecipeService;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
import net.minecraft.core.*;
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.Blocks;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Real placement and ingredient packets, Vulkan models/HUD, paid cooking and disk reopen. */
public final class Pan310ClientChecks implements FabricClientGameTest {
private static final BlockPos PAN=new BlockPos(0,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void use(ClientGameTestContext c,BlockPos pos,Direction face,double height){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(pos.getX()+.5,pos.getY()+height,pos.getZ()+.5),face,pos,false)));c.waitTicks(5);}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
c.runOnClient(m->{var device=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(device.backendName().equalsIgnoreCase("vulkan"),"Pan validation requires Vulkan");System.out.println("PAN310_BACKEND "+device.backendName()+" / "+device.name());m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
TestWorldSave saved;
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Pan310-"+UUID.randomUUID());s.setSeed("310");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);((net.minecraft.world.level.storage.ServerLevelData)l.getLevelData()).setGameTime(l.getGameTime()+2000);l.setBlockAndUpdate(PAN.below(),Blocks.STONE.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(KitchenAppliances.FRYING_PAN,5));});c.waitTicks(8);
use(c,PAN.below(),Direction.UP,1);
c.waitFor(m->m.level.getBlockState(PAN).is(KitchenAppliances.FRYING_PAN),100);
for(int count=2;count<=4;count++){use(c,PAN,Direction.UP,(count-1)/4D);final int n=count;c.waitFor(m->m.level.getBlockState(PAN).getValue(FryingPanBlock.PANS)==n,100);}
use(c,PAN,Direction.UP,1);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getMainHandItem().getCount()==1&&p.level().getBlockState(PAN.above()).isAir(),"Actual placement packets pay exactly four pans and reject a fifth above");});
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(1,1.5,.5),Direction.EAST,PAN,false)));c.waitTicks(6);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getMainHandItem().isEmpty()&&p.level().getBlockState(PAN.east()).is(KitchenAppliances.FRYING_PAN),"The fifth paid pan extends the horizontal line");p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.EGG,4));});c.waitTicks(5);
for(int i=0;i<4;i++)use(c,PAN,Direction.UP,1);
for(int i=0;i<4;i++){
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.MILK_BUCKET)));c.waitTicks(4);use(c,PAN,Direction.UP,1);
}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();var pan=(FryingPanBlockEntity)l.getBlockEntity(PAN);check(pan.getItem(0).getCount()==4&&pan.getItem(1).is(Items.MILK_BUCKET)&&pan.getItem(10).is(Items.MILK_BUCKET)&&p.getMainHandItem().isEmpty(),"Native interactions store four paid eggs and four separate milk buckets");
for(int count=1;count<=3;count++){var pos=new BlockPos(count-3,1,0);if(pos.equals(PAN))pos=PAN.east();l.setBlockAndUpdate(pos,KitchenAppliances.FRYING_PAN.defaultBlockState().setValue(FryingPanBlock.PANS,count));l.setBlockAndUpdate(pos.below(),Blocks.CAMPFIRE.defaultBlockState());var other=(FryingPanBlockEntity)l.getBlockEntity(pos);other.insertIngredient(new ItemStack(Items.BEEF,4),4);}
l.setBlockAndUpdate(PAN.below(),Blocks.CAMPFIRE.defaultBlockState());p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);p.teleportTo(.5,1,2.5);
});c.waitTicks(8);c.getInput().lookAt(180,20);c.waitTicks(5);c.runOnClient(m->{m.getWindow().setWindowed(1280,960);int models=0;
for(var state:KitchenAppliances.FRYING_PAN.getStateDefinition().getPossibleStates()){
var parts=new ArrayList<BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(310),parts);int quads=0;float maxY=0;
for(var part:parts)for(var direction:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})for(var q:part.getQuads(direction)){check(q.materialInfo().sprite().contents().name().toString().equals("sanctuary:block/frying_pan"),"Every layer uses the original resolved pan texture");for(int v=0;v<4;v++)maxY=Math.max(maxY,q.position(v).y());quads++;}
check(quads==30*state.getValue(FryingPanBlock.PANS)&&Math.abs(maxY-state.getValue(FryingPanBlock.PANS)/4F)<.0001,"Every lit/count model has all native cuboids at its physical height");models++;
}check(models==8,"All pan states baked under Vulkan");System.out.println("PAN310_MODELS states="+models+" heights=4,8,12,16");
});
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(PAN),"The actual pan HUD is targeted: "+m.hitResult+" eye="+m.player.getEyePosition());});c.waitTicks(3);c.takeScreenshot("pan310-preparation-"+locale+"-gui"+scale);}
server.runOnServer(s->{var l=s.overworld();for(int x=-2;x<=1;x++){var pan=(FryingPanBlockEntity)l.getBlockEntity(new BlockPos(x,1,0));var tag=pan.saveCustomOnly(l.registryAccess());tag.putLong("AssemblyStartedAt",l.getGameTime()-601);tag.putLong("LastIngredientChangedAt",l.getGameTime()-601);pan.loadWithComponents(net.minecraft.world.level.storage.TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,l.registryAccess(),tag));}});
c.waitFor(m->((FryingPanBlockEntity)m.level.getBlockEntity(PAN)).progress()>40,150);c.takeScreenshot("pan310-four-pans-cooking");
c.waitFor(m->((FryingPanBlockEntity)m.level.getBlockEntity(PAN)).getItem(4).getCount()==4,300);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pan=(FryingPanBlockEntity)p.level().getBlockEntity(PAN);check(pan.getItem(4).is(Cuisine.item("omelette"))&&pan.getItem(4).getCount()==4&&pan.getItem(1).is(Items.BUCKET)&&pan.getItem(1).getCount()==4,"Real ticks produce four paid omelettes and return four containers");for(int x=-2;x<=1;x++)if(x!=0){var other=(FryingPanBlockEntity)p.level().getBlockEntity(new BlockPos(x,1,0));check(other.getItem(4).is(Items.COOKED_BEEF)&&other.getItem(4).getCount()==other.panCount(),"Adjacent stacks independently cook a different recipe");}});
for(String locale:List.of("fr_fr","en_us")){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});c.waitTicks(3);c.takeScreenshot("pan310-ready-four-"+locale);}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(.5,3,4.5);p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();});c.waitTicks(5);c.getInput().lookAt(180,37);c.takeScreenshot("pan310-horizontal-one-two-four-three");saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pan=(FryingPanBlockEntity)p.level().getBlockEntity(PAN);check(pan.panCount()==4&&pan.getItem(4).getCount()==4&&pan.getItem(1).is(Items.BUCKET)&&pan.getItem(1).getCount()==4&&pan.getItem(10).isEmpty(),"Real disk reopening keeps the four pans and paid native inventory");p.setGameMode(GameType.SURVIVAL);p.getAbilities().flying=false;p.onUpdateAbilities();p.teleportTo(.5,1,2.5);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);});c.waitTicks(8);c.getInput().lookAt(180,20);c.takeScreenshot("pan310-paid-result-after-disk");
c.getInput().holdKey(o->o.keyShift);c.waitTicks(4);use(c,PAN,Direction.UP,1);c.getInput().releaseKey(o->o.keyShift);c.waitTicks(5);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Cuisine.item("omelette"))==4&&((FryingPanBlockEntity)p.level().getBlockEntity(PAN)).getItem(4).isEmpty(),"Actual shift interaction extracts exactly four cooked meals after disk");});c.runOnClient(m->m.player.getInventory().setSelectedSlot(1));c.waitTicks(4);c.takeScreenshot("pan310-returned-containers-hud");
c.getInput().holdKey(o->o.keyShift);c.waitTicks(4);use(c,PAN,Direction.UP,1);c.getInput().releaseKey(o->o.keyShift);c.waitTicks(5);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(Cuisine.item("omelette"))==4&&p.getInventory().countItem(Items.BUCKET)==4&&((FryingPanBlockEntity)p.level().getBlockEntity(PAN)).isEmpty(),"Actual shift interaction returns all four empty buckets exactly once");p.giveExperienceLevels(4);RecipeService.discover(p,AgricultureItems.TOMATO);RecipeService.buy(p);RecipeService.refresh(p);});c.waitFor(m->RecipeClient.allowed()&&SanctuaryJeiPlugin.runtime()!=null,300);
c.runOnClient(m->{var runtime=SanctuaryJeiPlugin.runtime();var type=KitchenJei.types().stream().filter(t->t.getUid().getPath().endsWith("frying_pan")).findFirst().orElseThrow();check(runtime.getRecipeManager().createRecipeLookup(type).includeHidden().get().count()==26,"Twenty-six original pan recipes remain in JEI");runtime.getRecipesGui().showTypes(List.of(type));});c.waitTicks(5);c.takeScreenshot("pan310-jei-twenty-six-recipes");c.getInput().pressKey(InputConstants.KEY_ESCAPE);
}
System.out.println("PAN310_CLIENT_PASS Vulkan, paid native placement 1-4/horizontal/cap, real four eggs + four unstackable milk packets, 8 baked states, FR/EN HUD GUI 2/3/4, real paid cooking, independent adjacent recipes, disk reopening, shift collection, exact containers and JEI26");
}
}
@@ -158,7 +158,7 @@ public final class Piston087ClientChecks implements FabricClientGameTest {
for(int i=0;i<9;i++){var pos=SuperPistons.tile(C,f,i);l.setBlockAndUpdate(pos,(sticky?Blocks.STICKY_PISTON:Blocks.PISTON).defaultBlockState().setValue(net.minecraft.world.level.block.piston.PistonBaseBlock.FACING,f));}
for(int depth=1;depth<=layers;depth++)for(var pos:SuperPistons.tiles(C,f,depth))l.setBlockAndUpdate(pos,Blocks.STONE.defaultBlockState());
}
private static void wrench(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
private static void wrench(ServerPlayer p,BlockPos pos){p.teleportTo(pos.getX()+.5,pos.getY(),pos.getZ()+3.5);UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
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);}
}
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.CuredSausageBlock;
import fr.koka.sanctuary.cuisine.culinary.block.entity.DryingRackBlockEntity;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.recipes.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.*;
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.gametest.v1.world.TestWorldSave;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.sounds.SoundEventListener;
import net.minecraft.core.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Actual key-use packets through rack/placement/slices, native models, audible bites and disk. */
public final class Sausage315ClientChecks implements FabricClientGameTest {
private static final BlockPos FOOD=new BlockPos(0,1,0),RACK=new BlockPos(-2,1,0);
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
private static void aim(ClientGameTestContext c,BlockPos pos){
var angles=new AtomicReference<float[]>();c.runOnClient(m->{var state=m.level.getBlockState(pos);var shape=state.getShape(m.level,pos);var point=shape.isEmpty()?Vec3.atCenterOf(pos):shape.bounds().getCenter().add(Vec3.atLowerCornerOf(pos));var d=point.subtract(m.player.getEyePosition());angles.set(new float[]{(float)(Math.toDegrees(Math.atan2(d.z,d.x))-90),(float)-Math.toDegrees(Math.atan2(d.y,Math.sqrt(d.x*d.x+d.z*d.z)))});});c.getInput().lookAt(angles.get()[0],angles.get()[1]);c.waitTicks(4);
}
private static void use(ClientGameTestContext c,BlockPos pos){aim(c,pos);c.runOnClient(m->check(m.hitResult instanceof BlockHitResult h&&h.getBlockPos().equals(pos),"The actual crosshair hits the shrinking food/target"));c.getInput().pressKey(o->o.keyUse);c.waitTicks(6);}
private static void bite(ClientGameTestContext c,net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext server,int eaten){
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getFoodData().setFoodLevel(0));c.waitFor(m->m.player.getFoodData().getFoodLevel()==0,100);use(c,FOOD);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var state=p.level().getBlockState(FOOD);check(p.getFoodData().getFoodLevel()==2&&(eaten==4?state.isAir():state.getValue(CuredSausageBlock.BITES)==eaten),"A real native key action pays one slice and restores two food points");});
}
@Override public void runTest(ClientGameTestContext c){
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);var audio=new CopyOnWriteArrayList<String>();var facing=new AtomicReference<Direction>();TestWorldSave saved;
SoundEventListener listener=(sound,event,distance)->{if(sound.getX()>=0&&sound.getX()<=1&&sound.getZ()>=0&&sound.getZ()<=1)audio.add(sound.getIdentifier().toString());};
c.runOnClient(m->{var d=com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();check(d.backendName().equalsIgnoreCase("vulkan"),"Only Vulkan is validated");System.out.println("SAUSAGE315_BACKEND "+d.backendName()+" / "+d.name());m.getWindow().setWindowed(1280,960);m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;m.getSoundManager().addListener(listener);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){IntroClient.setQuickTestIntro(true);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());press(screen,"sanctuary.progression.enter");intro.set(false);}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sausage315-"+UUID.randomUUID());s.setSeed("315");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);try{fr.koka.sanctuary.realtime.RealtimeService.setMode(s,false);fr.koka.sanctuary.weather.WeatherService.setMode(s,false);}catch(Exception e){throw new IllegalStateException(e);}s.clockManager().setTotalTicks(l.dimensionType().defaultClock().orElseThrow(),6000);l.getWeatherData().setRaining(false);l.setRainLevel(0);l.setBlockAndUpdate(RACK,KitchenAppliances.DRYING_RACK.defaultBlockState());l.setBlockAndUpdate(RACK.above(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(FOOD.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(new BlockPos(0,0,2),Blocks.STONE.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(KitchenAppliances.RAW_SAUSAGE));RecipeService.discover(p,KitchenAppliances.RAW_SAUSAGE);RecipeService.refresh(p);});c.waitTicks(8);
use(c,RACK);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(RACK);check(rack.dryingCount()==1&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==0,"The real hook interaction pays the one raw preparation");var tag=rack.saveCustomOnly(p.level().registryAccess());tag.putInt("DryingProgress0",5990);rack.loadWithComponents(TagValueInput.create(net.minecraft.util.ProblemReporter.DISCARDING,p.level().registryAccess(),tag));});c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(RACK)).readyCount()==1,120);use(c,RACK);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==1&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==0&&((DryingRackBlockEntity)p.level().getBlockEntity(RACK)).isEmpty(),"Real retrieval gives exactly one cured sausage from the paid preparation");});
use(c,FOOD.below());c.waitFor(m->m.level.getBlockState(FOOD).is(KitchenAppliances.CURED_SAUSAGE_BLOCK),100);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==0,"Native placement pays the one cured sausage");facing.set(p.level().getBlockState(FOOD).getValue(CuredSausageBlock.FACING));});aim(c,FOOD);c.takeScreenshot("sausage315-whole-on-table");
c.runOnClient(m->{int states=0;for(var state:KitchenAppliances.CURED_SAUSAGE_BLOCK.getStateDefinition().getPossibleStates()){var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(315),parts);int quads=0;var box=state.getShape(m.level,FOOD).bounds();for(var part:parts)for(Direction dir:new Direction[]{null,Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST})for(var q:part.getQuads(dir)){check(q.materialInfo().sprite().contents().name().getNamespace().equals("minecraft"),"Sausage uses native meat/thread/cut textures");for(int v=0;v<4;v++){var p=q.position(v);check(p.x()>=box.minX-.0001&&p.x()<=box.maxX+.0001&&p.y()>=box.minY-.0001&&p.y()<=box.maxY+.0001&&p.z()>=box.minZ-.0001&&p.z()<=box.maxZ+.0001,"All rotated visible geometry fits the actual serving outline");}quads++;}check(quads==(state.getValue(CuredSausageBlock.BITES)==0?48:30),"All cuboids and the visible cut bake without missing faces");states++;}check(states==16,"Four orientations times four remaining lengths");CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());var food=net.minecraft.core.registries.BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();check(food.getDisplayItems().stream().anyMatch(stack->stack.is(KitchenAppliances.CURED_SAUSAGE))&&KitchenAppliances.items().size()==15,"The same item stays in the existing food tab");var render=new net.minecraft.client.renderer.item.ItemStackRenderState();m.getItemModelResolver().updateForTopItem(render,new ItemStack(KitchenAppliances.CURED_SAUSAGE),ItemDisplayContext.GUI,m.level,m.player,315);check(!render.isEmpty()&&!render.pickParticleMaterial(net.minecraft.util.RandomSource.create(315)).sprite().contents().name().getPath().equals("missingno"),"The historical item model still renders");System.out.println("SAUSAGE315_MODELS states=16 item=1 native_food_tab=true");});
bite(c,server,1);c.takeScreenshot("sausage315-one-slice-eaten");bite(c,server,2);c.takeScreenshot("sausage315-half-eaten");
for(String locale:List.of("fr_fr","en_us"))for(int scale:new int[]{2,3,4}){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();check(!new ItemStack(KitchenAppliances.CURED_SAUSAGE).getHoverName().getString().startsWith("item.")&&!net.minecraft.network.chat.Component.translatable("sanctuary.sausage.tooltip").getString().startsWith("sanctuary."),"Serving labels are translated");});c.waitTicks(3);c.takeScreenshot("sausage315-half-"+locale+"-gui"+scale);}
saved=world.getWorldSave();
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var state=p.level().getBlockState(FOOD);check(state.is(KitchenAppliances.CURED_SAUSAGE_BLOCK)&&state.getValue(CuredSausageBlock.BITES)==2&&state.getValue(CuredSausageBlock.FACING)==facing.get()&&p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==0,"Actual disk reopen preserves orientation and exactly two eaten slices without refunding the item");});aim(c,FOOD);c.takeScreenshot("sausage315-half-after-disk");bite(c,server,3);c.takeScreenshot("sausage315-last-slice");bite(c,server,4);c.takeScreenshot("sausage315-finished");server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.level().getBlockState(FOOD).isAir()&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==0&&p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==0,"The fourth real bite consumes the single paid drying result");});
}
c.waitTicks(4);c.runOnClient(m->m.getSoundManager().removeListener(listener));check(audio.stream().filter(id->id.equals("minecraft:entity.generic.eat")).count()>=4,"The four actual slice interactions produce native eating sounds");System.out.println("SAUSAGE315_AUDIO "+audio);System.out.println("SAUSAGE315_CLIENT_PASS Vulkan, real hook payment, drying result, native crosshair/key placement, four real slices/eight food, 16 baked states, original item model/food tab, FR/EN GUI2/3/4, actual disk orientation/parts and eating audio");
}
}
@@ -0,0 +1,187 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.agriculture.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.progression.*;
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.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.renderer.block.dispatch.BlockStateModelPart;
import net.minecraft.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.phys.*;
import static fr.koka.sanctuary.agriculture.AgricultureBlocks.VANILLA_VINE;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Real survival planting/bonemeal/harvest packets, planar geometry, vanilla tabs and disk reload. */
public final class Vanilla301ClientChecks implements FabricClientGameTest {
private static final BlockPos BASE = new BlockPos(0, 1, 1);
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
var snapshot = new AtomicReference<Map<BlockPos, BlockState>>();
c.runOnClient(m -> {
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
check(device.backendName().equalsIgnoreCase("vulkan"), "Vanilla orchid validation requires Vulkan");
System.out.println("VANILLA301_BACKEND " + device.backendName() + " / " + device.name());
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
IntroClient.setQuickTestIntro(true); 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());
press(screen, "sanctuary.progression.enter"); intro.set(false);
}
});
TestWorldSave saved;
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("Vanilla301-" + UUID.randomUUID()); s.setSeed("301"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
var server = world.getServer();
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst(); var level = player.level();
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");
level.getGameRules().set(GameRules.SPAWN_MOBS, false, s);
level.getGameRules().set(GameRules.RANDOM_TICK_SPEED, 0, s);
var logs = List.of(Blocks.OAK_LOG, Blocks.JUNGLE_LOG, Blocks.STRIPPED_BIRCH_LOG, Blocks.CHERRY_LOG);
for (int i = 0; i < logs.size(); i++) for (int y = 1; y <= 7; y++)
level.setBlockAndUpdate(new BlockPos(i * 4, y, 0), logs.get(i).defaultBlockState());
for (int i = 0; i < logs.size(); i++) for (int y = 7; y <= 9; y++)
for (int x = -2; x <= 2; x++) for (int z = -2; z <= 2; z++) {
if (x == 0 && z == 0 && y == 7 || y == 9 && Math.abs(x) + Math.abs(z) > 2) continue;
level.setBlockAndUpdate(new BlockPos(i * 4 + x, y, z), Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true));
}
player.teleportTo(.5, 1, 3.5); player.setYRot(180); player.setXRot(15);
player.setGameMode(GameType.SURVIVAL); player.getInventory().clearContent();
player.getInventory().setItem(0, new ItemStack(AgricultureItems.VANILLA_CUTTING, 2)); player.inventoryMenu.broadcastChanges();
});
c.waitFor(m -> m.player.getMainHandItem().is(AgricultureItems.VANILLA_CUTTING), 400);
c.runOnClient(m -> m.gameMode.useItemOn(m.player, InteractionHand.MAIN_HAND, hit(BASE.north(), Direction.SOUTH)));
c.waitFor(m -> m.level.getBlockState(BASE).is(VANILLA_VINE), 200);
c.waitTicks(6);
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst();
check(player.getMainHandItem().getCount() == 1 && VanillaVineBlock.attached(player.level().getBlockState(BASE), Direction.NORTH), "Native survival placement consumes one cutting and attaches to the trunk: count=" + player.getMainHandItem().getCount() + " state=" + player.level().getBlockState(BASE));
player.getInventory().setItem(0, new ItemStack(Items.BONE_MEAL, 16)); player.inventoryMenu.broadcastChanges();
});
c.waitFor(m -> m.player.getMainHandItem().is(Items.BONE_MEAL), 200);
for (int i = 0; i < 4; i++) {
c.runOnClient(m -> m.gameMode.useItemOn(m.player, InteractionHand.MAIN_HAND, hit(BASE, Direction.SOUTH))); c.waitTicks(5);
}
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst();
check(player.level().getBlockState(BASE.above()).is(VANILLA_VINE) && !player.level().getBlockState(BASE.below()).is(VANILLA_VINE), "Actual bonemeal packets extend upward only");
check(player.getMainHandItem().getCount() == 12, "Exact native bonemeal consumption");
player.getInventory().setItem(0, ItemStack.EMPTY); player.inventoryMenu.broadcastChanges();
});
c.waitFor(m -> m.player.getMainHandItem().isEmpty(), 200);
c.runOnClient(m -> m.gameMode.useItemOn(m.player, InteractionHand.MAIN_HAND, hit(BASE, Direction.SOUTH))); c.waitTicks(5);
server.runOnServer(s -> {
var level = s.overworld();
check(level.getBlockState(BASE).getValue(VanillaVineBlock.AGE) == 1, "Actual empty-hand interaction picks ripe pods");
int count = level.getEntitiesOfClass(ItemEntity.class, new AABB(BASE).inflate(1)).stream()
.filter(item -> item.getItem().is(AgricultureItems.GREEN_VANILLA_POD)).mapToInt(item -> item.getItem().getCount()).sum();
check(count >= 1 && count <= 3, "Server awards the real pod harvest once");
for (int i = 0; i < 4; i++) {
var base = BASE.offset(i * 4, 0, 0);
if (i != 0) level.setBlockAndUpdate(base, vine(0));
for (int y = 0; y < 6; y++) {
var pos = base.above(y);
for (int n = 0; n < 4; n++) BoneMealItem.growCrop(new ItemStack(Items.BONE_MEAL), level, pos);
}
}
// Retain multiple faces and maturity on a supported corner for the actual disk test.
level.setBlockAndUpdate(BASE.east(), Blocks.OAK_LOG.defaultBlockState());
level.setBlockAndUpdate(BASE, vine(2).setValue(VanillaVineBlock.face(Direction.EAST), true));
});
c.runOnClient(m -> {
int states = 0;
for (var state : VANILLA_VINE.getStateDefinition().getPossibleStates()) {
var parts = new ArrayList<BlockStateModelPart>();
m.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(301), parts);
int faces = 0; for (var side : Direction.Plane.HORIZONTAL) if (VanillaVineBlock.attached(state, side)) faces++;
int quads = 0;
for (var part : parts) for (var direction : new Direction[]{null, Direction.UP, Direction.DOWN, Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST})
for (var quad : part.getQuads(direction)) {
var material = quad.materialInfo(); var contents = material.sprite().contents(); var sprite = contents.name();
check(sprite.equals(SanctuaryMod.id("block/vanilla_climbing_stage" + state.getValue(VanillaVineBlock.AGE))), "One integrated vanilla sprite is baked for each maturity stage");
check(contents.width() == 16 && contents.height() == 16, "All four vanilla growth sprites must be exactly 16 by 16 pixels in the loaded atlas");
if (faces == 1) {
var side = Direction.NORTH;
for (var candidate : Direction.Plane.HORIZONTAL) if (VanillaVineBlock.attached(state, candidate)) side = candidate;
for (int v = 0; v < 4; v++) {
var p = quad.position(v); float edge = switch (side) { case NORTH -> p.z(); case SOUTH -> 1 - p.z(); case WEST -> p.x(); case EAST -> 1 - p.x(); default -> 1; };
check(edge >= 0 && edge <= 1.01 / 16, "Actual baked vertices lie against the trunk face, not around the cell centre");
}
}
check(material.tintIndex() == -1, "The integrated leaves, flowers and pods retain their authored colours");
quads++;
}
check(quads == faces * 2, "One two-sided plane per attached face, no overlapping texture layers"); states++;
}
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(), true, m.level.registryAccess());
var natural = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.NATURAL_BLOCKS).value();
var food = BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.FOOD_AND_DRINKS).value();
check(natural.getDisplayItems().stream().anyMatch(stack -> stack.is(AgricultureItems.VANILLA_CUTTING)), "Cutting is in the existing Natural tab");
for (var item : List.of(AgricultureItems.GREEN_VANILLA_POD, AgricultureItems.VANILLA_POD)) check(food.getDisplayItems().stream().anyMatch(stack -> stack.is(item)), "Pod is in the existing Food & Drinks tab");
check(BuiltInRegistries.CREATIVE_MODE_TAB.getOptional(SanctuaryMod.id("agriculture")).isEmpty(), "No new creative tab");
for (String locale : List.of("fr_fr", "en_us")) {
m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
for (var item : List.of(AgricultureItems.VANILLA_CUTTING, AgricultureItems.GREEN_VANILLA_POD, AgricultureItems.VANILLA_POD))
check(!new ItemStack(item).getHoverName().getString().startsWith("item.sanctuary"), "Translated vanilla orchid item: " + locale);
}
m.getWindow().setWindowed(1280, 960);
System.out.println("VANILLA301_MODELS states=" + states + " supported=60 single_plane=true new_sprites=4 sprite_size=16x16 legacy_pixels=unchanged");
});
server.runOnServer(s -> {
var player = s.getPlayerList().getPlayers().getFirst(); player.setGameMode(GameType.CREATIVE);
player.teleportTo(6.5, 4, 15); player.setYRot(180); player.setXRot(2); player.getAbilities().flying = true; player.onUpdateAbilities();
});
c.waitTicks(18); c.runOnClient(m -> {m.player.setYRot(180);m.player.setXRot(2);});
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1); c.waitTicks(4); c.takeScreenshot("vanilla302-16px-climbs-tree-trunks");
server.runOnServer(s -> {var p = s.getPlayerList().getPlayers().getFirst();p.teleportTo(.5, 3.5, 5);p.setYRot(180);p.setXRot(0);});
c.waitTicks(8); c.runOnClient(m -> {m.player.setYRot(180);m.player.setXRot(0);}); c.waitTicks(3); c.takeScreenshot("vanilla302-16px-flat-leaves-and-pods");
server.runOnServer(s -> {
var map = new LinkedHashMap<BlockPos, BlockState>(); var level = s.overworld();
for (var pos : BlockPos.betweenClosed(new BlockPos(-1, 1, 0), new BlockPos(13, 8, 2)))
if (level.getBlockState(pos).is(VANILLA_VINE)) map.put(pos.immutable(), level.getBlockState(pos));
check(map.size() >= 20, "Actual upward cultivation produced a full orchid gallery"); snapshot.set(map);
});
saved = world.getWorldSave();
}
try (var world = saved.open()) {
c.waitFor(m -> m.player != null && m.gui.screen() == null, 1200);
world.getServer().runOnServer(s -> {
for (var entry : snapshot.get().entrySet()) check(s.overworld().getBlockState(entry.getKey()).equals(entry.getValue()), "Disk reload retains the actual attachment faces and age");
System.out.println("VANILLA301_DISK_RELOAD segments=" + snapshot.get().size());
});
c.waitTicks(6); c.takeScreenshot("vanilla302-16px-reloaded");
}
System.out.println("VANILLA301_CLIENT_PASS Vulkan, native survival packets, upward growth, thin tree faces, pods, vanilla tabs, FR/EN and disk reload");
}
private static BlockState vine(int age) { return VANILLA_VINE.defaultBlockState().setValue(VanillaVineBlock.face(Direction.NORTH), true).setValue(VanillaVineBlock.AGE, age); }
private static BlockHitResult hit(BlockPos pos, Direction side) { return new BlockHitResult(Vec3.atCenterOf(pos).add(side.getStepX() * .5, side.getStepY() * .5, side.getStepZ() * .5), side, pos, false); }
}
@@ -142,6 +142,12 @@ public final class Pbr134ClientChecks implements FabricClientGameTest {
check(failures==0,"Close PBR stays stable under subpixel motion: failing distances="+failures);
}
private static void materials(){
try(var materials=new PbrMaterials()){
check(!materials.candidate(Blocks.BARRIER.defaultBlockState())&&!materials.candidate(Blocks.MOVING_PISTON.defaultBlockState()),
"Invisible and entity-rendered blocks cannot leave normals from their baked terrain model");
check(materials.candidate(Blocks.OAK_PLANKS.defaultBlockState())&&materials.candidate(Blocks.GLASS.defaultBlockState()),
"Visible wooden and glass terrain models keep their material pass");
}
int[] flat=new int[16];Arrays.fill(flat,0xFF808080);var profile=PbrMaterials.profile("bricks");
for(int p:PbrMaterials.generate(flat,4,4,profile))check((p>>8&0xFFFF)==0x8080,"Flat source has neutral normal");
flat[5]=0xFFFFFFFF;var slope=PbrMaterials.generate(flat,4,4,profile);check((slope[4]>>16&255)<128&&(slope[6]>>16&255)>128,"Height gradients produce opposite normal slopes");
@@ -0,0 +1,285 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.agriculture.*;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.NbtOps;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.*;
import net.minecraft.gametest.framework.GameTestHelper;
import static fr.koka.sanctuary.agriculture.AgricultureBlocks.*;
/** Native placement, harvesting, loot, tree growth and state persistence in a fresh test world. */
public final class Agriculture300GameTests {
@GameTest public void allTwentyCropsPlantConsumeOneSeedAndGrow(GameTestHelper h) {
var player = Progression003GameTests.player(h);
h.assertTrue(crops().size() == 20, "Twenty agricultural crops are registered");
int index = 0;
for (var block : crops().values()) {
var crop = (AgriculturalCropBlock)block;
var pos = h.absolutePos(new BlockPos(2 + index % 5 * 2, 20, 2 + index++ / 5 * 2));
ground(h, pos, crop == RICE_CROP);
var seed = crop.defaultBlockState().getCloneItemStack(h.getLevel(), pos, false).getItem();
var stack = new ItemStack(seed, 3);
player.setItemInHand(InteractionHand.MAIN_HAND, stack);
player.setPos(Vec3.atCenterOf(pos).add(0, 0, 2));
seed.useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, hit(pos.below())));
h.assertTrue(h.getLevel().getBlockState(pos).is(crop) && stack.getCount() == 2,
"Native placement consumes exactly one seed: " + crop);
for (int i = 0; i < 8 && !crop.isMaxAge(h.getLevel().getBlockState(pos)); i++) {
h.assertTrue(BoneMealItem.growCrop(new ItemStack(Items.BONE_MEAL, 1), h.getLevel(), pos), "Native bonemeal grows " + crop);
}
h.assertTrue(crop.isMaxAge(h.getLevel().getBlockState(pos)), "Crop reaches maturity: " + crop);
}
h.succeed();
}
@GameTest public void immatureAndMatureLootUseNative263Conditions(GameTestHelper h) {
var pos = h.absolutePos(new BlockPos(4, 20, 4));
for (var block : crops().values()) {
var crop = (AgriculturalCropBlock)block;
var seed = crop.defaultBlockState().getCloneItemStack(h.getLevel(), pos, false).getItem();
var immature = Block.getDrops(crop.getStateForAge(0), h.getLevel(), pos, null);
h.assertTrue(immature.size() == 1 && immature.getFirst().is(seed) && immature.getFirst().getCount() == 1,
"Immature crop returns its single planting item: " + crop);
var mature = Block.getDrops(crop.getStateForAge(7), h.getLevel(), pos, null);
h.assertTrue(!mature.isEmpty() && mature.stream().mapToInt(ItemStack::getCount).sum() >= 1,
"Mature native loot is not empty: " + crop);
if (crop == TOMATO_CROP) h.assertTrue(mature.stream().anyMatch(s -> s.is(AgricultureItems.TOMATO)), "Ripe tomato drops fruit rather than only seeds");
if (crop == PINEAPPLE_CROP) h.assertTrue(mature.stream().anyMatch(s -> s.is(AgricultureItems.PINEAPPLE)), "Ripe pineapple drops its fruit");
}
h.succeed();
}
@GameTest public void perennialsHarvestOnceAndKeepTheirPlant(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var pos = h.absolutePos(new BlockPos(4, 20, 4));
for (var block : crops().values()) {
if (!(block instanceof HarvestableCropBlock crop)) continue;
ground(h, pos, false);
h.getLevel().setBlockAndUpdate(pos, crop.getStateForAge(7));
clearDrops(h, pos);
h.getLevel().getBlockState(pos).useWithoutItem(h.getLevel(), player, hit(pos));
int count = dropped(h, pos);
h.assertTrue(count >= 1 && count <= 4 && crop.getAge(h.getLevel().getBlockState(pos)) == 3,
"One ripe harvest resets the retained plant: " + crop);
h.getLevel().getBlockState(pos).useWithoutItem(h.getLevel(), player, hit(pos));
h.assertTrue(dropped(h, pos) == count, "Second click cannot duplicate a harvest: " + crop);
}
h.succeed();
}
@GameTest public void tallCropsRespectCeilingsAndSynchronizeTheirUpperHalf(GameTestHelper h) {
var pos = h.absolutePos(new BlockPos(4, 20, 4));
for (var crop : List.of(TOMATO_CROP, CORN_CROP, CUCUMBER_CROP, BEAN_CROP)) {
ground(h, pos, false);
h.getLevel().setBlockAndUpdate(pos, crop.getStateForAge(3));
h.getLevel().setBlockAndUpdate(pos.above(), Blocks.STONE.defaultBlockState());
var boneMeal = new ItemStack(Items.BONE_MEAL, 1);
h.assertTrue(!BoneMealItem.growCrop(boneMeal, h.getLevel(), pos) && boneMeal.getCount() == 1,
"Blocked height refuses bonemeal without consuming it");
h.assertTrue(h.getLevel().getBlockState(pos.above()).is(Blocks.STONE), "Growth cannot replace the ceiling");
h.getLevel().removeBlock(pos.above(), false);
for (int i = 0; i < 8 && !crop.isMaxAge(h.getLevel().getBlockState(pos)); i++)
BoneMealItem.growCrop(new ItemStack(Items.BONE_MEAL), h.getLevel(), pos);
var upper = h.getLevel().getBlockState(pos.above());
h.assertTrue(upper.getBlock() instanceof TallCropUpperBlock && upper.getValue(TallCropUpperBlock.AGE) == 7,
"Upper half synchronizes to actual mature age");
h.getLevel().destroyBlock(pos, false);
h.assertTrue(h.getLevel().getBlockState(pos.above()).isAir(), "Breaking the base removes the upper half");
}
h.succeed();
}
@GameTest public void upperHarvestAndUpperBreakNeverProduceASecondLoot(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var pos = h.absolutePos(new BlockPos(4, 20, 4));
ground(h, pos, false);
h.getLevel().setBlockAndUpdate(pos, TOMATO_CROP.getStateForAge(3));
for (int i = 0; i < 8 && !TOMATO_CROP.isMaxAge(h.getLevel().getBlockState(pos)); i++)
BoneMealItem.growCrop(new ItemStack(Items.BONE_MEAL), h.getLevel(), pos);
clearDrops(h, pos);
h.getLevel().getBlockState(pos.above()).useWithoutItem(h.getLevel(), player, hit(pos.above()));
h.assertTrue(TOMATO_CROP.getAge(h.getLevel().getBlockState(pos)) == 3 && h.getLevel().getBlockState(pos.above()).isAir(),
"Harvesting through the upper half resets the base and removes the top");
h.assertTrue(dropped(h, pos) >= 2 && dropped(h, pos) <= 4, "Only the lower plant awards fruit");
h.getLevel().setBlockAndUpdate(pos, CORN_CROP.getStateForAge(7));
h.getLevel().setBlockAndUpdate(pos.above(), CORN_CROP_UPPER.defaultBlockState().setValue(TallCropUpperBlock.AGE, 7));
clearDrops(h, pos);
player.gameMode.destroyBlock(pos.above());
h.assertTrue(h.getLevel().getBlockState(pos).isAir() && h.getLevel().getBlockState(pos.above()).isAir(), "Mining the top destroys the complete plant");
h.assertTrue(items(h, pos).stream().flatMap(e -> java.util.stream.Stream.of(e.getItem())).filter(s -> s.is(AgricultureItems.CORN)).mapToInt(ItemStack::getCount).sum() == 1,
"The non-obtainable upper half does not duplicate the corn harvest");
h.succeed();
}
@GameTest public void riceRequiresMudAndARealWaterSourceAndReturnsItsWater(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var pos = h.absolutePos(new BlockPos(4, 20, 4));
player.setPos(Vec3.atCenterOf(pos).add(0, 0, 2));
for (int mode = 0; mode < 4; mode++) {
h.getLevel().setBlockAndUpdate(pos.below(), (mode == 2 ? Blocks.DIRT : Blocks.MUD).defaultBlockState());
h.getLevel().setBlockAndUpdate(pos, mode == 0 ? Blocks.AIR.defaultBlockState()
: mode == 1 ? Blocks.WATER.defaultBlockState().setValue(LiquidBlock.LEVEL, 1) : Blocks.WATER.defaultBlockState());
var seeds = new ItemStack(AgricultureItems.RICE, 2);
player.setItemInHand(InteractionHand.MAIN_HAND, seeds);
seeds.getItem().useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, hit(pos.below())));
h.assertTrue(h.getLevel().getBlockState(pos).is(RICE_CROP) == (mode == 3), "Only mud plus source water allows planting; case " + mode);
h.assertTrue(seeds.getCount() == (mode == 3 ? 1 : 2), "Rejected planting preserves seeds");
}
h.getLevel().destroyBlock(pos, true);
h.assertTrue(h.getLevel().getFluidState(pos).isSource(), "Harvested rice leaves its native water source");
h.succeed();
}
@GameTest public void beansDistinguishGreenPodsFromDryBeans(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var pos = h.absolutePos(new BlockPos(4, 20, 4));
ground(h, pos, false);
for (int age : new int[]{5, 6, 7}) {
h.getLevel().setBlockAndUpdate(pos, BEAN_CROP.getStateForAge(age)); clearDrops(h, pos);
h.getLevel().getBlockState(pos).useWithoutItem(h.getLevel(), player, hit(pos));
var expected = age == 7 ? AgricultureItems.BEANS : AgricultureItems.GREEN_BEANS;
h.assertTrue(!items(h, pos).isEmpty() && items(h, pos).stream().allMatch(e -> e.getItem().is(expected)), "Bean stage determines the actual harvest");
h.assertTrue(BEAN_CROP.getAge(h.getLevel().getBlockState(pos)) == 3, "Beans remain planted after picking");
}
h.succeed();
}
@GameTest public void coffeeGrowsAndHarvestsCherriesOnce(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var pos = h.absolutePos(new BlockPos(4, 20, 4));
player.setPos(Vec3.atCenterOf(pos).add(0, 0, 2));
h.getLevel().setBlockAndUpdate(pos.below(), Blocks.GRASS_BLOCK.defaultBlockState());
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(AgricultureItems.COFFEE_BEANS, 2));
AgricultureItems.COFFEE_BEANS.useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, hit(pos.below())));
h.assertTrue(h.getLevel().getBlockState(pos).is(COFFEE_BUSH), "Coffee beans plant the coffee bush");
for (int i = 0; i < 3; i++) BoneMealItem.growCrop(new ItemStack(Items.BONE_MEAL), h.getLevel(), pos);
clearDrops(h, pos);
h.getLevel().getBlockState(pos).useWithoutItem(h.getLevel(), player, hit(pos));
int count = dropped(h, pos);
h.assertTrue(count >= 2 && count <= 4 && items(h, pos).stream().allMatch(e -> e.getItem().is(AgricultureItems.COFFEE_CHERRIES)), "Mature coffee awards cherries");
h.getLevel().getBlockState(pos).useWithoutItem(h.getLevel(), player, hit(pos));
h.assertTrue(dropped(h, pos) == count && h.getLevel().getBlockState(pos).getValue(SweetBerryBushBlock.AGE) == 1, "Picking resets the coffee bush without a duplicate reward");
h.succeed();
}
@GameTest public void fruitHarvestAndRegrowthKeepNativeLeafProperties(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var pos = h.absolutePos(new BlockPos(4, 20, 4));
for (var leaves : List.of(OLIVE_LEAVES, LEMON_LEAVES)) {
var state = leaves.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true).setValue(FruitLeavesBlock.FRUITING, true);
h.getLevel().setBlockAndUpdate(pos, state); clearDrops(h, pos);
state.useWithoutItem(h.getLevel(), player, hit(pos));
var after = h.getLevel().getBlockState(pos);
h.assertTrue(after.is(leaves) && after.getValue(LeavesBlock.PERSISTENT) && !after.getValue(FruitLeavesBlock.FRUITING) && dropped(h, pos) == 1,
"A fruit harvest retains the actual leaves and native properties");
after.useWithoutItem(h.getLevel(), player, hit(pos));
h.assertTrue(dropped(h, pos) == 1, "Fruit cannot be collected twice");
var random = RandomSource.create(300);
for (int i = 0; i < 4096 && !h.getLevel().getBlockState(pos).getValue(FruitLeavesBlock.FRUITING); i++)
h.getLevel().getBlockState(pos).randomTick(h.getLevel(), pos, random);
h.assertTrue(h.getLevel().getBlockState(pos).getValue(FruitLeavesBlock.FRUITING), "Native random ticks regrow fruit");
}
h.succeed();
}
@GameTest public void lemonSaplingCreatesItsOwnNativeTree(GameTestHelper h) { tree(h, LEMON_SAPLING, LEMON_LEAVES, Blocks.OAK_LOG); }
@GameTest public void oliveSaplingCreatesItsOwnNativeTree(GameTestHelper h) { tree(h, OLIVE_SAPLING, OLIVE_LEAVES, Blocks.ACACIA_LOG); }
@GameTest public void pineappleEatingReturnsExactlyOneCrown(GameTestHelper h) {
var player = Progression003GameTests.player(h);
player.getInventory().clearContent(); player.getFoodData().setFoodLevel(0);
var result = AgricultureItems.PINEAPPLE.finishUsingItem(new ItemStack(AgricultureItems.PINEAPPLE, 2), h.getLevel(), player);
h.assertTrue(result.getCount() == 1 && player.getInventory().countItem(AgricultureItems.PINEAPPLE_CROWN) == 1, "One consumed pineapple returns exactly one replantable crown");
h.succeed();
}
@GameTest public void agriculturalStatesRoundTripAndCompostingUsesNativeComponents(GameTestHelper h) {
var states = new ArrayList<BlockState>();
for (var block : crops().values()) states.add(((AgriculturalCropBlock)block).getStateForAge(7));
states.add(LEMON_LEAVES.defaultBlockState().setValue(FruitLeavesBlock.FRUITING, true));
states.add(OLIVE_LEAVES.defaultBlockState().setValue(FruitLeavesBlock.FRUITING, true));
states.add(COFFEE_BUSH.defaultBlockState().setValue(SweetBerryBushBlock.AGE, 3));
for (var state : states) {
var saved = BlockState.CODEC.encodeStart(NbtOps.INSTANCE, state).getOrThrow();
h.assertTrue(BlockState.CODEC.parse(NbtOps.INSTANCE, saved).getOrThrow().equals(state), "Native block-state persistence retains age/fruiting: " + state);
}
for (var item : AgricultureItems.PLANTING_ITEMS)
h.assertTrue(new ItemStack(item).has(DataComponents.COMPOSTABLE), "Planting item uses 26.3 compostable component: " + item);
h.succeed();
}
@GameTest public void nativeRecipesRecoverSeedsAndRoastCoffee(GameTestHelper h) {
var manager = h.getLevel().getServer().getRecipeManager();
var ingredients = List.of(AgricultureItems.CORN, AgricultureItems.COFFEE_CHERRIES, AgricultureItems.PINEAPPLE);
var expected = List.of(AgricultureItems.CORN_KERNELS, AgricultureItems.COFFEE_BEANS, AgricultureItems.PINEAPPLE_CROWN);
for (int i = 0; i < ingredients.size(); i++) {
var input = net.minecraft.world.item.crafting.CraftingInput.of(1, 1, List.of(new ItemStack(ingredients.get(i))));
var output = manager.getRecipeFor(net.minecraft.world.item.crafting.RecipeType.CRAFTING, input, h.getLevel()).orElseThrow().value().assemble(input);
h.assertTrue(output.is(expected.get(i)) && output.getCount() == (i == 2 ? 1 : 2), "Native crafting recovers the expected propagules");
}
var input = new net.minecraft.world.item.crafting.SingleRecipeInput(new ItemStack(AgricultureItems.COFFEE_BEANS));
var coffee = manager.getRecipeFor(net.minecraft.world.item.crafting.RecipeType.SMOKING, input, h.getLevel()).orElseThrow().value().assemble(input);
h.assertTrue(coffee.is(AgricultureItems.ROASTED_COFFEE_BEANS) && coffee.getCount() == 1, "Native smoker recipe roasts coffee beans");
h.succeed();
}
@GameTest public void fruitLeavesCanBeCollectedWithNativeShears(GameTestHelper h) {
var pos = h.absolutePos(new BlockPos(4, 20, 4));
for (var block : List.of(OLIVE_LEAVES, LEMON_LEAVES)) {
var drops = Block.getDrops(block.defaultBlockState(), h.getLevel(), pos, null, null, new ItemStack(Items.SHEARS));
h.assertTrue(drops.size() == 1 && drops.getFirst().is(block.asItem()), "Native shears retain the fruit leaf block");
}
h.succeed();
}
@GameTest public void nativeRandomTicksGrowRiceAndBothKindsOfTallCrop(GameTestHelper h) {
var pos = h.absolutePos(new BlockPos(4, 20, 4));
for (var crop : List.of(RICE_CROP, TOMATO_CROP, CORN_CROP, BEAN_CROP)) {
ground(h, pos, crop == RICE_CROP);
h.getLevel().setBlockAndUpdate(pos, crop.defaultBlockState());
var random = RandomSource.create(300);
for (int i = 0; i < 4096 && !crop.isMaxAge(h.getLevel().getBlockState(pos)); i++)
h.getLevel().getBlockState(pos).randomTick(h.getLevel(), pos, random);
h.assertTrue(crop.isMaxAge(h.getLevel().getBlockState(pos)), "Native random growth reaches maturity: " + crop);
if (crop != RICE_CROP) h.assertTrue(h.getLevel().getBlockState(pos.above()).getValue(TallCropUpperBlock.AGE) == 7, "Natural growth synchronizes the top too");
}
h.succeed();
}
private static void tree(GameTestHelper h, FruitTreeSaplingBlock sapling, FruitLeavesBlock leaves, Block log) {
var pos = h.absolutePos(new BlockPos(7, 20, 7));
for (var p : BlockPos.betweenClosed(pos.offset(-7, -1, -7), pos.offset(7, -1, 7))) h.getLevel().setBlockAndUpdate(p, Blocks.DIRT.defaultBlockState());
var witness = pos.offset(7, 8, 7);
var original = Blocks.OAK_LEAVES.defaultBlockState().setValue(LeavesBlock.PERSISTENT, true);
h.getLevel().setBlockAndUpdate(witness, original);
h.getLevel().setBlockAndUpdate(pos, sapling.defaultBlockState());
for (int i = 0; i < 8 && h.getLevel().getBlockState(pos).is(sapling); i++)
sapling.advanceTree(h.getLevel(), pos, h.getLevel().getBlockState(pos), RandomSource.create(300 + i));
h.assertTrue(h.getLevel().getBlockState(pos).is(log), "A planted fruit sapling grows its native trunk");
long count = BlockPos.betweenClosedStream(pos.offset(-7, 0, -7), pos.offset(7, 16, 7)).filter(p -> h.getLevel().getBlockState(p).is(leaves)).count();
h.assertTrue(count > 5, "The native feature creates fruit leaves directly");
h.assertTrue(h.getLevel().getBlockState(witness).equals(original), "An existing neighbouring tree is never converted");
h.succeed();
}
private static void ground(GameTestHelper h, BlockPos pos, boolean rice) {
h.getLevel().setBlockAndUpdate(pos.below(), rice ? Blocks.MUD.defaultBlockState() : Blocks.FARMLAND.defaultBlockState().setValue(FarmlandBlock.MOISTURE, 7));
h.getLevel().setBlockAndUpdate(pos, rice ? Blocks.WATER.defaultBlockState() : Blocks.AIR.defaultBlockState());
h.getLevel().setBlockAndUpdate(pos.above(), Blocks.AIR.defaultBlockState());
}
private static BlockHitResult hit(BlockPos pos) { return new BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false); }
private static List<ItemEntity> items(GameTestHelper h, BlockPos pos) { return h.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(.75)); }
private static int dropped(GameTestHelper h, BlockPos pos) { return items(h, pos).stream().mapToInt(e -> e.getItem().getCount()).sum(); }
private static void clearDrops(GameTestHelper h, BlockPos pos) { items(h, pos).forEach(ItemEntity::discard); }
}
@@ -0,0 +1,102 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.CulinaryAppliance;
import fr.koka.sanctuary.cuisine.culinary.block.CookingCauldronBlock;
import fr.koka.sanctuary.cuisine.culinary.block.entity.CookingCauldronBlockEntity;
import fr.koka.sanctuary.cuisine.culinary.recipe.CulinaryProcessRecipe;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.display.SlotDisplayContext;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
import net.minecraft.world.phys.*;
/** Native ingredients, real recipe manager, explicit gestures, paid containers and old storage. */
public final class Cauldron311GameTests {
private static CookingCauldronBlockEntity pot(GameTestHelper h){
var pos=h.absolutePos(new BlockPos(2,6,2));
if(h.getLevel().getBlockEntity(pos) instanceof CookingCauldronBlockEntity old)old.clearContent();
h.getLevel().removeBlock(pos,false);h.getLevel().setBlockAndUpdate(pos.below(),Blocks.CAMPFIRE.defaultBlockState());
h.getLevel().setBlockAndUpdate(pos,KitchenAppliances.COOKING_CAULDRON.defaultBlockState());return (CookingCauldronBlockEntity)h.getLevel().getBlockEntity(pos);
}
private static void tick(GameTestHelper h,CookingCauldronBlockEntity pot,int count){for(int t=0;t<count;t++)CookingCauldronBlockEntity.serverTick(h.getLevel(),pot.getBlockPos(),pot.getBlockState(),pot);}
private static ItemEntity drop(GameTestHelper h,BlockPos pos,ItemStack stack){var e=new ItemEntity(h.getLevel(),pos.getX()+.5,pos.getY()+.5,pos.getZ()+.5,stack);h.getLevel().addFreshEntity(e);return e;}
private static int count(GameTestHelper h,BlockPos pos,Item item){return h.getLevel().getEntitiesOfClass(ItemEntity.class,new AABB(pos).inflate(2),e->e.isAlive()&&e.getItem().is(item)).stream().mapToInt(e->e.getItem().getCount()).sum();}
private static void milk(GameTestHelper h,CookingCauldronBlockEntity p,int count){for(int i=0;i<count;i++)h.assertTrue(p.insertIngredient(new ItemStack(Items.MILK_BUCKET),1)==1,"One legal milk bucket inserted");}
private static CookingCauldronBlockEntity reload(GameTestHelper h,CookingCauldronBlockEntity p,net.minecraft.nbt.CompoundTag tag){var copy=(CookingCauldronBlockEntity)BlockEntity.loadStatic(p.getBlockPos(),p.getBlockState(),tag,h.getLevel().registryAccess());copy.setLevel(h.getLevel());h.getLevel().setBlockEntity(copy);return copy;}
@GameTest public void droppedMilkWaitsForANativeStickGestureAndReturnsItsBucketOnlyAfterCooking(GameTestHelper h){
var pos=h.absolutePos(new BlockPos(2,6,2));h.getLevel().setBlockAndUpdate(pos.below(),Blocks.CAMPFIRE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos,Blocks.CAULDRON.defaultBlockState());
var e=drop(h,pos,new ItemStack(Items.MILK_BUCKET));CookingCauldronBlock.tryActivateFromDroppedItem(e);var p=(CookingCauldronBlockEntity)h.getLevel().getBlockEntity(pos);
h.assertTrue(e.isRemoved()&&p.waterUnits()==0&&p.getItem(0).is(Items.MILK_BUCKET),"Empty vanilla cauldron captures the unpaid milk bucket");tick(h,p,1400);
h.assertTrue(p.getItem(6).isEmpty()&&p.progress()==0&&count(h,pos,Items.BUCKET)==0,"No automatic recipe, failed dish or premature bucket");
var player=Progression003GameTests.player(h);player.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.STICK));p.getBlockState().useItemOn(player.getMainHandItem(),h.getLevel(),player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));tick(h,p,179);
h.assertTrue(p.assemblyComplete()&&p.getItem(0).is(Items.MILK_BUCKET)&&p.getItem(6).isEmpty()&&player.getMainHandItem().getCount()==1,"Native stirring preserves the stick and unpaid milk until the final tick");tick(h,p,1);
h.assertTrue(p.getItem(6).is(Cuisine.item("cream"))&&p.getItem(6).getCount()==2&&p.getItem(0).isEmpty()&&count(h,pos,Items.BUCKET)==1,"One full preparation pays one milk bucket and ejects exactly its bucket");h.succeed();
}
@GameTest public void incompleteMixturesNeverFailAndCanReturnTheirFullIngredients(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.insertIngredient(new ItemStack(Items.CARROT),1);tick(h,p,1800);
h.assertTrue(!p.stir()&&p.getItem(0).is(Items.MILK_BUCKET)&&p.getItem(1).is(Items.CARROT)&&p.getItem(6).isEmpty()&&count(h,p.getBlockPos(),Items.BUCKET)==0,"Invalid mixture has no destructive timeout or payment");
var player=Progression003GameTests.player(h);player.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);
for(int i=0;i<2;i++)h.getLevel().getBlockState(p.getBlockPos()).useWithoutItem(h.getLevel(),player,new BlockHitResult(Vec3.atCenterOf(p.getBlockPos()),Direction.UP,p.getBlockPos(),false));
h.assertTrue(player.getInventory().countItem(Items.MILK_BUCKET)==1&&player.getInventory().countItem(Items.CARROT)==1&&h.getLevel().getBlockState(p.getBlockPos()).is(Blocks.CAULDRON),"Cancellation returns full milk, raw ingredients and vanilla empty cauldron");h.succeed();
}
@GameTest public void allTwentyFourRecipesRequireStirringAndPayOnlyAtTheirWholeDeadline(GameTestHelper h){
int total=0;
for(var holder:h.getLevel().getServer().getRecipeManager().getRecipes())if(holder.value() instanceof CulinaryProcessRecipe r&&r.appliance()==CulinaryAppliance.COOKING_CAULDRON){
var p=pot(h);for(var ingredient:r.ingredients())h.assertTrue(p.insertIngredient(ingredient.display().resolveForFirstStack(SlotDisplayContext.fromLevel(h.getLevel())),1)==1,"Native ingredient accepted: "+holder.id());
tick(h,p,700);h.assertTrue(p.getItem(6).isEmpty()&&p.progress()==0,"Every recipe waits for the gesture: "+holder.id());h.assertTrue(p.stir(),"Valid manual recipe: "+holder.id());
tick(h,p,r.processingTime()-1);h.assertTrue(p.getItem(6).isEmpty(),"No early result: "+holder.id());tick(h,p,1);h.assertTrue(ItemStack.matches(p.getItem(6),r.result()),"Exact historical paid result: "+holder.id());total++;
}h.assertTrue(total==24,"All cauldron recipes exercised");h.succeed();
}
@GameTest public void separateMilkBucketsProduceAPaidBatchWithoutIllegalStacks(GameTestHelper h){
var p=pot(h);milk(h,p,3);for(int i=0;i<3;i++)h.assertTrue(p.getItem(i).getCount()==1,"Native milk size one");h.assertTrue(p.stir(),"Three identical buckets form one preparation");tick(h,p,180);
h.assertTrue(p.getItem(6).is(Cuisine.item("cream"))&&p.getItem(6).getCount()==6&&count(h,p.getBlockPos(),Items.BUCKET)==3,"All three actual buckets are paid exactly once");for(int i=0;i<7;i++)h.assertTrue(p.getItem(i).getCount()<=p.getItem(i).getMaxStackSize(),"Legal saved stacks");h.succeed();
}
@GameTest public void aFinishedRecipeDoesNotAutomaticallyCookAnotherRecipeFromItsRemainingWater(GameTestHelper h){
var p=pot(h);p.setWaterUnits(3);milk(h,p,1);h.assertTrue(p.stir(),"Milk can be prepared over existing water");tick(h,p,180);p.takeFinishedProduct();tick(h,p,1600);
h.assertTrue(p.getItem(6).isEmpty()&&p.waterUnits()==3&&p.repeatsRecipe()&&p.repeatedRecipe().getPath().equals("physical_cauldron_cream"),"The remaining water does not turn into salt on its own");h.assertTrue(p.stir(),"Another recipe needs another deliberate gesture");tick(h,p,200);h.assertTrue(p.getItem(6).is(Cuisine.item("salt"))&&p.getItem(6).getCount()==6&&p.waterUnits()==0,"Explicit next preparation pays actual water only");h.succeed();
}
@GameTest public void changingAnAlmostFinishedPreparationCancelsPaymentAndRequiresAnotherGesture(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,179);p.insertIngredient(new ItemStack(Items.CARROT),1);tick(h,p,400);
h.assertTrue(p.progress()==0&&!p.assemblyComplete()&&p.getItem(6).isEmpty()&&p.getItem(0).is(Items.MILK_BUCKET),"Adding an ingredient disarms and cancels unpaid work");p.removeItemNoUpdate(1);tick(h,p,400);
h.assertTrue(p.getItem(6).isEmpty()&&p.getItem(0).is(Items.MILK_BUCKET),"Restoring the old ingredients does not arm a new recipe");p.stir();tick(h,p,180);h.assertTrue(p.getItem(6).getCount()==2&&count(h,p.getBlockPos(),Items.BUCKET)==1,"A new complete paid cycle is required");h.succeed();
}
@GameTest public void waterChangesCancelCookingAndNativeWaterCannotCreateBottles(GameTestHelper h){
var p=pot(h);p.setWaterUnits(3);p.insertIngredient(new ItemStack(Cuisine.item("ground_coffee"),3),3);p.stir();tick(h,p,199);p.setWaterUnits(2);tick(h,p,300);
h.assertTrue(p.progress()==0&&p.getItem(0).getCount()==3&&p.getItem(6).isEmpty(),"Changing liquid disarms the incomplete cycle");p.stir();tick(h,p,200);
h.assertTrue(p.getItem(6).getCount()==2&&p.getItem(0).getCount()==1&&p.waterUnits()==0&&count(h,p.getBlockPos(),Items.GLASS_BOTTLE)==0,"Exactly two real water levels pay two coffees without minting bottles");h.succeed();
}
@GameTest public void coldCauldronsCannotStartAndAnExplicitHeatedCyclePausesWhenHeatIsRemoved(GameTestHelper h){
var p=pot(h);milk(h,p,1);h.getLevel().setBlockAndUpdate(p.getBlockPos().below(),Blocks.STONE.defaultBlockState());h.assertTrue(!p.stir(),"A cold gesture cannot start cooking");tick(h,p,500);
h.getLevel().setBlockAndUpdate(p.getBlockPos().below(),Blocks.CAMPFIRE.defaultBlockState());p.stir();tick(h,p,90);h.getLevel().setBlockAndUpdate(p.getBlockPos().below(),Blocks.STONE.defaultBlockState());tick(h,p,500);
h.assertTrue(p.progress()==90&&p.getItem(0).is(Items.MILK_BUCKET),"No cold progress or payment");h.getLevel().setBlockAndUpdate(p.getBlockPos().below(),Blocks.CAMPFIRE.defaultBlockState());tick(h,p,90);h.assertTrue(p.getItem(6).getCount()==2,"The armed preparation resumes its remaining heated ticks");h.succeed();
}
@GameTest public void manualCookingSurvivesNativeStorageWithoutOfflinePayment(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,179);var copy=reload(h,p,p.saveWithFullMetadata(h.getLevel().registryAccess()));
h.assertTrue(copy.assemblyComplete()&&copy.progress()==179&&copy.getItem(0).is(Items.MILK_BUCKET)&&copy.getItem(6).isEmpty(),"Storage preserves the manual authorization and unpaid work");tick(h,copy,1);h.assertTrue(copy.getItem(6).getCount()==2&&count(h,p.getBlockPos(),Items.BUCKET)==1,"The remaining tick pays exactly once");h.succeed();
}
@GameTest public void oldAutomaticProgressIsDisarmedWhileItsSevenSlotsAndWaterStayIntact(GameTestHelper h){
var p=pot(h);p.setWaterUnits(2);milk(h,p,1);p.stir();tick(h,p,179);var old=p.saveWithFullMetadata(h.getLevel().registryAccess());old.remove("CauldronPreparationVersion");var copy=reload(h,p,old);tick(h,copy,300);
h.assertTrue(!copy.assemblyComplete()&&copy.progress()==0&&copy.waterUnits()==2&&copy.getItem(0).is(Items.MILK_BUCKET)&&copy.getItem(6).isEmpty(),"Old automatic timers cannot authorize the new preparation");copy.stir();tick(h,copy,180);h.assertTrue(copy.getItem(6).getCount()==2,"Old ingredients remain usable through the new gesture");h.succeed();
}
@GameTest public void nearbyDroppedIngredientsDoNotActivateOrEnterTheBowl(GameTestHelper h){
var pos=h.absolutePos(new BlockPos(2,6,2));h.getLevel().setBlockAndUpdate(pos,Blocks.CAULDRON.defaultBlockState());var outside=new ItemEntity(h.getLevel(),pos.getX()+.05,pos.getY()+.5,pos.getZ()+.05,new ItemStack(Items.MILK_BUCKET));h.getLevel().addFreshEntity(outside);CookingCauldronBlock.tryActivateFromDroppedItem(outside);
h.assertTrue(h.getLevel().getBlockState(pos).is(Blocks.CAULDRON)&&outside.getItem().getCount()==1,"Items outside the opening are left alone");outside.discard();var stone=drop(h,pos,new ItemStack(Items.STONE));CookingCauldronBlock.tryActivateFromDroppedItem(stone);h.assertTrue(h.getLevel().getBlockState(pos).is(Blocks.CAULDRON)&&stone.getItem().getCount()==1,"An unrelated item is not a preparation");h.succeed();
}
@GameTest public void ejectedBucketsAreNeverAbsorbedAgainAndBreakingReturnsOnlyUnpaidItems(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,180);var bucket=h.getLevel().getEntitiesOfClass(ItemEntity.class,new AABB(p.getBlockPos()).inflate(2),e->e.getItem().is(Items.BUCKET)).getFirst();bucket.setPos(Vec3.atCenterOf(p.getBlockPos()));p.takeFinishedProduct();tick(h,p,500);
h.assertTrue(bucket.isAlive()&&bucket.getItem().getCount()==1&&p.isEmpty(),"The thrown empty bucket cannot become an ingredient");bucket.discard();
p=pot(h);milk(h,p,1);p.insertIngredient(new ItemStack(Items.CARROT),1);var pos=p.getBlockPos();h.getLevel().destroyBlock(pos,true);h.getLevel().destroyBlock(pos,true);
h.assertTrue(count(h,pos,Items.MILK_BUCKET)==1&&count(h,pos,Items.CARROT)==1&&count(h,pos,Items.BUCKET)==0&&count(h,pos,KitchenAppliances.FAILED_DISH)==0,"Breaking returns only unconsumed actual ingredients, once");h.succeed();
}
@GameTest public void nativeHopperInsertionStillWaitsForStirring(GameTestHelper h){
var p=pot(h);var pos=p.getBlockPos().above();var state=Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING,Direction.DOWN);h.getLevel().setBlockAndUpdate(pos,state);var hopper=(HopperBlockEntity)h.getLevel().getBlockEntity(pos);hopper.setItem(0,new ItemStack(Items.MILK_BUCKET));hopper.setItem(1,new ItemStack(Items.MILK_BUCKET));
for(int t=0;t<40;t++){HopperBlockEntity.pushItemsTick(h.getLevel(),pos,state,hopper);tick(h,p,1);}h.assertTrue(hopper.isEmpty()&&p.getItem(0).is(Items.MILK_BUCKET)&&p.getItem(1).is(Items.MILK_BUCKET)&&p.getItem(6).isEmpty()&&!p.assemblyComplete(),"Native hopper fills physical slots without authorizing cooking");p.stir();tick(h,p,180);h.assertTrue(p.getItem(6).getCount()==4,"The explicitly started hopper-fed batch pays both buckets");h.succeed();
}
}
@@ -0,0 +1,66 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.entity.CookingCauldronBlockEntity;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.Identifier;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
import net.minecraft.world.phys.*;
/** Continuous paid batches, native replenishment/serving, exact recipe lock and storage. */
public final class Cauldron312GameTests {
private static CookingCauldronBlockEntity pot(GameTestHelper h){var pos=h.absolutePos(new BlockPos(2,8,2));if(h.getLevel().getBlockEntity(pos) instanceof CookingCauldronBlockEntity old)old.clearContent();h.getLevel().removeBlock(pos,false);h.getLevel().setBlockAndUpdate(pos.below(),Blocks.CAMPFIRE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos,KitchenAppliances.COOKING_CAULDRON.defaultBlockState());return (CookingCauldronBlockEntity)h.getLevel().getBlockEntity(pos);}
private static void tick(GameTestHelper h,CookingCauldronBlockEntity p,int n){for(int t=0;t<n;t++)CookingCauldronBlockEntity.serverTick(h.getLevel(),p.getBlockPos(),p.getBlockState(),p);}
private static void milk(GameTestHelper h,CookingCauldronBlockEntity p,int n){for(int i=0;i<n;i++)h.assertTrue(p.insertIngredient(new ItemStack(Items.MILK_BUCKET),1)==1,"A real milk bucket enters the series");}
private static int buckets(GameTestHelper h,CookingCauldronBlockEntity p){return h.getLevel().getEntitiesOfClass(ItemEntity.class,new AABB(p.getBlockPos()).inflate(2),e->e.isAlive()&&e.getItem().is(Items.BUCKET)).stream().mapToInt(e->e.getItem().getCount()).sum();}
private static CookingCauldronBlockEntity reload(GameTestHelper h,CookingCauldronBlockEntity p,net.minecraft.nbt.CompoundTag tag){var c=(CookingCauldronBlockEntity)BlockEntity.loadStatic(p.getBlockPos(),p.getBlockState(),tag,h.getLevel().registryAccess());c.setLevel(h.getLevel());h.getLevel().setBlockEntity(c);return c;}
@GameTest public void oneStirRepeatsPaidBatchesWhileProductsAccumulate(GameTestHelper h){
var p=pot(h);milk(h,p,2);h.assertTrue(p.stir(),"One initial gesture");tick(h,p,180);h.assertTrue(p.getItem(6).getCount()==4&&buckets(h,p)==2,"First real batch");tick(h,p,20);milk(h,p,2);tick(h,p,179);
h.assertTrue(p.getItem(6).getCount()==4&&buckets(h,p)==2&&p.getItem(0).is(Items.MILK_BUCKET),"Second batch remains unpaid until its full deadline");tick(h,p,1);
h.assertTrue(p.getItem(6).is(Cuisine.item("cream"))&&p.getItem(6).getCount()==8&&buckets(h,p)==4&&p.repeatsRecipe(),"Same output accumulates; each batch pays its own milk and containers");h.succeed();
}
@GameTest public void servingDoesNotDestroyRecipeMemoryOrAnAlmostFinishedCycle(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,200);milk(h,p,1);tick(h,p,179);var player=Progression003GameTests.player(h);player.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);
h.getLevel().getBlockState(p.getBlockPos()).useWithoutItem(h.getLevel(),player,new BlockHitResult(Vec3.atCenterOf(p.getBlockPos()),Direction.UP,p.getBlockPos(),false));
h.assertTrue(h.getLevel().getBlockEntity(p.getBlockPos())==p&&p.repeatsRecipe()&&p.progress()==179&&player.getInventory().countItem(Cuisine.item("cream"))==2,"Native serving preserves the cauldron, its recipe and pending work");tick(h,p,1);h.assertTrue(p.getItem(6).getCount()==2&&buckets(h,p)==2,"Next dish completes without restarting");h.succeed();
}
@GameTest public void compatibleTopUpsDoNotResetTheCycleButDifferentIngredientsDo(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,179);milk(h,p,1);h.assertTrue(p.progress()==179&&p.repeatsRecipe(),"Same recipe replenishment preserves unpaid work");tick(h,p,1);h.assertTrue(p.getItem(6).getCount()==2&&buckets(h,p)==1,"New milk waits for its own complete cycle");tick(h,p,179);h.assertTrue(p.getItem(6).getCount()==2,"Queued milk cannot finish early");tick(h,p,1);h.assertTrue(p.getItem(6).getCount()==4&&buckets(h,p)==2,"The queued portion pays only after a second complete cycle");tick(h,p,20);milk(h,p,1);tick(h,p,179);p.insertIngredient(new ItemStack(Items.CARROT),1);
h.assertTrue(!p.repeatsRecipe()&&p.progress()==0&&p.getItem(0).is(Items.MILK_BUCKET),"A different preparation cancels unpaid work");h.succeed();
}
@GameTest public void fullOutputWaitsWithoutPaymentThenResumesAfterCollection(GameTestHelper h){
var p=pot(h);milk(h,p,6);p.stir();p.setItem(6,new ItemStack(Cuisine.item("cream"),60));tick(h,p,180);
h.assertTrue(p.getItem(6).getCount()==64&&buckets(h,p)==2&&p.getItem(2).is(Items.MILK_BUCKET),"Only room for two paid recipes");tick(h,p,500);h.assertTrue(p.repeatState()==CookingCauldronBlockEntity.OUTPUT_FULL&&buckets(h,p)==2,"Full output cannot consume the four remaining buckets");
p.takeFinishedProduct();tick(h,p,180);h.assertTrue(p.getItem(6).getCount()==8&&buckets(h,p)==6,"Collection resumes the retained recipe without another gesture");h.succeed();
}
@GameTest public void missingMilkNeverReplacesTheRememberedRecipeWithSalt(GameTestHelper h){
var p=pot(h);p.setWaterUnits(3);milk(h,p,1);p.stir();tick(h,p,200);p.takeFinishedProduct();tick(h,p,1000);
h.assertTrue(p.repeatState()==CookingCauldronBlockEntity.NEEDS_INPUT&&p.getItem(6).isEmpty()&&p.waterUnits()==3&&p.repeatedRecipe().getPath().equals("physical_cauldron_cream"),"Water cannot replace the exact remembered milk recipe");milk(h,p,1);tick(h,p,180);h.assertTrue(p.getItem(6).is(Cuisine.item("cream"))&&p.getItem(6).getCount()==2&&p.waterUnits()==3,"Same recipe resumes when milk arrives");h.succeed();
}
@GameTest public void refillingRecipeWaterPreservesWorkAndColdOnlyPauses(GameTestHelper h){
var p=pot(h);p.setWaterUnits(1);p.insertIngredient(new ItemStack(Cuisine.item("ground_coffee"),4),4);p.stir();tick(h,p,199);p.setWaterUnits(3);h.assertTrue(p.progress()==199&&p.repeatsRecipe(),"Refilling the same recipe's water does not restart");tick(h,p,1);h.assertTrue(p.getItem(6).getCount()==1&&p.getItem(0).getCount()==3&&p.waterUnits()==2,"Refilled water waits for the next full batch");tick(h,p,200);h.assertTrue(p.getItem(6).getCount()==3&&p.getItem(0).getCount()==1&&p.waterUnits()==0,"The next full cycle pays the two queued waters");p.setWaterUnits(1);tick(h,p,100);h.getLevel().setBlockAndUpdate(p.getBlockPos().below(),Blocks.STONE.defaultBlockState());tick(h,p,300);h.assertTrue(p.progress()==100&&p.repeatState()==CookingCauldronBlockEntity.NEEDS_HEAT,"Cold pause preserves the series");h.getLevel().setBlockAndUpdate(p.getBlockPos().below(),Blocks.CAMPFIRE.defaultBlockState());tick(h,p,100);h.assertTrue(p.getItem(6).getCount()==4,"Heat resumes the remaining full cycle");h.succeed();
}
@GameTest public void nativeHopperCanReplenishAfterTheFirstBatchWithoutAnotherStir(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,200);var pos=p.getBlockPos().above();var state=Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING,Direction.DOWN);h.getLevel().setBlockAndUpdate(pos,state);var hopper=(HopperBlockEntity)h.getLevel().getBlockEntity(pos);hopper.setItem(0,new ItemStack(Items.MILK_BUCKET));hopper.setItem(1,new ItemStack(Items.MILK_BUCKET));
for(int t=0;t<400;t++){HopperBlockEntity.pushItemsTick(h.getLevel(),pos,state,hopper);tick(h,p,1);}h.assertTrue(hopper.isEmpty()&&p.getItem(6).getCount()==6&&buckets(h,p)==3,"Native hopper input feeds the remembered recipe while old output remains");h.succeed();
}
@GameTest public void repetitionAndPendingWorkSurviveNativeStorage(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,200);milk(h,p,1);tick(h,p,179);milk(h,p,1);var c=reload(h,p,p.saveWithFullMetadata(h.getLevel().registryAccess()));h.assertTrue(c.repeatsRecipe()&&c.repeatedRecipe().equals(p.repeatedRecipe())&&c.progress()==179&&c.getItem(6).getCount()==2,"Native storage preserves one output and unpaid work for the next cycle");tick(h,c,1);h.assertTrue(c.getItem(6).getCount()==4&&buckets(h,c)==2,"Reload completes exactly the remaining paid tick, preserving its original quantity");tick(h,c,179);h.assertTrue(c.getItem(6).getCount()==4,"Queued milk still needs its full cycle after reload");tick(h,c,1);h.assertTrue(c.getItem(6).getCount()==6&&buckets(h,c)==3,"Queued native milk completes the following paid cycle");h.succeed();
}
@GameTest public void earlierManualSavesResumeOnlyTheirAuthorizedRecipe(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,179);var tag=p.saveWithFullMetadata(h.getLevel().registryAccess());tag.remove("CauldronRepeatRecipe");tag.remove("CauldronCycleBatches");var c=reload(h,p,tag);tick(h,c,1);h.assertTrue(c.getItem(6).getCount()==2&&c.repeatedRecipe().getPath().equals("physical_cauldron_cream"),"A real beta.311 manual authorization acquires its exact recipe");
c=pot(h);milk(h,c,1);tag=c.saveWithFullMetadata(h.getLevel().registryAccess());tag.remove("CauldronRepeatRecipe");tag.remove("CauldronCycleBatches");c=reload(h,c,tag);tick(h,c,500);h.assertTrue(!c.repeatsRecipe()&&c.getItem(6).isEmpty(),"Unstirred earlier saves cannot authorize themselves");h.succeed();
}
@GameTest public void missingSavedRecipeStopsWithoutConsumingAnything(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();var tag=p.saveWithFullMetadata(h.getLevel().registryAccess());tag.putString("CauldronRepeatRecipe","sanctuary:removed_recipe_312");var c=reload(h,p,tag);tick(h,c,300);h.assertTrue(!c.repeatsRecipe()&&c.getItem(0).is(Items.MILK_BUCKET)&&c.getItem(6).isEmpty(),"Missing exact IDs disarm instead of silently selecting another recipe");h.succeed();
}
@GameTest public void emptyHandShiftStopsTheSeriesAndRestoresAnEmptyVanillaCauldron(GameTestHelper h){
var p=pot(h);milk(h,p,1);p.stir();tick(h,p,200);p.takeFinishedProduct();var player=Progression003GameTests.player(h);player.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);player.setShiftKeyDown(true);h.getLevel().getBlockState(p.getBlockPos()).useWithoutItem(h.getLevel(),player,new BlockHitResult(Vec3.atCenterOf(p.getBlockPos()),Direction.UP,p.getBlockPos(),false));player.setShiftKeyDown(false);h.assertTrue(h.getLevel().getBlockState(p.getBlockPos()).is(Blocks.CAULDRON),"An explicit native stop returns the empty vanilla block");h.succeed();
}
}
@@ -0,0 +1,82 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.CulinaryAppliance;
import fr.koka.sanctuary.cuisine.culinary.block.entity.FermentationBarrelBlockEntity;
import fr.koka.sanctuary.cuisine.culinary.recipe.*;
import fr.koka.sanctuary.cuisine.recipe.WineMaturationRecipe;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.*;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
import net.minecraft.world.level.storage.ServerLevelData;
import net.minecraft.world.phys.*;
import java.util.*;
/** Real native cake interactions and deterministic wall-clock maturation boundaries. */
public final class CheeseWine314GameTests {
private static final long DAY=86_400_000L;
private static BlockPos pos(GameTestHelper h){return h.absolutePos(new BlockPos(2,3,2));}
private static FermentationBarrelBlockEntity barrel(GameTestHelper h){var p=pos(h);if(h.getLevel().getBlockEntity(p) instanceof FermentationBarrelBlockEntity b)b.clearContent();h.getLevel().removeBlock(p,false);h.getLevel().setBlockAndUpdate(p,KitchenAppliances.FERMENTATION_BARREL.defaultBlockState());return (FermentationBarrelBlockEntity)h.getLevel().getBlockEntity(p);}
private static BlockHitResult hit(BlockPos p){return new BlockHitResult(Vec3.atCenterOf(p),Direction.UP,p,false);}
private static FermentationBarrelBlockEntity reload(GameTestHelper h,FermentationBarrelBlockEntity b){var copy=(FermentationBarrelBlockEntity)BlockEntity.loadStatic(b.getBlockPos(),b.getBlockState(),b.saveWithFullMetadata(h.getLevel().registryAccess()),h.getLevel().registryAccess());copy.setLevel(h.getLevel());return copy;}
private static int drops(GameTestHelper h,Item item){return h.getLevel().getEntitiesOfClass(ItemEntity.class,new AABB(pos(h)).inflate(2),e->e.isAlive()&&e.getItem().is(item)).stream().mapToInt(e->e.getItem().getCount()).sum();}
@GameTest public void existingCheeseItemPlacesAndPaysOneWholeWheel(GameTestHelper h){
var p=Progression003GameTests.player(h);p.getInventory().clearContent();p.setGameMode(net.minecraft.world.level.GameType.SURVIVAL);var pos=pos(h);h.getLevel().setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());h.getLevel().removeBlock(pos,false);p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Cuisine.item("cheese"),2));
var result=p.getMainHandItem().useOn(new UseOnContext(p,InteractionHand.MAIN_HAND,hit(pos.below())));
h.assertTrue(result.consumesAction()&&h.getLevel().getBlockState(pos).getBlock() instanceof fr.koka.sanctuary.cuisine.culinary.block.CheeseBlock&&p.getMainHandItem().getCount()==1,"The historic cheese ID places one real paid wheel");
h.assertTrue(h.getLevel().getBlockState(pos).getValue(CakeBlock.BITES)==0&&new ItemStack(Cuisine.item("cheese")).get(DataComponents.FOOD)==null,"A whole wheel starts full and is shared after placing, like cake");h.succeed();
}
@GameTest public void sevenRealSlicesShrinkAndFeedExactlyOnceThenRemoveTheWheel(GameTestHelper h){
var p=Progression003GameTests.player(h);var pos=pos(h);var block=((BlockItem)Cuisine.item("cheese")).getBlock();h.getLevel().setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos,block.defaultBlockState());p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);
for(int i=0;i<7;i++){p.getFoodData().setFoodLevel(0);var state=h.getLevel().getBlockState(pos);h.assertTrue(state.getValue(CakeBlock.BITES)==i&&Math.abs(state.getShape(h.getLevel(),pos).bounds().getXsize()-(14-i*2)/16D)<0.0001,"Visible/collision width follows the remaining native slices");h.assertTrue(state.useWithoutItem(h.getLevel(),p,hit(pos)).consumesAction()&&p.getFoodData().getFoodLevel()==2,"One real cake action restores two food points");}
h.assertTrue(h.getLevel().getBlockState(pos).isAir(),"Only the seventh bite removes the wheel");h.succeed();
}
@GameTest public void fullHungerNeverConsumesACheeseSliceAndCandlesNeverCreateCake(GameTestHelper h){
var p=Progression003GameTests.player(h);var pos=pos(h);var block=((BlockItem)Cuisine.item("cheese")).getBlock();h.getLevel().setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos,block.defaultBlockState());p.getFoodData().setFoodLevel(20);
var before=h.getLevel().getBlockState(pos);before.useWithoutItem(h.getLevel(),p,hit(pos));h.assertTrue(h.getLevel().getBlockState(pos).getValue(CakeBlock.BITES)==0,"A full player does not eat a slice");var candle=new ItemStack(Items.CANDLE);before.useItemOn(candle,h.getLevel(),p,InteractionHand.MAIN_HAND,hit(pos));h.assertTrue(h.getLevel().getBlockState(pos).is(block)&&candle.getCount()==1,"Native candle cake shortcut cannot transform cheese into free cake");h.succeed();
}
@GameTest public void cheeseBreakageOrLostSupportCannotRegenerateEatenParts(GameTestHelper h){
var pos=pos(h);var block=((BlockItem)Cuisine.item("cheese")).getBlock();h.getLevel().setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos,block.defaultBlockState().setValue(CakeBlock.BITES,3));h.getLevel().destroyBlock(pos,true);h.assertTrue(drops(h,Cuisine.item("cheese"))==0,"Breaking a half-eaten wheel cannot refund a whole one");h.getLevel().setBlockAndUpdate(pos,block.defaultBlockState());h.getLevel().destroyBlock(pos.below(),true);h.assertTrue(h.getLevel().getBlockState(pos).isAir()&&drops(h,Cuisine.item("cheese"))==0,"Like cake, lost support removes cheese without a whole-wheel refund");h.succeed();
}
@GameTest public void sameMustStillFermentsIntoYoungWineAfterExactlyThreeGameMinutes(GameTestHelper h){
var b=barrel(h);b.insertIngredient(new ItemStack(Cuisine.item("grape_must")),1);FermentationBarrelBlockEntity.serverTick(h.getLevel(),pos(h),b.getBlockState(),b);var d=(ServerLevelData)h.getLevel().getLevelData();long now=d.getGameTime();try{d.setGameTime(b.startedAtGameTime()+3599);FermentationBarrelBlockEntity.serverTick(h.getLevel(),pos(h),b.getBlockState(),b);h.assertTrue(!b.hasFinishedProduct(),"No premature young wine");d.setGameTime(b.startedAtGameTime()+3600);FermentationBarrelBlockEntity.serverTick(h.getLevel(),pos(h),b.getBlockState(),b);h.assertTrue(b.getItem(4).is(Cuisine.item("wine"))&&b.getItem(0).isEmpty()&&b.wineStartedAtEpochMillis()>0,"One paid young wine starts a real UTC maturation clock");}finally{d.setGameTime(now);}h.succeed();
}
@GameTest public void exactOneThreeAndSevenRealDayBoundariesUpgradeOneBottleWithoutDuplication(GameTestHelper h){
var b=barrel(h);b.setItem(4,new ItemStack(Cuisine.item("wine")));long t=System.currentTimeMillis();b.refreshWineAge(t);Item[] items={Cuisine.item("wine"),KitchenAppliances.TABLE_WINE,KitchenAppliances.RESERVE_WINE,KitchenAppliances.GRAND_CRU_WINE};long[] deadlines={DAY,3*DAY,7*DAY};for(int i=0;i<3;i++){b.refreshWineAge(t+deadlines[i]-1);h.assertTrue(b.getItem(4).is(items[i])&&b.remainingWineAgeMillis()==1,"One millisecond before real deadline retains the previous quality");b.refreshWineAge(t+deadlines[i]);h.assertTrue(b.getItem(4).is(items[i+1])&&b.getItem(4).getCount()==1&&b.wineStartedAtEpochMillis()==t,"Exact absolute day threshold changes only the one paid bottle");}b.refreshWineAge(t+100*DAY);h.assertTrue(b.getItem(4).is(KitchenAppliances.GRAND_CRU_WINE)&&b.getItem(4).getCount()==1&&!b.hasMaturingWine(),"Grand cru is stable and never duplicates");h.succeed();
}
@GameTest public void nativeDiskFieldsCatchUpSevenRealDaysAndClockRollbackNeverDegradesWine(GameTestHelper h){
var b=barrel(h);b.setItem(4,new ItemStack(Cuisine.item("wine")));long t=System.currentTimeMillis();b.refreshWineAge(t);var c=reload(h,b);h.assertTrue(c.wineStartedAtEpochMillis()==t&&c.getItem(4).is(Cuisine.item("wine")),"Native item and UTC origin reload exactly");c.refreshWineAge(t+7*DAY);h.assertTrue(c.getItem(4).is(KitchenAppliances.GRAND_CRU_WINE)&&c.getItem(4).getCount()==1,"One tick catches up a closed server without ticking seven days");c.refreshWineAge(t-1000);h.assertTrue(c.getItem(4).is(KitchenAppliances.GRAND_CRU_WINE),"A backwards host clock never downgrades a paid quality");h.succeed();
}
@GameTest public void historicalWineStartsFreshAndMinecraftTimeCannotAgeIt(GameTestHelper h){
var b=barrel(h);b.setItem(4,new ItemStack(Cuisine.item("wine")));var c=reload(h,b);h.assertTrue(c.wineStartedAtEpochMillis()<0,"Historical output has no invented date");long t=System.currentTimeMillis();c.refreshWineAge(t);var d=(ServerLevelData)h.getLevel().getLevelData();long now=d.getGameTime();try{d.setGameTime(now+100000000);FermentationBarrelBlockEntity.serverTick(h.getLevel(),pos(h),c.getBlockState(),c);h.assertTrue(c.getItem(4).is(Cuisine.item("wine"))&&c.wineStartedAtEpochMillis()==t,"Sleeping and Minecraft time commands cannot advance real maturation");}finally{d.setGameTime(now);}h.succeed();
}
@GameTest public void extractionClearsAgeAndANewBottleMustWaitItsOwnDays(GameTestHelper h){
var b=barrel(h);long t=System.currentTimeMillis();b.setItem(4,new ItemStack(Cuisine.item("wine")));b.refreshWineAge(t);b.refreshWineAge(t+3*DAY);var taken=b.takeOutputOrLastIngredient();h.assertTrue(taken.is(KitchenAppliances.RESERVE_WINE)&&b.wineStartedAtEpochMillis()<0&&b.isEmpty(),"Taking reserve wine gives exactly the current quality and releases the date");b.setItem(4,new ItemStack(Cuisine.item("wine")));b.refreshWineAge(t+3*DAY);h.assertTrue(b.getItem(4).is(Cuisine.item("wine"))&&b.remainingWineAgeMillis()==DAY,"The next bottle inherits no free age");h.succeed();
}
@GameTest public void breakageDropsExactlyTheAgedBottleAndOneBarrel(GameTestHelper h){
var b=barrel(h);long t=System.currentTimeMillis();b.setItem(4,new ItemStack(Cuisine.item("wine")));b.refreshWineAge(t);b.refreshWineAge(t+7*DAY);h.getLevel().destroyBlock(pos(h),true);h.assertTrue(drops(h,KitchenAppliances.GRAND_CRU_WINE)==1&&drops(h,Cuisine.item("wine"))==0&&drops(h,KitchenAppliances.FERMENTATION_BARREL.asItem())==1,"Breakage returns one actual grand cru and one barrel, without a second young bottle");h.succeed();
}
@GameTest public void allFourQualitiesWorkForVinegarAndImprovedWineKeepsNativeDrinking(GameTestHelper h){
var recipe=(CulinaryProcessRecipe)Cuisine303GameTests.recipe(h,"sanctuary:ferment_vinegar");float saturation=0;for(var item:List.of(Cuisine.item("wine"),KitchenAppliances.TABLE_WINE,KitchenAppliances.RESERVE_WINE,KitchenAppliances.GRAND_CRU_WINE)){var stack=new ItemStack(item);h.assertTrue(recipe.matches(new CulinaryRecipeInput(CulinaryAppliance.FERMENTATION_BARREL,List.of(stack)),h.getLevel()),"No quality is locked out of the historical vinegar recipe");var food=stack.get(DataComponents.FOOD);h.assertTrue(food.nutrition()==6&&food.saturation()>saturation&&stack.get(DataComponents.CONSUMABLE).animation()==net.minecraft.world.item.ItemUseAnimation.DRINK,"All four are native drinks, with increasing saturation and stable nutrition");saturation=food.saturation();}h.succeed();
}
@GameTest public void threeSynchronizedStagesUseRealSecondsAndNeverCompeteWithCrafting(GameTestHelper h){
for(var entry:Map.of("table_wine",1,"reserve_wine",3,"grand_cru_wine",7).entrySet()){var r=(WineMaturationRecipe)Cuisine303GameTests.recipe(h,"sanctuary:age_"+entry.getKey());h.assertTrue(r.requiredAgeSeconds()==entry.getValue()*86400&&r.getType()==WineMaturationRecipe.TYPE&&r.isSpecial(),"Each recipe describes total real seconds since young wine, never a manual crafting shortcut");}h.succeed();
}
@GameTest public void nativeHopperTakesYoungWineAndClearsOnlyItsOwnAge(GameTestHelper h){
var b=barrel(h);h.getLevel().setBlockAndUpdate(pos(h).below(),Blocks.HOPPER.defaultBlockState());b.setItem(4,new ItemStack(Cuisine.item("wine")));b.refreshWineAge(System.currentTimeMillis());
h.runAfterDelay(20,()->{var hopper=(HopperBlockEntity)h.getLevel().getBlockEntity(pos(h).below());h.assertTrue(b.isEmpty()&&b.wineStartedAtEpochMillis()<0&&hopper.countItem(Cuisine.item("wine"))==1,"A real lower hopper collects exactly the young bottle and clears its origin");long now=System.currentTimeMillis();b.setItem(4,new ItemStack(Cuisine.item("wine")));b.refreshWineAge(now);h.assertTrue(b.wineStartedAtEpochMillis()==now&&b.remainingWineAgeMillis()==DAY,"A new bottle gets its own full real day after automated extraction");h.succeed();});
}
@GameTest public void discoveringMustTeachesAllThreeMaturationStages(GameTestHelper h){
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());p.setAttached(fr.koka.sanctuary.progression.ProgressionService.RECORD,fr.koka.sanctuary.progression.ProgressRecord.INITIAL.json());fr.koka.sanctuary.progression.ProgressEffects.apply(p,fr.koka.sanctuary.progression.ProgressRecord.INITIAL);
fr.koka.sanctuary.recipes.RecipeService.discover(p,Cuisine.item("grape_must"));fr.koka.sanctuary.recipes.RecipeService.refresh(p);for(String stage:List.of("table_wine","reserve_wine","grand_cru_wine"))h.assertTrue(fr.koka.sanctuary.recipes.RecipeService.knowledge(p).recipes.contains("sanctuary:age_"+stage),"The real ageing stages are discoverable from the actual must");h.succeed();
}
}
@@ -0,0 +1,122 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.machine.*;
import fr.koka.sanctuary.multiblock.*;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.registries.*;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.*;
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.phys.AABB;
import net.minecraft.world.phys.shapes.CollisionContext;
import static fr.koka.sanctuary.cuisine.machine.CrushableProduceBlock.*;
public final class Crushing305GameTests {
private static List<Block> fruits(){return List.of(KitchenAppliances.TOMATO_BLOCK,KitchenAppliances.GRAPE_BLOCK,KitchenAppliances.OLIVE_BLOCK);}
private static Item result(Block b){return b==KitchenAppliances.TOMATO_BLOCK?Cuisine.item("tomato_pulp"):b==KitchenAppliances.GRAPE_BLOCK?Cuisine.item("grape_must"):Cuisine.item("olive_oil");}
private static Item raw(Block b){return Cuisine303GameTests.item(b==KitchenAppliances.TOMATO_BLOCK?"sanctuary:tomato":b==KitchenAppliances.GRAPE_BLOCK?"sanctuary:grapes":"sanctuary:olives");}
private static BlockPos at(GameTestHelper h,int n){return h.absolutePos(new BlockPos(2+n*4,4,2));}
private static int count(GameTestHelper h,BlockPos pos,Item item){return h.getLevel().getEntitiesOfClass(ItemEntity.class,new AABB(pos).inflate(2),e->e.isAlive()&&e.getItem().is(item)).stream().mapToInt(e->e.getItem().getCount()).sum();}
@GameTest public void eightRealLandingCallbacksReleaseExactlyNinePortionsForEveryFruit(GameTestHelper h){
int n=0;var player=Progression003GameTests.player(h);
for(Block block:fruits()){
var pos=at(h,n++);var l=h.getLevel();l.setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(pos,block.defaultBlockState());
for(int jump=1;jump<=8;jump++){
var state=l.getBlockState(pos);block.fallOn(l,state,pos,player,1.1D);
h.assertTrue(count(h,pos,result(block))==jump*9/8,"Incremental paid residue: "+block+" jump="+jump);
if(jump<8){var next=l.getBlockState(pos);h.assertTrue(next.getValue(LAYERS)==8-jump,"One layer per landing");
h.assertTrue(Math.abs(next.getCollisionShape(l,pos).max(Direction.Axis.Y)-(8-jump)/8D)<.00001,"Physical height matches visual layer");
}else h.assertTrue(l.getBlockState(pos).isAir(),"Final layer disappears");
}
}h.succeed();
}
@GameTest public void walkingItemsAndFlatteningDropDoNotCrushAdditionalLayers(GameTestHelper h){
var l=h.getLevel();var pos=at(h,0);var block=KitchenAppliances.TOMATO_BLOCK;var state=block.defaultBlockState();l.setBlockAndUpdate(pos,state);
var player=Progression003GameTests.player(h);block.stepOn(l,pos,state,player);block.fallOn(l,state,pos,player,.125D);
block.fallOn(l,state,pos,new ItemEntity(l,0,0,0,new ItemStack(Items.STONE)),3D);
h.assertTrue(l.getBlockState(pos).getValue(LAYERS)==8 && count(h,pos,result(block))==0,"No passive crushing, item entities or feedback from shrinking");h.succeed();
}
@GameTest public void breakingAndDecompactingCannotRecoverAlreadyCrushedFruit(GameTestHelper h){
var l=h.getLevel();int n=0;
for(Block block:fruits())for(int layers=1;layers<=8;layers++){
var pos=at(h,0);var state=block.defaultBlockState().setValue(LAYERS,layers);l.setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(pos,state);
var loot=Block.getDrops(state,l,pos,null);int left=remaining(layers,9);
h.assertTrue(loot.size()==1 && loot.getFirst().is(layers==8?block.asItem():raw(block)) && loot.getFirst().getCount()==(layers==8?1:left),"Breakage conserves remaining raw yield: "+state);
h.assertTrue(layers==8 || loot.stream().noneMatch(s->s.is(block.asItem())),"No partial state drops a new full block");
var round=NbtUtils.readBlockState(l.holderLookup(Registries.BLOCK),NbtUtils.writeBlockState(state));h.assertTrue(round.equals(state),"All eight states persist natively");
}h.succeed();
}
@GameTest public void oldFruitStateWithoutLayersLoadsAsAFullBlock(GameTestHelper h){
for(Block block:fruits()){
var old=NbtUtils.writeBlockState(block.defaultBlockState());old.remove("Properties");
var loaded=NbtUtils.readBlockState(h.getLevel().holderLookup(Registries.BLOCK),old);
h.assertTrue(loaded.is(block) && loaded.getValue(LAYERS)==8,"Existing IDs retain full yield without rewriting saves");
}h.succeed();
}
@GameTest public void ordinaryPistonCrushesRemainingYieldInAllSixDirections(GameTestHelper h){
var l=h.getLevel();var fixtures=new ArrayList<BlockPos>();var outputs=new ArrayList<Item>();int n=0;
for(Direction facing:Direction.values())for(Block block:fruits()){
var base=h.absolutePos(new BlockPos(2+n%3*5,10+n/3*6,2));n++;var fruit=base.relative(facing);fixtures.add(fruit);outputs.add(result(block));
l.setBlock(fruit.below(),Blocks.STONE.defaultBlockState(),2);
l.setBlock(base,Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING,facing),2);
l.setBlock(fruit,block.defaultBlockState().setValue(LAYERS,4),2);
l.setBlockAndUpdate(base.relative(facing.getOpposite()),Blocks.REDSTONE_BLOCK.defaultBlockState());
}
h.runAfterDelay(4,()->{for(int i=0;i<fixtures.size();i++){
var pos=fixtures.get(i);h.assertTrue(count(h,pos,outputs.get(i))==remaining(4,9),"Real native piston pays remaining five portions, orientation fixture "+i);
h.assertTrue(!(l.getBlockState(pos).getBlock() instanceof CrushableProduceBlock),"Native piston replaces crushed fruit");
}h.succeed();});
}
@GameTest(maxTicks=80) public void superPistonCrushesTwentySevenMixedBlocksPerFullActivation(GameTestHelper h){
var l=h.getLevel();var center=h.absolutePos(new BlockPos(5,128,5));var f=Direction.UP;
for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)l.setBlockAndUpdate(center.offset(x,-2,z),Blocks.STONE.defaultBlockState());
for(int i=0;i<9;i++)l.setBlockAndUpdate(SuperPistons.tile(center,f,i),Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING,f));
h.assertTrue(SuperPistons.assemble(l,center,f,false),"Assemble actual 3x3 super piston");
for(int depth=1;depth<=3;depth++)for(int i=0;i<9;i++)l.setBlockAndUpdate(SuperPistons.tile(center,f,i).relative(f,depth),fruits().get(i%3).defaultBlockState());
l.setBlockAndUpdate(SuperPistons.tile(center,f,0).below(),Blocks.REDSTONE_BLOCK.defaultBlockState());
h.runAfterDelay(40,()->{
var controller=(SuperPistonController)l.getBlockEntity(center);h.assertTrue(controller.stroke==3,"Full powered course reaches third depth");
for(int depth=1;depth<=3;depth++)for(var pos:SuperPistons.tiles(center,f,depth))h.assertTrue(!(l.getBlockState(pos).getBlock() instanceof CrushableProduceBlock),"All 27 fruit blocks crushed");
var box=new AABB(center).inflate(64,80,64);for(Block block:fruits()){
int portions=l.getEntitiesOfClass(ItemEntity.class,box,e->e.isAlive()&&e.getItem().is(result(block))).stream().mapToInt(e->e.getItem().getCount()).sum();
h.assertTrue(portions==81,"Nine blocks of each fruit pay exactly 81 portions: "+block+" = "+portions+" items="+l.getEntitiesOfClass(ItemEntity.class,box,e->e.isAlive()&&e.getItem().is(result(block))).stream().map(e->e.getItem()+"@"+e.position()).toList());
}h.succeed();
});
}
@GameTest public void blockedSuperPistonNeverCrushesOnlyPartOfItsFace(GameTestHelper h){
var l=h.getLevel();var center=h.absolutePos(new BlockPos(5,8,5));var f=Direction.UP;
for(int i=0;i<9;i++)l.setBlockAndUpdate(SuperPistons.tile(center,f,i),Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING,f));
h.assertTrue(SuperPistons.assemble(l,center,f,true)==false,"Mixed sticky assembly is refused");
h.assertTrue(SuperPistons.assemble(l,center,f,false),"Normal assembly");
for(int i=0;i<9;i++)l.setBlockAndUpdate(SuperPistons.tile(center,f,i).above(),(i==0?Blocks.OBSIDIAN:KitchenAppliances.TOMATO_BLOCK).defaultBlockState());
var controller=(SuperPistonController)l.getBlockEntity(center);
h.assertTrue(!SuperPistons.step(l,center,l.getBlockState(center),controller,true),"Obsidian blocks the entire atomic move");
for(int i=1;i<9;i++)h.assertTrue(l.getBlockState(SuperPistons.tile(center,f,i).above()).is(KitchenAppliances.TOMATO_BLOCK),"No fruit changed on failed move");
h.assertTrue(l.getEntitiesOfClass(ItemEntity.class,new AABB(center).inflate(7),e->e.getItem().is(Cuisine.item("tomato_pulp"))).isEmpty(),"No early residues");h.succeed();
}
@GameTest public void removedPressHasNoRecipeCreativeEntryOrProcessingTicker(GameTestHelper h){
h.assertTrue(h.getLevel().getServer().getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,fr.koka.sanctuary.SanctuaryMod.id("food_press"))).isEmpty(),"No press crafting");
h.assertTrue(KitchenAppliances.items().stream().noneMatch(i->i==KitchenAppliances.FOOD_PRESS.asItem()),"No press creative entry");
var pos=at(h,0);var state=KitchenAppliances.FOOD_PRESS.defaultBlockState();
h.assertTrue(state.getTicker(h.getLevel(),KitchenAppliances.FOOD_PRESS_BLOCK_ENTITY)==null,"Legacy ID has no processor");
h.getLevel().setBlockAndUpdate(pos,state);var legacy=(FoodPressBlockEntity)h.getLevel().getBlockEntity(pos);legacy.setItem(2,new ItemStack(Items.BOWL,7));
var copy=BlockEntity.loadStatic(pos,state,legacy.saveWithFullMetadata(h.getLevel().registryAccess()),h.getLevel().registryAccess());
h.assertTrue(((FoodPressBlockEntity)copy).getItem(2).getCount()==7,"Paid legacy inventory remains recoverable without world conversion");h.succeed();
}
}
@@ -0,0 +1,191 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.*;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.agriculture.AgricultureItems;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.recipes.*;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.*;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.*;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.world.effect.*;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.item.alchemy.*;
import net.minecraft.world.item.crafting.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.*;
public final class Cuisine303GameTests {
@GameTest public void allHistoricalFoodRecipesLoadAndProduceTheirReviewedOutputs(GameTestHelper h) {
int ports = 0;
for (var element : Cuisine.CATALOG.getAsJsonArray("ports")) {
var port = element.getAsJsonObject(); var ingredients = samples(port.getAsJsonArray("ingredients"));
var input = grid(ingredients); var ids = port.getAsJsonArray("recipes");
if (port.get("native_equivalent").getAsBoolean()) {
var shadow = ResourceKey.create(Registries.RECIPE, SanctuaryMod.id(port.get("id").getAsString()));
h.assertTrue(h.getLevel().getServer().getRecipeManager().byKey(shadow).isEmpty(), "Existing vanilla meat/fish/potato cooking has no competing port");
}
for (int i = 0; i < ids.size(); i++) {
var id = ids.get(i).getAsString(); var recipe = recipe(h, id);
ItemStack output;
if (recipe instanceof fr.koka.sanctuary.cuisine.culinary.recipe.CulinaryProcessRecipe physical) {
var processInput = new fr.koka.sanctuary.cuisine.culinary.recipe.CulinaryRecipeInput(physical.appliance(), ingredients);
h.assertTrue(physical.matches(processInput, h.getLevel()), "Physical fermentation matches " + id);
output = physical.assemble(processInput);
} else if (recipe instanceof CraftingRecipe crafting) {
h.assertTrue(crafting.matches(input, h.getLevel()), "Actual native crafting matches " + id);
output = crafting.assemble(input);
if (port.get("hot").getAsBoolean()) {
h.assertTrue(output.is(Cuisine.item("raw_" + port.get("id").getAsString())) && output.getCount() == 1, "One batch preparation: " + id);
continue;
}
} else if (recipe instanceof fr.koka.sanctuary.cuisine.recipe.FoodPressingRecipe pressing) {
output = pressing.assemble(new SingleRecipeInput(ingredients.getFirst()));
} else {
var cooking = (AbstractCookingRecipe) recipe;
var previous = ids.size() == 3 ? ((CraftingRecipe) recipe(h, ids.get(0).getAsString())).assemble(input) : ingredients.getFirst();
var single = new SingleRecipeInput(previous);
h.assertTrue(cooking.matches(single, h.getLevel()), "Native cooking matches " + id);
output = cooking.assemble(single);
}
h.assertTrue(output.is(item(port.get("result").getAsString())) && output.getCount() == port.get("count").getAsInt(), "Historical result/count retained: " + id);
}
ports++;
}
h.assertTrue(ports == 92, "Complete 92-recipe food catalogue"); h.succeed();
}
@GameTest public void nativeCraftingHasNoConflictingFoodOutputs(GameTestHelper h) {
for (var element : Cuisine.CATALOG.getAsJsonArray("ports")) {
var port = element.getAsJsonObject(); var input = grid(samples(port.getAsJsonArray("ingredients")));
var outputs = new HashSet<String>();
for (var holder : h.getLevel().getServer().getRecipeManager().getRecipes()) {
if (holder.value() instanceof CraftingRecipe crafting && crafting.matches(input, h.getLevel()))
outputs.add(BuiltInRegistries.ITEM.getKey(crafting.assemble(input).getItem()).toString());
}
h.assertTrue(outputs.size() <= 1, "Unambiguous table output for " + port.get("id") + ": " + outputs);
}
h.succeed();
}
@GameTest public void allCulinaryIngredientsHaveASurvivalProductionPath(GameTestHelper h) {
var reachable = new HashSet<String>();
for (var i : BuiltInRegistries.ITEM) if (BuiltInRegistries.ITEM.getKey(i).getNamespace().equals("minecraft")) reachable.add(BuiltInRegistries.ITEM.getKey(i).toString());
for (var i : AgricultureItems.PRODUCE_ITEMS) reachable.add(BuiltInRegistries.ITEM.getKey(i).toString());
for (var block : List.of(KitchenAppliances.TOMATO_BLOCK, KitchenAppliances.GRAPE_BLOCK, KitchenAppliances.OLIVE_BLOCK)) {
var compressed = h.getLevel().getServer().getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE, BuiltInRegistries.BLOCK.getKey(block))).orElseThrow().value();
h.assertTrue(compressed instanceof CraftingRecipe, "Compressed fruit is crafted from reachable produce");
for (var ingredient : compressed.placementInfo().ingredients()) h.assertTrue(reachable.contains(BuiltInRegistries.ITEM.getKey(ingredient.display().resolveForFirstStack(net.minecraft.world.item.crafting.display.SlotDisplayContext.fromLevel(h.getLevel())).getItem()).toString()), "Ingredient resolves");
reachable.add(BuiltInRegistries.BLOCK.getKey(block).toString());
}
for (int pass = 0; pass < 92; pass++) for (var e : Cuisine.CATALOG.getAsJsonArray("ports")) {
var p = e.getAsJsonObject(); boolean possible = true;
for (var ingredient : p.getAsJsonArray("ingredients")) {
String id = ingredient.getAsString();
if (id.startsWith("#")) possible &= BuiltInRegistries.ITEM.getOrThrow(TagKey.create(Registries.ITEM, Identifier.parse(id.substring(1))))
.stream().anyMatch(v -> reachable.contains(BuiltInRegistries.ITEM.getKey(v.value()).toString()));
else possible &= reachable.contains(id);
}
if (possible) reachable.add(p.get("result").getAsString());
}
for (var e : Cuisine.CATALOG.getAsJsonArray("items")) h.assertTrue(reachable.contains("sanctuary:" + e.getAsJsonObject().get("id").getAsString()), "No orphan food ingredient: " + e);
h.succeed();
}
@GameTest public void cookingUsesPlainWaterAndRefusesPotionsWithoutConsumingAnything(GameTestHelper h) {
var recipe = (CraftingRecipe) recipe(h, "sanctuary:prepare_cauldron_coffee");
var water = PotionContents.createItemStack(Items.POTION, Potions.WATER);
h.assertTrue(recipe.matches(grid(List.of(new ItemStack(Cuisine.item("ground_coffee")), water)), h.getLevel()), "Plain water accepted");
for (var potion : List.of(Potions.POISON, Potions.HEALING, Potions.AWKWARD)) {
var stack = PotionContents.createItemStack(Items.POTION, potion);
h.assertTrue(!recipe.matches(grid(List.of(new ItemStack(Cuisine.item("ground_coffee")), stack)), h.getLevel()) && stack.getCount() == 1, "Non-water potion refused intact");
}
water.set(DataComponents.POTION_CONTENTS, new PotionContents(Potions.WATER).withEffectAdded(new MobEffectInstance(MobEffects.POISON, 200)));
h.assertTrue(!recipe.matches(grid(List.of(new ItemStack(Cuisine.item("ground_coffee")), water)), h.getLevel()), "Custom potion effects cannot enter food"); h.succeed();
}
@GameTest public void nativeCraftingReturnsWaterHoneyMilkAndPineappleRemaindersOnce(GameTestHelper h) {
for (String id : List.of("prepare_cauldron_lemon_honey_tea", "prepare_cauldron_cream", "cold_fruit_salad")) {
var p = portForCraft(id); var input = grid(samples(p.getAsJsonArray("ingredients")));
var remaining = ((CraftingRecipe) recipe(h, "sanctuary:" + id)).getRemainingItems(input);
for (int i = 0; i < input.size(); i++) {
var stack = input.getItem(i);
if (stack.is(Items.POTION) || stack.is(Items.HONEY_BOTTLE)) h.assertTrue(remaining.get(i).is(Items.GLASS_BOTTLE) && remaining.get(i).getCount() == 1, "One original bottle returned");
if (stack.is(Items.MILK_BUCKET)) h.assertTrue(remaining.get(i).is(Items.BUCKET) && remaining.get(i).getCount() == 1, "One original milk bucket returned");
if (stack.is(AgricultureItems.PINEAPPLE)) h.assertTrue(remaining.get(i).is(AgricultureItems.PINEAPPLE_CROWN) && remaining.get(i).getCount() == 1, "One pineapple crown returned");
}
}
h.succeed();
}
@GameTest public void shiftedCraftingConsumesOnlyPaidBatchesAndKeepsTheBottle(GameTestHelper h) {
var p = Progression003GameTests.player(h); p.getInventory().clearContent();
var table = h.absolutePos(new BlockPos(2, 3, 2)); h.getLevel().setBlockAndUpdate(table, Blocks.CRAFTING_TABLE.defaultBlockState());
var menu = new CraftingMenu(1, p.getInventory(), ContainerLevelAccess.create(h.getLevel(), table));
menu.getSlot(1).set(new ItemStack(Cuisine.item("ground_coffee"), 8)); menu.getSlot(2).set(PotionContents.createItemStack(Items.POTION, Potions.WATER));
h.assertTrue(menu.getSlot(0).getItem().is(Cuisine.item("raw_cauldron_coffee")), "Native table previews preparation");
menu.quickMoveStack(p, 0);
h.assertTrue(count(p, Cuisine.item("raw_cauldron_coffee")) == 1 && menu.getSlot(1).getItem().getCount() == 7 && menu.getSlot(2).getItem().is(Items.GLASS_BOTTLE), "Shift craft consumes one water and one coffee, returns the bottle once");
menu.quickMoveStack(p, 0); h.assertTrue(count(p, Cuisine.item("raw_cauldron_coffee")) == 1, "Cannot duplicate from stale result"); h.succeed();
}
@GameTest public void actualSmokerConsumesFuelAndCooksThePreparation(GameTestHelper h) {
var pos = h.absolutePos(new BlockPos(3, 4, 3)); h.getLevel().setBlockAndUpdate(pos, Blocks.SMOKER.defaultBlockState());
var smoker = (AbstractFurnaceBlockEntity) h.getLevel().getBlockEntity(pos);
smoker.setItem(0, new ItemStack(Cuisine.item("raw_pan_ratatouille")));
for (int i = 0; i < 300; i++) AbstractFurnaceBlockEntity.serverTick(h.getLevel(), pos, h.getLevel().getBlockState(pos), smoker);
h.assertTrue(smoker.getItem(2).isEmpty() && smoker.getItem(0).getCount() == 1, "No free cooking without fuel");
smoker.setItem(1, new ItemStack(Items.COAL));
for (int i = 0; i < 300; i++) AbstractFurnaceBlockEntity.serverTick(h.getLevel(), pos, h.getLevel().getBlockState(pos), smoker);
h.assertTrue(smoker.getItem(0).isEmpty() && smoker.getItem(1).isEmpty() && smoker.getItem(2).is(Cuisine.item("ratatouille")) && smoker.getItem(2).getCount() == 1, "Native smoker produces exactly the paid meal"); h.succeed();
}
@GameTest public void foodPropertiesEffectsAndUncookedBatchesRemainDistinct(GameTestHelper h) {
for (var e : Cuisine.CATALOG.getAsJsonArray("items")) {
var row = e.getAsJsonObject(); var stack = new ItemStack(Cuisine.item(row.get("id").getAsString()));
if(row.get("id").getAsString().equals("cheese")) {
h.assertTrue(stack.getItem() instanceof net.minecraft.world.item.BlockItem&&!stack.has(DataComponents.FOOD),"Since beta.314 cheese is shared after placing, like cake");
continue;
}
h.assertTrue(stack.has(DataComponents.FOOD) == row.has("food"), "Historical ingredient edibility retained");
if (row.has("food")) h.assertTrue(stack.get(DataComponents.FOOD).nutrition() == row.getAsJsonObject("food").get("nutrition").getAsInt(), "Historical meal nutrition retained");
}
for (var e : Cuisine.CATALOG.getAsJsonArray("preparations")) h.assertTrue(!new ItemStack(Cuisine.item(e.getAsJsonObject().get("id").getAsString())).has(DataComponents.FOOD), "Uncooked preparation cannot be eaten as a finished dish");
var player = Progression003GameTests.player(h); player.getFoodData().setFoodLevel(2);
var coffee = new ItemStack(Cuisine.item("coffee")); coffee.finishUsingItem(h.getLevel(), player);
h.assertTrue(player.hasEffect(MobEffects.SPEED) && player.hasEffect(MobEffects.HASTE), "Actual consumption applies historical coffee effects"); h.succeed();
}
@GameTest public void discoveringACropTeachesTheActualCuisineRecipes(GameTestHelper h) {
var p = Progression003GameTests.player(h); p.setUUID(UUID.randomUUID());
p.setAttached(fr.koka.sanctuary.progression.ProgressionService.RECORD, fr.koka.sanctuary.progression.ProgressRecord.INITIAL.json());
fr.koka.sanctuary.progression.ProgressEffects.apply(p, fr.koka.sanctuary.progression.ProgressRecord.INITIAL);
RecipeService.discover(p, AgricultureItems.TOMATO); RecipeService.refresh(p);
for (var e : Cuisine.CATALOG.getAsJsonArray("ports")) for (var id : e.getAsJsonObject().getAsJsonArray("recipes"))
h.assertTrue(RecipeService.knowledge(p).recipes.contains(id.getAsString()), "Native cuisine blueprint discovered: " + id);
h.succeed();
}
public static Item item(String id) { return BuiltInRegistries.ITEM.getValue(Identifier.parse(id)); }
public static Recipe<?> recipe(GameTestHelper h, String id) { return h.getLevel().getServer().getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE, Identifier.parse(id))).orElseThrow().value(); }
public static List<ItemStack> samples(JsonArray ingredients) {
var stacks = new ArrayList<ItemStack>();
for (var e : ingredients) {
String id = e.getAsString();
if (id.equals("minecraft:potion")) stacks.add(PotionContents.createItemStack(Items.POTION, Potions.WATER));
else if (id.startsWith("#")) stacks.add(new ItemStack(BuiltInRegistries.ITEM.getOrThrow(TagKey.create(Registries.ITEM, Identifier.parse(id.substring(1)))).stream().findFirst().orElseThrow().value()));
else stacks.add(new ItemStack(item(id)));
}
return stacks;
}
public static CraftingInput grid(List<ItemStack> ingredients) { var list = new ArrayList<>(ingredients); while (list.size() < 9) list.add(ItemStack.EMPTY); return CraftingInput.of(3, 3, list); }
private static JsonObject portForCraft(String id) { return Cuisine.CATALOG.getAsJsonArray("ports").asList().stream().map(JsonElement::getAsJsonObject).filter(p -> p.getAsJsonArray("recipes").asList().stream().anyMatch(v -> v.getAsString().equals("sanctuary:" + id))).findFirst().orElseThrow(); }
private static int count(ServerPlayer p, Item item) { return p.getInventory().countItem(item); }
}
@@ -0,0 +1,178 @@
package fr.koka.sanctuary.gametest;
import com.google.gson.JsonObject;
import fr.koka.sanctuary.cuisine.*;
import fr.koka.sanctuary.cuisine.culinary.block.CuredSausageBlock;
import fr.koka.sanctuary.cuisine.culinary.block.entity.*;
import fr.koka.sanctuary.cuisine.culinary.recipe.*;
import fr.koka.sanctuary.cuisine.machine.RedstoneMillBlockEntity;
import fr.koka.sanctuary.cuisine.menu.RedstoneMillMenu;
import fr.koka.sanctuary.cuisine.recipe.*;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.*;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.item.alchemy.*;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.item.crafting.*;
import net.minecraft.world.item.crafting.display.SlotDisplayContext;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.*;
/** Paid native crafts, complete oven/pot/drying cycles, food containers and knowledge. */
public final class DriedCuisine318GameTests {
private static List<JsonObject> dishes() { return DriedCuisine.CATALOG.getAsJsonArray("dishes").asList().stream().map(e -> e.getAsJsonObject()).toList(); }
private static String recipeId(JsonObject row) {
return "sanctuary:" + switch(row.get("method").getAsString()) {
case "crafting" -> "prepare_"; case "mill" -> "mill_"; case "drying" -> "dry_"; default -> "cook_";
} + row.get("id").getAsString();
}
private static List<ItemStack> samples(GameTestHelper h, Recipe<?> recipe) { return recipe.placementInfo().ingredients().stream().map(i -> i.display().resolveForFirstStack(SlotDisplayContext.fromLevel(h.getLevel()))).toList(); }
private static ServerPlayer player(GameTestHelper h) { var p=Progression003GameTests.player(h);p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();p.setPos(h.absoluteVec(new Vec3(3.5,8,5)));return p; }
private static BlockPos pos(GameTestHelper h) { return h.absolutePos(new BlockPos(3,8,3)); }
private static void clear(GameTestHelper h) { var p=pos(h);if(h.getLevel().getBlockEntity(p) instanceof net.minecraft.world.Container c)c.clearContent();h.getLevel().removeBlock(p,false);h.getLevel().setBlockAndUpdate(p.below(),Blocks.CAMPFIRE.defaultBlockState());for(var e:h.getLevel().getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,new AABB(p).inflate(3)))e.discard(); }
private static void tick(GameTestHelper h, KitchenOvenBlockEntity oven, int n) { for(int i=0;i<n;i++)KitchenOvenBlockEntity.serverTick(h.getLevel(),oven.getBlockPos(),oven.getBlockState(),oven); }
private static void tick(GameTestHelper h, CookingCauldronBlockEntity pot, int n) { for(int i=0;i<n;i++)CookingCauldronBlockEntity.serverTick(h.getLevel(),pot.getBlockPos(),pot.getBlockState(),pot); }
@GameTest public void allEighteenProposalsHaveConcreteUnambiguousNativeRecipes(GameTestHelper h) {
h.assertTrue(dishes().size()==18 && DriedCuisine.items().size()==19,"Eighteen results plus one raw sausage preparation");
for(var row:dishes()) {
var recipe=Cuisine303GameTests.recipe(h,recipeId(row));var input=samples(h,recipe);long matches;
if(recipe instanceof CraftingRecipe r) {
var grid=Cuisine303GameTests.grid(input);h.assertTrue(r.assemble(grid).is(DriedCuisine.item(row.get("id").getAsString())),"Correct table result");
matches=h.getLevel().getServer().getRecipeManager().getRecipes().stream().filter(holder->holder.value() instanceof CraftingRecipe candidate && candidate.matches(grid,h.getLevel())).count();
} else if(recipe instanceof CulinaryProcessRecipe r) {
var view=new CulinaryRecipeInput(r.appliance(),input);
matches=h.getLevel().getServer().getRecipeManager().getRecipes().stream().filter(holder->holder.value() instanceof CulinaryProcessRecipe candidate && candidate.matches(view,h.getLevel())).count();
} else {
var view=new SingleRecipeInput(input.getFirst());
matches=h.getLevel().getServer().getRecipeManager().getRecipes().stream().filter(holder->
(holder.value() instanceof FoodDryingRecipe r && recipe instanceof FoodDryingRecipe && r.matches(view,h.getLevel())) ||
(holder.value() instanceof RedstoneMillingRecipe milling && recipe instanceof RedstoneMillingRecipe && milling.matches(view,h.getLevel()))).count();
}
h.assertTrue(matches==1,"One exact native process for "+recipeId(row));
}h.succeed();
}
@GameTest public void everyColdDishAndTheRawSausageArePaidThroughTheNativeCraftingMenu(GameTestHelper h) {
var ids=new ArrayList<String>();for(var row:dishes())if(row.get("method").getAsString().equals("crafting"))ids.add(recipeId(row));ids.add("sanctuary:prepare_spiced_sausage");
for(var id:ids) {
var recipe=(CraftingRecipe)Cuisine303GameTests.recipe(h,id);var input=samples(h,recipe);var expected=recipe.assemble(Cuisine303GameTests.grid(input));var p=player(h);
var table=pos(h);h.getLevel().setBlockAndUpdate(table,Blocks.CRAFTING_TABLE.defaultBlockState());var menu=new CraftingMenu(1,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),table));
for(int i=0;i<input.size();i++)menu.getSlot(i+1).set(input.get(i).copyWithCount(2));
h.assertTrue(ItemStack.matches(menu.getSlot(0).getItem(),expected),"Native preview: "+id);
menu.clicked(0,0,ContainerInput.PICKUP,p);
h.assertTrue(ItemStack.matches(menu.getCarried(),expected),"Native paid result: "+id);
for(int i=0;i<input.size();i++)h.assertTrue(menu.getSlot(i+1).getItem().getCount()==1,"Every ingredient pays exactly one unit: "+id);
menu.setCarried(ItemStack.EMPTY);menu.removed(p);
}h.succeed();
}
@GameTest public void fourOvenRecipesKeepIngredientsUntilFullCookingAndRepeatOnlyPaidBatches(GameTestHelper h) {
for(var row:dishes())if(row.get("method").getAsString().equals("oven")) {
clear(h);var recipe=(CulinaryProcessRecipe)Cuisine303GameTests.recipe(h,recipeId(row));h.getLevel().setBlockAndUpdate(pos(h),KitchenAppliances.KITCHEN_OVEN.defaultBlockState());var oven=(KitchenOvenBlockEntity)h.getLevel().getBlockEntity(pos(h));
var input=samples(h,recipe);for(int i=0;i<input.size();i++)oven.setItem(i,input.get(i).copyWithCount(2));oven.setItem(9,new ItemStack(Items.COAL));
tick(h,oven,recipe.processingTime()-1);h.assertTrue(oven.getItem(10).isEmpty()&&oven.getItem(0).getCount()==2,"No early output or input payment: "+row);
tick(h,oven,1);h.assertTrue(ItemStack.matches(oven.getItem(10),recipe.result())&&oven.getItem(0).getCount()==1,"Full native cycle: "+row);
tick(h,oven,recipe.processingTime());h.assertTrue(oven.getItem(10).getCount()==recipe.result().getCount()*2&&oven.getItem(0).isEmpty(),"Second paid batch with menu closed: "+row);
}h.succeed();
}
@GameTest public void fiveCauldronRecipesNeedWaterContainersAndAStirBeforePayingAtTheDeadline(GameTestHelper h) {
for(var row:dishes())if(row.get("method").getAsString().equals("cauldron")) {
clear(h);var recipe=(CulinaryProcessRecipe)Cuisine303GameTests.recipe(h,recipeId(row));h.getLevel().setBlockAndUpdate(pos(h),KitchenAppliances.COOKING_CAULDRON.defaultBlockState());var pot=(CookingCauldronBlockEntity)h.getLevel().getBlockEntity(pos(h));
for(var source:samples(h,recipe))if(!source.is(Items.POTION))h.assertTrue(pot.insertIngredient(source,1)==1,"Each real ingredient/container is accepted");
h.assertTrue(!pot.stir(),"Water is mandatory");pot.setWaterUnits(1);tick(h,pot,recipe.processingTime()+1);
h.assertTrue(pot.getItem(6).isEmpty()&&pot.waterUnits()==1,"No automatic intermediate dish before stirring");h.assertTrue(pot.stir(),"Explicit complete preparation: "+row);
tick(h,pot,recipe.processingTime()-1);h.assertTrue(pot.getItem(6).isEmpty()&&pot.waterUnits()==1,"No early payment");tick(h,pot,1);
h.assertTrue(ItemStack.matches(pot.getItem(6),recipe.result())&&pot.waterUnits()==0,"Full paid pot output: "+row);
for(int i=0;i<6;i++)h.assertTrue(pot.getItem(i).isEmpty(),"Every ingredient and serving container is consumed once");
}h.succeed();
}
@GameTest public void realMillingMakesPaidPowderAndBothSeasoningsWorkInExistingMeals(GameTestHelper h) {
clear(h);h.getLevel().setBlockAndUpdate(pos(h),KitchenAppliances.REDSTONE_MILL.defaultBlockState());var mill=(RedstoneMillBlockEntity)h.getLevel().getBlockEntity(pos(h));var p=player(h);var menu=(RedstoneMillMenu)mill.createMenu(1,p.getInventory(),p);
menu.getSlot(0).set(new ItemStack(DriedFoods.item("dried_chili_pepper"),4));h.assertTrue(menu.getSlot(1).getItem().is(DriedCuisine.item("chili_powder"))&&menu.getSlot(0).getItem().getCount()==4,"Manual preview keeps peppers");menu.clicked(1,0,ContainerInput.QUICK_MOVE,p);
h.assertTrue(p.getInventory().countItem(DriedCuisine.item("chili_powder"))==8&&menu.getSlot(0).getItem().isEmpty(),"Four real peppers pay eight powder portions");menu.removed(p);
var chili=(CulinaryProcessRecipe)Cuisine303GameTests.recipe(h,"sanctuary:physical_cauldron_bean_chili");var inputs=new ArrayList<>(samples(h,chili));
for(int i=0;i<inputs.size();i++)if(inputs.get(i).is(fr.koka.sanctuary.agriculture.AgricultureItems.CHILI_PEPPER))inputs.set(i,new ItemStack(DriedCuisine.item("chili_powder")));
h.assertTrue(chili.matches(new CulinaryRecipeInput(chili.appliance(),inputs),h.getLevel()),"Powder replaces chili in an existing pot recipe");
var herbs=List.of(new ItemStack(Items.EGG),new ItemStack(Items.MILK_BUCKET),new ItemStack(DriedCuisine.item("herb_seasoning")));
h.assertTrue(h.getLevel().getServer().getRecipeManager().getRecipeFor(RecipeType.CRAFTING,Cuisine303GameTests.grid(herbs),h.getLevel()).orElseThrow().value().assemble(Cuisine303GameTests.grid(herbs)).is(Cuisine.item("raw_pan_herb_omelette")),"Herb seasoning works at the actual preparation table");h.succeed();
}
@GameTest public void spicySausageSurvivesSavedDryingThenPlacesAndSharesFourSlices(GameTestHelper h) {
clear(h);h.getLevel().setBlockAndUpdate(pos(h).above(),Blocks.STONE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos(h),KitchenAppliances.DRYING_RACK.defaultBlockState());var rack=(DryingRackBlockEntity)h.getLevel().getBlockEntity(pos(h));rack.insertIngredient(new ItemStack(DriedCuisine.item("raw_spiced_sausage")));
for(int i=0;i<3000;i++)DryingRackBlockEntity.serverTick(h.getLevel(),pos(h),rack.getBlockState(),rack);
var copy=(DryingRackBlockEntity)BlockEntity.loadStatic(pos(h),rack.getBlockState(),rack.saveWithFullMetadata(h.getLevel().registryAccess()),h.getLevel().registryAccess());copy.setLevel(h.getLevel());h.getLevel().setBlockEntity(copy);
h.assertTrue(copy.progress(0)==3000&&!copy.ready(0),"Same saved rack format retains spicy work");
for(int i=0;i<2999;i++)DryingRackBlockEntity.serverTick(h.getLevel(),pos(h),copy.getBlockState(),copy);h.assertTrue(!copy.ready(0),"No early sausage");DryingRackBlockEntity.serverTick(h.getLevel(),pos(h),copy.getBlockState(),copy);
var food=copy.takeItem();h.assertTrue(food.is(DriedCuisine.item("spiced_sausage"))&&food.is(DriedCuisine.HANGING_SAUSAGES)&&copy.isEmpty(),"Same full five minutes and sausage renderer tag");
var p=player(h);var target=pos(h).east(3);h.getLevel().setBlockAndUpdate(target.below(),Blocks.STONE.defaultBlockState());h.getLevel().removeBlock(target,false);p.setItemInHand(InteractionHand.MAIN_HAND,food);var hit=new BlockHitResult(Vec3.atCenterOf(target.below()),Direction.UP,target.below(),false);
var placement=p.getMainHandItem().useOn(new UseOnContext(p,InteractionHand.MAIN_HAND,hit));h.assertTrue(placement.consumesAction()&&p.getMainHandItem().isEmpty(),"Real dried sausage placement: "+placement+" / "+h.getLevel().getBlockState(target)+" / "+p.getMainHandItem());p.getFoodData().setFoodLevel(0);
for(int i=0;i<4;i++){var state=h.getLevel().getBlockState(target);h.assertTrue(state.is(DriedCuisine.SPICED_SAUSAGE)&&state.getValue(CuredSausageBlock.BITES)==i,"The exact remaining spicy slices");state.useWithoutItem(h.getLevel(),p,new BlockHitResult(Vec3.atCenterOf(target),Direction.UP,target,false));}
h.assertTrue(h.getLevel().getBlockState(target).isAir()&&p.getFoodData().getFoodLevel()==8,"Four real slices, then no whole sausage refund");h.succeed();
}
@GameTest public void everyEdibleResultReturnsOnlyItsPaidContainerAndSpicesStayIngredients(GameTestHelper h) {
var p=player(h);
for(var row:dishes()) {
var item=DriedCuisine.item(row.get("id").getAsString());p.getInventory().clearContent();
if(row.has("nutrition")) {
p.getFoodData().setFoodLevel(0);var stack=new ItemStack(item,2);var result=stack.finishUsingItem(h.getLevel(),p);
h.assertTrue(result.getCount()==1&&p.getFoodData().getFoodLevel()==row.get("nutrition").getAsInt(),"One real edible portion: "+row);
if(row.has("container"))h.assertTrue(p.getInventory().countItem(Cuisine303GameTests.item(row.get("container").getAsString()))==1,"Exactly one paid serving container returned");
}else h.assertTrue(new ItemStack(item).get(DataComponents.FOOD)==null,"Spices and placeable sausage have no fake direct meal");
}h.succeed();
}
@GameTest public void discoveryAndSurvivalInputsCoverAllNineteenTransformations(GameTestHelper h) {
var p=player(h);p.setUUID(UUID.randomUUID());p.setAttached(fr.koka.sanctuary.progression.ProgressionService.RECORD,fr.koka.sanctuary.progression.ProgressRecord.INITIAL.json());fr.koka.sanctuary.progression.ProgressEffects.apply(p,fr.koka.sanctuary.progression.ProgressRecord.INITIAL);
var reachable=new HashSet<Item>();for(var i:net.minecraft.core.registries.BuiltInRegistries.ITEM)if(net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(i).getNamespace().equals("minecraft"))reachable.add(i);
reachable.addAll(fr.koka.sanctuary.agriculture.AgricultureItems.PRODUCE_ITEMS);reachable.addAll(Cuisine.items());reachable.addAll(DriedFoods.items());reachable.add(KitchenAppliances.RAW_SAUSAGE);
var prep=(CraftingRecipe)Cuisine303GameTests.recipe(h,"sanctuary:prepare_spiced_sausage");for(var ingredient:prep.placementInfo().ingredients())h.assertTrue(ingredient.items().anyMatch(v->reachable.contains(v.value())),"Raw spicy preparation uses survival inputs");reachable.add(DriedCuisine.item("raw_spiced_sausage"));
for(var row:dishes()) {
var recipe=Cuisine303GameTests.recipe(h,recipeId(row));for(var ingredient:recipe.placementInfo().ingredients())h.assertTrue(ingredient.items().anyMatch(v->reachable.contains(v.value())),"Reachable native ingredient: "+row);
for(var sample:samples(h,recipe))if(!sample.is(Items.POTION)&&!sample.is(Items.BOWL)&&!sample.is(Items.GLASS_BOTTLE))RecipeService.discover(p,sample.getItem());RecipeService.refresh(p);
h.assertTrue(RecipeService.knowledge(p).recipes.contains(recipeId(row)),"Discover actual blueprint: "+row);reachable.add(DriedCuisine.item(row.get("id").getAsString()));
}
h.assertTrue(RecipeService.knowledge(p).recipes.contains("sanctuary:prepare_spiced_sausage"),"Spicy preparation blueprint");h.succeed();
}
@GameTest public void unsafePotionsNeverMatchANewCauldronDish(GameTestHelper h) {
for(var row:dishes())if(row.get("method").getAsString().equals("cauldron")) {
var recipe=(CulinaryProcessRecipe)Cuisine303GameTests.recipe(h,recipeId(row));var input=new ArrayList<>(samples(h,recipe));
for(int i=0;i<input.size();i++)if(input.get(i).is(Items.POTION))input.set(i,PotionContents.createItemStack(Items.POTION,Potions.POISON));
h.assertTrue(!recipe.matches(new CulinaryRecipeInput(recipe.appliance(),input),h.getLevel()),"Potions cannot replace water in "+row);
}h.succeed();
}
@GameTest public void spicyMealsAndSlicesSendCosmeticHitsWithoutDamageOrKnockback(GameTestHelper h) {
var p=player(h);
var connection=new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);
var channel=new io.netty.channel.embedded.EmbeddedChannel(connection);
var cookie=net.minecraft.server.network.CommonListenerCookie.createInitial(new com.mojang.authlib.GameProfile(p.getUUID(),"spicy-test"),false);
p.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(h.getLevel().getServer(),connection,p,cookie);
p.setAbsorptionAmount(3);p.setInvulnerableTime(7);p.hurtTime=2;p.setDeltaMovement(.12,.03,-.2);
float health=p.getHealth(),absorption=p.getAbsorptionAmount();var velocity=p.getDeltaMovement();p.getFoodData().setFoodLevel(0);
new ItemStack(Cuisine.item("bean_chili"),2).finishUsingItem(h.getLevel(),p);channel.runPendingTasks();
long hits=channel.outboundMessages().stream().filter(packet->packet instanceof net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket).count();
h.assertTrue(hits==1,"Finishing an existing spicy meal sends one immediate animation packet");
p.getFoodData().setFoodLevel(0);new ItemStack(DriedCuisine.item("raisin_bread"),2).finishUsingItem(h.getLevel(),p);channel.runPendingTasks();
h.assertTrue(channel.outboundMessages().stream().filter(packet->packet instanceof net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket).count()==hits,"Mild food does not produce fake hits");
h.getLevel().setBlockAndUpdate(pos(h).below(),Blocks.STONE.defaultBlockState());h.getLevel().setBlockAndUpdate(pos(h),DriedCuisine.SPICED_SAUSAGE.defaultBlockState());p.getFoodData().setFoodLevel(0);
h.getLevel().getBlockState(pos(h)).useWithoutItem(h.getLevel(),p,new BlockHitResult(Vec3.atCenterOf(pos(h)),Direction.UP,pos(h),false));channel.runPendingTasks();
h.assertTrue(channel.outboundMessages().stream().filter(packet->packet instanceof net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket).count()==hits+1,"A real spicy sausage slice triggers the same reaction");
h.assertTrue(p.getHealth()==health&&p.getAbsorptionAmount()==absorption&&p.getInvulnerableTime()==7&&p.hurtTime==2&&p.getDeltaMovement().equals(velocity),"Cosmetic state: health="+p.getHealth()+"/"+health+", absorption="+p.getAbsorptionAmount()+"/"+absorption+", invulnerable="+p.getInvulnerableTime()+", hurt="+p.hurtTime+", velocity="+p.getDeltaMovement()+"/"+velocity);
h.assertTrue(channel.outboundMessages().stream().noneMatch(packet->packet instanceof net.minecraft.network.protocol.game.ClientboundDamageEventPacket),"No damage event is sent");
channel.finishAndReleaseAll();h.succeed();
}
}

Some files were not shown because too many files have changed in this diff Show More