Compare commits

..
7 Commits
Author SHA1 Message Date
koka aabc867a5c docs: prepare beta.325 release notes
Build Sanctuary / build (push) Waiting to run
2026-11-01 22:43:11 +01:00
koka 76a440ea80 fix: let rare chickens glide and power boats (beta.325) 2026-11-01 13:12:15 +01:00
koka e3b4613b10 Complete README history from beta.001 through beta.324
Build Sanctuary / build (push) Canceled after 0s
2026-10-31 14:00:50 +01:00
koka 4b9e44e75a Document verified beta.324 site and server delivery
Build Sanctuary / build (push) Canceled after 0s
2026-10-31 13:37:26 +01:00
koka ea1d84160d Integrate rare chickens, aptitude prices and JEI search (beta.324)
Build Sanctuary / build (push) Canceled after 0s
2026-10-31 13:30:43 +01:00
koka 7ca566292f feat: simplify drying racks to two upright foods (beta.322) 2026-10-31 13:09:00 +01:00
koka 0c68545b63 docs: record verified beta.321 deployment
Build Sanctuary / build (push) Canceled after 0s
2026-10-30 11:37:05 +01:00
108 changed files with 2751 additions and 218 deletions
+116 -36
View File
@@ -1,15 +1,62 @@
# Sanctuary
[Dernière release : beta.320](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.320) ·
[Reçu de livraison](docs/release-beta320.md) ·
[Dernière release : beta.324](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.324) ·
[Reçu de livraison](docs/release-beta324.md) ·
[Installation et mises à jour](docs/packwiz.md).
[Sources de la version publiée](https://git.botsu.net/koka/sanctuary-beta/src/tag/beta.324).
L’historique ci-dessous couvre **beta.001 à beta.324**. Chaque entrée décrit
le chantier à sa date de préparation ; les titres précisent les regroupements
livrés. Les essais annulés et les chantiers non finalisés gardent leur statut.
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
Nord glacial, Sud des canyons, Ouest océanique et Est volcanique.
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.325 — plané et bateaux des poules colorées (préparation locale)
[Les quatre poules rares utilisent les déplacements de la poule classique](docs/chicken-flight-beta325.md) :
plané lorsqu'elles sont portées et propulsion lorsqu'elles occupent un siège
de bateau, adultes comme poussins. Correctif ciblé sur le socle publié beta.324.
**25 tests serveur**, parcours **Vulkan avec Sodium**, construction et pack vérifiés.
## beta.324 — nouveautés regroupées et livrées
[Lot intégré et validé](docs/integration-beta324.md) : poules colorées rares,
aptitudes rééquilibrées avec conservation des anciens achats, recherche JEI
corrigée et séchoir beta.322 à deux aliments. **221 tests serveur** et parcours
commun **Vulkan avec Sodium** réussis. [Notes de version](docs/notes-beta324.md).
[Livraison vérifiée](docs/release-beta324.md) : Git poussé, serveur Beta actif,
canal et téléchargement du site en beta.324. La même instance Prism se met
à jour à son prochain lancement.
## beta.323 — poules colorées rares (intégrée à beta.324)
[Poules émeraude, rubis, saphir et or](docs/rare-chickens-beta323.md).
Une poule ordinaire peut pondre un œuf en or (une chance sur 256) ; lancé à
la main ou au distributeur, il fait éclore un poussin rare. Chaque variante
pond son produit à son propre rythme et reste stérile ; les textures adultes
et poussins de la 26.2 sont reprises. Préparation vérifiée : **34 tests serveur**
et parcours **Vulkan avec Sodium**. [Livrée dans beta.324](docs/release-beta324.md).
## beta.322 — préparation du séchoir, livrée en beta.324
[IA-14 : séchoir et fabrications](docs/drying-rack-beta322.md). Deux aliments
indépendants, affichage plus grand et droit, séchoir sans fil et moulin sans
tailleur de pierre. Le surplus des anciens séchoirs est conservé en attente
avec sa progression. Préparation sur la même branche, intégrée à beta.324.
## beta.321 — carte adaptative et modèles culinaires livrés
[Livraison vérifiée](docs/release-beta321.md) : serveur Beta, canal client/serveur
et téléchargement du site en beta.321. Carte de surface en extérieur et coupe
à hauteur du joueur en intérieur, y compris dans Pause ; baratte ouverte en
bois, saucissons en pavés et noms FR/EN corrigés. **55 tests serveur**, packs
vérifiés et JAR identique aux parcours de préparation **Vulkan avec Sodium**.
La même instance Prism récupère la mise à jour à son prochain lancement.
## beta.320 — cuisine, carte et activité saisonnière livrées
[Regroupement des trois chantiers](docs/integration-beta320.md) : cuisine directe
@@ -40,7 +87,7 @@ Manger épicé produit deux petits coups visuels et sonores sans dégât.
site et même instance Prism à jour sur le canal public. Build et packs vérifiés,
**204 tests serveur**, **7 tests du site** et binaire du parcours **Vulkan avec Sodium**.
## beta.317 — produits séchés (préparation locale)
## beta.317 — produits séchés (intégrée à beta.318)
[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
@@ -49,7 +96,7 @@ indépendants. Les herbes et piments séchés rejoignent la cuisine existante.
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)
## beta.316 — découverte alimentaire en survie (intégrée à beta.318)
[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 ;
@@ -76,7 +123,7 @@ et parts restantes conservées après sauvegarde. Préparation locale vérifiée
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)
## beta.314 — fromage à partager et vins de garde (intégrée à beta.315)
[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,
@@ -89,7 +136,7 @@ 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)
## beta.313 — saucisson au séchoir (intégrée à beta.315)
[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,
@@ -100,7 +147,7 @@ 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)
## beta.312 — recettes répétées au même chaudron (intégrée à beta.315)
[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
@@ -110,7 +157,7 @@ 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)
## beta.311 — chaudron préparé puis remué (intégrée à beta.315)
[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
@@ -121,7 +168,7 @@ 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)
## beta.310 — poêles en ligne et piles de quatre (intégrée à beta.315)
[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
@@ -131,7 +178,7 @@ 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)
## beta.309 — une seule sortie de cuisinière (intégrée à beta.315)
[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
@@ -141,7 +188,7 @@ 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)
## beta.308 — cuisinière automatique (intégrée à beta.315)
[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
@@ -152,7 +199,7 @@ 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)
## beta.307 — recette du moulin manuel (intégrée à beta.315)
[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
@@ -161,7 +208,7 @@ 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)
## beta.306 — moulin manuel (intégrée à beta.315)
[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
@@ -172,7 +219,7 @@ 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)
## beta.305 — écrasement direct des fruits (intégrée à beta.315)
[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.
@@ -182,7 +229,7 @@ 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)
## beta.304 — appareils culinaires (intégrée à beta.315)
[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
@@ -194,7 +241,7 @@ 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)
## beta.303 — recettes de cuisine (intégrée à beta.315)
[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.
@@ -206,7 +253,7 @@ 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)
## beta.302 — vanille en 16 × 16 (intégrée à beta.315)
[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
@@ -214,7 +261,7 @@ 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)
## beta.301 — vanille grimpante (intégrée à beta.315)
[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
@@ -225,7 +272,7 @@ 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)
## beta.300 — cultures, arbres fruitiers et caféier (intégrée à beta.315)
[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
@@ -240,7 +287,7 @@ 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)
## beta.299 — textures d'It's Alive ! (intégrée à beta.315)
[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
@@ -949,6 +996,15 @@ beta.235 intégrée, adaptée au choix de masquer la carte. `check build assembl
[MRpack local beta.237](build/Sanctuary-beta.237.mrpack) vérifié ; aucune
publication ni mise à jour personnelle.
## beta.236 — aptitudes par thème et essai de carte dans Pause
[Regroupement des aptitudes en cinq thèmes](docs/aptitudes-beta236.md), avec
Enchantement et Alchimie et un choix personnel d’activation. Beta.237 rend
ensuite les achats permanents, sauf Carte et Noms des créatures.
Un [essai distinct de carte derrière Pause](docs/pause-map-layer-beta236.md)
utilise aussi ce numéro : ce calque est annulé, tandis que le raccourci **M**
et Story sont conservés. Les changements retenus sont réunis en beta.239.
## beta.235 — Essai réversible du menu Pause
[Disposition adaptative](docs/pause-columns-beta235.md) : avant Atlas, Aventure
@@ -1673,6 +1729,20 @@ commune avec le site. Shaders beta.151 conservés.
[Contrat et configuration](docs/community-contract-v1.md) ·
[Vérifications et limites](docs/community-beta154.md).
## beta.153 — chantier shaders non finalisé
[Inventaire historique des publications](docs/publications-beta144-166.md).
Chantier de shaders conservé dans les fichiers de travail locaux ; aucun
commit de livraison ni JAR Sanctuary vérifiable retrouvé. Cette préparation
n’a pas de tag ni de binaire publié.
## beta.152 — chantier shaders non finalisé
[Inventaire historique des publications](docs/publications-beta144-166.md).
Autre numéro de préparation shaders, sans commit de livraison ni JAR Sanctuary
vérifiable retrouvé. Le travail local est conservé ; aucune publication
artificielle n’a été créée pour compléter la numérotation.
## beta.151 — Nether, reflets des blocs et SSR
Synchronisation de l’inventaire au changement de dimension, retour de l’intensité
@@ -1742,11 +1812,6 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter.
La version **beta.144** prépare une option d’**eau naturelle pixélisée** :
couleur du biome, trois profils selon le fond et visibilité adaptée en immersion.
Les océans gardent leur couleur native et le profil clair.
[Contrat et vérifications](docs/natural-water-beta144.md). Livraison en cours.
La version **beta.143** garde le sampler natif de précipitation pour la neige
saisonnière, mais ralentit son rythme : l'accumulation est espacée par un
facteur 12 et la fonte par le facteur temps réel 72. Les protections existantes
@@ -1765,6 +1830,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## beta.144 — eau naturelle pixélisée
[Eau naturelle selon le biome et le fond](docs/natural-water-beta144.md) :
profils clair, légèrement teinté et étang, avec visibilité adaptée en immersion.
Les océans gardent leur couleur native et le profil clair ; les réglages sont
personnalisables et les choix existants sont conservés.
Publication confirmée dans l’[inventaire beta.144 à beta.166](docs/publications-beta144-166.md).
## beta.143 — Neige saisonnière en temps réel
Accumulation ralentie par un facteur 12 et fonte saisonnière ralentie par le
@@ -2341,6 +2414,17 @@ Parcours natifs, montures, arènes, anciens duels, compilation et archives véri
[Pack normal](build/Sanctuary-beta.074.mrpack) ·
[Monde plat rapide](build/Sanctuary-Test-beta.074.mrpack).
## beta.073 — arènes et paris
**Menu pause → Duels et arènes** (depuis beta.076) : arènes sans plafond d'inscrits, duels entre
joueurs ou familiers, mode mixte, règles visibles et mises en objets. Les
spectateurs consultent les combats annoncés et parient avant leur lancement.
Les joueurs meurent réellement ; les familiers gardent leur K.-O. habituel.
[Parcours, règles et contrat de sauvegarde](docs/arenas-beta073.md).
Client natif, régression des anciens duels, compilation et archives vérifiés.
[Pack normal](build/Sanctuary-beta.073.mrpack) ·
[Monde plat rapide](build/Sanctuary-Test-beta.073.mrpack).
## beta.072 — statues et plans natifs
**K** ouvre l’outil natif : transformer un modèle Minecraft en statue de blocs,
@@ -2355,17 +2439,6 @@ interopérabilité avec Litemapy vérifiés.
[Pack normal](build/Sanctuary-beta.072.mrpack) ·
[Monde plat rapide](build/Sanctuary-Test-beta.072.mrpack).
## beta.073 — arènes et paris
**Menu pause → Duels et arènes** (depuis beta.076) : arènes sans plafond d'inscrits, duels entre
joueurs ou familiers, mode mixte, règles visibles et mises en objets. Les
spectateurs consultent les combats annoncés et parient avant leur lancement.
Les joueurs meurent réellement ; les familiers gardent leur K.-O. habituel.
[Parcours, règles et contrat de sauvegarde](docs/arenas-beta073.md).
Client natif, régression des anciens duels, compilation et archives vérifiés.
[Pack normal](build/Sanctuary-beta.073.mrpack) ·
[Monde plat rapide](build/Sanctuary-Test-beta.073.mrpack).
## beta.071 — personnalités et traits de monture
Chaque familier possède un caractère stable : agressif, protecteur, passif ou
@@ -3016,6 +3089,13 @@ Cette durée exclut le lancement et le rendu du client. Le détail des mesures,
de leur portée et des vérifications est dans [STARTUP-01](docs/initial-loading.md).
Livraison locale, non publiée et non installée dans Prism.
## beta.021 — préparation des cosmétiques de tête interactifs
[Début du chantier des accessoires portés](docs/head-cosmetics-beta021.md),
le 13 septembre 2026 : modèles et interactions des cosmétiques de tête.
Le correctif de chargement beta.022 intervient en parallèle ; la finalisation
et les vérifications du chantier sont documentées en **beta.023**.
## beta.020 — œufs singuliers, portage et plongeon
Les œufs portent désormais les couleurs de leur rareté et deux noms d’effets
+17
View File
@@ -1,5 +1,22 @@
# Provenance et crédits
## Poules rares beta.323
Le comportement et le rendu sont portés des classes `GemChickenEntity`,
`GemChickenVariant`, `GemChickenRenderer`, `ChickenMixin` et `ThrownEggMixin`
du module `itsalive` de Sanctuary 26.2, commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, sous GPL-3.0-or-later.
Les six PNG de `assets/itsalive/textures/entity/gem_chicken/` sont copiés
intacts dans `assets/sanctuary/textures/entity/gem_chicken/`. Ce sont des
recolorations des textures de poule Minecraft 26.2, par le script historique
`itsalive/tools/generate_gem_chickens.sh` ; Mojang/Microsoft restent titulaires
des textures sources. La nouvelle variante or référence la texture native
tempérée résolue par Minecraft et applique une teinte au rendu sans copier
cette texture. L'icône `assets/sanctuary/textures/item/golden_egg.png` est
reprise sans modification du module `anotherworld` du même commit.
Voir [le contrat et les différences du port](docs/rare-chickens-beta323.md).
## Saucisson posé en tranches, beta.315 locale
Le bloc de saucisson, ses quatre longueurs et orientations sont développés
+140
View File
@@ -0,0 +1,140 @@
# APT-297 — prix des aptitudes selon leur portée
Demande du créateur du 31 octobre 2026. Branche `codex/aptitude-prices`.
Préparation locale **beta.297**, isolée depuis le commit beta.293
`74dc1265`. Les travaux locaux beta.294–296 restent dans le dépôt initial. Le Catalogue des recettes devient un achat à 30 niveaux.
## Grille de prix
Les prix sont des **niveaux d’XP**, dépensés une seule fois. Les gestes et
petites aides restent accessibles tôt ; les outils de connaissance et de
fabrication avancée demandent davantage de progression.
| Aptitude | Niveaux | Intention |
| --- | ---: | --- |
| Noms des créatures | 2 | Petite aide d’identification |
| Se reposer | 4 | Geste de survie simple |
| Zoom | 5 | Aide d’observation ponctuelle |
| Savoir alimentaire | 6 | Connaissance générale des aliments |
| Nage | 8 | Déplacement utile à l’exploration |
| Rangement | 10 | Confort permanent d’inventaire |
| Grande carte | 12 | Lecture et exploration du territoire |
| Étude des familiers | 15 | Déchiffrage de l’ensemble des familiers |
| Lien actif | 20 | Accès aux pouvoirs de travail des familiers |
| Forgerie | 20 | Enclumes et table de forge |
| Alchimie | 25 | Accès personnel aux alambics |
| Enchantement | 30 | Amélioration avancée de l’équipement |
| Catalogue des recettes | 30 | Consultation complète des découvertes et recettes |
S’asseoir et s’allonger restent gratuits. Les six compétences, leurs paliers,
la fabrication manuelle, les découvertes et les conditions des postes natifs
conservent leurs règles. Acheter l’aptitude d’un métier ne paie pas les coûts
vanilla de ses opérations. Le même tarif alimente l’interface FR/EN et le
contrôle d’achat côté serveur.
L’achat du Catalogue synchronise immédiatement l’XP et l’écran de progression,
comme les autres aptitudes. En solo, la pause traitait bien l’achat mais
retardait auparavant le compteur d’XP natif jusqu’à la reprise du jeu.
## Contrat de transition
Aucun identifiant `sanctuary:*`, format de sauvegarde de joueur, attachement
ou protocole de paquet n’est remplacé. Aucun monde personnel n’est ouvert.
Les reçus historiques à **4 niveaux** de Catalogue, Savoir alimentaire et
Étude des familiers restent valides, avec leur prix, date et découvertes.
Les nouveaux reçus portent respectivement 30, 6 et 15 niveaux, dans le même
schéma 1. Les autres montants restent refusés. Les autres achats conservent
leur historique ou leur booléen acquis. Aucun supplément, remboursement ou
rachat n’est demandé aux anciens propriétaires ; mort et New Game+ gardent
les acquisitions.
Les configurations serveur adoptent les nouveaux prix au prochain chargement :
- `progression.json`, schéma **5 → 6** : seules les valeurs historiques
exactement égales à 4 de `namesCost`, `atlasCost`, `sortingCost` et
`swimmingCost` deviennent 2, 12, 10 et 8. Les autres valeurs personnalisées,
les prix des compétences et les réglages restent conservés. Les anciens
schémas passent par leurs transitions existantes puis celle-ci.
- `companion-powers.json`, schéma **1 → 2** : `activeCost` à 4 devient 20.
Les autres tarifs, espèces et réglages sont conservés. Le marqueur `.v2`
historique garde sa responsabilité sur les anciens délais des familiers ;
la transition de prix fonctionne aussi lorsqu’il existe déjà.
- Une configuration déjà au nouveau schéma n’est pas réécrite. Il reste
possible d’y configurer explicitement un prix à 4. Une ancienne valeur
personnalisée égale à 4 est indiscernable du défaut historique et suit la
transition ci-dessus ; la redéfinir après transition si nécessaire.
- Une configuration invalide reste refusée sans réécriture.
Client et serveur doivent être mis à jour ensemble. Une ancienne version ne
sait pas lire les nouveaux schémas de configuration ni les nouveaux montants
des trois reçus : un retour arrière exige une sauvegarde préalable des
configurations et données de joueurs, sans modifier les nouveaux reçus à la
main.
## Vérification
Commandes de référence :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=aptitudes236,forgerie238,progression,movement,sorting,recipes,foodknowledge,companions,familiar,starter,refonte \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
Les tests ciblent les paiements exacts des treize aptitudes, le refus à un
niveau du tarif et le refus du rachat même avec assez d’XP ; ils couvrent
également les reçus historiques, le rechargement natif, la mort, le New Game+,
les transitions de configuration et les menus des métiers.
**103 tests serveur requis réussis**, puis de nouveau 103/103 après le
correctif de synchronisation d’XP. Le dernier `check build assemblePack`
réussit en **1 min 17 s**. Les **109 contrôles purs** ont été exécutés au cours
des passages précédents : le premier a été interrompu lors du rattachement
du dossier de travail, après 56 contrôles terminés. La reprise puis le dernier
build excluent uniquement les contrôles purs déjà réussis, sans retirer les
tests serveur. Les commandes exactes et listes sont conservées dans
`build/aptitude-prices-beta297-resumed-checks.json` et
`build/aptitude-prices-beta297-final-checks.json`. Les contrôles de version,
d’index, de textures et de distribution restent actifs.
Journaux : `build/aptitude-prices-beta297-check-build-pack.log`,
`build/aptitude-prices-beta297-check-build-pack-final.log` et
`build/aptitude-prices-beta297-final-build-pack.log`.
Parcours client final réussi en **51 s**, Minecraft 26.3, **Vulkan / MoltenVK
1.4.2 sur Apple M1**, marqueur `APTITUDE_PRICES297_PASS` dans
`build/aptitude-prices-beta297-client-vulkan-verified.log` :
- Les trois achats des métiers depuis leurs widgets dépensent 75 niveaux
au total ; leurs libellés correspondent aux prix contrôlés côté serveur.
- Catalogue affiche 30, reste inaccessible à 29, puis s’achète à 30 ; l’XP
affichée devient immédiatement zéro pendant la pause solo.
- FR/EN, GUI 2/3/4 : treize aptitudes, achats permanents, douze paires de
désactivation/réactivation gratuites de Carte et Noms, conservation des
preuves et de l’XP, défilement, redimensionnement et retour à Pause.
- Les assertions du menu Pause reprennent le contrôle beta.275 actuel,
remplaçant l’attente historique beta.233 de six entrées. **25 captures**
dans `build/forgerie238-screenshots/` ; inspection visuelle du prix Catalogue,
des métiers FR GUI 4 et de Survie EN GUI 3.
## Artefacts locaux
Versions du mod et du pack alignées sur **beta.297**. L’export
[`Sanctuary-beta.297.mrpack`](../build/Sanctuary-beta.297.mrpack) est vérifié :
16 762 112 octets, intégrité ZIP, dépendances Minecraft 26.3 / Fabric Loader
0.19.5, un seul JAR Sanctuary identique au build final, sans classe de test,
monde ou module de laboratoire. Reçu
`build/aptitude-prices-beta297-artifact.json`.
- SHA-256 JAR : `5e18d3de7b56b9682f17025f57788426c349a981c796b7dc505582417bf6dbf7`.
- SHA-256 MRpack : `ca0acfe1b4b5c90c4a61d792666aa0d0a686dce96af370c7c5e9ea6422cc0113`.
Les essais utilisent uniquement les sauvegardes de développement. Windows
et multijoueur distant ne sont pas testés ; le rythme de cette grille reste
à apprécier sur des séances de jeu longues.
Cette préparation n’inclut ni publication, ni avancée du canal packwiz, ni
synchronisation de Prism ou du serveur Beta.
+81
View File
@@ -0,0 +1,81 @@
# CHICKEN-02 — Déplacements des poules colorées, beta.325
Demande du 1er novembre 2026, branche `codex/chicken-flight`.
Base : `e3b4613b1028baf5515064f0ffc58b521ee6f0d6`, branche d'intégration
`codex/itsalive-26-3`, au-dessus de la livraison publique beta.324.
Seuls six fichiers documentaires diffèrent du tag publié ; le code de départ
est exactement celui de beta.324. Le canal packwiz public a été relu en beta.324.
L'ancien prototype du chat n'est pas repris.
## Résultat
Les poules émeraude, rubis, saphir et or, adultes et poussins, sont reconnues
comme des poules pour les deux mécanismes existants :
- Portées sur la tête, elles permettent de planer avec la même limite de
descente et la même protection contre les dégâts de chute. Les restrictions
habituelles du portage et du vol continuent à s'appliquer ; déposer la poule
arrête immédiatement son assistance.
- Assises directement dans un bateau, elles fournissent exactement le moteur
d'une poule classique : portance, vitesse, maniabilité, descente et calcul
de charge. Cela couvre le bateau classique et les formats collectifs.
L'embarquement existant depuis la tête reste utilisable ; Espace fait monter
le bateau, relâcher le fait descendre. Une poule restée sur une tête n'est
pas un moteur de bateau.
Les deux contrôles limités à l'identifiant `minecraft:chicken` reconnaissent
désormais les instances de la classe native `Chicken`, dont hérite déjà
`GemChickenEntity`. Le cas existant des familiers de poule reste conservé.
Les classifications des autres espèces ne changent pas.
Aucun code de création, texture, identifiant, sauvegarde, règle de reproduction,
ponte, produit ou tirage des poules rares n'est modifié. Leurs responsabilités
restent dans l'intégration courante. Aucune modification de monde, publication,
installation Prism ou intervention sur un serveur personnel n'est réalisée.
## Vérification
Sur Minecraft 26.3, Java 25, Fabric Loader 0.19.5 et Fabric API 0.160.5+26.3 :
- `check build assemblePack` réussit en **3 min 44 s**, avec **25 GameTests
serveur requis réussis** et les contrôles communs complets. Le script local
limite les tests purs à 512 Mio et le serveur à 1 Gio, sans exclure de contrôle.
- Le parcours **Vulkan / MoltenVK / Apple M1 avec Sodium** réussit en
**41 secondes**. Les huit entités synchronisées utilisent les mêmes
classifications de vol côté client ; modèles et libellés FR/EN passent aussi.
- La comparaison avec le JAR publié beta.324 (SHA-256 contrôlé) ne trouve
que **trois entrées différentes** : `CarryGlide.class`, `FlightMotors.class`
et la version dans `fabric.mod.json`. Aucun fichier ajouté ni retiré.
Les deux classes livrées sont identiques à celles du parcours client.
- L'export MRpack est vérifié : CRC ZIP, version beta.325 et un seul JAR
Sanctuary, identique au binaire construit. Artefacts locaux :
`mods/sanctuary/build/libs/sanctuary-beta.325.jar` et
`build/Sanctuary-beta.325.mrpack`.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=chickens323,poultry,corrections -PsanctuaryAtlasOnly=true \
-I build/chicken-flight325/memory.init.gradle \
-Dorg.gradle.jvmargs=-Xmx768m --max-workers=1
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryChickens323ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=chickens323 -PsanctuaryQuickTests=true \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=2 \
-PsanctuaryClientNoVsync=true -Dorg.gradle.jvmargs=-Xmx768m --max-workers=1
```
Le script ignoré ci-dessus règle seulement `maxHeapSize` des tâches JavaExec ;
la commande sans `-I` exécute les mêmes contrôles avec les heaps par défaut.
Reçu local : `build/chicken-flight325/verification.json`.
Aucun essai de réseau distant ou multijoueur prolongé n'est revendiqué.
Deux scénarios serveur supplémentaires parcourent les quatre variantes et les
deux âges : plané effectif, vitesse horizontale conservée, remise à zéro de la
chute, absence de poussée ajoutée, dépose, état de ponte préservé ; embarquement,
propulsion et charge identiques, montée/descente et retrait du dernier moteur
pour les bateaux classiques, 2×2 et 3×3. Les suites poules rares, portage et
bateaux existantes sont rejouées.
Le parcours client Vulkan avec Sodium vérifie aussi la reconnaissance des huit
entités synchronisées, en plus des modèles et libellés FR/EN déjà présents.
Les preuves locales restent ignorées dans `build/chicken-flight325/`.
@@ -0,0 +1,53 @@
{
"version": "beta.322",
"date": "2026-10-31",
"branch": "codex/itsalive-26-3",
"local_only": true,
"jar": {
"path": "mods/sanctuary/build/libs/sanctuary-beta.322.jar",
"bytes": 19161350,
"sha256": "2e0d86c613f46be4636bf70d4f3b7e678472f62190d38d9225b4fe053d4dd152"
},
"client_server_copies_identical": true,
"active_hooks": 2,
"max_items_per_hook": 1,
"legacy_storage_version": 1,
"legacy_storage_positions": 4,
"legacy_reserve_slots": [
2,
3
],
"legacy_progress_preserved": true,
"legacy_waiting_food_does_not_advance": true,
"rack_wooden_cuboids": 7,
"crafting": {
"drying_rack": "5 sticks + 2 planks",
"mill": "3 smooth stone slabs + 2 copper ingots + 1 smooth stone + 1 stick"
},
"checks": {
"check_build_assemblePack": true,
"server_tests": 68,
"server_focus": "drying322,drying313,drying317,driedcuisine318,sausage315,mill306,kitchen304",
"sanctuaryAtlasOnly": true,
"vulkan_backend": "Apple M1",
"sodium": "0.9.2+mc26.3",
"full_native_sausage_and_mixed_food_path": true,
"final_upright_sprite_native_path": true,
"gui_scales": [
2,
3,
4
],
"locales": [
"fr_fr",
"en_us"
],
"native_inventory_payment_and_third_item_refusal": true,
"waterlogging_and_actual_disk_reload": true,
"first_interrupted_graphical_attempt_counted_as_pass": false
},
"evidence_directory": "build/drying322/",
"publication": false,
"existing_personal_or_server_world_modified": false,
"personal_instance_updated": false
}
+74
View File
@@ -0,0 +1,74 @@
# IA-14 — séchoir à deux aliments et fabrications simples
Branche `codex/itsalive-26-3`, préparation locale beta.322. Demande du
31 octobre 2026 : deux aliments visibles, plus grands et droits ; fabrication
du séchoir sans fil et du moulin sans tailleur de pierre.
Livré dans le [regroupement beta.324](release-beta324.md).
## Contrat de compatibilité avant développement
Le séchoir conserve son identifiant et le stockage `DryingVersion=1`, avec
les quatre positions et leurs compteurs historiques. Seules les positions
0 et 1 acceptent de nouveaux ingrédients et travaillent. Les anciennes
positions 2 et 3 forment une réserve de compatibilité : elles ne sont jamais
réalimentées, ne progressent pas en attente et gardent leurs objets,
composants, recettes, durées et état terminé.
Dès qu’une position active se libère, le prochain ancien objet y est repris
avec sa progression intacte. Tant qu’un surplus ancien attend, l’insertion
manuelle ou par entonnoir est refusée. Les anciens produits déjà prêts
restent récupérables. Le HUD signale le nombre d’anciens objets en attente.
La casse restitue tous les contenus présents exactement une fois.
Aucune réécriture globale de monde ou de chunk : ce comportement s’applique
aux séchoirs normalement chargés. Aucun format ni identifiant supprimé.
## Fabrication et rendu
- Séchoir : cinq bâtons et deux planches, grille `SSS / S S / P P`.
- Moulin : trois dalles de pierre lisse, deux lingots de cuivre, un bloc de
pierre lisse central et un bâton. Identifiant de recette conservé.
- Deux points d’accroche en bois ; sprites agrandis et présentés droits.
Les saucissons sont suspendus verticalement par quart de tour, sans rotation
oblique des cuboïdes. Les dessins diagonaux de viande et de poisson sont
redressés uniquement dans le séchoir ; les icônes d’inventaire restent intactes.
- Durées, pluie, immersion, récipients, recettes alimentaires et extraction
des produits prêts sont conservés. Les deux aliments restent indépendants.
## Validation locale
- **68 tests serveur réussis** : deux positions actives, refus d’un troisième
objet, entonnoirs, durées complètes et indépendantes, pluie et immersion,
restes d’ananas, sauvegardes et nouvelles fabrications payées à l’établi.
- Une sauvegarde native version 1 à quatre objets conserve ses composants,
quatre progressions et résultats. La réserve attend, reprend une place libre
sans réinitialisation, puis la casse restitue chaque objet exactement une fois.
- Parcours **Vulkan / Apple M1 avec Sodium** réussi : fabrication native,
deux accrochages payés, retrait/raccrochage, huit états de cadre, saucissons
verticaux, noms FR/EN aux échelles 2/3/4, vrais seaux, relecture sur disque,
récupération, part de saucisson posée et consommation des produits séchés.
- Contrôle complémentaire Vulkan après redressement des dessins diagonaux :
viande et cabillaud droits, refus de consommer le troisième ingrédient,
douze modèles alimentaires résolus, seize recettes JEI synchronisées.
Les captures finales ont été inspectées.
- Le premier essai graphique s’est interrompu avant sa fin ; il n’est pas
compté comme réussi. Le parcours regroupé a ensuite réussi, puis le contrôle
ciblé du rendu final a réussi également.
Les journaux et captures restent dans `build/drying322/` (ignoré par Git).
Aucune publication, modification du canal ou installation personnelle demandée.
Le site et le serveur restent sur leur livraison beta.321.
`./gradlew check build assemblePack` final réussi en **4 min 50 s**, avec
les **68 tests serveur** et les contrôles de construction et distribution.
Profil : `drying322,drying313,drying317,driedcuisine318,sausage315,mill306,kitchen304`,
avec `sanctuaryAtlasOnly=true`. Le parcours Vulkan regroupé réussit en
**1 min 22 s** ; le contrôle final des sprites redressés en **1 min 16 s**.
Les JAR client/serveur sont identiques, les nouvelles classes de test sont
absentes du binaire distribué, les langues FR/EN et recettes générées sont vérifiées.
JAR : `mods/sanctuary/build/libs/sanctuary-beta.322.jar`, 19161350 octets.
SHA-256 : `2e0d86c613f46be4636bf70d4f3b7e678472f62190d38d9225b4fe053d4dd152`.
[Reçu de vérification](drying-rack-beta322-verification.json).
@@ -0,0 +1,54 @@
{
"version": "beta.324",
"server_tests": 221,
"full_check_build_pack_server_pack": true,
"focused_tests": "chickens323,eggaudit,poultry,aptitudes236,forgerie238,progression,movement,sorting,recipes,foodknowledge,companions,familiar,starter,refonte,drying322,drying313,drying317,driedcuisine318,sausage315,mill306,kitchen304,cuisine319,map321,map319,map244,map,menus",
"atlas_only": true,
"heap_caps": {
"pure_checks": "512m",
"game_tests": "2g"
},
"checks_skipped_by_memory_script": [],
"client": {
"backend": "Vulkan",
"gpu": "Apple M1",
"sodium": true,
"heap": "2g",
"seconds": 74,
"combined_path_passed": true,
"parts": [
"GemChicken323ClientChecks",
"Aptitudes236ClientChecks",
"JeiSearch297ClientChecks",
"Drying317ClientChecks"
],
"locales": [
"fr_fr",
"en_us"
],
"gui_scales": [
2,
3,
4
]
},
"sources": {
"chickens": {
"root": "/Users/koka/Documents/sanctuary/sanctuary-beta",
"head": "74dc1265a8247a44410ed466ca04b371b25284c1"
},
"aptitudes": {
"commit": "0cbb38691001593f32093f9defee18c8f8a35dda"
},
"jei": {
"root": "/Users/koka/.codex/worktrees/jei-search/sanctuary-beta",
"head": "74dc1265a8247a44410ed466ca04b371b25284c1"
}
},
"original_imported_files_unchanged": true,
"code_jar_sha256": "2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72",
"embedded_jei": "30.32.0-sanctuary.5",
"embedded_media": "0.3.2",
"evidence": "build/integration324/",
"personal_world_opened": false
}
+83
View File
@@ -0,0 +1,83 @@
# Beta.324 — poules rares, aptitudes, recherche JEI et séchoir
Branche de regroupement : `codex/itsalive-26-3`. Demande du 31 octobre 2026 :
intégrer les trois chantiers, mettre Git à jour et publier Sanctuary.
La livraison comprend les sources Git et le pack publié sur le site et le serveur Beta.
## Sources reprises
- Séchoir beta.322 : commit `7ca566292fb4f3b8ad5a718285181834de917f32`.
Deux objets, affichage agrandi et redressé, fabrication sans fil et moulin
sans tailleur de pierre. Les anciennes positions excédentaires restent en
attente avec leur progression. [Contrat](drying-rack-beta322.md).
- Poules rares : changements locaux du chantier `codex/rare-chickens`, depuis
`74dc1265`, repris fichier par fichier, avec sept PNG historiques intacts.
Les autres changements locaux de ce dossier ne sont pas importés.
[Contrat et provenance](rare-chickens-beta323.md).
- Aptitudes : patch du commit `0cbb38691001593f32093f9defee18c8f8a35dda`,
repris par fusion à trois sources en conservant la cuisine récente.
[Prix et transition](aptitude-prices-beta297.md).
- Recherche JEI : patch local de `codex/jei-search-input`, au-dessus de
`74dc1265`. Le fork embarqué devient `30.32.0-sanctuary.5`.
[Correctif de saisie](jei-search-beta297.md).
Les numéros beta.297 et beta.323 des notes sources désignent leurs préparations
isolées. La livraison commune est beta.324 ; aucune ancienne release n’est
remplacée. Les dossiers sources restent conservés en place.
## Contrats de conservation avant livraison
Les poules rares et l’œuf en or ajoutent des identifiants sans modifier les
chunks ou la génération : une poule ordinaire a une chance sur 256 de pondre
un œuf en or. Celui-ci donne un poussin rare, selon les poids 8:4:2:1 pour
émeraude, rubis, saphir et or. Les variantes rares ne se reproduisent pas.
Leurs produits et délais chargés sont ceux du contrat source.
Les reçus d’aptitudes déjà achetées restent valides, sans supplément ni rachat.
Les nouvelles dépenses utilisent les tarifs du chantier Aptitudes. La
transition de configuration est explicitement reprise : `progression.json`
5 → 6, `companion-powers.json` 1 → 2 ; seuls les anciens tarifs par défaut
égaux à 4 deviennent les nouveaux tarifs. Les autres valeurs personnalisées
et les autres paramètres restent conservés. Une sauvegarde complète du
serveur et des configurations précède leur chargement par le nouveau binaire.
Après démarrage, seules ces transitions documentées de configuration sont
admises. Un retour à beta.321 exige la sauvegarde compatible, pas un lancement
de l’ancien code sur les nouveaux schémas.
JEI garde les favoris, les connaissances et l’événement de texte natif ;
seule la décision de conserver le focus utilise le code logique SDL.
Les fixtures culinaires paient désormais le prix courant du Catalogue ;
les tests de reçus historiques restent distincts.
Le site conserve son bouton de téléchargement direct. Publication du nouveau
pack par les outils versionnés ; aucun remplacement global du code Web,
changement de comptes, permissions, Alpha, réseau ou génération n’est prévu.
## Validation
- `check build assemblePack assembleServerPack` réussi en **2 min 58 s**,
**221 GameTests serveur requis réussis**, avec les contrôles purs du dépôt.
Le script de développement limite les tests purs à 512 Mio et le serveur
de test à 2 Gio ; aucun contrôle n’est exclu par ce script.
- Premier passage : un ancien test de carte donnait huit niveaux au joueur
pour un achat désormais à douze. La fixture utilise maintenant le tarif
configuré ; les assertions de paiement exact et de refus du rachat restent.
- Parcours commun **Vulkan / Apple M1 avec Sodium**, réussi en **1 min 14 s** :
quatre variantes et huit modèles adulte/poussin, couleurs et noms FR/EN ;
treize aptitudes, Catalogue refusé à 29 puis payé à 30 avec XP synchronisée
pendant la pause ; saisie JEI avec lettres, accents, Unicode, filtrage,
édition, focus clavier et chat ; séchoir à deux aliments, refus du troisième,
immersion, relecture sur disque, récupération et consommation.
- Captures inspectées : poules FR, achat Catalogue et recherche JEI FR GUI 4.
Langues FR/EN et interfaces 2/3/4 vérifiées dans le parcours commun.
- Les sept PNG historiques des poules/œuf et les fichiers sources originaux
importés restent intacts. Un seul JEI embarqué, en `.5`, plus Media 0.3.2.
Aucun module de test ajouté au JAR ; copies packwiz client/serveur et
override MRpack identiques au JAR validé.
JAR : `sanctuary-beta.324.jar`, SHA-256
`2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72`.
Les preuves du regroupement sont conservées dans `build/integration324/`.
[Livraison site/serveur vérifiée](release-beta324.md) le 31 octobre 2026 :
canal, CT 350 et téléchargement Web en beta.324 ; reçu committé après publication
sans déplacer le tag immuable.
+76
View File
@@ -0,0 +1,76 @@
# JEI-297 — Saisie dans la recherche de l’inventaire
Branche `codex/jei-search-input`, version préparée `beta.297`.
## Symptôme et cause
Cliquer dans la barre JEI donne bien le focus, mais le premier appui sur une
lettre le retire avant l’arrivée du caractère. Le texte reste vide.
Le portage SDL de Minecraft 26.3 distingue le scancode physique (`KeyEvent.key`)
du code logique dépendant du clavier (`KeyEvent.keycode`). JEI utilisait encore
le premier comme caractère pour décider si le champ devait consommer la touche.
Les scancodes A–Z valent 4–29 : le contrôle les prend pour des caractères de
commande interdits, puis le routeur abandonne le focus.
## Correctif
Le fork JEI `30.32.0-sanctuary.5` utilise le code logique pour cette décision.
L’insertion du texte reste assurée par les événements de caractères natifs ;
aucune lettre n’est fabriquée à partir du clavier physique. Les identifiants
des objets, les connaissances de recettes, les favoris et les sauvegardes ne
changent pas.
Le correctif est conservé dans `mods/jei/patches/26.3.patch` et reconstruit depuis
le commit upstream fixé. Sanctuary continue d’embarquer ce fork dans son JAR.
Le travail est isolé du dossier principal, dont les changements locaux sont conservés.
## Vérifications
La régression est reproduite sous **Vulkan** avec le fork `.4`, sur un monde plat
de développement neuf (graine `297`). Le clic, le focus et l’activation de la
saisie SDL passent ; l’appui physique sur P échoue ensuite sur l’assertion
`The physical letter key keeps JEI focused until its character arrives`.
Journal conservé dans `build/jei297-regression-before-vulkan.log`.
Les tâches `check`, `build` et `assemblePack` réussissent avec les propriétés
`sanctuaryFocusedTests=recipes,progression`, `sanctuaryAtlasOnly=true` et
`sanctuaryQuickTests=true` : contrôles purs du dépôt, **13 GameTests serveur**
et profils packwiz client/serveur assemblés. La première invocation combinée
échoue ensuite dans un contrôle client qui supposait Ctrl/Cmd+F déjà attribué.
Sans Amecs, le fork laisse ce raccourci non attribué : le test est corrigé pour
lier temporairement F, puis rétablir la liaison initiale. Le comportement des
raccourcis par défaut n’est pas modifié.
La relance cliente finale réussit, avec arrêt normal du client :
```sh
./gradlew :sanctuary:runClientGameTest \
-PsanctuaryClientTests=true -PsanctuaryJeiSearchClientTests=true \
-PsanctuaryQuickTests=true -PsanctuaryAtlasOnly=true \
-PsanctuaryFocusedTests=recipes,progression \
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=2 \
-PsanctuaryClientWindowed=true --console=plain
```
Parcours réel des callbacks clavier : clic dans la barre, maintien du focus
entre la touche et son caractère, texte inséré une seule fois, filtrage des
planches connues, Retour arrière, Échap et Entrée. Les noms français et anglais
sont recherchés après un véritable rechargement des ressources, en GUI 2/3/4.
Les 26 lettres passent, dont les touches d’inventaire et de recettes E/R/U,
ainsi que majuscule accentuée, texte IME et Unicode hors BMP. Le raccourci
temporaire ne s’insère pas dans la recherche, et le chat natif garde sa saisie
après fermeture de l’inventaire. Captures FR GUI 4 et EN GUI 2 inspectées.
Journaux ignorés : `build/jei297-check-build-pack-vulkan.log` et
`build/jei297-client-vulkan.log`. Le reçu `build/jei297-artifact.json` vérifie
le seul JEI embarqué en `.5`, le code des sources effectivement distribuées
et l’identité du JAR Sanctuary entre `mods/sanctuary/build/libs` et le profil packwiz.
Artefact : `mods/sanctuary/build/libs/sanctuary-beta.297.jar`.
SHA-256 : `e01f74675b24d79e4397a27baf363bd59f240e19030ca6a37d495e1f24e1fd97`.
Le pack est préparé dans `build/packwiz`, sans export MRpack ni nouveau kit
de serveur.
Préparation locale uniquement. Aucune publication, modification de serveur
personnel ni synchronisation de l’instance Prism.
+44
View File
@@ -0,0 +1,44 @@
# Sanctuary beta.324
Minecraft 26.3 · Fabric Loader 0.19.5 · Java 25.
- **Poules colorées rares** : émeraude, rubis, saphir et or. Une poule
ordinaire peut pondre un œuf en or, à raison d’une chance sur 256.
Le lancer fait éclore un poussin rare. Les variantes pondent leur produit
avec des délais de plus en plus longs et ne se reproduisent pas.
- **Aptitudes rééquilibrées** selon leur portée. Catalogue des recettes :
30 niveaux ; Savoir alimentaire : 6 ; Étude des familiers : 15.
Les aptitudes déjà achetées restent acquises, sans supplément ni rachat.
- **Recherche JEI réparée** : la première lettre ne retire plus le focus.
Saisie française/anglaise, accents, Unicode et touches d’édition conservés.
Le fork embarqué passe à `30.32.0-sanctuary.5`.
- **Séchoir à deux aliments**, agrandis et redressés. Les anciennes positions
excédentaires restent en attente avec leur progression. Nouvelle fabrication :
cinq bâtons et deux planches, sans fil.
- **Fabrication du moulin** : la pierre lisse remplace le tailleur de pierre.
Le moulin reste manuel et sans combustible.
Les nouveaux achats d’aptitudes coûtent des niveaux d’XP :
| Aptitude | Niveaux |
| --- | ---: |
| Noms des créatures | 2 |
| Se reposer | 4 |
| Zoom | 5 |
| Savoir alimentaire | 6 |
| Nage | 8 |
| Rangement | 10 |
| Grande carte | 12 |
| Étude des familiers | 15 |
| Lien actif | 20 |
| Forgerie | 20 |
| Alchimie | 25 |
| Enchantement | 30 |
| Catalogue des recettes | 30 |
Les prix personnalisés restent conservés ; les anciens tarifs par défaut
des configurations suivent la transition documentée. Les découvertes, favoris
JEI et reçus d’achat restent conservés. Aucun monde n’est régénéré et aucun
prototype Under n’est activé.
[Sources, compatibilité et validation](https://git.botsu.net/koka/sanctuary-beta/src/tag/beta.324/docs/integration-beta324.md).
+23
View File
@@ -0,0 +1,23 @@
# Sanctuary beta.325
Minecraft 26.3 · Fabric Loader 0.19.5 · Java 25.
Les poules émeraude, rubis, saphir et or utilisent les déplacements de la poule
classique, adultes comme poussins :
- Portées sur la tête, elles permettent de planer avec la même vitesse de
descente et la même protection contre les dégâts de chute.
- Assises dans un bateau classique ou collectif, elles fournissent la même
propulsion. Espace fait monter ; relâcher fait descendre.
Les pontes, produits, chances d'éclosion et règles de stérilité sont conservés.
Toutes les intégrations de beta.324 sont reprises : cuisine It's Alive et
séchoir, aptitudes et achats existants, recherche JEI `.5`, Media 0.3.2,
Dream Displays, voix et autres dépendances client/serveur.
Validation : **25 tests serveur** et parcours **Vulkan avec Sodium** réussis.
Comparé au JAR publié beta.324, seules les deux classes de reconnaissance des
poules et les métadonnées de version diffèrent. Aucun format de sauvegarde
ou réglage de génération n'est modifié ; aucun chunk n'est régénéré.
[Contrat et vérifications](https://git.botsu.net/koka/sanctuary-beta/src/tag/beta.325/docs/chicken-flight-beta325.md).
+6 -4
View File
@@ -1,6 +1,7 @@
# Beta.321 — modèles culinaires et carte adaptative
Branche : `codex/itsalive-26-3`. Travail local, sans publication ni déploiement.
Branche : `codex/itsalive-26-3`. Préparation locale validée, puis
[publiée sur le site et le serveur Beta](release-beta321.md) à la demande du propriétaire.
## Résultat attendu
@@ -51,6 +52,7 @@ réutilisent les relevés exacts déjà présents depuis la beta.320.
SHA-256 `c8a176f6954d3e7b5e1e736f08426f43fc7c278aaaef248cff1a45c4f8103673`.
Version intégrée vérifiée ; les nouvelles classes de test sont exclues du JAR.
Les sorties de vérification restent dans `build/presentation321/` (ignoré par
Git). L’instance personnelle, le canal public, le site et le serveur n’ont pas
été mis à jour dans ce lot ; leur dernière livraison reste la beta.320.
Les sorties de préparation restent dans `build/presentation321/` (ignoré par
Git). Cette préparation n’avait pas publié ni déployé la version. La
[livraison ultérieure beta.321](release-beta321.md) met à jour le canal, le site
et le serveur ; l’instance personnelle suivra le canal à son prochain lancement.
+123
View File
@@ -0,0 +1,123 @@
# CHICKEN-01 — Poules rares, beta.323
Demande du créateur du 31 octobre 2026, branche `codex/rare-chickens`.
Préparation locale ; publication, serveur Beta et instance Prism hors périmètre.
Les modifications locales antérieures du dépôt sont conservées.
Le socle de ce ticket reste le commit local `74dc1265a8247a44410ed466ca04b371b25284c1`
et ses changements existants. Le numéro 323 est choisi après vérification des
tags distants jusqu’à beta.321 et du chantier beta.322 en cours. Les travaux
des autres branches ne sont pas intégrés automatiquement : ce ticket doit
rejoindre la branche de livraison retenue avant une publication commune.
## Contrat jouable
Les poules ordinaires ont une chance sur 256 de remplacer leur ponte par
un `sanctuary:golden_egg`, comme dans la référence 26.2. Leur cadence normale
et leurs autres œufs restent natifs. L'œuf en or se lance à la main ou au
distributeur ; son impact le casse et fait toujours éclore **un seul poussin**.
L'éclosion est décidée côté serveur. Les œufs ordinaires restent inchangés.
Les pourcentages précis n'étant pas imposés dans la demande, cette première
implémentation choisit les poids **8:4:2:1**. L'éclosion est garantie comme
dans 26.2, puis la couleur est tirée selon ces poids :
| Variante | Probabilité par œuf en or | Produit unique | Intervalle de ponte |
| --- | --- | --- | --- |
| Émeraude | 8/15 ≈ 53,33 % | `minecraft:emerald` | 4 × l'intervalle normal, soit 20 à 40 min |
| Rubis | 4/15 ≈ 26,67 % | `sanctuary:ruby` | 8 ×, soit 40 à 80 min |
| Saphir | 2/15 ≈ 13,33 % | `sanctuary:sapphire` | 16 ×, soit 80 à 160 min |
| Or | 1/15 ≈ 6,67 % | `sanctuary:golden_egg` | 32 ×, soit 160 à 320 min |
Temps à 20 ticks/s, uniquement pour un adulte chargé et effectivement simulé.
Les poussins grandissent normalement et ne pondent pas. Le premier délai
adulte est lui aussi multiplié ; le délai restant est sauvegardé exactement,
sans réinitialisation ni multiplication supplémentaire au rechargement.
Un arrêt du serveur ou une poule déchargée ne produit aucun rattrapage.
Les quatre variantes ne peuvent entrer en amour, s'accoupler entre elles
ou avec une poule ordinaire, ni produire de descendant de reproduction.
Les graines permettent toujours d'attirer les poules et de nourrir les poussins.
Il n'existe aucun ajout aux listes de biomes, aucune apparition naturelle
et aucun œuf d'apparition classique pour cette entité. Les commandes opérateur
restent utilisables pour l'inspection :
```mcfunction
/give @s sanctuary:golden_egg
/summon sanctuary:gem_chicken ~ ~ ~ {GemChickenVariant:2}
```
Identifiants de variante : rubis `0`, saphir `1`, émeraude `2`, or `3`.
`sanctuary:gem_chicken` est une nouvelle entité avec sa variante synchronisée
et persistante. Aucun identifiant existant, format de monde, chunk ou mécanisme
de génération n'est modifié. Ce port n'est pas une migration des entités
`itsalive:gem_chicken` des sauvegardes 26.2.
## Provenance et limites
Référence en lecture seule : `../26.2`, commit
`26f2c2a0061619df1f146f9081d6740c560a7497`, modules `itsalive` et `anotherworld`.
Les six textures adulte/poussin émeraude, rubis et saphir, et l'icône d'œuf
en or, sont copiées sans modification. Le modèle et l'animation des poules
reprennent le rendu natif tempéré. La nouvelle poule en or utilise la texture
tempérée native, avec une teinte dorée appliquée par le moteur au rendu.
Le port remplace la reproduction historique, le tirage uniforme et les anciens
facteurs de ponte. Les gemmes viennent du registre Sanctuary déjà présent ;
aucune dépendance aux modules historiques n'est ajoutée. Les libellés de
chaque variante et de l'œuf existent en français et en anglais.
L'armure de poule envisagée dans la vision reste un futur ticket.
## Vérifications
Les **34 GameTests serveur requis réussissent** sur Minecraft 26.3 / Java 25,
dont neuf nouveaux scénarios des poules rares, plus les régressions sur les
œufs/familiers et les moteurs de bateaux. Pontes et produits réels, premier
délai et suivants, poussins, stérilité pure et mixte, sauvegarde native,
absence de spawn naturel, tirage pondéré, collision réelle de l'œuf,
lancer à la main et distributeur sont couverts.
Le premier contrôle global a été interrompu par le redémarrage de la machine.
La reprise conserve les 90 contrôles purs déjà terminés dans les journaux
`build/chickens297-check-interrupted.log` et `build/chickens297-check-resume.log` ;
seuls les tests touchés et les contrôles restants sont rejoués, avec un script
local `build/chickens323-memory.init.gradle` limitant les processus de test
à 512 Mio, ou 1 Gio pour le serveur de GameTests. Une assertion de parcours
des catégories de biome vides a été corrigée après le premier passage serveur.
Le passage final est consigné dans `build/chickens323-check.log`.
- `check build assemblePack` termine avec succès après la reprise documentée,
en **3 min 6 s** pour le passage final. Les profils packwiz client/serveur
sont assemblés et leurs index vérifiés.
- Le client graphique termine avec succès en **2 min 14 s**, sous **Vulkan /
MoltenVK 1.4.2 / Apple M1, avec Sodium** et un tas limité à 2 Gio.
Les quatre variantes synchronisées, les huit modèles adulte/poussin,
les textures, la teinte dorée et les noms FR/EN sont contrôlés. La capture
française a été inspectée pour les couleurs et les proportions ; le
client et son serveur intégré se ferment normalement.
- Les sept PNG repris sont identiques octet pour octet à 26.2. Le JAR livré
contient exactement les classes des poules exécutées par le client de test.
- L'export MRpack passe le contrôle ZIP/CRC : Minecraft 26.3, Loader 0.19.5,
version beta.323, un seul JAR Sanctuary identique à celui construit.
Les classes de GameTests sont exclues. Reçu ignoré :
`build/chickens323-artifacts.json`.
Artefacts locaux : `mods/sanctuary/build/libs/sanctuary-beta.323.jar` et
`build/Sanctuary-beta.323.mrpack`. Captures :
`build/chickens323-screenshots/`. Aucune installation de jeu personnelle ni
serveur publié n'a été modifié. Le calendrier de ponte complet de plusieurs
heures et une session multijoueur distante ne sont pas revendiqués : les
limites de temporisation et la persistance sont exercées côté serveur.
Commandes de validation :
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=chickens323,eggaudit,poultry -PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryChickens323ClientTests=true -PsanctuarySodiumClientTests=true \
-PsanctuaryFocusedTests=chickens323 \
-PsanctuaryQuickTests=true -PsanctuaryClientGraphicsBackend=vulkan \
-PsanctuaryClientHeapGb=2 -PsanctuaryClientNoVsync=true
```
Journaux de développement ignorés : `build/chickens323-*.log`.
+145
View File
@@ -0,0 +1,145 @@
{
"version": "beta.321",
"date": "2026-10-30",
"source_branch": "codex/itsalive-26-3",
"source_tag": "4e2b120bf2769da1349a82606ff22d3f670ae189",
"channel_commit": "f5012f481a093e6dbfffd0d1bf5cdbfe764cad7d",
"release_url": "https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.321",
"artifacts": {
"sanctuary-beta.321.jar": {
"bytes": 19160419,
"sha256": "c8a176f6954d3e7b5e1e736f08426f43fc7c278aaaef248cff1a45c4f8103673"
},
"Sanctuary-beta.321.mrpack": {
"bytes": 17417741,
"sha256": "f70d45fa09d112c6c28d73a8302a555e3df3af25747b5573bcef8914ba56a251"
},
"Sanctuary-Prism-beta.321.zip": {
"bytes": 3928525,
"sha256": "95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9"
},
"Sanctuary-Server-beta.321.zip": {
"bytes": 4036721,
"sha256": "c33ac4c08bb8b99598c71d44e1e5047f0810c4938f68d9d393507b950e865d43"
}
},
"validation": {
"check_build_assemblePack_assembleServerPack": true,
"required_server_tests": 55,
"focused_tests": "map321,map319,map244,map,cuisine319,sausage315,driedcuisine318,drying317,drying313",
"sanctuaryAtlasOnly": true,
"jar_identical_to_preparation_vulkan_sodium": true,
"new_graphical_session_during_deployment": false
},
"backup": {
"target": "beta.321",
"version_before": "beta.320",
"service_before": "active",
"backup": "/root/sanctuary-beta-backups/before-beta321-20261030T102727Z",
"backup_sha256": "8d4e71fd693250864c8660c40d9b4f6ea72b74fd762c0bbd99af386faf62ddd4",
"backup_bytes": 247055660,
"protected_files": 217,
"java_exit_confirmed": true,
"forced_kill_used": false
},
"server_update": {
"version": "beta.321",
"sanctuary_sha256": "c8a176f6954d3e7b5e1e736f08426f43fc7c278aaaef248cff1a45c4f8103673",
"server_jars": 5,
"protected_files_unchanged": 217,
"private_env_unchanged": true,
"started_since": "2026-10-30T10:31:09.995038+00:00",
"backup": "/root/sanctuary-beta-backups/before-beta321-20261030T102727Z"
},
"server_verified": {
"service": "active",
"loaded_version": "beta.321",
"embedded_media": "0.3.2",
"started": true,
"server_properties_unchanged": true,
"private_env_unchanged": true,
"community_uuid_unchanged": true,
"admission_status": 200,
"admission_reason": "invalid",
"no_redeem_called": true,
"startup_error_count": 0
},
"web_script": {
"path": "/root/sanctuary-beta-maintenance/beta321-20261030/publish_web_pack.py",
"sha256": "a1543aa5ec23454370739d9d09c80bad07a3e466843cf0d16a0838f4d5357f84"
},
"web_verified": {
"current": "releases/beta.321/prism",
"head": "eadc30e8c7b6dda50bc810584af057235e7fdba8",
"status_hash": "4c43579292d7a800aba013921f6606ea95a8857231d5d46a2136bc2fa772c555",
"status_entries": 73,
"protected_source_files_unchanged": 423,
"previous_release_retained": true,
"community_uuid_unchanged": true,
"metrics": {
"days": 5,
"deaths": 5,
"online": 0,
"running": true,
"fresh": true,
"measuredAt": 1793356493880
}
},
"public_verified": {
"/downloads/sanctuary-prism.zip": {
"status": 200,
"sha256": "95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9",
"bytes": 3928525,
"content_disposition": "attachment; filename=Sanctuary-Beta.zip"
},
"/downloads/sanctuary-beta.mrpack": {
"status": 200,
"sha256": "f70d45fa09d112c6c28d73a8302a555e3df3af25747b5573bcef8914ba56a251",
"bytes": 17417741,
"content_disposition": "attachment; filename=Sanctuary-Beta.mrpack"
},
"html_beta321": true,
"direct_prism_download": true,
"footer_button_tooltip_beta321": true,
"minecraft_ping": {
"version": "26.3",
"online": 0,
"max": 20,
"icon_sha256": "2b8966e8f2a0cfe064a1dfac4205874216373f8bb4c59e8eb712d5e7d204fce7",
"srv_target": "beta.sanctuary-minecraft.net",
"srv_port": 25567,
"origin": "LAN via public DNS/SRV"
},
"channel_commit": "f5012f481a093e6dbfffd0d1bf5cdbfe764cad7d",
"source_tag": "4e2b120bf2769da1349a82606ff22d3f670ae189"
},
"isolated_installation": {
"version": "beta.321",
"passes": 2,
"single_sanctuary": true,
"jars": {
"bobby-5.2.16+mc26.3.jar": "9d24790d45ccc2cf45ccb20eeea3eb72b51f5e8d34b1b0d221e24b259e599398",
"dreamdisplays-fabric-26.3-1.10.4.jar": "a7b6520a007b2683685cd42b6f5983d5d14b1923d37375eb52180c31156b2966",
"entityculling-fabric-1.11.3-mc26.3.jar": "d2f80514ee3382b1fa7afda4df762df50743226e3cd320b829ee507357aeafbc",
"fabric-api-0.160.5+26.3.jar": "5096e9774d629850338fc1c73b5571b527171374b11ac231691fb93731ee0ae6",
"lithium-fabric-0.26.2+mc26.3.jar": "0be5b17ef9f6e9244cd07b24b227553a73aec0cf93158cf28099f3094c167f1b",
"sanctuary-beta.321.jar": "c8a176f6954d3e7b5e1e736f08426f43fc7c278aaaef248cff1a45c4f8103673",
"sodium-fabric-0.9.2+mc26.3.jar": "87a5bb39f7e6e41106e77f122f82fc93f9d3bc0b6ebcb746b314172a446a287a",
"sound-physics-remastered-fabric-1.5.1+26.3.jar": "a27bb5e02315b5c9dafefd2763ca1a427b8de4f66c5b500678e368f3668324ae",
"voicechat-fabric-2.6.24+26.3.jar": "97074fa1d094a22f17d5ac11282176965a45137950b66627d3813618810c3d00"
},
"second_pass_identical": true,
"personal_instance_modified": false
},
"personal_prism": {
"synchronized_by_this_request": false,
"public_channel_confirmed_read_only": true,
"updates_on_next_launch": true
},
"limits": [
"No external-network connection test",
"No audible voice test with two players",
"No personal world opened"
],
"alpha_and_network_configuration_modified": false
}
+82
View File
@@ -0,0 +1,82 @@
# beta.321 — carte adaptative et modèles culinaires publiés
Livraison du **30 octobre 2026**, branche `codex/itsalive-26-3`.
[Notes de version](notes-beta321.md) · [détails du lot](presentation-beta321.md) ·
[reçu structuré](release-beta321-verification.json).
Le serveur Beta et le téléchargement du site utilisent **beta.321** : surface
complète dehors, coupe à hauteur du joueur à l’intérieur, aperçu Pause adapté,
baratte ouverte en bois et saucissons en pavés, noms d’inventaire FR/EN corrigés.
## Cibles vérifiées
| Cible | État |
| --- | --- |
| Source/tag `beta.321` | `4e2b120bf2769da1349a82606ff22d3f670ae189` |
| Canal client/serveur | `f5012f481a093e6dbfffd0d1bf5cdbfe764cad7d` |
| Release | [beta.321](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.321), quatre artefacts anonymes vérifiés |
| CT 350 | Active, `Done`, Sanctuary beta.321, Media 0.3.2, cinq JAR, zéro erreur au démarrage |
| CT 320 | Distribution `releases/beta.321/prism`, précédente beta.320 conservée |
| Prism personnel | Canal public confirmé en lecture seule ; mise à jour au prochain lancement |
Minecraft 26.3, Fabric Loader 0.19.5, Java 25 et les dépendances sont conservés.
Le reçu est committé après publication sans déplacer le tag immuable.
## Vérification
- `check build assemblePack assembleServerPack` réussi : **55 tests serveur**
du profil ciblé du lot, contrôles de construction et index vérifiés.
- JAR strictement identique à celui de la préparation **Vulkan avec Sodium** :
carte/Pause et basculement extérieur/intérieur, maintien du cadrage/zoom,
feuillage et verre, séparation des dimensions, noms FR/EN aux échelles 2/3/4,
chaîne laitière et seize états de baratte aux faces droites, quatre tranches
réelles et relecture sur disque. Ces preuves ne sont pas présentées comme une
nouvelle séance graphique pendant le déploiement.
- MRpack, ZIP Prism et kit serveur : intégrité ZIP, versions, hashes, huit
dépendances distantes et override Sanctuary identique au JAR livré.
- Deux synchronisations réelles depuis le ZIP Prism dans une instance isolée :
neuf JAR, un seul Sanctuary beta.321, deuxième passage identique. L’instance
personnelle conserve son canal et ses préférences, sans synchronisation
lancée par cette demande site/serveur.
- CT 350 : version chargée, `Done`, zéro erreur, environnement privé et UUID
communautaire conservés. Admission fictive : HTTP 200, `invalid`, sans
`redeem` et sans consommer de vrai code.
- Site : ZIP et MRpack anonymes HTTP 200 et hashes attendus ; ZIP reçu comme
`Sanctuary-Beta.zip`. Bouton, survol et footer beta.321. Métriques
`running=true`, `fresh=true`, même UUID communautaire.
- Ping Minecraft DNS/SRV depuis le LAN : 26.3, 0/20,
port 25567 et icône conservés. Cela ne prouve pas un accès depuis un autre
réseau ou un échange vocal audible entre deux joueurs.
## Artefacts immuables
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.321.jar` | 19160419 | `c8a176f6954d3e7b5e1e736f08426f43fc7c278aaaef248cff1a45c4f8103673` |
| `Sanctuary-beta.321.mrpack` | 17417741 | `f70d45fa09d112c6c28d73a8302a555e3df3af25747b5573bcef8914ba56a251` |
| `Sanctuary-Prism-beta.321.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
| `Sanctuary-Server-beta.321.zip` | 4036721 | `c33ac4c08bb8b99598c71d44e1e5047f0810c4938f68d9d393507b950e865d43` |
Les ZIP d’amorçage restent identiques à beta.320 ; leur canal stable fournit
le nouveau mod au lancement. Aucune sauvegarde ni configuration privée embarquée.
## Conservation
Arrêt propre de la Beta active et fin de Java confirmés avant la sauvegarde :
`/root/sanctuary-beta-backups/before-beta321-20261030T102727Z/instance.tar.gz`,
247055660 octets, SHA-256
`8d4e71fd693250864c8660c40d9b4f6ea72b74fd762c0bbd99af386faf62ddd4`. Environnement privé copié séparément en mode 600.
**217 fichiers protégés** identiques après synchronisation avant démarrage ;
propriétés comparées par clés/valeurs et environnement/UUID conservés après.
Aucun SIGKILL, monde réinitialisé ou chunk régénéré.
La publication Web ne modifie que les archives, leurs métadonnées et le lien
atomique `current`. HEAD `eadc30e8c7b6dda50bc810584af057235e7fdba8`, **73 changements
préexistants** et **423 fichiers source/assets** conservés. Script de publication
transféré avec son hash vérifié, simulation réussie avant bascule. Aucun
pull/reset, changement de cache, base, compte ou permission. Alpha et règles
réseau restent hors cible.
Les sorties opérationnelles sont dans `build/release321/`, hors de Git.
Les essais de jeu du propriétaire restent à effectuer sur la carte et les
nouveaux modèles. Aucun prototype Under n’est activé.
+211
View File
@@ -0,0 +1,211 @@
{
"date": "2026-10-31",
"version": "beta.324",
"source_branch": "codex/itsalive-26-3",
"source_commit": "ea1d84160d1ff1ad07289d03f6de2233efc9de9f",
"channel_commit": "d71cc7ff65fa2b5b3070d3ffb9786783ab9ca5b5",
"release_url": "https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.324",
"artifacts": {
"sanctuary-beta.324.jar": {
"bytes": 19179410,
"sha256": "2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72"
},
"Sanctuary-beta.324.mrpack": {
"bytes": 17432690,
"sha256": "d9ef01491a8ec010c12ad27e4754f376bf6f7a2a88654da17b8596a78e6e89cc"
},
"Sanctuary-Prism-beta.324.zip": {
"bytes": 3928525,
"sha256": "95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9"
},
"Sanctuary-Server-beta.324.zip": {
"bytes": 4036721,
"sha256": "c33ac4c08bb8b99598c71d44e1e5047f0810c4938f68d9d393507b950e865d43"
}
},
"build_verification": "integration-beta324-verification.json",
"backup": {
"target": "beta.324",
"version_before": "beta.321",
"service_before": "active",
"backup": "/root/sanctuary-beta-backups/before-beta324-20261031T123117Z",
"backup_sha256": "3cc8672e072603cdf96590cfac202aa1d44a69709aaca15dce4d1956735c60f1",
"backup_bytes": 247109295,
"protected_files": 219,
"java_exit_confirmed": true,
"forced_kill_used": false
},
"server_update": {
"version": "beta.324",
"sanctuary_sha256": "2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72",
"server_jars": 5,
"protected_files_unchanged": 219,
"private_env_unchanged": true,
"started_since": "2026-10-31T12:32:55.470981+00:00",
"backup": "/root/sanctuary-beta-backups/before-beta324-20261031T123117Z"
},
"server": {
"service": "active",
"loaded_version": "beta.324",
"embedded_media": "0.3.2",
"embedded_jei": "30.32.0-sanctuary.5",
"aptitude_config_transitions": {
"progression.json": {
"schema_before": 5,
"schema_after": 6,
"changed_keys": [
"atlasCost",
"namesCost",
"schema",
"sortingCost",
"swimmingCost"
],
"other_fields_preserved": true
},
"companion-powers.json": {
"schema_before": 1,
"schema_after": 2,
"changed_keys": [
"activeCost",
"schema"
],
"other_fields_preserved": true
}
},
"started": true,
"server_properties_unchanged": true,
"private_env_unchanged": true,
"community_uuid_unchanged": true,
"admission_status": 200,
"admission_reason": "invalid",
"no_redeem_called": true,
"startup_error_count": 0
},
"web": {
"current": "releases/beta.324/prism",
"head": "eadc30e8c7b6dda50bc810584af057235e7fdba8",
"status_hash": "4c43579292d7a800aba013921f6606ea95a8857231d5d46a2136bc2fa772c555",
"status_entries": 73,
"protected_source_files_unchanged": 423,
"previous_release_retained": true,
"community_uuid_unchanged": true,
"metrics": {
"days": 6,
"deaths": 5,
"online": 0,
"running": true,
"fresh": true,
"measuredAt": 1793450075907
}
},
"web_publisher": {
"path": "/root/sanctuary-beta-maintenance/beta324-20261031/publish_web_pack.py",
"sha256": "a1543aa5ec23454370739d9d09c80bad07a3e466843cf0d16a0838f4d5357f84"
},
"web_dry_run": {
"verified": {
"version": "beta.324",
"minecraft": "26.3",
"fabric": "0.19.5",
"mrpack_sha256": "d9ef01491a8ec010c12ad27e4754f376bf6f7a2a88654da17b8596a78e6e89cc",
"mrpack_size": 17432690,
"sanctuary_sha256": "2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72",
"prism_size": 3928525,
"prism_sha256": "95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9",
"packwiz_url": "https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml",
"source_url": "https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.324/Sanctuary-beta.324.mrpack",
"prism_source_url": "https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.324/Sanctuary-Prism-beta.324.zip"
},
"web_ct": 320,
"server_ct": 350
},
"web_publication": {
"published": {
"version": "beta.324",
"minecraft": "26.3",
"fabric": "0.19.5",
"mrpack_sha256": "d9ef01491a8ec010c12ad27e4754f376bf6f7a2a88654da17b8596a78e6e89cc",
"mrpack_size": 17432690,
"sanctuary_sha256": "2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72",
"prism_size": 3928525,
"prism_sha256": "95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9",
"packwiz_url": "https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml",
"source_url": "https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.324/Sanctuary-beta.324.mrpack",
"prism_source_url": "https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.324/Sanctuary-Prism-beta.324.zip"
},
"previous": "releases/beta.321/prism",
"current": "/var/www/sanctuary/storage/app/distribution/current"
},
"public": {
"/downloads/sanctuary-prism.zip": {
"status": 200,
"sha256": "95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9",
"bytes": 3928525,
"content_disposition": "attachment; filename=Sanctuary-Beta.zip"
},
"/downloads/sanctuary-beta.mrpack": {
"status": 200,
"sha256": "d9ef01491a8ec010c12ad27e4754f376bf6f7a2a88654da17b8596a78e6e89cc",
"bytes": 17432690,
"content_disposition": "attachment; filename=Sanctuary-Beta.mrpack"
},
"html_beta324": true,
"direct_prism_download": true,
"footer_button_tooltip_beta324": true,
"minecraft_ping": {
"version": "26.3",
"online": 0,
"max": 20,
"icon_sha256": "2b8966e8f2a0cfe064a1dfac4205874216373f8bb4c59e8eb712d5e7d204fce7",
"srv_target": "beta.sanctuary-minecraft.net",
"srv_port": 25567,
"origin": "LAN via public DNS/SRV"
},
"channel_commit": "d71cc7ff65fa2b5b3070d3ffb9786783ab9ca5b5",
"source_tag": "ea1d84160d1ff1ad07289d03f6de2233efc9de9f"
},
"isolated_installs": {
"version": "beta.324",
"passes": 2,
"single_sanctuary": true,
"jars": {
"bobby-5.2.16+mc26.3.jar": "9d24790d45ccc2cf45ccb20eeea3eb72b51f5e8d34b1b0d221e24b259e599398",
"dreamdisplays-fabric-26.3-1.10.4.jar": "a7b6520a007b2683685cd42b6f5983d5d14b1923d37375eb52180c31156b2966",
"entityculling-fabric-1.11.3-mc26.3.jar": "d2f80514ee3382b1fa7afda4df762df50743226e3cd320b829ee507357aeafbc",
"fabric-api-0.160.5+26.3.jar": "5096e9774d629850338fc1c73b5571b527171374b11ac231691fb93731ee0ae6",
"lithium-fabric-0.26.2+mc26.3.jar": "0be5b17ef9f6e9244cd07b24b227553a73aec0cf93158cf28099f3094c167f1b",
"sanctuary-beta.324.jar": "2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72",
"sodium-fabric-0.9.2+mc26.3.jar": "87a5bb39f7e6e41106e77f122f82fc93f9d3bc0b6ebcb746b314172a446a287a",
"sound-physics-remastered-fabric-1.5.1+26.3.jar": "a27bb5e02315b5c9dafefd2763ca1a427b8de4f66c5b500678e368f3668324ae",
"voicechat-fabric-2.6.24+26.3.jar": "97074fa1d094a22f17d5ac11282176965a45137950b66627d3813618810c3d00"
},
"second_pass_identical": true,
"personal_instance_modified": false
},
"personal_prism": {
"checked_prelaunch_commands": [
{
"file": "instance.cfg",
"public_channel": true,
"branded_installer_pinned": true
},
{
"file": "instance.cfg",
"public_channel": true,
"branded_installer_pinned": true
}
],
"instance_modified": false,
"mods_synchronized": false,
"updates_on_next_launch": true
},
"limits": [
"Ping depuis le LAN via DNS/SRV public ; accès depuis un autre réseau et audio vocal à deux joueurs non testés.",
"Instance Prism personnelle non synchronisée manuellement ; pré-lancement public vérifié, mise à jour au prochain lancement.",
"Ressenti et équilibre en jeu restent à tester par le propriétaire."
],
"no_world_regeneration": true,
"no_alpha_or_network_changes": true,
"runtime_config_rollback_requires_compatible_backup": true,
"private_secrets_included": false
}
+90
View File
@@ -0,0 +1,90 @@
# beta.324 — poules, aptitudes, recherche JEI et séchoir publiés
Livraison du **31 octobre 2026**, branche `codex/itsalive-26-3`.
[Notes de version](notes-beta324.md) · [regroupement validé](integration-beta324.md) ·
[reçu structuré](release-beta324-verification.json).
Le serveur Beta, le canal client/serveur et le téléchargement du site utilisent
**beta.324** : poules colorées rares, nouveaux tarifs des treize aptitudes,
recherche JEI corrigée, séchoir limité à deux aliments agrandis et redressés,
fabrications du séchoir sans fil et du moulin sans tailleur de pierre.
## Cibles vérifiées
| Cible | État |
| --- | --- |
| Source/tag `beta.324` | `ea1d84160d1ff1ad07289d03f6de2233efc9de9f` |
| Canal client/serveur | `d71cc7ff65fa2b5b3070d3ffb9786783ab9ca5b5` |
| Release | [beta.324](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.324), quatre artefacts anonymes vérifiés |
| CT 350 | Active, `Done`, Sanctuary beta.324, JEI .5, Media 0.3.2, cinq JAR, zéro erreur au démarrage |
| CT 320 | `releases/beta.324/prism`, précédente beta.321 conservée |
| Prism personnel | Deux pré-lancements publics vérifiés en lecture seule ; mise à jour au prochain lancement |
Minecraft 26.3, Fabric Loader 0.19.5, Java 25 et les dépendances sont conservés.
Le reçu est committé après publication sans déplacer le tag immuable.
## Vérification du binaire et de la livraison
- `check build assemblePack assembleServerPack` réussi, **221 tests serveur**
requis du profil ciblé et contrôles purs. Aucun contrôle exclu par le script
de limitation mémoire ; profil complet enregistré dans le reçu d’intégration.
- Parcours commun frais **Vulkan / Apple M1 avec Sodium** réussi : poules
adultes/poussins et noms FR/EN ; treize aptitudes, Catalogue refusé à 29 puis
prélevé à 30 et XP synchronisée en pause ; saisie et filtrage JEI FR/EN,
accents/Unicode, édition/focus et chat ; séchoir à deux aliments, refus du
troisième, immersion, relecture, récupération et consommation.
- JAR identique aux copies packwiz client/serveur et à l’override MRpack.
Un seul JEI embarqué `30.32.0-sanctuary.5`, aucun module de test distribué.
Les sept textures historiques de poules/œuf restent identiques aux sources.
- Deux synchronisations réelles depuis le ZIP Prism dans un dossier isolé :
neuf JAR, un seul Sanctuary beta.324, second passage identique. Aucune
modification ni ouverture de monde de l’instance personnelle.
- Serveur : version effectivement chargée, `Done`, zéro erreur, environnement
privé, propriétés et UUID communautaire conservés. Admission fictive :
HTTP 200 / `invalid`, sans `redeem` ni consommation d’un vrai code.
- Site : ZIP et MRpack anonymes HTTP 200 et hashes exacts ; ZIP reçu comme
`Sanctuary-Beta.zip`. Téléchargement direct, survol et footer beta.324.
Métriques `running=true`, `fresh=true`, même UUID communautaire.
- Ping Minecraft depuis le LAN via DNS/SRV public : 26.3, 0/20, port 25567,
icône conservée. Accès depuis un autre réseau et voix audible à deux joueurs
non testés. Les essais de jeu du propriétaire restent à effectuer.
## Artefacts immuables
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.324.jar` | 19179410 | `2fcfc84579660b494602fb35cb36cb1b6ba6d8964f664dbefde2abee7e1b1a72` |
| `Sanctuary-beta.324.mrpack` | 17432690 | `d9ef01491a8ec010c12ad27e4754f376bf6f7a2a88654da17b8596a78e6e89cc` |
| `Sanctuary-Prism-beta.324.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
| `Sanctuary-Server-beta.324.zip` | 4036721 | `c33ac4c08bb8b99598c71d44e1e5047f0810c4938f68d9d393507b950e865d43` |
Les ZIP d’amorçage restent identiques à beta.321 ; le canal stable fournit le
nouveau mod au lancement. Aucune sauvegarde ou configuration privée embarquée.
## Conservation et transition des aptitudes
Arrêt propre et fin de Java confirmés avant sauvegarde, sans SIGKILL :
`/root/sanctuary-beta-backups/before-beta324-20261031T123117Z/instance.tar.gz`, 247109295 octets, SHA-256
`3cc8672e072603cdf96590cfac202aa1d44a69709aaca15dce4d1956735c60f1`. Environnement privé copié séparément, mode 600.
**219 fichiers protégés** identiques après synchronisation avant démarrage.
Aucun monde réinitialisé ni chunk régénéré.
Après démarrage, les deux transitions de configuration prévues ont été
comparées au contenu de la sauvegarde : `progression.json` **5 → 6**, anciens
prix par défaut 4 remplacés pour Noms/Carte/Rangement/Nage ;
`companion-powers.json` **1 → 2**, Lien actif 4 → 20. Tous les autres champs
restent identiques. Les anciens reçus d’achat à quatre niveaux restent valides,
sans supplément ni rachat ; les tests historiques vérifient cette conservation.
Un retour à beta.321 exige la sauvegarde compatible : ne pas lancer son ancien
binaire sur les nouveaux schémas ou reçus.
La publication Web ne modifie que les archives, métadonnées et lien `current`.
HEAD `eadc30e8c7b6dda50bc810584af057235e7fdba8`, **73 changements préexistants**
et **423 fichiers source/assets** conservés. Script versionné transféré avec
son hash vérifié, simulation réussie avant bascule. Aucun pull/reset global,
changement de cache, base, compte ou permission. Alpha et réseau hors cible.
Les trois dossiers sources restent intacts. Les préparations isolées beta.297,
beta.322 et beta.323 ne remplacent aucune ancienne release ; leur regroupement
est publié uniquement en beta.324. Aucun prototype Under activé.
Sorties opérationnelles dans `build/release324/`, hors de Git.
+6
View File
@@ -272,6 +272,12 @@ rebascule son lien `current` vers une publication complète conservée ; elle ne
change pas à elle seule le serveur ou le canal. Ne pas démarrer un ancien JAR
sur des données migrées sans vérifier le contrat de sauvegarde.
Depuis beta.324, les tarifs d’aptitudes font évoluer `progression.json` de
schéma 5 à 6 et `companion-powers.json` de 1 à 2, et de nouveaux reçus peuvent
contenir les nouveaux prix. Un retour à beta.321 doit restaurer la sauvegarde
compatible complète avec les configurations et reçus, jamais le JAR seul.
[Contrat](aptitude-prices-beta297.md) et [livraison vérifiée](release-beta324.md).
Consigner pour chaque livraison : commit source, version/tag, commit du canal,
hashes et URL des artefacts, CT/service et version chargée, publication Web,
version locale effectivement synchronisée, sauvegardes, vérifications et limites.
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -1,10 +1,12 @@
# 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).
**Beta.322 locale — 31 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.
Deux crochets indépendants, un objet par crochet : on peut mélanger deux aliments différents.
Le séchoir se fabrique avec cinq bâtons et deux planches, sans fil.
Les surplus des anciens séchoirs restent en attente et reprennent sans perdre leur progression.
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.
+3 -3
View File
@@ -9,11 +9,11 @@ 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.321
pack_version=beta.321
mod_version=beta.325
pack_version=beta.325
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
jei_version=30.32.0-sanctuary.5
sound_physics_version=1.5.1+26.3
sound_physics_artifact=9loJgQlO
sound_physics_sha512=c6bbe6e2018a19fa98e9590f17feeac17ccea4eb2327f23915a4c112f4a7ed99220f99b9d2a9ee3c9a9919a847a8965d9d82aaee67371d4d8d675b3fcf4b46a5
+5 -1
View File
@@ -2,7 +2,7 @@
Source officielle : https://github.com/mezz/JustEnoughItems/tree/26.2
Commit : `aae2dfcfb82e6b5bce787ba72bb2eda7339b274f`.
Version source 30.32.0 ; fork `30.32.0-sanctuary.4` pour **Minecraft 26.3**
Version source 30.32.0 ; fork `30.32.0-sanctuary.5` pour **Minecraft 26.3**
(finale), Loader 0.19.5, API 0.160.5+26.3, Java 25.
Licence MIT, copyright 2014–2015 mezz. `LICENSE.txt` est inclus sans modification.
@@ -54,3 +54,7 @@ communes du projet ; le comportement du catalogue Sanctuary est conservé.
La révision Sanctuary.4 distribue ce portage comme mod séparé et facultatif.
Les fonctionnalités du catalogue et les deux bibliothèques de recherche sont conservées.
La révision Sanctuary.5 corrige la saisie dans la barre de recherche : la décision
de consommer une touche utilise son code logique SDL, et non son scancode physique.
Les lettres conservent ainsi le focus jusqu’à l’arrivée du caractère natif.
+6
View File
@@ -1098,6 +1098,12 @@
);
}
--- a/Gui/src/main/java/mezz/jei/gui/input/KeyUserInput.java
+++ b/Gui/src/main/java/mezz/jei/gui/input/KeyUserInput.java
@@ -65 +65,2 @@
- return StringUtil.isAllowedChatCharacter((char) this.key.getValue());
+ // SDL key values are physical scancodes (A-Z are 4-29), not characters.
+ return StringUtil.isAllowedChatCharacter(this.event.keycode());
--- a/Gui/src/main/java/mezz/jei/gui/overlay/IngredientListOverlay.java
+++ b/Gui/src/main/java/mezz/jei/gui/overlay/IngredientListOverlay.java
@@ -95,7 +95,7 @@
+13 -1
View File
@@ -293,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 = [cuisine319: 'Cuisine319GameTests', 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', map321: 'Atlas321GameTests', map319: 'Atlas319GameTests', 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 = [cuisine319: 'Cuisine319GameTests', 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', chickens323: 'GemChicken323GameTests', drying322: 'Drying322GameTests', map321: 'Atlas321GameTests', map319: 'Atlas319GameTests', 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)
@@ -436,6 +436,18 @@ tasks.named('processGametestResources') {
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 integration324Client = providers.gradleProperty('sanctuaryIntegration324ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('integration324Client', integration324Client)
if (integration324Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Integration324ClientChecks') }
def chickens323Client = providers.gradleProperty('sanctuaryChickens323ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('chickens323Client', chickens323Client)
if (chickens323Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.GemChicken323ClientChecks') }
def jeiSearchClient = providers.gradleProperty('sanctuaryJeiSearchClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('jeiSearchClient', jeiSearchClient)
if (jeiSearchClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.JeiSearch297ClientChecks') }
def drying322Client = providers.gradleProperty('sanctuaryDrying322ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('drying322Client', drying322Client)
if (drying322Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Drying322ClientChecks') }
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') }
@@ -41,14 +41,22 @@ public final class Aptitudes236ClientChecks implements FabricClientGameTest {
c.waitFor(m->m.player.experienceLevel==100,200);
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));check(!CraftAptitudes.learned(m.player,"enchanting") && !CraftAptitudes.learned(m.player,"alchemy") && !CraftAptitudes.learned(m.player,"smithing"),"New workstations start locked");});
for(String craft:List.of("enchanting","alchemy","smithing")) {
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get(craft));check(key(button).equals("sanctuary.progression.unlock_cost")&&button.active,"Locked craft offers its normal purchase button");button.onPress(enter());});
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get(craft));check(key(button).equals("sanctuary.progression.unlock_cost")&&button.active,"Locked craft offers its normal purchase button");check(((TranslatableContents)button.getMessage().getContents()).getArgs()[0].equals(CraftAptitudes.cost(craft)),"Displayed craft price matches the server charge");button.onPress(enter());});
c.waitFor(m->CraftAptitudes.learned(m.player,craft) && key(afterTitle(m.gui.screen(),TITLES.get(craft))).equals("sanctuary.progression.aptitude_acquired"),200);
}
c.runOnClient(m->check(m.player.experienceLevel==88,"Three UI purchases cost twelve levels"));
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(100);
c.runOnClient(m->check(m.player.experienceLevel==25,"Three UI purchases cost seventy-five levels"));
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(29);RecipeService.refresh(p);ProgressionService.send(p,"");});
c.waitFor(m->m.player.experienceLevel==29 && !afterTitle(m.gui.screen(),TITLES.get("catalogue")).active,200);
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get("catalogue"));check(key(button).equals("sanctuary.recipes.buy") && ((TranslatableContents)button.getMessage().getContents()).getArgs()[0].equals(30),"Catalogue displays thirty levels and refuses twenty-nine");reveal(m.gui.screen(),button);});
shot(c,"catalogue-29-levels");
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(30);RecipeService.refresh(p);ProgressionService.send(p,"");});
c.waitFor(m->m.player.experienceLevel==30 && afterTitle(m.gui.screen(),TITLES.get("catalogue")).active,200);
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get("catalogue"));reveal(m.gui.screen(),button);button.onPress(enter());});
c.waitFor(m->m.player.experienceLevel==0 && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned(),200);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(150);
for(String target:ProgressRecord.APTITUDES)check(ProgressionService.purchase(p,target,ProgressEffects.get(p).revision()),"Server acquires "+target);
check(CompanionService.buy(p),"Active Bond bought");check(CompanionStudy.buy(p),"Familiar Study bought");
check(RecipeService.buy(p),"Catalogue bought");check(ZoomService.buy(p),"Zoom bought");check(FoodKnowledgeService.buy(p),"Food Knowledge bought");
check(RecipeService.knowledge(p).learned(),"Catalogue retains its UI purchase");check(ZoomService.buy(p),"Zoom bought");check(FoodKnowledgeService.buy(p),"Food Knowledge bought");
CompanionService.send(p,"");ProgressionService.send(p,"");
});
c.runOnClient(m->m.gui.setScreen(null));
@@ -88,10 +96,10 @@ public final class Aptitudes236ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{check(aptitudes(m.gui.screen()).scrollAmount()==100,"Resize retains aptitude scroll");layout(m.gui.screen());press(m.gui.screen(),"gui.done");check(m.gui.screen() instanceof PauseScreen,"Done returns to Pause");m.gui.setScreen(null);});
}
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.experienceLevel==xp && p.getAttached(ProgressionService.RECORD).equals(purchaseProof),"Twelve disable/reactivate pairs preserve XP and purchases");});
System.out.println("FORGERIE238_PASS: Vulkan FR/EN GUI 2/3/4, five themes, two real server toggles, eleven permanent purchases, legacy preferences and merged Pause layout, three native UI purchases, preserved XP/purchases, scroll/resize and Done");
System.out.println("APTITUDE_PRICES297_PASS: Vulkan FR/EN GUI 2/3/4, thirteen aptitudes, four native UI purchases, Catalogue refused at 29 and charged at 30 with immediate XP sync, twelve free toggle pairs, current Pause layout, preserved purchases, scroll/resize and Done");
}finally{intro.set(false);}
}
private static void pauseLayout(Screen screen,boolean enabled) {Pause233ClientChecks.pause(screen,enabled);}
private static void pauseLayout(Screen screen,boolean enabled) {Menu275ClientChecks.pauseLayout(screen,enabled);}
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
private static Button afterTitle(Screen screen,String title) {
var list=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList();
@@ -60,7 +60,7 @@ public final class Cauldron311ClientChecks implements FabricClientGameTest {
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);});
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(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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));
@@ -38,7 +38,7 @@ public final class CheeseWine314ClientChecks implements FabricClientGameTest {
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);
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(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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");
@@ -134,7 +134,7 @@ public final class Crushing305ClientChecks implements FabricClientGameTest {
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);});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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();
@@ -102,7 +102,7 @@ public final class Cuisine303ClientChecks implements FabricClientGameTest {
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);
p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST); 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);
@@ -39,7 +39,7 @@ public final class Dairy319ClientChecks implements FabricClientGameTest {
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("Dairy319-"+UUID.randomUUID());s.setSeed("319");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(POS.below(),Blocks.STONE.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.MILK_BUCKET));p.getInventory().setItem(1,new ItemStack(KitchenAppliances.DAIRY_CHURN));p.getInventory().setItem(2,new ItemStack(Items.SUGAR));p.getInventory().setItem(3,new ItemStack(AgricultureItems.BASIL));p.giveExperienceLevels(8);RecipeService.buy(p);RecipeService.discover(p,Items.MILK_BUCKET);RecipeService.refresh(p);});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();l.getGameRules().set(GameRules.SPAWN_MOBS,false,s);l.setBlockAndUpdate(POS.below(),Blocks.STONE.defaultBlockState());p.setGameMode(GameType.SURVIVAL);p.teleportTo(.5,1,2.5);p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.MILK_BUCKET));p.getInventory().setItem(1,new ItemStack(KitchenAppliances.DAIRY_CHURN));p.getInventory().setItem(2,new ItemStack(Items.SUGAR));p.getInventory().setItem(3,new ItemStack(AgricultureItems.BASIL));p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST + 4);RecipeService.buy(p);RecipeService.discover(p,Items.MILK_BUCKET);RecipeService.refresh(p);});
select(c,1);use(c,POS.below());
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.level().getBlockEntity(POS) instanceof DairyChurnBlockEntity&&p.getInventory().countItem(KitchenAppliances.DAIRY_CHURN.asItem())==0,"Real survival placement pays one churn");});
select(c,0);use(c,POS);c.waitFor(m->m.level.getBlockEntity(POS) instanceof DairyChurnBlockEntity churn&&churn.duration()==6000&&churn.restProgress()>0,150);
@@ -59,7 +59,7 @@ public final class DriedCuisine318ClientChecks implements FabricClientGameTest {
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);
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(fr.koka.sanctuary.recipes.RecipeKnowledge.COST + 4);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);
@@ -44,7 +44,7 @@ public final class Drying313ClientChecks implements FabricClientGameTest {
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));
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(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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);}
@@ -52,13 +52,13 @@ public final class Drying313ClientChecks implements FabricClientGameTest {
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);
for(int i=0;i<2;i++)use(c);c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(POT)).dryingCount()==2,200);c.takeScreenshot("drying313-two-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)==2&&rack.progress(0)>rack.progress(1),"Two real native insertions pay two preparations and keep separate ages");});select(c,7);use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var rack=(DryingRackBlockEntity)p.level().getBlockEntity(POT);check(rack.getItem(1).isEmpty()&&rack.progress(0)>0&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==3,"Taking one unfinished preparation leaves the others drying");});select(c,0);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(quads==42&&Math.abs(maxY-1)<.0001,"Seven 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");
@@ -68,17 +68,18 @@ public final class Drying313ClientChecks implements FabricClientGameTest {
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.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()==2&&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<2;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,7);
c.waitFor(m->((DryingRackBlockEntity)m.level.getBlockEntity(POT)).readyCount()==2,220);c.takeScreenshot("drying313-two-finished-saucissons");for(int i=0;i<2;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)==2&&p.getInventory().countItem(KitchenAppliances.RAW_SAUSAGE)==2,"Two 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);
// Share the drying result after native placement, like cheese.
select(c,c.computeOnClient(m->m.player.getInventory().findSlotMatchingItem(new ItemStack(KitchenAppliances.CURED_SAUSAGE))));
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");});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getInventory().countItem(KitchenAppliances.CURED_SAUSAGE)==1&&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");
c.runOnClient(m->m.getSoundManager().removeListener(listener));System.out.println("DRYING313_CLIENT_PASS Vulkan, native table shift craft/payout, paid rack placement, two 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);}
@@ -25,7 +25,7 @@ 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. */
/** New dried foods with real native placement, two 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);}
@@ -33,27 +33,27 @@ public final class Drying317ClientChecks implements FabricClientGameTest {
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;});
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.getWindow().setWindowed(1280,960);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);});
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(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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");
for(int i=0;i<3;i++){select(c,i);use(c);}server.waitFor(s->((DryingRackBlockEntity)s.overworld().getBlockEntity(POT)).dryingCount()==2,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)==1&&p.getInventory().countItem(AgricultureItems.GRAPES)==1&&p.getInventory().countItem(KitchenAppliances.DRYING_RACK.asItem())==0,"Real rack placement and two insertions each consume one ingredient and the third stays in hand");});
c.getInput().lookAt(POT);c.waitTicks(6);c.takeScreenshot("drying322-two-upright-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.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()==2&&r.getBlockState().getValue(DryingRackBlock.WATERLOGGED),"Two 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);});
server.runOnServer(s->{var r=(DryingRackBlockEntity)s.overworld().getBlockEntity(POT);var tag=r.saveCustomOnly(s.registryAccess());for(int i=0;i<2;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()==2,200);c.getInput().lookAt(POT);c.takeScreenshot("drying322-two-dried-results");for(int i=0;i<2;i++)use(c);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(String id:List.of("dried_meat","dried_cod"))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);
@@ -0,0 +1,10 @@
package fr.koka.sanctuary.client;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
public final class Drying322ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext context){
new Drying313ClientChecks().runTest(context);
new Drying317ClientChecks().runTest(context);
System.out.println("DRYING322_CLIENT_PASS two paid hooks, upright enlarged food, recipes, buckets, persistence and portions on Vulkan");
}
}
@@ -0,0 +1,95 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.chicken.*;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.gamerules.GameRules;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native adult/baby models, historical textures, golden tint and synced variants under Vulkan. */
public final class GemChicken323ClientChecks implements FabricClientGameTest {
private static void check(boolean value, String message) { if (!value) 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"), "Vulkan required");
System.out.println("CHICKENS323_BACKEND: " + device.backendName() + " / " + device.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);
}
});
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("GemChickens323-" + UUID.randomUUID()); s.setSeed("323"); 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);
List<Integer> ids = world.getServer().computeOnServer(s -> {
var p = s.getPlayerList().getPlayers().getFirst();
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");
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
p.teleportTo(.5, 1, -4.5); p.setYRot(0); p.setXRot(5);
p.getInventory().clearContent(); p.getInventory().setItem(0, new ItemStack(GemChickens.GOLDEN_EGG));
p.inventoryMenu.broadcastChanges();
for (int x = -8; x <= 8; x++) for (int z = -3; z <= 10; z++)
p.level().setBlockAndUpdate(new BlockPos(x, 0, z), Blocks.GRASS_BLOCK.defaultBlockState());
var found = new ArrayList<Integer>(); int column = 0;
for (var v : new GemChickenVariant[]{GemChickenVariant.EMERALD, GemChickenVariant.RUBY, GemChickenVariant.SAPPHIRE, GemChickenVariant.GOLD}) {
for (boolean baby : new boolean[]{false, true}) {
var bird = GemChickens.TYPE.create(p.level(), EntitySpawnReason.COMMAND);
bird.setGemVariant(v); bird.setBaby(baby); bird.setNoAi(true);
bird.snapTo(-3.25 + column * 2.5, 1, baby ? 1.5 : 3.5, 180, 0);
bird.setYBodyRot(180); bird.setYHeadRot(180); bird.setCustomNameVisible(true);
p.level().addFreshEntity(bird); found.add(bird.getId());
}
column++;
}
return found;
});
c.waitFor(m -> ids.stream().allMatch(id -> m.level.getEntity(id) instanceof GemChickenEntity), 200);
for (String locale : new String[]{"fr_fr", "en_us"}) {
c.runOnClient(m -> {
m.getLanguageManager().setSelected(locale);
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
for (int id : ids) {
var bird = (GemChickenEntity)m.level.getEntity(id);
check(m.getEntityRenderDispatcher().getRenderer(bird) instanceof GemChickenRenderer, "Custom renderer installed");
var renderer = (GemChickenRenderer)m.getEntityRenderDispatcher().getRenderer(bird);
var state = renderer.createRenderState(); renderer.extractRenderState(bird, state, 0);
check(state.gemVariant == bird.getGemVariant() && state.isBaby == bird.isBaby(), "Variant and age synced");
check(fr.koka.sanctuary.cosmetics.CarryGlide.flyer(bird)
&& fr.koka.sanctuary.boats.FlightMotors.kind(bird) == fr.koka.sanctuary.boats.FlightBalance.Kind.CHICKEN,
"Adult and baby rare chickens use ordinary gliding and boat propulsion on the client");
check(m.getResourceManager().getResource(renderer.getTextureLocation(state)).isPresent(), "Texture resolved");
check(!bird.getName().getString().contains("entity.sanctuary"), "Translated variant name");
check((renderer.getModelTint(state) == 0xFFFFCF40) == (state.gemVariant == GemChickenVariant.GOLD), "Gold-only render tint");
}
check(!new ItemStack(GemChickens.GOLDEN_EGG).getHoverName().getString().contains("item.sanctuary"), "Translated golden egg");
});
c.waitTicks(5); c.takeScreenshot("chickens323-" + locale);
}
System.out.println("CHICKENS323_CLIENT_PASS: Vulkan, four synced variants, eight adult/baby renders, textures, gold tint, FR/EN");
}
}
}
@@ -0,0 +1,15 @@
package fr.koka.sanctuary.client;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
/** One Vulkan client exercises the integrated gameplay, prices and JEI fork. */
public final class Integration324ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext context) {
new GemChicken323ClientChecks().runTest(context);
new Aptitudes236ClientChecks().runTest(context);
new JeiSearch297ClientChecks().runTest(context);
new Drying317ClientChecks().runTest(context);
System.out.println("INTEGRATION324_CLIENT_PASS: coloured chickens, aptitude prices, JEI input and two upright drying foods on Vulkan");
}
}
@@ -0,0 +1,212 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.blaze3d.platform.TextInputManager;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.recipes.RecipeKnowledge;
import fr.koka.sanctuary.recipes.RecipeService;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import mezz.jei.common.util.ReflectionUtil;
import mezz.jei.gui.input.GuiTextFieldFilter;
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.keymapping.v1.KeyMappingHelper;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Exercises the keyboard callbacks and SDL text-input state on a fresh test world. */
public final class JeiSearch297ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext c) {
fr.koka.sanctuary.testing.QuickTestWorld.enable();
var intro = new AtomicBoolean(true);
c.runOnClient(m -> {
IntroClient.setQuickTestIntro(true);
m.options.renderDistance().set(5);
m.options.simulationDistance().set(5);
m.options.pauseOnLostFocus = false;
m.getWindow().setWindowed(1280, 960);
m.options.guiScale().set(2);
m.resizeGui();
m.getLanguageManager().setSelected("en_us");
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
});
ClientTickEvents.END_CLIENT_TICK.register(m -> {
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
press(screen, "sanctuary.progression.enter");
intro.set(false);
}
});
try (var world = c.worldBuilder().adjustSettings(s -> {
s.setName("JeiSearch297-" + UUID.randomUUID());
s.setSeed("297");
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
}).create()) {
c.waitFor(m -> m.player != null && ProgressionClient.active() && RecipeClient.ready
&& SanctuaryJeiPlugin.runtime() != null && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
world.getServer().runOnServer(s -> {
var p = s.getPlayerList().getPlayers().getFirst();
p.getAbilities().invulnerable = true;
p.giveExperienceLevels(RecipeKnowledge.COST);
check(RecipeService.buy(p), "Test player buys the catalogue through its server service");
p.getInventory().setItem(0, new ItemStack(Items.OAK_PLANKS));
RecipeService.refresh(p);
});
c.waitFor(m -> RecipeClient.allowed(), 120);
c.runOnClient(m -> m.gui.setScreen(new InventoryScreen(m.player)));
c.waitTicks(8);
c.runOnClient(m -> {
check(SanctuaryJeiPlugin.runtime().getIngredientListOverlay().isListDisplayed(), "Catalogue is visible");
SanctuaryJeiPlugin.runtime().getIngredientFilter().setFilterText("");
});
clickSearch(c);
c.takeScreenshot("jei297-focused-inventory");
c.runOnClient(m -> {
check(search().isFocused(), "Mouse click focuses JEI search");
check(m.gui.screen().getFocused() == search(), "Search owns screen focus");
assertTextInput(m);
});
c.getInput().pressKey(InputConstants.KEY_P);
c.runOnClient(m -> check(search().isFocused(), "The physical letter key keeps JEI focused until its character arrives"));
c.getInput().typeChars("plank");
c.waitTicks(3);
c.runOnClient(m -> {
check(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilterText().equals("plank"), "Typed characters reach JEI exactly once");
check(m.gui.screen() instanceof InventoryScreen, "Typing preserves the inventory");
check(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().anyMatch(i -> i.is(Items.OAK_PLANKS)), "Typed search filters known planks");
});
c.takeScreenshot("jei297-search-plank");
c.getInput().pressKey(InputConstants.KEY_BACKSPACE);
c.runOnClient(m -> check(search().getValue().equals("plan"), "Backspace edits search"));
c.getInput().pressKey(InputConstants.KEY_ESCAPE);
c.runOnClient(m -> check(!search().isFocused() && m.gui.screen() instanceof InventoryScreen, "First Escape leaves search"));
for (String locale : new String[]{"fr_fr", "en_us"}) {
var reload = c.computeOnClient(m -> {
m.gui.setScreen(null);
m.getLanguageManager().setSelected(locale);
return m.reloadResourcePacks();
});
c.waitFor(m -> reload.isDone() && m.gui.overlay() == null && SanctuaryJeiPlugin.runtime() != null, 1200);
reload.join();
for (int scale : new int[]{2, 3, 4}) {
c.runOnClient(m -> {
m.options.guiScale().set(scale);
m.resizeGui();
m.gui.setScreen(new InventoryScreen(m.player));
SanctuaryJeiPlugin.runtime().getIngredientFilter().setFilterText("");
});
c.waitTicks(4);
clickSearch(c);
String query = locale.equals("fr_fr") ? "planche" : "plank";
typeLetters(c, query);
c.runOnClient(m -> {
check(search().getValue().equals(query), "Every physical key and character is retained in " + locale + " GUI " + scale);
check(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().anyMatch(i -> i.is(Items.OAK_PLANKS)), "Localized search finds known planks");
});
c.takeScreenshot("jei297-" + locale + "-gui" + scale);
}
}
c.runOnClient(m -> SanctuaryJeiPlugin.runtime().getIngredientFilter().setFilterText(""));
typeLetters(c, "abcdefghijklmnopqrstuvwxyz");
c.runOnClient(m -> check(search().getValue().equals("abcdefghijklmnopqrstuvwxyz"), "All 26 physical letter keys preserve text and focus, including inventory and recipe shortcuts"));
c.getInput().holdShift();
c.getInput().pressKey(InputConstants.KEY_E);
c.getInput().typeChar('É');
c.getInput().releaseShift();
c.runOnClient(m -> {
m.keyboardHandler.textInput(m.getWindow().handle(), "é中🐝");
check(search().getValue().equals("abcdefghijklmnopqrstuvwxyzÉé中🐝"), "Native committed text preserves uppercase, accents, IME characters and supplementary Unicode");
assertTextInput(m);
});
c.getInput().pressKey(InputConstants.KEY_RETURN);
c.runOnClient(m -> check(!search().isFocused() && m.gui.screen() instanceof InventoryScreen, "Enter leaves search without closing inventory"));
c.runOnClient(m -> SanctuaryJeiPlugin.runtime().getIngredientFilter().setFilterText(""));
// This fork leaves modifier shortcuts unbound without optional Amecs support.
var originalFocusKey = c.computeOnClient(m -> {
var key = KeyMapping.get("key.jei.focusSearch");
var original = KeyMappingHelper.getBoundKeyOf(key);
key.setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_F));
KeyMapping.resetMapping();
return original;
});
try {
c.getInput().pressKey(InputConstants.KEY_F);
c.getInput().typeChar('f');
c.runOnClient(m -> check(search().isFocused() && search().getValue().isEmpty(), "A bound focus-search key focuses JEI without inserting its hotkey"));
typeLetters(c, "plank");
} finally {
c.runOnClient(m -> {
KeyMapping.get("key.jei.focusSearch").setKey(originalFocusKey);
KeyMapping.resetMapping();
});
}
c.runOnClient(m -> {
check(search().getValue().equals("plank"), "Search also accepts text after its keyboard shortcut");
m.gui.setScreen(new ChatScreen("", false));
});
c.waitTicks(3);
c.getInput().typeChars("chat é");
c.runOnClient(m -> {
check(edit(m.gui.screen()).getValue().equals("chat é"), "Closing JEI preserves native chat input");
assertTextInput(m);
});
System.out.println("JEI297_PASS: FR/EN GUI 2/3/4, mouse and bound-key focus, all letters, native Unicode, filtering, editing, Escape/Enter and chat");
c.runOnClient(m -> m.gui.setScreen(null));
} finally {
intro.set(false);
}
}
private static GuiTextFieldFilter search() {
return new ReflectionUtil().getFieldWithClass(SanctuaryJeiPlugin.runtime().getIngredientListOverlay(), GuiTextFieldFilter.class)
.findFirst().orElseThrow();
}
private static void typeLetters(ClientGameTestContext c, String text) {
for (char letter : text.toCharArray()) {
c.getInput().pressKey(InputConstants.KEY_A + letter - 'a');
c.getInput().typeChar(letter);
}
}
private static void clickSearch(ClientGameTestContext c) {
var point = c.computeOnClient(m -> {
var field = search();
var window = m.getWindow();
return new double[]{(field.getX() + 8.0) * window.getScreenWidth() / window.getGuiScaledWidth(),
(field.getY() + 4.0) * window.getScreenHeight() / window.getGuiScaledHeight()};
});
c.getInput().setCursorPos(point[0], point[1]);
c.runOnClient(m -> m.mouseHandler.handleAccumulatedMovement());
c.runOnClient(m -> check(search().isMouseOver(m.mouseHandler.getScaledXPos(m.getWindow()),
m.mouseHandler.getScaledYPos(m.getWindow())), "Mouse reaches the actual JEI search bounds"));
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);
c.waitTicks(2);
}
private static void assertTextInput(Minecraft m) {
try {
var enabled = TextInputManager.class.getDeclaredField("textInputEnabled");
enabled.setAccessible(true);
check(enabled.getBoolean(m.textInputManager()), "JEI focus enables SDL text input");
} catch (ReflectiveOperationException error) {
throw new AssertionError(error);
}
}
}
@@ -139,7 +139,7 @@ public final class Kitchen304ClientChecks implements FabricClientGameTest {
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);});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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");
@@ -82,7 +82,7 @@ public final class Menu275ClientChecks implements FabricClientGameTest {
String time=widget(s,"sanctuary.inhabitant.overview.play_time").getMessage().getString();
require((time.contains("1 d")||time.contains("1 j"))&&time.contains("2 h")&&time.contains("min")&&time.contains(" s")&&!time.matches(".*\\d+[.,]\\d+.*"),"Played time uses whole days, hours, minutes and seconds: "+time);
}
private static void pauseLayout(Screen s,boolean atlas){
static void pauseLayout(Screen s,boolean atlas){
var map=((PauseAtlasAccess)s).sanctuary$pauseMap();require((map!=null)==atlas,"Pause map matches purchased active Atlas");
String[] keys={"sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.progression.identity","sanctuary.arena.title"};
var article=widget(s,"sanctuary.community.article");var notice=widget(s,"sanctuary.community.notice");
@@ -81,7 +81,7 @@ public final class Mill306ClientChecks implements FabricClientGameTest {
}
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);
p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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));
@@ -100,7 +100,7 @@ public final class Oven308ClientChecks implements FabricClientGameTest {
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);
p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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();
@@ -74,7 +74,7 @@ public final class Pan310ClientChecks implements FabricClientGameTest {
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);
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(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);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");
@@ -39,8 +39,8 @@ public final class Recipes024ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{require(!RecipeClient.allowed(),"Catalogue initially locked");require(!SanctuaryJeiPlugin.runtime().getIngredientListOverlay().isListDisplayed(),"Locked catalogue has no sidebar");SanctuaryJeiPlugin.runtime().getRecipesGui().showTypes(List.of(RecipeTypes.CRAFTING));require(m.gui.screen() instanceof InventoryScreen,"JEI cannot open while locked");});
c.takeScreenshot("sanctuary-beta024-locked-inventory");
var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.OAK_PLANKS,8));p.giveExperienceLevels(4-p.experienceLevel);RecipeService.refresh(p);});
c.waitFor(m->RecipeClient.knowledge.recipes.contains("minecraft:crafting_table")&&RecipeClient.xp>=4,120);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.OAK_PLANKS,8));p.giveExperienceLevels(RecipeKnowledge.COST-p.experienceLevel);RecipeService.refresh(p);});
c.waitFor(m->RecipeClient.knowledge.recipes.contains("minecraft:crafting_table")&&RecipeClient.xp>=RecipeKnowledge.COST,120);
c.runOnClient(m->{require(!RecipeClient.knowledge.recipes.contains("minecraft:shield"),"Plank discovery does not teach shield");m.gui.setScreen(new RecipeScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));});c.waitTicks(3);c.takeScreenshot("sanctuary-beta024-collections-locked");
c.runOnClient(m->button(m.gui.screen(),"sanctuary.recipes.buy").onPress(enter()));c.waitFor(m->RecipeClient.allowed()&&RecipeClient.knowledge.learned(),120);
c.runOnClient(m->{var r=SanctuaryJeiPlugin.runtime();require(r.getIngredientFilter().getFilteredItemStacks().stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)),"Unknown item hidden");
@@ -22,11 +22,62 @@ import net.minecraft.network.chat.Component;
/** Behavioral checks on real server players, native menus and vanilla persistence. */
public final class Aptitudes236GameTests {
@GameTest public void balancedPricesAreChargedExactlyOnce(GameTestHelper h) {
var config=ProgressionService.session(h.getLevel().getServer()).config;
for(String target:ProgressRecord.APTITUDES) {
var p=player(h,ProgressRecord.INITIAL);
int expected=switch(target) {case "names"->2;case "atlas"->12;case "sorting"->10;case "swimming"->8;default->4;};
h.assertTrue(ProgressRecord.INITIAL.cost(target,config)==expected,"Balanced server price: "+target);
assertPurchase(h,p,expected,()->ProgressionService.purchase(p,target,ProgressEffects.get(p).revision()));
}
for(String target:new String[]{"smithing","alchemy","enchanting"}) {
var p=player(h,ProgressRecord.INITIAL);
assertPurchase(h,p,CraftAptitudes.cost(target),()->CraftAptitudes.buy(p,target,0));
}
var catalogue=player(h,ProgressRecord.INITIAL);
assertPurchase(h,catalogue,30,()->RecipeService.buy(catalogue));
var food=player(h,ProgressRecord.INITIAL);
assertPurchase(h,food,6,()->fr.koka.sanctuary.food.FoodKnowledgeService.buy(food));
var bond=player(h,ProgressRecord.INITIAL);
h.assertTrue(CompanionService.config(bond).activeCost()==20,"Balanced Active Bond server price");
assertPurchase(h,bond,20,()->CompanionService.buy(bond));
var study=player(h,ProgressRecord.INITIAL);
assertPurchase(h,study,15,()->CompanionStudy.buy(study));
var zoom=player(h,ProgressRecord.INITIAL);
assertPurchase(h,zoom,5,()->ZoomService.buy(zoom));
h.succeed();
}
@GameTest public void historicalFourLevelReceiptsRemainAcquired(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL.buy("names",4).buy("atlas",4));p.setExperienceLevels(100);
var recipes=new RecipeKnowledge();recipes.paid=4;recipes.purchasedAt=123;
recipes.items.add("minecraft:oak_planks");recipes.recipes.add("minecraft:chest");
String catalogue=recipes.json(),food="{\"schema\":1,\"tasted\":[\"minecraft:apple\"],\"paid\":4,\"purchasedAt\":123}",
study="{\"schema\":1,\"paid\":4,\"at\":123}";
p.setAttached(RecipeService.RECORD,catalogue);
p.setAttached(fr.koka.sanctuary.food.FoodKnowledgeService.RECORD,food);
p.setAttached(CompanionStudy.RECEIPT,study);
String progress=p.getAttached(ProgressionService.RECORD);
h.assertTrue(RecipeService.knowledge(p).learned() && fr.koka.sanctuary.food.FoodKnowledgeService.knowledge(p).studied()
&& CompanionStudy.learned(study),"Historical purchases still grant their aptitudes");
h.assertTrue(!RecipeService.buy(p) && !fr.koka.sanctuary.food.FoodKnowledgeService.buy(p) && !CompanionStudy.buy(p)
&& !ProgressionService.purchase(p,"atlas",2) && p.experienceLevel==100,"Old owners never pay again");
h.assertTrue(catalogue.equals(p.getAttached(RecipeService.RECORD)) && food.equals(p.getAttached(fr.koka.sanctuary.food.FoodKnowledgeService.RECORD))
&& study.equals(p.getAttached(CompanionStudy.RECEIPT)) && progress.equals(p.getAttached(ProgressionService.RECORD)),"Original prices, dates and discoveries remain unchanged");
h.succeed();
}
private static void assertPurchase(GameTestHelper h,ServerPlayer p,int cost,java.util.function.BooleanSupplier buy) {
p.setExperienceLevels(cost-1);
h.assertTrue(!buy.getAsBoolean() && p.experienceLevel==cost-1,"One level below price is refused without spending");
p.setExperienceLevels(cost);
h.assertTrue(buy.getAsBoolean() && p.experienceLevel==0,"Exact price buys the aptitude");
p.setExperienceLevels(100);
h.assertTrue(!buy.getAsBoolean() && p.experienceLevel==100,"Duplicate purchase never charges again");
}
@GameTest public void preferencesPreservePurchasesXpDeathAndCycle(GameTestHelper h) {
var record=ProgressRecord.INITIAL;
for(String skill:ProgressRecord.SKILLS)while(record.rank(skill)<ProgressRecord.cap(skill))record=record.buy(skill,record.cost(skill,ProgressionConfig.DEFAULT));
for(String aptitude:ProgressRecord.APTITUDES)record=record.buy(aptitude,4);
var p=player(h,record);p.setExperienceLevels(20);
var p=player(h,record);p.setExperienceLevels(100);
p.setAttached(CompanionService.RECORD,CompanionRecord.EMPTY.purchase(4,1).json());
p.setAttached(CompanionStudy.RECEIPT,"{\"schema\":1,\"paid\":4,\"at\":1}");
p.setAttached(ZoomService.LEARNED,true);
@@ -50,18 +101,18 @@ public final class Aptitudes236GameTests {
h.assertTrue(p.experienceLevel==xp && AptitudeService.preferences(p).equals(AptitudePreferences.DEFAULT),"Reactivation costs no XP");h.succeed();
}
@GameTest public void unknownUnownedPermanentAndStaleRequestsRefused(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL.buy("names",4));p.setExperienceLevels(10);
var p=player(h,ProgressRecord.INITIAL.buy("names",4));p.setExperienceLevels(100);
for(String target:new String[]{"swimming","enchanting","alchemy","food_knowledge","unknown","vitality"})
h.assertTrue(!toggle(p,target,false),"Cannot toggle unowned, permanent or non-aptitude target: "+target);
h.assertTrue(toggle(p,"names",false),"Owned name reveal disables");
h.assertTrue(!AptitudeService.set(p,new AptitudeService.Set("names",true,0)),"Stale mask cannot overwrite a newer choice");
h.assertTrue(!CraftAptitudes.buy(p,"enchanting",99) && !CraftAptitudes.buy(p,"unknown",1),"Stale and unknown purchases refused");
p.setExperienceLevels(3);h.assertTrue(!CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==3,"Insufficient XP never spent");
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==0 && !CraftAptitudes.buy(p,"alchemy",1),"Four levels buy once");
p.setExperienceLevels(24);h.assertTrue(!CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==24,"Insufficient XP never spent");
p.setExperienceLevels(25);h.assertTrue(CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==0 && !CraftAptitudes.buy(p,"alchemy",1),"Twenty-five levels buy once");
h.assertTrue(!AptitudeService.enabled(p,"names"),"Refusal preserves previous choice");h.succeed();
}
@GameTest public void legacyPreferencesRestorePermanentPurchases(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL.buy("names",4).buy("atlas",4));p.setExperienceLevels(12);
var p=player(h,ProgressRecord.INITIAL.buy("names",4).buy("atlas",4));p.setExperienceLevels(55);
h.assertTrue(CraftAptitudes.buy(p,"enchanting",2) && CraftAptitudes.buy(p,"alchemy",2),"Craft purchases exist before legacy migration");
p.setAttached(AptitudeService.PREFERENCES,AptitudePreferences.MAX_MASK);
String proof=p.getAttached(ProgressionService.RECORD);int xp=p.experienceLevel;
@@ -98,7 +149,7 @@ public final class Aptitudes236GameTests {
var p=player(h,ProgressRecord.INITIAL);p.setAttached(AptitudeService.PREFERENCES,2044);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.ENCHANTING_TABLE.defaultBlockState());
var provider=new SimpleMenuProvider((id,inventory,player)->new EnchantmentMenu(id,inventory,ContainerLevelAccess.create(h.getLevel(),at)),Component.literal("Enchanting"));
h.assertTrue(p.openMenu(provider).isEmpty() && p.containerMenu==p.inventoryMenu,"Unlearned player cannot open a native enchanting menu");
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"enchanting",0) && p.experienceLevel==36,"Unlock charges exactly four levels");
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"enchanting",0) && p.experienceLevel==10,"Unlock charges exactly thirty levels");
h.assertTrue(p.openMenu(provider).isPresent(),"Native menu opens after purchase");var menu=(EnchantmentMenu)p.containerMenu;
menu.getSlot(0).set(new ItemStack(Items.DIAMOND_SWORD));menu.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI,3));
h.assertTrue(menu.clickMenuButton(p,0) && !menu.getSlot(0).getItem().get(DataComponents.ENCHANTMENTS).isEmpty(),"Real vanilla enchanting remains functional");
@@ -110,7 +161,7 @@ public final class Aptitudes236GameTests {
var p=player(h,ProgressRecord.INITIAL);p.setAttached(AptitudeService.PREFERENCES,2044);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.BREWING_STAND.defaultBlockState());
var stand=(net.minecraft.world.level.block.entity.BrewingStandBlockEntity)h.getLevel().getBlockEntity(at);
h.assertTrue(p.openMenu(stand).isEmpty(),"Unlearned player cannot open brewing stand");
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"alchemy",0) && p.openMenu(stand).isPresent(),"Alchemy unlock opens the native stand");
p.setExperienceLevels(25);h.assertTrue(CraftAptitudes.buy(p,"alchemy",0) && p.openMenu(stand).isPresent(),"Alchemy unlock opens the native stand");
stand.setItem(3,new ItemStack(Items.NETHER_WART));stand.setItem(4,new ItemStack(Items.BLAZE_POWDER));
var menu=p.containerMenu;h.assertTrue(!toggle(p,"alchemy",false) && p.containerMenu==menu && menu.stillValid(p),"Alchemy cannot be disabled or closed by a toggle request");
h.assertTrue(stand.getItem(3).is(Items.NETHER_WART) && stand.getItem(4).is(Items.BLAZE_POWDER),"Shared ingredients are never removed");
@@ -20,7 +20,7 @@ public final class Atlas005GameTests {
helper.assertTrue(!ProgressionService.purchase(player,"atlas",0),"Insufficient levels cannot unlock map");
var store=AtlasService.observations(player);byte[] known=new byte[256];java.util.Arrays.fill(known,(byte)1);store.observe(new AtlasStore.Tile(-12,7,known));store.save();
helper.assertTrue(store.size()==1 && !ProgressEffects.get(player).atlas(),"Discovery persists independently before aptitude purchase");
player.giveExperienceLevels(8);
player.giveExperienceLevels(ProgressionService.session(helper.getLevel().getServer()).config.atlasCost()+4);
helper.assertTrue(ProgressionService.purchase(player,"atlas",0) && player.experienceLevel==4,"Server charges configurable map cost");
helper.assertTrue(AtlasService.canRead(player,false) && !AtlasService.canRead(player,true),"Map purchase never grants operator inspection");
helper.assertTrue(!ProgressionService.purchase(player,"atlas",0) && !ProgressionService.purchase(player,"atlas",1)
@@ -32,8 +32,8 @@ public final class Companion019GameTests {
h.assertTrue(found.size()==88&&found.size()==CompanionType.values().length,"Exactly 88 native familiar species");h.succeed();
}
@GameTest public void activePurchaseChargesOnceAndCooldownPersists(GameTestHelper h){
var p=player(h,CompanionType.ZOMBIE);p.giveExperienceLevels(10);
h.assertTrue(CompanionService.buy(p)&&p.experienceLevel==6,"Link costs four actual levels");
var p=player(h,CompanionType.ZOMBIE);p.giveExperienceLevels(26);
h.assertTrue(CompanionService.buy(p)&&p.experienceLevel==6,"Link costs twenty actual levels");
h.assertTrue(!CompanionService.buy(p)&&p.experienceLevel==6,"Duplicate purchase does not charge");
long until=CompanionService.now()+120000;CompanionService.save(p,CompanionService.record(p).cooldown(until));
CompanionService.clear(p);h.assertTrue(CompanionService.record(p).unlocked()&&CompanionService.record(p).readyAt()==until,"Lifecycle cache clearing preserves purchase and absolute cooldown");
@@ -142,7 +142,7 @@ public final class Companion032GameTests {
var entries=new TreeMap<>(CompanionConfig.DEFAULT.species());
entries.put("rabbit",new CompanionConfig.Entry(true,CompanionType.Rarity.COMMON,25));
entries.put("witch",new CompanionConfig.Entry(false,CompanionType.Rarity.RARE,37));
var original=new CompanionConfig(1,true,4,10,2,entries).json();java.nio.file.Files.writeString(file,original);
var original=new CompanionConfig(2,true,4,10,2,entries).json().replace("\"schema\": 2","\"schema\": 1");java.nio.file.Files.writeString(file,original);
var migrated=CompanionConfig.load(file);h.assertTrue(migrated.entry(CompanionType.RABBIT).cooldownSeconds()==8,"Legacy default cooldown migrates");
h.assertTrue(migrated.entry(CompanionType.WITCH).cooldownSeconds()==37&&!migrated.entry(CompanionType.WITCH).enabled(),"Custom cooldown, rarity and disable flag survive");
java.nio.file.Files.writeString(file,original);h.assertTrue(CompanionConfig.load(file).entry(CompanionType.RABBIT).cooldownSeconds()==25,"The marker makes migration one-time");
@@ -43,15 +43,15 @@ public final class Drying313GameTests {
}
@GameTest public void bothCraftsAreUnambiguousAndUseSurvivalIngredients(GameTestHelper h){
var raw=List.of(new ItemStack(Items.PORKCHOP),new ItemStack(Items.PORKCHOP),new ItemStack(Cuisine.item("salt")),new ItemStack(AgricultureItems.GARLIC),new ItemStack(Items.STRING));
var wood=List.of(new ItemStack(Items.STICK),new ItemStack(Items.STICK),new ItemStack(Items.STICK),new ItemStack(Items.STRING),ItemStack.EMPTY,new ItemStack(Items.STRING),new ItemStack(Items.OAK_PLANKS),ItemStack.EMPTY,new ItemStack(Items.OAK_PLANKS));
var wood=List.of(new ItemStack(Items.STICK),new ItemStack(Items.STICK),new ItemStack(Items.STICK),new ItemStack(Items.STICK),ItemStack.EMPTY,new ItemStack(Items.STICK),new ItemStack(Items.OAK_PLANKS),ItemStack.EMPTY,new ItemStack(Items.OAK_PLANKS));
for(var input:List.of(Cuisine303GameTests.grid(raw),CraftingInput.of(3,3,wood))){var outputs=new HashSet<Item>();for(var holder:h.getLevel().getServer().getRecipeManager().getRecipes())if(holder.value() instanceof CraftingRecipe r&&r.matches(input,h.getLevel()))outputs.add(r.assemble(input).getItem());h.assertTrue(outputs.size()==1,"Each new survival craft has one unambiguous output");}
var r=(FoodDryingRecipe)Cuisine303GameTests.recipe(h,"sanctuary:dry_sausage");h.assertTrue(r.processingTime()==6000&&r.matches(new SingleRecipeInput(new ItemStack(KitchenAppliances.RAW_SAUSAGE)),h.getLevel())&&r.assemble(new SingleRecipeInput(new ItemStack(KitchenAppliances.RAW_SAUSAGE))).is(KitchenAppliances.CURED_SAUSAGE),"Five-minute data-driven physical recipe");
h.assertTrue(new ItemStack(KitchenAppliances.RAW_SAUSAGE).get(DataComponents.FOOD)==null&&KitchenAppliances.CURED_SAUSAGE instanceof net.minecraft.world.item.BlockItem&&new ItemStack(KitchenAppliances.CURED_SAUSAGE).get(DataComponents.FOOD)==null,"The raw preparation stays inedible and, since beta.315, the cured item is shared after placing");h.succeed();
}
@GameTest public void fourHooksWaitTheirFullFiveMinutesAndTransformExactlyOnce(GameTestHelper h){
var r=rack(h);hang(h,r,4);h.assertTrue(r.insertIngredient(new ItemStack(KitchenAppliances.RAW_SAUSAGE))==0,"Four occupied hooks cannot swallow a fifth preparation");tick(h,r,5999);
for(int i=0;i<4;i++)h.assertTrue(r.getItem(i).is(KitchenAppliances.RAW_SAUSAGE)&&!r.ready(i)&&r.progress(i)==5999,"No early dried product");tick(h,r,1);h.assertTrue(r.readyCount()==4&&r.analogOutputSignal()==15,"Four paid products ready at the exact full deadline");tick(h,r,7000);
for(int i=0;i<4;i++)h.assertTrue(r.getItem(i).is(KitchenAppliances.CURED_SAUSAGE)&&r.getItem(i).getCount()==1,"No repeated drying or duplication");h.succeed();
@GameTest public void twoHooksWaitTheirFullFiveMinutesAndTransformExactlyOnce(GameTestHelper h){
var r=rack(h);hang(h,r,2);h.assertTrue(r.insertIngredient(new ItemStack(KitchenAppliances.RAW_SAUSAGE))==0,"Two occupied hooks cannot swallow a third preparation");tick(h,r,5999);
for(int i=0;i<2;i++)h.assertTrue(r.getItem(i).is(KitchenAppliances.RAW_SAUSAGE)&&!r.ready(i)&&r.progress(i)==5999,"No early dried product");tick(h,r,1);h.assertTrue(r.readyCount()==2&&r.analogOutputSignal()==15,"Two paid products ready at the exact full deadline");tick(h,r,7000);
for(int i=0;i<2;i++)h.assertTrue(r.getItem(i).is(KitchenAppliances.CURED_SAUSAGE)&&r.getItem(i).getCount()==1,"No repeated drying or duplication");h.succeed();
}
@GameTest public void hooksAreIndependentAndRemovingOneRestartsOnlyThatPreparation(GameTestHelper h){
var r=rack(h);hang(h,r,1);tick(h,r,1000);hang(h,r,1);tick(h,r,500);h.assertTrue(r.progress(0)==1500&&r.progress(1)==500,"Each addition has its own start");var removed=r.removeItem(0,1);h.assertTrue(removed.is(KitchenAppliances.RAW_SAUSAGE)&&r.progress(1)==500,"Raw cancellation returns the actual item without changing another hook");r.insertIngredient(removed);tick(h,r,100);h.assertTrue(r.progress(0)==100&&r.progress(1)==600,"Rehanging begins a full new drying cycle");h.succeed();
@@ -66,9 +66,9 @@ public final class Drying313GameTests {
var r=rack(h);hang(h,r,1);tick(h,r,123);var block=(SimpleWaterloggedBlock)KitchenAppliances.DRYING_RACK;
h.assertTrue(block.placeLiquid(h.getLevel(),r.getBlockPos(),r.getBlockState(),Fluids.WATER.getSource(false)),"Native waterlogging accepted");tick(h,r,500);h.assertTrue(r.paused()&&r.progress(0)==123,"Immersion preserves unpaid work");var water=block.pickupBlock(null,h.getLevel(),r.getBlockPos(),r.getBlockState());h.assertTrue(water.is(Items.WATER_BUCKET),"Native empty-bucket behavior recovers exactly the water");tick(h,r,1);h.assertTrue(!r.paused()&&r.progress(0)==124,"Drying resumes after draining");h.succeed();
}
@GameTest public void nativeSavePreservesFourTimersAndNeverPaysOfflineElapsedTime(GameTestHelper h){
var r=rack(h);for(int i=0;i<4;i++){hang(h,r,1);tick(h,r,100);}
var c=reload(h,r);h.assertTrue(c.progress(0)==400&&c.progress(3)==100&&c.getContainerSize()==4,"Four native item positions and independent timers reload unchanged");long now=h.getLevel().getGameTime();try{((net.minecraft.world.level.storage.ServerLevelData)h.getLevel().getLevelData()).setGameTime(now+100000);tick(h,c,1);h.assertTrue(c.progress(0)==401&&!c.ready(0),"Offline world time cannot finish loaded-tick drying");}finally{((net.minecraft.world.level.storage.ServerLevelData)h.getLevel().getLevelData()).setGameTime(now);}h.succeed();
@GameTest public void nativeSavePreservesTwoTimersAndNeverPaysOfflineElapsedTime(GameTestHelper h){
var r=rack(h);for(int i=0;i<2;i++){hang(h,r,1);tick(h,r,100);}
var c=reload(h,r);h.assertTrue(c.progress(0)==200&&c.progress(1)==100&&c.getContainerSize()==4,"Two active timers reload in backwards-compatible version-1 storage");long now=h.getLevel().getGameTime();try{((net.minecraft.world.level.storage.ServerLevelData)h.getLevel().getLevelData()).setGameTime(now+100000);tick(h,c,1);h.assertTrue(c.progress(0)==201&&!c.ready(0),"Offline world time cannot finish loaded-tick drying");}finally{((net.minecraft.world.level.storage.ServerLevelData)h.getLevel().getLevelData()).setGameTime(now);}h.succeed();
}
@GameTest public void collectingAReadySausageDoesNotCancelAnotherPendingHook(GameTestHelper h){
var r=rack(h);hang(h,r,1);tick(h,r,5900);hang(h,r,1);tick(h,r,100);var p=Progression003GameTests.player(h);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);h.getLevel().getBlockState(r.getBlockPos()).useWithoutItem(h.getLevel(),p,new BlockHitResult(Vec3.atCenterOf(r.getBlockPos()),Direction.UP,r.getBlockPos(),false));
@@ -81,9 +81,9 @@ public final class Drying313GameTests {
@GameTest public void nativeHoppersOnlyExtractCompletedProductsAndCanRefillHooks(GameTestHelper h){
var r=rack(h);var above=r.getBlockPos().above();var topState=Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING,Direction.DOWN);h.getLevel().setBlockAndUpdate(above,topState);var top=(HopperBlockEntity)h.getLevel().getBlockEntity(above);top.setItem(0,new ItemStack(KitchenAppliances.RAW_SAUSAGE,4));
var below=r.getBlockPos().below();var bottomState=Blocks.HOPPER.defaultBlockState();h.getLevel().setBlockAndUpdate(below,bottomState);var bottom=(HopperBlockEntity)h.getLevel().getBlockEntity(below);
for(int i=0;i<50;i++){HopperBlockEntity.pushItemsTick(h.getLevel(),above,topState,top);HopperBlockEntity.pushItemsTick(h.getLevel(),below,bottomState,bottom);tick(h,r,1);}h.assertTrue(top.isEmpty()&&bottom.isEmpty()&&r.dryingCount()==4,"Real hopper transfers fill one item per hook without extracting unfinished food");
for(int i=0;i<50;i++){HopperBlockEntity.pushItemsTick(h.getLevel(),above,topState,top);HopperBlockEntity.pushItemsTick(h.getLevel(),below,bottomState,bottom);tick(h,r,1);}h.assertTrue(top.getItem(0).getCount()==2&&bottom.isEmpty()&&r.dryingCount()==2,"Real hopper transfers fill one item per hook without extracting unfinished food");
tick(h,r,6000);for(int i=0;i<50;i++)HopperBlockEntity.pushItemsTick(h.getLevel(),below,bottomState,bottom);
int count=0;for(int i=0;i<5;i++)if(bottom.getItem(i).is(KitchenAppliances.CURED_SAUSAGE))count+=bottom.getItem(i).getCount();h.assertTrue(count==4&&r.isEmpty(),"Only the four completed native items reach the lower hopper");h.succeed();
int count=0;for(int i=0;i<5;i++)if(bottom.getItem(i).is(KitchenAppliances.CURED_SAUSAGE))count+=bottom.getItem(i).getCount();h.assertTrue(count==2&&r.isEmpty(),"Only the two completed native items reach the lower hopper");h.succeed();
}
@GameTest public void unrelatedItemsAreRefusedAndFinishedStatePersistsAcrossStorage(GameTestHelper h){
var r=rack(h);h.assertTrue(r.insertIngredient(new ItemStack(Items.COAL))==0&&r.insertIngredient(new ItemStack(Items.STRING))==0,"Neither fuel nor unrelated items can bypass the food recipes");hang(h,r,1);tick(h,r,6000);var c=reload(h,r);tick(h,c,6000);h.assertTrue(c.readyCount()==1&&c.takeItem().is(KitchenAppliances.CURED_SAUSAGE)&&c.takeItem().isEmpty(),"A saved finished hook gives exactly one product, once");h.succeed();
@@ -51,22 +51,21 @@ public final class Drying317GameTests {
}h.succeed();
}
@GameTest public void fourDifferentFoodsFinishIndependentlyAndHopperTakesOnlyReadyOutputs(GameTestHelper h){
var r=rack(h);for(Item i:List.of(AgricultureItems.BASIL,AgricultureItems.GRAPES,Items.COD,Items.BEEF))r.insertIngredient(new ItemStack(i));
tick(h,r,1200);h.assertTrue(r.readyCount()==1&&r.getItem(0).is(DriedFoods.item("dried_basil")),"One minute herbs, other hooks continue");
tick(h,r,600);h.assertTrue(r.readyCount()==2&&r.getItem(1).is(DriedFoods.item("raisins")),"Ninety second raisins");
@GameTest public void twoDifferentFoodsFinishIndependentlyAndHopperTakesOnlyReadyOutputs(GameTestHelper h){
var r=rack(h);for(Item item:List.of(AgricultureItems.BASIL,Items.COD))h.assertTrue(r.insertIngredient(new ItemStack(item))==1,"Both independent hooks accept one food");
h.assertTrue(r.insertIngredient(new ItemStack(Items.BEEF))==0,"No third hook");
tick(h,r,1200);h.assertTrue(r.readyCount()==1&&r.getItem(0).is(DriedFoods.item("dried_basil"))&&!r.ready(1),"One minute herbs, fish continues");
var p=r.getBlockPos().below();var state=Blocks.HOPPER.defaultBlockState();h.getLevel().setBlockAndUpdate(p,state);var hopper=(HopperBlockEntity)h.getLevel().getBlockEntity(p);
for(int i=0;i<40;i++)HopperBlockEntity.pushItemsTick(h.getLevel(),p,state,hopper);
h.assertTrue(r.getItem(0).isEmpty()&&r.getItem(1).isEmpty()&&r.getItem(2).is(Items.COD)&&r.getItem(3).is(Items.BEEF),"Hopper leaves both unfinished products");
tick(h,r,600);h.assertTrue(r.ready(2)&&r.getItem(2).is(DriedFoods.item("dried_cod"))&&!r.ready(3),"Two minute fish");
tick(h,r,2400);h.assertTrue(r.ready(3)&&r.getItem(3).is(DriedFoods.item("dried_meat")),"Four minute meat");h.succeed();
h.assertTrue(r.getItem(0).isEmpty()&&r.getItem(1).is(Items.COD)&&r.progress(1)==1200,"Hopper leaves unfinished fish and its exact progress");
tick(h,r,1200);h.assertTrue(r.ready(1)&&r.getItem(1).is(DriedFoods.item("dried_cod")),"Two minute fish");h.succeed();
}
@GameTest public void mixedSavedHooksKeepFinishedAndPausedWorkWithoutOfflineCatchup(GameTestHelper h){
var r=rack(h);for(Item i:List.of(AgricultureItems.MINT,Items.SALMON,Items.PORKCHOP,AgricultureItems.GREEN_VANILLA_POD))r.insertIngredient(new ItemStack(i));
var r=rack(h);for(Item i:List.of(AgricultureItems.MINT,Items.SALMON))r.insertIngredient(new ItemStack(i));
tick(h,r,1300);h.getLevel().setBlockAndUpdate(r.getBlockPos(),r.getBlockState().setValue(DryingRackBlock.WATERLOGGED,true));tick(h,r,200);
var copy=reload(h,r);h.assertTrue(copy.ready(0)&&copy.getItem(0).is(DriedFoods.item("dried_mint")),"Saved finished ingredient stays ready");
for(int i=1;i<4;i++)h.assertTrue(copy.progress(i)==1300,"A wet loaded hook does not advance");
for(int i=1;i<2;i++)h.assertTrue(copy.progress(i)==1300,"A wet loaded hook does not advance");
tick(h,copy,8000);h.assertTrue(copy.progress(1)==1300&&!copy.ready(1),"Wet time after reload cannot cure fish");
h.getLevel().setBlockAndUpdate(copy.getBlockPos(),copy.getBlockState().setValue(DryingRackBlock.WATERLOGGED,false));tick(h,copy,1100);
h.assertTrue(copy.ready(1)&&!copy.ready(2)&&!copy.ready(3),"Each original duration resumes independently");h.succeed();
@@ -0,0 +1,74 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.cuisine.KitchenAppliances;
import fr.koka.sanctuary.cuisine.culinary.block.DryingRackBlock;
import fr.koka.sanctuary.cuisine.culinary.block.entity.DryingRackBlockEntity;
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.network.chat.Component;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.AABB;
import java.util.List;
public final class Drying322GameTests {
private static DryingRackBlockEntity oldRack(GameTestHelper h){
var pos=h.absolutePos(new BlockPos(3,8,3));var level=h.getLevel();
level.setBlockAndUpdate(pos.below(),Blocks.STONE.defaultBlockState());
level.setBlockAndUpdate(pos,KitchenAppliances.DRYING_RACK.defaultBlockState().setValue(DryingRackBlock.WATERLOGGED,true));
var rack=(DryingRackBlockEntity)level.getBlockEntity(pos);
for(int i=0;i<4;i++)rack.setItem(i,new ItemStack(i==3?KitchenAppliances.CURED_SAUSAGE:KitchenAppliances.RAW_SAUSAGE));
rack.getItem(2).set(DataComponents.CUSTOM_NAME,Component.literal("Ancien lot"));
var tag=rack.saveWithFullMetadata(level.registryAccess());tag.putInt("DryingVersion",1);
for(int i=0;i<4;i++){
tag.putInt("DryingProgress"+i,i==3?6000:1500-i*500);tag.putInt("DryingDuration"+i,6000);
tag.putString("DryingRecipe"+i,"sanctuary:dry_sausage");tag.putBoolean("DryingReady"+i,i==3);
}
var restored=(DryingRackBlockEntity)BlockEntity.loadStatic(pos,rack.getBlockState(),tag,level.registryAccess());
restored.setLevel(level);level.setBlockEntity(restored);return restored;
}
private static void tick(GameTestHelper h,DryingRackBlockEntity rack){DryingRackBlockEntity.serverTick(h.getLevel(),rack.getBlockPos(),rack.getBlockState(),rack);}
@GameTest public void oldFourItemSaveKeepsSurplusAndProgressUntilAnActiveHookIsFree(GameTestHelper h){
var rack=oldRack(h);tick(h,rack);
h.assertTrue(rack.legacyCount()==2&&rack.progress(0)==1500&&rack.progress(2)==500&&rack.ready(3),"Version-1 objects and work survive loading and immersion");
h.assertTrue(rack.insertIngredient(new ItemStack(KitchenAppliances.RAW_SAUSAGE))==0&&!rack.canPlaceItem(2,new ItemStack(KitchenAppliances.RAW_SAUSAGE)),"No new input enters a legacy reserve");
rack.removeItem(0,1);tick(h,rack);
h.assertTrue(rack.progress(0)==500&&rack.legacyCount()==1&&rack.getItem(2).isEmpty()&&rack.getItem(0).getHoverName().getString().equals("Ancien lot"),"Promotion preserves the original stack components and progress");
rack.removeItem(1,1);tick(h,rack);
h.assertTrue(rack.ready(1)&&rack.getItem(1).is(KitchenAppliances.CURED_SAUSAGE)&&rack.legacyCount()==0,"An old finished product is preserved without another transformation");
h.getLevel().setBlockAndUpdate(rack.getBlockPos(),rack.getBlockState().setValue(DryingRackBlock.WATERLOGGED,false));
h.getLevel().setBlockAndUpdate(rack.getBlockPos().above(),Blocks.STONE.defaultBlockState());tick(h,rack);
h.assertTrue(rack.progress(0)==501&&rack.progress(1)==6000,"Only unfinished active food resumes on dry loaded ticks");
rack.takeItem();h.assertTrue(rack.insertIngredient(new ItemStack(KitchenAppliances.RAW_SAUSAGE))==1&&rack.insertIngredient(new ItemStack(KitchenAppliances.RAW_SAUSAGE))==0,"After legacy recovery the rack has exactly two usable positions");
h.succeed();
}
@GameTest public void anOldWaitingFoodNeverAdvancesAndBreakageReturnsEveryPaidItemOnce(GameTestHelper h){
var rack=oldRack(h);h.getLevel().setBlockAndUpdate(rack.getBlockPos(),rack.getBlockState().setValue(DryingRackBlock.WATERLOGGED,false));h.getLevel().setBlockAndUpdate(rack.getBlockPos().above(),Blocks.STONE.defaultBlockState());
for(int i=0;i<100;i++)tick(h,rack);
h.assertTrue(rack.progress(0)==1600&&rack.progress(1)==1100&&rack.progress(2)==500,"At most two preparations work simultaneously");
var pos=rack.getBlockPos();h.getLevel().destroyBlock(pos,true);h.getLevel().destroyBlock(pos,true);
var drops=h.getLevel().getEntitiesOfClass(ItemEntity.class,new AABB(pos).inflate(2),e->e.isAlive());
int raw=0,cooked=0,frames=0;for(var entity:drops){var item=entity.getItem();if(item.is(KitchenAppliances.RAW_SAUSAGE))raw+=item.getCount();if(item.is(KitchenAppliances.CURED_SAUSAGE))cooked+=item.getCount();if(item.is(KitchenAppliances.DRYING_RACK.asItem()))frames+=item.getCount();}
h.assertTrue(raw==3&&cooked==1&&frames==1,"Breaking a legacy rack never discards or duplicates reserve food");h.succeed();
}
@GameTest public void nativeCraftsPayWoodAndPlainStoneWithoutStringOrAStonecutter(GameTestHelper h){
var player=Progression003GameTests.player(h);var pos=h.absolutePos(new BlockPos(2,3,2));h.getLevel().setBlockAndUpdate(pos,Blocks.CRAFTING_TABLE.defaultBlockState());
var rack=List.of(Items.STICK,Items.STICK,Items.STICK,Items.STICK,Items.AIR,Items.STICK,Items.OAK_PLANKS,Items.AIR,Items.OAK_PLANKS);
var mill=List.of(Items.SMOOTH_STONE_SLAB,Items.SMOOTH_STONE_SLAB,Items.SMOOTH_STONE_SLAB,Items.COPPER_INGOT,Items.SMOOTH_STONE,Items.COPPER_INGOT,Items.AIR,Items.STICK,Items.AIR);
for(var grid:List.of(rack,mill)){
var menu=new CraftingMenu(1,player.getInventory(),ContainerLevelAccess.create(h.getLevel(),pos));
for(int i=0;i<9;i++)menu.getSlot(i+1).set(new ItemStack(grid.get(i)));
var expected=grid==rack?KitchenAppliances.DRYING_RACK:KitchenAppliances.REDSTONE_MILL;
h.assertTrue(menu.getSlot(0).getItem().is(expected.asItem()),"The native crafting table resolves the new recipe");
menu.clicked(0,0,ContainerInput.PICKUP,player);
h.assertTrue(menu.getCarried().is(expected.asItem())&&menu.getCarried().getCount()==1,"One paid appliance");
for(int i=1;i<=9;i++)h.assertTrue(menu.getSlot(i).getItem().isEmpty(),"Every material is paid exactly once");menu.setCarried(ItemStack.EMPTY);menu.removed(player);
}h.succeed();
}
}
@@ -103,7 +103,7 @@ public final class Familiar020GameTests {
h.assertTrue(FamiliarService.active(p).getCustomName().getString().equals("Nuage"),"Reequipped named egg keeps the name");h.succeed();
}
@GameTest public void studyChargesOnceAndSurvivesDeathAndPrestige(GameTestHelper h) throws Exception {
var p=player(h);p.giveExperienceLevels(10);
var p=player(h);p.giveExperienceLevels(21);
h.assertTrue(CompanionStudy.buy(p)&&p.experienceLevel==6&&!CompanionStudy.buy(p)&&p.experienceLevel==6,"Study purchase charges exactly once");
var output=output(h);p.saveWithoutId(output);var loaded=player(h);
loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
@@ -123,8 +123,8 @@ public final class Familiar020GameTests {
var p=player(h);h.assertTrue(CompanionService.record(p).acceptAllies(),"New inhabitants accept familiar assistance");
CompanionService.save(p,CompanionService.record(p).consent(false));CompanionService.clear(p);
h.assertTrue(!CompanionService.record(p).acceptAllies(),"An existing refusal is respected");
String raw="{\"schema\":99}";p.setAttached(CompanionStudy.RECEIPT,raw);p.giveExperienceLevels(10);
h.assertTrue(!CompanionStudy.buy(p)&&p.experienceLevel==10&&p.getAttached(CompanionStudy.RECEIPT).equals(raw),"Bad study receipt is preserved and never charged");h.succeed();
String raw="{\"schema\":99}";p.setAttached(CompanionStudy.RECEIPT,raw);p.giveExperienceLevels(20);
h.assertTrue(!CompanionStudy.buy(p)&&p.experienceLevel==20&&p.getAttached(CompanionStudy.RECEIPT).equals(raw),"Bad study receipt is preserved and never charged");h.succeed();
}
@GameTest public void divingRequiresSprintTakeoffAndCannotRepeat(GameTestHelper h){
var p=player(h);p.setOnGround(false);p.setDeltaMovement(0,.3,.3);
@@ -29,17 +29,17 @@ public final class FoodKnowledge039GameTests {
h.assertTrue(!FoodKnowledgeService.knows(p,new ItemStack(Items.COOKED_BEEF)),"Raw and cooked foods are distinct");
h.assertTrue(!FoodKnowledgeService.knows(player(h),apple),"Knowledge belongs to the eater");h.succeed();
}
@GameTest public void studyCostsExactlyFourAndCannotBeRepurchased(GameTestHelper h){
var p=player(h);p.setExperienceLevels(3);h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==3,"Insufficient XP retained");p.setExperienceLevels(9);
h.assertTrue(FoodKnowledgeService.buy(p)&&p.experienceLevel==5,"Buy four levels");var raw=p.getAttached(FoodKnowledgeService.RECORD);
h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==5&&raw.equals(p.getAttached(FoodKnowledgeService.RECORD)),"Duplicate buy neither charges nor changes receipt");
@GameTest public void studyCostsExactlySixAndCannotBeRepurchased(GameTestHelper h){
var p=player(h);p.setExperienceLevels(5);h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==5,"Insufficient XP retained");p.setExperienceLevels(9);
h.assertTrue(FoodKnowledgeService.buy(p)&&p.experienceLevel==3,"Buy six levels");var raw=p.getAttached(FoodKnowledgeService.RECORD);
h.assertTrue(!FoodKnowledgeService.buy(p)&&p.experienceLevel==3&&raw.equals(p.getAttached(FoodKnowledgeService.RECORD)),"Duplicate buy neither charges nor changes receipt");
h.assertTrue(FoodKnowledgeService.knows(p,new ItemStack(Items.GOLDEN_CARROT)),"Study reveals untasted food");h.succeed();
}
@GameTest public void knowledgeAndReceiptSurviveSaveDeathAndPrestige(GameTestHelper h)throws Exception{
var p=player(h);eat(p,Items.APPLE);p.setExperienceLevels(12);h.assertTrue(FoodKnowledgeService.buy(p),"Study purchased");
var raw=p.getAttached(FoodKnowledgeService.RECORD);var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(out);
var loaded=player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),out.buildResult()));loaded.setUUID(p.getUUID());
h.assertTrue(raw.equals(loaded.getAttached(FoodKnowledgeService.RECORD))&&loaded.experienceLevel==8,"Knowledge and payment save together");
h.assertTrue(raw.equals(loaded.getAttached(FoodKnowledgeService.RECORD))&&loaded.experienceLevel==6,"Knowledge and payment save together");
var respawn=player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(loaded,false);ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(loaded,respawn,false);
h.assertTrue(raw.equals(respawn.getAttached(FoodKnowledgeService.RECORD)),"Death preserves attachment");
var session=ProgressionService.session(p.level().getServer());session.inhabitants.create(p.getUUID(),"food"+p.getUUID().toString().substring(0,8),"",session.inhabitants.available().getFirst());
@@ -22,9 +22,9 @@ public final class Forgerie238GameTests {
for(String skill:ProgressRecord.SKILLS)while(r.rank(skill)<ProgressRecord.cap(skill))r=r.buy(skill,r.cost(skill,ProgressionConfig.DEFAULT));
var p=player(h,r);String proof=p.getAttached(ProgressionService.RECORD);
h.assertTrue(!p.hasAttached(CraftAptitudes.SMITHING) && !CraftAptitudes.learned(p,"smithing"),"Existing players start without the new purchase");
p.setExperienceLevels(3);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==3,"Three levels cannot unlock Forgerie");
p.setExperienceLevels(4);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()-1) && p.experienceLevel==4,"Stale purchase cannot spend XP");
h.assertTrue(CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==0,"Four levels acquire Forgerie once");
p.setExperienceLevels(19);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==19,"Nineteen levels cannot unlock Forgerie");
p.setExperienceLevels(20);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()-1) && p.experienceLevel==20,"Stale purchase cannot spend XP");
h.assertTrue(CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==0,"Twenty levels acquire Forgerie once");
p.setExperienceLevels(8);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==8,"Repeat purchase costs nothing");
h.assertTrue(!AptitudeService.set(p,new AptitudeService.Set("smithing",false,0)) && CraftAptitudes.allowed(p,"smithing"),"Bought Forgerie has no disable request");
h.assertTrue(!CraftAptitudes.learned(p,"enchanting") && !CraftAptitudes.learned(p,"alchemy") && proof.equals(p.getAttached(ProgressionService.RECORD)),"Other purchases and progression proofs stay independent");
@@ -42,7 +42,7 @@ public final class Forgerie238GameTests {
h.getLevel().setBlockAndUpdate(at,block.defaultBlockState());
h.assertTrue(p.openMenu(h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at)).isEmpty(),"Every anvil variant is locked before purchase");
}
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.experienceLevel==36,"Anvil unlock charges four levels");
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.experienceLevel==20,"Anvil unlock charges twenty levels");
for(var block:new net.minecraft.world.level.block.Block[]{Blocks.ANVIL,Blocks.CHIPPED_ANVIL,Blocks.DAMAGED_ANVIL}) {
h.getLevel().setBlockAndUpdate(at,block.defaultBlockState());
h.assertTrue(p.openMenu(h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at)).isPresent(),"Every acquired anvil variant opens");
@@ -60,7 +60,7 @@ public final class Forgerie238GameTests {
var p=player(h,ProgressRecord.INITIAL);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.SMITHING_TABLE.defaultBlockState());
var provider=h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at);
h.assertTrue(p.openMenu(provider).isEmpty(),"Smithing table is locked before purchase");
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.openMenu(provider).isPresent(),"Same purchase opens smithing table");
p.setExperienceLevels(20);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.openMenu(provider).isPresent(),"Same purchase opens smithing table");
var menu=(SmithingMenu)p.containerMenu;
inputs(menu,Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE,Items.DIAMOND_SWORD,Items.NETHERITE_INGOT);
var upgrade=menu.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p);
@@ -91,7 +91,7 @@ public final class Forgerie238GameTests {
AccessoryAccess.of(owner).setItem(AccessoryContainer.HEAD,new ItemStack(item));
HeadBlockService.use(actor,owner,false);h.assertTrue(actor.containerMenu==actor.inventoryMenu,"Worn workstation cannot bypass the visitor's locked aptitude: "+item);
}
actor.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(actor,"smithing",0),"Visitor purchases Forgerie");
actor.setExperienceLevels(20);h.assertTrue(CraftAptitudes.buy(actor,"smithing",0),"Visitor purchases Forgerie");
for(var item:items) {
AccessoryAccess.of(owner).setItem(AccessoryContainer.HEAD,new ItemStack(item));
h.assertTrue(HeadBlockService.use(actor,owner,false) && actor.containerMenu!=actor.inventoryMenu,"Visitor uses acquired worn workstation: "+item);
@@ -0,0 +1,263 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.chicken.*;
import fr.koka.sanctuary.boats.*;
import fr.koka.sanctuary.cosmetics.*;
import fr.koka.sanctuary.progression.*;
import java.util.*;
import net.fabricmc.fabric.api.event.player.UseEntityCallback;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.*;
import net.minecraft.core.dispenser.BlockSource;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.*;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.animal.chicken.Chicken;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg;
import net.minecraft.world.entity.player.Input;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.DispenserBlockEntity;
import net.minecraft.world.level.storage.*;
import net.minecraft.world.phys.*;
public final class GemChicken323GameTests {
@GameTest public void everyRareChickenGlidesWhenCarriedLikeAnOrdinaryChicken(GameTestHelper h) {
var player = flightPlayer(h);
try {
for (var variant : GemChickenVariant.values()) for (boolean baby : new boolean[]{false, true}) {
var bird = chicken(h, variant); bird.setBaby(baby);
bird.setPos(player.position().add(0, 0, 1)); h.getLevel().addFreshEntity(bird);
int eggTime = bird.eggTime;
try {
h.assertTrue(CarryService.interact(player, bird), "Pick up " + variant + ", baby=" + baby);
player.setOnGround(false); player.setDeltaMovement(.2, -2, .1); player.fallDistance = 32;
CarryGlide.tick(player);
h.assertTrue(CarryGlide.active(player) && player.getDeltaMovement().equals(new Vec3(.2, -CarryGlide.DESCENT, .1))
&& player.fallDistance == 0, "Same glide speed, direction and fall protection as an ordinary chicken");
player.setDeltaMovement(0, .3, 0); CarryGlide.tick(player);
h.assertTrue(player.getDeltaMovement().y == .3, "No extra upward boost");
h.assertTrue(bird.getGemVariant() == variant && bird.isBaby() == baby && bird.eggTime == eggTime
&& !bird.canFallInLove(), "Carrying preserves the rare chicken's own rules and state");
CarryService.release(bird, true);
player.setDeltaMovement(0, -2, 0); player.fallDistance = 8; CarryGlide.tick(player);
h.assertTrue(!CarryGlide.supported(player) && player.getDeltaMovement().y == -2 && player.fallDistance == 8,
"Ordinary falling returns immediately after putting the chicken down");
} finally { CarryService.detach(player); bird.discard(); }
}
} finally { CarryService.detach(player); }
h.succeed();
}
@GameTest public void everyRareChickenBoardsAndPowersOrdinaryAndCrewBoats(GameTestHelper h) {
var player = flightPlayer(h);
var origin = player.position();
for (var variant : GemChickenVariant.values()) for (boolean baby : new boolean[]{false, true})
for (int side : new int[]{1, 2, 3}) {
player.setPos(origin);
var boat = side == 1 ? EntityTypes.OAK_BOAT.create(h.getLevel(), EntitySpawnReason.COMMAND)
: CrewBoats.variant("oak", side).type().create(h.getLevel(), EntitySpawnReason.COMMAND);
var bird = chicken(h, variant); bird.setBaby(baby);
boat.setPos(player.position().add(0, 0, 2)); bird.setPos(player.position().add(1, 0, 0));
h.getLevel().addFreshEntity(boat); h.getLevel().addFreshEntity(bird);
try {
h.assertTrue(CarryService.interact(player, bird), "Pick up rare chicken before boarding");
var result = UseEntityCallback.EVENT.invoker().interact(player, h.getLevel(), InteractionHand.MAIN_HAND, boat, null);
h.assertTrue(result == InteractionResult.SUCCESS && bird.getVehicle() == boat && !CarryService.marked(bird),
"Existing head-to-boat gesture works for " + variant + ", baby=" + baby + ", size=" + side);
h.assertTrue(player.startRiding(boat, true, true) && boat.isFlyingVehicle(), "Pilot and native flight validation accept the chicken");
h.assertTrue(FlightMotors.engines(boat).equals(FlightBalance.Engines.NONE.add(FlightBalance.Kind.CHICKEN, false))
&& FlightMotors.load(boat) == 1, "Exactly the ordinary chicken's propulsion and passenger weight");
player.setLastClientInput(new Input(false, false, false, false, true, false, false));
boat.setDeltaMovement(Vec3.ZERO);
for (int i = 0; i < 30; i++) ChickenBoat.floatBoat(boat);
h.assertTrue(boat.getDeltaMovement().y > 0, "Holding jump makes the boat climb");
player.setLastClientInput(Input.EMPTY);
for (int i = 0; i < 60; i++) ChickenBoat.floatBoat(boat);
h.assertTrue(boat.getDeltaMovement().y < 0, "Releasing jump makes the boat descend");
bird.stopRiding();
h.assertTrue(FlightMotors.engines(boat).count() == 0 && !boat.isFlyingVehicle(), "Removing the last chicken stops animal-powered flight");
} finally {
CarryService.detach(player); player.stopRiding(); player.setLastClientInput(Input.EMPTY);
bird.discard(); boat.discard();
}
}
h.succeed();
}
private static ServerPlayer flightPlayer(GameTestHelper h) {
var player = Progression003GameTests.player(h);
player.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
player.setAttached(ProgressionService.RECORD, ProgressRecord.INITIAL.json());
ProgressEffects.apply(player, ProgressRecord.INITIAL);
var at = player.blockPosition();
for (int x = -5; x <= 5; x++) for (int z = -5; z <= 5; z++) for (int y = -1; y < 10; y++)
h.getLevel().setBlockAndUpdate(at.offset(x, y, z), (y == -1 ? Blocks.STONE : Blocks.AIR).defaultBlockState());
return player;
}
private static GemChickenEntity chicken(GameTestHelper h, GemChickenVariant variant) {
var chicken = GemChickens.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
chicken.setGemVariant(variant); chicken.setNoAi(true);
chicken.setPos(Vec3.atCenterOf(h.absolutePos(new BlockPos(2, 2, 2))));
return chicken;
}
private static List<ItemEntity> drops(GameTestHelper h) {
return h.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(h.absolutePos(BlockPos.ZERO)).inflate(5));
}
@GameTest public void firstAndSubsequentLaysUseFullIntervalsAndOnlyTheirProduct(GameTestHelper h) {
for (var v : GemChickenVariant.values()) {
var bird = chicken(h, v); int factor = v.intervalMultiplier();
h.assertTrue(bird.eggTime >= 6000 * factor && bird.eggTime < 12000 * factor,
"Full first interval for " + v);
bird.setBaby(true); int first = bird.eggTime; bird.aiStep();
h.assertTrue(bird.eggTime == first && drops(h).isEmpty(), "Babies do not lay or count down");
bird.setBaby(false);
for (int i = 0; i < 2; i++) {
bird.eggTime = 1; bird.aiStep();
var items = drops(h);
h.assertTrue(items.size() == 1 && items.getFirst().getItem().is(v.product())
&& items.getFirst().getItem().getCount() == 1, "Exactly one correct product for " + v);
int interval = bird.eggTime;
h.assertTrue(interval >= 6000 * factor && interval < 12000 * factor, "Full reset interval for " + v);
bird.aiStep();
h.assertTrue(bird.eggTime == interval - 1 && drops(h).size() == 1, "Interval expands exactly once");
items.forEach(Entity::discard);
}
}
h.succeed();
}
@GameTest public void nativeReloadPreservesVariantAgeNameAndRemainingTime(GameTestHelper h) {
for (var v : GemChickenVariant.values()) {
var bird = chicken(h, v); bird.eggTime = 12345; bird.setAge(-1234);
bird.setCustomName(Component.literal("Gemme " + v));
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
h.assertTrue(bird.save(output), "Rare chicken saves natively");
var input = TagValueInput.create(ProblemReporter.DISCARDING, h.getLevel().registryAccess(), output.buildResult());
var loaded = (GemChickenEntity) EntityType.loadEntityRecursive(input, h.getLevel(), EntitySpawnReason.LOAD, e -> e);
h.assertTrue(loaded != null && loaded.getGemVariant() == v && loaded.eggTime == 12345
&& loaded.getAge() == -1234 && loaded.getName().equals(bird.getName())
&& loaded.getUUID().equals(bird.getUUID()), "Native reload retains all chicken state for " + v);
loaded.setBaby(false); loaded.aiStep();
h.assertTrue(loaded.eggTime == 12344, "Reload resumes remaining time without rescaling");
}
h.succeed();
}
@GameTest public void allVariantsRejectSeedsLoveAndPureOrMixedBreeding(GameTestHelper h) {
var player = Progression003GameTests.player(h);
var ordinary = EntityTypes.CHICKEN.create(h.getLevel(), EntitySpawnReason.COMMAND);
for (var v : GemChickenVariant.values()) {
var bird = chicken(h, v);
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.WHEAT_SEEDS, 4));
bird.mobInteract(player, InteractionHand.MAIN_HAND); bird.setInLove(player);
h.assertTrue(!bird.canFallInLove() && !bird.isInLove() && player.getMainHandItem().getCount() == 4,
"Adult seeds cannot start love or consume food for " + v);
for (var other : GemChickenVariant.values()) {
var partner = chicken(h, other); partner.setInLoveTime(600); bird.setInLoveTime(600);
h.assertTrue(!bird.canMate(partner) && !partner.canMate(bird)
&& bird.getBreedOffspring(h.getLevel(), partner) == null, "Rare pairs remain sterile");
}
ordinary.setInLoveTime(600);
h.assertTrue(!bird.canMate(ordinary) && !ordinary.canMate(bird)
&& bird.getBreedOffspring(h.getLevel(), ordinary) == null, "No mixed breeding with vanilla chicken");
}
h.succeed();
}
@GameTest public void noBiomeOrNaturalSpawnAndNoConventionalSpawnEgg(GameTestHelper h) {
var random = RandomSource.create(323);
for (var reason : new EntitySpawnReason[]{EntitySpawnReason.NATURAL, EntitySpawnReason.CHUNK_GENERATION})
h.assertTrue(!SpawnPlacements.checkSpawnRules(GemChickens.TYPE, h.getLevel(), reason,
h.absolutePos(new BlockPos(2, 2, 2)), random), "No natural placement");
for (var biome : h.getLevel().registryAccess().lookupOrThrow(Registries.BIOME))
for (var category : MobCategory.values())
h.assertTrue(biome.getAttributes().applyModifier(net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS,
net.minecraft.world.level.biome.MobSpawnSettings.EMPTY).getMobsToSpawn(category).unwrap().stream()
.noneMatch(entry -> entry.value().type() == GemChickens.TYPE), "No biome spawn entry");
h.assertTrue(SpawnEggItem.byId(GemChickens.TYPE).isEmpty(), "Golden egg is the only gameplay source");
h.succeed();
}
@GameTest public void weightedHatchesFollowEightFourTwoOne(GameTestHelper h) {
var random = RandomSource.create(323); int[] counts = new int[4];
for (int i = 0; i < 150000; i++) counts[GemChickenVariant.choose(random).id()]++;
for (var v : GemChickenVariant.values())
h.assertTrue(Math.abs(counts[v.id()] - 10000 * v.weight()) < 1000, "Seeded hatch frequency for " + v);
h.assertTrue(counts[2] > counts[0] && counts[0] > counts[1] && counts[1] > counts[3], "Strict rarity order");
h.succeed();
}
/** Exposes the real native collision callback without a test-only production hook. */
private static final class ImpactEgg extends ThrownEgg {
ImpactEgg(GameTestHelper h, Item item) {
super(h.getLevel(), h.absolutePos(new BlockPos(2, 2, 2)).getX() + .5,
h.absolutePos(new BlockPos(2, 2, 2)).getY(),
h.absolutePos(new BlockPos(2, 2, 2)).getZ() + .5, new ItemStack(item));
}
void impact() { super.onHit(new BlockHitResult(position(), Direction.UP, blockPosition(), false)); }
}
@GameTest public void goldenImpactAlwaysHatchesOneWeightedBabyAndVanillaEggStaysVanilla(GameTestHelper h) {
var area = new AABB(h.absolutePos(BlockPos.ZERO)).inflate(5);
for (var v : GemChickenVariant.values()) {
long seed = 0;
while (GemChickenVariant.choose(RandomSource.create(seed)) != v) seed++;
var egg = new ImpactEgg(h, GemChickens.GOLDEN_EGG); egg.getRandom().setSeed(seed); egg.impact();
var birds = h.getLevel().getEntitiesOfClass(GemChickenEntity.class, area);
h.assertTrue(egg.isRemoved() && birds.size() == 1 && birds.getFirst().getGemVariant() == v
&& birds.getFirst().getAge() == -24000, "One baby of the selected variant on golden impact: " + v);
birds.forEach(Entity::discard);
}
for (int i = 0; i < 64; i++) { var egg = new ImpactEgg(h, Items.EGG); egg.getRandom().setSeed(i); egg.impact(); }
h.assertTrue(h.getLevel().getEntitiesOfClass(GemChickenEntity.class, area).isEmpty(), "Ordinary eggs cannot hatch rare chickens");
h.succeed();
}
@GameTest public void ordinaryLayingCanProduceGoldenEggAndKeepsNativeEggs(GameTestHelper h) {
var bird = EntityTypes.CHICKEN.create(h.getLevel(), EntitySpawnReason.COMMAND);
bird.setPos(Vec3.atCenterOf(h.absolutePos(new BlockPos(2, 2, 2)))); bird.setNoAi(true);
bird.getRandom().setSeed(323); int golden = 0;
for (int i = 0; i < 1024; i++) {
bird.eggTime = 1; bird.aiStep(); var items = drops(h);
h.assertTrue(items.size() == 1, "One item per ordinary laying");
var stack = items.getFirst().getItem();
if (stack.is(GemChickens.GOLDEN_EGG)) golden++;
else h.assertTrue(stack.is(Items.EGG), "Ordinary native laying remains available");
h.assertTrue(bird.eggTime >= 6000 && bird.eggTime < 12000, "Ordinary interval stays native");
items.forEach(Entity::discard);
}
h.assertTrue(golden > 0 && golden < 20, "Seeded ordinary flock produces rare golden eggs: " + golden);
h.succeed();
}
@GameTest public void handAndDispenserLaunchTheRealGoldenEggAndConsumeOne(GameTestHelper h) {
var player = Progression003GameTests.player(h);
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(GemChickens.GOLDEN_EGG, 3));
GemChickens.GOLDEN_EGG.use(h.getLevel(), player, InteractionHand.MAIN_HAND);
h.assertTrue(player.getMainHandItem().getCount() == 2, "Survival throw consumes one egg");
var pos = h.absolutePos(new BlockPos(2, 2, 2));
var state = Blocks.DISPENSER.defaultBlockState().setValue(DispenserBlock.FACING, Direction.UP);
h.getLevel().setBlockAndUpdate(pos, state);
var dispenser = (DispenserBlockEntity) h.getLevel().getBlockEntity(pos);
var remaining = DispenserBlock.DISPENSER_REGISTRY.get(GemChickens.GOLDEN_EGG)
.dispense(new BlockSource(h.getLevel(), pos, state, dispenser), new ItemStack(GemChickens.GOLDEN_EGG, 3));
h.assertTrue(remaining.getCount() == 2, "Dispenser consumes one egg");
var area = new AABB(pos).inflate(10);
h.assertTrue(h.getLevel().getEntitiesOfClass(ThrownEgg.class, area).stream()
.filter(e -> e.getItem().is(GemChickens.GOLDEN_EGG)).count() == 2, "Both paths launch a golden projectile");
h.succeed();
}
@GameTest(maxTicks = 40) public void physicalGoldenProjectileHatchesOnTheServerFloor(GameTestHelper h) {
var floor = h.absolutePos(new BlockPos(2, 1, 2));
h.getLevel().setBlockAndUpdate(floor, Blocks.STONE.defaultBlockState());
var egg = new ThrownEgg(h.getLevel(), floor.getX() + .5, floor.getY() + 2.5,
floor.getZ() + .5, new ItemStack(GemChickens.GOLDEN_EGG));
egg.setDeltaMovement(0, -.5, 0); h.getLevel().addFreshEntity(egg);
h.runAfterDelay(12, () -> {
var birds = h.getLevel().getEntitiesOfClass(GemChickenEntity.class, new AABB(floor).inflate(4));
h.assertTrue(egg.isRemoved() && birds.size() == 1 && birds.getFirst().isBaby(),
"A ticking native projectile hits stone and hatches exactly one rare baby");
h.succeed();
});
}
}
@@ -75,9 +75,9 @@ public final class Inventory014GameTests {
h.succeed();
}
@GameTest public void purchaseDeathAndSortedPlayerSave(GameTestHelper h) throws Exception {
var p=player(h,2,false);int revision=ProgressEffects.get(p).revision();p.setExperienceLevels(3);
h.assertTrue(!ProgressionService.purchase(p,"sorting",revision) && p.experienceLevel==3,"Four-level price cannot be underpaid");
p.setExperienceLevels(4);h.assertTrue(ProgressionService.purchase(p,"sorting",revision) && p.experienceLevel==0,"Aptitude costs four levels once");
var p=player(h,2,false);int revision=ProgressEffects.get(p).revision();p.setExperienceLevels(9);
h.assertTrue(!ProgressionService.purchase(p,"sorting",revision) && p.experienceLevel==9,"Ten-level price cannot be underpaid");
p.setExperienceLevels(10);h.assertTrue(ProgressionService.purchase(p,"sorting",revision) && p.experienceLevel==0,"Aptitude costs ten levels once");
h.assertTrue(!ProgressionService.purchase(p,"sorting",revision+1),"No repeat aptitude purchase");
p.getInventory().setItem(9,new ItemStack(Items.STONE,11));p.getInventory().setItem(17,new ItemStack(Items.COAL,4));
h.assertTrue(sort(p,1,0),"Purchased aptitude enables server sort");var before=snapshot(p);String progress=p.getAttachedOrThrow(ProgressionService.RECORD);
@@ -15,13 +15,13 @@ public final class Menus033GameTests {
BlockKnowledgeService.recordSeen(p,Blocks.STONE);
var before=BlockKnowledgeNetworking.visibleSnapshot(p);
h.assertTrue(before.stream().anyMatch(k->k.blockId().equals("minecraft:oak_planks"))&&before.stream().allMatch(BlockKnowledge::possessed),"Before purchase, network response contains owned blocks only");
p.giveExperienceLevels(4);h.assertTrue(RecipeService.buy(p)&&p.experienceLevel==0,"Existing Catalogue receipt pays exactly four levels");
p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);h.assertTrue(RecipeService.buy(p)&&p.experienceLevel==0,"Existing Catalogue receipt pays exactly the configured catalogue price");
var after=BlockKnowledgeNetworking.visibleSnapshot(p);
h.assertTrue(after.stream().anyMatch(k->k.blockId().equals("minecraft:stone"))&&after.stream().allMatch(BlockKnowledge::known),"Purchase adds seen-only discoveries, never unknown blocks");
h.assertTrue(after.stream().noneMatch(k->k.blockId().equals("minecraft:diamond_block")),"A missing catalogue entry is not sent to the client");h.succeed();
}
@GameTest public void recipesDoNotCreateDiscoveryEntries(GameTestHelper h){
var p=player(h,true);RecipeService.discover(p,Items.OAK_PLANKS);RecipeService.refresh(p);p.giveExperienceLevels(4);RecipeService.buy(p);
var p=player(h,true);RecipeService.discover(p,Items.OAK_PLANKS);RecipeService.refresh(p);p.giveExperienceLevels(fr.koka.sanctuary.recipes.RecipeKnowledge.COST);RecipeService.buy(p);
h.assertTrue(RecipeService.knowledge(p).recipes.contains("minecraft:chest"),"Basic wood blueprint is known");
h.assertTrue(BlockKnowledgeNetworking.visibleSnapshot(p).stream().noneMatch(k->k.blockId().equals("minecraft:chest")),"A chest recipe does not fabricate an owned or seen chest");h.succeed();
}
@@ -25,7 +25,7 @@ public final class Movement017GameTests {
h.assertTrue(!p.isSprinting() && !p.isSwimming(),"Entering water cancels a previous land sprint");
p.setSprinting(true);p.setSwimming(true);
h.assertTrue(!p.isSprinting() && !p.isSwimming(),"Direct sprint and swim flags cannot bypass the aptitude");
p.giveExperienceLevels(4);h.assertTrue(ProgressionService.purchase(p,"swimming",ProgressEffects.get(p).revision()),"Server purchase charges four levels");
p.giveExperienceLevels(8);h.assertTrue(ProgressionService.purchase(p,"swimming",ProgressEffects.get(p).revision()),"Server purchase charges eight levels");
p.setSprinting(true);p.setSwimming(true);p.updateSwimming();
h.assertTrue(p.isSprinting() && p.isSwimming(),"Learned swimming permits the native aquatic posture and sprint");
p.getFoodData().setFoodLevel(6);p.getFoodData().tick(p);p.updateSwimming();
@@ -63,17 +63,17 @@ public final class Progression003GameTests {
helper.assertTrue(player.experienceLevel==9 && player.getMaxHealth()==8,"Price charged once and heart applied");
helper.assertTrue(!ProgressionService.purchase(player,"vitality",0) && player.experienceLevel==9,"Duplicate revision rejected");
helper.assertTrue(!ProgressionService.purchase(player,"unknown",1),"Unknown target rejected");
helper.assertTrue(ProgressionService.purchase(player,"names",1) && player.experienceLevel==5,"First aptitude purchased");
helper.assertTrue(ProgressionService.purchase(player,"names",1) && player.experienceLevel==7,"First aptitude purchased");
helper.assertTrue(!ProgressionService.purchase(player,"names",2),"Owned aptitude cannot be repurchased");
helper.assertTrue(ProgressionService.purchase(player,"mining",2) && player.experienceLevel==4,"Minage is a skill purchase");
helper.assertTrue(ProgressionService.purchase(player,"building",3) && player.experienceLevel==3,"Construction is a skill purchase");
helper.assertTrue(ProgressionService.purchase(player,"mining",2) && player.experienceLevel==6,"Minage is a skill purchase");
helper.assertTrue(ProgressionService.purchase(player,"building",3) && player.experienceLevel==5,"Construction is a skill purchase");
String expected=player.getAttachedOrThrow(ProgressionService.RECORD);
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,helper.getLevel().registryAccess());
player.saveWithoutId(output);
ServerPlayer loaded=player(helper);
loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,helper.getLevel().registryAccess(),output.buildResult()));
helper.assertTrue(loaded.getAttachedOrThrow(ProgressionService.RECORD).equals(expected)
&& loaded.experienceLevel==3,"XP and progress round trip in the same vanilla player snapshot");
&& loaded.experienceLevel==5,"XP and progress round trip in the same vanilla player snapshot");
ServerPlayer respawn=player(helper);
respawn.restoreFrom(player,false);
net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(player,respawn,false);
@@ -53,7 +53,7 @@ public class Recipes024GameTests {
h.assertTrue(ids.size()>50&&data.recipes.containsAll(ids)&&data.collections.contains("sanctuary:brewing"),"Native advancement teaches brewing collection");h.succeed();
}
@GameTest public void cataloguePurchaseIsOnceAndProgressResetKeepsKnowledge(GameTestHelper h){
var p=player(h);p.giveExperienceLevels(3);h.assertTrue(!RecipeService.buy(p)&&p.experienceLevel==3,"Insufficient XP does not spend");
var p=player(h);p.giveExperienceLevels(29);h.assertTrue(!RecipeService.buy(p)&&p.experienceLevel==29,"Insufficient XP does not spend");
p.giveExperienceLevels(1);RecipeService.discover(p,Items.OAK_PLANKS);RecipeService.refresh(p);
h.assertTrue(RecipeService.buy(p)&&p.experienceLevel==0&&!RecipeService.buy(p),"Exact price, no duplicate purchase");
String stored=p.getAttached(RecipeService.RECORD);ProgressEffects.apply(p,ProgressRecord.INITIAL);RecipeService.refresh(p);
@@ -62,8 +62,8 @@ public class Recipes024GameTests {
h.assertTrue(RecipeService.knowledge(restored).learned()&&RecipeService.knowledge(restored).items.contains("minecraft:oak_planks"),"Serialized knowledge reloads exactly");h.succeed();
}
@GameTest public void invalidRecordIsPreservedWithoutSpending(GameTestHelper h){
var p=player(h);String bad="{\"schema\":999}";p.setAttached(RecipeService.RECORD,bad);p.giveExperienceLevels(20);
h.assertTrue(!RecipeService.buy(p)&&p.experienceLevel==20&&p.getAttached(RecipeService.RECORD).equals(bad),"Invalid original record preserved");h.succeed();
var p=player(h);String bad="{\"schema\":999}";p.setAttached(RecipeService.RECORD,bad);p.giveExperienceLevels(40);
h.assertTrue(!RecipeService.buy(p)&&p.experienceLevel==40&&p.getAttached(RecipeService.RECORD).equals(bad),"Invalid original record preserved");h.succeed();
}
@GameTest public void manualCraftingRemainsUnrestricted(GameTestHelper h){
var p=player(h);var input=CraftingInput.of(2,2,List.of(new ItemStack(Items.OAK_PLANKS),new ItemStack(Items.OAK_PLANKS),new ItemStack(Items.OAK_PLANKS),new ItemStack(Items.OAK_PLANKS)));
@@ -20,7 +20,7 @@ public final class Starter039GameTests {
for(int i=0;i<10000;i++){var choices=StarterCompanions.draw(CompanionConfig.DEFAULT,random);h.assertTrue(choices.size()==3&&choices.stream().map(StarterCompanions.Choice::mob).distinct().count()==3,"Three different species");int count=(int)choices.stream().filter(c->c.rarity()==CompanionType.Rarity.UNCOMMON).count();h.assertTrue(count<=1,"At most one uncommon and no higher rarity");uncommon+=count;}
h.assertTrue(uncommon>850&&uncommon<1150,"Ten percent uncommon across deterministic 10000 draws: "+uncommon);
var entries=new TreeMap<>(CompanionConfig.DEFAULT.species());for(var type:CompanionType.values())entries.put(type.id(),new CompanionConfig.Entry(Set.of("cat","pig","cow").contains(type.id()),CompanionType.Rarity.COMMON,type.cooldown));
var c=new CompanionConfig(1,true,4,10,2,entries);var offer=StarterCompanions.draw(c,random);
var c=new CompanionConfig(2,true,20,10,2,entries);var offer=StarterCompanions.draw(c,random);
h.assertTrue(offer.stream().map(StarterCompanions.Choice::mob).collect(java.util.stream.Collectors.toSet()).equals(Set.of("cat","pig","cow")),"Disabled species are excluded; current server rarity is authoritative");h.succeed();
}
@GameTest public void offerAndFinalChoiceSurviveRestartWithoutReroll(GameTestHelper h)throws Exception{
File diff suppressed because one or more lines are too long
@@ -50,6 +50,7 @@ public final class SanctuaryMod implements ModInitializer {
fr.koka.sanctuary.insects.Insects.register();
fr.koka.sanctuary.infected.Infected.register();
fr.koka.sanctuary.whale.FlyingWhales.register();
fr.koka.sanctuary.chicken.GemChickens.register();
fr.koka.sanctuary.waystone.Waystones.register();
fr.koka.sanctuary.zipline.Ziplines.register();
fr.koka.sanctuary.palace.OriginBlock.register();
@@ -2,6 +2,7 @@ package fr.koka.sanctuary.boats;
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.animal.chicken.Chicken;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
@@ -9,7 +10,7 @@ public final class FlightMotors {
private FlightMotors() { }
public static FlightBalance.Kind kind(Entity entity) {
var type = entity.getType();
if (type == EntityTypes.CHICKEN) return FlightBalance.Kind.CHICKEN;
if (entity instanceof Chicken) return FlightBalance.Kind.CHICKEN;
if (type == EntityTypes.BAT) return FlightBalance.Kind.BAT;
if (type == EntityTypes.PARROT) return FlightBalance.Kind.PARROT;
if (type == EntityTypes.BEE) return FlightBalance.Kind.BEE;
@@ -0,0 +1,56 @@
package fr.koka.sanctuary.chicken;
import net.minecraft.network.chat.Component;
import net.minecraft.network.syncher.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.animal.chicken.Chicken;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.*;
/** Port of the 26.2 chicken, with sterile variants and a full slow interval from the first laying. */
public final class GemChickenEntity extends Chicken {
public static final String VARIANT_KEY = "GemChickenVariant";
private static final EntityDataAccessor<Integer> GEM_VARIANT =
SynchedEntityData.defineId(GemChickenEntity.class, EntityDataSerializers.INT);
public GemChickenEntity(EntityType<? extends Chicken> type, Level level) {
super(type, level);
eggTime *= getGemVariant().intervalMultiplier();
}
@Override protected void defineSynchedData(SynchedEntityData.Builder builder) {
super.defineSynchedData(builder);
builder.define(GEM_VARIANT, GemChickenVariant.EMERALD.id());
}
public GemChickenVariant getGemVariant() { return GemChickenVariant.byId(entityData.get(GEM_VARIANT)); }
public void setGemVariant(GemChickenVariant variant) {
entityData.set(GEM_VARIANT, variant.id());
eggTime = (6000 + random.nextInt(6000)) * variant.intervalMultiplier();
}
@Override public void aiStep() {
int previous = eggTime;
super.aiStep();
// Vanilla resets its timer immediately after laying. Scale only that new interval.
if (!level().isClientSide() && previous <= 1 && eggTime >= 6000)
eggTime *= getGemVariant().intervalMultiplier();
}
@Override protected Component getTypeName() {
return Component.translatable("entity.sanctuary.gem_chicken." + getGemVariant().serializedName());
}
@Override public boolean canFallInLove() { return false; }
@Override public boolean canMate(Animal partner) { return false; }
@Override public void setInLove(Player player) { }
@Override public GemChickenEntity getBreedOffspring(ServerLevel level, AgeableMob partner) { return null; }
@Override protected void addAdditionalSaveData(ValueOutput output) {
super.addAdditionalSaveData(output);
output.putInt(VARIANT_KEY, getGemVariant().id());
}
@Override protected void readAdditionalSaveData(ValueInput input) {
super.readAdditionalSaveData(input);
// Do not call setGemVariant: the native EggLayTime must resume exactly after reload.
entityData.set(GEM_VARIANT, GemChickenVariant.byId(input.getIntOr(VARIANT_KEY, 2)).id());
resetLove();
}
}
@@ -0,0 +1,38 @@
package fr.koka.sanctuary.chicken;
import fr.koka.sanctuary.gems.Gems;
import java.util.Locale;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
/** Historical variant ids are retained; gold is appended. Weights are independent of laying speed. */
public enum GemChickenVariant {
RUBY(0, 8, 4), SAPPHIRE(1, 16, 2), EMERALD(2, 4, 8), GOLD(3, 32, 1);
private final int id, intervalMultiplier, weight;
GemChickenVariant(int id, int intervalMultiplier, int weight) {
this.id = id; this.intervalMultiplier = intervalMultiplier; this.weight = weight;
}
public int id() { return id; }
public int intervalMultiplier() { return intervalMultiplier; }
public int weight() { return weight; }
public String serializedName() { return name().toLowerCase(Locale.ROOT); }
public Item product() {
return switch (this) {
case EMERALD -> Items.EMERALD;
case RUBY -> Gems.RUBY.gem();
case SAPPHIRE -> Gems.SAPPHIRE.gem();
case GOLD -> GemChickens.GOLDEN_EGG;
};
}
public static GemChickenVariant choose(RandomSource random) {
int roll = random.nextInt(15);
if (roll < 8) return EMERALD;
if (roll < 12) return RUBY;
return roll < 14 ? SAPPHIRE : GOLD;
}
public static GemChickenVariant byId(int id) {
return switch (id) { case 0 -> RUBY; case 1 -> SAPPHIRE; case 3 -> GOLD; default -> EMERALD; };
}
}
@@ -0,0 +1,33 @@
package fr.koka.sanctuary.chicken;
import fr.koka.sanctuary.SanctuaryMod;
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.*;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.DispenserBlock;
import net.minecraft.world.level.levelgen.Heightmap;
public final class GemChickens {
public static final int GOLDEN_EGG_CHANCE = 256;
public static final EntityType<GemChickenEntity> TYPE = Registry.register(BuiltInRegistries.ENTITY_TYPE,
SanctuaryMod.id("gem_chicken"), EntityType.Builder.of(GemChickenEntity::new, MobCategory.CREATURE)
.sized(.4F, .7F).eyeHeight(.644F).clientTrackingRange(10)
.build(ResourceKey.create(Registries.ENTITY_TYPE, SanctuaryMod.id("gem_chicken"))));
public static final Item GOLDEN_EGG = Registry.register(BuiltInRegistries.ITEM, SanctuaryMod.id("golden_egg"),
new EggItem(new Item.Properties().stacksTo(16)
.setId(ResourceKey.create(Registries.ITEM, SanctuaryMod.id("golden_egg")))));
public static void register() {
FabricDefaultAttributeRegistry.register(TYPE, net.minecraft.world.entity.animal.chicken.Chicken.createAttributes());
// No biome spawn entry, no conventional spawn egg; natural placement is always refused.
SpawnPlacements.register(TYPE, SpawnPlacementTypes.ON_GROUND, Heightmap.Types.MOTION_BLOCKING_NO_LEAVES,
(type, level, reason, pos, random) -> false);
DispenserBlock.registerProjectileBehavior(GOLDEN_EGG);
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.INGREDIENTS).register(out -> out.accept(GOLDEN_EGG));
}
private GemChickens() { }
}
@@ -0,0 +1,8 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.chicken.GemChickenVariant;
import net.minecraft.client.renderer.entity.state.ChickenRenderState;
public final class GemChickenRenderState extends ChickenRenderState {
public GemChickenVariant gemVariant = GemChickenVariant.EMERALD;
}
@@ -0,0 +1,33 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.chicken.*;
import net.minecraft.client.model.animal.chicken.*;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.renderer.entity.*;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
/** 26.2 gem textures and native temperate chicken models, including the native baby model. */
public final class GemChickenRenderer extends AgeableMobRenderer<GemChickenEntity, GemChickenRenderState, ChickenModel> {
public GemChickenRenderer(EntityRendererProvider.Context context) {
super(context, new AdultChickenModel(context.bakeLayer(ModelLayers.CHICKEN)),
new BabyChickenModel(context.bakeLayer(ModelLayers.CHICKEN_BABY)), .3F);
}
@Override public GemChickenRenderState createRenderState() { return new GemChickenRenderState(); }
@Override public void extractRenderState(GemChickenEntity entity, GemChickenRenderState state, float partialTick) {
super.extractRenderState(entity, state, partialTick);
state.flap = Mth.lerp(partialTick, entity.oFlap, entity.flap);
state.flapSpeed = Mth.lerp(partialTick, entity.oFlapSpeed, entity.flapSpeed);
state.gemVariant = entity.getGemVariant();
}
@Override public Identifier getTextureLocation(GemChickenRenderState state) {
String suffix = state.isBaby ? "_baby" : "";
if (state.gemVariant == GemChickenVariant.GOLD)
return Identifier.withDefaultNamespace("textures/entity/chicken/chicken_temperate" + suffix + ".png");
return SanctuaryMod.id("textures/entity/gem_chicken/" + state.gemVariant.serializedName() + suffix + ".png");
}
@Override protected int getModelTint(GemChickenRenderState state) {
return state.gemVariant == GemChickenVariant.GOLD ? 0xFFFFCF40 : -1;
}
}
@@ -212,7 +212,7 @@ public final class ProgressionScreen extends BlocodexFrame {
}
private Button purchaseButton(String target,int width) {
int cost=CraftAptitudes.target(target) ? CraftAptitudes.COST : ProgressionClient.progress.cost(target,ProgressionClient.config);
int cost=CraftAptitudes.target(target) ? CraftAptitudes.cost(target) : ProgressionClient.progress.cost(target,ProgressionClient.config);
Button button=Button.builder(cost<0 ? ProgressionService.label("completed")
: ProgressionService.label(ProgressRecord.APTITUDES.contains(target) || CraftAptitudes.target(target) ? "unlock_cost" : "upgrade_cost",cost),b -> {
pendingTicks=80;message="";purchases.forEach(widget -> widget.active=false);
@@ -49,6 +49,8 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
fr.koka.sanctuary.infected.Infected.SMOKER_LASSO,SmokerLassoRenderer::new);
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
fr.koka.sanctuary.infected.Infected.HUNTER,InfectedRenderer::new);
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
fr.koka.sanctuary.chicken.GemChickens.TYPE, GemChickenRenderer::new);
InsectSoundClient.register();
WaystoneScreen.register();
PropertyScreen.register();
@@ -15,7 +15,7 @@ public record CompanionConfig(int schema, boolean enabled, int activeCost, int e
public static final CompanionConfig DEFAULT=defaults();
private static final int LIMIT=32768;
public CompanionConfig {
if(schema!=1 || activeCost<0 || activeCost>10000 || equipSeconds<0 || equipSeconds>600
if(schema!=2 || activeCost<0 || activeCost>10000 || equipSeconds<0 || equipSeconds>600
|| !Float.isFinite(pvpDamageCap) || pvpDamageCap<0 || pvpDamageCap>20 || species==null
|| species.size()!=CompanionType.values().length)throw new IllegalArgumentException("Invalid companion config");
for(var type:CompanionType.values())if(species.get(type.id())==null)throw new IllegalArgumentException("Missing companion: "+type.id());
@@ -24,7 +24,7 @@ public record CompanionConfig(int schema, boolean enabled, int activeCost, int e
private static CompanionConfig defaults(){
var entries=new TreeMap<String,Entry>();
for(var t:CompanionType.values())entries.put(t.id(),new Entry(true,t.rarity,t.cooldown));
return new CompanionConfig(1,true,4,10,2,entries);
return new CompanionConfig(2,true,20,10,2,entries);
}
public Entry entry(CompanionType type){return species.get(type.id());}
public String json(){return new GsonBuilder().setPrettyPrinting().create().toJson(this);}
@@ -39,6 +39,11 @@ public record CompanionConfig(int schema, boolean enabled, int activeCost, int e
bool(e.getValue().getAsJsonObject().get("enabled"));
e.getValue().getAsJsonObject().get("cooldownSeconds").getAsBigDecimal().intValueExact();
}
if(tree.getAsJsonObject().get("schema").getAsInt()==1){
if(tree.getAsJsonObject().get("activeCost").getAsInt()==4)
tree.getAsJsonObject().addProperty("activeCost",DEFAULT.activeCost());
tree.getAsJsonObject().addProperty("schema",2);
}
return Objects.requireNonNull(new GsonBuilder().create().fromJson(tree,CompanionConfig.class));
}
private static void bool(com.google.gson.JsonElement value){if(!value.isJsonPrimitive()||!value.getAsJsonPrimitive().isBoolean())throw new IllegalArgumentException("Expected boolean");}
@@ -46,14 +51,19 @@ public record CompanionConfig(int schema, boolean enabled, int activeCost, int e
public static CompanionConfig load(Path file)throws IOException{
byte[] bytes=AtomicJson.read(file,LIMIT);
if(bytes==null){AtomicJson.write(file,null,DEFAULT.json(),LIMIT);java.nio.file.Files.writeString(file.resolveSibling(file.getFileName()+".v2"),"beta.032\n");return DEFAULT;}
var parsed=parse(new String(bytes,StandardCharsets.UTF_8));
String json=new String(bytes,StandardCharsets.UTF_8);
var parsed=parse(json);
var marker=file.resolveSibling(file.getFileName()+".v2");
if(java.nio.file.Files.exists(marker))return parsed;
if(java.nio.file.Files.exists(marker)){
if(AtomicJson.parse(json).getAsJsonObject().get("schema").getAsInt()<2)
AtomicJson.write(file,bytes,parsed.json(),LIMIT);
return parsed;
}
var entries=new TreeMap<>(parsed.species());
for(var t:CompanionType.values()){
var old=entries.get(t.id());if(old.cooldownSeconds()==legacyCooldown(t))entries.put(t.id(),new Entry(old.enabled(),old.rarity(),t.cooldown));
}
var migrated=new CompanionConfig(1,parsed.enabled(),parsed.activeCost(),parsed.equipSeconds(),parsed.pvpDamageCap(),entries);
var migrated=new CompanionConfig(2,parsed.enabled(),parsed.activeCost(),parsed.equipSeconds(),parsed.pvpDamageCap(),entries);
var backup=file.resolveSibling(file.getFileName()+".pre-v2");
if(!java.nio.file.Files.exists(backup))java.nio.file.Files.write(backup,bytes,java.nio.file.StandardOpenOption.CREATE_NEW);
AtomicJson.write(file,bytes,migrated.json(),LIMIT);
@@ -93,7 +93,7 @@ public final class CompanionService {
try{CONFIGS.put(s,CompanionConfig.load(FabricLoader.getInstance().getConfigDir().resolve("sanctuary/companion-powers.json")));}
catch(Exception error){
var c=CompanionConfig.DEFAULT;
CONFIGS.put(s,new CompanionConfig(1,false,c.activeCost(),c.equipSeconds(),c.pvpDamageCap(),c.species()));
CONFIGS.put(s,new CompanionConfig(2,false,c.activeCost(),c.equipSeconds(),c.pvpDamageCap(),c.species()));
SanctuaryMod.LOGGER.error("Companion config is invalid; powers are disabled and file preserved",error);
}
});
@@ -11,7 +11,7 @@ import net.minecraft.server.level.ServerPlayer;
/** Separate purchase receipt: old companion records keep their exact schema. */
public final class CompanionStudy {
public static final int COST=4;
public static final int COST=15;
public static final AttachmentType<String> RECEIPT=AttachmentRegistry.create(SanctuaryMod.id("companion_study"),
b->b.persistent(Codec.STRING).copyOnDeath());
public record Capability() implements CustomPacketPayload {
@@ -29,7 +29,8 @@ public final class CompanionStudy {
var value=obj.get(name);
if(!value.isJsonPrimitive()||!value.getAsJsonPrimitive().isNumber())throw new IllegalArgumentException("Invalid study receipt");
}
if(obj.get("schema").getAsBigDecimal().intValueExact()!=1||obj.get("paid").getAsBigDecimal().intValueExact()!=COST
int paid=obj.get("paid").getAsBigDecimal().intValueExact();
if(obj.get("schema").getAsBigDecimal().intValueExact()!=1||(paid!=4 && paid!=COST)
||obj.get("at").getAsBigDecimal().longValueExact()<1)throw new IllegalArgumentException("Invalid study receipt");
return true;
}
@@ -2,6 +2,7 @@ package fr.koka.sanctuary.cosmetics;
import fr.koka.sanctuary.progression.ProgressEffects;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.animal.chicken.Chicken;
import net.minecraft.world.entity.player.Player;
/** Holding a real flying passenger slows descent; shared client/server physics, no timed potion. */
@@ -12,7 +13,7 @@ public final class CarryGlide {
public static boolean flyer(Entity rider){
if(rider instanceof FamiliarEntity familiar&&!(familiar.egg().getItem() instanceof net.minecraft.world.item.SpawnEggItem))return false;
var type=rider instanceof FamiliarEntity familiar?net.minecraft.world.item.SpawnEggItem.getType(familiar.egg()):rider.getType();
return rider.isAlive()&&(FamiliarMotion.of(type).flies()||type==EntityTypes.CHICKEN);
return rider.isAlive()&&(rider instanceof Chicken||FamiliarMotion.of(type).flies()||type==EntityTypes.CHICKEN);
}
public static boolean supported(Player p){
return ProgressEffects.get(p)!=null&&p.isAlive()&&!p.isSpectator()&&!p.isPassenger()&&!p.getAbilities().flying&&!p.isFallFlying()
@@ -20,12 +20,12 @@ import net.minecraft.world.level.material.*;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.*;
/** A freestanding wooden frame with four exposed food hooks and native waterlogging. */
/** A freestanding wooden frame with two exposed food hooks and native waterlogging. */
public final class DryingRackBlock extends BaseEntityBlock implements SimpleWaterloggedBlock {
public static final EnumProperty<Direction> FACING=BlockStateProperties.HORIZONTAL_FACING;
public static final BooleanProperty WATERLOGGED=BlockStateProperties.WATERLOGGED;
private static final VoxelShape NS=Shapes.or(Block.box(1,0,6,3,16,10),Block.box(13,0,6,15,16,10),Block.box(1,14,6,15,16,10),Block.box(0,0,2,5,2,14),Block.box(11,0,2,16,2,14));
private static final VoxelShape EW=Shapes.or(Block.box(6,0,1,10,16,3),Block.box(6,0,13,10,16,15),Block.box(6,14,1,10,16,15),Block.box(2,0,0,14,2,5),Block.box(2,0,11,14,2,16));
private static final VoxelShape NS=Shapes.or(Block.box(0,0,6,2,16,10),Block.box(14,0,6,16,16,10),Block.box(0,14,6,16,16,10),Block.box(0,0,2,5,2,14),Block.box(11,0,2,16,2,14),Block.box(4.5,13,7.5,5.5,14,8.5),Block.box(10.5,13,7.5,11.5,14,8.5));
private static final VoxelShape EW=Shapes.or(Block.box(6,0,0,10,16,2),Block.box(6,0,14,10,16,16),Block.box(6,14,0,10,16,16),Block.box(2,0,0,14,2,5),Block.box(2,0,11,14,2,16),Block.box(7.5,13,4.5,8.5,14,5.5),Block.box(7.5,13,10.5,8.5,14,11.5));
private final Supplier<? extends BlockEntityType<DryingRackBlockEntity>> type;
public DryingRackBlock(Supplier<? extends BlockEntityType<DryingRackBlockEntity>> type,Properties properties){super(properties);this.type=type;registerDefaultState(stateDefinition.any().setValue(FACING,Direction.NORTH).setValue(WATERLOGGED,false));}
@Override public BlockEntity newBlockEntity(BlockPos pos,BlockState state){return new DryingRackBlockEntity(pos,state);}
@@ -16,10 +16,12 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.*;
/** Four independent campfire-style hooks: real items, loaded dry ticks and one result each. */
/** Two active hooks; historical positions 2–3 are a lossless, read-only reserve. */
public final class DryingRackBlockEntity extends AbstractCulinaryContainerBlockEntity {
public static final int CAPACITY=2;
// Keep version-1 storage readable in both directions. No new input enters 2–3.
public static final int SIZE=4;
private static final int[] SLOTS={0,1,2,3};
private static final int[] INPUT_SLOTS={0,1}, RECOVERY_SLOTS={0,1,2,3};
private final int[] progress=new int[SIZE],duration=new int[SIZE];
private final Identifier[] recipe=new Identifier[SIZE];
private final boolean[] ready=new boolean[SIZE];
@@ -31,8 +33,9 @@ public final class DryingRackBlockEntity extends AbstractCulinaryContainerBlockE
public int duration(int slot){return duration[slot];}
public boolean paused(){return paused;}
public int readyCount(){int n=0;for(int i=0;i<SIZE;i++)if(!items.get(i).isEmpty()&&ready[i])n++;return n;}
public int dryingCount(){int n=0;for(int i=0;i<SIZE;i++)if(!items.get(i).isEmpty()&&!ready[i])n++;return n;}
public int remainingTicks(){int min=Integer.MAX_VALUE;for(int i=0;i<SIZE;i++)if(!items.get(i).isEmpty()&&!ready[i]&&duration[i]>0)min=Math.min(min,Math.max(0,duration[i]-progress[i]));return min==Integer.MAX_VALUE?0:min;}
public int dryingCount(){int n=0;for(int i=0;i<CAPACITY;i++)if(!items.get(i).isEmpty()&&!ready[i])n++;return n;}
public int legacyCount(){int n=0;for(int i=CAPACITY;i<SIZE;i++)if(!items.get(i).isEmpty())n++;return n;}
public int remainingTicks(){int min=Integer.MAX_VALUE;for(int i=0;i<CAPACITY;i++)if(!items.get(i).isEmpty()&&!ready[i]&&duration[i]>0)min=Math.min(min,Math.max(0,duration[i]-progress[i]));return min==Integer.MAX_VALUE?0:min;}
private FoodDryingRecipe match(ItemStack stack){
if (level==null||stack.isEmpty())return null;
if (!(level instanceof ServerLevel server)) return ((net.fabricmc.fabric.api.recipe.v1.FabricRecipeAccess)level.recipeAccess()).getSynchronizedRecipes().getAllOfType(FoodDryingRecipe.TYPE).stream()
@@ -42,8 +45,8 @@ public final class DryingRackBlockEntity extends AbstractCulinaryContainerBlockE
}
public boolean isIngredient(ItemStack stack){return match(stack)!=null;}
public int insertIngredient(ItemStack stack){
if(match(stack)==null)return 0;
for(int i=0;i<SIZE;i++)if(items.get(i).isEmpty()){
if(legacyCount()>0||match(stack)==null)return 0;
for(int i=0;i<CAPACITY;i++)if(items.get(i).isEmpty()){
setItem(i,stack.copyWithCount(1));
((ServerLevel)level).playSound(null,worldPosition,SoundEvents.ITEM_FRAME_ADD_ITEM,SoundSource.BLOCKS,.35F,1F);return 1;
}return 0;
@@ -55,8 +58,8 @@ public final class DryingRackBlockEntity extends AbstractCulinaryContainerBlockE
}
public static void serverTick(ServerLevel level,BlockPos pos,BlockState state,DryingRackBlockEntity rack){
boolean wet=state.getValue(DryingRackBlock.WATERLOGGED)||level.isRainingAt(pos.above());
boolean changed=rack.paused!=wet,finished=false;rack.paused=wet;
for(int i=0;i<SIZE;i++){
boolean changed=rack.resumeLegacyContents() | (rack.paused!=wet),finished=false;rack.paused=wet;
for(int i=0;i<CAPACITY;i++){
var stack=rack.items.get(i);if(stack.isEmpty()||rack.ready[i])continue;
var holder=level.recipeAccess().getRecipeFor(FoodDryingRecipe.TYPE,new SingleRecipeInput(stack),level).orElse(null);
if(holder==null||holder.value().resultTemplate().count()!=1){
@@ -77,12 +80,23 @@ public final class DryingRackBlockEntity extends AbstractCulinaryContainerBlockE
if(changed)rack.sync();
if(finished)level.playSound(null,pos,SoundEvents.ITEM_PICKUP,SoundSource.BLOCKS,.20F,1.1F);
}
private boolean resumeLegacyContents(){
boolean moved=false;
for(int slot=0;slot<CAPACITY;slot++)if(items.get(slot).isEmpty()){
for(int old=CAPACITY;old<SIZE;old++)if(!items.get(old).isEmpty()){
items.set(slot,items.get(old));items.set(old,ItemStack.EMPTY);
progress[slot]=progress[old];duration[slot]=duration[old];recipe[slot]=recipe[old];ready[slot]=ready[old];
reset(old);moved=true;break;
}
}
return moved;
}
private void reset(int slot){progress[slot]=0;duration[slot]=0;recipe[slot]=null;ready[slot]=false;}
@Override protected void onSlotChanged(int slot){if(slot>=0&&slot<SIZE)reset(slot);}
@Override protected void onContentsChanged(){for(int i=0;i<SIZE;i++)reset(i);}
@Override public ItemStack removeItemNoUpdate(int slot){var removed=super.removeItemNoUpdate(slot);if(!removed.isEmpty())contentsChanged(slot);return removed;}
@Override public boolean canPlaceItem(int slot,ItemStack stack){return slot>=0&&slot<SIZE&&items.get(slot).isEmpty()&&match(stack)!=null;}
@Override public int[] getSlotsForFace(Direction side){return SLOTS;}
@Override public boolean canPlaceItem(int slot,ItemStack stack){return slot>=0&&slot<CAPACITY&&legacyCount()==0&&items.get(slot).isEmpty()&&match(stack)!=null;}
@Override public int[] getSlotsForFace(Direction side){return side==Direction.DOWN?RECOVERY_SLOTS:INPUT_SLOTS;}
@Override public boolean canTakeItemThroughFace(int slot,ItemStack stack,Direction side){return side==Direction.DOWN&&ready[slot];}
public int analogOutputSignal(){return readyCount()>0?15:dryingCount()*3;}
@Override public float getVisualRotationYInDegrees(){return 0;}
@@ -13,5 +13,6 @@ public final class DryingRackHud {
Component text=rack.readyCount()>0?Component.translatable("hud.sanctuary.drying.ready",rack.readyCount(),rack.dryingCount())
:rack.paused()?Component.translatable("hud.sanctuary.drying.wet"):Component.translatable("hud.sanctuary.drying.progress",rack.dryingCount(),String.format("%d:%02d",seconds/60,seconds%60));
int x=g.guiWidth()/2,y=g.guiHeight()/2+18,w=m.font.width(text)/2+4;g.fill(x-w,y-3,x+w,y+11,0xB0000000);g.centeredText(m.font,text,x,y,0xFFFFFFFF);
if(rack.legacyCount()>0){var extra=Component.translatable("hud.sanctuary.drying.legacy",rack.legacyCount());int half=m.font.width(extra)/2+4;g.fill(x-half,y+11,x+half,y+25,0xB0000000);g.centeredText(m.font,extra,x,y+14,0xFFFFFFFF);}
});}
}
@@ -12,6 +12,8 @@ import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.Items;
import fr.koka.sanctuary.cuisine.DriedFoods;
import net.minecraft.world.phys.Vec3;
/** Native item cuboids suspended from the beam, including each hook's independent result. */
@@ -21,15 +23,24 @@ public final class DryingRackBlockEntityRenderer implements BlockEntityRenderer<
@Override public DryingRackRenderState createRenderState(){return new DryingRackRenderState();}
@Override public void extractRenderState(DryingRackBlockEntity rack,DryingRackRenderState state,float partial,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay overlay){
BlockEntityRenderer.super.extractRenderState(rack,state,partial,camera,overlay);state.facing=rack.getBlockState().getValue(DryingRackBlock.FACING);
for(int i=0;i<4;i++){state.items[i].clear();state.sausages[i]=rack.getItem(i).is(fr.koka.sanctuary.cuisine.DriedCuisine.HANGING_SAUSAGES);if(!rack.getItem(i).isEmpty())resolver.updateForTopItem(state.items[i],rack.getItem(i),ItemDisplayContext.FIXED,rack.level(),rack,(int)rack.getBlockPos().asLong()+i);}
for(int i=0;i<DryingRackBlockEntity.CAPACITY;i++){state.items[i].clear();state.sausages[i]=rack.getItem(i).is(fr.koka.sanctuary.cuisine.DriedCuisine.HANGING_SAUSAGES);if(!rack.getItem(i).isEmpty())resolver.updateForTopItem(state.items[i],rack.getItem(i),ItemDisplayContext.FIXED,rack.level(),rack,(int)rack.getBlockPos().asLong()+i);}
for(int i=0;i<DryingRackBlockEntity.CAPACITY;i++){
var item=rack.getItem(i);
// These vanilla drawings have a diagonal long axis in FIXED display.
// Correct the drawing's angle so the meat/fish itself hangs vertically.
state.spriteRotation[i]=item.is(Items.BEEF)||item.is(Items.PORKCHOP)||item.is(Items.COD)||item.is(Items.SALMON)
||item.is(DriedFoods.item("dried_meat"))||item.is(DriedFoods.item("dried_cod"))||item.is(DriedFoods.item("dried_salmon")) ? -45F : 0F;
}
}
@Override public void submit(DryingRackRenderState state,PoseStack poses,SubmitNodeCollector collector,CameraRenderState camera){
poses.pushPose();poses.translate(.5,0,.5);poses.rotate(Axis.YP.rotationDegrees(switch(state.facing){case EAST->90;case SOUTH->180;case WEST->270;default->0;}));
float[] x={-.24375F,-.08125F,.08125F,.24375F};
for(int i=0;i<4;i++)if(!state.items[i].isEmpty()){
// Flat food sprites fit between hooks without overlapping. Sausage cuboids keep their original silhouette.
float scale=state.sausages[i]?.5F:.16F;
poses.pushPose();poses.translate(x[i],state.sausages[i]?.65625:.80,0);poses.rotate(Axis.ZP.rotationDegrees(90));poses.scale(scale,scale,scale);state.items[i].submit(poses,collector,state.lightCoords,OverlayTexture.NO_OVERLAY,0);poses.popPose();
float[] x={-.1875F,.1875F};
for(int i=0;i<DryingRackBlockEntity.CAPACITY;i++)if(!state.items[i].isEmpty()){
float scale=state.sausages[i]?.9F:.36F;
poses.pushPose();poses.translate(x[i],state.sausages[i]?.53125:.625,0);
if(state.sausages[i])poses.rotate(Axis.ZP.rotationDegrees(90));
else if(state.spriteRotation[i]!=0)poses.rotate(Axis.ZP.rotationDegrees(state.spriteRotation[i]));
poses.scale(scale,scale,scale);state.items[i].submit(poses,collector,state.lightCoords,OverlayTexture.NO_OVERLAY,0);poses.popPose();
}poses.popPose();
}
}
@@ -3,7 +3,8 @@ import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.core.Direction;
public final class DryingRackRenderState extends BlockEntityRenderState {
public final ItemStackRenderState[] items={new ItemStackRenderState(),new ItemStackRenderState(),new ItemStackRenderState(),new ItemStackRenderState()};
public final boolean[] sausages=new boolean[4];
public final ItemStackRenderState[] items={new ItemStackRenderState(),new ItemStackRenderState()};
public final boolean[] sausages=new boolean[2];
public final float[] spriteRotation=new float[2];
public Direction facing=Direction.NORTH;
}
@@ -7,14 +7,14 @@ import java.util.*;
/** An independent aptitude receipt and a personal tasting record. */
public record FoodKnowledge(int schema,Set<String> tasted,int paid,long purchasedAt) {
public static final int COST=4,MAX_FOODS=512,MAX_JSON=96_000;
public static final int COST=6,MAX_FOODS=512,MAX_JSON=96_000;
public static final FoodKnowledge EMPTY=new FoodKnowledge(1,Set.of(),0,0);
public FoodKnowledge{
if(schema!=1||tasted==null||tasted.size()>MAX_FOODS||!(paid==0&&purchasedAt==0||paid==COST&&purchasedAt>0))throw new IllegalArgumentException("Invalid food knowledge");
if(schema!=1||tasted==null||tasted.size()>MAX_FOODS||!(paid==0&&purchasedAt==0||(paid==4||paid==COST)&&purchasedAt>0))throw new IllegalArgumentException("Invalid food knowledge");
for(var id:tasted)if(id==null||id.length()>128||Identifier.tryParse(id)==null||!id.contains(":"))throw new IllegalArgumentException("Invalid tasted food");
tasted=Collections.unmodifiableSortedSet(new TreeSet<>(tasted));
}
public boolean studied(){return paid==COST;}
public boolean studied(){return paid==4 || paid==COST;}
public boolean knows(String food){return studied()||tasted.contains(food);}
public FoodKnowledge taste(String food){if(tasted.contains(food)||tasted.size()>=MAX_FOODS)return this;var next=new TreeSet<>(tasted);next.add(food);return new FoodKnowledge(1,next,paid,purchasedAt);}
public FoodKnowledge buy(long now){if(studied())throw new IllegalStateException("Already studied");return new FoodKnowledge(1,tasted,COST,Math.max(1,now));}
@@ -0,0 +1,26 @@
package fr.koka.sanctuary.mixin;
import com.llamalad7.mixinextras.injector.wrapoperation.*;
import fr.koka.sanctuary.chicken.*;
import java.util.function.BiConsumer;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.animal.chicken.Chicken;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootTable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(Chicken.class)
public abstract class ChickenLayMixin {
@WrapOperation(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/animal/chicken/Chicken;dropFromGiftLootTable(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/resources/ResourceKey;Ljava/util/function/BiConsumer;)Z"))
private boolean sanctuary$lay(Chicken chicken, ServerLevel level, ResourceKey<LootTable> table,
BiConsumer<ServerLevel, ItemStack> drop, Operation<Boolean> original) {
if (chicken instanceof GemChickenEntity gem)
return chicken.spawnAtLocation(level, gem.getGemVariant().product()) != null;
if (chicken.getType() == EntityTypes.CHICKEN && chicken.getRandom().nextInt(GemChickens.GOLDEN_EGG_CHANCE) == 0)
return chicken.spawnAtLocation(level, GemChickens.GOLDEN_EGG) != null;
return original.call(chicken, level, table, drop);
}
}
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.chicken.*;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg;
import net.minecraft.world.phys.HitResult;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ThrownEgg.class)
public abstract class GoldenEggMixin {
@Inject(method = "onHit", at = @At("HEAD"), cancellable = true)
private void sanctuary$hatch(HitResult hit, CallbackInfo ci) {
var egg = (ThrownEgg) (Object) this;
if (!egg.getItem().is(GemChickens.GOLDEN_EGG) || !(egg.level() instanceof ServerLevel level)) return;
var chicken = GemChickens.TYPE.create(level, EntitySpawnReason.TRIGGERED);
if (chicken != null) {
chicken.setGemVariant(GemChickenVariant.choose(egg.getRandom()));
chicken.setAge(-24000);
chicken.snapTo(egg.getX(), egg.getY(), egg.getZ(), egg.getYRot(), 0);
level.addFreshEntity(chicken);
}
level.broadcastEntityEvent(egg, (byte) 3);
egg.discard();
ci.cancel();
}
}
@@ -9,7 +9,14 @@ import net.minecraft.world.inventory.*;
/** Access to personal workstations; shared brewing processes keep vanilla behavior. */
public final class CraftAptitudes {
public static final int COST=4;
public static int cost(String target) {
return switch(target) {
case "smithing" -> 20;
case "alchemy" -> 25;
case "enchanting" -> 30;
default -> throw new IllegalArgumentException("Unknown craft aptitude");
};
}
public static final AttachmentType<Boolean> ENCHANTING=AttachmentRegistry.create(SanctuaryMod.id("enchanting_learned"),
b->b.persistent(Codec.BOOL).copyOnDeath());
public static final AttachmentType<Boolean> ALCHEMY=AttachmentRegistry.create(SanctuaryMod.id("alchemy_learned"),
@@ -37,7 +44,7 @@ public final class CraftAptitudes {
public static boolean buy(ServerPlayer p,String target,int revision) {
var session=ProgressionService.session(p.level().getServer());var progress=ProgressEffects.get(p);
if(!target(target) || session==null || !session.healthy || progress==null || progress.revision()!=revision
|| !p.isAlive() || p.isSpectator() || learned(p,target) || p.experienceLevel<COST)return false;
p.giveExperienceLevels(-COST);p.setAttached(attachment(target),true);return true;
|| !p.isAlive() || p.isSpectator() || learned(p,target) || p.experienceLevel<cost(target))return false;
p.giveExperienceLevels(-cost(target));p.setAttached(attachment(target),true);return true;
}
}
@@ -9,10 +9,10 @@ import java.util.List;
public record ProgressionConfig(int schema, boolean enableExistingWorlds, int initialColors,
List<Integer> statCosts, int namesCost, int atlasCost, int sortingCost, int swimmingCost, int lyingCost, int restingCost, double deathXpFraction) {
private static final List<Integer> LEGACY_COSTS = List.of(1, 2, 4, 8, 16, 32, 64);
public static final ProgressionConfig DEFAULT = new ProgressionConfig(5, false, 20,
List.of(1, 2, 4, 8, 16, 32, 48), 4, 4, 4, 4, 4, 4, 0.7);
public static final ProgressionConfig DEFAULT = new ProgressionConfig(6, false, 20,
List.of(1, 2, 4, 8, 16, 32, 48), 2, 12, 10, 8, 4, 4, 0.7);
public ProgressionConfig {
if (schema != 5 || initialColors < 1 || initialColors > 256 || statCosts == null
if (schema != 6 || initialColors < 1 || initialColors > 256 || statCosts == null
|| statCosts.size() != 7 || statCosts.stream().anyMatch(v -> v == null || v < 0 || v > 10000)
|| restingCost < 0 || restingCost > 10000 || lyingCost < 0 || lyingCost > 10000 || swimmingCost < 0 || swimmingCost > 10000 || sortingCost < 0 || sortingCost > 10000 || atlasCost < 0 || atlasCost > 10000 || namesCost < 0 || namesCost > 10000 || !Double.isFinite(deathXpFraction)
|| deathXpFraction < 0 || deathXpFraction > 1)
@@ -28,7 +28,7 @@ public record ProgressionConfig(int schema, boolean enableExistingWorlds, int in
}
String json = new String(bytes, StandardCharsets.UTF_8);
var config = parse(json);
if (AtomicJson.parse(json).getAsJsonObject().get("schema").getAsInt() < 5)
if (AtomicJson.parse(json).getAsJsonObject().get("schema").getAsInt() < 6)
AtomicJson.write(file, bytes, gson.toJson(config), 16384);
return config;
}
@@ -66,6 +66,22 @@ public record ProgressionConfig(int schema, boolean enableExistingWorlds, int in
throw new IllegalArgumentException("Invalid aptitude price");
value.getAsBigDecimal().intValueExact();
}
if (tree.getAsJsonObject().get("schema").getAsInt() == 5) {
// Upgrade historical default prices once; custom prices keep their values.
for (String price:List.of("namesCost","atlasCost","sortingCost","swimmingCost")) {
var value=tree.getAsJsonObject().get(price);
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber())
throw new IllegalArgumentException("Invalid aptitude price");
if (value.getAsBigDecimal().intValueExact()==4)
tree.getAsJsonObject().addProperty(price,switch(price) {
case "namesCost" -> DEFAULT.namesCost();
case "atlasCost" -> DEFAULT.atlasCost();
case "sortingCost" -> DEFAULT.sortingCost();
default -> DEFAULT.swimmingCost();
});
}
tree.getAsJsonObject().addProperty("schema",6);
}
return java.util.Objects.requireNonNull(gson.fromJson(tree, ProgressionConfig.class));
}
}
@@ -12,7 +12,7 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.server.level.ServerPlayer;
public final class ZoomService {
public static final int COST=4;
public static final int COST=5;
public static final AttachmentType<Boolean> LEARNED=AttachmentRegistry.create(SanctuaryMod.id("zoom_learned"),b->b.persistent(Codec.BOOL).copyOnDeath());
private static final Map<ServerPlayer,State> SENT=new WeakHashMap<>();
public record Buy() implements CustomPacketPayload{
@@ -7,11 +7,11 @@ import net.minecraft.resources.Identifier;
/** Independent additive record. Learning a blueprint never fabricates an item discovery. */
public final class RecipeKnowledge {
public static final int COST=4, MAX_IDS=8192;
public static final int COST=30, MAX_IDS=8192;
public int paid;
public long purchasedAt;
public final Set<String> items=new TreeSet<>(), recipes=new TreeSet<>(), collections=new TreeSet<>();
public boolean learned(){return paid==COST;}
public boolean learned(){return paid==4 || paid==COST;}
public static RecipeKnowledge parse(String raw){
var result=new RecipeKnowledge(); if(raw==null)return result;
if(raw.length()>1_000_000)throw new IllegalArgumentException("Recipe knowledge too large");
@@ -19,7 +19,7 @@ public final class RecipeKnowledge {
var obj=tree.getAsJsonObject();
if(integer(obj,"schema")!=1)throw new IllegalArgumentException("Unsupported recipe schema");
result.paid=Math.toIntExact(integer(obj,"paid"));result.purchasedAt=integer(obj,"purchasedAt");
if((result.paid!=0&&result.paid!=COST)||(result.paid==0?result.purchasedAt!=0:result.purchasedAt<=0))throw new IllegalArgumentException("Invalid catalogue receipt");
if((result.paid!=0&&!result.learned())||(result.paid==0?result.purchasedAt!=0:result.purchasedAt<=0))throw new IllegalArgumentException("Invalid catalogue receipt");
for(String name:List.of("items","recipes","collections")){
var arr=obj.get(name).getAsJsonArray();if(arr.size()>MAX_IDS)throw new IllegalArgumentException("Too many recipe identifiers");
var target=name.equals("items")?result.items:name.equals("recipes")?result.recipes:result.collections;
@@ -45,7 +45,7 @@ public final class RecipeService {
public static boolean buy(ServerPlayer p){
var s=session(p);if(ProgressEffects.get(p)==null||s.invalid||s.data.learned()||!p.isAlive()||p.isSpectator()||p.experienceLevel<RecipeKnowledge.COST)return false;
s.data.paid=RecipeKnowledge.COST;s.data.purchasedAt=Math.max(1,System.currentTimeMillis());
p.giveExperienceLevels(-RecipeKnowledge.COST);persist(p,s);send(p,s);return true;
p.giveExperienceLevels(-RecipeKnowledge.COST);persist(p,s);ProgressionService.send(p,"");send(p,s);return true;
}
public static void refresh(ServerPlayer p){
var s=session(p);if(ProgressEffects.get(p)!=null&&!s.invalid){
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:item/golden_egg"
}
}
@@ -3552,5 +3552,12 @@
"tag.item.sanctuary.legacy_preparations": "Retired Preparations",
"item.sanctuary.dairy_churn": "Dairy Churn",
"item.sanctuary.drying_rack": "Drying Rack",
"sanctuary.atlas.surface": "Surface view · outdoors"
"sanctuary.atlas.surface": "Surface view · outdoors",
"hud.sanctuary.drying.legacy": "Previous stock waiting: %s",
"entity.sanctuary.gem_chicken": "Rare Chicken",
"entity.sanctuary.gem_chicken.emerald": "Emerald Chicken",
"entity.sanctuary.gem_chicken.ruby": "Ruby Chicken",
"entity.sanctuary.gem_chicken.sapphire": "Sapphire Chicken",
"entity.sanctuary.gem_chicken.gold": "Golden Chicken",
"item.sanctuary.golden_egg": "Golden Egg"
}
@@ -3552,5 +3552,12 @@
"tag.item.sanctuary.legacy_preparations": "Anciennes préparations désactivées",
"item.sanctuary.dairy_churn": "Baratte",
"item.sanctuary.drying_rack": "Séchoir",
"sanctuary.atlas.surface": "Vue de surface · extérieur"
"sanctuary.atlas.surface": "Vue de surface · extérieur",
"hud.sanctuary.drying.legacy": "Ancien stock en attente : %s",
"entity.sanctuary.gem_chicken": "Poule rare",
"entity.sanctuary.gem_chicken.emerald": "Poule émeraude",
"entity.sanctuary.gem_chicken.ruby": "Poule rubis",
"entity.sanctuary.gem_chicken.sapphire": "Poule saphir",
"entity.sanctuary.gem_chicken.gold": "Poule en or",
"item.sanctuary.golden_egg": "Œuf en or"
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "sanctuary:item/golden_egg"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

@@ -0,0 +1,59 @@
{
"type": "minecraft:entity",
"pools": [
{
"entries": [
{
"type": "minecraft:item",
"functions": [
{
"count": {
"type": "minecraft:uniform",
"min": 0.0,
"max": 2.0
},
"function": "minecraft:set_count"
},
{
"count": {
"type": "minecraft:uniform",
"min": 0.0,
"max": 1.0
},
"enchantment": "minecraft:looting",
"function": "minecraft:enchanted_count_increase"
}
],
"name": "minecraft:feather"
}
],
"rolls": 1.0
},
{
"entries": [
{
"type": "minecraft:item",
"functions": [
{
"conditions": [
{
"condition": "minecraft:entity_properties",
"entity": "this",
"predicate": {
"minecraft:flags": {
"is_on_fire": true
}
}
}
],
"function": "minecraft:furnace_smelt"
}
],
"name": "minecraft:chicken"
}
],
"rolls": 1.0
}
],
"random_sequence": "sanctuary:entities/gem_chicken"
}
@@ -4,6 +4,8 @@
"plugin": "fr.koka.sanctuary.mixin.OptionalModMixins",
"compatibilityLevel": "JAVA_25",
"mixins": [
"ChickenLayMixin",
"GoldenEggMixin",
"ThinFruitLandingMixin",
"FruitPistonCrushingMixin",
"KitchenItemEntityMixin",

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