Compare commits

..
Author SHA1 Message Date
koka 309d0ba6f3 Add permanent Smithing aptitude for anvils and smithing tables in beta.238
Build Sanctuary / build (push) Waiting to run
2026-10-06 16:12:32 +02:00
koka 5dee98106e Keep purchased aptitudes permanent and integrate Pause in beta.237
Build Sanctuary / build (push) Waiting to run
2026-10-06 14:43:01 +02:00
koka d6f3258442 Merge verified beta.235 Pause layout into progression aptitudes 2026-10-06 14:35:34 +02:00
koka 1c8e6437d5 Group aptitudes and unlock enchanting and alchemy in beta.236 2026-10-06 14:25:15 +02:00
46 changed files with 1149 additions and 306 deletions
+22 -9
View File
@@ -6,16 +6,29 @@ 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).
## Essai de carte derrière Pause — annulé
## beta.238 — Forgerie
[L'essai plein écran](docs/pause-map-layer-beta236.md) est annulé à la demande
du créateur. Pause retrouve la disposition beta.235, avec la carte au centre
et une colonne de navigation à gauche lorsque l'Atlas est déverrouillé.
Story et **M**, configurable dans les contrôles, sont conservés.
Parcours client Vulkan FR/EN GUI 2/3/4 et `check build` de l'annulation réussis.
L'ancien pack d'essai beta.236 est retiré des artefacts actifs ; aucune nouvelle
livraison n'est créée par cette annulation. La future livraison commune avec
les aptitudes et Forgerie beta.238 devra utiliser le prochain compteur global.
[Aptitude Forgerie](docs/forgerie-beta238.md) dans Arts et métiers : quatre
niveaux pour débloquer les enclumes et la table de forge, achat définitif.
Les réparations, noms, améliorations en netherite et ornements conservent
leurs règles natives. L'achat est contrôlé côté serveur, y compris pour les
postes portés sur la tête. Icône fournie et libellés FR/EN intégrés.
Build, 21 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[MRpack local beta.238](build/Sanctuary-beta.238.mrpack) vérifié, sans
publication ni mise à jour personnelle.
## beta.237 — Aptitudes acquises définitivement
[Aptitudes par thème et achats permanents](docs/aptitudes-beta237.md) : seuls
World Map et Mob Names sont activables/désactivables. Les dix autres aptitudes
restent utilisables après achat ; Enchantement et Alchimie coûtent chacun
quatre niveaux et débloquent l'accès personnel aux postes natifs. Icônes
fournies intégrées, libellés FR/EN. Les anciens choix de désactivation de ces
achats sont effacés sans toucher aux achats ni à l'XP. Disposition Pause
beta.235 intégrée, adaptée au choix de masquer la carte. `check build assemblePack`,
37 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
[MRpack local beta.237](build/Sanctuary-beta.237.mrpack) vérifié ; aucune
publication ni mise à jour personnelle.
## beta.235 — Essai réversible du menu Pause
+140
View File
@@ -0,0 +1,140 @@
# APT-236 — aptitudes par thème et activation personnelle
La règle de désactivation décrite ici correspond à la livraison beta.236.
[Beta.237](aptitudes-beta237.md) la remplace : seuls World Map et Mob Names
restent désactivables, les autres aptitudes sont acquises définitivement.
Le chantier Pause beta.235 est intégré dans cette nouvelle livraison.
Branche `codex/progression-aptitudes`, base beta.234 (`f9c2ce8`). Le travail
parallèle du menu Pause occupe beta.235 ; ce lot réserve beta.236.
## Contrat avant implémentation
Progression regroupe les aptitudes en cinq thèmes : Survie (Swimming, Rest,
Food Knowledge), Inventaire et fabrication (Inventory Sorting, Catalogue),
Familiers (Active Bond, Familiar Study), Exploration (World Map, Mob Names,
Zoom), Arts et métiers (Enchantement, Alchimie). Le minage et la forgerie
restent hors de ce ticket.
Enchantement et Alchimie coûtent chacun 4 niveaux, comme les aptitudes actuelles.
L'usage personnel de la table d'enchantement et de l'alambic est verrouillé au
départ dans les mondes avec progression Sanctuary. Les menus natifs sont
refusés côté serveur tant que l'aptitude correspondante n'est pas acquise et
active, y compris via les blocs fonctionnels portés sur la tête. Les enclumes,
la table de forge, les équipements enchantés et les potions déjà obtenues
restent inchangés. Les alambics partagés et leur automatisation continuent
leur fonctionnement autonome : ce lot contrôle l'accès personnel au poste,
sans attribuer de propriétaire à un bloc ni arrêter une préparation lancée.
Les personnages existants avec progression Sanctuary doivent aussi acquérir
ces deux nouvelles aptitudes ; aucune acquisition gratuite n'est ajoutée.
Une aptitude achetée peut être activée ou désactivée sans coût et sans perdre
son achat. Food Knowledge reste permanent après apprentissage. Les compétences,
les prix et les découvertes ne changent pas. Les effets de gameplay sont
contrôlés côté serveur ; les aides d'interface utilisent le choix serveur.
Active Bond contrôle l'usage des pouvoirs de travail, pas les techniques de
combat, les pouvoirs passifs ou l'équipement du familier. Une action déjà
lancée se termine normalement ; les nouvelles activations sont refusées.
## Persistance et compatibilité
L'attachement additif `sanctuary:aptitude_preferences` conserve un entier de
0 à 2047 : un bit par aptitude désactivée, dans l'ordre immuable names, atlas,
sorting, swimming, resting, active_bond, familiar_study, catalogue, zoom,
enchanting, alchemy.
L'absence de cet attachement équivaut à zéro : toutes les aptitudes acquises
gardent leur comportement actuel. Il est copié à la mort et conservé au New
Game+. Deux attachements booléens additifs `sanctuary:enchanting_learned` et
`sanctuary:alchemy_learned` conservent leurs nouveaux achats ; leur absence
équivaut à false et ils sont copiés à la mort et conservés au New Game+.
Aucun schéma ou contenu des achats existants n'est réécrit. Ce contrat
est limité à cet attachement ; aucun monde personnel n'est ouvert ou modifié.
Une requête transmet la cible, le choix et le masque attendu ; le serveur
vérifie l'achat, la session, l'état du joueur et le masque courant. Un choix
inconnu, Food Knowledge, une aptitude non acquise ou une requête périmée est
refusé sans XP dépensée. Un état distinct est renvoyé avant le snapshot de
progression. La capacité réseau `aptitude_preferences_v1` exige les versions
compatibles sur client et serveur sans changer les paquets existants.
Désactiver Swimming interrompt le sprint aquatique ; désactiver Rest arrête
la récupération. World Map masque son entrée et refuse les nouvelles lectures
personnelles ; les relevés continuent d'être conservés et l'outil opérateur
reste disponible. Catalogue masque l'aide personnelle JEI et la consultation
complète de Discovery, sans toucher aux recettes ni aux découvertes.
Un ancien mod ignore ces trois nouveaux attachements et retrouve son ancien
accès aux postes. Il peut perdre les attachements inconnus en sauvegardant :
conserver une sauvegarde avant un retour arrière. Le schéma 9 de progression
et les preuves des aptitudes antérieures restent identiques.
## Vérifications
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=aptitudes236,progression,movement,sorting,recipes,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
Première passe serveur : `check build assemblePack` réussi en 3 min 13 s,
**33 GameTests requis réussis**, dont cinq scénarios APT-236. Contrôles purs
du dépôt réussis. Le banc Atlas existant évite le conflit de génération du
chemin par défaut déjà documenté en beta.232. Monde de développement neuf.
Log : `build/aptitudes236-check-build-pack.log`.
- Onze choix indépendants, refus des requêtes inconnues, non acquises,
périmées et de Food Knowledge ; conservation exacte de l'XP et des preuves
d'achat. Achat à 3 niveaux refusé, à 4 accepté une seule fois.
- Aller-retour des nouveaux attachements dans le NBT joueur natif, copie à
la mort, conservation lors de la remise à zéro des compétences du cycle.
- Swimming arrête le sprint aquatique et interdit les flags de mouvement
directs ; Rest arrête la récupération ; tri falsifié et lectures Atlas
personnelles refusés. Réactivation du tri contrôlée avec la même requête.
Active Bond refuse une nouvelle activation avant toute planification.
- Table d'enchantement native refusée avant acquisition, ouverte ensuite ;
**une épée diamant est réellement enchantée**. Désactiver ferme le menu,
refuse sa réutilisation et son bouton d'enchantement sans dépense d'XP.
Alambic natif refusé puis ouvert, fermeture et invalidation à la désactivation,
ingrédients partagés conservés et réouverture après réactivation.
Parcours client final réussi en **52 s**, Minecraft 26.3, Vulkan confirmé par
MoltenVK 1.4.2 / Apple M1. Log : `build/aptitudes236-client-vulkan.log`, marqueur
`APTITUDES236_PASS` ; douze captures dans `build/aptitudes236-screenshots/`.
- FR/EN, GUI 2/3/4, fenêtre 1280 × 960 : cinq thèmes dans l'ordre, les douze
aptitudes présentes, Food Knowledge permanent et sans interrupteur.
- Achat des deux arts depuis les véritables boutons de Progression : huit
niveaux au total. **66 paires désactivation/réactivation** via les widgets
et le réseau, état serveur contrôlé, XP et progression finale identiques.
- Libellés d'état entièrement contenus dans les boutons ; défilement et
position conservée au redimensionnement 1344 × 1008 ; Terminé revient à
Pause. Une première passe a trouvé le libellé permanent trop large en GUI 4 :
retour à la ligne ajouté. Les interrupteurs affichent un état court et
expliquent leur action gratuite dans l'infobulle.
Les essais concernent les sauvegardes de développement. Pas de validation
Windows ou multijoueur distant. L'automatisation des alambics et les pouvoirs
déjà lancés restent les limites explicites du contrat ; la forgerie attend
son prochain ticket.
## Livraison locale
Build final `check build assemblePack` réussi en **2 min 24 s**, 136 tâches,
33/33 GameTests requis et contrôles purs réussis après les derniers réglages
d’interface. Log : `build/aptitudes236-check-build-pack-final.log`.
Versions du mod, du pack et du manifeste alignées sur **beta.236**.
[MRpack local](../build/Sanctuary-beta.236.mrpack) vérifié : 12797046 octets,
intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5, un seul JAR Sanctuary
identique au build final, sans classe de test ni monde ni module de laboratoire.
Les deux PNG fournis sont identiques octet pour octet aux originaux dans le JAR.
Reçu : `build/aptitudes236-artifact.json`.
- SHA-256 JAR : `21a4411ebed223a1d9f9d204dc9d7300af0d5b946416e39bb138f5c575bf5847`.
- SHA-256 MRpack : `9d3eca559805d451e8c653cca1d59186ae46e3c6964147b9fc072b9da832f921`.
La branche reste indépendante du chantier Pause beta.235 ; leur intégration
reste à effectuer. Aucune publication distante, mise à jour du canal packwiz
ou synchronisation d’instance personnelle.
+99
View File
@@ -0,0 +1,99 @@
# APT-237 — achats permanents et intégration du menu Pause
Branche `codex/progression-aptitudes`. Intégration des commits beta.235
`a7bdfe6` et `00d175c`, avec les aptitudes beta.236 `1c8e643`.
## Contrat avant implémentation
Seuls World Map et Mob Names disposent d'un interrupteur gratuit après achat.
Swimming, Rest, Inventory Sorting, Catalogue, Active Bond, Familiar Study,
Zoom, Enchantement, Alchimie et Food Knowledge restent acquis et utilisables
sans désactivation. Les prix et les conditions d'achat restent identiques.
Enchantement et Alchimie exigent toujours leur achat avant l'accès personnel
au poste natif. Les achats, l'XP et les preuves existantes sont conservés.
Le menu Pause reprend la disposition beta.235 : deux colonnes Aventure et
Communauté avant World Map, une colonne à gauche avec la carte. Le choix
personnel de masquer World Map suit aussi cette disposition.
## Contrat de migration des préférences beta.236
L'identifiant `sanctuary:aptitude_preferences`, son codec entier 0–2047 et
l'ordre historique des onze bits restent stables. Les bits 0 (Mob Names) et
1 (World Map) restent effectifs. Les bits 2 à 10 deviennent inertes et sont
retirés de cet attachement lors de sa prochaine synchronisation serveur.
Ainsi un ancien masque 2047 devient 3 : les deux aides restent masquées et
les neuf autres aptitudes achetées retrouvent automatiquement leur usage.
Le chargement accepte encore les anciennes valeurs ; les lectures et effets
ignorent immédiatement leurs bits désormais inertes. Aucune preuve d'achat,
aucun format de progression, aucun monde ou terrain n'est réécrit.
Les requêtes de désactivation des aptitudes permanentes sont refusées même
si elles proviennent d'un ancien client. Les paquets v1 restent inchangés ;
une capacité additive `sanctuary:aptitude_permanent_v1` impose un client avec
cette règle côté serveur. Les nouvelles interfaces sont traduites FR/EN.
Le retour à une ancienne version nécessite comme auparavant une sauvegarde,
car cette version ne peut restaurer les anciennes désactivations effacées.
## Vérification et livraison
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=aptitudes236,menus,progression,movement,sorting,recipes,foodknowledge \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
Les suites APT-236 sont actualisées pour vérifier la nouvelle règle et la
migration ; leurs noms Gradle restent stables. `check build assemblePack`
réussi en **2 min 34 s**, **37 GameTests requis réussis**, dont six scénarios
aptitudes. Contrôles purs du dépôt réussis. Le banc Atlas évite le chemin de
génération par défaut déjà documenté en beta.232. Log :
`build/aptitudes237-check-build-pack.log`.
- Les deux choix personnels sont gratuits ; refus des requêtes périmées,
inconnues, non acquises et des désactivations d'aptitudes permanentes.
Prix des nouveaux achats vérifié à trois/quatre niveaux, achat unique.
- Ancien masque 2047 rechargé dans le NBT natif, interprété en 3 puis
normalisé sans changement d'XP ni de preuve d'achat. Conservation des
choix et des achats à la mort et au New Game+.
- Avec d'anciens bits désactivés : sprint aquatique, Rest et tri restent
utilisables ; Active Bond suit son contrôle normal de familier équipé.
World Map conserve son contrôle d'accès et sa réactivation gratuite.
- Enchantement et Alchimie refusés avant achat, puis disponibles lors des
visites suivantes. Une épée diamant est réellement enchantée. Une requête
de désactivation refusée ne ferme ni n'invalide les menus acquis ; ingrédients
partagés de l'alambic conservés. Accès vanilla hors progression conservé.
Parcours client réussi en **32 s**, Minecraft 26.3, Vulkan / MoltenVK 1.4.2
sur Apple M1. FR/EN, GUI 2/3/4, fenêtre 1280 × 960 : cinq thèmes, douze
aptitudes, dix achats permanents, **12 paires désactivation/réactivation**
réelles des deux aides. Deux achats des arts via les boutons natifs de
Progression, huit niveaux au total. Libellés contenus dans leurs boutons,
XP et preuves conservées, défilement conservé au redimensionnement et retour
Terminé vers Pause.
La même suite réutilise les assertions Pause du collègue : disposition,
ordre clavier, marges, titres et libellés de navigation vérifiés après achat
de World Map, avec son choix personnel désactivé puis activé. Les deux
colonnes et la colonne près de la carte sont conservées. Les captures de
Pause portent sur la disposition ; les relevés et fils commencent à charger.
Log `build/aptitudes237-client-vulkan.log`, marqueur `APTITUDES237_PASS` ;
**24 captures** dans `build/aptitudes237-screenshots/`. Inspection des captures
Arts FR GUI 3, Pause sans carte FR GUI 4 et Pause avec carte EN GUI 3.
Versions alignées sur **beta.237**. [MRpack local](../build/Sanctuary-beta.237.mrpack)
vérifié : 12797901 octets, intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5,
un seul JAR Sanctuary identique au build final, sans tests ni monde ni module
de laboratoire. Les deux icônes fournies restent identiques octet pour octet
aux originaux. Reçu `build/aptitudes237-artifact.json`.
- SHA-256 JAR : `7ac741c6a15b8ab05a909cf0132c9d605b863723c0742a92d8b60c741fd760a6`.
- SHA-256 MRpack : `7635e9e7a425cbd7a7f6d020f9bb2bcced8cc1287967093346295e4b4b2250e9`.
Les anciens artefacts beta.236 sont conservés. Tests dans des mondes de
développement neufs ; Windows et multijoueur distant non testés. Les
alambics autonomes et pouvoirs déjà lancés gardent le contrat beta.236.
La forgerie, les enclumes et la table de forge attendent leur prochain ticket.
Aucune publication, avancée du canal ni synchronisation d'installation personnelle.
+15 -6
View File
@@ -1,12 +1,21 @@
# Backlog Sanctuary
## PAUSE-236 — Atlas plein écran derrière Pause — essai annulé
## APT-238 — Forgerie — livré localement
Le créateur annule uniquement la carte derrière le menu. Le commit de
disposition `5ea7950` est inversé ; affichage beta.235 restauré, Story et
raccourci M (`94fefc0`) conservés. [Historique et annulation](pause-map-layer-beta236.md).
L'ancien pack local est retiré des artefacts actifs. Cette annulation ne publie
aucune nouvelle version et ne modifie pas la branche des aptitudes/Forgerie.
Branche `codex/forgerie-beta238`, base beta.237. Aptitude permanente des Arts
et métiers ; quatre niveaux pour les trois variantes d'enclume et la table
de forge, y compris les postes portés. [Contrat et persistance](forgerie-beta238.md).
Réparations, noms, améliorations et ornements gardent leurs règles natives.
Build, 21 GameTests requis, Vulkan FR/EN GUI 2/3/4 et MRpack vérifiés.
## APT-237 — achats permanents et intégration Pause — livré localement
Branche `codex/progression-aptitudes`, travaux beta.235 et beta.236 réunis.
Cinq thèmes, douze aptitudes ; seuls World Map et Mob Names ont un interrupteur.
Enchantement et Alchimie restent à acheter avant l'accès personnel au poste.
[Contrat et migration](aptitudes-beta237.md) des préférences antérieures,
avec conservation des achats et de l'XP. Build, 37 GameTests requis,
Vulkan FR/EN GUI 2/3/4 et MRpack local vérifiés. La forgerie attend ses précisions.
## PAUSE-235 — groupes côte à côte avant Atlas — essai livré localement
+95
View File
@@ -0,0 +1,95 @@
# APT-238 — Forgerie
Branche `codex/forgerie-beta238`, base beta.237 (`5dee981`).
## Contrat avant implémentation
Une aptitude Forgerie est ajoutée dans Arts et métiers de Progression, avec
l'icône `aptitudes_forgerie.png` fournie. Comme Enchantement et Alchimie,
elle coûte quatre niveaux, est achetée une seule fois et reste acquise
sans interrupteur. Elle débloque l'usage personnel de toutes les variantes
d'enclume et de la table de forge. Avant l'achat, les menus natifs de ces
postes sont refusés côté serveur, y compris les postes fonctionnels portés
sur la tête. Les réparations, noms, améliorations et ornements natifs restent
fonctionnels après achat. Équipements déjà obtenus et recettes conservés.
La meule, les fours et les autres postes restent sous leurs règles actuelles.
Les deux aides World Map / Mob Names restent les seuls choix désactivables.
Les interfaces ajoutées sont traduites en français et en anglais.
## Persistance et compatibilité
L'attachement booléen additif `sanctuary:smithing_learned` conserve l'achat.
Absent : false. Copie à la mort et conservation au New Game+. Aucun schéma,
aucune preuve ni aucun attachement existant n'est réécrit. Les personnages
existants avec progression doivent aussi acquérir Forgerie. Les joueurs
hors progression Sanctuary gardent l'accès vanilla. Aucun monde personnel,
aucune génération ni aucun terrain n'est modifié.
La capacité `sanctuary:smithing_v1` et l'état `sanctuary:smithing_state_v1`
transmettent l'achat sans modifier
les paquets v1 existants. Le serveur exige un client compatible et contrôle
la session, le joueur, la révision et les niveaux avant l'achat. Un retour
à une version ancienne peut perdre cet attachement inconnu à la sauvegarde ;
conserver une sauvegarde avant ce retour.
## Vérifications et livraison
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=forgerie238,aptitudes236,headblocks \
-PsanctuaryAtlasOnly=true
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
```
`check build assemblePack` réussi en **2 min 44 s**, **21 GameTests requis**
réussis : cinq nouveaux scénarios Forgerie, six scénarios d'aptitudes et dix
scénarios de postes portés. Contrôles purs du dépôt réussis. Le banc Atlas
évite le chemin de génération par défaut déjà documenté en beta.232.
Log `build/forgerie238-check-build-pack.log`.
- Achat refusé avec trois niveaux ou une révision périmée ; quatre niveaux
dépensés une seule fois. Autres achats et preuves inchangés. Requête de
désactivation refusée ; conservation native NBT, à la mort et au New Game+.
- Trois variantes d'enclume refusées avant achat puis ouvertes après achat.
**Réparation et renommage réels d'une épée diamant** sur chacune ; XP et
diamant consommés selon le coût natif du menu.
- Table de forge refusée avant achat puis ouverte : **épée netherite et
plastron diamant avec ornement Sentry réellement produits**, entrées
consommées une fois, coût d'XP natif conservé et accès aux visites suivantes.
- Menus construits en dehors de l'ouverture normale : validité et prise de
résultat refusées avant acquisition, sans dépense ni perte d'entrées.
Joueurs sans progression Sanctuary autorisés comme en vanilla.
- Enclumes et table de forge sur la tête : contrôle de l'achat du visiteur,
indépendamment du porteur. La fixture générale des postes natifs possède
explicitement les trois aptitudes des Arts avant de tester leurs menus.
Sources et bytecode du **Minecraft 26.3 local** vérifiés pour les méthodes
héritées de `ItemCombinerMenu`, l'override de prise d'`AnvilMenu` et les recettes
natives de forge. Aucun changement de recette ni de dépendance externe.
Parcours graphique en **34 s**, Vulkan / MoltenVK 1.4.2 sur Apple M1,
FR/EN GUI 2/3/4. La suite d'aptitudes existante contrôle désormais treize
aptitudes, dont onze achats permanents. Trois Arts achetés depuis les boutons
natifs : douze niveaux au total, Forgerie synchronisée depuis le serveur et
présentée comme acquise sans interrupteur. Deux seules aides désactivables,
XP et progression conservées lors de douze paires de changement, disposition
Pause intégrée, défilement et retour Terminé conservés. Log
`build/forgerie238-client-vulkan.log`, marqueur `FORGERIE238_PASS`.
Vingt-quatre captures dans `build/forgerie238-screenshots/` ; Arts FR GUI 3
et EN GUI 4 inspectés, texte et icône contenus dans le cadre.
Versions du mod, du pack et du manifeste alignées sur **beta.238**.
[MRpack local](../build/Sanctuary-beta.238.mrpack) vérifié : 12802487 octets,
intégrité ZIP, 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.
Les trois icônes fournies des Arts sont identiques octet pour octet aux
originaux dans le JAR. Reçu `build/forgerie238-artifact.json`.
- SHA-256 JAR : `e29ea6f7f56c57290d0307076f641f2e714ec72a80be858a5fd9c5f08e27eb8e`.
- SHA-256 MRpack : `2fbd605acf555dd8b192c0c0ad7171414a13f5820e5d440ba54debcd4cb897a7`.
Tests dans des mondes de développement neufs. Pas de validation Windows ni
multijoueur distant. Les sauvegardes et installations personnelles restent
intactes ; aucune publication ni avancée du canal packwiz.
-157
View File
@@ -1,157 +0,0 @@
# PAUSE-236 — Atlas plein écran derrière le menu
## Statut : essai annulé le 6 octobre 2026
Le créateur demande d'annuler **uniquement la carte derrière Pause**.
Le patch de disposition `5ea7950` est inversé, sans réécrire l'historique :
carte centrale et colonne Communauté → Aventure → Game à gauche lorsque
l'Atlas est déverrouillé, comme en beta.235. Story, la disposition sans Atlas
et le raccourci M (`94fefc0`) restent présents. Le Nord retrouve sa position
dans le panneau de carte. Aucun changement sur les autres branches.
Les vérifications et la livraison ci-dessous décrivent l'essai historique,
avant annulation. Son pack est déplacé dans
`build/withdrawn/Sanctuary-beta.236.mrpack` pour ne pas le proposer comme résultat
actuel ; le staging packwiz de cet essai et son JAR d'origine sont également
archivés dans `build/withdrawn/`. Le numéro beta.236 était également utilisé par la branche des aptitudes,
qui a depuis atteint beta.238 avec Forgerie : cette branche d'essai ne constitue
pas la livraison commune. Aucun nouvel artefact de distribution n'est livré
par l'annulation ; la consolidation et son prochain compteur global restent
un travail distinct.
Le parcours client Vulkan du retour réussit en 46 s : FR/EN GUI 2/3/4, géométrie
beta.235 avec/sans Atlas, Story et ses 100 entrées, recherche, filtres, retours,
focus et redimensionnement. M depuis le jeu et Pause, verrou de progression,
réaffectation et désactivation passent également. Log :
`build/pause-map-layer-cancel-client-vulkan.log`.
Comparaison Git : les deux fichiers de disposition/carte et leurs assertions
Pause/Solo sont identiques à beta.235 ; implémentation du raccourci et libellés
FR/EN identiques au commit `94fefc0`.
`./gradlew check build -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true`
réussit en 2 min 34 s : quatre GameTests requis et contrôles purs du dépôt.
Log : `build/pause-map-layer-cancel-check-build.log`. Aucun nouvel assemblage
de distribution : il s'agit de l'annulation ciblée des sources de l'essai.
## Historique de l'essai
Branche d'essai `codex/pause-map-layer-beta236`, base beta.235, commit `00d175c`,
Minecraft 26.3. Le rendu reste à apprécier par le créateur.
## Résultat
Gazette reste à gauche et Notice Board à droite, sur toute la hauteur du corps
de Pause, avec ou sans Atlas. Au centre, Aventure (Discovery, Progression,
Story) et Communauté (Habitant, Faction, Battle) sont côte à côte ; Partie /
Game suit juste dessous. Reprendre la partie garde le pied de page natif.
Avec Atlas, la carte occupe littéralement tout l'écran sur le calque derrière
les contrôles. Le terrain est légèrement assombri pour lire les libellés. Le
Nord `N ↑` se trouve au centre horizontal, juste sous le bandeau du message
serveur ; un espace réservé le sépare du menu. Les boutons et espacements
s'adaptent aux petites hauteurs.
La carte est rendue avant les widgets de premier plan, puis reçoit les entrées
après eux. Un clic dans une zone libre ouvre l'Atlas complet ; les boutons,
les fils, l'en-tête et le pied de page gardent leur priorité. Les infobulles
et les marqueurs de la carte ne capturent pas le survol derrière un contrôle.
Les marqueurs suivis restent accessibles lorsqu'ils sont exposés. Tab après
Quit atteint la carte ; Entrée ouvre l'Atlas, Échap ou Terminé revient à la
même Pause. Redimensionner reconstruit le calque aux dimensions de l'écran.
Seul le terrain personnel déjà cartographié est affiché. Ce changement ne
révèle ni ne génère de nouveaux chunks. Les contrats serveur, sauvegardes,
progression, données communautaires et contenus Story restent identiques.
Les libellés FR/EN existants sont réutilisés pour la disposition.
## Raccourci carte
La lettre **M** ouvre l'Atlas complet depuis le jeu et depuis Pause, une fois
la carte déverrouillée dans Progression. Avant ce déblocage, elle affiche le
message existant rappelant cette condition. Depuis le jeu, Échap et Terminé
reviennent au jeu ; depuis Pause, ils reviennent à la même Pause.
Le raccourci figure dans les contrôles natifs, catégorie Sanctuary, sous
« Ouvrir la carte » / « Open world map ». Il suit la lettre M de la disposition
du clavier, y compris AZERTY, et préserve une affectation sauvegardée ou
désactivée. Les écrans de saisie conservent leurs touches. Aucun nouveau
protocole, fichier persistant ou déblocage serveur n'est ajouté.
## Réversibilité
Le code de l'essai et ses assertions sont isolés du raccourci M et du commit de
livraison contenant les versions et la documentation : `git revert 5ea7950` annule le
calque et sa disposition. Après ajout du raccourci (`94fefc0`), l'application
inverse du patch a été vérifiée avec `git apply --reverse --3way --cached`
dans un index temporaire : aucun conflit et raccourci M conservé, sans modifier
les fichiers de travail. La branche
`codex/pause-columns-beta235` conserve la variante précédente. Annuler le
commit de disposition ramène à cette variante ; une livraison suivante prend
le prochain numéro beta, sans diminuer le compteur.
## Vérifications
```sh
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryStory234ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryPause233ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
./gradlew check build assemblePack -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true
```
Parcours Story et raccourci réussi en 42 s (`STORY234_PASS`) : Vulkan FR/EN GUI 2/3/4,
fenêtre 1280 × 960, avec et sans Atlas, puis redimensionnement à 1344 × 1008.
Nord centré sous le message serveur, terrain réellement reçu, calque aux
dimensions de l'écran, boutons et titres des fils prioritaires pour les clics
et le survol. Clic natif sur une zone libre et Entrée ouvrent l'Atlas ; Échap
et Terminé reviennent à la même Pause. Le clic natif sur Story traverse bien
le premier plan, ses 100 entrées, recherche et filtres restent disponibles.
Saisie continue, focus, défilement, retours et progression serveur conservés.
Événements clavier natifs M depuis le jeu et Pause avant/après déblocage,
réaffectation à J, suppression de l'ancienne affectation, désactivation et
restauration de la touche testées. La saisie de M dans Story garde le texte,
le focus et son écran ; les intitulés du raccourci sont vérifiés en FR/EN.
Log : `build/pause236-story-client-vulkan-final.log`.
21 captures copiées dans `build/pause236-story-screenshots/`. Captures inspectées :
avec Atlas FR/2 et EN/4, sans Atlas FR/2. La fixture du monde plat neuf
cartographie explicitement 25 chunks : la majorité du fond reste inconnue.
Ce terrain limité ne représente pas une sauvegarde personnelle explorée.
Aux GUI étroits, les longs libellés natifs et titres des fils gardent leur
défilement ou découpage ; les actions Sanctuary restent lisibles en entier.
Validation macOS/Vulkan uniquement, aucun essai OpenGL ou Windows revendiqué.
Le parcours Pause final réussit en 42 s (`PAUSE233_PASS`), avec les mêmes
langues, GUI et états Atlas. Les fils restent de part et d'autre du menu ;
publication, photo, retours des sélecteurs, brouillons et ancienne catégorie
sont conservés. Inventaire réel et fichier communautaire restent inchangés
pendant les lectures ; configuration temporaire restaurée à la fin.
Log : `build/pause236-client-vulkan-final.log` ; 38 captures copiées dans
`build/pause236-screenshots/`. Captures inspectées : Pause avec Atlas et posts
FR/2 et EN/4. Vulkan confirmé : MoltenVK 1.4.2, Apple M1.
Le contrôle `check build assemblePack` réussit en 2 min 31 s : quatre GameTests
requis et contrôles purs du dépôt, dont fichier communautaire, photos,
demandes et recherche. Log : `build/pause236-check-build-pack-final.log`.
La sélection menus/Atlas reste celle du banc existant ; le conflit du générateur
sur le chemin natif par défaut, documenté en beta.232, reste hors de ce ticket.
La compilation des sources finales du banc client passe également.
## Livraison locale
`mod_version`, `pack_version` et `packwiz/pack.toml` sont alignés sur beta.236.
Les commits `5ea7950` (disposition) et `94fefc0` (raccourci) restent séparés
des métadonnées de livraison.
Export historique [Sanctuary-beta.236.mrpack](../build/withdrawn/Sanctuary-beta.236.mrpack),
12 779 209 octets : intégrité ZIP, Minecraft 26.3 et Fabric Loader 0.19.5,
un seul JAR Sanctuary embarqué, identique au JAR construit, sans monde ni module
de test. Les 3 303 fichiers d'assets, données et resourcepacks sont identiques
à beta.235, sauf un nouveau libellé de raccourci dans chaque langue ; toutes
les traductions existantes sont conservées. Story reste dans le JAR de production
et les classes des bancs client en sont absentes.
Reçu : `build/pause236-mrpack-receipt.json`.
- JAR : `74a8be9a078e9ecdc17b8cf332254cdb4a6dca4c37e4fb728d98632d2e8c3918`.
- MRpack : `0fac46765a4430df8f90398471dedd2c2636d6b120482cc78466011fdd7574c2`.
Aucune publication distante, modification du canal packwiz, synchronisation
Prism ou ouverture d'un monde personnel effectuée.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.236
pack_version=beta.236
mod_version=beta.238
pack_version=beta.238
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+4 -1
View File
@@ -228,7 +228,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 = [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 = [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)
@@ -337,6 +337,9 @@ tasks.named('processGametestResources') {
def pause233Client = providers.gradleProperty('sanctuaryPause233ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryPause233ClientTests', pause233Client)
if (pause233Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Pause233ClientChecks') }
def aptitudes236Client = providers.gradleProperty('sanctuaryAptitudes236ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('aptitudes236Client', aptitudes236Client)
if (aptitudes236Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Aptitudes236ClientChecks') }
def story234Client = providers.gradleProperty('sanctuaryStory234ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryStory234ClientTests', story234Client)
if (story234Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Story234ClientChecks') }
@@ -0,0 +1,105 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.companion.*;
import fr.koka.sanctuary.recipes.RecipeService;
import fr.koka.sanctuary.food.FoodKnowledgeService;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.gui.components.*;
import net.minecraft.client.gui.screens.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.network.chat.contents.TranslatableContents;
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
/** Native Progression widgets and real server requests in a disposable Vulkan world. */
public final class Aptitudes236ClientChecks implements FabricClientGameTest {
private static final Map<String,String> TITLES=Map.ofEntries(
Map.entry("names","sanctuary.progression.names"),Map.entry("atlas","sanctuary.progression.atlas"),
Map.entry("sorting","sanctuary.progression.sorting"),Map.entry("swimming","sanctuary.progression.swimming"),
Map.entry("resting","sanctuary.progression.resting"),Map.entry("active_bond","sanctuary.companion.link"),
Map.entry("familiar_study","sanctuary.companion.study"),Map.entry("catalogue","sanctuary.recipes.catalogue"),
Map.entry("zoom","sanctuary.zoom.title"),Map.entry("enchanting","sanctuary.progression.enchanting"),Map.entry("alchemy","sanctuary.progression.alchemy"),Map.entry("smithing","sanctuary.progression.smithing"));
@Override public void runTest(ClientGameTestContext c) {
var intro=new AtomicBoolean(true);fr.koka.sanctuary.testing.QuickTestWorld.enable();
c.runOnClient(m->{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("Forgerie238 "+UUID.randomUUID());s.setSeed("238");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
c.waitFor(m->ProgressionClient.active() && m.player!=null && m.gui.screen()==null && !IntroSpawnFlash.active(),1200);
var server=world.getServer();
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.setExperienceLevels(100);ProgressionService.send(p,"");});
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.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);
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");
CompanionService.send(p,"");ProgressionService.send(p,"");
});
c.runOnClient(m->m.gui.setScreen(null));
c.waitFor(m->ProgressionClient.progress.names() && CompanionClient.studied && ZoomClient.learned() && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned() && FoodKnowledgeClient.ready(),300);
c.waitTicks(30);
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setAttached(AptitudeService.PREFERENCES,2044);ProgressionService.send(p,"");check(p.getAttached(AptitudeService.PREFERENCES)==0,"Legacy preferences are normalized without losing purchases");});
c.runOnClient(m->{m.player.setAttached(AptitudeService.PREFERENCES,2044);for(String target:AptitudePreferences.PERMANENT)check(AptitudeClient.enabled(target),"Client ignores legacy disabled bits for "+target);});
String purchaseProof=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
int xp=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel);
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));});
c.waitTicks(3);
c.runOnClient(m->{var screen=m.gui.screen();check(screen.isPauseScreen(),"Progression retains Pause semantics");layout(screen);var titles=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).map(Aptitudes236ClientChecks::key).toList();
int previous=-1;for(String theme:List.of("survival","inventory_crafting","familiars","exploration","crafts")){int index=titles.indexOf("sanctuary.progression.theme_"+theme);check(index>previous,"All five themes are present in order");previous=index;}
check(has(screen,"sanctuary.progression.aptitude_permanent"),"Food Knowledge is visibly permanent");
check(key(afterTitle(screen,"sanctuary.food_knowledge.title")).equals("sanctuary.food_knowledge.learned") && !afterTitle(screen,"sanctuary.food_knowledge.title").active,"Food Knowledge has no activation switch");
for(var entry:TITLES.entrySet()){var button=afterTitle(screen,entry.getValue());boolean toggle=AptitudePreferences.TOGGLEABLE.contains(entry.getKey());
check(key(button).equals(toggle?"sanctuary.progression.aptitude_enabled":"sanctuary.progression.aptitude_acquired") && button.active==toggle,"Only World Map and Mob Names have switches: "+entry.getKey());check(m.font.width(button.getMessage())+8<=button.getWidth(),"Aptitude label fits without clipping");}
});
shot(c,"top-"+language+"-"+scale);
for(String target:AptitudePreferences.TOGGLEABLE) {
c.runOnClient(m->{var screen=m.gui.screen();var button=afterTitle(screen,TITLES.get(target));reveal(screen,button);check(button.active,"Toggle is ready: "+target);button.onPress(enter());});
c.waitFor(m->!AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_disabled"),200);
check(server.computeOnServer(s->!AptitudeService.enabled(s.getPlayerList().getPlayers().getFirst(),target)),"UI choice is authoritative: "+target);
if(target.equals("atlas")) {
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),false);});shot(c,"pause-map-disabled-"+language+"-"+scale);
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(new PauseScreen(true))));
}
c.runOnClient(m->afterTitle(m.gui.screen(),TITLES.get(target)).onPress(enter()));
c.waitFor(m->AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_enabled"),200);
}
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),true);});shot(c,"pause-map-enabled-"+language+"-"+scale);
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));reveal(m.gui.screen(),afterTitle(m.gui.screen(),TITLES.get("smithing")));});
shot(c,"crafts-"+language+"-"+scale);
c.runOnClient(m->{var screen=m.gui.screen();var area=aptitudes(screen);area.setScrollAmount(100);});
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
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");
}finally{intro.set(false);}
}
private static void pauseLayout(Screen screen,boolean enabled) {Pause233ClientChecks.pause(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();
int index=java.util.stream.IntStream.range(0,list.size()).filter(i->key(list.get(i)).equals(title)).findFirst().orElseThrow(()->new AssertionError("Missing title "+title));
return list.subList(index+1,list.size()).stream().filter(Button.class::isInstance).map(Button.class::cast).findFirst().orElseThrow();
}
private static AbstractScrollArea aptitudes(Screen screen){return widgets(screen).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).max(Comparator.comparingInt(AbstractWidget::getX)).orElseThrow();}
private static void reveal(Screen screen,AbstractWidget widget){var area=aptitudes(screen);area.setScrollAmount(Math.clamp(area.scrollAmount()+widget.getBottom()-area.getBottom()+4,0,area.maxScrollAmount()));}
private static void layout(Screen screen){var area=aptitudes(screen);check(area.getX()>=0&&area.getRight()<=screen.width&&area.getBottom()<screen.height-26,"Aptitudes viewport stays inside native frame");for(var button:widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).toList())check(button.getX()>=0&&button.getRight()<=screen.width,"Buttons remain within the screen horizontally");}
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("forgerie238-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/forgerie238-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
}
@@ -10,7 +10,6 @@ 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.loader.api.FabricLoader;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.gui.components.*;
import net.minecraft.client.gui.screens.*;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
@@ -38,7 +37,6 @@ public final class Story234ClientChecks implements FabricClientGameTest {
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(s).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
String before=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
shortcut(c,unlocked);
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
c.waitTicks(3);
@@ -46,10 +44,6 @@ public final class Story234ClientChecks implements FabricClientGameTest {
shot(c,"pause-"+unlocked+"-"+language+"-"+scale);
c.runOnClient(m->{var pause=m.gui.screen();press(pause,"sanctuary.story.title");check(m.gui.screen() instanceof StoryScreen,"Story opens from Pause without Atlas gating");check(m.gui.screen().isPauseScreen(),"Story retains solo pause");check(m.gui.screen().getTitle().getString().equals("Story"),"Story has its own accessible screen title");check(edit(m.gui.screen()).getValue().isEmpty()&&cycle(m.gui.screen()).getValue().toString().equals("ALL"),"Empty search and All on opening");assertEntries(m.gui.screen(),100);layout(m.gui.screen());});
shot(c,"story-"+language+"-"+scale);
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);
s.keyPressed(new KeyEvent(mapKey().getDefaultKey().getValue(),InputConstants.KEYCODE_M,0));
check(s.charTyped(new CharacterEvent('m'))&&m.gui.screen()==s&&search.getValue().equals("m")&&search.isFocused(),"Typing M in Story keeps its screen, text and focus");
check(net.minecraft.network.chat.Component.translatable("sanctuary.atlas.key").getString().equals(language.equals("fr_fr")?"Ouvrir la carte":"Open world map"),"Map shortcut has a localized Controls label");search.setValue("");});
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);for(int codepoint:"100".codePoints().toArray()){check(s.charTyped(new CharacterEvent(codepoint)),"Native typing handled");check(s.getFocused()==search&&search.isFocused(),"List refresh retains search focus");}check(search.getValue().equals("100")&&search.getCursorPosition()==3,"Continuous typing retains cursor");assertEntries(s,1);check(entries(s).getFirst().getMessage().getString().equals(StoryScreen.label("unknown_entry",100).getString()),"Search selects story 100");search.setValue(" 100 ");assertEntries(s,1);cycle(s).onPress(enter());check(has(s,"sanctuary.story.no_discoveries")&&!has(s,"sanctuary.story.no_results")&&entries(s).isEmpty(),"Discovered has a distinct empty state");cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("UNKNOWN"),"Third filter is Unknown");assertEntries(s,1);search.setValue("absent-story");check(has(s,"sanctuary.story.no_results")&&!has(s,"sanctuary.story.no_discoveries"),"Search empty state is distinct");search.setValue(language.equals("fr_fr")?"INCONNUE":"UNKNOWN");assertEntries(s,100);var area=area(s);check(s.mouseScrolled(area.getX()+5,area.getY()+5,0,-5)&&area.scrollAmount()>0,"Native wheel scrolls Story");area.setScrollAmount(area.maxScrollAmount());check(entries(s).getLast().getBottom()<=area.getBottom(),"Story 100 can be reached at the end");area.setScrollAmount(100);});
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
c.runOnClient(m->{var s=m.gui.screen();check(edit(s).getValue().equals(language.equals("fr_fr")?"INCONNUE":"UNKNOWN")&&cycle(s).getValue().toString().equals("UNKNOWN"),"Resize retains query and filter");check(area(s).scrollAmount()==100,"Resize retains scroll position");layout(s);edit(s).setValue("100");check(area(s).scrollAmount()==0,"Search resets scroll");edit(s).setValue("");area(s).setScrollAmount(100);cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("ALL")&&area(s).scrollAmount()==0,"Filter resets scroll and cycles back to All");assertEntries(s,100);});
@@ -68,35 +62,6 @@ public final class Story234ClientChecks implements FabricClientGameTest {
}finally{intro.set(false);}
}
private static KeyMapping mapKey(){return KeyMapping.get("sanctuary.atlas.key");}
private static void shortcut(ClientGameTestContext c,boolean unlocked) {
var saved=c.computeOnClient(m->InputConstants.getKey(mapKey().saveString()));
int scan=c.computeOnClient(m->{var key=mapKey();check(key!=null,"Map is registered in native Controls");
int nativeScan=org.lwjgl.sdl.SDLKeyboard.SDL_GetScancodeFromKey(InputConstants.KEYCODE_M,(java.nio.ShortBuffer)null);
check(nativeScan>0&&key.getDefaultKey().getValue()==nativeScan,"Default follows the keyboard layout's M letter");
key.setKey(key.getDefaultKey());KeyMapping.resetMapping();m.gui.setScreen(null);return nativeScan;});
try {
c.getInput().pressKey(scan);c.waitTicks(5);
c.runOnClient(m->{check(unlocked?m.gui.screen() instanceof SanctuaryAtlasScreen:m.gui.screen()==null,"Game M respects Atlas unlock");});
if(unlocked){c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==null,"Atlas opened from game returns to game"));}
var pause=c.computeOnClient(m->{var screen=new PauseScreen(true);m.gui.setScreen(screen);return screen;});
c.getInput().pressKey(scan);c.waitTicks(3);
c.runOnClient(m->{check(unlocked?m.gui.screen() instanceof SanctuaryAtlasScreen:m.gui.screen()==pause,"Pause M respects Atlas unlock");
if(unlocked){press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"M from Pause keeps original parent");}m.gui.setScreen(null);});
if(unlocked) {
c.runOnClient(m->{mapKey().setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_J));KeyMapping.resetMapping();});
c.getInput().pressKey(scan);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==null,"Old M binding stops opening the map"));
c.getInput().pressKey(InputConstants.KEY_J);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof SanctuaryAtlasScreen,"Remapped J opens map from game"));
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
c.runOnClient(m->m.gui.setScreen(pause));c.getInput().pressKey(scan);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==pause,"Pause respects removed M binding"));
c.getInput().pressKey(InputConstants.KEY_J);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof SanctuaryAtlasScreen,"Pause respects remapped J"));
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->{check(m.gui.screen()==pause,"Remapped shortcut retains Pause parent");mapKey().setKey(InputConstants.UNKNOWN);KeyMapping.resetMapping();});
c.getInput().pressKey(scan);c.getInput().pressKey(InputConstants.KEY_J);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==pause,"Unbound disables map shortcut"));
}
System.out.println("ATLAS236_SHORTCUT_PASS: native layout-aware M, game/Pause returns, unlock="+unlocked+", configurable key and Unbound, binding restored");
}finally{c.getInput().releaseKey(scan);c.getInput().releaseKey(InputConstants.KEY_J);c.runOnClient(m->{m.gui.setScreen(null);mapKey().setKey(saved);KeyMapping.resetMapping();});}
}
private static List<Button> entries(Screen s){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("sanctuary.story.unknown_entry")).toList();}
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
private static CycleButton<?> cycle(Screen s){return widgets(s).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();}
@@ -0,0 +1,129 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.companion.*;
import fr.koka.sanctuary.recipes.*;
import fr.koka.sanctuary.inventory.*;
import fr.koka.sanctuary.atlas.AtlasService;
import java.util.UUID;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.storage.*;
import net.minecraft.util.ProblemReporter;
import net.minecraft.network.chat.Component;
/** Behavioral checks on real server players, native menus and vanilla persistence. */
public final class Aptitudes236GameTests {
@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);
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);
h.assertTrue(RecipeService.buy(p),"Catalogue acquired through its normal server purchase");
h.assertTrue(CraftAptitudes.buy(p,"enchanting",record.revision()) && CraftAptitudes.buy(p,"alchemy",record.revision()),"New craft purchases accepted");
String proof=p.getAttachedOrThrow(ProgressionService.RECORD),recipes=p.getAttached(RecipeService.RECORD),bond=p.getAttached(CompanionService.RECORD);
int xp=p.experienceLevel;
h.assertTrue(!p.hasAttached(AptitudeService.PREFERENCES),"Old players need no preference migration");
for(String target:AptitudePreferences.TOGGLEABLE)h.assertTrue(toggle(p,target,false),"Owned aptitude can be disabled: "+target);
h.assertTrue(AptitudeService.preferences(p).disabledMask()==AptitudePreferences.ACTIVE_MASK && p.experienceLevel==xp,"Both display choices are independent and free");
h.assertTrue(proof.equals(p.getAttached(ProgressionService.RECORD)) && recipes.equals(p.getAttached(RecipeService.RECORD)) && bond.equals(p.getAttached(CompanionService.RECORD)),"Purchase proofs remain byte-for-byte unchanged");
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
h.assertTrue(AptitudeService.preferences(loaded).equals(AptitudeService.preferences(p)) && CraftAptitudes.learned(loaded,"enchanting") && CraftAptitudes.learned(loaded,"alchemy"),"Choices and craft purchases reload from vanilla player NBT");
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
h.assertTrue(AptitudeService.preferences(respawn).equals(AptitudeService.preferences(p)) && CraftAptitudes.learned(respawn,"alchemy"),"Choices and new purchases survive ordinary death");
bind(p,record.nextCycle());
h.assertTrue(AptitudeService.preferences(p).disabledMask()==AptitudePreferences.ACTIVE_MASK && CraftAptitudes.learned(p,"enchanting"),"Skill reset preserves separate preferences and craft purchases");
for(String target:AptitudePreferences.TOGGLEABLE)h.assertTrue(toggle(p,target,true),"Reactivation needs no new purchase: "+target);
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);
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");
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);
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;
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
h.assertTrue(loaded.getAttachedOrThrow(AptitudeService.PREFERENCES)==2047,"Historical codec still reloads all eleven bits");
h.assertTrue(AptitudeService.preferences(loaded).disabledMask()==3 && !AptitudeService.enabled(loaded,"names") && !AptitudeService.enabled(loaded,"atlas"),"Legacy display choices retain their original bit positions");
for(String target:AptitudePreferences.PERMANENT) {
h.assertTrue(AptitudeService.enabled(loaded,target),"Legacy permanent preference has no effect: "+target);
h.assertTrue(!toggle(p,target,false),"Permanent aptitude cannot be disabled: "+target);
}
AptitudeService.send(loaded);
h.assertTrue(loaded.getAttachedOrThrow(AptitudeService.PREFERENCES)==3 && loaded.experienceLevel==xp && proof.equals(loaded.getAttached(ProgressionService.RECORD)),"Sync normalizes only preferences, preserving XP and purchase proofs");
h.assertTrue(CraftAptitudes.learned(loaded,"enchanting") && CraftAptitudes.learned(loaded,"alchemy"),"Migration retains both craft purchases");h.succeed();
}
@GameTest public void permanentServerEffectsIgnoreLegacyDisabledBits(GameTestHelper h) {
var r=ProgressRecord.INITIAL.buy("hunger",1).buy("swimming",4).buy("resting",4).buy("sorting",4).buy("atlas",4);
var p=player(h,r);p.getFoodData().setFoodLevel(8);p.setAttached(AptitudeService.PREFERENCES,2044);
var at=p.blockPosition();for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int y=0;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.WATER.defaultBlockState());
p.baseTick();p.baseTick();p.setSprinting(true);p.setSwimming(true);
h.assertTrue(p.isSwimming() && !toggle(p,"swimming",false) && p.isSwimming() && ProgressEffects.canSwim(p),"Swimming stays usable with an old disabled bit and a rejected request");
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int y=0;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.AIR.defaultBlockState());
p.baseTick();p.baseTick();p.setOnGround(true);
h.assertTrue(LyingService.request(p,new LyingService.Request(2,r.revision())),"Purchased Rest ignores its legacy disabled bit");
h.assertTrue(!toggle(p,"resting",false) && LyingService.posture(p)==2,"Refused disable request never stops Rest");
h.assertTrue(InventorySortService.sort(p,new InventorySortPayloads.Sort(1,p.containerMenu.containerId,p.containerMenu.getStateId(),r.revision(),InventorySortPayloads.HOTBAR)),"Purchased inventory sorting ignores its legacy disabled bit");
h.assertTrue(AtlasService.canRead(p,false) && toggle(p,"atlas",false) && !AtlasService.canRead(p,false),"Personal World Map access follows the display choice");
h.assertTrue(toggle(p,"atlas",true) && AtlasService.canRead(p,false),"World Map reactivates with its existing purchase");
LyingService.request(p,new LyingService.Request(0,r.revision()));
p.setAttached(CompanionService.RECORD,CompanionRecord.EMPTY.purchase(4,1).json());
h.assertTrue(!toggle(p,"active_bond",false) && CompanionService.activate(p,-1).equals("no_familiar"),"Active Bond reaches the normal familiar requirement despite its legacy disabled bit");h.succeed();
}
@GameTest public void enchantingNativeMenuRequiresPermanentPurchase(GameTestHelper h) {
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");
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");
int xp=p.experienceLevel;
h.assertTrue(!toggle(p,"enchanting",false) && p.containerMenu==menu && menu.stillValid(p) && p.experienceLevel==xp,"Purchased enchanting cannot be disabled or closed by a toggle request");
p.closeContainer();h.assertTrue(p.openMenu(provider).isPresent(),"Enchanting remains available on later visits");p.closeContainer();h.succeed();
}
@GameTest public void alchemyNativeMenuRetainsSharedInventory(GameTestHelper h) {
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");
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");
p.closeContainer();h.assertTrue(p.openMenu(stand).isPresent(),"Purchased Alchemy stays available on later visits");
p.closeContainer();var vanilla=Progression003GameTests.player(h);
h.assertTrue(CraftAptitudes.allowed(vanilla,"enchanting") && CraftAptitudes.allowed(vanilla,"alchemy"),"Inactive vanilla players retain native access");h.succeed();
}
private static boolean toggle(ServerPlayer p,String target,boolean enabled) {return AptitudeService.set(p,new AptitudeService.Set(target,enabled,AptitudeService.preferences(p).disabledMask()));}
private static ServerPlayer player(GameTestHelper h,ProgressRecord record) {
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
try {var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;identities.create(p.getUUID(),"apt"+p.getUUID().toString().substring(0,8),"",identities.available().getFirst());}
catch(Exception e){throw new AssertionError(e);}
bind(p,record);return p;
}
private static void bind(ServerPlayer p,ProgressRecord record){p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
}
@@ -0,0 +1,110 @@
package fr.koka.sanctuary.gametest;
import fr.koka.sanctuary.progression.*;
import fr.koka.sanctuary.inventory.*;
import fr.koka.sanctuary.cosmetics.HeadBlockService;
import java.util.UUID;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.inventory.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.storage.*;
import net.minecraft.util.ProblemReporter;
/** Purchase persistence and real native repairs/upgrades, including worn workstations. */
public final class Forgerie238GameTests {
@GameTest public void purchaseIsPermanentAndSurvivesSaveDeathAndCycle(GameTestHelper h) {
var r=ProgressRecord.INITIAL;
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(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");
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
h.assertTrue(CraftAptitudes.learned(loaded,"smithing") && loaded.experienceLevel==8 && proof.equals(loaded.getAttached(ProgressionService.RECORD)),"Native NBT reload preserves the purchase and XP");
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
h.assertTrue(CraftAptitudes.learned(respawn,"smithing"),"Forgerie survives ordinary death");
bind(p,r.nextCycle());h.assertTrue(CraftAptitudes.learned(p,"smithing") && CraftAptitudes.allowed(p,"smithing"),"Forgerie survives New Game+");h.succeed();
}
@GameTest public void allAnvilsRequirePurchaseThenRepairAndRename(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at.below(),Blocks.STONE.defaultBlockState());
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)).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");
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");
var menu=(AnvilMenu)p.containerMenu;var sword=new ItemStack(Items.DIAMOND_SWORD);sword.setDamageValue(100);
menu.getSlot(AnvilMenu.INPUT_SLOT).set(sword);menu.getSlot(AnvilMenu.ADDITIONAL_SLOT).set(new ItemStack(Items.DIAMOND));
h.assertTrue(menu.setItemName("Forgerie238") && menu.stillValid(p),"Native anvil accepts a repair and name");
int cost=menu.getCost(),xp=p.experienceLevel;
var result=menu.getSlot(AnvilMenu.RESULT_SLOT).safeTake(1,1,p);
h.assertTrue(result.is(Items.DIAMOND_SWORD) && result.getDamageValue()==0 && result.get(DataComponents.CUSTOM_NAME).getString().equals("Forgerie238"),"Native result is repaired and renamed");
h.assertTrue(cost>0 && p.experienceLevel==xp-cost && menu.getSlot(AnvilMenu.ADDITIONAL_SLOT).getItem().isEmpty(),"Native operation consumes its normal XP and repair material");p.closeContainer();
}
h.succeed();
}
@GameTest public void smithingTableUpgradesAndTrimsAfterPurchase(GameTestHelper h) {
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");
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);
h.assertTrue(upgrade.is(Items.NETHERITE_SWORD) && p.experienceLevel==0,"Real netherite upgrade works without extra XP");
h.assertTrue(menu.getSlot(0).getItem().isEmpty() && menu.getSlot(1).getItem().isEmpty() && menu.getSlot(2).getItem().isEmpty(),"Upgrade consumes each native input once");
inputs(menu,Items.SENTRY_ARMOR_TRIM_SMITHING_TEMPLATE,Items.DIAMOND_CHESTPLATE,Items.REDSTONE);
var trimmed=menu.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p);
h.assertTrue(trimmed.is(Items.DIAMOND_CHESTPLATE) && trimmed.has(DataComponents.TRIM),"Native armor trim remains available");
p.closeContainer();h.assertTrue(p.openMenu(provider).isPresent(),"Forgerie remains available on later visits");p.closeContainer();h.succeed();
}
@GameTest public void forgedMenusCannotYieldAnUnownedResult(GameTestHelper h) {
var p=player(h,ProgressRecord.INITIAL);p.setExperienceLevels(20);var at=p.blockPosition();
h.getLevel().setBlockAndUpdate(at,Blocks.ANVIL.defaultBlockState());
var anvil=new AnvilMenu(7,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),at));
anvil.getSlot(0).set(new ItemStack(Items.DIAMOND_SWORD));anvil.setItemName("Forbidden");
h.assertTrue(anvil.getSlot(AnvilMenu.RESULT_SLOT).hasItem() && !anvil.stillValid(p) && !anvil.getSlot(AnvilMenu.RESULT_SLOT).mayPickup(p),"Anvil constructed outside normal opening is still locked");
h.assertTrue(anvil.getSlot(AnvilMenu.RESULT_SLOT).safeTake(1,1,p).isEmpty() && p.experienceLevel==20 && anvil.getSlot(0).hasItem(),"Rejected anvil take changes neither XP nor inputs");anvil.removed(p);
h.getLevel().setBlockAndUpdate(at,Blocks.SMITHING_TABLE.defaultBlockState());
var forge=new SmithingMenu(8,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),at));inputs(forge,Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE,Items.DIAMOND_SWORD,Items.NETHERITE_INGOT);
h.assertTrue(forge.getSlot(SmithingMenu.RESULT_SLOT).hasItem() && !forge.stillValid(p) && !forge.getSlot(SmithingMenu.RESULT_SLOT).mayPickup(p),"Smithing's inherited result gate rejects an unowned menu");
h.assertTrue(forge.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p).isEmpty() && forge.getSlot(1).hasItem(),"Rejected smithing take never consumes its base item");forge.removed(p);
var vanilla=Progression003GameTests.player(h);h.assertTrue(CraftAptitudes.allowed(vanilla,"smithing"),"Players outside Sanctuary progression retain vanilla access");h.succeed();
}
@GameTest public void wornWorkstationsRespectTheVisitorsPurchase(GameTestHelper h) {
var owner=player(h,ProgressRecord.INITIAL);var actor=player(h,ProgressRecord.INITIAL);actor.setPos(owner.position().add(1,0,0));
Item[] items={Items.ANVIL,Items.CHIPPED_ANVIL,Items.DAMAGED_ANVIL,Items.SMITHING_TABLE};
for(var item:items) {
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");
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);
var menu=actor.containerMenu;h.assertTrue(HeadBlockService.validMenu(menu,actor,()->menu.stillValid(actor)),"Native worn menu stays valid for learned visitor");actor.closeContainer();
}
AccessoryAccess.of(owner).removeItemNoUpdate(AccessoryContainer.HEAD);h.succeed();
}
private static void inputs(SmithingMenu menu,Item template,Item base,Item addition) {menu.getSlot(0).set(new ItemStack(template));menu.getSlot(1).set(new ItemStack(base));menu.getSlot(2).set(new ItemStack(addition));}
private static ServerPlayer player(GameTestHelper h,ProgressRecord record) {
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
try {var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;identities.create(p.getUUID(),"forge"+p.getUUID().toString().substring(0,8),"",identities.available().getFirst());}
catch(Exception e){throw new AssertionError(e);}
bind(p,record);return p;
}
private static void bind(ServerPlayer p,ProgressRecord record){p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
}
@@ -68,6 +68,7 @@ public class HeadBlocks043GameTests {
}
@GameTest public void nativeWorkstationsOpenAndCloseAfterMovement(GameTestHelper h){
var owner=player(h);var actor=player(h);actor.setPos(owner.position().add(1,0,0));
for(var target:new String[]{"enchanting","alchemy","smithing"}){actor.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(actor,target,0),"Native workstation fixture owns "+target);}
Item[] items={Items.CRAFTING_TABLE,Items.CHEST,Items.TRAPPED_CHEST,Items.BARREL,Items.ENDER_CHEST,Items.SHULKER_BOX,
Items.FURNACE,Items.BLAST_FURNACE,Items.SMOKER,Items.BREWING_STAND,Items.HOPPER,Items.DISPENSER,Items.DROPPER,
Items.CRAFTER,Items.STONECUTTER,Items.SMITHING_TABLE,Items.LOOM,Items.CARTOGRAPHY_TABLE,Items.GRINDSTONE,Items.ANVIL,Items.ENCHANTING_TABLE,Items.BEACON};
@@ -8,7 +8,7 @@
"entrypoints": {
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
"fabric-gametest": ["fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests"],
"fabric-gametest": ["fr.koka.sanctuary.gametest.Forgerie238GameTests","fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -44,7 +44,7 @@ public final class AtlasService {
var session=ProgressionService.session(player.level().getServer());
var record=ProgressEffects.get(player);
return session!=null && session.healthy && player.level().dimension()==Level.OVERWORLD
&& (operator ? canOperate(player) : record!=null && record.atlas());
&& (operator ? canOperate(player) : record!=null && record.atlas() && fr.koka.sanctuary.progression.AptitudeService.enabled(player,"atlas"));
}
private static State state(ServerPlayer player) {
var server=player.level().getServer();
@@ -0,0 +1,60 @@
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.progression.*;
import net.fabricmc.fabric.api.client.networking.v1.*;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.layouts.LinearLayout;
public final class AptitudeClient {
private static boolean ready;
private static long pendingUntil;
private AptitudeClient() {}
public static boolean enabled(String target) {
var p=Minecraft.getInstance().player;
return p==null || AptitudeService.enabled(p,target);
}
public static void register() {
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.Capability.TYPE,(p,c)->{});
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.PermanentCapability.TYPE,(p,c)->{});
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.SmithingCapability.TYPE,(p,c)->{});
ClientPlayNetworking.registerGlobalReceiver(AptitudeService.SmithingState.TYPE,(state,c)->c.client().execute(()->{
var player=c.client().player;
if(player==null || player.getId()!=state.entity())return;
player.setAttached(CraftAptitudes.SMITHING,state.learned());
if(c.client().gui.screen() instanceof ProgressionScreen screen)screen.accept("");
}));
ClientPlayNetworking.registerGlobalReceiver(AptitudeService.State.TYPE,(state,c)->c.client().execute(()->{
var player=c.client().player;
if(player==null || player.getId()!=state.entity())return;
var value=new AptitudePreferences(state.disabledMask());
boolean mapChanged=enabled("atlas")!=value.enabled("atlas");
player.setAttached(AptitudeService.PREFERENCES,value.disabledMask());
player.setAttached(CraftAptitudes.ENCHANTING,state.enchanting());
player.setAttached(CraftAptitudes.ALCHEMY,state.alchemy());ready=true;pendingUntil=0;
if(mapChanged && c.client().gui.screen() instanceof SanctuaryAtlasScreen map)map.permissionsChanged();
if(c.client().gui.screen() instanceof ProgressionScreen screen)screen.accept("");
}));
ClientPlayConnectionEvents.DISCONNECT.register((h,c)->reset());
ClientConfigurationConnectionEvents.INIT.register((h,c)->reset());
}
private static void reset(){ready=false;pendingUntil=0;}
public static void acquired(LinearLayout group,int width) {
var button=group.addChild(Button.builder(ProgressionService.label("aptitude_acquired"),b->{}).width(width)
.tooltip(Tooltip.create(ProgressionService.label("aptitude_acquired_hint"))).build());
button.active=false;
}
public static void toggle(LinearLayout group,int width,String target,boolean learned) {
if(!AptitudePreferences.TOGGLEABLE.contains(target))throw new IllegalArgumentException("Permanent aptitude has no switch");
var button=group.addChild(Button.builder(ProgressionService.label(enabled(target)?"aptitude_enabled":"aptitude_disabled"),b->{
var player=Minecraft.getInstance().player;
if(player==null || !ready || System.nanoTime()<pendingUntil || !ClientPlayNetworking.canSend(AptitudeService.Set.TYPE))return;
pendingUntil=System.nanoTime()+4_000_000_000L;b.active=false;
if(Minecraft.getInstance().gui.screen() instanceof ProgressionScreen screen)screen.waitForResponse();
ClientPlayNetworking.send(new AptitudeService.Set(target,!enabled(target),AptitudeService.preferences(player).disabledMask()));
}).width(width).tooltip(Tooltip.create(ProgressionService.label(enabled(target)?"aptitude_disable_hint":"aptitude_enable_hint")
.copy().append("\n").append(ProgressionService.label("aptitude_toggle_hint")))).build());
button.active=learned && ready && System.nanoTime()>=pendingUntil;
}
}
@@ -1,20 +1,11 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.atlas.AtlasPayloads;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.KeyEvent;
/** No cross-server atlas cache: responses belong only to the currently open screen/token. */
public final class AtlasClient {
private static int sequence;
private static KeyMapping mapKey;
private AtlasClient() { }
public static void register() {
ClientPlayNetworking.registerGlobalReceiver(fr.koka.sanctuary.atlas.AtlasMarkerPayload.TYPE,(p,c) -> c.client().execute(() -> {
@@ -28,49 +19,6 @@ public final class AtlasClient {
else if (c.client().gui.screen() instanceof PauseAtlasAccess pause && pause.sanctuary$pauseMap()!=null) pause.sanctuary$pauseMap().accept(p);
}));
}
public static void registerShortcut(KeyMapping.Category category) {
var shortcut=new MapKey(category);
mapKey=KeyMappingHelper.registerKeyMapping(shortcut);
ClientLifecycleEvents.CLIENT_STARTED.register(client->shortcut.resolveLayout());
ClientTickEvents.END_CLIENT_TICK.register(client->{
while(mapKey.consumeClick())if(client.gui.screen()==null)openFromShortcut(null);
});
}
public static boolean openFromKey(Screen parent,KeyEvent event) {
return mapKey!=null && mapKey.matches(event) && openFromShortcut(parent);
}
private static boolean openFromShortcut(Screen parent) {
var client=Minecraft.getInstance();
if(client.player==null || client.level==null || !client.player.isAlive() || !ProgressionClient.active())return false;
if(!ProgressionClient.progress.atlas())client.player.sendOverlayMessage(SanctuaryAtlasScreen.label("locked"));
else ProgressionClient.open(parent,"history");
return true;
}
private static final class MapKey extends KeyMapping {
private InputConstants.Key layoutDefault;
private final boolean savedBinding;
MapKey(Category category) {
super("sanctuary.atlas.key",InputConstants.Type.KEYBOARD,InputConstants.KEY_M,category);
layoutDefault=super.getDefaultKey();
// An explicit saved binding, including Unbound, takes precedence over the layout default.
var options=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("options.txt");
boolean saved=false;
if(java.nio.file.Files.exists(options))try(var lines=java.nio.file.Files.lines(options)) {
saved=lines.anyMatch(line->line.startsWith("key_"+getName()+":"));
}catch(java.io.IOException error){saved=true;}
savedBinding=saved;
}
void resolveLayout() {
// Resolve the letter M after native startup: KEY_M is only its QWERTY position.
boolean useDefault=!savedBinding && isDefault();
int scan=org.lwjgl.sdl.SDLKeyboard.SDL_GetScancodeFromKey(InputConstants.KEYCODE_M,(java.nio.ShortBuffer)null);
if(scan>0)layoutDefault=InputConstants.Type.KEYBOARD.getOrCreate(scan);
if(useDefault)setKey(layoutDefault);
KeyMapping.resetMapping();
}
@Override public InputConstants.Key getDefaultKey(){return layoutDefault;}
@Override public boolean isDefault(){return key.equals(layoutDefault);}
}
public static int nextToken() {sequence=sequence==Integer.MAX_VALUE ? 1 : sequence+1;return sequence;}
public static void send(AtlasPayloads.Request request) {
if (ClientPlayNetworking.canSend(AtlasPayloads.Request.TYPE)) ClientPlayNetworking.send(request);
@@ -24,7 +24,7 @@ public final class CommunityPausePanels implements AutoCloseable {
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,List<Button> nativeButtons) {
this.screen=screen;
boolean compact=screen.height<280;
boolean unlocked=ProgressionClient.progress.atlas();
boolean unlocked=ProgressionClient.progress.atlas() && AptitudeClient.enabled("atlas");
int margin=8,gap=10,top=compact?46:50,bottom=screen.height-39;
String[] sections={"blocks","identity","progress","faction","story","battle"};
int columnGap=compact?4:8;
@@ -117,10 +117,11 @@ public final class CompanionClient {
var buy=section.addChild(Button.builder(label("buy",config.activeCost()),b->action("buy",-1)).width(width).build());
buy.active=config.enabled()&&Minecraft.getInstance().player.experienceLevel>=config.activeCost();
}
if(record!=null&&record.unlocked())section.addChild(new StringWidget(fr.koka.sanctuary.progression.ProgressionService.label("completed"),font));
if(record!=null&&record.unlocked())AptitudeClient.acquired(section,width);
var study=contents.addChild(LinearLayout.vertical().spacing(4));
AptitudeHeading.add(study,width,font,"familiar",label("study"));
study.addChild(new MultiLineTextWidget(label("study_description"),font).setMaxWidth(width));
if(studied){AptitudeClient.acquired(study,width);return;}
var buy=study.addChild(Button.builder(label(studyInvalid?"storage_invalid":studied?"study_learned":"study_buy",CompanionStudy.COST),b->action("buy_study",-1)).width(width).build());
buy.active=!studied&&!studyInvalid&&record!=null&&config.enabled()&&Minecraft.getInstance().player.experienceLevel>=CompanionStudy.COST;
}
@@ -29,7 +29,7 @@ public final class CompanionTooltips {
public static void register(){
ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{
var type=type(stack);if(type==null||!ProgressionClient.active())return;
boolean details=CompanionClient.studied||ProgressionClient.operator;
boolean details=(CompanionClient.studied||ProgressionClient.operator);
var extra=new java.util.ArrayList<Component>();
extra.add(rarity(stack).label().copy().withStyle(color(stack)));
if(fr.koka.sanctuary.cosmetics.FamiliarEntity.burnsInDaylight(stack))
@@ -34,5 +34,6 @@ public final class FoodKnowledgeClient {
boolean learned=ready()&&knowledge.studied();
var button=group.addChild(Button.builder(label(invalid?"invalid":learned?"learned":"buy",FoodKnowledge.COST),b->{if(ClientPlayNetworking.canSend(FoodKnowledgeService.Buy.TYPE))ClientPlayNetworking.send(new FoodKnowledgeService.Buy());}).width(width).build());
button.active=ready()&&!learned&&Minecraft.getInstance().player.experienceLevel>=FoodKnowledge.COST;
if(learned)group.addChild(new MultiLineTextWidget(fr.koka.sanctuary.progression.ProgressionService.label("aptitude_permanent"),font).setMaxWidth(width));
}
}
@@ -23,7 +23,7 @@ public final class LyingClient {
if(current==3)continue; // The server ends a dive on contact, not on another key press.
int desired=m.options.keyShift.isDown() ? (current==2 ? 0 : 2) : (current!=0 ? 0 : 1);
if(desired==1&&!m.player.onGround()&&!m.player.isInWater())desired=3;
boolean learned=desired==0 || m.player.isCreative() || (desired==1 || desired==3 || ProgressionClient.progress.resting());
boolean learned=(desired==0 || m.player.isCreative() || desired==1 || desired==3 || ProgressionClient.progress.resting());
if(!learned)m.player.sendOverlayMessage(ProgressionService.label("resting_locked"));
else ClientPlayNetworking.send(new LyingService.Request(desired,ProgressionClient.progress.revision()));
}
@@ -34,6 +34,7 @@ public final class ProgressionClient {
else if (active()) gui.setScreen(section.equals("history") ? new SanctuaryAtlasScreen(parent) : new ProgressionScreen(parent,section));
}
public static void register() {
AptitudeClient.register();
IntroClient.register();
fr.koka.sanctuary.client.AccessoryClient.register();
AtlasClient.register();
@@ -92,24 +92,47 @@ public final class ProgressionScreen extends BlocodexFrame {
skills=scroll(skillContents,contentX+6,listTop,Math.max(24,listBottom-listTop),skillOffset);
int innerWidth=contentWidth-panel-32;
LinearLayout contents=LinearLayout.vertical().spacing(12);
for(String target:ProgressRecord.APTITUDES) {
var aptitude=contents.addChild(LinearLayout.vertical().spacing(4));
String icon=switch(target){case "atlas"->"worldmap";case "swimming"->"swim";case "resting"->"rest";default->target;};
AptitudeHeading.add(aptitude,innerWidth,font,icon,ProgressionService.label(target));
var description=ProgressionService.label(target+"_description");
if(target.equals("lying") || target.equals("resting"))description=description.copy().append("\n").append(LyingClient.controls(target.equals("resting")));
if(target.equals("sorting"))description=description.copy().append("\n").append(InventorySortClient.controls());
aptitude.addChild(new MultiLineTextWidget(description,font).setMaxWidth(innerWidth));
aptitude.addChild(purchaseButton(target,innerWidth));
for(var theme:List.of(
List.of("survival","swimming","resting","food_knowledge"),
List.of("inventory_crafting","sorting","catalogue"),
List.of("familiars","active_bond","familiar_study"),
List.of("exploration","atlas","names","zoom"),
List.of("crafts","enchanting","alchemy","smithing"))) {
contents.addChild(new MultiLineTextWidget(ProgressionService.label("theme_"+theme.getFirst()).copy()
.withStyle(net.minecraft.ChatFormatting.AQUA),font).setMaxWidth(innerWidth));
for(String target:theme.subList(1,theme.size())) {
switch(target) {
case "active_bond" -> CompanionClient.aptitude(contents,innerWidth,font,this);
case "familiar_study" -> {} // Rendered alongside Active Bond by the familiar module.
case "catalogue" -> RecipeClient.aptitude(contents,innerWidth,font);
case "food_knowledge" -> FoodKnowledgeClient.aptitude(contents,innerWidth,font);
case "zoom" -> ZoomClient.aptitude(contents,innerWidth,font);
default -> aptitude(contents,innerWidth,target);
}
}
}
double previousScroll=aptitudes==null ? 0 : aptitudes.getScrollAmount();
CompanionClient.aptitude(contents,innerWidth,font,this);
RecipeClient.aptitude(contents,innerWidth,font);
FoodKnowledgeClient.aptitude(contents,innerWidth,font);
ZoomClient.aptitude(contents,innerWidth,font);
aptitudes=scroll(contents,contentX+panel+16,listTop,listBottom-listTop,previousScroll);
}
private void aptitude(LinearLayout contents,int width,String target) {
var section=contents.addChild(LinearLayout.vertical().spacing(4));
String icon=switch(target){case "atlas"->"worldmap";case "swimming"->"swim";case "resting"->"rest";default->target;};
AptitudeHeading.add(section,width,font,icon,ProgressionService.label(target));
var description=ProgressionService.label(target+"_description");
if(target.equals("resting"))description=description.copy().append("\n").append(LyingClient.controls(true));
if(target.equals("sorting"))description=description.copy().append("\n").append(InventorySortClient.controls());
section.addChild(new MultiLineTextWidget(description,font).setMaxWidth(width));
boolean learned=CraftAptitudes.target(target) ? CraftAptitudes.learned(minecraft.player,target) : ProgressionClient.progress.rank(target)==1;
if(learned) {
if(AptitudePreferences.TOGGLEABLE.contains(target))AptitudeClient.toggle(section,width,target,true);
else AptitudeClient.acquired(section,width);
}
else section.addChild(purchaseButton(target,width));
}
public void waitForResponse() {pendingTicks=80;message="";purchases.forEach(button->button.active=false);}
private StringWidget textCell(LinearLayout row,int width,Component text) {
// StringWidget reports its text width in 26.3; the frame reserves a stable column.
return row.addChild(new FrameLayout(width,20)).addChild(new StringWidget(width,20,text,font)
@@ -194,13 +217,13 @@ public final class ProgressionScreen extends BlocodexFrame {
}
private Button purchaseButton(String target,int width) {
int cost=ProgressionClient.progress.cost(target,ProgressionClient.config);
int cost=CraftAptitudes.target(target) ? CraftAptitudes.COST : ProgressionClient.progress.cost(target,ProgressionClient.config);
Button button=Button.builder(cost<0 ? ProgressionService.label("completed")
: ProgressionService.label(ProgressRecord.APTITUDES.contains(target) ? "unlock_cost" : "upgrade_cost",cost),b -> {
: ProgressionService.label(ProgressRecord.APTITUDES.contains(target) || CraftAptitudes.target(target) ? "unlock_cost" : "upgrade_cost",cost),b -> {
pendingTicks=80;message="";purchases.forEach(widget -> widget.active=false);
ProgressionClient.action(target);
}).width(width).build();
button.active=ProgressionClient.progress.canRecord() && cost>=0 && minecraft.player!=null && minecraft.player.experienceLevel>=cost && pendingTicks==0;
button.active=(CraftAptitudes.target(target) || ProgressionClient.progress.canRecord()) && cost>=0 && minecraft.player!=null && minecraft.player.experienceLevel>=cost && pendingTicks==0;
purchases.add(button);
return button;
}
@@ -45,7 +45,7 @@ public final class RecipeClient {
String key="sanctuary.recipes.collection."+id.replace(':','.');
return Component.translatableWithFallback(key,id);
}
public static boolean allowed(){return ready&&!invalid&&(!active||operator||knowledge!=null&&knowledge.learned());}
public static boolean allowed(){return ready&&!invalid&&(!active||(operator||knowledge!=null&&knowledge.learned()));}
public static void reset(){ready=false;knowledge=null;active=false;invalid=false;operator=false;operatorAllowed=false;PENDING.setLength(0);revision=-1;nextPart=0;applied="";}
public static void request(String action){if(ClientPlayNetworking.canSend(RecipePayloads.Request.TYPE))ClientPlayNetworking.send(new RecipePayloads.Request(action));}
private static void accept(RecipePayloads.Snapshot p){
@@ -73,6 +73,7 @@ public final class RecipeClient {
var section=contents.addChild(LinearLayout.vertical().spacing(4));
AptitudeHeading.add(section,width,font,"recipes",label("catalogue"));
section.addChild(new MultiLineTextWidget(label("description"),font).setMaxWidth(width));
if(knowledge!=null && knowledge.learned()){AptitudeClient.acquired(section,width);return;}
var buy=section.addChild(Button.builder(label(invalid?"invalid":knowledge!=null&&knowledge.learned()?"learned":"buy",RecipeKnowledge.COST),b->request("buy")).width(width).build());
buy.active=ready&&!invalid&&knowledge!=null&&!knowledge.learned()&&xp>=RecipeKnowledge.COST;
}
@@ -339,7 +339,7 @@ public final class SanctuaryBlocodexScreen extends DiscoveryFrame {
}
private static boolean canBrowseAll() {
return ProgressionClient.operator || RecipeClient.ready && !RecipeClient.invalid && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned();
return (ProgressionClient.operator || RecipeClient.ready && !RecipeClient.invalid && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned());
}
private List<Filter> availableFilters() {
return fullCatalogue ? List.of(Filter.POSSESSED,Filter.ALL,Filter.SEEN,Filter.MINED,Filter.DROPPED,Filter.PLACED,Filter.CARRIED)
@@ -53,7 +53,6 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
GraveInventoryScreen.register();
TutorialClient.register();
var category=KeyMapping.Category.register(SanctuaryMod.id("sanctuary"));
AtlasClient.registerShortcut(category);
DynamicLightClient.register(category);
ZoomClient.register(category);
LyingClient.register(category);
@@ -84,7 +84,7 @@ public final class SanctuaryMobNames {
}
public static boolean shouldDisplay(Entity entity) {
return ProgressionClient.active() && ProgressionClient.progress.names() && ProgressionClient.names.enabled() && isTargeted(entity)
return ProgressionClient.active() && ProgressionClient.progress.names() && AptitudeClient.enabled("names") && ProgressionClient.names.enabled() && isTargeted(entity)
&& entity instanceof Mob && !entity.hasCustomName();
}
@@ -33,6 +33,7 @@ public final class ZoomClient {
var group=contents.addChild(LinearLayout.vertical().spacing(4));
AptitudeHeading.add(group,width,font,"zoom",label("title"));
group.addChild(new MultiLineTextWidget(label("description",key.getTranslatedKeyMessage()),font).setMaxWidth(width));
if(learned()){AptitudeClient.acquired(group,width);return;}
var button=group.addChild(Button.builder(label(learned()?"learned":"buy",ZoomService.COST),b->{
if(ClientPlayNetworking.canSend(ZoomService.Buy.TYPE))ClientPlayNetworking.send(new ZoomService.Buy());
}).width(width).build());
@@ -0,0 +1,17 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.progression.CraftAptitudes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.*;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Anvil overrides the combiner pickup check; Smithing inherits it. */
@Mixin({ItemCombinerMenu.class,AnvilMenu.class})
abstract class CraftAptitudeCombinerMixin {
@Inject(method="mayPickup",at=@At("HEAD"),cancellable=true)
private void sanctuary$forgePickup(Player player,boolean hasItem,CallbackInfoReturnable<Boolean> ci) {
if(!CraftAptitudes.allowed(player,(AbstractContainerMenu)(Object)this))ci.setReturnValue(false);
}
}
@@ -0,0 +1,16 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.progression.CraftAptitudes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.EnchantmentMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(EnchantmentMenu.class)
abstract class CraftAptitudeEnchantMixin {
@Inject(method="clickMenuButton",at=@At("HEAD"),cancellable=true)
private void sanctuary$enchantAccess(Player player,int button,CallbackInfoReturnable<Boolean> ci) {
if(!CraftAptitudes.allowed(player,"enchanting"))ci.setReturnValue(false);
}
}
@@ -0,0 +1,16 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.progression.CraftAptitudes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.*;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin({EnchantmentMenu.class,BrewingStandMenu.class,ItemCombinerMenu.class})
abstract class CraftAptitudeMenuMixin {
@Inject(method="stillValid",at=@At("HEAD"),cancellable=true)
private void sanctuary$craftAccess(Player player,CallbackInfoReturnable<Boolean> ci) {
if(!CraftAptitudes.allowed(player,(AbstractContainerMenu)(Object)this))ci.setReturnValue(false);
}
}
@@ -0,0 +1,23 @@
package fr.koka.sanctuary.mixin;
import com.llamalad7.mixinextras.injector.wrapoperation.*;
import fr.koka.sanctuary.progression.*;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.*;
import net.minecraft.world.inventory.AbstractContainerMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ServerPlayer.class)
abstract class CraftAptitudeOpenMixin {
@WrapOperation(method="openMenu",at=@At(value="INVOKE",target="Lnet/minecraft/world/MenuProvider;createMenu(ILnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/entity/player/Player;)Lnet/minecraft/world/inventory/AbstractContainerMenu;"))
private AbstractContainerMenu sanctuary$craftAccess(MenuProvider provider,int id,Inventory inventory,Player player,Operation<AbstractContainerMenu> original) {
var menu=original.call(provider,id,inventory,player);
if(menu!=null && !CraftAptitudes.allowed(player,menu)) {
((ServerPlayer)player).sendOverlayMessage(ProgressionService.label(CraftAptitudes.target(menu)+"_locked"));
menu.removed(player);return null;
}
return menu;
}
}
@@ -1,7 +1,6 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.client.ProgressionClient;
import fr.koka.sanctuary.client.AtlasClient;
import fr.koka.sanctuary.client.PauseAtlasAccess;
import fr.koka.sanctuary.client.PauseAtlasWidget;
import net.minecraft.client.gui.components.Button;
@@ -9,7 +8,6 @@ import fr.koka.sanctuary.client.CommunityPausePanels;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.PauseScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.client.input.KeyEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -32,10 +30,6 @@ public abstract class CommunityPauseMixin extends Screen implements PauseAtlasAc
}
@Inject(method="tick",at=@At("TAIL"))
private void sanctuary$communityTick(CallbackInfo callback) { if(sanctuary$community!=null)sanctuary$community.tick(); }
@Override public boolean keyPressed(KeyEvent event) {
if(sanctuary$community!=null && AtlasClient.openFromKey(this,event))return true;
return super.keyPressed(event);
}
@Override public PauseAtlasWidget sanctuary$pauseMap() {return sanctuary$community==null?null:sanctuary$community.map();}
@Inject(method="removed",at=@At("HEAD"))
private void sanctuary$communityRemoved(CallbackInfo callback) {if(sanctuary$community!=null)sanctuary$community.close();}
@@ -0,0 +1,27 @@
package fr.koka.sanctuary.progression;
import java.util.List;
/** Keep the beta.236 codec and bit order; only the first two choices remain effective. */
public record AptitudePreferences(int disabledMask) {
public static final List<String> TOGGLEABLE=List.of("names","atlas");
public static final List<String> PERMANENT=List.of("sorting","swimming","resting",
"active_bond","familiar_study","catalogue","zoom","enchanting","alchemy","smithing","food_knowledge");
public static final int MAX_MASK=2047;
public static final int ACTIVE_MASK=3;
public static final AptitudePreferences DEFAULT=new AptitudePreferences(0);
public AptitudePreferences {
if(disabledMask<0 || disabledMask>MAX_MASK)throw new IllegalArgumentException("Invalid aptitude preferences");
disabledMask&=ACTIVE_MASK;
}
private static int bit(String target) {
int index=TOGGLEABLE.indexOf(target);
if(index<0)throw new IllegalArgumentException("Unknown toggleable aptitude");
return 1<<index;
}
public boolean enabled(String target) {return PERMANENT.contains(target) || (disabledMask&bit(target))==0;}
public AptitudePreferences with(String target,boolean enabled) {
int bit=bit(target);
return new AptitudePreferences(enabled ? disabledMask&~bit : disabledMask|bit);
}
}
@@ -0,0 +1,95 @@
package fr.koka.sanctuary.progression;
import com.mojang.serialization.Codec;
import fr.koka.sanctuary.SanctuaryMod;
import net.fabricmc.fabric.api.attachment.v1.*;
import net.fabricmc.fabric.api.networking.v1.*;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
/** Personal, server-authoritative choices; never removes or rewrites a purchase. */
public final class AptitudeService {
public static final AttachmentType<Integer> PREFERENCES=AttachmentRegistry.create(SanctuaryMod.id("aptitude_preferences"),
b->b.persistent(Codec.intRange(0,AptitudePreferences.MAX_MASK)).copyOnDeath());
private AptitudeService() {}
public record Capability() implements CustomPacketPayload {
public static final Type<Capability> TYPE=new Type<>(SanctuaryMod.id("aptitude_preferences_v1"));
public static final StreamCodec<FriendlyByteBuf,Capability> CODEC=StreamCodec.unit(new Capability());
public Type<Capability> type(){return TYPE;}
}
public record PermanentCapability() implements CustomPacketPayload {
public static final Type<PermanentCapability> TYPE=new Type<>(SanctuaryMod.id("aptitude_permanent_v1"));
public static final StreamCodec<FriendlyByteBuf,PermanentCapability> CODEC=StreamCodec.unit(new PermanentCapability());
public Type<PermanentCapability> type(){return TYPE;}
}
public record SmithingCapability() implements CustomPacketPayload {
public static final Type<SmithingCapability> TYPE=new Type<>(SanctuaryMod.id("smithing_v1"));
public static final StreamCodec<FriendlyByteBuf,SmithingCapability> CODEC=StreamCodec.unit(new SmithingCapability());
public Type<SmithingCapability> type(){return TYPE;}
}
public record SmithingState(int entity,boolean learned) implements CustomPacketPayload {
public static final Type<SmithingState> TYPE=new Type<>(SanctuaryMod.id("smithing_state_v1"));
public static final StreamCodec<RegistryFriendlyByteBuf,SmithingState> CODEC=CustomPacketPayload.codec(
(p,b)->{b.writeVarInt(p.entity);b.writeBoolean(p.learned);},b->new SmithingState(b.readVarInt(),b.readBoolean()));
public Type<SmithingState> type(){return TYPE;}
}
public record Set(String target,boolean enabled,int expectedMask) implements CustomPacketPayload {
public static final Type<Set> TYPE=new Type<>(SanctuaryMod.id("aptitude_set_v1"));
public static final StreamCodec<RegistryFriendlyByteBuf,Set> CODEC=CustomPacketPayload.codec(
(p,b)->{b.writeUtf(p.target,32);b.writeBoolean(p.enabled);b.writeVarInt(p.expectedMask);},
b->new Set(b.readUtf(32),b.readBoolean(),b.readVarInt()));
public Type<Set> type(){return TYPE;}
}
public record State(int entity,int disabledMask,boolean enchanting,boolean alchemy) implements CustomPacketPayload {
public static final Type<State> TYPE=new Type<>(SanctuaryMod.id("aptitude_state_v1"));
public static final StreamCodec<RegistryFriendlyByteBuf,State> CODEC=CustomPacketPayload.codec(
(p,b)->{b.writeVarInt(p.entity);b.writeVarInt(p.disabledMask);b.writeBoolean(p.enchanting);b.writeBoolean(p.alchemy);},
b->new State(b.readVarInt(),b.readVarInt(),b.readBoolean(),b.readBoolean()));
public Type<State> type(){return TYPE;}
}
public static AptitudePreferences preferences(Player p) {return new AptitudePreferences(p.getAttachedOrElse(PREFERENCES,0));}
public static boolean enabled(Player p,String target) {return preferences(p).enabled(target);}
public static boolean owned(ServerPlayer p,String target) {
var progress=ProgressEffects.get(p);
if(progress==null)return false;
return AptitudePreferences.TOGGLEABLE.contains(target) && progress.rank(target)==1;
}
public static boolean set(ServerPlayer p,Set request) {
var session=ProgressionService.session(p.level().getServer());
if(session==null || !session.healthy || !p.isAlive() || p.isSpectator()
|| !AptitudePreferences.TOGGLEABLE.contains(request.target()))return false;
var old=preferences(p);
if(request.expectedMask()!=old.disabledMask())return false;
try {if(!owned(p,request.target()))return false;}
catch(RuntimeException invalid){return false;}
var next=old.with(request.target(),request.enabled());
if(!next.equals(old))p.setAttached(PREFERENCES,next.disabledMask());
return true;
}
public static void send(ServerPlayer p) {
// Normalize legacy preferences on sync without altering purchases or progression.
int mask=preferences(p).disabledMask();
if(p.getAttachedOrElse(PREFERENCES,0)!=mask)p.setAttached(PREFERENCES,mask);
if(ServerPlayNetworking.canSend(p,State.TYPE))ServerPlayNetworking.send(p,new State(p.getId(),preferences(p).disabledMask(),CraftAptitudes.learned(p,"enchanting"),CraftAptitudes.learned(p,"alchemy")));
if(ServerPlayNetworking.canSend(p,SmithingState.TYPE))ServerPlayNetworking.send(p,new SmithingState(p.getId(),CraftAptitudes.learned(p,"smithing")));
}
public static void register() {
java.util.Objects.requireNonNull(CraftAptitudes.ENCHANTING);
java.util.Objects.requireNonNull(CraftAptitudes.ALCHEMY);
java.util.Objects.requireNonNull(CraftAptitudes.SMITHING);
PayloadTypeRegistry.clientboundConfiguration().register(Capability.TYPE,Capability.CODEC);
PayloadTypeRegistry.clientboundConfiguration().register(PermanentCapability.TYPE,PermanentCapability.CODEC);
PayloadTypeRegistry.clientboundConfiguration().register(SmithingCapability.TYPE,SmithingCapability.CODEC);
PayloadTypeRegistry.clientboundPlay().register(SmithingState.TYPE,SmithingState.CODEC);
PayloadTypeRegistry.serverboundPlay().register(Set.TYPE,Set.CODEC);
PayloadTypeRegistry.clientboundPlay().register(State.TYPE,State.CODEC);
ServerPlayNetworking.registerGlobalReceiver(Set.TYPE,(request,c)->c.server().execute(()->{
boolean accepted=set(c.player(),request);
ProgressionService.send(c.player(),accepted ? "" : "aptitude_refused");
}));
}
}
@@ -0,0 +1,43 @@
package fr.koka.sanctuary.progression;
import com.mojang.serialization.Codec;
import fr.koka.sanctuary.SanctuaryMod;
import net.fabricmc.fabric.api.attachment.v1.*;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
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 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"),
b->b.persistent(Codec.BOOL).copyOnDeath());
public static final AttachmentType<Boolean> SMITHING=AttachmentRegistry.create(SanctuaryMod.id("smithing_learned"),
b->b.persistent(Codec.BOOL).copyOnDeath());
private CraftAptitudes() {}
public static boolean target(String target){return target.equals("enchanting") || target.equals("alchemy") || target.equals("smithing");}
private static AttachmentType<Boolean> attachment(String target) {
return switch(target){case "enchanting"->ENCHANTING;case "alchemy"->ALCHEMY;case "smithing"->SMITHING;default->throw new IllegalArgumentException("Unknown craft aptitude");};
}
public static boolean learned(Player p,String target){return p.getAttachedOrElse(attachment(target),false);}
public static String target(AbstractContainerMenu menu){return menu instanceof EnchantmentMenu ? "enchanting" : menu instanceof BrewingStandMenu ? "alchemy" : menu instanceof AnvilMenu || menu instanceof SmithingMenu ? "smithing" : "";}
public static boolean allowed(Player p,String target) {
if(ProgressEffects.get(p)==null)return true;
if(p instanceof ServerPlayer server) {
var session=ProgressionService.session(server.level().getServer());
if(session==null || !session.healthy)return false;
}
return learned(p,target);
}
public static boolean allowed(Player p,AbstractContainerMenu menu) {
String target=target(menu);return target.isEmpty() || allowed(p,target);
}
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;
}
}
@@ -26,7 +26,7 @@ public final class ProgressEffects {
}
public static boolean canSwim(Player player) {
var progress=get(player);
return progress==null || progress.swimming() || player.isCreative() || player.isSpectator();
return progress==null || (progress.swimming() || player.isCreative() || player.isSpectator());
}
public static void bind(Player player, ProgressRecord progress) {
PLAYERS.put(player, progress); FOOD.put(player.getFoodData(), progress);
@@ -67,6 +67,7 @@ public final class ProgressionService {
PayloadTypeRegistry.clientboundConfiguration().register(AccessPayloads.Status.TYPE,AccessPayloads.Status.CODEC);
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Check.TYPE,AccessPayloads.Check.CODEC);
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Admit.TYPE,AccessPayloads.Admit.CODEC);
AptitudeService.register();
SanctuarySitting.register();
fr.koka.sanctuary.inventory.HotbarService.register();
fr.koka.sanctuary.inventory.InventorySortService.register();
@@ -116,6 +117,13 @@ public final class ProgressionService {
if (!ServerConfigurationNetworking.canSend(handler,TutorialPayloads.Capability.TYPE)) {
handler.disconnect(TutorialService.label("version_required"));return;
}
if (!ServerConfigurationNetworking.canSend(handler,AptitudeService.Capability.TYPE)
|| !ServerConfigurationNetworking.canSend(handler,AptitudeService.PermanentCapability.TYPE)) {
handler.disconnect(label("aptitude_version_required"));return;
}
if (!ServerConfigurationNetworking.canSend(handler,AptitudeService.SmithingCapability.TYPE)) {
handler.disconnect(label("smithing_version_required"));return;
}
if (!ServerConfigurationNetworking.canSend(handler,ProgressionPayloads.SwimmingCapability.TYPE)) {
handler.disconnect(label("swimming_version_required"));return;
}
@@ -522,6 +530,10 @@ public final class ProgressionService {
boolean removed=refund(player,action.target().substring(7),action.revision());
send(player,removed ? "refunded" : "refund_refused");return;
}
if (CraftAptitudes.target(action.target())) {
boolean bought=CraftAptitudes.buy(player,action.target(),action.revision());
send(player,bought ? "purchased" : "purchase_refused");return;
}
if (!ProgressEffects.get(player).canRecord()) {send(player,"history_full");return;}
boolean bought=purchase(player,action.target(),action.revision());
send(player,bought ? "purchased" : "purchase_refused");
@@ -536,6 +548,7 @@ public final class ProgressionService {
// The solo pause menu processes networking without ticking the player's XP synchronizer.
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetExperiencePacket(
player.experienceProgress,player.totalExperience,player.experienceLevel));
AptitudeService.send(player);
ServerPlayNetworking.send(player,new ProgressionPayloads.Snapshot(
JSON.toJson(session.inhabitants.get(player.getUUID())),player.getAttachedOrThrow(RECORD),JSON.toJson(session.config),message,canOperate(player)));
player.inventoryMenu.broadcastFullState();
@@ -569,7 +569,7 @@
"sanctuary.progression.sorting_version_required": "Inventory sorting requires matching Sanctuary beta.014 or later versions on the client and server.",
"screen.sanctuary.inventory.sort_key": "Sort hovered inventory area",
"screen.sanctuary.inventory.sort_hint": "%s: sort hovered storage; over the hotbar, sort only the hotbar.",
"screen.sanctuary.inventory.sort_locked": "Unlock Inventory sorting in the Blocodex Aptitudes panel.",
"screen.sanctuary.inventory.sort_locked": "Unlock or enable Inventory sorting in Progression.",
"screen.sanctuary.inventory.sort_refused": "Sorting refused: check the menu and empty your cursor, then try again.",
"sanctuary.tutorial.movement": "Move and look around",
"sanctuary.tutorial.find_tree": "Find a tree",
@@ -589,7 +589,7 @@
"sanctuary.progression.resting": "Rest",
"sanctuary.progression.resting_description": "Rest in the sleeping pose to regain half a heart every 10 s, using half a food icon. Moving or taking damage interrupts rest.",
"sanctuary.progression.resting_controls": "Shift + %s to rest (remappable key)",
"sanctuary.progression.resting_locked": "Unlock Rest in the Blocodex aptitudes.",
"sanctuary.progression.resting_locked": "Unlock or enable Rest in Progression.",
"sanctuary.progression.next_cost_short": "%s lv.",
"sanctuary.progression.rank_max_short": "Max.",
"sanctuary.progression.skill_bar_hint": "The bar shows acquired tiers; the number is the next tier’s cost in XP levels.",
@@ -802,7 +802,7 @@
"sanctuary.companion.unavailable": "Familiar powers are unavailable.",
"sanctuary.companion.storage_invalid": "The bond data cannot be read. It has been preserved.",
"sanctuary.companion.refused": "This action is not allowed here.",
"sanctuary.companion.locked": "Unlock Active bond first.",
"sanctuary.companion.locked": "Unlock or enable Active bond in Progression.",
"sanctuary.companion.no_familiar": "Equip a familiar egg.",
"sanctuary.companion.attuning": "This familiar is still attuning.",
"sanctuary.companion.cooldown": "The power is still cooling down.",
@@ -2452,7 +2452,6 @@
"sanctuary.community.actions_for": "Actions · %s",
"sanctuary.community.reply_hint": "Write a reply…",
"sanctuary.pause.map": "World map",
"sanctuary.atlas.key": "Open world map",
"sanctuary.pause.map_hint": "Open the map and explore",
"sanctuary.community.more": "Load more",
"sanctuary.community.browse": "All publications",
@@ -2619,5 +2618,30 @@
"biome.sanctuary.diagonal_swamp229": "Upland Swamp",
"biome.sanctuary.diagonal_pale229": "Sheltered Pale Garden",
"biome.sanctuary.diagonal_sulfur229": "Sulfur Cavities",
"biome.sanctuary.diagonal_pale230": "Lower Pale Garden"
"biome.sanctuary.diagonal_pale230": "Lower Pale Garden",
"sanctuary.progression.aptitude_enabled": "Enabled",
"sanctuary.progression.aptitude_disabled": "Disabled",
"sanctuary.progression.aptitude_toggle_hint": "Changing this choice is free and keeps your purchase.",
"sanctuary.progression.aptitude_permanent": "Permanent knowledge",
"sanctuary.progression.aptitude_refused": "The choice was not applied. Try again.",
"sanctuary.progression.aptitude_version_required": "Permanent aptitudes require Sanctuary beta.237 or later on both client and server.",
"sanctuary.progression.theme_survival": "Survival",
"sanctuary.progression.theme_inventory_crafting": "Inventory and crafting",
"sanctuary.progression.theme_familiars": "Familiars",
"sanctuary.progression.theme_exploration": "Exploration",
"sanctuary.progression.theme_crafts": "Arts and crafts",
"sanctuary.progression.enchanting": "Enchanting",
"sanctuary.progression.enchanting_description": "Use an enchanting table to enchant items.",
"sanctuary.progression.enchanting_locked": "Unlock and enable Enchanting in Progression to use this table.",
"sanctuary.progression.alchemy": "Alchemy",
"sanctuary.progression.alchemy_description": "Use a brewing stand to brew potions.",
"sanctuary.progression.alchemy_locked": "Unlock and enable Alchemy in Progression to use this brewing stand.",
"sanctuary.progression.aptitude_disable_hint": "Click to disable.",
"sanctuary.progression.aptitude_enable_hint": "Click to enable.",
"sanctuary.progression.aptitude_acquired": "Acquired",
"sanctuary.progression.aptitude_acquired_hint": "This aptitude remains permanently available after purchase.",
"sanctuary.progression.smithing": "Smithing",
"sanctuary.progression.smithing_description": "Use anvils and the smithing table to repair, rename and upgrade equipment.",
"sanctuary.progression.smithing_locked": "Acquire Smithing in Progression to use an anvil or smithing table.",
"sanctuary.progression.smithing_version_required": "Smithing requires Sanctuary beta.238 or later on both client and server."
}
@@ -569,7 +569,7 @@
"sanctuary.progression.sorting_version_required": "Le rangement nécessite la même version de Sanctuary beta.014 ou ultérieure sur le client et le serveur.",
"screen.sanctuary.inventory.sort_key": "Ranger la zone d’inventaire visée",
"screen.sanctuary.inventory.sort_hint": "%s : ranger le stockage visé ; sur la hotbar, ranger seulement la hotbar.",
"screen.sanctuary.inventory.sort_locked": "Débloque Rangement dans les Aptitudes du Blocodex.",
"screen.sanctuary.inventory.sort_locked": "Débloquez ou activez le tri d’inventaire dans Progression.",
"screen.sanctuary.inventory.sort_refused": "Rangement refusé : vérifie le menu et libère le curseur, puis réessaie.",
"sanctuary.tutorial.movement": "Se déplacer et regarder autour de soi",
"sanctuary.tutorial.find_tree": "Trouver un arbre",
@@ -589,7 +589,7 @@
"sanctuary.progression.resting": "Se reposer",
"sanctuary.progression.resting_description": "Repose-toi dans la posture de sommeil pour récupérer un demi-cœur toutes les 10 s, contre une demi-barre de faim. Bouger ou subir des dégâts interrompt le repos.",
"sanctuary.progression.resting_controls": "Maj + %s pour se reposer (touche configurable)",
"sanctuary.progression.resting_locked": "Débloque Se reposer dans les aptitudes du Blocodex.",
"sanctuary.progression.resting_locked": "Débloquez ou activez Se reposer dans Progression.",
"sanctuary.progression.next_cost_short": "%s niv.",
"sanctuary.progression.rank_max_short": "Max.",
"sanctuary.progression.skill_bar_hint": "La barre indique les paliers acquis ; le nombre est le prix du prochain palier en niveaux d’XP.",
@@ -802,7 +802,7 @@
"sanctuary.companion.unavailable": "Les pouvoirs de familiers sont indisponibles.",
"sanctuary.companion.storage_invalid": "Les données du lien ne peuvent pas être lues. Elles ont été conservées.",
"sanctuary.companion.refused": "Ce geste n’est pas autorisé ici.",
"sanctuary.companion.locked": "Débloque d’abord l’aptitude Lien actif.",
"sanctuary.companion.locked": "Débloquez ou activez Lien actif dans Progression.",
"sanctuary.companion.no_familiar": "Équipe un œuf de familier.",
"sanctuary.companion.attuning": "Le lien avec ce familier se forme encore.",
"sanctuary.companion.cooldown": "Le pouvoir est encore en recharge.",
@@ -2452,7 +2452,6 @@
"sanctuary.community.actions_for": "Actions · %s",
"sanctuary.community.reply_hint": "Écrire une réponse…",
"sanctuary.pause.map": "Carte du monde",
"sanctuary.atlas.key": "Ouvrir la carte",
"sanctuary.pause.map_hint": "Ouvrir la carte et explorer",
"sanctuary.community.more": "Charger la suite",
"sanctuary.community.browse": "Toutes les publications",
@@ -2619,5 +2618,30 @@
"biome.sanctuary.diagonal_swamp229": "Marais perché",
"biome.sanctuary.diagonal_pale229": "Jardin pâle abrité",
"biome.sanctuary.diagonal_sulfur229": "Cavités de soufre",
"biome.sanctuary.diagonal_pale230": "Jardin pâle inférieur"
"biome.sanctuary.diagonal_pale230": "Jardin pâle inférieur",
"sanctuary.progression.aptitude_enabled": "Activée",
"sanctuary.progression.aptitude_disabled": "Désactivée",
"sanctuary.progression.aptitude_toggle_hint": "Changer ce choix est gratuit et conserve votre achat.",
"sanctuary.progression.aptitude_permanent": "Connaissance permanente",
"sanctuary.progression.aptitude_refused": "Le choix n’a pas été appliqué. Réessayez.",
"sanctuary.progression.aptitude_version_required": "Les aptitudes permanentes nécessitent Sanctuary beta.237 ou plus récent côté client et serveur.",
"sanctuary.progression.theme_survival": "Survie",
"sanctuary.progression.theme_inventory_crafting": "Inventaire et fabrication",
"sanctuary.progression.theme_familiars": "Familiers",
"sanctuary.progression.theme_exploration": "Exploration",
"sanctuary.progression.theme_crafts": "Arts et métiers",
"sanctuary.progression.enchanting": "Enchantement",
"sanctuary.progression.enchanting_description": "Utiliser une table d’enchantement pour enchanter des objets.",
"sanctuary.progression.enchanting_locked": "Débloquez et activez Enchantement dans Progression pour utiliser cette table.",
"sanctuary.progression.alchemy": "Alchimie",
"sanctuary.progression.alchemy_description": "Utiliser un alambic pour préparer des potions.",
"sanctuary.progression.alchemy_locked": "Débloquez et activez Alchimie dans Progression pour utiliser cet alambic.",
"sanctuary.progression.aptitude_disable_hint": "Cliquer pour désactiver.",
"sanctuary.progression.aptitude_enable_hint": "Cliquer pour activer.",
"sanctuary.progression.aptitude_acquired": "Acquise",
"sanctuary.progression.aptitude_acquired_hint": "Cette aptitude est acquise définitivement après achat.",
"sanctuary.progression.smithing": "Forgerie",
"sanctuary.progression.smithing_description": "Utiliser les enclumes et la table de forge pour réparer, renommer et améliorer les équipements.",
"sanctuary.progression.smithing_locked": "Acquérez Forgerie dans Progression pour utiliser une enclume ou une table de forge.",
"sanctuary.progression.smithing_version_required": "Forgerie nécessite Sanctuary beta.238 ou plus récent côté client et serveur."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

@@ -3,6 +3,10 @@
"package": "fr.koka.sanctuary.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"CraftAptitudeOpenMixin",
"CraftAptitudeMenuMixin",
"CraftAptitudeEnchantMixin",
"CraftAptitudeCombinerMixin",
"IslandChestLootMixin",
"BadlandsBands211Accessor",
"WestMonument217Accessor",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.236"
version = "beta.238"
pack-format = "packwiz:1.1.0"
[index]