Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aabc867a5c | ||
|
|
76a440ea80 | ||
|
|
e3b4613b10 | ||
|
|
4b9e44e75a | ||
|
|
ea1d84160d | ||
|
|
7ca566292f | ||
|
|
0c68545b63 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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é.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
@@ -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)&©.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{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 187 B |
|
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",
|
||||
|
||||