Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49feb5a376 | ||
|
|
85e062ed91 |
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## beta.092 — Clic molette des super pistons
|
||||
|
||||
- Socles, têtes et tiges renvoient le piston normal ou collant d’origine.
|
||||
- Pièces mobiles couvertes ; Ctrl + clic molette exclut les données techniques.
|
||||
- Sélection en survie et création en créatif selon les règles natives.
|
||||
- [Détails et vérifications](docs/multiblock-pick-beta092.md).
|
||||
|
||||
## beta.091 — Interactions avec les animaux embarqués
|
||||
|
||||
- Ciblage au clic gauche/droit des mobs assis dans le même bateau.
|
||||
- Coups, coffres portés et activation des distributeurs depuis les sièges.
|
||||
- Protection conservée pour les joueurs et animaux portés sur la tête.
|
||||
- [Détails et vérifications](docs/boat-passengers-beta091.md).
|
||||
|
||||
## beta.090 — Minecraft Java 26.3 finale
|
||||
|
||||
- Migration des quatre modules vers Minecraft 26.3 et Fabric API 0.160.5+26.3.
|
||||
- Fork JEI 30.32.0-sanctuary.3, cible et dépendances centralisées.
|
||||
- Édition beta.090 du pack de textures, format 97.1, images conservées.
|
||||
- Packs normal et Test reconstruits ; aucun monde personnel migré.
|
||||
- [Contrat et vérifications](docs/minecraft-26-3-beta090.md).
|
||||
|
||||
## beta.061 — continuité musicale et montures colossales
|
||||
|
||||
- La musique Minecraft commence au dévoilement du portail, après les lettres. Son flux est conservé
|
||||
|
||||
@@ -20,8 +20,8 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
Les sources Git sont synchronisées jusqu’à **beta.061**. Le récapitulatif de
|
||||
[synchronisation](docs/git-sync-beta061.md) précise les vérifications et les
|
||||
Les sources Git sont synchronisées jusqu’à **beta.092**. Le récapitulatif de
|
||||
[synchronisation](docs/git-sync-beta092.md) précise les vérifications et les
|
||||
limites. Les archives bêta mentionnées dans ce document sont des fichiers
|
||||
locaux ; la dernière release du canal packwiz reste **alpha.30.7**.
|
||||
|
||||
@@ -31,6 +31,305 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.092 — Clic molette des super pistons
|
||||
|
||||
Viser un socle, une tête ou une tige sélectionne le piston normal ou collant
|
||||
d’origine. Les règles de sélection créatif/survie restent natives.
|
||||
[Détails et vérifications](docs/multiblock-pick-beta092.md).
|
||||
[Pack normal](build/Sanctuary-beta.092.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.092.mrpack).
|
||||
|
||||
## beta.091 — Animaux embarqués interactifs
|
||||
|
||||
Clic gauche et clic droit ciblent les animaux assis dans le même bateau : coups,
|
||||
coffres et blocs de tête utilisables. Les différents formats de bateaux et radeaux
|
||||
sont couverts. [Détails et vérifications](docs/boat-passengers-beta091.md).
|
||||
[Pack normal](build/Sanctuary-beta.091.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.091.mrpack).
|
||||
|
||||
## beta.090 — Minecraft 26.3 finale
|
||||
|
||||
Le projet cible Minecraft Java **26.3**, avec Fabric API 0.160.5+26.3,
|
||||
Loader 0.19.5 et Java 25. Sanctuary, Demeure, JEI intégré et le profil Test
|
||||
partagent la même cible. Le pack de textures reçoit une édition beta.090.
|
||||
[Contrat et vérifications](docs/minecraft-26-3-beta090.md).
|
||||
[Pack normal](build/Sanctuary-beta.090.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.090.mrpack).
|
||||
|
||||
## beta.089 — collision des super pistons
|
||||
|
||||
Correction du crash lorsqu'un joueur touche la tête ou la tige d'un super
|
||||
piston. Leurs propriétés de suffocation et de visibilité ne lisent plus
|
||||
l’état réservé au socle. Les machines existantes conservent leur état et leurs textures.
|
||||
[Détails et vérifications](docs/super-piston-collision-beta089.md).
|
||||
[Pack normal](build/Sanctuary-beta.089.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.089.mrpack).
|
||||
|
||||
## beta.088 — navigation du Fût
|
||||
|
||||
La souris reste sur les boutons lorsque l'on change de page ou applique une
|
||||
recherche dans le Fût. La même correction couvre les onglets du Fourneau ;
|
||||
les clics envoyés pour une ancienne page restent rejetés.
|
||||
[Détails et vérifications](docs/fut-navigation-beta088.md).
|
||||
[Pack normal](build/Sanctuary-beta.088.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.088.mrpack).
|
||||
|
||||
## beta.087 — super pistons
|
||||
|
||||
La clé dorée réunit neuf pistons identiques en une tête 3 × 3, normale ou
|
||||
gluante, avec une course de trois blocs et une charge commune de 108 blocs.
|
||||
Fonctionne dans les six orientations, avec les textures fournies et la plaque
|
||||
avant de quatre pixels qui sort du socle. [Utilisation et contrat](docs/super-pistons-beta087.md).
|
||||
[Pack normal](build/Sanctuary-beta.087.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.087.mrpack).
|
||||
|
||||
## beta.086 — façade du Fût
|
||||
|
||||
Le Fût reçoit ses textures de côtés, dessus et dessous, raccordées sur le
|
||||
cube complet. Le stockage commun reste inchangé ; les textures du Fourneau
|
||||
sont également incluses. [Détails et vérifications](docs/fut-textures-beta086.md).
|
||||
[Pack normal](build/Sanctuary-beta.086.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.086.mrpack).
|
||||
|
||||
## beta.085 — façade du Fourneau
|
||||
|
||||
Les textures originales forment un seul grand four sur les 27 composants,
|
||||
avec façade orientée, côtés et dessus raccordés. Les fours indépendants
|
||||
conservent leur apparence. [Détails et vérifications](docs/fourneau-textures-beta085.md).
|
||||
[Pack normal](build/Sanctuary-beta.085.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.085.mrpack).
|
||||
|
||||
## beta.084 — premiers multiblocs
|
||||
|
||||
La clé dorée assemble volontairement les cubes de 27 barils ou de 27 fours.
|
||||
Le Fût réunit 729 cases avec recherche et pages ; le Fourneau partage le
|
||||
combustible entre ses cuissons. Le démontage conserve les composants et leur
|
||||
stock actuel. [Gestes, contrat et vérifications](docs/multiblocs-beta084.md).
|
||||
[Pack normal](build/Sanctuary-beta.084.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.084.mrpack).
|
||||
|
||||
## beta.083 — menus organisés
|
||||
|
||||
La pause regroupe six rubriques. Découvertes rassemble les catalogues et
|
||||
statistiques ; Progression accueille Progrès, Prestige et Historique ;
|
||||
Factions possède son accès et son journal. L’économie reste différée.
|
||||
Hello World garde un fond gris uni, qui s’assombrit avant l’intro complète.
|
||||
[Organisation et vérifications](docs/menu-organization-beta083.md).
|
||||
[Pack normal](build/Sanctuary-beta.083.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.083.mrpack).
|
||||
|
||||
## beta.082 — commandes météo opérateur
|
||||
|
||||
`/weather overcast`, `/weather fog` et `/weather showers` permettent de
|
||||
choisir les nouvelles météos. Les six profils sont accessibles sous
|
||||
`/sanctuary weather`, pour la journée Real Time courante.
|
||||
[Commandes et contrat](docs/weather-commands-beta082.md).
|
||||
[Pack normal](build/Sanctuary-beta.082.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.082.mrpack).
|
||||
|
||||
## beta.081 — pose jusqu’à six blocs
|
||||
|
||||
La portée de pose atteint **5,5 blocs au rang 6**, puis **6 blocs au rang 7**.
|
||||
[Réglage et vérifications](docs/build-reach-beta081.md).
|
||||
[Pack normal](build/Sanctuary-beta.081.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.081.mrpack).
|
||||
|
||||
## beta.080 — portée et vitesse des compétences
|
||||
|
||||
Construction fait progresser la portée de pose de **3 à 5,5 blocs**.
|
||||
Minage fait progresser sa propre portée de **3 à 5,5 blocs** et sa vitesse
|
||||
de **75 % à 130 %** du vanilla, avec retour aux valeurs normales au rang 3.
|
||||
[Courbe et validations](docs/build-mining-beta080.md).
|
||||
[Pack normal](build/Sanctuary-beta.080.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.080.mrpack).
|
||||
|
||||
## beta.079 — ordres et défense des familiers
|
||||
|
||||
L’ouverture de H ne bloque plus l’ordre cliqué juste après. Suivre conserve
|
||||
la défense immédiate quand le joueur est attaqué, et les ordres affichent
|
||||
leur confirmation. [Causes reproduites et vérifications](docs/familiar-solo-beta079.md).
|
||||
[Pack normal](build/Sanctuary-beta.079.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.079.mrpack).
|
||||
|
||||
## beta.078 — Combats / Battles
|
||||
|
||||
Le menu de duels et d’arènes s’appelle désormais **Combats** en français et
|
||||
**Battles** en anglais. Les annonces renvoient au menu pause.
|
||||
[Renommage et vérifications](docs/combat-beta078.md).
|
||||
Le [cadrage des menus](docs/menu-architecture.md) propose leur future
|
||||
organisation ; l’économie reste différée.
|
||||
[Pack normal](build/Sanctuary-beta.078.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.078.mrpack).
|
||||
|
||||
## beta.077 — nuages du vide et menus
|
||||
|
||||
Une seconde couche à **Y = 0** dessine un grand tourbillon autour de Sanctuary,
|
||||
deux fois plus large que l’île. Le rendu natif conserve les nuages supérieurs
|
||||
et sa géométrie reste en cache. Cette livraison regroupe aussi les correctifs
|
||||
[des menus et du pouvoir utilitaire](docs/menu-navigation-beta076.md).
|
||||
[Contrat et vérifications](docs/cloud-vortex-beta077.md). Tests natifs, build et
|
||||
archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.077.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.077.mrpack).
|
||||
|
||||
## beta.076 — navigation des menus
|
||||
|
||||
**Duels et arènes** s’ouvre directement dans le menu pause. **Progression**
|
||||
propose Prestige, Factions et Historique, avec les achats du cycle à nouveau
|
||||
visibles. Le pouvoir utilitaire du familier affiche sa sélection et son accès.
|
||||
[Correctifs et vérifications](docs/menu-navigation-beta076.md).
|
||||
|
||||
## beta.075 — bibliothèque de plans
|
||||
|
||||
Le bouton de recherche Minecraft Schematics est retiré de **K → Bibliothèque**.
|
||||
[Correctif et vérifications](docs/plans-library-beta075.md). Build et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.075.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.075.mrpack).
|
||||
|
||||
## beta.074 — familiers autonomes
|
||||
|
||||
Les caractères prennent des décisions distinctes : chasse à vue, garde autour
|
||||
du joueur, riposte et attente des ordres. H bref désigne une cible ou reprend
|
||||
le suivi ; H maintenu ouvre les ordres. F conserve l’échange des mains.
|
||||
[Contrat et vérifications](docs/familiar-autonomy-beta074.md).
|
||||
Parcours natifs, montures, arènes, anciens duels, compilation et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.074.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.074.mrpack).
|
||||
|
||||
## beta.072 — statues et plans natifs
|
||||
|
||||
**K** ouvre l’outil natif : transformer un modèle Minecraft en statue de blocs,
|
||||
importer un `.litematic`, `.schem` v2/v3 ou `.nbt`, déplacer et tourner son plan,
|
||||
afficher une couche et consulter les blocs restants. En créatif, le serveur
|
||||
contrôle la zone puis place le plan ; en survie, on construit normalement.
|
||||
Le lien de recherche web présent dans cette version est retiré en beta.075.
|
||||
[Contrat, formats et vérifications](docs/statues-plans-beta072.md).
|
||||
Sans dépendance Litematica/MaLiLib. Les anciennes `.schematic` nécessitent une
|
||||
conversion externe. Parcours client FR/EN, sept tests serveur, build et
|
||||
interopérabilité avec Litemapy vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.072.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.072.mrpack).
|
||||
|
||||
## beta.073 — arènes et paris
|
||||
|
||||
**Menu pause → Duels et arènes** (depuis beta.076) : arènes sans plafond d'inscrits, duels entre
|
||||
joueurs ou familiers, mode mixte, règles visibles et mises en objets. Les
|
||||
spectateurs consultent les combats annoncés et parient avant leur lancement.
|
||||
Les joueurs meurent réellement ; les familiers gardent leur K.-O. habituel.
|
||||
[Parcours, règles et contrat de sauvegarde](docs/arenas-beta073.md).
|
||||
Client natif, régression des anciens duels, compilation et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.073.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.073.mrpack).
|
||||
|
||||
## beta.071 — personnalités et traits de monture
|
||||
|
||||
Chaque familier possède un caractère stable : agressif, protecteur, passif ou
|
||||
pacifiste. Les tendances dépendent de l’espèce, avec des exceptions ; le
|
||||
pacifiste obéit aux ordres d’attaque. La poursuite est plus vive, les araignées
|
||||
montées grimpent aux murs, les Nautilus colossaux conservent le Souffle du
|
||||
Nautilus et le Strider retrouve son appui sur la lave.
|
||||
[Contrat, audit des montures et tests](docs/familiar-personality-beta071.md).
|
||||
Client natif, compilation et archives isolées vérifiés ; textures beta.070.
|
||||
[Pack normal](build/Sanctuary-beta.071.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.071.mrpack).
|
||||
|
||||
## beta.070 — resource pack intégré
|
||||
|
||||
Les textures fournies sont versionnées et **actives par défaut**, dans les
|
||||
packs normal et Test. Les nouveaux dessins de faim et de saturation utilisent
|
||||
les ressources natives ; le pack reste désactivable dans les options.
|
||||
[Source versionnée](ressources-pack/sanctuary/README.md) ·
|
||||
[Contrat et vérifications](docs/resourcepack-beta070.md).
|
||||
Client natif, compilation et égalité des sources/archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.070.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.070.mrpack) ·
|
||||
[Resource pack autonome](build/Sanctuary-Resource-Pack-beta.070.zip).
|
||||
|
||||
## beta.069 — ordres du familier sur H
|
||||
|
||||
**H** ouvre les ordres, **F** échange les objets des deux mains et **G** reste
|
||||
le pouvoir du familier. Le réglage F hérité des deux dernières versions est
|
||||
reconverti une seule fois en H. [Contrat et tests](docs/familiar-orders-beta069.md).
|
||||
Parcours natif, compilation et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.069.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.069.mrpack).
|
||||
|
||||
## beta.068 — animation du préchargement
|
||||
|
||||
Une petite ligne SGA s’éclaire doucement pendant la préparation et le
|
||||
chargement du terrain. Elle s’anime sans ticks de jeu, conserve les vraies
|
||||
étapes et laisse la progression native des chunks lisible.
|
||||
[Résultat et vérifications](docs/loading-animation-beta068.md).
|
||||
Client natif, comparaison de huit captures, compilation et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.068.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.068.mrpack).
|
||||
|
||||
## beta.067 — touche du familier et dragon volant
|
||||
|
||||
F remplace H pour les ordres du familier, sans échanger les objets tenus.
|
||||
Le dragon regarde dans le sens de son déplacement. Les petits portés planent ;
|
||||
le grand porté sur la tête monte avec Espace et plane au relâchement.
|
||||
Le colossal reste une monture pilotable. [Contrat et tests](docs/familiar-controls-beta067.md).
|
||||
Parcours client, compilation et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.067.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.067.mrpack).
|
||||
|
||||
## beta.066 — cosmétiques animés sans doublon
|
||||
|
||||
Les coffres portés n'affichent plus de coffre fermé sous leur couvercle animé.
|
||||
La cloche conserve son support et une seule cloche mobile. Le rendu partagé
|
||||
avec les mobs bénéficie du même correctif ; les textures restent identiques.
|
||||
[Contrat et tests](docs/head-render-beta066.md). Parcours client natif,
|
||||
compilation et deux archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.066.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.066.mrpack).
|
||||
|
||||
## beta.065 — collisions des bateaux
|
||||
|
||||
Les collisions couvrent toute la coque des huit formats de bateaux et radeaux.
|
||||
Un virage ne peut plus introduire le bateau dans un mur avant le déplacement.
|
||||
Le recul reste possible au contact. Les 88 variantes, les formes de blocs
|
||||
minces et les commandes natives sur l’eau et en vol sont vérifiés.
|
||||
[Contrat et tests](docs/boat-collisions-beta065.md). Build et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.065.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.065.mrpack).
|
||||
|
||||
## beta.064 — cache et étapes réelles de préparation
|
||||
|
||||
Les plans de terrain sont conservés sur disque et vérifiés à la réouverture.
|
||||
Sur le scénario de l’audit, la réouverture passe d’environ **59 s à 2,1 s** ;
|
||||
la première création reste autour de **122 s**, introduction complète comprise.
|
||||
L’attente affiche le travail réellement en cours, en texte Minecraft sur fond
|
||||
uni, sans portail du Nether. Un cache manquant ou corrompu est recalculé.
|
||||
[Contrat et vérifications](docs/loading-cache-beta064.md). Build, cache corrompu,
|
||||
parcours natifs et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.064.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.064.mrpack).
|
||||
|
||||
## beta.063 — chargement et règle de jeu Sanctuary
|
||||
|
||||
La règle **Utiliser Sanctuary**, choisie à la création du monde, permet de jouer
|
||||
avec Hello World et la progression sur un terrain Minecraft classique ou plat.
|
||||
Ce choix reste fixé pour la partie. L'audit mesure 122 s à la création d'une
|
||||
île et 59 s à sa réouverture sur le Mac testé ; les plans recalculés dominent.
|
||||
[Audit, mesures et prochains tickets](docs/loading-gameplay-beta063.md).
|
||||
Cette livraison conservait toute l'introduction et proposait l'écran épuré.
|
||||
Le cache et les étapes réelles sont implémentés dans beta.064 ci-dessus.
|
||||
Build, parcours natifs et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.063.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.063.mrpack).
|
||||
|
||||
## beta.062 — soleil et chapeaux des familiers
|
||||
|
||||
Les familiers sensibles au soleil brûlent comme leur espèce Minecraft.
|
||||
Un objet équipé sur leur tête protège des nouvelles inflammations ; cela
|
||||
fonctionne aussi sur les mobs ordinaires. Les dégâts utilisent la santé et le
|
||||
K.-O. du familier, sans détruire l'œuf. Maj + clic droit avec un objet équipe
|
||||
le chapeau, à main vide le récupère. [Contrat et vérifications](docs/familiar-sunlight-beta062.md).
|
||||
Build, parcours natif et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.062.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.062.mrpack).
|
||||
|
||||
## beta.061 — musique d’arrivée et montures familières
|
||||
|
||||
La musique commence au dévoilement du portail et accompagne le flash blanc
|
||||
@@ -946,7 +1245,7 @@ Le relief de l’île initiale reste celui de l’alpha.30.7. Les anciennes
|
||||
sauvegardes gardent leur contrat et ne reçoivent pas ces îles automatiquement.
|
||||
Voir [le contrat et les vérifications](docs/expeditions-beta001.md).
|
||||
|
||||
Les sources bêta sont disponibles dans Git jusqu’à beta.061 ; la dernière
|
||||
Les sources bêta sont disponibles dans Git jusqu’à beta.092 ; la dernière
|
||||
release du canal packwiz reste l’alpha.30.7 ci-dessous. Les sections alpha
|
||||
constituent l’historique.
|
||||
|
||||
@@ -1585,19 +1884,19 @@ deux fois, avec 466 fichiers personnels et réglages suivis conservés.
|
||||
|
||||
## Versions et prérequis
|
||||
|
||||
Au 8 septembre 2026, la cible disponible est **26.3-pre-2**. Le dépôt ne prétend
|
||||
pas cibler une version finale 26.3 déjà sortie. Le passage à la version finale
|
||||
sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembre
|
||||
2026. Les contrôles et les limites de migration sont documentés dans
|
||||
[le ticket 26.3](docs/minecraft-26-3-beta090.md).
|
||||
|
||||
| Composant | Version fixée |
|
||||
| --- | --- |
|
||||
| Minecraft Java | 26.3-pre-2 |
|
||||
| Minecraft Java | 26.3 |
|
||||
| Java JDK | 25 |
|
||||
| Fabric Loader | 0.19.5 |
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric API | 0.160.5+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | beta.061 |
|
||||
| Sanctuary / pack | beta.092 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -1605,9 +1904,9 @@ uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
par le wrapper. `packwiz` est utile pour modifier les dépendances ou servir le
|
||||
pack ; il n'est pas nécessaire pour le construire.
|
||||
|
||||
Références vérifiées : [Minecraft 26.3-pre-2](https://www.minecraft.net/en-us/article/minecraft-26-3-pre-release-2),
|
||||
Références vérifiées : [Minecraft 26.3](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3),
|
||||
[Fabric](https://fabricmc.net/develop/),
|
||||
[version Fabric API](https://modrinth.com/mod/fabric-api/version/o9uChmGq).
|
||||
[version Fabric API](https://modrinth.com/mod/fabric-api/version/oCYG2H4L).
|
||||
|
||||
## Construire et lancer
|
||||
|
||||
@@ -1626,7 +1925,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.061.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.092.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
@@ -1676,7 +1975,7 @@ sauvegarde 26.2 : aucun outil de migration de ses chunks n'est livré ici.
|
||||
|
||||
It's Alive !, Only Fun et Master Key restent des modules autonomes prévus par la
|
||||
vision. Leurs sources 26.2 ne sont pas copiées dans ce socle. Fabric API est
|
||||
requise ; Demeure et le port expérimental de JEI pour 26.3-pre-2 sont imbriqués
|
||||
requise ; Demeure et le port source de JEI pour 26.3 sont imbriqués
|
||||
dans le JAR Sanctuary. Leurs sources ou leur procédure de reconstruction et
|
||||
leurs attributions figurent dans `mods/demeure/` et `mods/jei/`.
|
||||
|
||||
|
||||
+22
-1
@@ -15,10 +15,31 @@ tasks.register('verifyPack', Exec) {
|
||||
tasks.register('assemblePack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Assemble a local packwiz pack including the built Sanctuary mod.'
|
||||
dependsOn(':sanctuary:build', 'verifyPack')
|
||||
dependsOn(':sanctuary:build', 'verifyPack', 'assembleResourcePack')
|
||||
commandLine('python3', 'scripts/pack.py', 'assemble')
|
||||
}
|
||||
|
||||
tasks.register('verifyResourcePack', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Verify the versioned Sanctuary texture sources and their release hashes.'
|
||||
commandLine('python3', 'scripts/resource_pack.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyResourcePack') }
|
||||
|
||||
tasks.register('assembleResourcePack', Zip) {
|
||||
group = 'distribution'
|
||||
description = 'Export the same textures as the built-in resource pack for standalone use.'
|
||||
dependsOn('verifyResourcePack')
|
||||
from('ressources-pack/sanctuary') {
|
||||
include 'assets/**', 'pack.mcmeta', 'pack.png'
|
||||
exclude '**/.DS_Store'
|
||||
}
|
||||
destinationDirectory = layout.buildDirectory
|
||||
archiveFileName = "Sanctuary-Resource-Pack-${resource_pack_version}.zip"
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
tasks.register('assembleTestPack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Stage the separate flat-world Sanctuary Test profile.'
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# ARENA-01 — duels publics, arènes et paris — beta.073
|
||||
|
||||
Branche `codex/familiar-arenas-beta073`. Ticket du 15 septembre 2026.
|
||||
**Implémenté, client natif, compilation et archives locales vérifiés.**
|
||||
|
||||
## Parcours en jeu
|
||||
|
||||
**Pause → Progression → Duels et arènes** ouvre la liste publique. Le même
|
||||
accès figure dans le menu Duel historique. Aucune aptitude de familier ni
|
||||
prestige n'est requis pour organiser un combat entre joueurs.
|
||||
|
||||
1. Créer un **duel** (deux inscrits) ou une **arène** (sans plafond de
|
||||
participants), à l'emplacement de l'organisateur dans sa dimension.
|
||||
2. Choisir **familiers seuls**, **joueurs seuls**, ou **joueurs avec familiers**.
|
||||
Choisir un rayon de 24, 48 ou 96 blocs, une durée de 3, 5 ou 10 minutes,
|
||||
et l'autorisation de la nourriture, des potions et des perles. Les armes,
|
||||
boucliers et munitions restent utilisables. Les familiers gardent leurs
|
||||
statistiques, personnalités, ressources et recharges réelles.
|
||||
3. Choisir l'objet commun dans l'inventaire débloqué, et une mise d'entrée
|
||||
de 0 à 64 objets. Zéro signifie gratuit. Par défaut, l'objet est l'émeraude.
|
||||
Les composants natifs doivent correspondre : un objet renommé ou un
|
||||
contenant rempli n'est pas équivalent à sa version vierge. La création
|
||||
inscrit l'organisateur et prélève sa mise après validation serveur.
|
||||
4. Les autres joueurs consultent les règles, viennent dans le périmètre et
|
||||
s'inscrivent. Chacun valide **Prêt**. Tout changement de la liste invalide
|
||||
les validations. L'organisateur lance quand tous sont présents et prêts.
|
||||
5. **Dix secondes de préparation**, puis chacun pour soi. Les inscriptions
|
||||
et les paris sont fermés dès le lancement de la préparation.
|
||||
|
||||
La liste présente les coordonnées, la dimension, les inscrits, les pots et
|
||||
l'état du combat. Elle contient aussi les duels historiques à invitation,
|
||||
qui gardent leurs deux mises libres et leur double validation. Pour ces seuls
|
||||
duels historiques, le marché des spectateurs utilise des émeraudes ; les mises
|
||||
historiques des deux adversaires restent dans leur menu existant.
|
||||
|
||||
Les annonces d'ouverture, de lancement et de résultat vont dans le chat du
|
||||
serveur. Les combats terminés restent affichés cinq minutes. Les mises dues
|
||||
restent disponibles aussi longtemps que nécessaire. La consultation se fait
|
||||
par pages de douze entrées ; la pagination ne limite pas les inscriptions.
|
||||
Il n'y a ni téléportation de spectateur, ni création automatique de terrain :
|
||||
la carte de playtest se prépare normalement à côté.
|
||||
|
||||
## Combat et règles du monde
|
||||
|
||||
- **Joueurs : morts réelles**, selon le choix du créateur. Aucune santé
|
||||
artificielle à la fin, aucun inventaire restauré par l'arène. Inventaire,
|
||||
perte d'expérience, règles de conservation et tête-tombe suivent Sanctuary
|
||||
et les règles effectives du monde. La mort élimine après le chemin natif
|
||||
de création de la tombe.
|
||||
- **Familiers : K.-O. existant**, santé persistante et récupération habituelle.
|
||||
En mode mixte, le K.-O. du familier laisse son joueur combattre. La mort du
|
||||
joueur élimine son camp. En mode familiers seuls, les propriétaires ne sont
|
||||
pas des cibles et ne frappent pas directement les familiers adverses.
|
||||
- Le dernier camp encore en lice gagne. Abandon, déconnexion pendant le
|
||||
combat, changement d'individu, rappel du familier, sortie du périmètre ou
|
||||
passage en créatif/spectateur éliminent. Le rayon est une distance 3D au
|
||||
centre annoncé : l'altitude compte également.
|
||||
- Une interruption du serveur, un rechargement du catalogue, l'expiration des
|
||||
inscriptions ou une durée écoulée sans vainqueur rembourse les mises.
|
||||
Un joueur déjà éliminé qui se déconnecte ne termine pas le combat des autres.
|
||||
- Avant départ, se désinscrire rembourse son entrée et les paris placés sur
|
||||
soi. Les autres inscriptions restent ouvertes. Le départ de l'organisateur
|
||||
annule l'événement entier. Les duels historiques gardent leur contrat
|
||||
d'interruption/refund antérieur.
|
||||
- La permission PvP du combat ne vise que les adversaires inscrits en lice,
|
||||
même si le PvP général est désactivé ou s'ils sont de la même faction.
|
||||
Les dégâts directs ne franchissent pas cette frontière ; les familiers
|
||||
hors combat n'apportent pas leur assistance aux combattants.
|
||||
- Pose, utilisation et casse de blocs par les combattants sont désactivées,
|
||||
y compris les gestes de minage/construction groupés déjà commencés.
|
||||
Cela ne transforme pas le périmètre en claim : les mécanismes du monde,
|
||||
cosmétiques, pièges ou interventions d'opérateurs restent ceux de la map.
|
||||
|
||||
## Paris en objets
|
||||
|
||||
La mise d'inscription est identique pour tous ; son pot revient au vainqueur.
|
||||
Les spectateurs choisissent un inscrit et déposent une quantité libre de
|
||||
l'objet commun, jusqu'à 3 456 unités par geste et dans la limite de leur
|
||||
inventaire débloqué. Plusieurs gestes sont possibles avant le lancement.
|
||||
Il n'y a pas de commission.
|
||||
|
||||
Le pot des spectateurs est partagé **au prorata des mises gagnantes**. Les
|
||||
unités indivisibles restantes sont réparties dans l'ordre stable des UUID.
|
||||
Exemple : 4 objets sur A, 2 autres sur A et 3 sur B ; si A gagne, les deux
|
||||
parieurs gagnants récupèrent respectivement 6 et 3 objets. Si personne n'avait
|
||||
misé sur le vainqueur, les paris sont remboursés.
|
||||
|
||||
Un inscrit ne peut pas parier sur son combat. Un parieur ne peut plus s'y
|
||||
inscrire. Les boutons utilisent des identifiants et des révisions contrôlés
|
||||
par le serveur ; aucune pile fournie par le client ne sert de preuve de fonds.
|
||||
Les fonds sont pris uniquement dans les rangées réellement débloquées.
|
||||
|
||||
Les gains se récupèrent dans **Duels et arènes**. Un inventaire plein conserve
|
||||
le solde dans le registre ; libérer même une partie d'une pile permet de le
|
||||
récupérer progressivement. Aucun paiement ne tombe au sol et un joueur mort
|
||||
ne peut pas encaisser dans l'inventaire en cours de remplacement.
|
||||
|
||||
## Contrat de sauvegarde additif
|
||||
|
||||
Aucun registre historique, identifiant d'objet, génération ou monde personnel
|
||||
n'est converti. Nouveau fichier séparé :
|
||||
`sanctuary/arena-stakes-v1.json`, schéma 1. Nouveau reçu joueur persistant et
|
||||
copié à la mort : `sanctuary:arena_receipt`.
|
||||
|
||||
Chaque dépôt est écrit avant le débit, puis l'inventaire natif et son reçu sont
|
||||
sauvegardés ensemble dans `playerdata` et relus. Le journal confirme ensuite
|
||||
le débit. Chaque résultat répartit la totalité des fonds en une écriture
|
||||
atomique avant le paiement. Chaque paiement sauvegarde de même l'inventaire
|
||||
et son reçu avant de retirer la dette du journal. Une collecte partielle garde
|
||||
le reste et ne repaie pas une tranche déjà reçue.
|
||||
|
||||
Au redémarrage, les combats vivants ne reprennent pas : les mises réellement
|
||||
débitées sont remboursées. Un résultat déjà réparti conserve ses destinataires.
|
||||
Les dépôts inachevés sont résolus à la reconnexion du payeur d'après son reçu,
|
||||
sans inventer de crédit. Une erreur disque, un schéma inconnu ou un journal
|
||||
invalide préserve les fichiers et suspend les transactions.
|
||||
|
||||
Le journal a une borne de lecture/écriture de 32 Mio pour détecter les états
|
||||
anormaux. C'est une limite de stockage des opérations en attente, pas un nombre
|
||||
maximum de participants. Avant un retour à une ancienne version, terminer
|
||||
les événements et récupérer les soldes ; sinon restaurer une sauvegarde
|
||||
complète cohérente, jamais un seul registre ou un seul fichier joueur.
|
||||
|
||||
## Vérifications et limites
|
||||
|
||||
Minecraft **26.3-pre-2**, Java 25, Fabric API **0.160.0+26.3**.
|
||||
`Arena073ClientChecks` utilise un client intégré et des joueurs serveur
|
||||
synthétiques avec connexions natives, dans un monde plat de développement.
|
||||
Aucun serveur personnel ni EULA de serveur dédié n'est modifié.
|
||||
|
||||
La validation couvre trois familiers en combat autonome, des joueurs avec
|
||||
morts réelles, le mode mixte, les permissions PvP, les paris, le partage exact,
|
||||
les remboursements, l'inventaire plein et les composants de contenants. Une
|
||||
arène de 25 inscrits vérifie la pagination sans plafond de participants.
|
||||
Les menus FR/EN et l'aller-retour réseau du bouton de création sont contrôlés.
|
||||
Résultat client : **réussi**, `ARENA073_PASS`, 1 min 52 s. Les tests
|
||||
`Duel054Checks` sont rejoués dans la même session : validation mutuelle, anciennes
|
||||
mises, projectiles natifs, K.-O., refus d’un projectile tardif et remboursements
|
||||
passent aussi. La mort pendant les inscriptions libère l’inscription sans
|
||||
annuler les autres. Le journal est éprouvé avant débit, après sauvegarde du
|
||||
débit, après sauvegarde du paiement et avec un schéma inconnu. Les essais
|
||||
conservent les quantités exactes et refusent le schéma inconnu sans réécriture.
|
||||
|
||||
Journal client : `build/arena073-workspace/build/arena073-client.log`.
|
||||
Captures copiées dans `build/arena073-evidence/`.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 56 s**, 124 tâches (108 exécutées). Les GameTests serveur
|
||||
dédiés sont exclus conformément au refus d'accepter leur EULA ; les scénarios
|
||||
multijoueurs de ce ticket tournent dans le serveur intégré du client natif.
|
||||
|
||||
Les **1 689 classes** compilées, les ressources et les sources Java correspondent
|
||||
aux archives. Par rapport à beta.072, seules les classes de ce ticket et les
|
||||
métadonnées de version évoluent ; les ressources existantes, les 88 profils et
|
||||
le resource pack beta.070 restent identiques. Les **79 nouvelles clés FR/EN**
|
||||
sont présentes dans les deux langues. Les archives beta.070 à beta.072 restent
|
||||
inchangées.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.073.mrpack) : 9 592 283 octets.
|
||||
- [Pack Test, monde plat rapide](../build/Sanctuary-Test-beta.073.mrpack) :
|
||||
9 611 218 octets.
|
||||
- [Reçu des artefacts et SHA-256](../build/arena073-artifact.json).
|
||||
|
||||
La livraison a été construite dans `build/arena073-workspace`, copie isolée
|
||||
incluant la version beta.072 vérifiée, puis les seuls changements de ce ticket
|
||||
ont été réintégrés dans les sources partagées. Aucun canal distant, serveur
|
||||
personnel ou instance Prism n'est mis à jour.
|
||||
|
||||
Ce ticket fournit un outil de playtest. Il ne constitue pas une validation
|
||||
exhaustive de l'équilibrage des 88 espèces, un benchmark de très grand serveur,
|
||||
ni une protection compétitive contre les complicités et abandons arrangés.
|
||||
Les tests de reprise ne simulent pas une panne physique du disque.
|
||||
+160
@@ -1,5 +1,159 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## SKY-077 — tourbillon au-dessus du vide
|
||||
|
||||
Branche `codex/cloud-vortex-beta077`. [Ticket](cloud-vortex-beta077.md).
|
||||
Couche native à Y = 0, diamètre double de l’île, rotation et géométrie en cache.
|
||||
Livraison groupée avec les correctifs de menus beta.076. Tests natifs, cache,
|
||||
transparence Fabulous, build et archives normal/Test vérifiés.
|
||||
|
||||
## UI-076 — navigation et pouvoirs sélectionnés
|
||||
|
||||
Branche `codex/menu-navigation-beta076`. [Ticket](menu-navigation-beta076.md).
|
||||
Accès direct aux combats depuis Pause, parcours prestige/faction/historique,
|
||||
achats visibles et sélection du pouvoir utilitaire explicite. Parcours natif vérifié ;
|
||||
distribution regroupée avec beta.077.
|
||||
|
||||
## PLAN-FIX-01 — retirer la recherche web (beta.075)
|
||||
|
||||
Branche `codex/plans-library-beta075`. [Correctif](plans-library-beta075.md).
|
||||
Retirer le bouton Minecraft Schematics de K → Bibliothèque et ses libellés.
|
||||
Build, sources et archives normal/Test vérifiés ; anciens packs conservés.
|
||||
|
||||
## FAMILIAR-05 — autonomie et ordres rapides (beta.074)
|
||||
|
||||
Branche `codex/familiar-autonomy-beta074`. [Contrat](familiar-autonomy-beta074.md).
|
||||
Quatre règles de comportement distinctes, garde locale, sélection des menaces,
|
||||
retour après poursuite, gestion des cibles inaccessibles et H bref/maintenu.
|
||||
Parcours natif, régressions montures/arènes/duels et `check build` réussis.
|
||||
Archives normal/Test vérifiées ; textures beta.070 conservées.
|
||||
|
||||
## PLAN-01 — statues et plans natifs (beta.072)
|
||||
|
||||
Branche `codex/statues-plans-beta072`.
|
||||
[Contrat, formats et vérifications](statues-plans-beta072.md). Modèles et textures
|
||||
du jeu convertis en statues creuses ; bibliothèque compatible `.litematic`,
|
||||
`.schem` v2/v3 et `.nbt`, aperçu par couche, rotation, matériaux et collage
|
||||
créatif contrôlé côté serveur. En survie, construction manuelle native.
|
||||
Aucune dépendance Litematica/MaLiLib ajoutée.
|
||||
Capture native des 88 espèces, parcours creeper créatif/survie FR/EN,
|
||||
interopérabilité Litemapy, sept tests serveur et 130 tâches Gradle réussis.
|
||||
Archives normal/Test vérifiées ; anciennes `.schematic` à convertir en externe.
|
||||
|
||||
## MB-01 — Reprendre le catalogue des multiblocs
|
||||
|
||||
**Ticket local ouvert le 15 septembre 2026 ; premier lot implémenté en beta.084.**
|
||||
[Catalogue et décisions historiques](multiblocs-conception.md) ·
|
||||
[Ticket de cadrage et lots d'implémentation proposés](multiblocs-ticket.md).
|
||||
Branche documentaire réservée `codex/multiblocs-ticket`.
|
||||
|
||||
Premier lot choisi le 16 septembre 2026 : **clé dorée, Fourneau et Fût**,
|
||||
implémenté en [beta.084](multiblocs-beta084.md) sur `codex/multiblocs-beta084`. Fermentation, présentoirs,
|
||||
méga-pistons, Trémie, Carillon et Métablit restent au catalogue.
|
||||
La clé dorée assemble volontairement et dissocie ; les composants restent
|
||||
indépendants à la pose. Le Fût à 729 cases avec recherche et pages est accepté.
|
||||
Les gestes et la chauffe du premier lot sont décrits dans sa livraison. La fusion de tout
|
||||
It's Alive dans Sanctuary est différée ; le Fourneau pourrait alors remplacer
|
||||
la cuisinière, selon le [cadrage corrigé](multiblocs-itsalive-contrat.md).
|
||||
Aucun nouveau multibloc ni binaire livré ici.
|
||||
Les huit candidats de la [recherche complémentaire](multiblocs-recherche.md)
|
||||
sont rejetés par le créateur, jugés sans intérêt ou contraires au contrat
|
||||
Minecraft Vanilla. Reprendre le catalogue antérieur pour la liste d'implémentation.
|
||||
|
||||
## FAMILIAR-04 — personnalité et montures natives (beta.071)
|
||||
|
||||
Branche de départ `codex/familiar-personality-beta071`.
|
||||
[Contrat et vérifications](familiar-personality-beta071.md). Quatre caractères
|
||||
stables avec tendances d’espèce ; priorité des ordres, poursuite plus vive et
|
||||
verrouillage de combat conservé. Escalade des deux araignées, Souffle du
|
||||
Nautilus pour les deux variantes colossales et surface de lave du Strider.
|
||||
Audit des 14 espèces montables natives, parcours client et 124 tâches Gradle
|
||||
réussis ; archives isolées de la beta.072 en cours, resource pack beta.070.
|
||||
|
||||
## RP-02 — textures versionnées et actives par défaut (beta.070)
|
||||
|
||||
Branche `codex/resourcepack-beta070`.
|
||||
[Contrat et vérifications](resourcepack-beta070.md). Import des dessins fournis,
|
||||
source et manifeste d’empreintes enregistrés dans Git ; pack natif activé par
|
||||
défaut et désactivable. Sept textures de faim/saturation mises à jour, respiration
|
||||
vanilla conservée. Client natif, build, deux MRpacks et ZIP autonome vérifiés.
|
||||
|
||||
## FAMILIAR-FIX-02 — retour des ordres sur H (beta.069)
|
||||
|
||||
Branche `codex/familiar-orders-h-beta069`.
|
||||
[Contrat et tests](familiar-orders-beta069.md). H pour les ordres, F pour
|
||||
l’échange des mains, G pour la technique. Conversion unique de l’ancien
|
||||
raccourci F en H ; suppression du filtrage de l’échange natif. Parcours
|
||||
client et compilation vérifiés, deux archives locales conformes.
|
||||
|
||||
## LOAD-03 — animation discrète de l’attente (beta.068)
|
||||
|
||||
Branche `codex/loading-animation-beta068`.
|
||||
[Résultat et vérifications](loading-animation-beta068.md) : neuf glyphes SGA
|
||||
avec une lumière mobile, animés par l’horloge de rendu ; étapes réelles et
|
||||
carte des chunks conservées. Huit captures natives comparées, FR/EN inspectés,
|
||||
compilation et archives normal/Test vérifiées. Introduction et cache inchangés.
|
||||
|
||||
## FAMILIAR-FIX-01 — touche et vol du dragon (beta.067)
|
||||
|
||||
Branche `codex/familiar-controls-beta067`.
|
||||
[Contrat et vérifications](familiar-controls-beta067.md) : ordres sur F,
|
||||
remappage unique de l'ancien H et prévention du double déclenchement de
|
||||
l'échange des mains ; orientation du dragon corrigée. Plané des petits
|
||||
vérifié, montée du grand porté avec Espace et monture colossale vérifiées.
|
||||
Compilation et deux archives validées localement.
|
||||
|
||||
## HEAD-FIX-01 — modèles animés superposés (beta.066)
|
||||
|
||||
Branche `codex/cosmetic-render-beta066`.
|
||||
[Résultat et tests](head-render-beta066.md). Doublon de coffre reproduit dans
|
||||
le collecteur natif puis corrigé ; une seule cloche animée avec son support.
|
||||
Ouvertures/fermetures, aliments, autres modèles et douze cas sur des mobs
|
||||
adultes ou bébés vérifiés. Build et archives validés localement.
|
||||
|
||||
## BOAT-FIX-01 — collisions des coques (beta.065)
|
||||
|
||||
Branche `codex/boat-collisions-beta065`.
|
||||
[Résultat et tests](boat-collisions-beta065.md) : coque entière couverte,
|
||||
virages contrôlés avant la translation, recul conservé. Passage au travers
|
||||
des murs reproduit puis corrigé ; 88 variantes et seize parcours pilotés
|
||||
vérifiés en client natif et serveur intégré. Build et deux archives validés
|
||||
localement ; pas de déploiement personnel.
|
||||
|
||||
## LOAD-02 — cache et étapes réelles de préchargement (beta.064)
|
||||
|
||||
Branche `codex/loading-cache-beta064`.
|
||||
[Contrat, mesures et vérifications](loading-cache-beta064.md). Plans dérivés
|
||||
mis en cache sur disque, validation des données et recalcul automatique si
|
||||
une entrée manque ou est corrompue. Réouverture mesurée à 2,1 s contre 58,7 s
|
||||
lors de l’audit beta.063 ; première création à 122 s, introduction complète.
|
||||
Étapes réelles du serveur, écran natif épuré sans portail du Nether. Pas de
|
||||
migration des chunks ou des journaux d’expansion. Build, récupération d’un cache
|
||||
corrompu, huit ouvertures classiques/plates et archives vérifiés localement.
|
||||
|
||||
## RP-HUD-01 — base de textures modifiable
|
||||
|
||||
**Préparée le 15 septembre 2026**, branche `codex/resourcepack-hud-base`.
|
||||
[Résultat et vérifications](resourcepack-hud-base.md) : 53 sprites vanilla
|
||||
de vie et de faim, manifeste 26.3-pre-2 et guide de retouche FR/EN dans le
|
||||
resource pack Sanctuary existant. Les dessins seront modifiés par le créateur.
|
||||
Ce lot graphique séparé ne livre pas de nouveau binaire du mod.
|
||||
|
||||
## CAPE-01 — Des capes rares aux pouvoirs durables
|
||||
|
||||
**Ticket préparé le 15 septembre 2026 ; à arbitrer puis à implémenter.**
|
||||
[Intention, premier lot proposé et critères d'acceptation](capes-gameplay.md).
|
||||
Branche documentaire réservée `codex/capes-ticket` ; branche d'implémentation
|
||||
prévue `codex/capes-gameplay`.
|
||||
|
||||
Les capes apportent généralement un bonus passif permanent, proposé tant
|
||||
qu'elles sont portées. Des capes très puissantes restent possibles, avec rareté,
|
||||
contreparties ou périodes actives à éprouver selon leur impact. Le ticket
|
||||
prépare trois capes aux usages distincts, leur acquisition rare en survie et
|
||||
les essais de cumuls, de circulation et de progression. Catalogue, valeurs,
|
||||
malus et règles temporelles restent des propositions. Cape Zéro est actuellement
|
||||
visuelle ; aucun nouveau pouvoir ni changement de binaire livré par ce ticket.
|
||||
|
||||
## ANO-01 — Les doubles de Steve
|
||||
|
||||
**Conception du 15 septembre 2026 ; non implémentée.**
|
||||
@@ -1566,3 +1720,9 @@ secrets et traversées. Comparer les champs natifs 27, 30.1, 30.4 et 30.5 par
|
||||
coupes et cartes scientifiques, archivées avec leurs sources et leurs graines.
|
||||
Les expéditions anciennes et les îles supérieures ciblées restent à définir.
|
||||
Voir [la génération](generation-alpha30.5.md) et [l’atlas](terrain-atlas.md).
|
||||
|
||||
## ARENA-01 — beta.073 — duels publics et arènes
|
||||
|
||||
Livré et vérifié : trois modes de combat, inscriptions sans plafond pour les arènes,
|
||||
paris en objets, annonces et liste publique. Joueurs avec morts réelles.
|
||||
[Contrat et essais](arenas-beta073.md).
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# beta.065 — collisions des bateaux collectifs
|
||||
|
||||
Branche `codex/boat-collisions-beta065`.
|
||||
|
||||
## Problème et portée
|
||||
|
||||
Les huit formes de bateaux et radeaux collectifs utilisent le déplacement natif
|
||||
côté serveur. Leurs anciennes limites étaient fondées sur une tuile carrée de
|
||||
1,375 bloc, alors que les modèles ont une longueur de 28 pixels par module.
|
||||
La coque visible dépassait donc sa collision dans le sens longitudinal. Un
|
||||
virage agrandissait aussi directement les limites sans vérifier le terrain ;
|
||||
après cette intrusion, le déplacement natif pouvait traverser le mur.
|
||||
|
||||
Les limites couvrent désormais les coques complètes, et le volume balayé
|
||||
par chaque virage est contrôlé avant le déplacement. Le mouvement natif reste utilisé pour
|
||||
les translations, l’appui au sol et sur l’eau, les formes de blocs et les
|
||||
collisions avec les autres véhicules. Les sièges, les commandes partagées,
|
||||
les moteurs, les vitesses et les règles de chute des piles restent inchangés.
|
||||
|
||||
Les modèles et les textures ne changent pas. Les bateaux simples Minecraft
|
||||
conservent leur comportement. Aucune modification de chunk, de recette, de
|
||||
journal ou de format de sauvegarde ; les identifiants existants restent stables.
|
||||
Les positions enregistrées des anciens bateaux ne sont pas déplacées par la
|
||||
mise à jour : seule leur collision utilise les dimensions corrigées.
|
||||
|
||||
## Collision de la coque
|
||||
|
||||
Les huit formes sont couvertes : 1 × 2, 2 × 1, 1 × 3, 3 × 1, 2 × 2,
|
||||
2 × 3, 3 × 2 et 3 × 3, sur les onze bois. Largeur et longueur de la coque :
|
||||
|
||||
- Bois : `colonnes + 0,25` × `rangées × 1,75 + 0,25` blocs, rebords compris.
|
||||
- Bambou : `colonnes × 1,25` × `rangées × 1,75` blocs.
|
||||
|
||||
Comme dans le moteur natif, la collision est une boîte alignée sur les axes ;
|
||||
elle englobe la coque orientée. Les rames animées restent décoratives. Un virage
|
||||
vérifie aussi les extrema entre ses deux orientations, pour qu’un coin ne
|
||||
traverse pas un mur avant de revenir dans une position libre. Au contact,
|
||||
l’inertie de rotation s’arrête ; le recul et le glissement le long du mur restent
|
||||
résolus par Minecraft. Les rotations de placement ou de téléportation ne sont
|
||||
pas remplacées par des tentatives de conduite.
|
||||
|
||||
## Vérification
|
||||
|
||||
Client Minecraft 26.3-pre-2 et serveur intégré de développement, monde plat
|
||||
neuf de graine `65`. Le test avant correction traverse effectivement le mur
|
||||
situé en X=8 : après quinze mouvements avec virage, le centre atteint X=12,55.
|
||||
Ce même cas passe après correction.
|
||||
|
||||
- **88 variantes** : **3 168 déplacements** contre des murs dans les deux sens
|
||||
de X et Z, huit orientations, diagonales et déplacements de vingt blocs en
|
||||
un appel ; collision native détectée et recul libre de 1,5 bloc.
|
||||
- **440 virages** : contact et tentative de déplacement dans le mur, virages
|
||||
libres, puis cas où les orientations de départ et d’arrivée sont libres
|
||||
mais où le milieu du virage heurterait le mur. Ce dernier cas est refusé.
|
||||
- **128 passages** contre les formes natives minces des barrières et vitres,
|
||||
sur bateaux et radeaux, sans traversée ni recouvrement des blocs.
|
||||
- **704 modèles orientés** : les sommets réels des fonds et rebords sont inclus
|
||||
dans les collisions, côté client. Les rames sont explicitement exclues.
|
||||
- **16 parcours pilotés avec les vraies touches**, huit formes en chêne et en
|
||||
bambou, répartis entre eau et vol avec Allay et les quatre directions :
|
||||
avancer contre le mur, maintenir le virage, reculer. Les limites serveur et
|
||||
client restent à l’intérieur des murs, le pilote reste assis et le recul
|
||||
déplace effectivement le bateau. Deux captures de contact ont été relues.
|
||||
|
||||
Journaux locaux : `build/boat065-before.log`,
|
||||
`build/boat065-control-before.log`, `build/boat065-fixed-probe.log`,
|
||||
`build/boat065-client.log`. Captures : `build/boat065-evidence/`.
|
||||
Le parcours complet passe en **1 min 59 s**.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryBoat065ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Les essais utilisent le serveur intégré ; aucun serveur dédié ni EULA acceptée.
|
||||
Le test client couvre la synchronisation locale ; une session avec plusieurs
|
||||
clients distants et latence n’a pas été relancée pour ce correctif.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack` réussi en **2 min 8 s**,
|
||||
**122 tâches**, avec `-x :sanctuary:runGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryQuickTests=true`. Les essais serveur intégrés ci-dessus remplacent
|
||||
les tests dédiés exclus conformément au refus antérieur d’accepter leur EULA.
|
||||
|
||||
Les archives normal/Test sont vérifiées : **1 645 classes** conformes à la
|
||||
compilation, aucune classe de test embarquée, mêmes ressources et dépendances
|
||||
qu’en beta.064. Seule la source principale `CrewBoat` change, en plus des
|
||||
métadonnées de version. Modèles, textures, recettes, sièges, moteurs et autres
|
||||
systèmes restent conservés. Les anciennes archives beta.064 sont intactes.
|
||||
|
||||
|
||||
- [Pack normal beta.065](../build/Sanctuary-beta.065.mrpack), 5721084 octets.
|
||||
SHA-256 : `71ff1736817e2953f88a8afda5858b2e7ae23c360efdb7e25f149cbff049d764`.
|
||||
- [Monde plat rapide beta.065](../build/Sanctuary-Test-beta.065.mrpack), 5740018 octets.
|
||||
SHA-256 : `4c4931146a05d6f57e9abf2303b039ab7a7bc16471a0b864873b86bef297431d`.
|
||||
|
||||
Reçu `build/boat065-artifact.json`, build `build/boat065-check.log`.
|
||||
Aucun canal publié, instance Prism ou monde personnel modifié.
|
||||
@@ -0,0 +1,74 @@
|
||||
# beta.091 — Interagir avec les animaux embarqués
|
||||
|
||||
Branche `codex/boat-passenger-interactions-beta091`, Minecraft 26.3.
|
||||
|
||||
## Contrat
|
||||
|
||||
Depuis un bateau, viser un mob directement installé sur un siège permet les
|
||||
interactions habituelles : clic gauche pour le frapper, clic droit pour utiliser
|
||||
son objet de tête ou son interaction native. Cela couvre les bateaux/radeaux
|
||||
Minecraft et tous les formats Sanctuary. Les coups sur les animaux ordinaires
|
||||
infligent leurs dégâts habituels ; les familiers conservent leur réaction sans
|
||||
blessure hors des combats prévus, avec déclenchement de leur équipement.
|
||||
|
||||
Le ciblage natif excluait les entités partageant le véhicule racine du joueur.
|
||||
L'exception est limitée au curseur du joueur et aux mobs assis directement dans
|
||||
son bateau. Portée, obstacles et choix de la cible la plus proche restent ceux
|
||||
de Minecraft. Les collisions des projectiles ne sont pas modifiées.
|
||||
|
||||
Le bateau limite aussi normalement le regard à ±105 degrés. Avec un mob à bord,
|
||||
les joueurs peuvent désormais regarder autour d’eux à 360 degrés, notamment
|
||||
vers le siège arrière. Les commandes de déplacement et la rotation du bateau
|
||||
restent indépendantes du regard ; les mobs gardent leur orientation native.
|
||||
|
||||
La protection de portage des familiers distingue désormais un siège de bateau
|
||||
d'un familier porté sur la tête ou monté. Les protections entre joueurs portés
|
||||
restent en place. Les dégâts et interactions continuent à être validés côté
|
||||
serveur ; aucun nouveau paquet, inventaire, identifiant ou format de sauvegarde.
|
||||
Aucune modification des mondes existants ni de la génération.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `BoatPassengers091ClientChecks` crée un monde plat de développement,
|
||||
graine 42, et utilise le curseur natif et les vrais clics souris. Il teste un
|
||||
coffre porté puis un distributeur chargé sur une poule embarquée.
|
||||
|
||||
Le parcours natif passe en **1 min 13 s** sur Minecraft 26.3/macOS :
|
||||
|
||||
- 13 cas : bateau et radeau vanilla, les huit formats Sanctuary, radeau 3 × 3,
|
||||
poule bébé et familier installé comme passager pour vérifier sa protection.
|
||||
- Vrai ciblage du corps depuis le siège, y compris vers l’arrière ; aucun
|
||||
remplacement artificiel du résultat de visée dans le test.
|
||||
- Clic droit : ouverture du coffre natif contenant les sept diamants attendus.
|
||||
- Clic gauche : perte de vie de la poule, une flèche consommée et un projectile
|
||||
réel créé par le distributeur, sans éjection de l’animal.
|
||||
- Familier : même tir au toucher, vie inchangée. Une fois porté sur la tête,
|
||||
protection symétrique conservée et aucun nouveau déclenchement au toucher.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryBoatPassengers091ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journal : `build/boat091-client.log`, marqueur `BOAT091_PASS`.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 20 s**, 124 tâches. Journal : `build/boat091-check.log`.
|
||||
Les deux exports sont vérifiés contre les JAR et leurs sources. Les textures,
|
||||
modèles, données et JEI restent identiques à beta.090 ; ses archives sont intactes.
|
||||
Le reçu local est `build/boat091-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.091.mrpack), SHA-256
|
||||
`0d0e847e5b8f22637b6e280d0694ab5a2cdb8cb01a47442d92c5103e3e3f53ac`.
|
||||
- [Pack Test](../build/Sanctuary-Test-beta.091.mrpack), SHA-256
|
||||
`392773b8e5fc8948cdb5ea9cd7e8cc8cd97ed43bdc5fe413fef32b05758f8edc`.
|
||||
|
||||
## Limites
|
||||
|
||||
Les essais utilisent un client natif et son serveur intégré de développement ;
|
||||
pas de session LAN à deux clients pour cette livraison. Les tests dédiés restent
|
||||
exclus et aucune EULA n’est acceptée. Aucun déploiement dans une instance
|
||||
personnelle ni publication du canal packwiz.
|
||||
@@ -0,0 +1,85 @@
|
||||
# beta.080 — portée de Construction et progression de Minage
|
||||
|
||||
Branche `codex/build-mining-balance-beta080`, Minecraft 26.3-pre-2, textures beta.070.
|
||||
|
||||
Construction augmente la distance de pose. Minage augmente indépendamment
|
||||
la distance et la vitesse de casse. Le départ est inférieur au vanilla ;
|
||||
les derniers paliers le dépassent modérément. Les coûts, sept achats par
|
||||
compétence et tailles des groupes restent ceux du système existant.
|
||||
|
||||
| Rang | Vitesse de minage, référence vanilla | Portée de minage | Portée de pose |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | 75 % | 3 blocs | 3 blocs |
|
||||
| 1 | 85 % | 3,5 blocs | 3,5 blocs |
|
||||
| 2 | 95 % | 4 blocs | 4 blocs |
|
||||
| 3 | 100 % | 4,5 blocs | 4,5 blocs |
|
||||
| 4 | 107,5 % | 4,75 blocs | 4,75 blocs |
|
||||
| 5 | 115 % | 5 blocs | 5 blocs |
|
||||
| 6 | 122,5 % | 5,25 blocs | 5,25 blocs |
|
||||
| 7 | 130 % | 5,5 blocs | 5,5 blocs |
|
||||
|
||||
Chaque colonne dépend de sa propre compétence. La vitesse s’applique via
|
||||
l’attribut natif de casse, avec les outils, enchantements et autres effets.
|
||||
Les portées préservent les bonus externes de l’attribut d’interaction ;
|
||||
les valeurs du tableau supposent cet attribut à sa valeur survival de base.
|
||||
Créatif, spectateur et joueurs sans progression Sanctuary conservent leur
|
||||
portée native. Les infobulles FR/EN affichent le palier actuel et le suivant.
|
||||
|
||||
La visée peut sélectionner le plus éloigné des gestes disponibles ; les
|
||||
validations de chaque action appliquent ensuite sa propre portée. Ainsi,
|
||||
voir le contour d’un bloc n’accorde pas forcément la portée pour le miner
|
||||
ou y poser un bloc : le même contour sert aux interactions ordinaires.
|
||||
La portée des coffres, autres interactions et attaques n’augmente pas.
|
||||
Un coffre hors de portée native peut servir de support de construction,
|
||||
sans s’ouvrir à distance. La main gauche et les plantes aquatiques suivent
|
||||
la portée de Construction.
|
||||
|
||||
Les deux sélections groupées vérifient leur ancre avec leur compétence.
|
||||
Le plan et la veine verrouillés conservent leur contrat : les cellules du
|
||||
groupe peuvent dépasser la portée de l’ancre. Les droits, protections,
|
||||
collisions, stock, conditions de pose et chunks chargés restent vérifiés.
|
||||
La validation réseau conserve la tolérance native de distance à l’entrée
|
||||
des paquets ; la pose vérifie aussi la distance du point cliqué côté serveur.
|
||||
|
||||
Aucun identifiant, schéma de sauvegarde ni terrain modifié. Les anciens
|
||||
rangs reçoivent les nouvelles valeurs à leur application habituelle ; un
|
||||
reset ou prestige revient aux valeurs de départ.
|
||||
|
||||
## Vérification
|
||||
|
||||
`Balance080ClientChecks` réussit en **1 min 03 s**, dans un monde plat solo
|
||||
jetable, avec les gestes natifs et les paquets réels client/serveur :
|
||||
|
||||
- Les huit rangs, vitesse native de casse d’un bloc et composition des bonus.
|
||||
- Pose proche au rang zéro, refus de la pose lointaine sans perte de stock.
|
||||
- Minage et pose au-delà de la portée vanilla ; indépendance des deux compétences.
|
||||
- Pose en main gauche et refus serveur d’un paquet visant trop loin.
|
||||
- Coffre lointain utilisable comme support sans ouvrir son inventaire ; un
|
||||
briquet en main principale ne profite pas de la portée du bloc en main gauche.
|
||||
- Construction d’un plan complet de neuf blocs à la nouvelle portée.
|
||||
- Vein mining de neuf blocs à cette portée, regard détourné après verrouillage.
|
||||
- Pose d’un nénuphar, retour aux rangs zéro, créatif et joueur sans progression.
|
||||
|
||||
Journal : `build/balance080-client.log`. Les interactions des blocs portés
|
||||
sur la tête ont également été relues : elles passent par leur contexte
|
||||
propre, sans la pose de bloc interceptée ici.
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en **2 min 27 s**,
|
||||
124 tâches (104 exécutées, 20 à jour), avec `-x :sanctuary:runGameTest`.
|
||||
Journal : `build/balance080-check.log`.
|
||||
|
||||
Les **1 700 classes** du JAR correspondent aux fichiers compilés. Les 17
|
||||
classes ajoutées ou modifiées par rapport à beta.079 concernent uniquement
|
||||
ce ticket ; quatre libellés évoluent par langue. Sources et ressources
|
||||
vérifiées, textures beta.070 identiques, même JAR intégré aux deux packs.
|
||||
Les archives beta.079 sont conservées à l’identique.
|
||||
Reçu : `build/balance080-artifact.json`. Aucun déploiement personnel.
|
||||
Le serveur dédié reste exclu conformément au refus de son EULA ; le test
|
||||
natif ci-dessus utilise le serveur intégré.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.080.mrpack](../build/Sanctuary-beta.080.mrpack).
|
||||
SHA-256 : `feb20b882442149789ed6d90e32073aa6201314a57782105a0330394c76e796d`.
|
||||
- [Sanctuary-Test-beta.080.mrpack](../build/Sanctuary-Test-beta.080.mrpack).
|
||||
SHA-256 : `fc277f081f160ae8ae835501cddddd572cdffe4749454e1652a080a117bfb941`.
|
||||
@@ -0,0 +1,43 @@
|
||||
# beta.081 — portée de pose jusqu’à six blocs
|
||||
|
||||
Branche `codex/build-reach-six-beta081`, Minecraft 26.3-pre-2, textures beta.070.
|
||||
|
||||
À la demande du joueur, Construction atteint maintenant **six blocs**.
|
||||
Les portées des rangs 0 à 7 sont : **3 ; 3,5 ; 4 ; 4,5 ; 4,75 ; 5 ; 5,5 ; 6**.
|
||||
Le rang 6 est également ajusté pour terminer par deux gains d’un demi-bloc.
|
||||
La référence reste l’attribut d’interaction vanilla sans bonus externe.
|
||||
|
||||
Le minage conserve sa portée maximale de 5,5 blocs et sa vitesse de 130 %.
|
||||
Le contrat client/serveur, la pose en main gauche, les plans groupés et les
|
||||
infobulles FR/EN utilisent déjà la même source des valeurs. Aucun changement
|
||||
des coûts, identifiants, sauvegardes ou mondes ; les archives beta.080 restent
|
||||
immuables.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le scénario natif `Balance080ClientChecks`, ajusté à cette courbe, réussit
|
||||
en **1 min 21 s** dans un monde solo jetable : pose réelle à 5,75 blocs,
|
||||
refus serveur d’un clic à 6,25 blocs sans perte de stock, et régression des
|
||||
huit rangs, du minage indépendant, de la main gauche, des groupes, des
|
||||
nénuphars, du créatif et des bonus externes.
|
||||
Journal : `build/reach081-client.log`.
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en **2 min 30 s**,
|
||||
124 tâches (103 exécutées, 21 à jour), avec `-x :sanctuary:runGameTest`.
|
||||
Journal : `build/reach081-check.log`.
|
||||
|
||||
Les 1 700 classes et les sources correspondent aux fichiers compilés :
|
||||
seule `BuildingLimits` change par rapport à beta.080. Ressources et textures
|
||||
identiques, même JAR dans les deux packs, archives beta.080 préservées.
|
||||
Reçu : `build/reach081-artifact.json`.
|
||||
|
||||
Le serveur dédié reste exclu
|
||||
conformément au refus de son EULA ; le test utilise le serveur intégré.
|
||||
Aucun déploiement personnel.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.081.mrpack](../build/Sanctuary-beta.081.mrpack).
|
||||
SHA-256 : `7f789522eaa4a2574cc9bee4a4def7862a4893c8f462e88502ca858440c8ee02`.
|
||||
- [Sanctuary-Test-beta.081.mrpack](../build/Sanctuary-Test-beta.081.mrpack).
|
||||
SHA-256 : `7811cdc47ab4a8af2fc6ccd530775eb4a239f18894965362a1c57a5e0f12ded5`.
|
||||
@@ -0,0 +1,205 @@
|
||||
# CAPE-01 — Des capes rares aux pouvoirs durables
|
||||
|
||||
**Ticket local rédigé le 15 septembre 2026 — à arbitrer, puis à implémenter.**
|
||||
Cette livraison prépare le ticket ; les effets et l'acquisition décrits ci-dessous
|
||||
ne sont pas implémentés.
|
||||
|
||||
- Branche documentaire réservée : `codex/capes-ticket`.
|
||||
- Branche d'implémentation prévue : `codex/capes-gameplay`.
|
||||
- Aucun numéro `beta.xxx` réservé pour cette préparation documentaire.
|
||||
|
||||
## Ce que le joueur doit pouvoir faire
|
||||
|
||||
Trouver une cape Sanctuary doit être un événement : un objet très rare,
|
||||
reconnaissable et désirable, que l'on a envie de porter pour son pouvoir.
|
||||
Les capes donnent généralement un **bonus passif permanent**. Certaines peuvent
|
||||
offrir un pouvoir exceptionnel, voire volontairement « cheaté », si son impact
|
||||
reste acceptable dans la partie collective.
|
||||
|
||||
Un catalogue riche est souhaitable : il peut contenir beaucoup de modèles,
|
||||
y compris plusieurs capes très puissantes. La diversité du catalogue et le
|
||||
nombre d'exemplaires en circulation sont deux choix distincts.
|
||||
|
||||
### Intentions exprimées par le créateur
|
||||
|
||||
- Privilégier les bonus durables et l'envie d'utiliser les capes.
|
||||
- Faire des capes des objets très rares.
|
||||
- Garder ouverte la possibilité de malus ou de contreparties.
|
||||
- Accepter des pouvoirs très forts ; leur puissance seule ne les exclut pas.
|
||||
- Examiner leurs conséquences réelles sur le gameplay et les autres joueurs.
|
||||
- Envisager la rareté et des périodes de fonctionnement comme moyens
|
||||
d'équilibrage, sans rendre toutes les capes temporaires.
|
||||
|
||||
### Interprétation proposée de « permanent »
|
||||
|
||||
Le pouvoir reste disponible **tant que la cape est équipée dans sa case dédiée**,
|
||||
sans renouveler une potion ou appuyer régulièrement sur une touche. Posséder
|
||||
la cape dans un coffre ou l'inventaire n'accorde rien. Le retrait arrête son
|
||||
effet ; découvrir plusieurs capes ne donne pas plusieurs bonus permanents au
|
||||
personnage. Cette interprétation reste à confirmer avant le code.
|
||||
|
||||
Les exceptions temporaires sont annoncées cape par cape. Proposition : la cape
|
||||
reste un objet de collection après sa période active ; c'est son pouvoir qui
|
||||
s'endort. Une cape consommable ou détruite à l'expiration serait un autre choix,
|
||||
à expliciter, pas une conséquence implicite du mot « temporaire ».
|
||||
|
||||
## État réel du socle
|
||||
|
||||
La [beta.018](companions-cosmetics-beta018.md) a livré la case cape, les gestes
|
||||
d'équipement, la sauvegarde et le rendu partagé. Le code consulté contient une
|
||||
seule cape, `sanctuary:zero_cape`, disponible en créatif ou par `/give`.
|
||||
**Cape Zéro est actuellement visuelle : aucun bonus de gameplay n'est appliqué
|
||||
par son service.** Son apparence est provisoire. Le filtre d'équipement et le
|
||||
rendu reconnaissent explicitement cet objet ; un catalogue demande leur extension.
|
||||
|
||||
Les [lootboxes de capes](vision.md#panneaux-et-lootboxes) appartiennent à la vision.
|
||||
Elles ne constituent pas une acquisition en survie déjà livrée. Les familiers,
|
||||
la progression, le portage, les bateaux et le temps réel existent et devront
|
||||
être pris en compte lors des essais des effets retenus.
|
||||
|
||||
## Périmètre proposé pour un premier lot jouable
|
||||
|
||||
Livrer **trois capes aux usages distincts**, leur description FR/EN, leur effet
|
||||
serveur et une première voie d'obtention rare effectivement jouable. Le choix
|
||||
des trois capes ci-dessous est une proposition de prototype, pas un catalogue
|
||||
approuvé. Les autres modèles pourront suivre par petits lots.
|
||||
|
||||
| Piste FR / EN | Pouvoir envisagé | Point d'équilibrage à éprouver |
|
||||
| --- | --- | --- |
|
||||
| **Cape de la Brise / Breeze Cape** | Bonus permanent de vitesse de déplacement au sol. | Utilité quotidienne ; cumul avec Sprint, potions, familiers et ralentissement du portage. Un bonus simple peut rester sans malus. |
|
||||
| **Cape du Brasier / Ember Cape** | Forte augmentation des dégâts de mêlée ; réduction de la vie maximale comme contrepartie possible. | Gain offensif réellement sensible ; risque assumé et lisible. Vérifier si le malus influe sur les combats ou s'annule facilement par une combinaison. |
|
||||
| **Cape de l'Éclipse / Eclipse Cape** | Vol libre pendant une courte fenêtre récurrente, pouvoir volontairement exceptionnel. | Accès entre îles, exploration, transport et sortie de fenêtre en plein vol. Durée, fréquence et éventuelles restrictions à décider après essai. |
|
||||
|
||||
Pour chaque cape sélectionnée, renseigner avant implémentation : nom FR/EN,
|
||||
identifiant stable, visuel et provenance, effet chiffré, conditions, éventuel
|
||||
malus, cumuls, acquisition, fréquence d'obtention et règle temporelle complète.
|
||||
Le devenir de Cape Zéro reste une décision distincte : ne pas attribuer
|
||||
automatiquement un nouveau pouvoir aux exemplaires existants.
|
||||
|
||||
Ce lot réutilise la case existante. Il ne nécessite ni nouveau monde, ni nouvelle
|
||||
génération, ni économie complète, ni système général de quêtes ou de lootboxes.
|
||||
La source de récompense doit être choisie parmi les mécanismes effectivement
|
||||
disponibles au démarrage ; si elle exige un chantier autonome, la référencer
|
||||
comme dépendance. Un prototype accessible uniquement par `/give` valide les
|
||||
pouvoirs, mais ne clôt pas le résultat « trouver une cape rare en survie ».
|
||||
|
||||
## Équilibrage : mesurer ce que la cape change
|
||||
|
||||
**La rareté ne suffit pas à prouver l'équilibre.** Une cape obtenue une seule fois
|
||||
peut servir quotidiennement, être prêtée à tout un groupe ou aider son détenteur
|
||||
à en obtenir d'autres. Une faible probabilité répétable peut finir par produire
|
||||
beaucoup d'exemplaires. À l'inverse, une cape très forte dans un contexte précis
|
||||
peut créer un moment mémorable sans dominer toute la progression.
|
||||
|
||||
L'objectif proposé est que plusieurs capes aient des usages désirables, tout en
|
||||
gardant un parcours intéressant sans cape. Les exceptions qui court-circuitent
|
||||
une étape doivent être identifiées et assumées dans leur fiche.
|
||||
|
||||
| Axe | Essai à réaliser et décision à en tirer |
|
||||
| --- | --- |
|
||||
| **Exploration et vide** | Comparer un trajet entre îles sans cape, avec cape et avec moyens de transport existants. Identifier les risques supprimés, les accès anticipés et l'utilité restante des infrastructures. |
|
||||
| **Combat PvE et PvP** | Mesurer dégâts, survie et possibilité de réponse à équipement comparable, puis en combinaison forte. Décider explicitement du traitement PvP ; ne pas le désactiver par défaut dans la conception. |
|
||||
| **Progression et production** | Relever temps gagné, ressources et XP obtenues. Vérifier si une cape rend superflus une compétence, une potion, un familier ou une étape collective. |
|
||||
| **Cumuls et coopération** | Éprouver cape + familier + équipement + potions, portage et prêt entre joueurs. Une seule case cape ne limite pas les autres sources de bonus. |
|
||||
| **Rareté dans le temps** | Estimer les exemplaires et détenteurs après une semaine et un mois, pour un joueur occasionnel, un joueur intensif et un groupe qui mutualise les récompenses. |
|
||||
| **Plaisir et choix** | Observer si les joueurs veulent réellement porter chaque cape, changent selon l'activité ou choisissent toujours la même. Un malus qui conduit à tout laisser au coffre rate l'intention. |
|
||||
|
||||
Ajuster en priorité le contexte d'utilité, les cumuls, l'acquisition ou la période
|
||||
active lorsqu'ils permettent de conserver un pouvoir spectaculaire. Réduire
|
||||
la puissance ou ajouter un malus reste possible, sans imposer une pénalité à
|
||||
chaque cape. Une cape maudite à malus seul reste une piste à arbitrer.
|
||||
|
||||
### Rareté et circulation
|
||||
|
||||
- Fixer la source, les joueurs éligibles, la fréquence des tentatives, le taux
|
||||
ou quota éventuel et la possibilité de renouveler la récompense.
|
||||
- Distinguer rareté d'un modèle et rareté de l'ensemble : beaucoup de modèles
|
||||
très rares peuvent rendre l'obtention d'une cape quelconque fréquente.
|
||||
- Examiner les prêts, échanges, doublons, récompenses rejouées et New Game+.
|
||||
Liaison au joueur et exemplaire unique au serveur sont des options, pas des
|
||||
règles acquises. Les nouveaux arrivants doivent être inclus dans l'essai.
|
||||
- Documenter les hypothèses de calcul et confronter la rareté attendue à des
|
||||
essais. Un taux de butin bas, seul, n'est pas un critère d'acceptation suffisant.
|
||||
|
||||
### Périodes d'activité des capes exceptionnelles
|
||||
|
||||
Choisir, pour chaque cape concernée, **un modèle temporel explicite** : fenêtre
|
||||
commune récurrente, durée depuis l'obtention, ou budget de temps d'utilisation.
|
||||
Une période d'obtention limitée et une période d'effet limitée sont distinctes.
|
||||
|
||||
Le contrat précise l'horloge utilisée, début, fin, fréquence, temps hors ligne,
|
||||
arrêts serveur et effet des changements de mode Vanilla/Real Time. Une fenêtre
|
||||
liée à une heure réelle doit aussi être essayée du point de vue des joueurs
|
||||
qui ne peuvent pas se connecter à cette heure.
|
||||
|
||||
Le serveur fait autorité. Retirer, rééquiper, prêter, mourir, se reconnecter ou
|
||||
redémarrer ne doit pas réinitialiser involontairement une durée ou une recharge.
|
||||
L'interface indique l'état actif/dormant, le temps restant et la prochaine
|
||||
occasion lorsqu'elle est prévisible. Prévoir l'avertissement et la transition
|
||||
de fin : pour le vol, définir une sortie praticable, sans mort surprise ni
|
||||
prolongation illimitée par rééquipement.
|
||||
|
||||
## Contrat technique à écrire avant le code
|
||||
|
||||
- Une seule cape active par joueur ; effets et conditions calculés côté serveur.
|
||||
Une préférence visuelle ne peut pas accorder ou prolonger un pouvoir.
|
||||
- Appliquer et retirer uniquement la contribution de la cape. Préserver un
|
||||
effet identique venant d'une potion ou d'un familier ; préciser addition,
|
||||
multiplication, priorité ou plafond pour chaque cumul pertinent.
|
||||
- Traiter changement de cape, mort, tombe, `keepInventory`, changement de
|
||||
dimension, reconnexion et New Game+ avec les règles d'inventaire existantes.
|
||||
Définir aussi le retrait d'un bonus de vie ou d'un malus de vie maximale,
|
||||
sans soin gratuit par alternance de capes.
|
||||
- Conserver `sanctuary:zero_cape` et les emplacements existants. Tout ajout de
|
||||
données persistantes, notamment temporelles, exige un contrat de migration
|
||||
explicite et des essais sur des copies de sauvegardes de développement.
|
||||
- Décrire en FR/EN bonus, contrepartie, conditions et durée avant équipement.
|
||||
Vérifier le rendu porté et les élytres sur la version Minecraft exacte du lot.
|
||||
|
||||
## Critères d'acceptation de la future livraison
|
||||
|
||||
- [ ] Les trois fiches sont complètes ; valeurs, obtention, cumuls et traitement
|
||||
temporel sont fixés, avec les conséquences fortes assumées par la conception.
|
||||
- [ ] Un joueur peut obtenir une cape par la voie de survie retenue, l'équiper,
|
||||
comprendre son pouvoir et constater son effet réel. Une récompense unique
|
||||
ne peut pas être réclamée plusieurs fois par reconnexion.
|
||||
- [ ] Le bonus permanent reste actif au-delà de la durée d'une potion ordinaire
|
||||
tant que la cape est portée. Stockage et retrait ne laissent aucun bonus indu.
|
||||
- [ ] Deux joueurs voient un équipement cohérent ; changement rapide de cape,
|
||||
effet concurrent et prêt ne produisent ni cumul résiduel ni duplication.
|
||||
- [ ] Les limites de période fonctionnent avant, pendant et après la fenêtre,
|
||||
y compris après arrêt/reprise et transfert ; la sortie d'un pouvoir dangereux
|
||||
respecte la transition annoncée.
|
||||
- [ ] Mort/tombe, `keepInventory`, dimension, reconnexion, redémarrage et
|
||||
New Game+ conservent les objets et durées selon le contrat de migration.
|
||||
- [ ] Un compte rendu compare sans cape, chaque cape seule et les combinaisons
|
||||
les plus fortes, en début et en fin de progression, en solo et à 2–4 joueurs.
|
||||
Il relève gains, abus possibles, circulation attendue et ajustements retenus.
|
||||
- [ ] Aucun pouvoir n'est déclaré équilibré sur la seule base de sa rareté ou
|
||||
d'un build réussi ; les limites de l'essai sont documentées.
|
||||
- [ ] `./gradlew check build` passe avec les tests utiles aux effets retenus ;
|
||||
`./gradlew assemblePack` vérifie la distribution. Versions et note de livraison
|
||||
suivent [le compteur bêta](versioning.md). Parcours client FR/EN vérifié.
|
||||
|
||||
## Décisions encore ouvertes
|
||||
|
||||
1. Confirmer « permanent tant que porté » et le rôle des capes à malus seul.
|
||||
2. Choisir les trois premières capes, leurs valeurs et le devenir de Cape Zéro.
|
||||
3. Choisir la première acquisition jouable et la rareté visée, puis décider
|
||||
des échanges et des éventuels quotas.
|
||||
4. Choisir la règle temporelle des exceptions, leurs cumuls, leur traitement
|
||||
PvP et les étapes de progression qu'elles peuvent volontairement dépasser.
|
||||
|
||||
## Références vérifiées pour cette préparation
|
||||
|
||||
- [Vision : progression](vision.md#inventaire-prestige-et-déblocages) et
|
||||
[récompenses](vision.md#panneaux-et-lootboxes).
|
||||
- [Service actuel des accessoires](../mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/AccessoryService.java),
|
||||
[filtre et stockage](../mods/sanctuary/src/main/java/fr/koka/sanctuary/inventory/AccessoryContainer.java)
|
||||
et [rendu](../mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/AccessoryAvatarRendererMixin.java).
|
||||
- [Catalogue des familiers beta.032](spawn-eggs-v2-catalogue-beta032.md), à
|
||||
confronter au code courant lors des essais de cumul.
|
||||
|
||||
Validation de ce ticket : lecture du socle, vérification des références locales
|
||||
et relecture du diff documentaire. Aucun essai de gameplay ni build exécuté
|
||||
pour cette rédaction ; les critères ci-dessus concernent la future implémentation.
|
||||
@@ -0,0 +1,49 @@
|
||||
# beta.077 — tourbillon de nuages au-dessus du vide
|
||||
|
||||
Branche `codex/cloud-vortex-beta077`. Minecraft 26.3-pre-2, textures beta.070.
|
||||
|
||||
La dernière précision du créateur fixe la couche à **Y = 0**, et non 64.
|
||||
Les nuages supérieurs restent présents. Un tourbillon continu tourne autour
|
||||
du centre de l’île de Sanctuary ; son diamètre vaut deux fois celui de l’île
|
||||
(1 448 blocs / environ 91 chunks pour l’île habituelle de 724 blocs).
|
||||
Une révolution dure 20 minutes de simulation. Le style garde les cellules de
|
||||
12 blocs, l’épaisseur de 4 blocs et l’éclairage des nuages Minecraft.
|
||||
|
||||
Le serveur transmet uniquement le centre et le diamètre déjà définis par le
|
||||
générateur. Aucun chunk, bloc, sauvegarde ou génération n’est modifié. Les
|
||||
mondes classiques/plats et les autres dimensions ne reçoivent pas de couche.
|
||||
Les réglages de nuages désactivés/rapides/détaillés sont respectés.
|
||||
|
||||
La géométrie est calculée une fois puis conservée sur le GPU. La rotation et
|
||||
la couleur passent par les uniforms natifs, sans remaillage à chaque mouvement
|
||||
de caméra. Les passes normales et de transparence Fabulous réutilisent les
|
||||
pipelines de nuages du jeu. Les buffers sont libérés à la déconnexion.
|
||||
|
||||
Cette livraison inclut les [correctifs de menus beta.076](menu-navigation-beta076.md)
|
||||
et le retrait du lien web de la [bibliothèque beta.075](plans-library-beta075.md).
|
||||
|
||||
## Vérifications
|
||||
|
||||
Parcours client natif réussi en 1 min 8 s : formes connectées pour les îles de
|
||||
512, 724 et 1 024 blocs ; rendu rapide/détaillé/Fabulous (passages OIT réellement
|
||||
exécutés), rotation, vues au-dessus/dessous/dans la couche, désactivation et
|
||||
extension finie. Une seule construction de géométrie pendant les mouvements
|
||||
et changements de qualité. 5 888 cellules pour l’île habituelle.
|
||||
Captures : `build/cloud077-evidence/`.
|
||||
`check build assemblePack assembleTestPack` réussit en 3 min 22 s (124 tâches),
|
||||
avec le serveur dédié exclu conformément au refus de son EULA.
|
||||
Les 1 693 classes et les sources du JAR correspondent au build ; seuls les
|
||||
menus et le nouveau rendu changent depuis beta.075. Les textures, données de
|
||||
jeu et le resource pack restent identiques. Les onze libellés sont présents
|
||||
en FR/EN. Les deux packs sont vérifiés et 155 archives précédentes restent
|
||||
identiques. Reçu : `build/cloud077-artifact.json`.
|
||||
Pas de déploiement dans une installation personnelle ni de serveur dédié.
|
||||
Les shaders externes qui remplacent entièrement le rendu des nuages restent
|
||||
à vérifier séparément ; aucune compatibilité non testée n’est promise.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.077.mrpack](../build/Sanctuary-beta.077.mrpack), 9608669 octets.
|
||||
SHA-256 : `1c3839f086e9545c920e4ebfc13f99a25d0684fa6f2dc8e1b2a842a8e7044045`.
|
||||
- [Sanctuary-Test-beta.077.mrpack](../build/Sanctuary-Test-beta.077.mrpack), 9627602 octets.
|
||||
SHA-256 : `1b0c302b9d4d5f59d5e954adbecde4acbacd9409c8cd6a743ccbe116826d7c0a`.
|
||||
@@ -0,0 +1,36 @@
|
||||
# beta.078 — Combats / Battles
|
||||
|
||||
Branche `codex/battle-menu-beta078`. Minecraft 26.3-pre-2 ; textures beta.070.
|
||||
|
||||
Le bouton et le titre « Duels et arènes » deviennent **Combats** en français
|
||||
et **Battles** en anglais. Le lien depuis les duels de familiers utilise
|
||||
également ce libellé. Les annonces d’inscription indiquent le menu pause,
|
||||
et les résultats renvoient au nouveau nom pour récupérer les gains.
|
||||
|
||||
Le [plan des menus](menu-architecture.md) conserve cette appellation. Le reste
|
||||
de cette réorganisation reste une proposition ; l’économie est différée.
|
||||
Aucune règle de combat, sauvegarde ou transaction n’est modifiée.
|
||||
|
||||
## Vérification
|
||||
|
||||
`check build assemblePack assembleTestPack` réussi en 2 min 33 s, avec le
|
||||
serveur dédié exclu conformément au refus de son EULA. Après la précision
|
||||
du créateur sur le pluriel, les ressources et packs sont reconstruits en
|
||||
8 s ; vérification finale des quatre traductions par langue et de leurs
|
||||
paramètres de formatage.
|
||||
|
||||
Les 1 693 classes du JAR sont identiques à beta.077. Les textures, données
|
||||
de jeu et le resource pack restent inchangés. Les deux archives embarquent
|
||||
le JAR vérifié ; les archives beta.077 sont conservées à l’identique.
|
||||
Reçu : `build/battle078-artifact.json`. Journaux : `build/battle078-check.log`
|
||||
et `build/battle078-assemble.log`.
|
||||
|
||||
Pas de nouveau parcours client graphique pour ce changement de libellés.
|
||||
Aucun déploiement dans une installation personnelle.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.078.mrpack](../build/Sanctuary-beta.078.mrpack).
|
||||
SHA-256 : `9adfa5b5b8d49ed10943121ac2a2561d880886925a5f76db5ba98f0377d8d7e3`.
|
||||
- [Sanctuary-Test-beta.078.mrpack](../build/Sanctuary-Test-beta.078.mrpack).
|
||||
SHA-256 : `92bc1d59723546c31a6e7476dd9f53225c7957e5180091dfeaeade69ff265a86`.
|
||||
@@ -0,0 +1,98 @@
|
||||
# beta.074 — autonomie et ordres des familiers
|
||||
|
||||
Branche `codex/familiar-autonomy-beta074`. Implémentation et parcours client natif vérifiés.
|
||||
|
||||
## Comportements
|
||||
|
||||
| Personnalité | Décision autonome |
|
||||
| --- | --- |
|
||||
| Agressif | Engage les monstres qu’il voit à 12 blocs du joueur, accompagne ses attaques et riposte. |
|
||||
| Protecteur | Garde 6 blocs autour du joueur, intercepte en priorité les monstres qui le ciblent, défend le joueur et lui-même. Abandonne la poursuite au-delà de 10 blocs du point gardé. |
|
||||
| Passif | N’engage pas de combat spontanément ; défend le joueur attaqué et riposte s’il est frappé. |
|
||||
| Pacifiste | Évite le combat et s’écarte d’un agresseur. Attend un ordre d’attaque ou de garde. |
|
||||
|
||||
La vue est celle du familier : son propriétaire n’a pas besoin de voir le
|
||||
monstre ou de le viser. Les animaux paisibles, les monstres neutres non engagés,
|
||||
les alliés et les autres joueurs ne sont pas attaqués par la détection de
|
||||
proximité. La riposte PvP et les ordres conservent les permissions du monde,
|
||||
les factions et les règles des duels/arènes.
|
||||
|
||||
Un ordre d’attaque prime sur la personnalité et les autres menaces. « Protéger »
|
||||
ordonne explicitement de garder un allié visé, soi-même par défaut, ou le point
|
||||
visé en maintenant Maj. Même un pacifiste peut exécuter cette garde demandée.
|
||||
Pendant une poursuite spontanée, le protecteur peut changer de cible pour
|
||||
intercepter une menace qui commence à viser son joueur ou l’allié gardé.
|
||||
Une cible morte laisse place à une nouvelle décision, au plus tard lors du
|
||||
prochain balayage. Le protecteur revient près de son point de garde.
|
||||
Les espèces de soutien disposent du coup de défense lorsqu’elles s’engagent ;
|
||||
leurs signatures, leurs dégâts existants et leurs coûts en fournitures restent inchangés.
|
||||
|
||||
## Commandes
|
||||
|
||||
H bref : attaquer l’ennemi visé, ou revenir au suivi s’il n’y a pas de cible.
|
||||
H maintenu environ 0,4 seconde : ouvrir le menu d’ordres. La touche reste
|
||||
configurable ; F échange les mains, G déclenche la technique. Relâcher après
|
||||
l’ouverture du menu ne lance pas d’attaque. Les messages brefs confirment
|
||||
l’ordre. « Suivre » suspend les réactions spontanées pendant cinq secondes.
|
||||
|
||||
## Exécution et conservation
|
||||
|
||||
Une recherche d’entités chargées par seconde, décalée entre les familiers.
|
||||
Aucun chargement de chunk ; chemins demandés au maximum toutes les dix ticks.
|
||||
Une cible autonome perdue de vue cinq secondes, ou sans progression six
|
||||
secondes, est écartée temporairement pendant dix secondes. Les commandes
|
||||
explicites peuvent la désigner de nouveau. Les montures, familiers portés,
|
||||
œufs rappelés et mode travail n’entament pas de recherche autonome.
|
||||
|
||||
Le tirage de personnalité, les UUID, les 88 profils, la santé, l’XP et les
|
||||
souvenirs sont conservés. Aucun format de sauvegarde, terrain ni journal de
|
||||
mise ne change. Les règles de combat des arènes restent prioritaires.
|
||||
Textures beta.070, Minecraft 26.3-pre-2 et dépendances identiques.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `Autonomy074ClientChecks` réussit en **1 min 44 s** avec un
|
||||
client et son serveur intégré, dans un nouveau monde plat de développement :
|
||||
|
||||
- Les quatre personnalités : recherche à 12/6 blocs, attaque du propriétaire,
|
||||
dégâts réellement reçus par le joueur et déplacement jusqu’à la cible.
|
||||
- Protecteur villageois : interruption d’une poursuite pour une menace visant
|
||||
le joueur, dégâts sans bouclier, cible suivante et retour dans la zone.
|
||||
- Abeille neutre paisible puis agressive, garde explicitement demandée à un
|
||||
pacifiste et priorité d’une cible ordonnée.
|
||||
- Vue propre au familier, obstacle opaque, cible enfermée, permissions,
|
||||
mode travail, suspension puis reprise après Suivre.
|
||||
- Événements clavier natifs : H bref/maintenu, reconfiguration sur Y,
|
||||
absence d’attaque au relâchement du menu, F natif ; fiches FR/EN.
|
||||
|
||||
La régression `Familiar071ClientChecks` réussit en **1 min 50 s** :
|
||||
352 000 identités sur les 88 profils, réactions, dégâts, araignées montées,
|
||||
Strider sur lave et les deux Nautilus aux tailles 2 500 et 6 000 ‰.
|
||||
La fermeture/réouverture du monde conserve l’UUID et le caractère.
|
||||
|
||||
Journaux : `build/autonomy074-client.log`, `build/autonomy074-mounts.log`.
|
||||
Captures de la fiche actuelle et des montures : `build/autonomy074-evidence/`.
|
||||
`Arena073ClientChecks` et la régression `Duel054Checks` passent en
|
||||
**1 min 10 s** : arènes de familiers/joueurs/mixtes, 25 inscrits simulés,
|
||||
consentement PvP, K.-O., morts réelles, mises, remboursements et récupération
|
||||
après interruption. Journal : `build/autonomy074-arenas.log`.
|
||||
`check build assemblePack assembleTestPack` réussit en **3 min 6 s**
|
||||
(124 tâches). Les deux exports packwiz sont vérifiés : **1 689 classes**
|
||||
comparées au JAR, sources testées identiques, huit libellés FR/EN mis à jour,
|
||||
ressources et textures beta.070 intactes. Seules les quatre classes sources
|
||||
prévues changent par rapport à beta.073. Les neuf archives précédentes
|
||||
beta.070 à beta.073 restent strictement identiques.
|
||||
Reçu : `build/autonomy074-artifact.json`.
|
||||
|
||||
Ces essais couvrent l’IA commune ; ils ne constituent pas un nouveau playtest
|
||||
des 88 techniques ni un test à plusieurs clients distants. Le serveur dédié
|
||||
reste exclu (`-x :sanctuary:runGameTest`) conformément au refus d’accepter
|
||||
son EULA ; les essais natifs utilisent le serveur intégré.
|
||||
Aucun monde personnel ni déploiement Prism.
|
||||
|
||||
## Archives locales
|
||||
|
||||
- [Sanctuary-beta.074.mrpack](../build/Sanctuary-beta.074.mrpack), 9595113 octets.
|
||||
SHA-256 : `cc73e93c2ae17d6b52b6c8dd53639771645c5e5becd848e2c76765e32c919c5d`.
|
||||
- [Sanctuary-Test-beta.074.mrpack](../build/Sanctuary-Test-beta.074.mrpack), 9614048 octets.
|
||||
SHA-256 : `0851e10350788b76efa96ec1a50db951a74200213399e2a4216f8d5b1b8fafcb`.
|
||||
@@ -0,0 +1,80 @@
|
||||
# beta.067 — touche du familier et dragon volant
|
||||
|
||||
Branche `codex/familiar-controls-beta067`.
|
||||
|
||||
## Contrat
|
||||
|
||||
La touche des ordres du familier passe de H à F. La touche G de technique
|
||||
reste indépendante. Le réglage H hérité est déplacé une seule fois vers F
|
||||
au démarrage ; les autres touches personnalisées restent conservées. Un
|
||||
marqueur local `config/sanctuary-familiar-key-v1.txt` permet ensuite de choisir
|
||||
à nouveau H sans être remappé au démarrage suivant. L'action reste configurable.
|
||||
|
||||
Pendant le jeu Sanctuary, ouvrir les ordres avec F ne déclenche plus l'échange
|
||||
des objets des deux mains. Le raccourci natif garde son comportement dans les
|
||||
inventaires et hors Sanctuary ; réassigner les ordres libère également F.
|
||||
|
||||
Le modèle natif du dragon utilise un axe avant opposé aux modèles vivants
|
||||
ordinaires. Son historique de vol reçoit cette correction d'orientation ;
|
||||
la direction physique du familier et des attaques reste inchangée.
|
||||
|
||||
Le petit dragon porté utilise le planeur existant : descente bornée à
|
||||
0,12 bloc par tick, distance de chute accumulée effacée. Le grand dragon porté
|
||||
(taille existante > 1 200 et < 2 500 millièmes) permet une montée maintenue avec
|
||||
la touche de saut, bornée à 0,18 bloc par tick. Relâcher reprend la descente
|
||||
planée. Le seuil colossal de 2 500 reste une monture sur laquelle le joueur
|
||||
s'assoit ; il ne peut pas être porté sur la tête. Seul le dragon reçoit la
|
||||
montée lorsqu'il est porté ; les autres petits animaux volants gardent leur
|
||||
plané et les montures colossales leur pilotage existant.
|
||||
|
||||
La physique du joueur reste native avec collisions. Le serveur n'exempte du
|
||||
contrôle d'immobilité aérienne que la montée réellement soutenue par un grand
|
||||
dragon porté et la touche de saut reçue. Retrait du familier, immersion,
|
||||
déconnexion et perte du portage interrompent l'assistance. Les règles
|
||||
existantes de collision du passager s'appliquent au dragon sur la tête.
|
||||
|
||||
Aucun changement du format des œufs, des sauvegardes ou de la génération.
|
||||
Les indications de montée dans l'infobulle utilisent la touche de saut
|
||||
configurée, avec libellés FR/EN.
|
||||
|
||||
## Vérification
|
||||
|
||||
`Familiar067ClientChecks` réussit en 55 secondes sur un monde plat de
|
||||
développement avec serveur intégré :
|
||||
|
||||
- F ouvre réellement les ordres sans changer les objets des deux mains ; H
|
||||
ne les ouvre plus. Réassigner sur K fonctionne et libère l'échange natif F.
|
||||
- La position de la tête dans le modèle de dragon soumis au rendu est devant
|
||||
le corps pour quatre directions cardinales, pas derrière.
|
||||
- Dragons de tailles 350 et 650 : Maj + clic droit natif, passager synchronisé,
|
||||
descente bornée côté client, distance de chute effacée des deux côtés ;
|
||||
Espace ne permet pas de monter avec ces petits.
|
||||
- Taille 1 500 : montée de plus de huit blocs en maintenant Espace pendant
|
||||
cent ticks, connexion maintenue, reprise du plané au relâchement, collision
|
||||
au plafond sans traversée du joueur.
|
||||
- Taille 3 500 : le joueur monte sur le dragon, puis décolle avec Espace.
|
||||
|
||||
Le plané des petits existait déjà : le parcours le confirme sur le dragon.
|
||||
Le nouveau comportement de montée concerne la catégorie « grand » portée.
|
||||
La transparence de proximité existante reste conservée.
|
||||
|
||||
Journaux : `build/familiar067-client.log` et `build/familiar067-check.log`.
|
||||
L'attente initiale de tous les chunks du rayon échouait dans le parcours ;
|
||||
le test attend désormais la partie jouable et utilise sa zone de test locale.
|
||||
`./gradlew check build assemblePack assembleTestPack` réussit en 2 min 12 s
|
||||
avec `-x :sanctuary:runGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryQuickTests=true`. Le parcours natif est lancé séparément avec
|
||||
`-PsanctuaryFamiliar067ClientTests=true -PsanctuaryClientNoVsync=true`.
|
||||
|
||||
Les deux MRpacks contiennent les 1 646 classes compilées et leurs ressources
|
||||
vérifiées. Les différences avec beta.066 se limitent aux cinq classes du
|
||||
correctif, au mixin enregistré, à l'indication FR/EN et aux versions des mods.
|
||||
JEI, les textures, les données de génération et les archives beta.066 restent
|
||||
identiques. Reçu : `build/familiar067-artifact.json`.
|
||||
|
||||
[Pack normal](../build/Sanctuary-beta.067.mrpack) ·
|
||||
[Monde plat rapide](../build/Sanctuary-Test-beta.067.mrpack).
|
||||
|
||||
Validation en client natif avec serveur intégré, sans serveur dédié ni essai
|
||||
à plusieurs clients distants. Aucun canal publié, instance personnelle ou
|
||||
monde existant modifié.
|
||||
@@ -0,0 +1,55 @@
|
||||
# beta.069 — retour des ordres du familier sur H
|
||||
|
||||
Branche `codex/familiar-orders-h-beta069`.
|
||||
|
||||
Les ordres du familier reviennent sur **H**. **F** conserve l’échange natif des
|
||||
objets entre les mains ; **G** reste la technique du familier. Les touches
|
||||
restent configurables dans les options et les libellés FR/EN existants suivent
|
||||
le raccourci assigné.
|
||||
|
||||
Le remplacement introduit en beta.067/.068 est annulé au premier démarrage :
|
||||
une ancienne affectation F des ordres devient H. Les autres touches
|
||||
personnalisées sont conservées. Le nouveau marqueur local
|
||||
`config/sanctuary-familiar-key-v2.txt` rend cette opération unique, même si le
|
||||
marqueur v1 existe déjà. Les modifications manuelles ultérieures ne sont pas
|
||||
réécrites. Le filtrage qui consommait les actions du raccourci d’échange des
|
||||
mains est supprimé. Aucun changement du serveur ou des sauvegardes.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Familiar067ClientChecks`, actualisé pour le retour sur H, réussit
|
||||
en **1 min 7 s** : H ouvre les ordres sans échanger les mains ; F échange un
|
||||
diamant et un lingot d’or dans les deux sens sans ouvrir de menu ; G conserve
|
||||
son affectation par défaut ; une personnalisation des ordres sur K fonctionne.
|
||||
Le reste de ce parcours existant (orientation, plané et vol du dragon) passe
|
||||
également. Le test utilise un monde plat de développement et un serveur intégré.
|
||||
|
||||
Après le parcours, les options enregistrées sont bien H/F/G et le marqueur v2
|
||||
contient beta.069 (`build/familiar069-controls.json`). Le lanceur réinitialise
|
||||
la configuration de test ; ce parcours ne prétend donc pas valider la mise à
|
||||
jour complète d’une ancienne installation. La conversion conditionnelle F→H
|
||||
et le garde par marqueur ont été vérifiés dans le code ; la configuration
|
||||
personnelle de Prism n’a pas été modifiée.
|
||||
|
||||
Journal : `build/familiar069-client.log`. Le sélecteur de ce parcours reste
|
||||
`-PsanctuaryFamiliar067ClientTests=true`, avec `-PsanctuaryClientTests=true
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true`.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`check build assemblePack assembleTestPack` réussi en **2 min 13 s**, 122 tâches,
|
||||
avec exclusion du serveur
|
||||
dédié (`-x :sanctuary:runGameTest`) et les profils client/test rapide.
|
||||
Voir `build/familiar069-check.log`. Aucun serveur dédié ni acceptation d’EULA.
|
||||
|
||||
Les 1 647 classes du mod correspondent à la compilation. Seule la classe
|
||||
`FamiliarClient` change par rapport à beta.068 ; textures, traductions,
|
||||
chargement, dragon et JEI restent identiques. Les archives beta.068 gardent
|
||||
leurs empreintes. Reçu : `build/familiar069-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.069.mrpack), 5726225 octets.
|
||||
SHA-256 : `1c923452ff4d5d186309390fac3306b8e05162143c852619b7519741002aa165`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.069.mrpack), 5745158 octets.
|
||||
SHA-256 : `bed499fa29f4247a9e321207df503b3147b8b4fc8c8541d817cdfe2a2db9a945`.
|
||||
|
||||
Aucune instance personnelle ni canal public modifié.
|
||||
@@ -0,0 +1,169 @@
|
||||
# beta.071 — personnalité et engagement des familiers
|
||||
|
||||
Branche `codex/familiar-personality-beta071`.
|
||||
|
||||
## Comportement
|
||||
|
||||
Chaque œuf individualisé possède une personnalité de combat, visible dans
|
||||
son infobulle et sa fiche de familier. Les ordres restent sur H, la technique
|
||||
sur G et l’échange des mains sur F.
|
||||
|
||||
| Personnalité | Réaction spontanée | Poursuite | Cadence d’initiative |
|
||||
| --- | --- | --- | --- |
|
||||
| Agressif | Engage les monstres proches, accompagne les attaques et riposte | ×1,55 | délai ×0,90 |
|
||||
| Protecteur | Accompagne les attaques et défend le joueur, l’allié protégé et lui-même | ×1,45 | délai habituel |
|
||||
| Passif | Riposte seulement lorsqu’il est frappé | ×1,35 | délai ×1,05 |
|
||||
| Pacifiste | S’éloigne du danger, sans attaque automatique | ×1,35 sur ordre | délai habituel |
|
||||
|
||||
**Tous obéissent à un ordre explicite d’attaque**, y compris le pacifiste.
|
||||
Une autre menace ne remplace pas une cible désignée tant qu’elle reste valide.
|
||||
Un Enderman ou un Piglin zombifié non engagé est laissé tranquille ; les
|
||||
familiers et les joueurs ne déclenchent pas cette recherche de proximité. Le PvP conserve ses règles et les
|
||||
duels leur consentement. Suivre suspend les réactions automatiques cinq
|
||||
secondes ; Rappeler ou le mode travail interrompent l’engagement spontané.
|
||||
|
||||
Les vitesses sont des multiplicateurs de navigation : la poursuite utilisait
|
||||
auparavant ×1,15. L’approche gagne donc environ 17 à 35 % selon le caractère,
|
||||
sans changer la vitesse de promenade, celle des montures ni les dégâts.
|
||||
Les attaques à distance gardent leur distance utile. Les feintes au contact
|
||||
cessent suffisamment tôt pour entrer réellement à portée. Les préparations
|
||||
d’attaque restent lisibles : leurs durées ne sont pas raccourcies.
|
||||
|
||||
Les espèces de soutien conservent leurs techniques signature. Un villageois
|
||||
agressif, ou un soutien explicitement envoyé attaquer, dispose d’un coup au
|
||||
contact (2 dégâts de base, préparation 0,25 s, intervalle 2 s), sans exiger un
|
||||
bouclier dans l’inventaire du joueur. Les techniques offensives natives des
|
||||
espèces restent utilisées.
|
||||
|
||||
## Individus et tendances
|
||||
|
||||
Le tirage utilise l’UUID déjà enregistré et l’identifiant d’espèce. Sa fonction
|
||||
et son sel sont figés dans `FamiliarPersonality`. Il ne dépend ni du nom, ni
|
||||
de la taille, ni de l’XP, ni du propriétaire ou de la reconnexion. Les anciens
|
||||
œufs identifiés reçoivent donc un caractère stable sans conversion de fichier.
|
||||
Un œuf sans identité le découvre à sa première individualisation habituelle.
|
||||
|
||||
| Tendance d’espèce | Agressif | Protecteur | Passif | Pacifiste |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Vindicateur et espèces farouches listées dans le code | 70 % | 20 % | 7 % | 3 % |
|
||||
| Villageois et espèces paisibles listées dans le code | 5 % | 15 % | 25 % | 55 % |
|
||||
| Autres espèces et futurs profils sans règle spécifique | 25 % | 40 % | 25 % | 10 % |
|
||||
|
||||
Toutes gardent des exceptions. Le tempérament historique hors combat
|
||||
(curieux, calme, joueur), les souvenirs, les identifiants, les 88 profils et
|
||||
les schémas de sauvegarde restent intacts. Changer ces seuils ultérieurement
|
||||
changerait le caractère d’œufs existants : cela demanderait un contrat de
|
||||
migration, pas une simple retouche d’équilibrage.
|
||||
|
||||
## Araignées
|
||||
|
||||
L’araignée et l’araignée venimeuse utilisent la navigation d’escalade native.
|
||||
En monture colossale, avancer contre une paroi les fait grimper à environ
|
||||
quatre blocs par seconde. Relâcher ou s’écarter permet de redescendre. La
|
||||
vérification porte sur le corps de l’araignée : un plafond touché par le joueur
|
||||
ne déclenche pas une ascension. Les collisions du corps et de la tête du
|
||||
cavalier restent actives. Un espace vide n’autorise aucune montée.
|
||||
|
||||
L’éligibilité à la monte reste celle des familiers colossaux ; les petits
|
||||
familiers se portent selon les règles existantes.
|
||||
|
||||
## Effets des montures natives
|
||||
|
||||
Audit du code de Minecraft **26.3-pre-2** : `AbstractHorse` et ses variantes,
|
||||
`Camel`/`CamelHusk`, `Pig`, `Strider`, `HappyGhast` et
|
||||
`AbstractNautilus`/`Nautilus`/`ZombieNautilus`. Les sources de comparaison
|
||||
sont extraites du JAR utilisé par le build dans `build/native071-source/`.
|
||||
|
||||
| Espèces natives pouvant recevoir un cavalier | Effet transmis au cavalier | Trait de déplacement |
|
||||
| --- | --- | --- |
|
||||
| Cheval, âne, mule, cheval squelette, cheval zombie | Aucun effet d’état natif | Déplacement et saut terrestres |
|
||||
| Lama, lama de marchand | Aucun | Montables, mais pas pilotables en vanilla |
|
||||
| Dromadaire, dromadaire momifié | Aucun | Déplacement terrestre ; dash natif distinct d’un effet d’état |
|
||||
| Cochon | Aucun | Direction à la carotte sur bâton en vanilla |
|
||||
| Strider | Aucun bonus de résistance au feu au joueur | Appui à la surface de la lave |
|
||||
| Ghast joyeux | Aucun | Vol |
|
||||
| Nautilus, Nautilus zombie | **Souffle du Nautilus** | Nage et dash natifs |
|
||||
|
||||
Le Souffle du Nautilus est ajouté au moment de monter et entretenu tant que
|
||||
le joueur reste sur son Nautilus colossal. Il utilise l’effet Minecraft
|
||||
original : durée de 60 ticks, renouvellement toutes les 40 ticks, niveau zéro.
|
||||
L’air restant est figé sous l’eau, sans être rempli comme avec une potion.
|
||||
Après la descente, l’effet expire naturellement sous trois secondes ; il
|
||||
n’est pas supprimé brutalement au risque d’effacer une autre source.
|
||||
|
||||
Il fonctionne pour les deux espèces, sur toute la plage colossale existante
|
||||
(2 500 à 6 000 ‰, y compris les plus grands « titans »), sans achat de
|
||||
technique ou condition de personnalité. Porter simplement l’œuf ou le petit
|
||||
familier sur sa tête ne donne pas cet effet de **cavalier**.
|
||||
|
||||
Le Strider retrouve le support de collision de lave et la remontée natifs,
|
||||
avec une infobulle dédiée. Il maintient son cavalier au-dessus de la surface ;
|
||||
ce n’est pas une immunité personnelle à la lave après avoir sauté du Strider.
|
||||
|
||||
Les autres espèces n’appliquent pas de potion native à leur cavalier : aucune
|
||||
n’a été inventée. Les commandes Sanctuary de ces montures restent communes,
|
||||
comme demandé lors de leur introduction. Cet audit des traits ne porte pas
|
||||
les selles, harnais, coffres d’équidés, sièges supplémentaires, charges de saut
|
||||
ou commandes de dash propres aux interfaces natives. Les pouvoirs familiers
|
||||
existants restent disponibles selon leurs règles.
|
||||
|
||||
## Coût et validation
|
||||
|
||||
La recherche agressive est locale, dans les entités déjà chargées à huit
|
||||
blocs du propriétaire, une fois par seconde, décalée entre familiers. Aucun
|
||||
chargement de chunk. La navigation reste limitée à une demande de chemin
|
||||
par dix ticks. Les décisions et les dégâts font autorité côté serveur.
|
||||
|
||||
Le parcours `Familiar071ClientChecks` réussit en **2 min 23 s** dans un
|
||||
nouveau monde plat de développement, avec le client et son serveur intégré :
|
||||
|
||||
- 352 000 UUID répartis entre les 88 espèces ; quatre caractères possibles,
|
||||
tendances villageois/vindicateur, stabilité après changement d’XP et de souvenirs.
|
||||
- Quatre personnalités de loups : réactions aux événements, ordre explicite,
|
||||
navigation jusqu’à la cible et dégâts réels. Verrouillage de combat maintenu
|
||||
même pour un familier qui refuse l’engagement spontané.
|
||||
- Villageois agressif : initiative spontanée et dégâts sans bouclier ; la cible
|
||||
manuelle garde sa priorité. Fiche FR/EN et nom du familier inspectés.
|
||||
- Araignée et araignée venimeuse colossales : touches natives, ascension du mur,
|
||||
collision du cavalier avec le plafond et absence de montée dans l’espace vide.
|
||||
- Strider colossal : déplacement avec les touches natives à la surface d’un
|
||||
bassin de lave, cavalier hors de la lave et sans potion de résistance au feu.
|
||||
- Nautilus et Nautilus zombie : tailles 2 500 et 6 000 ‰, cavalier effectivement
|
||||
immergé, air figé à 40 pendant 90 ticks, renouvellement de l’effet, expiration
|
||||
et reprise de la consommation après la descente. Équiper l’œuf seul ne le donne pas.
|
||||
- Fermeture puis réouverture réelle du monde : même UUID et même personnalité.
|
||||
|
||||
Les captures sont dans `build/familiar071-evidence/`, le journal dans
|
||||
`build/familiar071-client.log`. La matrice de personnalité couvre les 88
|
||||
profils ; elle ne constitue pas un nouveau playtest de toutes leurs techniques.
|
||||
Pas de validation à deux clients distants ou sur serveur dédié dans cette passe.
|
||||
|
||||
La construction de la distribution est effectuée dans
|
||||
`build/familiar071-workspace/` : les sources de la release beta.070, puis les
|
||||
changements ciblés de la 71. Cette copie évite de compiler les fichiers de
|
||||
statues de la beta.072 ajoutés simultanément dans le dossier partagé.
|
||||
`build/familiar071-source-snapshot.json` fixe les empreintes de cette copie.
|
||||
Les sources courantes du familier conservent les mêmes changements.
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en **5 min 50 s**,
|
||||
124 tâches (108 exécutées, 16 à jour), dans cette copie isolée. Le serveur
|
||||
dédié `:sanctuary:runGameTest` reste exclu conformément au refus d’accepter
|
||||
son EULA ; le parcours natif utilise uniquement le serveur intégré du client.
|
||||
|
||||
Les deux exports MRpack sont vérifiés : version exacte, dépendances figées,
|
||||
JAR comparé aux **1 651 classes compilées** et aux ressources traitées,
|
||||
égalité normal/Test sauf module de test, absence de classes de test dans le
|
||||
mod livré. Seuls les neuf fichiers Java prévus et douze libellés par langue
|
||||
changent par rapport à beta.070 ; les schémas, le catalogue, JEI, le code de
|
||||
Demeure et les 150 fichiers du resource pack restent identiques. Le manifeste
|
||||
de Demeure suit seulement le nouveau numéro du pack.
|
||||
|
||||
Reçu et empreintes : `build/familiar071-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.071.mrpack), 9 462 323 octets.
|
||||
SHA-256 : `1ab36663fe24313597c5328170a3eb2ca2e33bfdfc4c6fbc85c1f5c03102628c`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.071.mrpack), 9 481 258 octets.
|
||||
SHA-256 : `06309f4cd382053fb42d80c3f131152932e07523ace799c50d1d899026029a92`.
|
||||
|
||||
Le resource pack demeure beta.070, actif par défaut et désactivable. Aucun
|
||||
monde personnel, aucune instance Prism et aucun canal public ne sont modifiés.
|
||||
@@ -0,0 +1,67 @@
|
||||
# beta.079 — ordres et défense des familiers en solo
|
||||
|
||||
Branche `codex/familiar-solo-beta079`, Minecraft 26.3-pre-2, textures beta.070.
|
||||
|
||||
Signalement : les familiers paraissent passifs en solo ; H ouvre le menu,
|
||||
mais les ordres ne semblent pas exécutés. Espèces citées : slime, loup, poule.
|
||||
|
||||
## Causes reproduites
|
||||
|
||||
L’ouverture du menu H envoie `refresh`. Cette lecture consommait la même
|
||||
limite de fréquence que les ordres : un clic immédiatement après l’ouverture
|
||||
était rejeté sans message. La mise à jour possède désormais sa propre limite,
|
||||
sans affaiblir celle des actions. Le test natif échouait sur Rappeler avant
|
||||
cette correction et réussit ce passage après séparation des requêtes.
|
||||
|
||||
Suivre suspendait pendant cinq secondes toutes les réactions automatiques,
|
||||
y compris la défense du joueur attaqué. Le délai ne concerne désormais que
|
||||
les initiatives spontanées. Une personnalité qui défend son propriétaire
|
||||
ou riposte peut réagir immédiatement aux dégâts. Le pacifiste conserve son
|
||||
comportement ; l’ordre explicite de protection reste prioritaire.
|
||||
|
||||
Les ordres du menu confirment leur réception : suivi, attaque, protection
|
||||
ou rappel. Une attaque sans familier disponible, sur une cible refusée ou
|
||||
avec un familier porté/monté donne une explication au lieu d’une confirmation
|
||||
d’attaque inexécutable. FR/EN disponibles. H bref et H maintenu sont conservés.
|
||||
|
||||
Le mode utilitaire continue de suspendre les attaques autonomes. La santé,
|
||||
les UUID, les personnalités, les capacités et les sauvegardes sont conservés.
|
||||
Les nouveaux compteurs de requêtes sont temporaires côté serveur ; aucune
|
||||
migration ni modification d’un monde personnel.
|
||||
|
||||
## Vérification
|
||||
|
||||
Deux reproductions natives avant correction : rejet de Rappeler juste après
|
||||
l’ouverture de H ; absence de défense d’un loup passif juste après Suivre.
|
||||
Le scénario utilise un nouveau monde plat solo de développement, sans
|
||||
commandes, sans droits opérateur et sans achat du Lien actif.
|
||||
|
||||
`Solo079ClientChecks` réussit en **1 min 53 s** : rappel immédiatement après
|
||||
l’ouverture de H, retour au suivi, garde du joueur, attaques ordonnées et
|
||||
défense immédiate après Suivre pour le loup, le slime et la poule. Les tests
|
||||
attendent le déplacement et la frappe, contrôlent la baisse de santé et
|
||||
identifient le familier comme auteur du dégât, pour exclure le soleil ou les
|
||||
autres dégâts environnementaux. Les confirmations sont reçues côté client.
|
||||
|
||||
`Autonomy074ClientChecks` réussit en **1 min 58 s** : quatre personnalités,
|
||||
garde, vue et obstacles, cibles neutres, priorité des ordres, permissions,
|
||||
mode utilitaire, délai avant les poursuites spontanées, H bref/maintenu,
|
||||
touche reconfigurée et échange des mains avec F. Fiches FR/EN vérifiées.
|
||||
|
||||
Journaux : `build/solo079-client-final.log`, `build/solo079-regression.log`.
|
||||
Captures : `build/solo079-evidence/`.
|
||||
`check build assemblePack assembleTestPack` réussit en **2 min 21 s**,
|
||||
124 tâches, avec `-x :sanctuary:runGameTest` conformément au refus du serveur
|
||||
dédié. Les 1 693 classes du JAR correspondent au build ; seules
|
||||
`FamiliarBattle` et son état temporaire changent par rapport à beta.078.
|
||||
Deux nouveaux libellés par langue, ressources et textures inchangées.
|
||||
Sources vérifiées, même JAR embarqué dans les deux packs ; archives beta.078
|
||||
conservées à l’identique. Reçu : `build/solo079-artifact.json`.
|
||||
Aucun déploiement personnel ; les essais utilisent le serveur intégré.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.079.mrpack](../build/Sanctuary-beta.079.mrpack).
|
||||
SHA-256 : `7df042d720e2511310387183efcc432cafec4786623e530e5552fd2ba36ca7db`.
|
||||
- [Sanctuary-Test-beta.079.mrpack](../build/Sanctuary-Test-beta.079.mrpack).
|
||||
SHA-256 : `0ca193b2918c7d650503c2ce2fa97d2b3c626fee5ddd99df0cf51ea05a2bb82e`.
|
||||
@@ -0,0 +1,81 @@
|
||||
# beta.062 — soleil et chapeaux des familiers
|
||||
|
||||
Branche `codex/familiar-sunlight-beta062`.
|
||||
|
||||
Le familier reprend la sensibilité solaire du type natif de son œuf : tag
|
||||
`minecraft:burn_in_daylight`, avec respect de l'immunité native au feu. La
|
||||
vérification utilise le moteur Minecraft 26.3-pre-2 : lumière, ciel dégagé,
|
||||
eau/pluie/neige poudreuse et attribut environnemental `MONSTERS_BURN`.
|
||||
Les bébés familiers concernés brûlent aussi. Les autres espèces conservent
|
||||
leur comportement actuel.
|
||||
|
||||
Tout objet équipé dans l'emplacement de tête Sanctuary protège des nouvelles
|
||||
inflammations solaires, sur un mob ordinaire comme sur un familier. Maj + clic
|
||||
droit avec un objet l'équipe ; Maj + clic droit à main vide le récupère.
|
||||
L'armure de tête native conserve sa protection et son usure habituelles quand
|
||||
aucun chapeau Sanctuary n'est équipé. Un chapeau ne rend pas résistant au feu :
|
||||
une flamme déjà allumée termine sa durée native, ou s'éteint dans l'eau.
|
||||
|
||||
Les familiers sensibles perdent leur santé de combat en brûlant, jusqu'au K.-O.
|
||||
habituel, sans détruire l'œuf. Leur feu reste dangereux pendant un duel ; les
|
||||
attaques extérieures au duel restent exclues. Les flammes suivent la taille
|
||||
visible du compagnon. Une indication FR/EN sur les œufs concernés explique le
|
||||
besoin d'un chapeau, sans achat préalable d'une aptitude.
|
||||
|
||||
Aucun identifiant ni format de sauvegarde ne change. Le chapeau utilise
|
||||
l'équipement existant : il est rendu au sol quand le familier disparaît et
|
||||
n'est pas stocké dans son œuf. Aucun monde existant ni canal n'est modifié.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`Sunlight062ClientChecks` passe dans le client natif 26.3-pre-2, sur un monde
|
||||
plat de développement neuf :
|
||||
|
||||
- 88 œufs exposés au moteur solaire natif : zombies, villageois zombies,
|
||||
noyés, chevaux zombies, nautiles zombies, squelettes, vagabonds, embourbés
|
||||
et phantoms brûlent ; les autres conservent leur immunité, dont le Wither
|
||||
squelette naturellement résistant au feu ;
|
||||
- mob ordinaire avec pierre, diamant, bâton et torche en chapeau, protection
|
||||
et usure du casque natif conservées, reprise du feu après retrait ;
|
||||
- vrai paquet Maj + clic droit pour équiper puis retirer un diamant de la
|
||||
tête du familier, synchronisation de l'objet sur son modèle côté client ;
|
||||
- santé persistée qui baisse sur les vrais ticks de feu, feu déjà allumé qui
|
||||
expire sous un chapeau, absence de nouvelle inflammation ;
|
||||
- toit, nuit, pluie et immersion ; l'eau éteint aussi les flammes existantes ;
|
||||
- K.-O. déclenché par un tick natif de brûlure, identité et œuf conservés ;
|
||||
- brûlure et fin normale d'un duel au K.-O., refus des coups directs d'un
|
||||
joueur extérieur et des autres dégâts environnementaux pendant le duel ;
|
||||
- flammes synchronisées aux dimensions visibles du familier bébé, capture
|
||||
inspectée dans `build/sun062-evidence/sunburn.png`, indication FR/EN.
|
||||
|
||||
Journal `build/sun062-client.log` : **55 s**, marqueurs
|
||||
`SUNLIGHT062_SPECIES_PASS` et `SUNLIGHT062_PASS`. La première tentative de
|
||||
lancement a été arrêtée au blocage OpenGL de synchronisation verticale ;
|
||||
le parcours réussi utilise `-PsanctuaryClientNoVsync=true` et le réglage VSync
|
||||
uniquement dans le dossier du client de développement.
|
||||
|
||||
L'essai de duel utilise deux entités serveur avec une connexion simulée pour
|
||||
la seconde ; aucun essai avec deux clients humains n'est revendiqué. La
|
||||
capture inspectée est celle du zombie bébé ; le rendu individuel des huit
|
||||
autres espèces sensibles n'a pas été revu visuellement.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuarySunlight062ClientTests=true
|
||||
-PsanctuaryQuickTests=true` réussit : **122 tâches**, **2 min 10 s**.
|
||||
Le parcours client est exécuté séparément par `:sanctuary:runClientGameTest`
|
||||
avec les mêmes propriétés et `-PsanctuaryClientNoVsync=true`.
|
||||
Aucun EULA de serveur dédié n'a été accepté.
|
||||
|
||||
## Archives locales
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.062.mrpack), 5678508 octets.
|
||||
SHA-256 : `3eb3729023bcb1f6931198d50694ecac1fee1c660aeb0cf1edc9776aebbfe2fa`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.062.mrpack), 5697441 octets.
|
||||
SHA-256 : `f71dd47ef228d2acab99fe9b7b58e11d4f39234f926d7a313956bdd69e9cd2b6`.
|
||||
|
||||
Les **1624 classes** embarquées correspondent au build. Le reçu
|
||||
`build/sun062-artifact.json` vérifie le contenu des archives, les différences
|
||||
ciblées avec beta.061, les dépendances imbriquées et la conservation des
|
||||
archives précédentes. JEI, la génération et les autres ressources du pack
|
||||
restent identiques. Aucun canal publié, instance personnelle ni monde
|
||||
existant n'est modifié.
|
||||
@@ -0,0 +1,60 @@
|
||||
# beta.085 — textures du Fourneau
|
||||
|
||||
Les trois images originales de 48 × 48 pixels fournies par le créateur sont
|
||||
conservées, octet pour octet, dans `assets/sanctuary/textures/block/fourneau/`.
|
||||
Chaque image couvre une face entière du cube de trois blocs de côté. Les UV
|
||||
des 27 composants prélèvent chacun un tiers de l'image, à la densité native
|
||||
de 16 pixels par bloc. Façade dans l'orientation choisie à l'assemblage ;
|
||||
côtés et arrière avec `side`, dessus et dessous avec `top`.
|
||||
|
||||
## Contrat de compatibilité
|
||||
|
||||
Aucun changement au schéma 1 de `sanctuary:multiblocks`, aux identifiants des
|
||||
blocs ou aux données de leurs inventaires. Les assemblages beta.084 utilisent
|
||||
leur orientation déjà enregistrée. Un four isolé garde le modèle de son pack
|
||||
de ressources. Les nouveaux modèles ne s'appliquent qu'aux fours assemblés.
|
||||
Dissocier ou casser un composant rétablit les modèles ordinaires ; cuisson,
|
||||
combustible et objets restent dans les BlockEntities vanilla.
|
||||
|
||||
Le serveur transmet seulement l'apparence des machines dans les chunks
|
||||
suivis par le joueur : à l'envoi natif du chunk, à l'assemblage et à la
|
||||
dissociation. Aucun paquet par tick de cuisson. Le client invalide les
|
||||
sections concernées et utilise les modèles de blocs natifs via Fabric,
|
||||
avec éclairage, occultation des faces et destruction habituels. Il efface
|
||||
son cache au déchargement d'un chunk, au changement de monde et à la
|
||||
déconnexion. Les modèles sont rebâtis lors du rechargement des ressources.
|
||||
|
||||
Les modèles JSON se régénèrent avec `python3 tools/generate-fourneau-models.py`.
|
||||
Les textures originales ne sont pas générées. Le pack intégré beta.070
|
||||
reste inchangé : ces nouveaux assets appartiennent au mod beta.085.
|
||||
|
||||
## Limites visuelles
|
||||
|
||||
Aucune variante allumée n'a été fournie : l'image reste celle du créateur,
|
||||
y compris pendant la cuisson. L'état lumineux natif du Fourneau est conservé.
|
||||
Le Fût conserve son apparence de barils dans cette livraison.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
- `check build assemblePack assembleTestPack` : tâches réussies dans
|
||||
`build/fourneau085-check.log`. La première extension du test visuel de chauffe
|
||||
surveillait par erreur la case de diamants, qui ne cuit pas ; son attente a
|
||||
échoué après les tâches de construction. Le scénario a été corrigé pour
|
||||
observer le compartiment contenant le minerai, sans changement du code livré.
|
||||
- `Fourneau085ClientChecks` relancé avec succès : nouveau monde plat intégré,
|
||||
quatre orientations, 27 modèles répartis sur quatre chunks, état allumé,
|
||||
rechargement des ressources, rejet d'un paquet d'une autre dimension,
|
||||
déchargement réel puis retour, dissociation, casse et conservation des
|
||||
17 diamants stockés. Journal final : `build/fourneau085-client-final.log`.
|
||||
- Contrôle des trois PNG à l'octet près, des 108 modèles et de leurs UV,
|
||||
des sources embarquées, des archives et de la préservation des assets
|
||||
antérieurs. Reçu : `build/fourneau085-artifact.json`.
|
||||
- Captures natives : `build/fourneau085-screenshots/`, dont
|
||||
`0001_fourneau085-north.png` et `0006_fourneau085-lit.png`.
|
||||
|
||||
Archives locales vérifiées : `build/Sanctuary-beta.085.mrpack` et
|
||||
`build/Sanctuary-Test-beta.085.mrpack`. Les archives beta.084 restent inchangées.
|
||||
Le canal packwiz et l'instance Prism n'ont pas été déployés. Aucun monde
|
||||
personnel modifié, aucune EULA acceptée, aucun serveur dédié lancé : le test
|
||||
utilise uniquement le serveur intégré au client. Le rendu avec deux clients
|
||||
LAN distincts et avec un moteur de shaders tiers reste à éprouver séparément.
|
||||
@@ -0,0 +1,60 @@
|
||||
# beta.088 — Navigation du Fût sans recentrage de souris
|
||||
|
||||
Branche : `codex/fut-navigation-beta088`.
|
||||
|
||||
## Problème et correction ciblée
|
||||
|
||||
Changer de page ou appliquer une recherche remplaçait le menu serveur via
|
||||
`openMenu`. Minecraft envoyait d'abord une fermeture au client, puis une
|
||||
ouverture. Cette fermeture reprenait la souris pour le jeu et la recentrait,
|
||||
obligeant à revenir sur le bouton à chaque clic.
|
||||
|
||||
La transition nettoie désormais l'ancien menu côté serveur avec
|
||||
`doCloseContainer`, sans envoyer l'écran de fermeture intermédiaire. Le
|
||||
nouveau menu est ensuite ouvert normalement : la souris reste libérée et
|
||||
conserve sa position. Cela concerne la pagination et la recherche du Fût,
|
||||
ainsi que les onglets du Fourneau qui utilisent la même navigation.
|
||||
|
||||
Chaque page garde un nouvel identifiant de menu : les paquets de clics ou de
|
||||
navigation de la page précédente sont toujours rejetés. Les contrôles de
|
||||
portée, d'accès, de structure, d'objet porté au curseur et la limite de
|
||||
fréquence restent en place. La fermeture réelle du coffre reste native.
|
||||
Aucun changement de stockage, de sauvegarde, de textures ou de libellés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours natif existant a été étendu avec un contrôle des paquets réseau
|
||||
sur un second joueur de test : sans correction, la navigation envoie un
|
||||
`ClientboundContainerClosePacket` et le test échoue. Avec la correction,
|
||||
aucune fermeture intermédiaire, et une fermeture réelle envoie toujours
|
||||
son paquet. Ce contrôle reste valable si la fenêtre de test est inactive.
|
||||
|
||||
Côté client, le test place réellement le curseur sur le bouton puis clique
|
||||
trois fois sur Suivant et deux fois sur Précédent sans le déplacer. Sa
|
||||
position est vérifiée après chaque page, après une recherche localisée en
|
||||
français et après chacun des trois onglets du Fourneau. Échap ferme le menu
|
||||
normalement côté client et côté serveur.
|
||||
|
||||
Les essais existants passent également : 729 cases, deux joueurs utilisant
|
||||
le même stock, objets au curseur, identifiants périmés, transfert rapide,
|
||||
hoppers, démontage, casse, persistance et cuisson native partagée.
|
||||
Aucun serveur dédié lancé, aucune EULA acceptée, aucun monde personnel touché.
|
||||
|
||||
Journaux : `build/fut088-before-client.log` (régression reproduite) et
|
||||
`build/fut088-client.log` (succès en 39 secondes). Le parcours reste activé
|
||||
par `-PsanctuaryMultiblocks084ClientTests=true` avec
|
||||
`-PsanctuaryClientTests=true -PsanctuaryQuickTests=true`.
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en 2 min 23 s
|
||||
(124 tâches, 103 exécutées), avec le GameTest dédié explicitement exclu.
|
||||
Journal : `build/fut088-check.log`. Le reçu `build/fut088-artifact.json`
|
||||
vérifie les sources, le JAR, les archives normal/Test et la conservation de
|
||||
tous les assets beta.087. Seules `MultiblockMenu` et sa classe interne de
|
||||
transfert changent dans les classes compilées (décalage des lignes de debug
|
||||
pour cette dernière).
|
||||
|
||||
Livraison locale : `build/Sanctuary-beta.088.mrpack` et
|
||||
`build/Sanctuary-Test-beta.088.mrpack`. Les archives beta.087 restent intactes.
|
||||
Aucun déploiement dans l'instance Prism ni sur le canal publié. Un essai LAN
|
||||
avec deux clients distincts reste à réaliser ; le test d'accès partagé utilise
|
||||
un second joueur serveur dans le client de développement.
|
||||
@@ -0,0 +1,51 @@
|
||||
# beta.086 — textures du Fût
|
||||
|
||||
Le Fût assemblé reçoit les trois images originales de 48 × 48 pixels :
|
||||
côtés, dessus et dessous. Chacune couvre une face entière de trois blocs
|
||||
sur trois, avec 16 pixels par bloc et sans retouche de l'image.
|
||||
|
||||
## Contrat avant implémentation
|
||||
|
||||
Aucun changement de sauvegarde : les 27 barils et leurs 729 cases restent
|
||||
vanilla, avec le même registre d'appartenance et les mêmes données. Seul le
|
||||
modèle affiché change quand le Fût est assemblé. Les barils indépendants
|
||||
conservent le modèle du pack de ressources actif. Dissociation et casse
|
||||
rétablissent leur apparence. Les textures du Fourneau beta.085 sont conservées.
|
||||
|
||||
Le Fût ajoute un paquet client `sanctuary:fut_appearance_v1` ; le paquet
|
||||
du Fourneau reste inchangé. Les deux utilisent la même mise en cache visuelle
|
||||
par chunk et le même rendu natif. Aucun paquet supplémentaire par tick,
|
||||
aucun stockage parallèle d'objets. Le pack intégré beta.070 reste inchangé ;
|
||||
les textures du Fût appartiennent aux assets du mod beta.086.
|
||||
|
||||
## Rendu et vérifications
|
||||
|
||||
Les modèles JSON se régénèrent avec `python3 tools/generate-fut-models.py`.
|
||||
Les PNG sources sont conservés dans `assets/sanctuary/textures/block/fut/`.
|
||||
Aucune variante ouverte n'a été fournie : le Fût conserve le même dessus
|
||||
pendant l'accès à son inventaire.
|
||||
|
||||
`Fut086ClientChecks` a réussi dans un nouveau monde plat intégré : Fût et
|
||||
Fourneau voisins, modèles distincts, raccords des côtés/dessus/dessous,
|
||||
ouverture et prélèvement dans les 729 cases, rechargement des ressources,
|
||||
déchargement réel puis retour dans quatre chunks, dissociation et casse.
|
||||
Les objets conservés restent dans les mêmes barils physiques ; retirer la
|
||||
façade du Fût ne retire pas celle du Fourneau voisin.
|
||||
|
||||
Journal du client : `build/fut086-client.log`. Captures natives :
|
||||
`build/fut086-screenshots/`, notamment `0001_fut086-top-side.png` et
|
||||
`0002_fut086-bottom-side.png`. Le Fût est suspendu dans cette scène de test
|
||||
pour que le dessous puisse être inspecté.
|
||||
|
||||
`check build assemblePack assembleTestPack` a réussi en 2 min 36 s
|
||||
(124 tâches, 104 exécutées), sans serveur dédié ; journal
|
||||
`build/fut086-check.log`. Le reçu `build/fut086-artifact.json` vérifie
|
||||
l'identité des PNG, les 27 modèles, les sources embarquées et les archives,
|
||||
ainsi que la conservation de tous les assets beta.085 du Fourneau.
|
||||
|
||||
Livraison locale : `build/Sanctuary-beta.086.mrpack` et
|
||||
`build/Sanctuary-Test-beta.086.mrpack`. Les archives beta.085 restent
|
||||
immuables. Aucun déploiement sur Prism ou sur le canal publié, aucun monde
|
||||
personnel modifié, aucune EULA acceptée. Les vérifications multijoueur
|
||||
avec deux clients distincts et avec un moteur de shaders tiers restent
|
||||
à effectuer séparément.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Synchronisation Git jusqu’à beta.092
|
||||
|
||||
Ticket du 16 septembre 2026, branche `codex/git-sync-beta092`.
|
||||
|
||||
Le dépôt distant était resté sur [beta.061](git-sync-beta061.md). Cette
|
||||
synchronisation enregistre les sources, ressources, outils et comptes rendus
|
||||
locaux jusqu’à la livraison **beta.092**, pour **Minecraft 26.3**. Elle inclut
|
||||
le commit déjà local du resource pack beta.070. `main` avance directement et
|
||||
le tag exact `beta.092` désigne les sources de la livraison vérifiée.
|
||||
|
||||
Le compteur reste à beta.092 : ce rattrapage n’ajoute aucun changement de jeu.
|
||||
Les documents de conception restent des intentions, selon leurs contrats.
|
||||
Les anciens tags et les posts de release existants sont conservés. Les versions
|
||||
intermédiaires ne reçoivent pas artificiellement un tag pointant sur beta.092.
|
||||
|
||||
## Sources et vérifications
|
||||
|
||||
Une copie des 2 636 fichiers sources non ignorés est figée dans un worktree
|
||||
isolé, sans modifier les fichiers de travail. Les JAR générés, archives ZIP,
|
||||
MRpack, sauvegardes et dépendances téléchargées restent ignorés ; seul le
|
||||
wrapper Gradle est versionné. Les références courantes des README sont mises
|
||||
à jour, avec ce compte rendu.
|
||||
|
||||
Les journaux et le reçu de la [livraison beta.092](multiblock-pick-beta092.md)
|
||||
confirment :
|
||||
|
||||
- `./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussi en **3 min 1 s**, **124 tâches**.
|
||||
- Parcours client natif `Pick092ClientChecks` réussi en **1 min 19 s**, avec
|
||||
`PICK092_PASS` : **864 états**, douze combinaisons normal/gluant et six
|
||||
directions, vrais clics molette, inventaire de survie, parties mobiles et
|
||||
Ctrl + clic sans données techniques.
|
||||
- Les empreintes des deux MRpack locaux correspondent au reçu de livraison.
|
||||
|
||||
La copie destinée à Git est reconstruite sous Java 25 avec :
|
||||
|
||||
```sh
|
||||
./gradlew assemblePack assembleTestPack \
|
||||
-x :sanctuary:check -x :demeure:check \
|
||||
-x :jei:check -x :sanctuary-test:check
|
||||
```
|
||||
|
||||
Réussite en **48 s**, **23 tâches**. Les contrôles déjà réussis sur la livraison
|
||||
ne sont pas relancés pour cette copie. La vérification des manifestes packwiz
|
||||
et des ressources graphiques passe pendant l’assemblage. Les quatre JAR
|
||||
reconstruits sont **identiques octet pour octet** à ceux de la livraison :
|
||||
|
||||
| JAR | Entrées | Classes |
|
||||
| --- | ---: | ---: |
|
||||
| Sanctuary beta.092 | 2 960 | 1 753 |
|
||||
| Demeure beta.092 | 24 | 14 |
|
||||
| Sanctuary Test beta.092 | 25 | 3 |
|
||||
| JEI 30.32.0-sanctuary.3 pour 26.3 | 1 378 | 1 101 |
|
||||
|
||||
Les 811 fichiers Java des trois modules Sanctuary, Demeure et Test correspondent
|
||||
également aux archives de sources de la livraison. Les ressources, métadonnées
|
||||
et dépendances imbriquées sont incluses dans la comparaison des JAR.
|
||||
|
||||
SHA-256 du JAR Sanctuary :
|
||||
`8eb91eb41d0236ac6d6d064e9638c99802d28a2d83b8aef5ff1291c9495bcc8c`.
|
||||
|
||||
Reçus locaux ignorés : `build/git-sync-beta092-verification.json`,
|
||||
`build/git-sync-beta092-assemble.log`, `build/pick092-artifact.json`,
|
||||
`build/pick092-check.log` et `build/pick092-client.log`.
|
||||
|
||||
## Limites et distribution
|
||||
|
||||
Les essais beta.092 utilisent un client macOS et son serveur intégré ; pas de
|
||||
session LAN à deux clients. Le serveur GameTest dédié est exclu et aucune EULA
|
||||
n’est acceptée. La dernière exécution générale consignée dans le rattrapage
|
||||
[beta.060](git-sync-beta060.md#vérifications) comptait 235 réussites sur 246,
|
||||
avec 11 échecs à trier ; elle ne constitue pas une validation de beta.092.
|
||||
Cette synchronisation ne prétend pas résoudre ces échecs ni valider Windows/Linux.
|
||||
|
||||
Les sources et leur tag sont publiés sur Git. Les archives bêta restent locales,
|
||||
le canal packwiz reste sur alpha.30.7 et aucune instance Prism, sauvegarde
|
||||
personnelle ou installation de serveur n’est modifiée.
|
||||
@@ -0,0 +1,65 @@
|
||||
# beta.066 — cosmétiques animés sans doublon
|
||||
|
||||
Branche `codex/cosmetic-render-beta066`.
|
||||
|
||||
## Contrat
|
||||
|
||||
Un coffre porté n'affiche qu'un coffre : son couvercle s'ouvre et se ferme
|
||||
sans laisser un second coffre fermé dessous. Une cloche portée conserve
|
||||
son support et une seule cloche qui oscille. Le même correctif s'applique
|
||||
aux modèles partagés avec les mobs portant un cosmétique.
|
||||
|
||||
Minecraft 26.3 distingue la géométrie du bloc posé de son modèle isolé.
|
||||
Ce dernier peut déjà inclure un coffre fermé, une cloche fixe ou un livre.
|
||||
Sanctuary dessinait ce modèle isolé puis le rendu animé de l'entité de bloc.
|
||||
Le rendu avec une entité de bloc utilise désormais la géométrie native du
|
||||
bloc posé pour ses parties fixes, avec les teintes et la lumière du bloc,
|
||||
puis l'entité de bloc pour les parties animées.
|
||||
|
||||
Les panneaux conservent leur modèle isolé : Sanctuary soumet séparément
|
||||
leur texte, pas une deuxième planche. Les bannières et les têtes conservent
|
||||
leur rendu personnalisé ; les lits et portes gardent leurs deux moitiés.
|
||||
Le feu de camp conserve sa base et les aliments synchronisés. Aucun asset
|
||||
n'est remplacé. Aucun changement serveur, d'interaction, de stockage,
|
||||
de sauvegarde ou de génération.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le parcours `Cosmetic066ClientChecks` crée un monde plat de développement,
|
||||
utilise les modèles et le collecteur de rendu natifs, et reçoit les événements
|
||||
du serveur intégré. Avant correction, le test reproduit deux soumissions
|
||||
`ChestModel` pour un seul cosmétique ouvert. Après correction, les
|
||||
contrôles n'en comptent plus qu'une.
|
||||
|
||||
Le parcours client réussit en 40 secondes :
|
||||
|
||||
- coffres normal, piégé, de l'Ender et Shulker : une seule représentation,
|
||||
ouverture et fermeture issues des événements serveur ;
|
||||
- cloche : un seul modèle animé, un support fixe, oscillation puis arrêt ;
|
||||
- feu de camp : base conservée et aliment reçu du serveur visible ;
|
||||
- table d'enchantement : un seul livre sur sa base ; panneaux, bannière,
|
||||
tête de joueur, lit complet, porte complète, four et étagère conservés ;
|
||||
- douze cas de coffres/cloches sur poules, vaches et zombies adultes ou bébés.
|
||||
|
||||
Le test compte les soumissions réelles de modèles, pas seulement la présence
|
||||
d'un état d'animation. Les captures avant/après du coffre et celle de la cloche corrigée ont
|
||||
été relues dans `build/cosmetic066-evidence/`.
|
||||
|
||||
Journaux : `build/cosmetic066-before.log`, `build/cosmetic066-client.log`.
|
||||
`./gradlew check build assemblePack assembleTestPack` réussit en 2 min 22 s
|
||||
avec `-x :sanctuary:runGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryQuickTests=true`. Le parcours client dédié utilise aussi
|
||||
`-PsanctuaryCosmetic066ClientTests=true -PsanctuaryClientNoVsync=true`.
|
||||
Les tests avec serveur dédié restent exclus ; la validation des animations
|
||||
utilise le serveur intégré du client.
|
||||
|
||||
Les deux archives sont vérifiées contre les 1 645 classes et les ressources
|
||||
compilées. Seul `HeadCosmeticModels` change dans les sources principales
|
||||
par rapport à beta.065. Les textures, JEI et les archives beta.065 restent
|
||||
identiques. Le code de Demeure reste identique ; sa version suit le pack.
|
||||
Reçu : `build/cosmetic066-artifact.json` ; build : `build/cosmetic066-check.log`.
|
||||
|
||||
[Pack normal](../build/Sanctuary-beta.066.mrpack) ·
|
||||
[Monde plat rapide](../build/Sanctuary-Test-beta.066.mrpack).
|
||||
Archives locales, sans publication du canal ni déploiement personnel.
|
||||
Une session avec plusieurs clients distants n'a pas été relancée.
|
||||
@@ -0,0 +1,68 @@
|
||||
# beta.068 — animation discrète du préchargement
|
||||
|
||||
Branche `codex/loading-animation-beta068`, ticket LOAD-03.
|
||||
|
||||
## Comportement
|
||||
|
||||
Une ligne de neuf glyphes SGA, encadrée de crochets ASCII, accompagne les
|
||||
étapes réelles de préparation. Elle utilise la police d’enchantement native
|
||||
`minecraft:alt` : les lettres de Sanctuary, avec une lumière grise/blanche
|
||||
qui parcourt la ligne dans les deux sens en 3,2 secondes. Le fond uni et les
|
||||
textes Minecraft restent sobres. Aucun pourcentage fictif ni nouvelle texture.
|
||||
|
||||
L’animation dépend de l’horloge monotone lue pendant chaque rendu, pas des
|
||||
ticks : elle continue pendant les longs calculs d’une même phase. Les glyphes
|
||||
sont préparés une seule fois, sans sondage de terrain ni travail serveur.
|
||||
|
||||
La même ligne accompagne ensuite le téléchargement natif du terrain, sous la
|
||||
carte des chunks lorsqu’elle existe. La carte et la barre de progression de
|
||||
Minecraft conservent leur rendu et leur comportement. Si une interface trop
|
||||
petite ne laisse pas de place sous les chunks, l’indicateur est masqué plutôt
|
||||
que de recouvrir la progression. Les transports par portail sont hors de ce
|
||||
changement. L’introduction longue, sa musique, le flash et Hello World restent
|
||||
inchangés, comme le cache dérivé et les étapes publiées par le serveur.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Client natif 26.3-pre-2 réussi en **41 s**. Huit captures pendant une phase
|
||||
identique, avec la méthode `tick()` de l’écran volontairement inactive. Les
|
||||
sept comparaisons de pixels successives diffèrent uniquement dans la ligne
|
||||
de glyphes ; le titre, le fond et l’étape restent identiques.
|
||||
|
||||
Inspection FR/EN dans la fenêtre native 854 × 480, réglages GUI 2/3/4 avec
|
||||
le plafonnement automatique de Minecraft à la taille disponible. La carte des
|
||||
chunks et sa barre native restent visibles ; le message générique hors
|
||||
préparation garde son rendu habituel. Ces écrans de test utilisent des états
|
||||
fixes, sans créer de monde ni simuler un pourcentage dans le code livré.
|
||||
Les caches et les cycles de démarrage n’ont pas été modifiés ni remesurés ici.
|
||||
|
||||
Preuves locales : `build/loading068-client.log`, les captures et la comparaison
|
||||
`build/loading068-evidence/animation-check.json`. Le test ne lance aucun serveur
|
||||
et ne demande aucune acceptation d’EULA.
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/Users/koka/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home \
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryLoading068ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`check build assemblePack assembleTestPack` réussi en **2 min 15 s**, 122 tâches,
|
||||
avec `-x :sanctuary:runGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryQuickTests=true`. Aucun serveur dédié lancé.
|
||||
|
||||
Les **1 647 classes** de l’archive correspondent à la compilation. Seuls
|
||||
`LoadingGlyphs`, `PreloadScreenMixin` et `PreloadTerrainScreenMixin` changent
|
||||
par rapport à beta.067. Intro, Hello World, cache, logique serveur, textures,
|
||||
sons, traductions et JEI restent identiques. Les archives beta.067 gardent
|
||||
leurs empreintes. Reçu : `build/loading068-artifact.json`.
|
||||
|
||||
- [Pack normal beta.068](../build/Sanctuary-beta.068.mrpack), 5726390 octets.
|
||||
SHA-256 : `cfb55d414186e6be5f0e19770049158302d4fbf37c0fa696306acda428594a2e`.
|
||||
- [Monde plat rapide beta.068](../build/Sanctuary-Test-beta.068.mrpack), 5745324 octets.
|
||||
SHA-256 : `643913caf577ee61b23747f378e9a21f5b1a54531901791d47d8ad0a75531dff`.
|
||||
|
||||
Aucun déploiement dans Prism, aucune publication du canal et aucun monde
|
||||
personnel modifié.
|
||||
@@ -0,0 +1,138 @@
|
||||
# beta.064 — cache des plans et préchargement natif
|
||||
|
||||
Branche `codex/loading-cache-beta064`. Reprend l'audit beta.063 et conserve les
|
||||
travaux préexistants (HUD du resource pack et documentation des capes inclus).
|
||||
|
||||
## Contrat du cache dérivé
|
||||
|
||||
Le cache est facultatif et reconstructible dans `cache/sanctuary-plans-v1/`
|
||||
sous le dossier du monde. Il ne change aucun chunk, aucun journal d'expansion,
|
||||
aucune donnée d'habitant ni les paramètres du générateur. Un ancien monde sans
|
||||
cache suit exactement le calcul existant et remplit le cache ; supprimer ce
|
||||
répertoire rétablit le même calcul. Pas de migration de sauvegarde nécessaire.
|
||||
|
||||
Les clés comprennent la graine, le générateur et ses paramètres, l'option
|
||||
structures, les versions des mods et toutes les couches de ressources de
|
||||
génération (`worldgen`, `structure`, `tags`, `dimension`, `dimension_type`) et
|
||||
les modèles NBT locaux de `generated/`. Les
|
||||
plans structurels incluent aussi la liste des îles concernées. Un changement
|
||||
invalide le cache ; une entrée illisible, incomplète ou dont l'empreinte diffère
|
||||
est ignorée puis recalculée. Écriture temporaire, synchronisation et renommage
|
||||
atomique. Aucun objet Minecraft vivant, monde, chunk ou état RNG n'est sérialisé.
|
||||
Les index dérivés sont reconstruits à partir des géométries validées.
|
||||
|
||||
Le cache couvre l'hydrologie régionale (eau, terrasses, écoulements et lave),
|
||||
les traversées, salles souterraines, sanctuaire minier, ruines et patrimoine de
|
||||
la racine, ainsi que les pièces natives du village, des mines, des temples et
|
||||
des ruines asséchées. Ces dernières utilisent le NBT natif de Minecraft, dont
|
||||
la lecture doit restituer exactement la même écriture ; sinon elles sont
|
||||
recalculées. Les pièces restaurées appartiennent au nouveau serveur, sans
|
||||
réutiliser les objets du serveur précédent. Le lecteur vanilla des pièces jigsaw
|
||||
recalcule leurs limites à partir du modèle et perd les extensions réservées aux
|
||||
jonctions : le cache réapplique ces limites enregistrées, puis exige une égalité
|
||||
NBT complète. Cette correction reste locale au lecteur du cache, sans mixin de
|
||||
chargement des chunks ni modification des structures sauvegardées.
|
||||
|
||||
Les plans aériens conservent leur journal persistant existant, qui fait
|
||||
également autorité. Les anciennes générations et les villes des expansions
|
||||
conservent leurs planificateurs actuels ; la livraison cible les plans de
|
||||
l'île Sanctuary actuelle (`generation24`, terrain alpha.30.7).
|
||||
|
||||
## Préchargement
|
||||
|
||||
Texte et étapes issus du travail réel du serveur intégré, fond uni et graphisme
|
||||
Minecraft. Aucun portail du Nether dans cette préparation. L'introduction,
|
||||
Hello World, sa durée, sa musique et le flash restent inchangés. Les étapes de
|
||||
calcul n'affichent pas de pourcentage inventé ; Minecraft fournit sa progression
|
||||
réelle des chunks dès qu'elle est disponible. La préparation locale affiche
|
||||
les étapes publiées par le serveur intégré avant même l'admission réseau.
|
||||
Un client distant ne reçoit pas de faux état d'un serveur qui n'a pas encore
|
||||
ouvert sa connexion : il conserve les étapes natives du téléchargement.
|
||||
Le transport dimensionnel par portail reste un écran Minecraft distinct.
|
||||
|
||||
Les libellés FR/EN suivent le travail : vérification/lecture du cache, eau et
|
||||
lave, traversées, salles, sanctuaire minier, ruines, patrimoine, îlots aériens,
|
||||
village, mines, temples, enregistrement et point d'arrivée. Deux lignes
|
||||
centrées sur fond uni remplacent l'attente générique ; aucune texture ajoutée.
|
||||
La progression native des chunks et ses conditions de fermeture restent gérées
|
||||
par Minecraft. Aucun nouveau bouton d'annulation ne force un arrêt au milieu
|
||||
d'un calcul ou d'une écriture.
|
||||
|
||||
## Vérification reproductible
|
||||
|
||||
Client natif 26.3-pre-2, serveur intégré uniquement, monde de développement
|
||||
neuf : graine `42`, île moyenne de diamètre `724`, structures actives,
|
||||
distances de rendu et simulation de `8`. Première arrivée avec les 29,5 secondes
|
||||
d’introduction complètes. Ensuite réouverture du même monde, puis remplacement
|
||||
d’une seule entrée de cache par un contenu tronqué et nouvelle réouverture.
|
||||
|
||||
Le contrôle compare les géométries des plans, l’eau et la lave, les index par
|
||||
colonne et par chunk, les écritures de blocs et de butin des renderers et une
|
||||
grille de points de protection. La lecture des pièces natives exige une égalité
|
||||
NBT complète après restauration. Les essais de fichiers couvrent l’absence,
|
||||
le changement de clé, une copie sous une mauvaise identité, un checksum faux,
|
||||
une validation refusée et un gzip tronqué. Chaque refus retombe sur le calcul.
|
||||
|
||||
L’écran est rendu par Minecraft en FR/EN pour inspection visuelle. Les phases
|
||||
observées pendant le véritable démarrage sont enregistrées indépendamment des
|
||||
ticks du client, qui ne tournent pas pendant sa première boucle de chargement.
|
||||
Mesure locale du 15 septembre 2026, même scénario que l’audit beta.063 :
|
||||
|
||||
| Parcours | Durée jusqu’au jeu |
|
||||
|---|---:|
|
||||
| Première création, cache vide, introduction complète | 122,255 s |
|
||||
| Réouverture avec cache | 2,126 s |
|
||||
| Entrée structurelle corrompue, recalcul automatique | 39,177 s |
|
||||
| Référence beta.063, réouverture sans cache | 58,701 s |
|
||||
|
||||
Il s’agit d’une mesure sur cette machine et cette graine, pas d’une garantie de
|
||||
durée. Le cache accélère surtout les réouvertures ; un monde neuf doit toujours
|
||||
calculer ses plans et préparer ses chunks. Les mises à jour de version ou de
|
||||
ressources invalident volontairement les anciennes entrées. La première
|
||||
réouverture d’un ancien monde sans cache sert à le remplir.
|
||||
|
||||
Réouverture : **3 lectures réussies, 0 échec, 0 écriture**. Récupération :
|
||||
**3 lectures réussies, 1 échec attendu, 1 remplacement**. Comparaison identique
|
||||
des **217 053 écritures de blocs/butin** et de 93 150 points de protection,
|
||||
ainsi que des plans et des index fluides. Les 17 étapes utiles du scénario ont
|
||||
été observées ; les libellés FR/EN et les captures natives sont vérifiés.
|
||||
Preuves locales : `build/loading064-client.log`,
|
||||
`build/loading064-evidence/timings.json` et captures dans le même dossier.
|
||||
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/Users/koka/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home \
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryLoading064ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le drapeau de test rapide facilite l’automatisation ; ce scénario réactive
|
||||
explicitement l’introduction longue avant la validation de Hello World.
|
||||
Aucun serveur dédié lancé, aucune EULA acceptée, aucun monde personnel modifié.
|
||||
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack` réussi en **2 min 21 s**,
|
||||
**122 tâches**, avec `-x :sanctuary:runGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryQuickTests=true`. Le parcours natif plat/classique, avec Sanctuary
|
||||
activé/désactivé puis réouverture, passe ses **8 ouvertures** en 53 s
|
||||
(`build/gameplay064-client.log`). Les tests dédiés exclus restent remplacés ici
|
||||
par les essais du serveur intégré, conformément au refus d’accepter leur EULA.
|
||||
|
||||
Archives comparées à beta.063 : **1 645 classes conformes à la compilation**,
|
||||
ressources FR/EN, mixins, métadonnées, dépendances exactes et absence de tests
|
||||
dans le mod. Les classes de Hello World, de l’introduction, les sons, les
|
||||
textures, les données de génération et JEI sont inchangés. Les variantes
|
||||
normal/Test diffèrent uniquement par le module de test rapide et leur nom.
|
||||
Les anciens MRpacks beta.063 conservent leurs empreintes originales.
|
||||
|
||||
- [Pack normal beta.064](../build/Sanctuary-beta.064.mrpack), 5720330 octets.
|
||||
SHA-256 : `c05b78e2caf27d52cb318979ee66e19e5bca9c90673f8868ae50f85f0e2ff2c0`.
|
||||
- [Monde plat rapide beta.064](../build/Sanctuary-Test-beta.064.mrpack), 5739264 octets.
|
||||
SHA-256 : `962800291875ab5bebda1abaedc798e7cf110ba0ef13ac168a0b85b3b2c19d24`.
|
||||
|
||||
Reçu : `build/loading064-artifact.json`. Aucun canal publié ni instance Prism
|
||||
modifié. Les optimisations de sondages partagés et de parallélisation de la
|
||||
première création proposées dans l’audit restent des tickets ultérieurs.
|
||||
@@ -0,0 +1,231 @@
|
||||
# beta.063 — audit du chargement et règle de jeu Sanctuary
|
||||
|
||||
> Suite livrée dans [beta.064](loading-cache-beta064.md) : cache des plans et
|
||||
> étapes réelles sur fond uni, sans le portail de la proposition ci-dessous.
|
||||
> L’audit et les mesures beta.063 restent conservés ici.
|
||||
|
||||
Branche `codex/loading-audit-gameplay-beta063`, après beta.062 conservée.
|
||||
|
||||
## Contrat
|
||||
|
||||
L'audit mesure séparément le démarrage du client, la création du terrain,
|
||||
Hello World, l'introduction et l'arrivée. Il compare une île neuve, sa
|
||||
réouverture, un monde plat et un monde Minecraft classique, sans utiliser
|
||||
de sauvegarde personnelle. L'habillage du préchargement est une proposition
|
||||
à examiner, pas une optimisation de terrain déjà livrée.
|
||||
|
||||
La règle native `sanctuary:use_sanctuary`, « Utiliser Sanctuary », permet
|
||||
de choisir les règles Sanctuary à la création d'un monde, indépendamment
|
||||
de son générateur. Elle est activée par défaut dans le pack ; la désactiver
|
||||
avant la création conserve les capacités ordinaires du joueur. Activée sur
|
||||
un monde plat ou classique, elle apporte Hello World, la progression,
|
||||
l'inventaire, les familiers, les factions et les systèmes qui en dépendent,
|
||||
ainsi que les modes temporel et météorologique Sanctuary.
|
||||
|
||||
Cette règle ne change jamais le générateur, le spawn natif, les chunks ou les
|
||||
structures du monde choisi. Les fonctions propres aux îles et aux expansions
|
||||
restent réservées à ce terrain. Les ressources et ajouts autonomes du mod
|
||||
restent chargés, y compris quand cette règle est désactivée.
|
||||
|
||||
Le choix est fixé pour la partie : les commandes et options d'une partie
|
||||
déjà ouverte ne peuvent pas le basculer. Le serveur conserve le choix natif
|
||||
dans `level.dat` ; les données existantes d'habitant continuent d'identifier
|
||||
une partie Sanctuary lors des réouvertures. Pas de nouveau format propriétaire.
|
||||
Les parties Sanctuary et le module plat de test antérieurs conservent leur
|
||||
admission historique ; la protection existante des anciennes parties sans
|
||||
progression reste appliquée. Aucun Overworld classique déjà joué n'est
|
||||
converti automatiquement. Une conversion reste hors de cette livraison,
|
||||
conformément à la précision de l'utilisateur.
|
||||
|
||||
## Mesures du 15 septembre 2026
|
||||
|
||||
Client natif Minecraft **26.3-pre-2**, Fabric **0.19.5**, API
|
||||
**0.160.0+26.3**, Java 25 ; Mac Apple M1, huit processeurs logiques,
|
||||
tas JVM 2 Gio. Une exécution par scénario dans le même client, dans cet
|
||||
ordre, avec enregistrement JFR `profile`. Graine **42**, île Moyen **724**,
|
||||
structures actives, distances de rendu et de simulation **8**. Les autres
|
||||
applications du bureau restent ouvertes. Ce sont des observations locales,
|
||||
pas une moyenne représentative ni une preuve de gain entre versions.
|
||||
|
||||
Le compteur commence à la demande de création/ouverture. « Jouable » signifie
|
||||
joueur présent, écran de jeu actif et fondu d'arrivée terminé. Hello World est
|
||||
validé automatiquement ; le temps passé à choisir son personnage est exclu.
|
||||
L'introduction complète reste active, même sur les terrains Minecraft.
|
||||
|
||||
| Scénario | Hello World prêt | Fin de la séquence d'introduction | Jouable |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Nouvelle île Sanctuary | 74,292 s | 103,889 s | **122,101 s** |
|
||||
| Réouverture de cette île | Déjà accompli | Non rejouée | **58,701 s** |
|
||||
| Nouveau monde plat + Sanctuary | 0,929 s | 30,489 s | **33,246 s** |
|
||||
| Nouveau monde classique + Sanctuary | 3,809 s | 33,458 s | **37,632 s** |
|
||||
|
||||
Le premier lancement du processus client jusqu'au début du parcours au menu
|
||||
prend environ **15 s**, en plus de ces durées. Il utilise les ressources déjà
|
||||
présentes sur cette machine : ce n'est pas une installation ou un cache disque
|
||||
à froid. Le profilage commence aux scénarios monde ; ce démarrage client est
|
||||
chronométré, mais pas attribué fonction par fonction.
|
||||
|
||||
## Ce qui se passe réellement
|
||||
|
||||
1. Le client charge les classes, ressources, textures et sons. L'enregistrement
|
||||
des services Sanctuary n'est pas encore la planification complète des îles.
|
||||
2. Après « Créer », le serveur intégré construit les niveaux. Dans
|
||||
`ExpansionRuntime.register`, l'événement `ServerLevelEvents.LOAD` prépare les
|
||||
plans du générateur : eau, traversées, réseaux souterrains, sanctuaire minier,
|
||||
patrimoine, village et expéditions. Cette étape bloque l'accès à Hello World.
|
||||
3. Le serveur choisit le spawn et prépare les chunks d'arrivée. La configuration
|
||||
réseau ouvre Hello World puis l'introduction ; une partie de la préparation
|
||||
du monde continue pendant ces **29,5 s** de cinéma.
|
||||
4. La séquence se termine, mais le monde peut ne pas être prêt. L'écran blanc
|
||||
couvre alors l'attente, puis le fondu rend le jeu visible. Sur l'île mesurée,
|
||||
environ **18,2 s** séparent la fin de l'intro de l'état jouable.
|
||||
|
||||
La première planification de l'Overworld occupe environ **58,8 s** entre
|
||||
`SERVER_STARTING` et la fin de son événement `LOAD`. À la réouverture, elle
|
||||
occupe encore **56,8 s**. La majeure partie de ces plans est recalculée à chaque
|
||||
nouvelle instance du générateur ; le journal aérien existant ne met pas tous
|
||||
les autres plans en cache.
|
||||
|
||||
Le `Time elapsed` natif est trompeur pour cet audit : il annonce **434 ms** à
|
||||
la réouverture alors que l'entrée prend **58,7 s**. Ce compteur démarre après
|
||||
les calculs lourds. Il ne doit pas servir seul de mesure du chargement complet.
|
||||
|
||||
## Où part le CPU
|
||||
|
||||
Les journaux de cette réouverture attribuent **23,913 s** aux traversées,
|
||||
**14,590 s** au patrimoine, **7,758 s** au sanctuaire minier et **2,999 s** au
|
||||
village. L'hydrologie annonce **13,052 s**, mais elle est appelée pendant la
|
||||
préparation des traversées : **ne pas additionner ces deux durées**.
|
||||
|
||||
Dans les échantillons JFR des méthodes Java en exécution à la réouverture :
|
||||
|
||||
| Méthode | Part des échantillons |
|
||||
| --- | ---: |
|
||||
| `SmearedPerlinNoise.addToVolume` | 37,81 % |
|
||||
| `NoiseStack.Perlin.get` | 22,80 % |
|
||||
| `GradientNoise.permute` | 8,51 % |
|
||||
|
||||
Ces trois fonctions de bruit totalisent **69,12 %** des échantillons. Ce n'est
|
||||
ni une part exacte du temps mural ni un gain récupérable annoncé. Les pauses
|
||||
GC totalisent **838 ms** sur le profil de réouverture, contre **1,71 s** pour
|
||||
la création de l'île. La pression mémoire existe, mais elle n'explique pas
|
||||
l'essentiel de cette attente ; augmenter seulement la RAM ne cible pas le
|
||||
coût dominant identifié.
|
||||
|
||||
Les profils incluent quelques secondes de jeu et la fermeture du monde.
|
||||
Une première tentative avec `waitForChunksDownload()` a attendu la complétion
|
||||
du rendu au-delà de l'arrivée jouable ; elle est conservée à part et exclue
|
||||
des résultats ci-dessus. Les assertions finales utilisent la présence du joueur,
|
||||
l'écran natif et la fin du fondu, pas ce critère plus strict du banc de tests.
|
||||
|
||||
Sources : `ExpansionRuntime.java:35`, `SanctuaryChunkGenerator.java:93` et `:291`,
|
||||
`ProgressionService.java:194`, `IntroScreen.java:51` et `:170`,
|
||||
`IntroSpawnFlash.java:20`. Les appels répétés à `prepareTransit` sont gardés :
|
||||
certains sont protégés et certains prennent en compte les liens du patrimoine.
|
||||
L'audit n'en déduit pas qu'ils peuvent simplement être supprimés.
|
||||
|
||||
## Habillage proposé, après retour utilisateur
|
||||
|
||||
**Conserver toute l'introduction**, sa durée, sa musique au dévoilement du
|
||||
portail et son flash. L'habillage proposé concerne uniquement l'attente qui
|
||||
précède Hello World et la réouverture d'une partie.
|
||||
|
||||
Écran sobre proche de Minecraft : fond sombre uni, portail natif au centre,
|
||||
texte Minecraft discret et bouton Annuler natif. Aucun grand logo ajouté,
|
||||
aucune carte décorative, aucun encadrement ni panneau de diagnostic permanent.
|
||||
Les textures du portail, de l'obsidienne, du bouton et la police bitmap de la
|
||||
proposition proviennent des ressources locales de Minecraft 26.3-pre-2.
|
||||
|
||||
Le texte décrit l'étape réelle : ressources, calcul du terrain, préparation
|
||||
des alentours. Une barre et un compte de chunks peuvent apparaître uniquement
|
||||
lorsque leur total est connu ; les calculs de plans restent sans faux pourcentage.
|
||||
Le compteur `86 / 225` de la maquette est un exemple d'état, pas une mesure.
|
||||
Annuler demande un arrêt propre au serveur, sans quitter au milieu d'une
|
||||
écriture. Son comportement réel devra être intégré et testé avec l'écran.
|
||||
|
||||
La proposition interactive a été simplifiée après rejet de la première
|
||||
direction. **Elle n'est pas intégrée au rendu du jeu dans beta.063.**
|
||||
|
||||
## Prochains tickets proposés
|
||||
|
||||
| Priorité | Résultat vérifiable | Garde-fous / validation |
|
||||
| --- | --- | --- |
|
||||
| 1 — Cache des plans | Réouvrir une île en relisant ses plans dérivés validés | Clé graine, révision de génération, paramètres et configurations ; écriture atomique ; corruption ou cache absent → recalcul ; comparaison exacte des plans et chunks, sans régénérer de monde utilisateur. Contrat de cache à formaliser avant développement. |
|
||||
| 2 — Préchargement natif | Afficher l'écran épuré dès le début de la création et à la réouverture | Phases instrumentées, fermeture propre, texte FR/EN, annulation et erreur visibles ; intro intacte ; mesures du coût de l'écran. |
|
||||
| 3 — Réutiliser les sondages | Éviter les colonnes de terrain recalculées entre plusieurs planificateurs | Cache borné, mêmes entrées et mêmes valeurs, empreintes bit à bit sur plusieurs graines ; reprendre la méthode de `docs/initial-loading.md`. |
|
||||
| 4 — Travail en parallèle | Réduire le chemin critique restant après les caches | Seulement sur données immuables ; aucun accès au niveau Minecraft depuis des threads arbitraires ; gains mesurés et déterminisme vérifié. |
|
||||
|
||||
Aucun gain de ces futurs tickets n'est compté comme livré. La génération et
|
||||
ses paramètres sont inchangés dans beta.063. La proposition respecte le choix
|
||||
de conserver l'intro ; l'attente blanche mesurée reste un constat, pas une
|
||||
modification autorisée de cette séquence.
|
||||
|
||||
## Utiliser la règle de jeu
|
||||
|
||||
À la création d'un nouveau monde, choisir le terrain **classique** ou
|
||||
**plat**, puis laisser **Utiliser Sanctuary : Oui** dans les règles de jeu,
|
||||
catégorie Joueur. Hello World et les règles Sanctuary s'appliquent au terrain
|
||||
natif. Les capacités ordinaires sont conservées avec **Non**. Ce choix ne
|
||||
nécessite pas une installation du module de test rapide.
|
||||
|
||||
`/gamerule sanctuary:use_sanctuary` permet de consulter le réglage. Une tentative
|
||||
de changement en jeu est refusée avec une explication FR/EN ; les autres règles
|
||||
restent modifiables. La capture du choix se fait au chargement de l'Overworld :
|
||||
dans cette version, `getGlobalGameRules()` n'est pas disponible avant sa création.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Audit client natif : île neuve, même île réouverte, plat et classique neufs,
|
||||
intro complète ; `LOADING063_AUDIT_PASS`.
|
||||
- Parcours fonctionnel client et serveur intégré : plat/classique × activé/
|
||||
désactivé, chacun créé puis réouvert, soit **8 ouvertures** ;
|
||||
`GAMEPLAY063_PASS`. Vérifie Hello World uniquement si activé, sa non-répétition,
|
||||
le familier initial conservé, les 3/10 cœurs et 1/4 rangées selon le mode,
|
||||
l'absence de session d'expansion sur terrain natif, le temps et la météo.
|
||||
- Vérifie le refus des mutations en jeu des règles globales et du niveau,
|
||||
le refus explicite de la commande et la modification d'une autre gamerule.
|
||||
Libellé et description natifs testés en français et en anglais.
|
||||
- La non-conversion des anciens mondes classiques repose sur la garde
|
||||
d'admission examinée dans le code ; aucun monde personnel n'est ouvert.
|
||||
|
||||
Commandes reproductibles (Java 25) :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryLoading063ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryGameplay063ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Preuves locales ignorées : `build/loading063-audit.log`,
|
||||
`build/loading063-evidence/timings.tsv` (extrait des événements du journal),
|
||||
les deux profils JFR d'île et leurs vues CPU/GC dans ce même dossier,
|
||||
`build/gameplay063-client.log`. Les JFR plat/classique du dossier temporaire
|
||||
du runner ont été nettoyés au test suivant ; leurs chronométrages restent dans
|
||||
le journal intégral. Archiver `mods/sanctuary/build/run/clientGameTest/loading063/`
|
||||
avant de relancer un parcours pour conserver tous les profils.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack` réussit en **2 min 22 s**,
|
||||
avec `-x :sanctuary:runGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryGameplay063ClientTests=true -PsanctuaryQuickTests=true
|
||||
-PsanctuaryClientNoVsync=true`. Les tests serveur dédiés restent exclus, selon
|
||||
le refus antérieur d'accepter leur EULA ; les huit ouvertures ci-dessus utilisent
|
||||
le serveur intégré au client natif et vérifient réellement les règles côté serveur.
|
||||
|
||||
Les deux archives locales sont vérifiées : dépendances exactes, ZIP, identité
|
||||
des **1 628 classes compilées**, ressources, absence de classes de test, même
|
||||
contenu normal/Test sauf le module rapide. Le JAR, les classes d'introduction,
|
||||
les ressources et les sons non concernés sont comparés à beta.062 ; le terrain
|
||||
et toute la séquence d'introduction restent identiques. Les anciens packs
|
||||
beta.062 sont conservés avec leurs empreintes originales.
|
||||
|
||||
- [Pack normal beta.063](../build/Sanctuary-beta.063.mrpack), SHA-256
|
||||
`1af1ff168f5cf97d7f44bb28398eb79efae8e1c4858f05d5b4291b1dfe48ad93`.
|
||||
- [Pack Test beta.063](../build/Sanctuary-Test-beta.063.mrpack), SHA-256
|
||||
`405a55e76233c8403f27320f104e09ee590cae652b97b4396dd6a5b1a7dd0531`.
|
||||
|
||||
Reçu `build/loading063-artifact.json`, build `build/loading063-check.log`.
|
||||
Pas de déploiement Prism, de changement de canal ou de sauvegarde personnelle.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Menus Sanctuary — organisation
|
||||
|
||||
Cadrage du 15 septembre 2026, à partir de beta.077.
|
||||
Branche documentaire initiale : `codex/menu-architecture`.
|
||||
**Statut : organisation appliquée en [beta.083](menu-organization-beta083.md).
|
||||
Le renommage Combats / Battles a été livré en [beta.078](combat-beta078.md).**
|
||||
|
||||
Objectif : retrouver une pause compacte, avec une rubrique par intention et
|
||||
des sous-pages cohérentes. Conserver les composants Minecraft, les textures
|
||||
existantes et le retour vers l’écran d’origine.
|
||||
|
||||
## Pause
|
||||
|
||||
Reprendre la partie occupe toute la première ligne. Les rubriques suivent :
|
||||
|
||||
| Colonne gauche | Colonne droite |
|
||||
| --- | --- |
|
||||
| Découvertes / Discovery | Progression / Progression |
|
||||
| Monde / World | Habitant / Inhabitant |
|
||||
| Factions / Factions | Combats / Battles |
|
||||
|
||||
Options et Options du monde restent dessous, puis Sauvegarder et quitter ou
|
||||
Déconnexion. Les entrées séparées Progrès et Statistiques sont déplacées dans
|
||||
Progression et Découvertes. Monde ouvre toujours directement la carte.
|
||||
Combats regroupe duels et arènes et reste un accès direct, conformément au
|
||||
choix déjà confirmé.
|
||||
|
||||
**Économie est différée à la demande du créateur : aucun bouton économique,
|
||||
même désactivé, n’est prévu dans cette première passe.**
|
||||
|
||||
## Découvertes : ce que je connais et ce que j’ai fait
|
||||
|
||||
Un seul bandeau de catégories accueille le catalogue et
|
||||
les statistiques correspondantes :
|
||||
|
||||
| Catégorie FR / EN | Contenu cible |
|
||||
| --- | --- |
|
||||
| Général / General | Statistiques générales vanilla : temps, déplacements, morts et autres compteurs existants. |
|
||||
| Blocs / Blocks | Catalogue de blocs découverts, avec les compteurs disponibles : minés, posés, etc. |
|
||||
| Objets / Items | Objets connus, avec leurs compteurs disponibles : fabriqués, utilisés, récupérés, etc. |
|
||||
| Créatures / Mobs | Créatures découvertes et statistiques vanilla de rencontres mortelles : tuées et morts causées. |
|
||||
| Recettes / Recipes | Recettes révélées et collections qui permettent de les débloquer. |
|
||||
|
||||
Les statistiques d’un objet ou d’une créature sont accessibles dans sa fiche,
|
||||
sans imposer d’ouvrir un second catalogue. Les compteurs vanilla et Sanctuary
|
||||
gardent leur sens et leur source ; ne pas inventer un compteur absent ou
|
||||
additionner deux compteurs décrivant la même action.
|
||||
|
||||
À la première ouverture, conserver l’entrée actuelle sur les blocs possédés.
|
||||
Les filtres de connaissance et l’aptitude Catalogue restent en vigueur :
|
||||
« Tout » signifie tout ce qui est déjà découvert, jamais les éléments inconnus.
|
||||
Les statistiques ne deviennent pas une voie de contournement de ces règles.
|
||||
Les outils opérateur restent invisibles et désactivés en mode normal.
|
||||
|
||||
La beta.083 réunit les cinq catégories. Objets reprend les découvertes et
|
||||
les compteurs existants ; Créatures s’appuie sur les victimes et les morts
|
||||
causées. Les simples observations de créatures ne sont pas enregistrées dans
|
||||
les sauvegardes actuelles et ne sont donc pas présentées comme connues.
|
||||
|
||||
## Progression : où je vais et ce que je débloque
|
||||
|
||||
- Le titre et le nombre de niveaux disponibles ont une ligne réservée.
|
||||
- À gauche, un accès **Progrès / Advancements**, au-dessus des six compétences.
|
||||
Il ouvre l’arbre Minecraft existant et conserve le suivi des objectifs.
|
||||
- Sous cet accès, les compétences gardent leurs barres d’XP, leurs coûts et
|
||||
leurs commandes d’achat. Le remboursement opérateur reste conditionné
|
||||
aux permissions.
|
||||
- À droite, les **Aptitudes / Abilities** restent à leur place, avec leur
|
||||
propre défilement.
|
||||
- **Prestige / Prestige** et **Historique / History** occupent une rangée
|
||||
dédiée, indépendante du compteur d’XP. L’historique conserve les achats,
|
||||
remboursements et événements personnels existants.
|
||||
- Factions disparaît de cette navigation. Les sous-pages Prestige et
|
||||
Historique ne doivent pas recréer l’ancien trio Prestige–Factions–Historique.
|
||||
|
||||
L’arbre de Progrès conserve son écran complet : l’imbriquer dans la colonne
|
||||
des compétences rendrait les deux illisibles. « Au-dessus des compétences »
|
||||
désigne ici son accès, pas un arbre miniature.
|
||||
|
||||
### Chevauchement corrigé en beta.083
|
||||
|
||||
Les boutons Prestige, Factions et Historique occupaient la même ligne que
|
||||
les niveaux disponibles. La beta.083 réserve une ligne au compteur, puis
|
||||
une rangée à Prestige / Historique, avant les deux colonnes de contenu.
|
||||
|
||||
## Monde, Habitant, Factions et combats
|
||||
|
||||
**Monde** conserve l’accès direct à la carte et ses réglages. **Habitant**
|
||||
reste la fiche personnelle : nom, bio, prestige, faction, personnage et
|
||||
familier. Le nom de faction peut ouvrir la même page Factions que la pause.
|
||||
|
||||
**Factions** devient une destination dédiée : ma faction, membres,
|
||||
invitations, capacité, gestion et journal de faction. Sans faction, cette
|
||||
page présente la création et les invitations reçues. La navigation ne change
|
||||
ni les coûts ni les règles existantes : dix niveaux pour créer, deux places
|
||||
initiales, extensions liées aux charges de prestige.
|
||||
|
||||
**Combats** conserve sa liste publique de duels et arènes, les inscriptions, règles et
|
||||
paris actuels. Les mises en objets existantes ne nécessitent pas de créer
|
||||
l’économie générale pour fonctionner.
|
||||
|
||||
## Idées économiques conservées pour plus tard
|
||||
|
||||
Le créateur souhaite à terme une entrée Économie rassemblant le marché,
|
||||
la banque, la bourse, la loterie, les navets, le marché noir, les offres,
|
||||
requêtes et contacts. Aucun de ces services n’est implémenté par ce cadrage.
|
||||
|
||||
Proposition à reprendre ultérieurement : un tableau d’affichage commun aux
|
||||
missions, contrats, projets et annonces, relié aux habitants et aux factions.
|
||||
Une fiche indique l’auteur, le besoin, la récompense éventuelle et l’état ;
|
||||
des liens ouvrent le même contenu depuis chaque rubrique concernée.
|
||||
Courrier et contacts permettent de se répondre dans une présentation proche
|
||||
des livres, panneaux et inventaires Minecraft. La texture de courrier
|
||||
mentionnée par le créateur reste à retrouver ou recevoir avant intégration.
|
||||
|
||||
Ne pas fixer ici une nouvelle monnaie, un transfert automatique d’objets,
|
||||
une banque sans risque ou les conditions d’un contrat. La vision contient
|
||||
déjà des intentions sur les trois gemmes, la mailbox, le coffre-fort et les
|
||||
navets du dimanche ; elles devront être conciliées dans un ticket dédié.
|
||||
|
||||
## Première passe vérifiable
|
||||
|
||||
1. Réorganiser la pause et séparer Factions de la navigation personnelle.
|
||||
2. Réserver une ligne aux niveaux disponibles ; placer l’accès Progrès
|
||||
au-dessus des compétences, en conservant les aptitudes à droite.
|
||||
3. Rattacher les statistiques à Découvertes en conservant tous les accès
|
||||
existants, puis unifier les catégories selon les données disponibles.
|
||||
|
||||
Contrôler en français et en anglais, aux échelles GUI 2 et 3, avec retour
|
||||
Échap/Terminé vers le bon parent, textes longs, défilement, mode normal et
|
||||
opérateur. Vérifier le suivi des Progrès, les achats et remboursements,
|
||||
Prestige, invitations de faction et fermeture des arènes avant réponse réseau.
|
||||
Les nouveaux regroupements ne modifient ni sauvegardes ni transactions.
|
||||
|
||||
Le cadrage initial était documentaire. Sa livraison de code, ses limites et
|
||||
ses vérifications sont décrites dans [beta.083](menu-organization-beta083.md).
|
||||
@@ -0,0 +1,40 @@
|
||||
# beta.076 — navigation et pouvoirs sélectionnés
|
||||
|
||||
Branche `codex/menu-navigation-beta076`. Minecraft 26.3-pre-2 ; textures beta.070.
|
||||
|
||||
Le menu pause ouvre directement **Duels et arènes**. **Progression** contient
|
||||
les accès **Prestige**, **Factions** et **Historique**. La carte reste accessible
|
||||
par **Monde**. Les retours reviennent à l’écran d’origine.
|
||||
|
||||
L’historique regroupe les événements de prestige/faction et les achats ou
|
||||
remboursements du cycle actuel, avec les aptitudes conservées. Cette dernière
|
||||
liste était restée dans l’ancienne page Monde, devenue inaccessible depuis
|
||||
l’ouverture directe de la carte. Les achats des cycles terminés restent dans
|
||||
l’archive serveur : aucun nouveau format de sauvegarde ou réseau n’est ajouté.
|
||||
Les noms de faction invalides et invitations vides ne proposent plus un clic
|
||||
inutile. Le serveur conserve toutes ses validations et les confirmations.
|
||||
|
||||
« Usages historiques » devient **Choisir le pouvoir utilitaire**. Le mode
|
||||
sélectionné et son pouvoir sont visibles, le prérequis Lien actif est expliqué,
|
||||
et le contrôle affiché suit la touche configurée. La sélection ne lance pas
|
||||
le pouvoir : fermer le menu puis utiliser la touche (G par défaut). Les
|
||||
messages des pouvoirs utilitaires retrouvent leurs traductions propres.
|
||||
La dérogation opérateur existante est conservée.
|
||||
|
||||
Les arènes s’ouvrent immédiatement avec un état de chargement. Une réponse
|
||||
arrivée après fermeture ne rouvre plus l’écran.
|
||||
|
||||
## Vérification
|
||||
|
||||
Parcours client natif final réussi en 1 min 3 s : FR/EN, échelles 2/3, navigation,
|
||||
fermeture avant réponse, achats, fondation à deux places, annulation et
|
||||
confirmation de prestige, conservation de faction/aptitude, sélection utilitaire
|
||||
et message de recharge via G. Correction d’un rafraîchissement manquant lors
|
||||
de la réception du déblocage Lien actif. Le défilement est conservé lors
|
||||
d’un changement de pouvoir et ce comportement est aussi vérifié.
|
||||
Captures : `build/navigation076-evidence-final/`.
|
||||
|
||||
Ce lot est distribué avec beta.077 et sa couche de nuages, ajoutée à la
|
||||
demande du créateur pendant cette passe. Aucun pack beta.076 publié. Le build complet et les deux archives beta.077
|
||||
sont vérifiés ; voir [la livraison](cloud-vortex-beta077.md).
|
||||
Aucun déploiement personnel. Pas de serveur dédié (EULA non autorisée).
|
||||
@@ -0,0 +1,108 @@
|
||||
# beta.083 — organisation des menus
|
||||
|
||||
Application du [plan des menus](menu-architecture.md), sur la branche
|
||||
`codex/menu-organization-beta083`.
|
||||
|
||||
## Navigation
|
||||
|
||||
La pause conserve Reprendre, puis trois rangées de deux rubriques :
|
||||
Découvertes / Progression, Monde / Habitant, Factions / Combats.
|
||||
Options, Options du monde et la sortie restent natifs, ainsi que les
|
||||
éventuels ajouts du serveur. Aucun accès Économie n’est ajouté.
|
||||
|
||||
Progression réserve une ligne aux niveaux disponibles, puis une rangée
|
||||
Prestige / Historique. Progrès ouvre l’arbre Minecraft complet au-dessus
|
||||
des compétences de gauche ; les aptitudes gardent leur défilement à droite.
|
||||
Les sous-pages Prestige et Historique ne proposent plus Factions.
|
||||
|
||||
Factions devient une entrée directe, avec gestion et journal de faction.
|
||||
Les coûts, confirmations, invitations, permissions et transactions restent
|
||||
inchangés. Le journal reprend les événements de faction autorisés par le
|
||||
serveur, y compris les événements personnels des anciennes factions.
|
||||
L’historique personnel conserve les achats, remboursements et événements
|
||||
déjà disponibles.
|
||||
|
||||
## Découvertes et statistiques
|
||||
|
||||
Un bandeau unique donne accès à Général, Blocs, Objets, Créatures et Recettes.
|
||||
Changer de catégorie conserve le même écran parent : Échap / Terminé revient
|
||||
directement à la pause, sans retraverser les catégories visitées. Les fiches
|
||||
de statistiques reviennent à leur liste.
|
||||
|
||||
- **Général** : tous les compteurs généraux natifs, avec leurs unités et
|
||||
leur format Minecraft (temps, distances, etc.).
|
||||
- **Blocs** : catalogue Sanctuary existant, par défaut sur Possédés,
|
||||
avec recherche, filtres et compteurs existants.
|
||||
- **Objets** : par défaut les objets portés, y compris extension et
|
||||
accessoires. L’aptitude Catalogue permet d’afficher également les objets
|
||||
déjà découverts. Une fiche présente fabrication, utilisation, casse,
|
||||
récupération et abandon, chacun avec son compteur natif distinct.
|
||||
Une recette révélée ne suffit pas à déclarer son résultat découvert.
|
||||
- **Créatures** : espèces déjà tuées ou ayant tué le joueur, avec leurs
|
||||
deux compteurs natifs. Les sauvegardes n’ont pas de registre persistant
|
||||
des simples observations de mobs ; cette limite est indiquée dans la
|
||||
page. Les créatures inconnues ne sont pas listées.
|
||||
- **Recettes** : catalogue et collections existants, avec le même contrôle
|
||||
d’accès à l’aptitude et aux recettes connues.
|
||||
|
||||
Les statistiques sont demandées au serveur à l’ouverture ou à l’actualisation,
|
||||
puis affichées après réception du paquet Minecraft. Aucun compteur n’est
|
||||
additionné à son équivalent Sanctuary. Aucun format de sauvegarde, règle
|
||||
de gameplay ou identifiant existant ne change.
|
||||
|
||||
## Hello World et arrivée
|
||||
|
||||
Hello World dessine un fond uni `#171717`, identique au préchargement, sans
|
||||
le panorama hérité de l’écran principal. À la réception de l’intro après
|
||||
Hello World, ce fond s’assombrit progressivement jusqu’au noir en une
|
||||
seconde. La séquence originale de 29,5 secondes commence ensuite intégralement.
|
||||
La musique reste liée à l’apparition du portail, et le flash d’arrivée reste
|
||||
inchangé. Rejouer l’intro en jeu conserve son départ noir habituel.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`Menus083ClientChecks` est passé dans un client Minecraft 26.3-pre-2 natif :
|
||||
|
||||
- français / anglais, GUI 2 et 3, captures de la pause, des catégories,
|
||||
de Progression, de Factions et des fiches ;
|
||||
- six destinations de pause, accès direct à la carte et aux combats,
|
||||
fermeture conservée si une réponse d’arène arrive après Échap ;
|
||||
- catégories sœurs et retours vers leur parent ; objets portés avant
|
||||
Catalogue, objets déjà découverts après achat, objets et mobs inconnus
|
||||
toujours masqués ;
|
||||
- clic réel sur un Progrès pour activer/désactiver son suivi, arbre complet
|
||||
et retour dans Progression ;
|
||||
- achat de compétence, remboursement opérateur du dernier coût, visibilité
|
||||
des contrôles après activation/révocation native des commandes ;
|
||||
- création de faction avec annulation puis confirmation, deux places,
|
||||
journal de faction séparé des achats, historique avec remboursements ;
|
||||
- passage de prestige avec conservation de la faction et des aptitudes,
|
||||
remise des compétences à zéro, charge reçue une seule fois.
|
||||
|
||||
`Welcome083ClientChecks` est passé en 53 secondes. Les captures natives
|
||||
confirment le gris uniforme aux quatre coins, puis les valeurs RVB neutres
|
||||
23 → 19 → 12 → 4 → 0 pendant le fondu. Le test vérifie aussi l’intro complète,
|
||||
le départ musical au portail, le replay noir et la fin sans double validation.
|
||||
La passe native des menus a duré 1 min 30 s.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
est passé en **2 min 38 s**, avec 124 tâches. Les sources du JAR et ses
|
||||
1 706 classes correspondent aux sorties vérifiées. La comparaison avec
|
||||
beta.082 confirme que seuls les écrans et leur intégration changent ; données,
|
||||
textures, resource pack beta.070 et classes serveur sont conservés.
|
||||
|
||||
Archives locales vérifiées :
|
||||
|
||||
| Archive | SHA-256 |
|
||||
| --- | --- |
|
||||
| `Sanctuary-beta.083.mrpack` | `83b98db727665ab29fc3f0070f1616d781d2995efde1d2d344b50436251300ee` |
|
||||
| `Sanctuary-Test-beta.083.mrpack` | `1aea33aecbf247088702a39229815a1365afa54b53ba91be06f70fb89e1e86d3` |
|
||||
|
||||
Le reçu est dans `build/menus083-artifact.json`, les journaux dans
|
||||
`build/menus083-client.log`, `build/welcome083-client.log` et
|
||||
`build/menus083-check.log`. Les archives beta.082 restent intactes.
|
||||
|
||||
Pas de déploiement dans une instance personnelle ni de publication du canal
|
||||
packwiz dans cette tâche. Le GameTest serveur dédié reste exclu conformément
|
||||
au refus précédent d’accepter son EULA ; les tests avec serveur intégré
|
||||
utilisent uniquement des mondes plats de développement.
|
||||
@@ -0,0 +1,102 @@
|
||||
# beta.090 — Passage à Minecraft Java 26.3
|
||||
|
||||
Ticket de migration du projet, branche `codex/minecraft-26-3-beta090`.
|
||||
|
||||
## Cible et contrat avant modification
|
||||
|
||||
Passage de `26.3-pre-2` à la version finale **26.3**, publiée le 15 septembre
|
||||
2026. Cible Java 25, Fabric Loader 0.19.5 et Fabric API 0.160.5+26.3.
|
||||
Sanctuary, Demeure, le profil Test et le fork JEI sont compilés pour la même
|
||||
version exacte. Le fork JEI devient `30.32.0-sanctuary.3`, avec sa provenance
|
||||
et son patch reproductible ; aucune substitution du catalogue Sanctuary par
|
||||
le JEI standard.
|
||||
|
||||
Cette livraison change les binaires du projet et les archives locales. Aucun
|
||||
monde existant ni instance personnelle n'est ouvert ou modifié. Pas de
|
||||
régénération, d'expansion activée ni de migration des schémas Sanctuary.
|
||||
Les identifiants restent stables. Les essais utilisent des mondes neufs et
|
||||
les graines documentées des parcours ; les algorithmes de génération
|
||||
Sanctuary ne sont pas modifiés. Les résultats de génération sur la nouvelle
|
||||
version sont validés par les contrôles existants, sans garantir l'identité
|
||||
binaire de tous les chunks générés par deux versions de Minecraft.
|
||||
|
||||
Le pack de textures reçoit une édition beta.090 pour déclarer la cible 26.3.
|
||||
Le format reste 97.1 et les images existantes sont conservées ; le manifeste
|
||||
et l'archive beta.070 restent immuables. Les anciens packs beta.089 restent
|
||||
également immuables.
|
||||
|
||||
## Sources vérifiées
|
||||
|
||||
- [Sortie officielle de Minecraft Java 26.3](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3) : Java 25, données 121.0 et ressources 97.1.
|
||||
- [Manifeste Minecraft officiel](https://piston-meta.mojang.com/mc/game/version_manifest_v2.json) : entrée release 26.3, métadonnées et empreintes Mojang.
|
||||
- [Fabric Loader pour 26.3](https://meta.fabricmc.net/v2/versions/loader/26.3) : 0.19.5 stable.
|
||||
- [Fabric API 0.160.5+26.3](https://modrinth.com/mod/fabric-api/version/0.160.5+26.3) : version déclarée pour 26.3, JAR et empreinte épinglés dans packwiz.
|
||||
|
||||
## Adaptations
|
||||
|
||||
- Les cibles Minecraft/Fabric sont communes à Sanctuary, Demeure, Sanctuary Test
|
||||
et au fork JEI. Les métadonnées des JAR et les deux distributions reprennent
|
||||
ces versions ; Fabric API est épinglé par URL et empreinte.
|
||||
- Le bouton d'ouverture du dossier des plans utilise `Blaze3D.openPath` :
|
||||
l'ancien `Util.getPlatform().openPath` n'existe plus dans la version finale.
|
||||
- Le scénario client du catalogue utilise la règle actuelle « Allow Commands »
|
||||
pour activer puis révoquer le mode opérateur. Sa vieille attente d'un second
|
||||
bouton d'inspection est retirée ; aucune permission de production n'est changée.
|
||||
- Le rapport TerrainAtlas récupère la version Minecraft réellement exécutée.
|
||||
- Minecraft passe du DataVersion 5018 (pre-2) à 5023 (finale), protocole 777.
|
||||
Les schémas Sanctuary restent inchangés. Les caches de plans dérivés incluent
|
||||
déjà les versions des mods et de Minecraft dans leur identité : la nouvelle
|
||||
version recalcule ses plans sans réutiliser un cache de l'ancien moteur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Les essais clients utilisent Minecraft **26.3** natif et des mondes de
|
||||
développement neufs, graine **42**. Aucun serveur dédié n'est lancé et aucune
|
||||
EULA n'est acceptée ; `:sanctuary:runGameTest` est explicitement exclu.
|
||||
|
||||
- Super pistons : états de collision client/serveur, six directions, versions
|
||||
normale/gluante, course, charge 108/109, obstacles, transport, sauvegarde et
|
||||
rechargement. Le correctif de collision beta.089 est conservé et retesté.
|
||||
- Multiblocs : navigation du Fût et recherche avec souris immobile, onglets du
|
||||
Fourneau, fermeture normale, stockage, cuisson et persistance. Le correctif
|
||||
beta.088 ne réintroduit pas de paquet de fermeture entre deux pages.
|
||||
- Catalogue JEI : découverte, familles de recettes débloquées par advancements,
|
||||
permissions opérateur et révocation, transfert réel depuis la sixième rangée,
|
||||
ressources FR/EN, prestige, mort puis rechargement du monde.
|
||||
- Monde Sanctuary : génération neuve, graine 42, introduction complète, cache
|
||||
écrit puis relu sans recalcul, corruption volontaire d'un cache de développement
|
||||
puis reconstruction. Les 217 053 écritures de blocs contrôlées, plans, géométries
|
||||
des fluides, butins et zones protégées restent identiques après les deux reprises.
|
||||
Le parcours natif passe en 3 min 35 s. À titre d'observation locale, la création
|
||||
avec introduction dure 147,4 s, la reprise avec cache 2,7 s et la reprise après
|
||||
corruption 40,5 s ; ces parcours ne constituent pas un benchmark comparable.
|
||||
|
||||
Les journaux locaux des quatre parcours sont
|
||||
`build/migration090-{piston,multiblocks,recipes,world}-client.log`.
|
||||
|
||||
## Assemblage vérifié
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe sous Java 25 : **124 tâches**, en 2 min 53 s. Les quatre parcours clients
|
||||
séparés passent également. Les deux exports packwiz sont vérifiés après écriture.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.090.mrpack) — SHA-256
|
||||
`4cbb9c06126874ce4c767b5fb6b3bf61c2903784e908f030ebbfbc662aac4dfc`.
|
||||
- [Pack Test](../build/Sanctuary-Test-beta.090.mrpack) — SHA-256
|
||||
`ffe9f70ec0f17d567d0c108478ed90915eb5d8cdc407be9ba39860dd87adfc05`.
|
||||
- [Textures seules](../build/Sanctuary-Resource-Pack-beta.090.zip).
|
||||
- [Reçu de vérification local](../build/migration090-artifact.json).
|
||||
|
||||
Les JAR imbriqués, leurs cibles Minecraft, les archives de sources et les versions
|
||||
packwiz concordent. L'empreinte du JAR Fabric API téléchargé par Gradle correspond
|
||||
à celle de la dépendance packwiz. Les images, modèles, données et identifiants du
|
||||
JAR Sanctuary sont identiques à beta.089 ; seul le `pack.mcmeta` graphique change.
|
||||
La seule classe de production Sanctuary différente est `PlansScreen`. Les archives
|
||||
normal/Test beta.089 et les sources du manifeste graphique beta.070 sont conservées.
|
||||
|
||||
## Limites et distribution
|
||||
|
||||
Les clients testés sont natifs sur macOS, avec serveur intégré. Pas de validation
|
||||
Windows/Linux ou LAN à deux clients pour cette migration, ni de conversion d'une
|
||||
sauvegarde personnelle. Les archives locales n'avancent pas le canal packwiz et
|
||||
ne mettent pas à jour une instance Prism existante.
|
||||
@@ -0,0 +1,68 @@
|
||||
# beta.092 — Clic molette sur les super pistons
|
||||
|
||||
Branche `codex/multiblock-pick-beta092`, Minecraft 26.3.
|
||||
|
||||
## Contrat
|
||||
|
||||
Le clic molette sur un super piston renvoie le piston Minecraft d'origine :
|
||||
`minecraft:piston` pour le normal, `minecraft:sticky_piston` pour le gluant.
|
||||
Cela couvre les neuf socles, toutes les têtes et les tiges, dans les six
|
||||
orientations et à chaque profondeur de course. Une sélection reçue côté serveur
|
||||
pendant une transition de mouvement retrouve aussi le composant source.
|
||||
Les blocs techniques n'obtiennent pas d'objet dédié.
|
||||
|
||||
La sélection reste native : en créatif, un exemplaire ; en survie, sélection du
|
||||
piston déjà présent dans l'inventaire, sans création d'objet. Ctrl + clic molette
|
||||
ne copie pas les données techniques du contrôleur ou du piston mobile sur
|
||||
l'objet ordinaire. Le comportement des autres blocs et charges mobiles vanilla
|
||||
reste inchangé. Fourneau et Fût gardent leurs blocs source natifs, four et baril.
|
||||
|
||||
Aucun format de sauvegarde, identifiant, modèle, texture ou règle de génération
|
||||
n'est modifié. Aucun monde existant n'est ouvert ou migré.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `Pick092ClientChecks` utilise un monde plat neuf de développement,
|
||||
graine 42 : tous les états client/serveur, puis de vrais clics molette sur des
|
||||
super pistons assemblés et étendus, en créatif et en survie.
|
||||
|
||||
Le parcours natif passe en **1 min 19 s** sur Minecraft 26.3/macOS :
|
||||
|
||||
- 864 états vérifiés côté client et serveur, avec et sans demande de données.
|
||||
- Douze combinaisons normal/gluant × six directions ; vrais clics molette sur
|
||||
chaque socle central, tête et les deux tiges du multibloc assemblé.
|
||||
- Le résultat est exactement un piston natif, avec les composants ordinaires.
|
||||
- En survie, un stack de cinq pistons présent est sélectionné sans duplication ;
|
||||
inventaire vide, aucun objet n'est créé.
|
||||
- Sélection des états mobiles d'extension/rétraction via le gestionnaire de paquet
|
||||
serveur natif. Ctrl + clic ne transporte pas le NBT du contrôleur ou du mouvement.
|
||||
- Les charges mobiles vanilla conservent leur comportement. Four et baril
|
||||
conservent aussi leur sélection native.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPick092ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journal : `build/pick092-client.log`, marqueur `PICK092_PASS`.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 1 s**, 124 tâches. Journal : `build/pick092-check.log`.
|
||||
Les exports correspondent aux JAR et à leurs sources. Textures, modèles,
|
||||
données, JEI et archives beta.091 sont conservés à l'identique.
|
||||
Reçu local : `build/pick092-artifact.json`.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.092.mrpack), SHA-256
|
||||
`3c1907fb3427e3df421eb84eb8f05c2e89c7cb3d27ef30d856602559cd33a383`.
|
||||
- [Pack Test](../build/Sanctuary-Test-beta.092.mrpack), SHA-256
|
||||
`a8acaffc0b803b81e12da7439af24abfb6dadfe890a952b6e89916317e9f4914`.
|
||||
|
||||
## Limites
|
||||
|
||||
Tests sur client natif macOS avec serveur intégré de développement ; pas de
|
||||
session LAN à deux clients pour cette livraison. Serveur GameTest dédié exclu,
|
||||
aucune EULA acceptée. Aucun déploiement dans une instance personnelle ni
|
||||
publication du canal packwiz.
|
||||
@@ -0,0 +1,98 @@
|
||||
# beta.084 — Clé dorée, Fût et Fourneau
|
||||
|
||||
Premier lot implémenté sur `codex/multiblocs-beta084`.
|
||||
Référence historique lue, sans modification : `../26.2/anotherworld`,
|
||||
`VaultStructure`, `SafeVaultMenu` et `SafeVaultScreen`. Reprise du repérage
|
||||
borné autour d'un centre et du contrôle serveur, pas du stockage monétaire,
|
||||
de la propriété privée ni de sa forme creuse à 26 composants.
|
||||
|
||||
## Contrat de sauvegarde et de migration — avant implémentation
|
||||
|
||||
Aucune génération ni conversion automatique d'une construction existante.
|
||||
L'assemblage est une action volontaire avec une clé dorée. Nouveau SavedData
|
||||
par dimension `sanctuary:multiblocks`, schéma 1 : origine du cube, type,
|
||||
orientation et identifiant UUID. Aucun contenu d'inventaire dans ce registre.
|
||||
Les 27 BlockEntities vanilla conservent leurs objets, composants, temps de
|
||||
cuisson, combustible restant et expérience. Les identifiants de blocs restent
|
||||
`minecraft:barrel` et `minecraft:furnace`. Le chargement d'une ancienne
|
||||
sauvegarde sans ce registre ne change rien : aucun fichier n’est écrit avant
|
||||
le premier assemblage. Le codec refuse un schéma inconnu et le chargeur refuse
|
||||
d’écraser un fichier illisible. La catégorie native de stockage NBT est
|
||||
`SAVED_DATA_COMMAND_STORAGE` ; les données métier restent dans notre codec.
|
||||
|
||||
Une dissociation retire seulement l'appartenance : elle ne restitue jamais
|
||||
une copie du stock initial. Une casse ou explosion laisse Minecraft déposer
|
||||
le contenu du composant cassé et dissout la machine ; les autres composants
|
||||
restent en place. Une partie déchargée suspend l'accès commun et la chauffe,
|
||||
sans chargement forcé ni rattrapage hors ligne. À la reprise, validation des
|
||||
27 composants. Les sauvegardes suivent l'atomicité native des chunks ; ce
|
||||
lot n'ajoute pas de journal transactionnel inter-chunks en cas de crash brutal.
|
||||
Les anciens inventaires ouverts sont fermés à l'assemblage/dissociation.
|
||||
|
||||
## Gestes et valeurs de la première version
|
||||
|
||||
- Clé : recette de trois lingots d'or en tête et un bâton en manche, sans usure.
|
||||
- Clic droit sur 27 barils ou fours en cube plein : repérage et confirmation
|
||||
par un deuxième clic dans les dix secondes. Une forme ambiguë est refusée.
|
||||
- Maj + clic droit sur une machine assemblée : dissocier.
|
||||
- Maj + clic droit sur un four ou baril indépendant : tourner, en conservant
|
||||
ses données. Pas de rotation d'une machine chargée.
|
||||
- Clic droit sans clé sur une façade : ouvrir le stock commun public.
|
||||
- Fût : 729 cases physiques, 54 par page, recherche par nom et identifiant.
|
||||
Les noms localisés sont résolus côté client en identifiants d’objets, puis
|
||||
filtrés uniquement dans le stock accessible validé par le serveur. Le
|
||||
Maj-clic de dépôt utilise tout le stock, même pendant une recherche.
|
||||
Une recherche fige ses emplacements jusqu'à la prochaine recherche.
|
||||
Changer de page/recherche ouvre un nouvel identifiant de menu et exige un
|
||||
curseur vide ; les anciens paquets de clic ne peuvent pas viser une autre page.
|
||||
- Fourneau : les 27 entrées, combustibles et sorties natifs, trois vues.
|
||||
Jusqu'à 27 cuissons simultanées à vitesse vanilla, avec chaleur partagée.
|
||||
Chaque cuisson paie un tick de combustible par tick de travail : le débit
|
||||
augmente, pas le rendement par charbon. Les combustibles et leurs restes,
|
||||
les recettes et l'XP restent gérés par le four vanilla. Pas de recette
|
||||
culinaire It's Alive dans ce lot. Une panne de combustible met le travail
|
||||
en attente sans effacer sa progression ; ajouter du combustible le reprend.
|
||||
Le débit correspond aux 27 fours physiques, sans multiplicateur de minerai.
|
||||
- Trémies : stock commun du Fût ; Fourneau, entrées en haut, combustible
|
||||
latéral, résultats en bas, en respectant les règles natives des seaux.
|
||||
- Apparence : composants vanilla conservés ; les fours montrent leur chauffe.
|
||||
Aucune nouvelle grande texture de façade dans cette première livraison.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`Multiblocks084ClientChecks` tourne dans un nouveau monde plat intégré :
|
||||
confirmation volontaire, refus de chevauchement, cube traversant une frontière
|
||||
de chunks, fichier SavedData écrit puis relu, schéma inconnu, sérialisation
|
||||
native des objets nommés et de la cuisson, deux joueurs serveur dont un invité
|
||||
automatisé, trémies natives, inventaire plein, recherche localisée, pagination,
|
||||
curseur plein et paquet périmé, dissociation, casse et explosion avec conservation
|
||||
du stock. Cuisson : deux recettes simultanées, rendement du charbon partagé,
|
||||
sortie pleine, seau de lave/éponge mouillée, résultat natif/XP et panne puis
|
||||
reprise des 27 cuissons. Captures natives en FR/EN aux échelles 2 et 3.
|
||||
|
||||
Validation finale réussie : `check build assemblePack assembleTestPack`
|
||||
et `:sanctuary:runClientGameTest`, en excluant `:sanctuary:runGameTest`
|
||||
(serveur dédié/EULA non autorisés). 131 tâches, 105 exécutées, en 3 min 16 s.
|
||||
Le dernier scénario couvre aussi les six rangées, le dépôt depuis la rangée
|
||||
supplémentaire et le maintien des menus voisins sans lien avec la machine.
|
||||
|
||||
Archives locales : `build/Sanctuary-beta.084.mrpack` et
|
||||
`build/Sanctuary-Test-beta.084.mrpack`. Journaux :
|
||||
`build/multiblocs084-check.log` ; reçu d’intégrité :
|
||||
`build/multiblocs084-artifact.json`. Pack de textures beta.070 inchangé.
|
||||
Le canal publié reste inchangé.
|
||||
|
||||
Pour essayer rapidement : fabriquer la clé avec le motif `G G / G / S `
|
||||
(G = lingot d’or, S = bâton), ou obtenir `sanctuary:golden_wrench` en mode
|
||||
opérateur. Construire un cube plein de 27 barils ou de 27 fours et cliquer
|
||||
deux fois avec la clé. Maj-clic avec un autre bloc permet toujours d’en poser
|
||||
un contre la façade, notamment une trémie.
|
||||
|
||||
Limites de cette livraison : façade composée des blocs vanilla ; comparateurs
|
||||
sur le contenu du composant physique ; commandes de chauffe avancées et
|
||||
recettes composées différées. La sauvegarde est vérifiée par relecture disque
|
||||
et sérialisation native, pas par redémarrage complet d’une session à deux
|
||||
clients. Le déchargement réel d’une moitié de machine pendant un transfert
|
||||
reste à éprouver manuellement ; le refus d’accès aux chunks absents est testé.
|
||||
Aucun serveur dédié démarré, aucune EULA acceptée, aucun déploiement Prism
|
||||
ou monde personnel modifié.
|
||||
@@ -0,0 +1,204 @@
|
||||
# Multiblocs — reprise du catalogue
|
||||
|
||||
**Cahier rouvert le 15 septembre 2026. Conception, aucune machine livrée ici.**
|
||||
Le créateur demande de reprendre **tout le catalogue** avant de choisir un
|
||||
premier appareil. Ce document remet les décisions historiques à portée de la
|
||||
bêta ; les propositions nouvelles restent indiquées comme telles.
|
||||
Le [ticket MB-01](multiblocs-ticket.md) suit ce cadrage et prépare les lots de code.
|
||||
|
||||
**Reprise du 16 septembre 2026 : le premier duo choisi est Fourneau + Fût.**
|
||||
La séance porte sur leur conception avant intégration. Le créateur confirme
|
||||
ensuite l'inclusion de la clé, le Fût à 729 cases avec recherche et pages et
|
||||
le principe des fournées à combustible commun. Les gestes de la clé et les
|
||||
détails de chauffe restent ouverts ; les autres appareils restent au catalogue
|
||||
pour plus tard. Le créateur précise ensuite que tout It's Alive sera fusionné
|
||||
dans Sanctuary, mais ultérieurement ; le Fourneau pourrait remplacer sa
|
||||
cuisinière. Voir le [cadrage corrigé](multiblocs-itsalive-contrat.md).
|
||||
|
||||
Les huit propositions de la [recherche du 15 septembre](multiblocs-recherche.md)
|
||||
ont été **rejetées par le créateur**, jugées sans intérêt ou contraires au
|
||||
contrat Minecraft Vanilla. Le travail reprend sur le catalogue antérieur
|
||||
ci-dessous ; cette recherche ne constitue pas une liste d'implémentation.
|
||||
|
||||
## Les dossiers retrouvés
|
||||
|
||||
La source principale est le [cahier des machines que l'on construit](../../sanctuary-conception/docs/machines-multiblocs.md),
|
||||
dans le chantier de conception WG-26. Sa dernière modification est le commit
|
||||
`0e95da36ae7ebc99be5768a2f51e7be65aaf6702`, du 11 septembre 2026 :
|
||||
« valider l’assemblage volontaire avec la clé dorée ». Le fichier consulté
|
||||
ne comporte pas de modification locale.
|
||||
|
||||
Les dossiers complémentaires sont :
|
||||
|
||||
- [Redstone Language — ensemble](../../sanctuary-conception/docs/redstone-language-extensions.md) :
|
||||
catalogue des composants, transport, capteurs et connexions.
|
||||
- [Fiches des composants](../../sanctuary-conception/docs/redstone-language-composants.md) :
|
||||
contrôleur, terminal, afficheur et disquettes.
|
||||
- [Langage et machines](../../sanctuary-conception/docs/langage-et-machines.md) :
|
||||
programmation et installation d'expansion Galactium.
|
||||
- [Vision historique](../../sanctuary-conception/docs/vision.md) et
|
||||
[backlog historique](../../sanctuary-conception/docs/backlog.md) : recoupement
|
||||
des choix retenus et des propositions.
|
||||
|
||||
Ces liens pointent vers un autre worktree local. Le commit source est conservé
|
||||
pour retrouver la référence si ce worktree est déplacé. Les dossiers historiques
|
||||
restent consultés en lecture seule. Ils décrivent de la conception ; leur
|
||||
présence ne prouve pas une implémentation dans la bêta actuelle.
|
||||
|
||||
## Le principe déjà retenu
|
||||
|
||||
**Des blocs identiques réunissent leur fonction dans une construction plus grande.**
|
||||
Le joueur pose les composants, les assemble volontairement à la clé dorée,
|
||||
utilise une machine commune, puis peut retrouver ses blocs indépendants.
|
||||
|
||||
- **La pose ne fusionne rien.** Un cube de fours peut rester une batterie de
|
||||
fours ; neuf hoppers voisins peuvent garder neuf circuits.
|
||||
- **La clé à molette dorée assemble, dissocie et oriente.** Ces fonctions sont
|
||||
retenues ; recette, obtention, usure et gestes précis restent ouverts.
|
||||
- **Dissocier laisse les composants sur place.** La machine ne se transforme
|
||||
pas implicitement en un objet transportable. La séparation persiste après
|
||||
rechargement, même si la forme reste complète.
|
||||
- **Le stock actuel est conservé une seule fois.** Dissocier ne restaure pas
|
||||
des ingrédients déjà consommés. La répartition entre composants est à définir.
|
||||
- **L'usage local suffit.** Four, collecte ou musique restent utilisables sans
|
||||
ordinateur ; la programmation pourra coordonner les appareils.
|
||||
|
||||
## Catalogue principal
|
||||
|
||||
« Retenu » désigne une décision consignée dans l'ancien cahier, encore ouverte
|
||||
à une révision explicite pendant cette séance. Cela ne signifie pas « codé ».
|
||||
|
||||
| Appareil | Construction / fonction retenue | Ce qui reste à concevoir |
|
||||
| --- | --- | --- |
|
||||
| **Fourneau** — nom proposé | **27 fours ordinaires**, cube plein **3 × 3 × 3** ; chauffe commune, fournées et recettes à chaud. | Façade, orientation, entrées/sorties, chaleur, capacité, durée, rendement et première recette composée utile. |
|
||||
| **Fût** — nom retenu | **27 barils**, cube plein **3 × 3 × 3** ; stockage commun par pages et/ou défilement. | Capacité réelle, pagination, dessin, faces d'accès et répartition au démontage. |
|
||||
| **Grand baril de fermentation** — nom retenu | Plusieurs stacks d'**une même recette**, éventuellement à plusieurs ingrédients. | Composants, forme, volume, proportions du lot, conditions, vieillissement et temps hors ligne. |
|
||||
| **Présentoirs** — nom proposé | **9 objets par face de bloc**, **81 par façade 3 × 3**, **324 distincts sur quatre façades**. Une façade seule suffit. | Bloc support et fabrication, gestes de dépôt/retrait, modèles, consultation des cartes/plans, coût du rendu. |
|
||||
| **Méga-pistons** | **9 pistons en façade 3 × 3**, tête commune **3 × 3**, **course de 3 blocs** ; variante collante demandée. | Charge déplaçable, durée, collisions, traction, slime/miel, pièces déployées et interruption. |
|
||||
| **Trémie** — nom retenu | **9 hoppers à plat en 3 × 3**, surface de collecte commune et **45 cases**. Assemblage facultatif. | Sortie, débit, redstone, comparateur, insertion et orientations rendues au démontage. |
|
||||
| **Carillon** — nom retenu | Blocs musicaux **adjacents choisis**, pour accords et courtes séquences ; aucun carré ni nombre de neuf imposé. | Sélection, ordre, taille maximale, tempo, déclenchement, retour au début et réparation. |
|
||||
| **Métablit** — nom proposé par l'auteur | La fonction d'**établi collectif avec projet persistant** est une proposition. | Confirmer cette fonction, ses composants, sa grille et ce qu'elle apporte par rapport à l'établi et au crafter. |
|
||||
|
||||
### Fourneau : faire une fournée et transformer une recette
|
||||
|
||||
Les 27 fours forment un cube **plein**, centre compris. L'ancien four creux
|
||||
entouré de briques a été remplacé par ce choix. Le volume ne promet ni 27 fois
|
||||
la vitesse ni un multiplicateur de minerais.
|
||||
|
||||
L'ancien cahier propose deux usages explicites : **cuisson par lots**, avec
|
||||
les résultats des recettes admises, et **recette à chaud**, qui transforme
|
||||
ensemble plusieurs ingrédients. Neuf cases d'entrée et une réserve de
|
||||
combustible commune sont des propositions, pas des valeurs validées.
|
||||
Le premier alliage reste à choisir avec un objet utile auquel il sert.
|
||||
|
||||
L'ancien cahier prévoyait de reprendre le rôle du kitchen oven en conservant
|
||||
It's Alive autonome. **La direction actuelle remplace cette architecture :
|
||||
tout It's Alive sera fusionné dans Sanctuary, plus tard.** Le Fourneau pourrait
|
||||
alors remplacer la cuisinière, dont les usages précis restent à recenser.
|
||||
Concevoir le Fourneau pour pouvoir accueillir ces recettes ultérieurement ;
|
||||
le premier lot n'importe pas It's Alive et ne dépend pas d'une passerelle.
|
||||
|
||||
### Fût et fermentation : stockage et transformation
|
||||
|
||||
Le Fût conserve un stock fini, partagé entre toutes ses ouvertures ; changer
|
||||
de page ne change pas les objets accessibles à l'automatisation. **729 cases**,
|
||||
soit la somme des capacités de 27 barils ordinaires, ont été acceptées le
|
||||
16 septembre 2026, avec recherche et pages. Ce choix actuel ne provient pas
|
||||
de l'ancien cahier. La taille d'une page et l'intégration à l'inventaire
|
||||
Sanctuary restent à préciser.
|
||||
|
||||
La fermentation engage un lot d'une recette : les ingrédients ajoutés ensuite
|
||||
n'acquièrent pas rétroactivement son âge. Cette règle de lot est proposée dans
|
||||
l'ancien cahier. La composition du Fût n'est pas automatiquement celle de la
|
||||
cuve, et les règles du vin ne s'appliquent pas implicitement à la bière.
|
||||
|
||||
### Présentoirs : une collection réellement exposée
|
||||
|
||||
Les **81 objets d'une façade sont visibles ensemble**, sans pagination.
|
||||
Un angle peut présenter deux faces avec des contenus distincts ; dessus et
|
||||
dessous ne rajoutent pas implicitement des places aux 324 objets confirmés.
|
||||
Cartes, photos, plans, recettes, disquettes, disques et œufs étaient demandés.
|
||||
Un objet physique par emplacement est proposé ; accepter tous les autres
|
||||
objets reste à éprouver. La disponibilité des supports futurs ne doit pas
|
||||
être supposée dans la bêta.
|
||||
|
||||
### Méga-pistons : un mouvement commun
|
||||
|
||||
La tête parcourt réellement trois blocs. Les **27 cellules balayées** ne
|
||||
définissent pas une limite de 27 blocs poussables. L'ancien cahier propose
|
||||
un refus de tout le mouvement si une partie est bloquée et un démontage
|
||||
uniquement rétracté et immobile. Poids, traction et déplacement de conteneurs
|
||||
restent ouverts ; les états de neuf pistons indépendants ne suffisent pas
|
||||
à décrire cette mécanique.
|
||||
|
||||
### Trémie : collecter sur une surface
|
||||
|
||||
La proposition historique est **une sortie sous le centre**, raccordable à
|
||||
un hopper ordinaire pour poursuivre sur le côté. Cette sortie n'est pas encore
|
||||
retenue. Même distinction pour la commande : alimenter le centre pour bloquer
|
||||
toute la Trémie était proposé. **45 cases ne signifie pas débit multiplié par 9.**
|
||||
Une destination pleine doit laisser les objets dans la réserve ; une réserve
|
||||
pleine ne supprime pas les nouveaux drops.
|
||||
|
||||
### Carillon : construire un instrument
|
||||
|
||||
La forme libre adjacente remplace l'ancienne préférence pour une rangée.
|
||||
L'ordre de sélection comme ordre musical reste proposé. Le cahier compare :
|
||||
|
||||
- **Accord :** une impulsion joue les notes ensemble.
|
||||
- **Séquence :** une impulsion avance d'une note, avec tempo donné par la redstone.
|
||||
- **Variante à discuter :** une impulsion lance toute une phrase au tempo interne.
|
||||
|
||||
Chaque note garde son instrument, notamment son support propre. Les gestes
|
||||
d'accordage doivent rester accessibles. Le nombre de notes, les silences et
|
||||
la manière de réordonner la mélodie restent à concevoir.
|
||||
|
||||
## Pistes et installations voisines
|
||||
|
||||
| Piste | État retrouvé |
|
||||
| --- | --- |
|
||||
| **Veilleur** | Proposition : 9 observateurs en façade pour surveiller les positions devant eux et identifier laquelle a changé. |
|
||||
| **Batterie de distribution** | Proposition : 9 distributeurs en façade, sélection d'une bouche ou séquence avec leur stock réel. |
|
||||
| **Bassin de traitement, serre climatique, écluse, alambic** | En réserve ; pas automatiquement des assemblages de blocs identiques. |
|
||||
| **Presse à moule** | Écartée par l'auteur, fonction jugée déjà couverte par le crafter. |
|
||||
| **Afficheur programmable** | Cahier voisin : panneaux coplanaires, rectangle plein, texte continu et huit lignes par bloc de hauteur. Ses règles de regroupement ont leur propre contrat, à confronter à celles de la clé. |
|
||||
| **Installation Galactium** | Proposition voisine : construction autour d'un contrôleur, ressources et charge collective pour une expansion. Dépend du contrat d'expansion ; ne fait pas partie d'un premier appareil autonome. |
|
||||
|
||||
Le **terminal en titane reliant jusqu'à 128 coffres** est un réseau de stockage
|
||||
distinct du Fût. Le coffre 3 × 3 × 3 cité dans la discussion historique n'y a
|
||||
pas été identifié comme implémenté. Ces références ne deviennent pas de nouvelles
|
||||
machines validées par leur simple mention.
|
||||
|
||||
## Proposition pour reprendre la conception ensemble
|
||||
|
||||
Pour chaque appareil, partir de **ce que la construction change dans le jeu** :
|
||||
une chauffe partagée, un dépôt commun, une collection visible, une grande
|
||||
course, une surface de collecte ou une phrase musicale. Puis compléter une
|
||||
fiche courte :
|
||||
|
||||
1. **Construire :** blocs, forme, taille fixe ou variantes, orientation.
|
||||
2. **Reconnaître :** aspect assemblé, pièces actives, état visible, nom FR/EN.
|
||||
3. **Utiliser :** gestes, inventaire éventuel, recette ou commande, effet obtenu.
|
||||
4. **Raccorder :** faces d'entrée/sortie, hoppers et redstone.
|
||||
5. **Démonter :** casse, dissociation, récupération du contenu et reprise.
|
||||
6. **Progresser :** obtention, matériaux, intérêt par rapport aux blocs séparés.
|
||||
|
||||
Les trois premiers arbitrages transversaux proposés sont **l'aspect des blocs
|
||||
une fois réunis**, **les gestes de la clé** et **les tailles disponibles**.
|
||||
Le 3 × 3 et le 3 × 3 × 3 sont retenus pour certains appareils ; les étendre
|
||||
à toutes les machines effacerait le choix particulier du Carillon et des
|
||||
façades de collection. Aucun nouvel arbitrage n'est acquis à ce stade.
|
||||
|
||||
## État et vérification de cette reprise
|
||||
|
||||
Le catalogue et ses statuts ont été recoupés avec le cahier, la vision et le
|
||||
backlog de conception. La recherche ciblée dans les sources et les traductions
|
||||
de la bêta n'a pas retrouvé ces appareils sous leurs noms ; elle ne constitue
|
||||
pas un audit exhaustif de tout le code historique.
|
||||
La cible déclarée reste celle de `gradle.properties`, **26.3-pre-2** au moment
|
||||
de la lecture. Les anciennes vérifications d'API ne dispensent pas le futur
|
||||
ticket de vérifier ses usages sur cette version exacte.
|
||||
|
||||
Cette reprise modifie uniquement la documentation. Aucun build ni essai en
|
||||
jeu n'est nécessaire pour le catalogue ; les validations de gameplay figurent
|
||||
dans le ticket et seront exécutées lors des lots concernés.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Fourneau et It's Alive — fusion future dans Sanctuary
|
||||
|
||||
16 septembre 2026. Cadrage corrigé après clarification du créateur.
|
||||
Aucun portage, adaptateur ou changement de sauvegarde livré ici.
|
||||
|
||||
## Direction retenue
|
||||
|
||||
Le créateur souhaite intégrer **tout It's Alive dans Sanctuary**, mais
|
||||
**pas maintenant**. La proposition précédente de maintenir deux mods
|
||||
indépendants reliés par un module multiblocs partagé est abandonnée pour ce
|
||||
chantier. Aucune passerelle entre mods n'est un prérequis au premier lot.
|
||||
|
||||
Le lot actuel reste **clé dorée, Fourneau et Fût**. Le Fût à 729 cases avec
|
||||
recherche et pages et le principe du Fourneau à fournées et combustible commun
|
||||
sont retenus. Les gestes de la clé, la chauffe et les détails de fonctionnement
|
||||
restent à concevoir dans le ticket multiblocs.
|
||||
|
||||
## Place future du Fourneau
|
||||
|
||||
Le Fourneau **pourrait remplacer la cuisinière d'It's Alive** lors de la
|
||||
fusion. C'est la piste exprimée par le créateur (« je pense »), pas encore
|
||||
un remplacement détaillé et définitivement arbitré.
|
||||
|
||||
Ne pas réduire cette intention à la seule classe historique
|
||||
`KitchenOvenBlockEntity` : il faudra identifier précisément les usages de
|
||||
la cuisinière à reprendre au moment de la fusion. Les autres appareils,
|
||||
recettes et systèmes d'It's Alive seront examinés dans ce futur chantier.
|
||||
|
||||
Pour éviter une réécriture inutile, concevoir la cuisson du Fourneau de façon
|
||||
à pouvoir accueillir ultérieurement des recettes culinaires composées.
|
||||
Cette possibilité d'évolution n'impose aujourd'hui ni import de recettes,
|
||||
ni moteur culinaire complet, ni nouveau module autonome.
|
||||
|
||||
## Références techniques conservées pour plus tard
|
||||
|
||||
Le projet bêta charge Sanctuary, Demeure, JEI et le module de test ; It's
|
||||
Alive n'est pas déclaré dans `settings.gradle`.
|
||||
|
||||
La référence consultée en lecture seule est `../26.2/itsalive/` :
|
||||
|
||||
- son manifeste cible Minecraft `~26.2`, pas `26.3-pre-2` ;
|
||||
- `KitchenOvenBlockEntity` possède neuf cases d'ingrédients, une case de
|
||||
combustible et une sortie ;
|
||||
- `AbstractTimedCulinaryBlockEntity` recherche les recettes, calcule les lots,
|
||||
suit leur durée, consomme les ingrédients et produit les résultats.
|
||||
|
||||
Ces constats serviront à l'inventaire du futur portage, sans présumer que
|
||||
ce four constitue à lui seul toute la cuisinière visée par le créateur.
|
||||
La reprise des identifiants, des contenus déjà stockés et des travaux en
|
||||
cours devra recevoir son contrat de migration avant toute conversion.
|
||||
Les sources historiques et les sauvegardes restent inchangées.
|
||||
@@ -0,0 +1,290 @@
|
||||
# Multiblocs — recherche de compléments au catalogue
|
||||
|
||||
**Recherche du 15 septembre 2026, pour MB-01. Propositions rejetées par le créateur.**
|
||||
|
||||
**Décision après présentation :** les huit candidats sont écartés, jugés sans
|
||||
intérêt ou contraires au contrat Minecraft Vanilla. Le contenu ci-dessous est
|
||||
conservé comme historique de recherche, y compris son ordre proposé désormais
|
||||
sans valeur de sélection. Reprendre le [catalogue antérieur](multiblocs-conception.md)
|
||||
pour déterminer la liste d'implémentation ; ne pas réintroduire ces propositions
|
||||
comme candidates actives sans nouvelle demande du créateur.
|
||||
La demande est de compléter le [catalogue retrouvé](multiblocs-conception.md)
|
||||
avant de déterminer ensemble la liste d'implémentation. Aucune nouvelle entrée,
|
||||
forme, recette ou priorité ci-dessous n'est une décision du créateur.
|
||||
|
||||
## Conclusion proposée avant rejet — historique
|
||||
|
||||
Huit candidats méritent une discussion : **Atelier de taille, Alambic,
|
||||
Compostière, Citerne, Broyeur, Tisserie, Discothèque et Antenne sculk**.
|
||||
L'Alambic précise une piste déjà en réserve. La Citerne donne un usage de
|
||||
stockage au thème des bassins, distinct d'un bassin de transformation.
|
||||
|
||||
Je commencerais la sélection des ajouts par **Atelier de taille, Alambic et
|
||||
Compostière**, puis **Discothèque** pour un usage collectif musical. La
|
||||
**Citerne** devient intéressante si l'on souhaite des installations de liquides.
|
||||
Le **Broyeur** mérite un arbitrage sur les ressources avant d'être retenu.
|
||||
**Tisserie** et **Antenne sculk** ont un potentiel plus spécialisé à éprouver.
|
||||
|
||||
Leur intérêt vient respectivement des commandes de chantier, des préparatifs
|
||||
d'expédition, de la valorisation des récoltes, d'une programmation musicale,
|
||||
d'une réserve liquide, de transformations minérales, de motifs textiles et
|
||||
de la lecture des vibrations. Les rangs ci-dessus sont mon appréciation de
|
||||
conception, pas des mesures de popularité ou de temps de développement.
|
||||
|
||||
## Ce que les références apportent
|
||||
|
||||
Recherche dans les documents des projets et de Mojang. Les exemples attestent
|
||||
des principes de jeu ; **leur compatibilité avec Fabric 26.3-pre-2 n'a pas été
|
||||
évaluée et aucun de ces mods/plugins n'est proposé comme dépendance**.
|
||||
|
||||
| Référence consultée | Fait utile à la conception | Application proposée à Sanctuary |
|
||||
| --- | --- | --- |
|
||||
| [Create — Mechanical Crafter](https://github.com/Creators-of-Create/Create/wiki/Mechanical-Crafter), ancien wiki du projet, page de 2021 | Liaisons et direction se règlent à la clé ; des groupes adjacents peuvent rester distincts. | Faire voir l'appartenance et les raccords, garder les ingrédients lisibles dans la construction. |
|
||||
| [Create — Crushing Wheels](https://github.com/Creators-of-Create/Create/wiki/Crushing-Wheels), page de 2024 | Deux roues transforment des objets introduits entre elles, avec des recettes de broyage. | Donner au Broyeur un geste et une transformation identifiables ; choisir nos propres recettes et rendements. |
|
||||
| [Create — Mechanical Pump](https://github.com/Creators-of-Create/Create/wiki/Mechanical-Pump), page de 2021 | Pompage, sens et tuyaux assurent le transfert physique des fluides. | Distinguer réserve, transfert et transformation ; une Citerne seule n'est pas une pompe. |
|
||||
| [Botania — Botanical Brewery, dans le Lexicon](https://botaniamod.net/lexicon.html) | La préparation rassemble récipient, ingrédients et mana avant de produire un breuvage. | Montrer le lot engagé et son état dans un atelier ; mana et breuvages Botania ne sont pas repris. |
|
||||
| [CraftBook — Bridge](https://craftbook.enginehub.org/en/3.x/mechanics/bridge/) | Une construction de pont peut être commandée par panneau ou redstone. | Concevoir des usages architecturaux des méga-pistons : passage, porte et quai mobile. |
|
||||
| [Immersive Engineering — changelog 1.21.1](https://github.com/BluSunrize/ImmersiveEngineering/blob/1.21.1/changelog.md) | Le projet documente des corrections de pertes de contenu à la casse de machines et de problèmes de fluides au chargement des chunks. | Mettre conservation du contenu et reprise dans le premier scénario de réception, avant de multiplier les appareils. |
|
||||
|
||||
Ces anciens documents de Create sont utilisés comme références historiques de
|
||||
conception. Le [wiki GitHub annonce son déplacement](https://github.com/Creators-of-Create/Create/wiki)
|
||||
vers un nouveau site ; ses pages récentes n'ont pas été récupérées lors de cette
|
||||
recherche. Les descriptions ci-dessus ne prétendent donc pas résumer la dernière
|
||||
version distribuée de Create.
|
||||
|
||||
## Huit fiches candidates
|
||||
|
||||
Les gabarits et noms anglais sont proposés pour rendre les idées examinables.
|
||||
Ils restent modifiables. Tous les regroupements suivent la clé volontaire et
|
||||
la dissociation durable du catalogue. Aucun appareil ne fonctionne gratuitement
|
||||
du seul fait qu'il est plus grand.
|
||||
|
||||
### 1. Atelier de taille / Stoneworks
|
||||
|
||||
**Base vérifiée :** le tailleur de pierre transforme un matériau selon une
|
||||
recette choisie. [Présentation Mojang du tailleur de pierre](https://www.minecraft.net/de-de/article/block-week--stonecutter).
|
||||
|
||||
**Proposition :** neuf tailleurs de pierre en plateau **3 × 3** donnent un
|
||||
atelier de préparation de chantier. Le joueur prépare une commande avec
|
||||
plusieurs sorties et quantités : par exemple escaliers, dalles et murets dans
|
||||
la même pierre. L'atelier répartit les matériaux apportés entre les recettes,
|
||||
montre ce qui manque et s'arrête aux quantités demandées.
|
||||
|
||||
Le bénéfice est une commande persistante avec approvisionnement commun,
|
||||
utilisable à la main et par hoppers. Un raccord ultérieur aux besoins d'un plan
|
||||
peut préparer la liste, mais le premier atelier fonctionne par saisie manuelle.
|
||||
Il produit des objets ; les règles de pose et la compétence Construction
|
||||
continuent de s'appliquer lors du chantier.
|
||||
|
||||
**À décider :** nombre de lignes de commande, coût du travail, cadence et
|
||||
sorties. Réutiliser les recettes admises ; ne pas ouvrir silencieusement le
|
||||
catalogue encore inconnu du joueur. **Essai :** deux commandes concurrentes,
|
||||
stock insuffisant puis complété, sortie pleine, reprise sans surproduction.
|
||||
|
||||
### 2. Alambic / Brewing Array
|
||||
|
||||
**Base vérifiée :** l'alambic vanilla combine fioles et ingrédients avec du
|
||||
combustible. [Présentation Mojang de l'alambic](https://www.minecraft.net/en-us/article/taking-inventory--brewing-stand).
|
||||
|
||||
**Proposition :** neuf alambics en plateau **3 × 3** deviennent un atelier
|
||||
commun de potions. Préparer un lot homogène, afficher ses étapes, puis les
|
||||
enchaîner avec les ingrédients physiquement déposés. Exemple : préparer les
|
||||
potions de résistance au feu d'une expédition et les récupérer terminées.
|
||||
Une capacité de **27 fioles** est une proposition issue des trois fioles par
|
||||
composant, pas une capacité déjà approuvée.
|
||||
|
||||
Le regroupement doit apporter la préparation d'un lot et le suivi des étapes.
|
||||
Les recettes de potions et leurs effets restent ceux admis par le jeu.
|
||||
La fermentation alimentaire garde son propre appareil et sa temporalité.
|
||||
La forme et le rôle de cet Alambic précisent la piste ancienne sans l'imposer.
|
||||
|
||||
**À décider :** consommation par groupe de fioles, partage de chauffe,
|
||||
cadence et ordre des ingrédients. Une seule dose ne traite pas arbitrairement
|
||||
27 fioles. **Essai :** un lot partiel, ingrédient manquant au milieu, retrait
|
||||
pendant le cycle et redémarrage ; aucune étape ni consommation doublée.
|
||||
|
||||
### 3. Compostière / Composting Bed
|
||||
|
||||
**Base vérifiée :** le composteur accepte des matières végétales et produit
|
||||
de la poudre d'os ; leur efficacité varie. [Présentation Mojang du composteur](https://www.minecraft.net/en-us/article/block-month--composter).
|
||||
|
||||
**Proposition :** neuf composteurs à plat **3 × 3**, une réserve commune
|
||||
alimentée par les récoltes et une maturation visible. La construction reçoit
|
||||
un lot mélangé d'objets compostables ; la poudre d'os terminée est récupérée
|
||||
dans une sortie identifiée. Le travail peut accompagner un jardin partagé.
|
||||
|
||||
Le gain est la gestion d'un lot et la collecte de ses résultats, avec un
|
||||
remplissage lisible sur une grande surface. Le premier essai conserve les
|
||||
matières admises et compare le résultat à neuf composteurs indépendants.
|
||||
Un compost spécial, des engrais ou des sols enrichis seraient une extension
|
||||
à concevoir avec It's Alive ! ; ils ne sont pas requis pour cette première fiche.
|
||||
|
||||
**À décider :** progression commune ou neuf cellules de maturation derrière
|
||||
le stock commun, rendement, cadence et éventuel travail manuel.
|
||||
**Essai :** graines acceptées, objet non compostable refusé, mélange de stocks,
|
||||
sortie pleine et maturation conservée après déchargement.
|
||||
|
||||
### 4. Citerne / Cistern
|
||||
|
||||
**Base vérifiée :** les chaudrons stockent notamment eau et lave ; certaines
|
||||
fonctions de potions et de teinture de leur présentation appartiennent à
|
||||
Bedrock. [Présentation Mojang des chaudrons](https://www.minecraft.net/en-us/article/cauldron).
|
||||
|
||||
**Proposition :** neuf chaudrons en **3 × 3** forment une réserve commune
|
||||
avec niveau visible et point de puisage. Premier usage proposé : l'eau,
|
||||
apportée et retirée avec de vrais récipients. La lave serait un autre contenu
|
||||
admis, à décider avec son comportement de rupture. Un bassin unique conserve
|
||||
un seul contenu compatible ; aucune conversion d'un liquide en un autre.
|
||||
|
||||
La Citerne peut devenir le réservoir d'une cuisine ou d'un atelier de potions.
|
||||
Son intérêt augmente avec les futures connexions physiques ; stockage et
|
||||
pompage restent deux fonctions distinctes. La capacité ne doit pas seulement
|
||||
dupliquer celle d'un coffre de seaux sans avantage d'usage perceptible.
|
||||
|
||||
**À décider :** capacité, unités, pluie éventuelle, raccords, récipients et
|
||||
restitution au démontage. Les hoppers déplacent les objets récipients selon
|
||||
le contrat ; ils ne deviennent pas implicitement des tuyaux de liquide.
|
||||
**Essai :** remplir, prélever une quantité, reprendre après sauvegarde et
|
||||
dissocier sans créer de seaux ni déverser involontairement tout le stock.
|
||||
|
||||
### 5. Broyeur / Crusher
|
||||
|
||||
**Référence :** Create documente le broyage de cobblestone en gravier et de
|
||||
gravier en sable dans sa liste de transformations. [Crushing Wheels](https://github.com/Creators-of-Create/Create/wiki/Crushing-Wheels).
|
||||
|
||||
**Proposition :** deux rangées de trois meules, séparées par un passage,
|
||||
forment deux rouleaux qui traitent des matériaux déposés. Une autre forme
|
||||
compacte reste possible. Il s'agit d'une **nouvelle fonction** donnée aux
|
||||
meules assemblées, à valider explicitement : la meule vanilla n'est pas un
|
||||
broyeur de roche. Le trajet des objets explique visuellement la transformation.
|
||||
|
||||
Des granulats pour la construction seraient un premier usage. La mouture des
|
||||
aliments reste à coordonner avec le moulin déjà prévu dans It's Alive !.
|
||||
Recettes de recyclage, pigments et traitement des minerais sont des extensions
|
||||
distinctes ; aucun doublement de minerai n'est adopté par la référence à Create.
|
||||
|
||||
**Arbitrage central :** la chaîne pierre renouvelable → gravier → sable
|
||||
modifierait l'intérêt des gisements et de l'exploration dans Sanctuary.
|
||||
Décider si ce résultat est souhaité, puis choisir rendement et coût réel.
|
||||
**Essai :** bilan de matière d'une recette, cadence, arrêt et boucles de
|
||||
recyclage ; le retour ne doit pas fabriquer plus de matière qu'il n'en engage.
|
||||
|
||||
### 6. Tisserie / Textile Workshop
|
||||
|
||||
**Base vérifiée :** le métier à tisser applique des motifs et couleurs
|
||||
successifs aux bannières. [Présentation Mojang du métier à tisser](https://www.minecraft.net/en-us/article/block-week--loom).
|
||||
|
||||
**Proposition :** six métiers à tisser en façade **3 × 2** deviennent un
|
||||
atelier qui conserve une composition de motifs, ses couleurs et une commande
|
||||
de bannières. Plusieurs joueurs apportent supports, colorants et motifs ; la
|
||||
construction présente un grand aperçu et prépare la série demandée.
|
||||
|
||||
L'intérêt à éprouver est l'édition persistante d'une composition et son suivi
|
||||
matériel, par exemple pour une faction. Une simple copie de bannière ne suffit
|
||||
pas à justifier cette machine. Textile culinaire, tentures et nouveaux tissus
|
||||
ne deviennent pas des dépendances obligatoires de cette proposition.
|
||||
|
||||
**À décider :** avantage sur les outils de bannière existants, format du modèle,
|
||||
droits sur les motifs, consommation et lien avec les collections.
|
||||
**Essai :** réalisation d'un motif composé, réapprovisionnement et reprise ;
|
||||
une bannière rare ou un motif manquant ne doit pas être copié gratuitement.
|
||||
|
||||
### 7. Discothèque / Record Console
|
||||
|
||||
**Base vérifiée :** Minecraft Java permet déjà les interactions des hoppers
|
||||
et droppers avec les jukebox, ainsi qu'un signal pendant la lecture.
|
||||
[Notes officielles Java 1.19.4](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-19-4).
|
||||
|
||||
**Proposition :** plusieurs jukebox adjacents choisis, par exemple un meuble
|
||||
**3 × 2**, forment une collection écoutable avec ordre de lecture, prochain
|
||||
morceau, arrêt et répétition. Chaque disque reste un objet réel ; une seule
|
||||
lecture commune est émise. Le meuble pourrait montrer les disques disponibles.
|
||||
|
||||
La valeur est le choix et la modification d'une programmation musicale
|
||||
sur place. Le Carillon compose des notes ; la Discothèque joue des disques.
|
||||
Une simple boucle automatique serait trop proche des montages déjà possibles.
|
||||
|
||||
**À décider :** sélection du groupe, capacité, interface, commandes redstone,
|
||||
reprise du morceau et portée sonore. **Essai :** modifier la liste pendant
|
||||
l'écoute, rejoindre à deux joueurs, retirer le disque actif et casser un
|
||||
composant ; pas de musique fantôme ni de disque perdu.
|
||||
|
||||
### 8. Antenne sculk / Vibration Array
|
||||
|
||||
**Base vérifiée :** un capteur sculk calibré filtre les vibrations suivant
|
||||
un signal ; Mojang documente également la réémission avec l'améthyste.
|
||||
[Snapshot Java 23w12a](https://www.minecraft.net/de-de/article/minecraft-snapshot-23w12a).
|
||||
|
||||
**Proposition :** neuf capteurs calibrés en surface **3 × 3**, avec lecture
|
||||
commune des catégories reçues et petite mémoire des derniers événements.
|
||||
Plusieurs catégories peuvent être surveillées et affichées ensemble. Exemple :
|
||||
une porte réagit à une courte suite de gestes ; l'instrument permet de voir
|
||||
lequel a été reçu ou manque. La durée de mémoire reste bornée.
|
||||
|
||||
Le Veilleur observe des changements de blocs devant lui. L'Antenne apporte
|
||||
une lecture d'événements vibratoires ; elle ne révèle pas automatiquement le
|
||||
nom d'un joueur, ses coordonnées ou des lieux hors de portée. Le simple gain
|
||||
de rayon ne suffit pas à justifier neuf composants.
|
||||
|
||||
**À décider :** filtres, lecture visible, sorties et distinction avec un
|
||||
contrôleur branché sur des capteurs ordinaires. **Essai :** catégories distinctes,
|
||||
obstacles de laine, événements simultanés, boucles provoquées par la propre
|
||||
machine et expiration de la mémoire.
|
||||
|
||||
## Compléter les machines existantes avant de les multiplier
|
||||
|
||||
| Idée examinée | Recommandation pour notre catalogue |
|
||||
| --- | --- |
|
||||
| Grande fonderie, four à alliages, grand fumoir | Examiner d'abord les modes et recettes du **Fourneau** ; un autre bâtiment doit offrir un procédé distinct. |
|
||||
| Silo à matériau unique | Examiner un réglage du **Fût** avant d'ajouter un deuxième stockage volumineux. |
|
||||
| Table de fabrication géante | Préciser le **Métablit** ; le [crafter vanilla automatise déjà la fabrication](https://www.minecraft.net/en-us/article/crafting-crafter). Une grille plus grande demande des recettes et un usage propres. |
|
||||
| Pont rétractable, grande porte, élévateur court | En faire des montages de réception des **méga-pistons**, avec leur course de trois blocs. Un ascenseur à longue course demanderait un autre contrat. |
|
||||
| Mur de cartes, bibliothèque de plans | Éprouver les **présentoirs** et les plans existants avant de créer une table cartographique supplémentaire. |
|
||||
| Grand afficheur lumineux | Confronter à l'**afficheur programmable** déjà conçu ; ne pas créer deux systèmes d'écran par simple changement de nom. |
|
||||
| Presse à moule | Conserver le choix historique d'abandon. La présence de presses dans d'autres mods ne réouvre pas cette décision à elle seule. |
|
||||
| Serre qui accélère tout, carrière automatique, générateur de ressources | Réserver une conception propre avec coûts et conséquences sur exploration, métiers, progression et terrain. |
|
||||
|
||||
## Ancienne base de liste d'implémentation — non retenue
|
||||
|
||||
**Ordre conseillé, encore à choisir ensemble.** Chaque appareil reste un lot
|
||||
jouable autonome ; les groupes ci-dessous rapprochent les usages et dépendances.
|
||||
La clé arrive avec le premier appareil. Aucune branche de code n'est créée ici.
|
||||
|
||||
| Groupe proposé | Entrées | Pourquoi cet ordre / condition |
|
||||
| --- | --- | --- |
|
||||
| **1. Première construction** | **Clé dorée + Fût** | Éprouver assemblage volontaire, contenu commun, accès à deux et dissociation avec une fonction immédiatement utilisable. |
|
||||
| **2. Atelier quotidien** | **Trémie, Fourneau, Atelier de taille, Compostière** | Faire circuler, transformer et préparer les matériaux. Le Fourneau exige sa première recette composée utile et un choix de chauffe. |
|
||||
| **3. Lieux collectifs** | **Présentoirs, Carillon, Discothèque** | Donner des usages visibles aux collections et à la musique ; ces lots peuvent avancer indépendamment des recettes industrielles. |
|
||||
| **4. Préparation et liquides** | **Alambic, fermentation, Citerne** | Fixer les lots, consommations et règles de temps. La fermentation dépend du contrat culinaire ; l'Alambic peut fonctionner avec ses fioles sans attendre la Citerne. |
|
||||
| **5. Mécanismes** | **Méga-pistons, Veilleur, batterie de distribution** ; **Antenne sculk** à départager | Éprouver grandes constructions mobiles, observation et commandes. L'Antenne doit démontrer son apport face aux capteurs et au futur contrôleur. |
|
||||
| **6. Recettes et ateliers spécialisés** | **Broyeur, Métablit, Tisserie** | Choisir respectivement l'effet sur les ressources, le rôle du projet collectif et l'avantage d'une composition textile persistante. |
|
||||
|
||||
Les familles historiques retenues gardent leur statut ; les nouvelles entrées
|
||||
de cette table sont des candidates. Si la première livraison doit montrer
|
||||
prioritairement une transformation plutôt qu'un stockage, **Fourneau + clé**
|
||||
reste une alternative cohérente au Fût. Ce choix appartient à la prochaine
|
||||
discussion, pas à la recherche seule.
|
||||
|
||||
## Vérification de la cible et limites de l'étude
|
||||
|
||||
Lecture complémentaire du JAR de sources Minecraft **26.3-pre-2** déjà présent
|
||||
dans le cache Gradle local, sans copie des dépendances dans Git :
|
||||
|
||||
- `StonecutterMenu` : recettes à entrée unique, sélection et résultat natif.
|
||||
- `BrewingStandBlockEntity` : trois positions de fioles ; combustible lu via
|
||||
`DataComponents.BREWING_FUEL`, durée influencée par son multiplicateur.
|
||||
- `ComposterBlock` : couches apportées par le composant `Compostable` et son
|
||||
contexte, plutôt qu'une table d'efficacité à recopier depuis un vieux guide.
|
||||
- `CalibratedSculkSensorBlockEntity` : filtre de fréquence et rayon d'écoute
|
||||
local de 16 blocs ; aucune portée de multibloc décidée par cette lecture.
|
||||
- `JukeboxBlockEntity` : lecteur de morceau, objet conservé et données de reprise.
|
||||
|
||||
Ces lectures vérifient des points précis et ne constituent pas un prototype
|
||||
de multibloc ni une mesure de performance. Le modèle de chaudron de Bedrock
|
||||
n'est pas une preuve de comportement Java ; le futur contrat des liquides doit
|
||||
être vérifié sur la cible avant implémentation.
|
||||
|
||||
La recherche a porté sur les fonctions, les gestes, les recouvrements et les
|
||||
dépendances. Le coût relatif d'implémentation est une appréciation technique ;
|
||||
aucune estimation de durée ni compatibilité de mod externe n'est promise.
|
||||
Liens locaux et diff documentaire vérifiés. Aucun code ou binaire modifié.
|
||||
@@ -0,0 +1,153 @@
|
||||
# MB-01 — Cadrer le catalogue des machines multiblocs
|
||||
|
||||
**Ticket local ouvert le 15 septembre 2026 — conception en cours.**
|
||||
Le créateur demande de reprendre tout le catalogue avant de sélectionner le
|
||||
premier lot d'implémentation. Le [cahier de reprise](multiblocs-conception.md)
|
||||
conserve les sources, les anciennes décisions et les propositions ouvertes.
|
||||
|
||||
**Décision sur le complément :** les huit candidats de la
|
||||
[recherche sourcée](multiblocs-recherche.md) sont rejetés par le créateur,
|
||||
jugés sans intérêt ou contraires au contrat Minecraft Vanilla. Reprendre
|
||||
la liste antérieure ; les recommandations de cette recherche ne sont pas retenues.
|
||||
|
||||
Branche documentaire réservée : `codex/multiblocs-ticket`.
|
||||
Elle est créée ; le checkout de travail existant reste sur sa branche en cours.
|
||||
Aucun numéro `beta.xxx` réservé et aucune issue distante publiée.
|
||||
|
||||
## Ce que le joueur doit pouvoir faire à terme
|
||||
|
||||
Construire une machine à partir de blocs, choisir de les réunir avec la clé
|
||||
à molette dorée, bénéficier d'une fonction commune visible, puis retrouver
|
||||
les composants indépendants sans perdre ni dupliquer le stock actuel.
|
||||
Les blocs simplement voisins gardent leur fonctionnement individuel.
|
||||
|
||||
## Résultat vérifiable de ce ticket de conception
|
||||
|
||||
Obtenir un catalogue dont chaque entrée indique sa fonction, son statut,
|
||||
ses choix ouverts et un scénario jouable de réception. Compléter ensuite les
|
||||
fiches retenues et ouvrir des tickets d'implémentation autonomes avec leur
|
||||
branche. **MB-01 ne déclare pas toutes les machines prêtes à coder en un seul lot.**
|
||||
|
||||
**Sélection du 16 septembre 2026 : Fourneau et Fût.** Le créateur demande
|
||||
de réfléchir à leur intégration. Ce duo constitue le premier périmètre à
|
||||
concevoir ; aucun ordre de livraison entre les deux ni valeur de fonctionnement
|
||||
supplémentaire n'est encore validé. Branche de cette reprise documentaire :
|
||||
`codex/multiblocs-fourneau-fut-cadrage`.
|
||||
|
||||
Confirmation du créateur : **la clé dorée fait partie du premier lot**.
|
||||
Les cubes pleins de 27 fours et de 27 barils, la cuisson par fournées avec
|
||||
combustible commun et le Fût à **729 cases avec recherche et pages** sont
|
||||
acceptés. Les neuf entrées du Fourneau, ses durées, son débit, les gestes et
|
||||
la recette de la clé restent à préciser.
|
||||
|
||||
**Clarification : tout It's Alive sera intégré à Sanctuary ultérieurement,
|
||||
pas dans ce lot.** Le Fourneau pourrait alors remplacer la cuisinière ; le
|
||||
détail de ce remplacement reste à définir. Le [cadrage corrigé](multiblocs-itsalive-contrat.md)
|
||||
remplace la proposition de passerelle entre mods autonomes. Le premier lot
|
||||
multiblocs ne dépend pas d'un portage immédiat d'It's Alive.
|
||||
|
||||
## Périmètre à passer en revue
|
||||
|
||||
Fourneau, Fût, grand baril de fermentation, présentoirs, méga-pistons, Trémie,
|
||||
Carillon et Métablit. Réexaminer séparément Veilleur et batterie de distribution.
|
||||
Conserver les installations en réserve et les idées écartées avec leur statut.
|
||||
Rapprocher les contrats de l'afficheur et de Galactium lorsque nécessaire,
|
||||
sans imposer leur livraison aux machines utilisables localement.
|
||||
|
||||
## Décisions à produire avant les tickets de code
|
||||
|
||||
- [ ] Pour chaque entrée, confirmer maintien, révision, report ou abandon.
|
||||
- [x] Compléments de la recherche arbitrés : les huit propositions sont
|
||||
rejetées. Aucun de ces ajouts n'entre dans la liste d'implémentation.
|
||||
- [x] Premier périmètre sélectionné : Fourneau et Fût, le 16 septembre 2026.
|
||||
- [ ] Compléter les six rubriques de la fiche du cahier : construction,
|
||||
apparence, usage, raccords, démontage et progression. Distinguer les nombres
|
||||
retenus des valeurs d'essai, et donner les noms et libellés FR/EN nécessaires.
|
||||
- [ ] Fixer les gestes de la clé sans conflit entre tourner, sélectionner,
|
||||
assembler et dissocier ; décrire l'aperçu, l'annulation et les refus visibles.
|
||||
- [ ] Fixer obtention, recette et éventuelle usure de la clé. Définir les premiers
|
||||
blocs orientables, la conservation de leur état et les opérations admissibles.
|
||||
- [ ] Décider apparence après assemblage, orientations et éventuelles tailles
|
||||
supplémentaires, appareil par appareil.
|
||||
- [ ] Choisir un premier résultat jouable et rédiger son ticket d'implémentation
|
||||
avec valeurs d'essai, dépendances réelles et branche `codex/<sujet>`.
|
||||
|
||||
## Découpage d'implémentation proposé
|
||||
|
||||
Fourneau et Fût sont les deux lots retenus pour la conception de la première
|
||||
intégration. Les autres lignes restent des **lots candidats**, sans ordre de
|
||||
livraison validé ni sous-ticket distant publié. Le premier appareil livré emporte
|
||||
la clé et le contrat commun nécessaires à son usage réel ; éviter de livrer
|
||||
un mécanisme d'assemblage sans machine utilisable.
|
||||
|
||||
| Lot candidat / branche prévue | Résultat jouable et vérifiable |
|
||||
| --- | --- |
|
||||
| **Fourneau** — `codex/multibloc-fourneau` | Assembler 27 fours, cuire une fournée, retirer le résultat, arrêter/reprendre et dissocier. Chauffe et capacités à préciser. Prévoir l'accueil futur de recettes composées ; la fusion culinaire d'It's Alive est différée. |
|
||||
| **Fût** — `codex/multibloc-fut` | Assembler 27 barils, déposer et retrouver un stock commun sur plusieurs pages, l'utiliser à deux joueurs et avec un hopper, puis dissocier sans perte. |
|
||||
| **Fermentation** — `codex/multibloc-fermentation` | Engager plusieurs stacks d'une recette aux bonnes proportions, suivre le lot et récupérer une seule production. Dépend d'un contrat culinaire et temporel explicite. |
|
||||
| **Présentoirs** — `codex/multibloc-presentoirs` | Remplir une façade de 81 objets distincts visibles et quatre façades totalisant 324, consulter les supports admis et récupérer les objets d'un angle cassé une seule fois. |
|
||||
| **Méga-pistons** — `codex/multibloc-mega-pistons` | Commander une tête 3 × 3 sur une course animée de trois blocs, vérifier un obstacle intermédiaire, la variante collante et une interruption. |
|
||||
| **Trémie** — `codex/multibloc-tremie` | Choisir entre neuf circuits individuels et une collecte commune de 45 cases, constater la sortie et son débit, le verrouillage retenu et le cas d'une destination pleine. |
|
||||
| **Carillon** — `codex/multibloc-carillon` | Choisir un groupe adjacent, jouer un accord puis une courte séquence selon le mode retenu, réaccorder une note et garder un groupe voisin indépendant. |
|
||||
| **Métablit** — `codex/multibloc-metablit` | Si la fonction est retenue : préparer un projet persistant à deux joueurs, quitter/revenir, apporter les pièces manquantes et produire un résultat unique. |
|
||||
|
||||
## Contrat commun requis par la première implémentation
|
||||
|
||||
Le code reste côté serveur pour l'appartenance des composants, les stocks,
|
||||
les cycles et les opérations. Un bloc ne peut appartenir à deux machines.
|
||||
La reconnaissance des formes est bornée ; elle ne charge pas des chunks
|
||||
distants. Les protections effectivement présentes sont respectées.
|
||||
|
||||
Avant de créer des données persistantes, écrire un **contrat de sauvegarde
|
||||
et de migration explicite** : identité de machine, composants et orientations,
|
||||
contenu actuel, travail engagé, version, suspension et reprise. Charger une
|
||||
ancienne construction ne doit pas l'assembler. Définir le devenir des matières
|
||||
en cas de casse, d'explosion, de dissociation et d'interruption, ainsi que les
|
||||
opérations refusées. Un démontage impossible faute de place attend une
|
||||
récupération explicite ; il ne supprime pas le surplus.
|
||||
|
||||
Décharger une partie suspend le travail concerné sans produire deux fois ni
|
||||
rattraper silencieusement une durée hors ligne. Une éventuelle fermentation
|
||||
hors ligne reçoit son propre contrat. Le démontage conserve l'état courant,
|
||||
y compris les combustibles déjà dépensés et les résultats déjà obtenus.
|
||||
|
||||
La représentation technique des composants reste à choisir. Aucun nouveau
|
||||
registre ni renommage d'identifiant `sanctuary:*` n'est décidé par ce ticket.
|
||||
Les plans et les recettes accessibles conservent leurs responsabilités
|
||||
actuelles. La fusion future d'It's Alive ne rend pas ses fonctions disponibles
|
||||
dans ce lot du seul fait de la conception historique.
|
||||
|
||||
## Vérifications exigées des futurs lots
|
||||
|
||||
- [ ] Sans clé, poser une forme complète conserve ses blocs indépendants.
|
||||
Assembler, dissocier puis recharger conserve le choix explicite du joueur.
|
||||
- [ ] Une forme incomplète, un chevauchement, une mauvaise orientation ou un
|
||||
accès refusé donne une explication sans modification partielle.
|
||||
- [ ] Deux interactions simultanées, un transfert automatique et deux façades
|
||||
ouvertes travaillent sur le même stock ; aucune duplication de contenu.
|
||||
- [ ] Inventaires pleins, stocks préexistants, rupture, déchargement partiel,
|
||||
redémarrage et réassemblage conservent matières, état et résultats selon
|
||||
le contrat retenu. Tester aussi les composants répartis entre deux chunks.
|
||||
- [ ] Le scénario propre à la machine fonctionne en survie par la voie
|
||||
d'obtention retenue, avec interface FR/EN, puis à deux joueurs.
|
||||
- [ ] Les tests utiles et `./gradlew check build` passent. Si la distribution
|
||||
change, `./gradlew assemblePack` passe ; livraison et incrément mod/pack
|
||||
suivent [le versionnement](versioning.md). Aucun déploiement personnel
|
||||
n'est implicite dans ce ticket.
|
||||
|
||||
## État de la préparation
|
||||
|
||||
Catalogue retrouvé, décisions historiques recoupées, scénarios proposés et
|
||||
références locales vérifiées. Fourneau et Fût sélectionnés ; **leurs modalités
|
||||
de fonctionnement et les autres arbitrages ci-dessus restent ouverts.**
|
||||
La demande courante porte sur la conception : aucun code, binaire, monde ou
|
||||
format de sauvegarde modifié, aucun test de gameplay présenté comme exécuté.
|
||||
|
||||
## Livraison du premier lot — beta.084
|
||||
|
||||
La demande a évolué du cadrage à l’implémentation le 16 septembre 2026.
|
||||
Clé, Fût et Fourneau sont implémentés sur `codex/multiblocs-beta084` ; le
|
||||
[contrat de livraison](multiblocs-beta084.md) précise les valeurs retenues,
|
||||
la sauvegarde, les tests et leurs limites. Il remplace l’état documentaire
|
||||
« aucun code modifié » ci-dessus pour ce premier lot uniquement. Les autres
|
||||
multiblocs et la fusion It's Alive restent différés.
|
||||
+217
@@ -13,6 +13,223 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## beta.092 — Clic molette des super pistons
|
||||
|
||||
Les packs locaux normal/Test beta.092 corrigent la sélection des composants
|
||||
de super piston. Dépendances et textures beta.090 conservées.
|
||||
[Contrat et vérifications](multiblock-pick-beta092.md). Aucun canal publié
|
||||
ni instance personnelle modifié.
|
||||
|
||||
## beta.091 — Interactions des passagers
|
||||
|
||||
Les packs locaux normal/Test beta.091 ajoutent les clics sur les animaux embarqués
|
||||
et permettent de les viser à l’arrière du bateau. Minecraft 26.3, Fabric et JEI
|
||||
restent ceux de beta.090 ; les textures beta.090 sont conservées.
|
||||
[Contrat et vérifications](boat-passengers-beta091.md). Aucune publication du canal
|
||||
ou installation personnelle n’est effectuée.
|
||||
|
||||
## beta.090 — Minecraft 26.3 finale
|
||||
|
||||
La cible locale passe à Minecraft 26.3, Fabric API 0.160.5+26.3, avec Loader
|
||||
0.19.5 et Java 25. Demeure et JEI sont reconstruits pour la même version ;
|
||||
le pack graphique devient beta.090. [Contrat et vérifications](minecraft-26-3-beta090.md).
|
||||
Les archives normal/Test beta.090 sont des livraisons locales : le canal
|
||||
publié et l'instance Prism ne sont pas modifiés par cette migration du projet.
|
||||
|
||||
## beta.077 — tourbillon et navigation
|
||||
|
||||
[Nuages](cloud-vortex-beta077.md) · [Menus](menu-navigation-beta076.md).
|
||||
Client natif, `check build`, sources et archives normal/Test vérifiés.
|
||||
Textures beta.070 et 155 anciennes archives conservées. Le lot de menus
|
||||
beta.076 est inclus ici ; aucun pack intermédiaire beta.076 n’a été publié.
|
||||
Aucun déploiement dans une instance personnelle ni sur le canal distant.
|
||||
|
||||
- [Sanctuary-beta.077.mrpack](../build/Sanctuary-beta.077.mrpack), 9608669 octets.
|
||||
SHA-256 : `1c3839f086e9545c920e4ebfc13f99a25d0684fa6f2dc8e1b2a842a8e7044045`.
|
||||
- [Sanctuary-Test-beta.077.mrpack](../build/Sanctuary-Test-beta.077.mrpack), 9627602 octets.
|
||||
SHA-256 : `1b0c302b9d4d5f59d5e954adbecde4acbacd9409c8cd6a743ccbe116826d7c0a`.
|
||||
|
||||
## beta.075 — bibliothèque locale
|
||||
|
||||
[Correctif et vérifications](plans-library-beta075.md). Retrait du bouton
|
||||
Minecraft Schematics. Build, sources, JAR et exports normal/Test vérifiés ;
|
||||
onze archives précédentes conservées. Aucun déploiement personnel ou distant.
|
||||
|
||||
- [Sanctuary-beta.075.mrpack](../build/Sanctuary-beta.075.mrpack), 9594917 octets.
|
||||
SHA-256 : `fba6e648c2569b115713060862166b80ffc38620929f2fb3e110868d61d6257e`.
|
||||
- [Sanctuary-Test-beta.075.mrpack](../build/Sanctuary-Test-beta.075.mrpack), 9613850 octets.
|
||||
SHA-256 : `65a7f40f8c10c58437980cf6ea40e45b5b8c9e1e75e8dbadcc981aa5f41e19fd`.
|
||||
|
||||
## beta.074 — autonomie des familiers
|
||||
|
||||
[Contrat et vérifications](familiar-autonomy-beta074.md). Quatre comportements
|
||||
spontanés distincts, garde locale et ordres H bref/maintenu. Textures beta.070.
|
||||
Trois parcours client natifs et `check build assemblePack assembleTestPack`
|
||||
réussis. JAR, 1 689 classes et sources comparés ; neuf archives précédentes
|
||||
conservées. Aucun déploiement sur le canal ou dans une instance personnelle.
|
||||
|
||||
- [Sanctuary-beta.074.mrpack](../build/Sanctuary-beta.074.mrpack), 9595113 octets.
|
||||
SHA-256 : `cc73e93c2ae17d6b52b6c8dd53639771645c5e5becd848e2c76765e32c919c5d`.
|
||||
- [Sanctuary-Test-beta.074.mrpack](../build/Sanctuary-Test-beta.074.mrpack), 9614048 octets.
|
||||
SHA-256 : `0851e10350788b76efa96ec1a50db951a74200213399e2a4216f8d5b1b8fafcb`.
|
||||
|
||||
[Reçu de vérification](../build/autonomy074-artifact.json).
|
||||
|
||||
## beta.073 — arènes et paris
|
||||
|
||||
Livraison locale isolée sur Minecraft 26.3-pre-2. Le resource pack reste
|
||||
beta.070. [Contrat du ticket](arenas-beta073.md). Aucun canal distant ni aucune
|
||||
instance personnelle n'est modifié. Client natif, `check build`,
|
||||
assemblage et vérification des archives réussis.
|
||||
|
||||
- [Pack normal beta.073](../build/Sanctuary-beta.073.mrpack), 9 592 283 octets.
|
||||
SHA-256 : `9ef9c1c2f156d1eb9dcb31ae0f0364a798e46867ce86d56ae25fa2ae5c5105ca`.
|
||||
- [Monde plat rapide beta.073](../build/Sanctuary-Test-beta.073.mrpack), 9 611 218 octets.
|
||||
SHA-256 : `5394215474faea6a321cf44d64c01acaaa7fda554448110a3c8b47dcddf2f9c7`.
|
||||
- [Contrôles et provenance](../build/arena073-artifact.json).
|
||||
|
||||
|
||||
## beta.072 — statues et plans natifs
|
||||
|
||||
[Contrat, formats et essais](statues-plans-beta072.md). Outil K intégré à
|
||||
Sanctuary, création de statues à partir des modèles et textures du jeu,
|
||||
import/export de plans, aperçu et collage créatif contrôlé côté serveur.
|
||||
Sans dépendance Litematica/MaLiLib ; textures beta.070 conservées.
|
||||
|
||||
- [Pack normal beta.072](../build/Sanctuary-beta.072.mrpack), 9 534 082 octets.
|
||||
SHA-256 : `ccadfa614595f4409cb8eab9c795f112e65854c8e84864c8a8fb67dbc2de450e`.
|
||||
- [Monde plat rapide beta.072](../build/Sanctuary-Test-beta.072.mrpack), 9 553 013 octets.
|
||||
SHA-256 : `cb1786ffbf0290fc2cc5437647423e96abee8bca7737537aecbb12de98aee811`.
|
||||
|
||||
Reçu `build/statues-research/artifacts.json` : 1 671 classes, 758 sources Java,
|
||||
150 ressources graphiques conformes ; index et ZIP vérifiés. Client natif,
|
||||
sept tests serveur et 130 tâches Gradle réussis. Archives locales uniquement ;
|
||||
aucun canal public ni instance personnelle modifié.
|
||||
|
||||
## beta.071 — personnalités et montures
|
||||
|
||||
[Contrat, audit et tests](familiar-personality-beta071.md). Quatre caractères,
|
||||
poursuite plus vive, araignées grimpantes, Nautilus qui préserve l’air du
|
||||
cavalier et Strider à la surface de la lave. Construction depuis les sources
|
||||
isolées de la 71 pour préserver le chantier simultané de la 72.
|
||||
|
||||
- [Pack normal beta.071](../build/Sanctuary-beta.071.mrpack), 9462323 octets.
|
||||
SHA-256 : `1ab36663fe24313597c5328170a3eb2ca2e33bfdfc4c6fbc85c1f5c03102628c`.
|
||||
- [Monde plat rapide beta.071](../build/Sanctuary-Test-beta.071.mrpack), 9481258 octets.
|
||||
SHA-256 : `06309f4cd382053fb42d80c3f131152932e07523ace799c50d1d899026029a92`.
|
||||
|
||||
Reçu `build/familiar071-artifact.json` : 1 651 classes conformes, seules les
|
||||
classes du familier prévues par le ticket changent. Les 150 ressources de la
|
||||
beta.070, JEI, les autres ressources et les archives précédentes sont intacts.
|
||||
Aucun canal public ni instance personnelle modifié.
|
||||
|
||||
## beta.070 — resource pack embarqué
|
||||
|
||||
[Contrat et vérifications](resourcepack-beta070.md). La source versionnée du
|
||||
pack est embarquée dans le JAR et activée par défaut via Fabric ; aucun fichier
|
||||
d’options personnelles n’est distribué. Les variantes normal/Test et le ZIP
|
||||
autonome utilisent les mêmes 150 ressources, comparées octet pour octet.
|
||||
|
||||
- [Pack normal beta.070](../build/Sanctuary-beta.070.mrpack), 9451989 octets.
|
||||
SHA-256 : `9a582e17394bcb7327bd3b5a5a789b297431e41bf43409ed117fac728bbda46c`.
|
||||
- [Monde plat rapide beta.070](../build/Sanctuary-Test-beta.070.mrpack), 9470924 octets.
|
||||
SHA-256 : `5732e570c1af20f4d4896292c51bb38fb5c642d0ec81c6a1273ba27318938cd4`.
|
||||
- [Resource pack autonome beta.070](../build/Sanctuary-Resource-Pack-beta.070.zip), 3757596 octets.
|
||||
SHA-256 : `fe81653d09584f67da5f56c4fd6c866c733ec81e07559a5f132a44d44bbaadb6`.
|
||||
|
||||
Source du resource pack : commit `85e062ed91c7`. Reçu
|
||||
`build/resourcepack070-artifact.json`, 1 648 classes conformes et archives
|
||||
beta.069 intactes. Aucun canal public ni instance personnelle modifié.
|
||||
|
||||
## beta.069 — ordres du familier sur H
|
||||
|
||||
[Contrat et vérifications](familiar-orders-beta069.md). Retour des ordres
|
||||
sur H et du changement de main sur F ; G reste le pouvoir. Parcours client,
|
||||
compilation générale et archives vérifiés.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.069.mrpack), 5726225 octets.
|
||||
SHA-256 : `1c923452ff4d5d186309390fac3306b8e05162143c852619b7519741002aa165`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.069.mrpack), 5745158 octets.
|
||||
SHA-256 : `bed499fa29f4247a9e321207df503b3147b8b4fc8c8541d817cdfe2a2db9a945`.
|
||||
|
||||
Reçu `build/familiar069-artifact.json` : 1 647 classes conformes, seule
|
||||
`FamiliarClient` change ; anciens packs conservés. Aucun canal public ni
|
||||
instance personnelle modifié.
|
||||
|
||||
## beta.068 — animation du préchargement
|
||||
|
||||
[Contrat et vérifications](loading-animation-beta068.md). Petite ligne SGA
|
||||
animée pendant l’attente, étapes réelles et progression native conservées.
|
||||
Client natif, comparaison de huit captures et compilation générale réussis.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.068.mrpack), 5726390 octets.
|
||||
SHA-256 : `cfb55d414186e6be5f0e19770049158302d4fbf37c0fa696306acda428594a2e`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.068.mrpack), 5745324 octets.
|
||||
SHA-256 : `643913caf577ee61b23747f378e9a21f5b1a54531901791d47d8ad0a75531dff`.
|
||||
|
||||
Reçu `build/loading068-artifact.json` : 1 647 classes conformes ; archives
|
||||
beta.067, introduction, cache, textures et JEI préservés. Aucun canal public
|
||||
ni instance personnelle modifié.
|
||||
|
||||
## beta.067 — touche et vol du dragon
|
||||
|
||||
[Contrat et vérifications](familiar-controls-beta067.md). F ouvre les ordres,
|
||||
le dragon regarde devant lui, le grand porté monte avec la touche de saut.
|
||||
Client natif et compilation générale réussis.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.067.mrpack), 5724176 octets.
|
||||
SHA-256 : `a0dd2dc94aee0418b6ec8e29c24c847506627d63f6d22f1980ef7eacf38bc3fa`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.067.mrpack), 5743110 octets.
|
||||
SHA-256 : `e2605534bbe7eed0559304f7c2cb9bd6618178eb64b042e45ff2aa596f0bec22`.
|
||||
|
||||
Reçu `build/familiar067-artifact.json` : 1 646 classes compilées conformes ;
|
||||
archives beta.066, textures et JEI inchangés. Aucun déploiement personnel
|
||||
ni modification du canal public.
|
||||
|
||||
## beta.066 — cosmétiques animés sans doublon
|
||||
|
||||
[Contrat et vérifications](head-render-beta066.md). Le rendu sépare les parties
|
||||
fixes des blocs portés de leur animation native : plus de second coffre fermé
|
||||
ou de cloche immobile. Parcours client et compilation générale réussis.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.066.mrpack), 5721524 octets.
|
||||
SHA-256 : `04e584ba7047e87e66316ba6e06f9d809e2813a6f4227067f20ca2ace7239f3a`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.066.mrpack), 5740457 octets.
|
||||
SHA-256 : `de6563aa48dededb6c1afa967987bd25e5134644596ea1908b1fa1768463aae5`.
|
||||
|
||||
Reçu `build/cosmetic066-artifact.json`. Archives beta.065, textures et JEI
|
||||
préservés ; seule la source principale `HeadCosmeticModels` change.
|
||||
Aucun canal ni instance personnelle modifié.
|
||||
|
||||
## beta.065 — collisions des bateaux
|
||||
|
||||
[Contrat et vérifications](boat-collisions-beta065.md). Les coques complètes et
|
||||
les virages respectent les murs. 88 variantes et seize parcours pilotés en eau
|
||||
ou en vol vérifiés. Build et comparaison des archives réussis.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.065.mrpack), 5721084 octets.
|
||||
SHA-256 : `71ff1736817e2953f88a8afda5858b2e7ae23c360efdb7e25f149cbff049d764`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.065.mrpack), 5740018 octets.
|
||||
SHA-256 : `4c4931146a05d6f57e9abf2303b039ab7a7bc16471a0b864873b86bef297431d`.
|
||||
|
||||
Reçu `build/boat065-artifact.json`. Archives beta.064 intactes ; seul
|
||||
`CrewBoat` change dans le code principal. Aucun canal ni instance personnelle
|
||||
modifié.
|
||||
|
||||
## beta.064 — cache et préparation du monde
|
||||
|
||||
[Contrat et vérifications](loading-cache-beta064.md). Plans dérivés réutilisés
|
||||
à la réouverture, étapes réelles sur fond uni, portail de préparation supprimé.
|
||||
Introduction complète conservée. Build et parcours client natif réussis.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.064.mrpack), 5720330 octets.
|
||||
SHA-256 : `c05b78e2caf27d52cb318979ee66e19e5bca9c90673f8868ae50f85f0e2ff2c0`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.064.mrpack), 5739264 octets.
|
||||
SHA-256 : `962800291875ab5bebda1abaedc798e7cf110ba0ef13ac168a0b85b3b2c19d24`.
|
||||
|
||||
Reçu `build/loading064-artifact.json` : 1 645 classes conformes au build,
|
||||
archives beta.063 préservées. Aucun canal ni instance personnelle modifié.
|
||||
|
||||
## beta.061 — musique au portail et montures colossales
|
||||
|
||||
[Contrat et vérifications](arrival-mount-beta061.md). Musique démarrant au
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# beta.075 — bibliothèque de plans
|
||||
|
||||
Branche `codex/plans-library-beta075`.
|
||||
|
||||
Dans **K → Bibliothèque**, le bouton « Rechercher sur Minecraft Schematics »
|
||||
est retiré à la demande du créateur. Ses libellés FR/EN inutilisés sont supprimés.
|
||||
Le placement des autres widgets se réajuste avec la disposition native.
|
||||
|
||||
L’ouverture du dossier, l’actualisation, le glisser-déposer et l’import des
|
||||
plans locaux conservent leur fonctionnement. Aucun format, monde, texture ou
|
||||
identifiant de contenu ne change. Minecraft 26.3-pre-2, resource pack beta.070.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en 2 min 39 s, 124 tâches.
|
||||
Les 1 689 classes du JAR correspondent au build ; seul PlansScreen change
|
||||
par rapport à beta.074. Les archives normal/Test et les sources sont vérifiées,
|
||||
le lien est absent du bytecode et onze archives précédentes restent identiques.
|
||||
Reçu : `build/library075-artifact.json`. Aucun nouvel essai client graphique.
|
||||
Le serveur dédié reste
|
||||
exclu conformément au refus d’accepter son EULA (`-x :sanctuary:runGameTest`).
|
||||
Aucun déploiement dans une instance personnelle.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.075.mrpack](../build/Sanctuary-beta.075.mrpack), 9594917 octets.
|
||||
SHA-256 : `fba6e648c2569b115713060862166b80ffc38620929f2fb3e110868d61d6257e`.
|
||||
- [Sanctuary-Test-beta.075.mrpack](../build/Sanctuary-Test-beta.075.mrpack), 9613850 octets.
|
||||
SHA-256 : `65a7f40f8c10c58437980cf6ea40e45b5b8c9e1e75e8dbadcc981aa5f41e19fd`.
|
||||
@@ -306,6 +306,19 @@ jusqu'à ce ticket. Mini-carte, marqueurs, partage, recettes, plans et autres
|
||||
aptitudes gardent leurs tickets propres. Leur absence ne bloque pas le New
|
||||
Game+ lorsque les six compétences sont au maximum.
|
||||
|
||||
Le [cadrage CLAIM-01 — titres de propriété temporaires](property-titles-ticket.md)
|
||||
précise les décisions du 15 septembre 2026 : un titre couvre un seul chunk ;
|
||||
il faut renommer l'objet puis faire un clic droit pour revendiquer le chunk.
|
||||
Un autre titre portant le même nom permet d'étendre la même propriété,
|
||||
y compris à un chunk séparé, sans revendiquer les chunks intermédiaires.
|
||||
Les titres restent attachés aux chunks ; une réserve de **saphirs consommés
|
||||
successivement** entretient leur protection en temps réel, même lorsque le
|
||||
joueur est déconnecté, tant que le serveur tourne. À la fin de la période
|
||||
payée, sans assez de saphirs pour renouveler, la protection disparaît
|
||||
immédiatement. Acquisition des titres, tarifs d'entretien, paliers, durée des
|
||||
périodes, traitement des arrêts du serveur et droits de réservation sans
|
||||
protection restent à décider ; ce mécanisme n'est pas encore implémenté.
|
||||
|
||||
## Références et validation des livraisons
|
||||
|
||||
La référence historique est consultée en lecture seule dans `../26.2/sanctuary` :
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# CLAIM-01 — Titres de propriété temporaires
|
||||
|
||||
**Ticket local — conception en cours, décisions du 15 septembre 2026.**
|
||||
Ce document prépare le résultat jouable ; la protection n'est pas implémentée.
|
||||
Branche réservée : `codex/property-titles`. Le checkout existant reste sur sa
|
||||
branche de travail pour préserver les travaux en cours. Aucun numéro de
|
||||
livraison réservé et aucune issue distante publiée.
|
||||
|
||||
## Intention et décisions confirmées
|
||||
|
||||
Le joueur achète des **titres de propriété temporaires** donnant des privilèges
|
||||
sur un terrain. Il choisit un niveau de protection et constitue une réserve
|
||||
de saphirs pour l'entretenir. Le titre reste attaché au chunk ; la protection
|
||||
accordée est temporaire et renouvelable.
|
||||
|
||||
**Un titre couvre un seul chunk**, soit une emprise horizontale de 16 × 16
|
||||
blocs alignée sur la grille Minecraft. Un même titre ne couvre pas plusieurs
|
||||
chunks à la fois. Son entretien utilise ensuite des saphirs.
|
||||
|
||||
**Le titre est un objet à renommer avant utilisation.** Le joueur fait un
|
||||
clic droit avec ce titre dans le chunk à revendiquer. Le nom donné au titre
|
||||
sert de nom à la propriété ; utiliser un autre titre portant le même nom
|
||||
dans un autre chunk étend cette propriété. Chaque chunk ajouté nécessite
|
||||
son propre titre, conformément à la règle « un titre = un chunk ».
|
||||
**Les chunks d'une même propriété peuvent être séparés** : aucune contiguïté
|
||||
n'est requise. Les chunks situés entre eux ne sont pas revendiqués par cette
|
||||
extension. Le moyen de renommer l'objet reste à préciser.
|
||||
|
||||
**Les saphirs sont consommés successivement pour entretenir le niveau de
|
||||
protection choisi. Les titres restent attachés aux chunks.** À niveau et
|
||||
emprise identiques, ajouter des saphirs prolonge l'entretien disponible.
|
||||
Cette décision remplace la consommation successive de titres envisagée au
|
||||
début de la discussion.
|
||||
|
||||
**L'entretien se fait en temps réel**, y compris lorsque le joueur est
|
||||
déconnecté, tant que le serveur tourne. Il ne dépend pas du temps de présence
|
||||
du titulaire. Le traitement du temps écoulé pendant un arrêt du serveur reste
|
||||
à décider séparément.
|
||||
|
||||
**La protection disparaît immédiatement à épuisement de la couverture** :
|
||||
lorsque la dernière période payée se termine et que la réserve ne contient
|
||||
pas assez de saphirs pour renouveler, aucun délai de grâce ni baisse
|
||||
progressive ne s'applique. Une réserve vide ne raccourcit pas la période
|
||||
déjà payée. Le titre reste attaché au chunk ; la possibilité pour un autre
|
||||
titulaire de revendiquer ce chunk sans protection reste à décider.
|
||||
|
||||
La protection envisagée rend les blocs plus longs à miner par les autres.
|
||||
Le créateur évoque un premier ralentissement doublé et une protection pouvant
|
||||
aller jusqu'à des blocs presque incassables, voire incassables. Les valeurs,
|
||||
les bénéficiaires et la limite exacte restent à décider.
|
||||
|
||||
## Modèle proposé à préciser
|
||||
|
||||
Séparer dans le fonctionnement et l'interface :
|
||||
|
||||
- **Niveau choisi** : résistance appliquée et coût d'entretien associé.
|
||||
- **Réserve** : saphirs disponibles pour les renouvellements successifs.
|
||||
- **Échéance en cours** : moment de la prochaine consommation ou fin de
|
||||
couverture, selon le contrat temporel à retenir.
|
||||
|
||||
Proposition : un niveau plus élevé coûte davantage de saphirs. Le coût et
|
||||
la durée d'une période payée ne sont pas fixés ; l'emprise est d'un chunk
|
||||
par titre. La gestion des réserves pour plusieurs chunks reste à préciser.
|
||||
La consommation en temps réel est confirmée ; une consommation
|
||||
supplémentaire provoquée par les attaques n'a pas été demandée.
|
||||
|
||||
Pour le premier ralentissement, l'interprétation proposée est un **temps de
|
||||
minage multiplié par deux** pour un joueur non autorisé, à bloc, outil et
|
||||
conditions identiques. Cela reste à confirmer. Les privilèges supplémentaires
|
||||
du titulaire ne sont pas encore définis.
|
||||
|
||||
## Économie : entretien en saphirs, acquisition à définir
|
||||
|
||||
Le **saphir est retenu pour entretenir la protection des titres déjà posés**.
|
||||
La [vision](vision.md#les-trois-gemmes) lui attribue déjà la réservation et la
|
||||
sauvegarde de quantités limitées d'objets dans le catalogue. Ce nouvel usage
|
||||
prolonge son rôle de conservation, sans supposer cette économie déjà disponible.
|
||||
|
||||
Restent à choisir : monnaie et prix d'achat initial des titres, lieu et geste
|
||||
d'achat, obtention effective des saphirs, tarifs d'entretien, partage et
|
||||
éventuel transfert. Le titre utilisable en jeu est un objet renommable ; le
|
||||
registre serveur et le dépôt des saphirs dans la réserve restent à définir.
|
||||
|
||||
## Arbitrages nécessaires au premier lot jouable
|
||||
|
||||
- Emprise d'un chunk par titre confirmée ; préciser la portée verticale,
|
||||
les dimensions concernées, le nombre maximal de chunks par titulaire et
|
||||
les règles de chevauchement. Choisir une réserve par chunk ou une réserve
|
||||
commune avec des règles explicites d'affectation des saphirs et de
|
||||
renouvellement lorsque le solde ne suffit pas pour tous les chunks.
|
||||
- Création et extension par titre renommé puis clic droit confirmées ;
|
||||
préciser le moyen de renommage, les noms admis, leur comparaison et les
|
||||
collisions entre titulaires. Les chunks séparés sont autorisés. Définir
|
||||
comment départager le chunk du joueur et celui du bloc visé à une frontière,
|
||||
et l'effet d'un clic dans un chunk déjà revendiqué.
|
||||
Le nom sert au regroupement ; le serveur doit aussi vérifier les droits
|
||||
d'extension pour qu'un nom identique ne permette pas de prendre le contrôle
|
||||
de la propriété d'un autre titulaire.
|
||||
- Titulaire : joueur, faction ou les deux ; droits des membres, invités et
|
||||
tiers, et rapport éventuel au prestige. Demeure mesure l'habitation et ne
|
||||
vaut pas attribution d'un titre.
|
||||
- Paliers de résistance, privilèges et incassabilité éventuelle ; articulation
|
||||
avec les outils, enchantements et aptitudes de minage existants.
|
||||
- Coût en saphirs et durée réelle de chaque période selon le niveau ;
|
||||
financement de la première activation ; traitement des chunks déchargés
|
||||
et du temps écoulé pendant les arrêts du serveur. La déconnexion du joueur
|
||||
ne suspend pas l'entretien.
|
||||
- Changement de niveau : prise d'effet, traitement de la période payée et du
|
||||
temps restant, pour éviter de gagner de la couverture par des changements
|
||||
répétés.
|
||||
- Avertissement avant épuisement, droits de réservation d'un titre sans
|
||||
protection et réactivation après réapprovisionnement. Le titre reste attaché
|
||||
au chunk et la protection disparaît immédiatement en fin de couverture.
|
||||
- Abandon, transfert, conflits de réservation et devenir du stock restant.
|
||||
- Portée de la protection face aux explosions, pistons, fluides, mobs,
|
||||
inventaires et actions groupées ; distinguer les droits d'interaction de la
|
||||
résistance au minage.
|
||||
|
||||
## Réception de la future implémentation
|
||||
|
||||
Le premier lot doit permettre d'obtenir des titres par la voie retenue,
|
||||
réserver un terrain, choisir sa protection, approvisionner la réserve en
|
||||
saphirs et observer leur consommation successive ainsi que l'effet sur le
|
||||
minage, avec conservation du titre attaché au chunk.
|
||||
|
||||
- Renommer un titre « Verger » et faire un clic droit dans un chunk libre
|
||||
crée la propriété « Verger ». Utiliser un autre titre de même nom dans un
|
||||
second chunk admissible étend la même propriété à ce chunk. Une autre
|
||||
propriété, nommée différemment, conserve son emprise.
|
||||
- Répéter l'extension dans un chunk séparé du premier par des chunks libres :
|
||||
les deux chunks appartiennent à la même propriété, sans revendiquer ni
|
||||
protéger les chunks intermédiaires.
|
||||
- Un titre non renommé ne revendique aucun chunk et explique le renommage
|
||||
nécessaire. Un refus d'extension ou un conflit ne dépense pas de titre et
|
||||
ne modifie pas de terrain ; vérifier les collisions de noms entre joueurs.
|
||||
- Deux joueurs vérifient les droits et le ralentissement de chaque palier,
|
||||
à conditions identiques, ainsi que les limites du terrain.
|
||||
- Un titre couvre exactement le chunk désigné : sa protection ne déborde
|
||||
pas sur le chunk voisin. Couvrir deux chunks nécessite des titres distincts ;
|
||||
renouveler le même chunk ne modifie pas son emprise.
|
||||
- Ajouter des saphirs à niveau et emprise constants prolonge l'entretien sans
|
||||
modifier la résistance choisie. Un renouvellement conserve la couverture
|
||||
et le titre attaché, sans exiger un nouveau titre pour le même chunk.
|
||||
- Déconnecter le titulaire en laissant le serveur tourner : à son retour,
|
||||
la réserve et la couverture reflètent le temps réel écoulé, y compris si
|
||||
plusieurs renouvellements en saphirs ont eu lieu pendant son absence.
|
||||
- Épuisement, réapprovisionnement et changement de niveau suivent les règles
|
||||
arrêtées, avec un état et une échéance compréhensibles en FR/EN.
|
||||
- Avec une réserve vide ou insuffisante pour renouveler, vérifier la
|
||||
protection jusqu'à la fin de la période payée, puis sa disparition immédiate
|
||||
à l'échéance, y compris lorsque le titulaire est déconnecté. Le titre reste
|
||||
attaché ; le minage retrouve les règles ordinaires applicables, sans
|
||||
résistance résiduelle provenant des titres.
|
||||
- Achat, consommation et droits font autorité côté serveur. Des requêtes
|
||||
répétées ou simultanées et un redémarrage ne dupliquent ni dépense, ni titre,
|
||||
ni saphir, et ne remettent pas gratuitement la durée à zéro.
|
||||
- Un contrat de sauvegarde et de migration explicite précède toute donnée
|
||||
persistante. Le chargement d'une ancienne partie n'attribue aucun terrain
|
||||
implicitement ; aucune régénération de chunks n'est nécessaire.
|
||||
- Tester les interactions retenues ci-dessus et les actions groupées, puis
|
||||
lancer `./gradlew check build`. Une livraison de distribution exige aussi
|
||||
`./gradlew assemblePack` et le [versionnement](versioning.md) mod/pack.
|
||||
|
||||
## État actuel
|
||||
|
||||
L'emprise est fixée à un chunk par titre. Le titre est renommé puis utilisé
|
||||
par clic droit ; un même nom permet d'étendre la propriété à un autre chunk.
|
||||
Les chunks d'une même propriété peuvent être séparés.
|
||||
Les titres restent attachés aux chunks. La consommation successive de saphirs
|
||||
en temps réel pour maintenir un niveau choisi, y compris lorsque le joueur est
|
||||
déconnecté, est confirmée.
|
||||
La protection disparaît immédiatement lorsque la couverture est épuisée.
|
||||
Les autres arbitrages sont explicitement ouverts ou proposés ci-dessus.
|
||||
Ce cadrage est documentaire : aucun code, binaire, monde ou format de
|
||||
sauvegarde modifié ; aucun test de gameplay exécuté pour ce ticket.
|
||||
@@ -0,0 +1,90 @@
|
||||
# beta.070 — resource pack versionné et actif par défaut
|
||||
|
||||
Branche `codex/resourcepack-beta070`.
|
||||
|
||||
## Source et version
|
||||
|
||||
La source unique est `ressources-pack/sanctuary/`. Le dossier fourni
|
||||
`Sanctuary-Resource-Pack-base-26-3` contient 107 fichiers, dont 85 PNG.
|
||||
Tous ses assets sont repris sans retouche. Sept images diffèrent de la base :
|
||||
les six états `hud/food_*` et l’atlas `sanctuary/textures/gui/food_details.png`.
|
||||
Les 47 cœurs vanilla déjà présents dans la source restent identiques.
|
||||
Aucun remplacement des bulles de respiration.
|
||||
|
||||
`ressources-pack/sanctuary/releases/beta.070.json` fixe les empreintes des
|
||||
150 fichiers distribués, la provenance des 107 fichiers fournis et le format
|
||||
exact **97.1** de Minecraft **26.3-pre-2**. Les guides ont été actualisés ; le
|
||||
manifeste Minecraft porte le numéro beta.070 et une description FR/EN.
|
||||
|
||||
Le numéro graphique `resource_pack_version` est distinct des versions du mod
|
||||
et du modpack. Un correctif de code ultérieur peut garder ces mêmes textures.
|
||||
Une retouche graphique demande un nouveau manifeste de release, en conservant
|
||||
celui des versions précédentes. `verifyResourcePack` détecte toute dérive des
|
||||
fichiers, des empreintes, des CRC PNG, des JSON ou du format.
|
||||
|
||||
## Chargement par défaut
|
||||
|
||||
Gradle copie directement cette source dans le mod à
|
||||
`resourcepacks/textures/`. L’API Fabric 0.160.0+26.3 enregistre le pack natif
|
||||
`sanctuary:textures` avec `DEFAULT_ENABLED`. Il est donc actif dès sa première
|
||||
apparition, y compris dans une instance existante qui découvre ce pack.
|
||||
Le joueur garde la possibilité de le désactiver ou de placer un autre pack
|
||||
au-dessus ; son choix n’est pas réinitialisé à chaque lancement.
|
||||
|
||||
Aucun `options.txt` livré, ni écrasement des réglages personnels. La version
|
||||
normal et la version Test embarquent le même resource pack dans le même mod.
|
||||
Le ZIP `Sanctuary-Resource-Pack-beta.070.zip` est également exporté pour un usage
|
||||
séparé ; il n’est pas nécessaire de l’ajouter une seconde fois dans Sanctuary.
|
||||
Il conserve les namespaces natifs : la jauge dynamique, les aperçus alimentaires
|
||||
et les infobulles utilisent leurs chemins de textures existants.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`ResourcePack070ClientChecks` réussit en **46 s** : activation au démarrage
|
||||
sans modifier la sélection, format reconnu compatible, priorité effective et
|
||||
égalité des octets chargés pour les sept PNG modifiés, respiration native,
|
||||
désactivation conservée après redécouverte des packs, réactivation.
|
||||
|
||||
Captures inspectées : le pack est dans la colonne « Selected », la description
|
||||
porte beta.070, les sprites de faim et de saturation proviennent des nouveaux
|
||||
dessins dans l’atlas natif. Preuves : `build/resourcepack070-client.log` et
|
||||
`build/resourcepack070-evidence/`. Pas de nouveau monde ni de serveur dans ce
|
||||
test ; les formules alimentaires ne sont pas modifiées.
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/Users/koka/Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home \
|
||||
./gradlew :sanctuary:runClientGameTest -x :sanctuary:runGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryResourcePack070ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
./gradlew verifyResourcePack assembleResourcePack
|
||||
```
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
Source du resource pack enregistrée dans le commit `85e062ed91c74fcc3695e1e224fe51ac8fd010e0`.
|
||||
Les **150 fichiers distribués** ont été relus dans l’objet Git et leurs
|
||||
empreintes correspondent au manifeste. Ce commit ne contient que la source
|
||||
du resource pack ; les chantiers de code préexistants restent dans l’arbre de
|
||||
travail. Aucun push ni tag de l’ensemble des mods n’a été créé.
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en **2 min 28 s**, 124 tâches,
|
||||
avec les profils client/test rapide et `-x :sanctuary:runGameTest`. Le serveur
|
||||
dédié reste exclu conformément au choix de ne pas accepter son EULA.
|
||||
|
||||
Les deux MRpacks contiennent les mêmes textures que le ZIP et que la source.
|
||||
**1 648 classes** conformes à la compilation, seuls `SanctuaryKnowledgeClient`
|
||||
et `SanctuaryResourcePack` changent par rapport à beta.069. Les autres ressources
|
||||
existantes et JEI restent identiques ; deux libellés FR/EN et le pack embarqué
|
||||
sont ajoutés. Les archives beta.069 conservent leurs empreintes.
|
||||
|
||||
Reçu : `build/resourcepack070-artifact.json`.
|
||||
|
||||
|
||||
- [Pack normal beta.070](../build/Sanctuary-beta.070.mrpack), 9451989 octets.
|
||||
SHA-256 : `9a582e17394bcb7327bd3b5a5a789b297431e41bf43409ed117fac728bbda46c`.
|
||||
- [Monde plat rapide beta.070](../build/Sanctuary-Test-beta.070.mrpack), 9470924 octets.
|
||||
SHA-256 : `5732e570c1af20f4d4896292c51bb38fb5c642d0ec81c6a1273ba27318938cd4`.
|
||||
- [Resource pack autonome beta.070](../build/Sanctuary-Resource-Pack-beta.070.zip), 3757596 octets.
|
||||
SHA-256 : `fe81653d09584f67da5f56c4fd6c866c733ec81e07559a5f132a44d44bbaadb6`.
|
||||
|
||||
Aucun déploiement personnel, aucune publication du canal, aucun monde modifié.
|
||||
@@ -0,0 +1,50 @@
|
||||
# RP-HUD-01 — base de textures de vie et de faim
|
||||
|
||||
Ticket du 15 septembre 2026, branche `codex/resourcepack-hud-base`.
|
||||
Demande du créateur : préparer les textures Minecraft d'origine pour qu'il
|
||||
dessine ensuite lui-même la vie, la faim et les interfaces Sanctuary.
|
||||
|
||||
## Résultat
|
||||
|
||||
- 53 PNG ajoutés au resource pack existant : les 47 sprites de `hud/heart/`
|
||||
et les six sprites `hud/food_*`, extraits sans retouche du client 26.3-pre-2.
|
||||
- Manifeste corrigé du format provisoire `0` au format exact **97.1**,
|
||||
avec `min_format` et `max_format`.
|
||||
- [Guide de retouche FR/EN](../ressources-pack/sanctuary/HUD.md), correspondance
|
||||
des variantes et recette d'export ZIP ; empreintes d'origine dans
|
||||
[HUD-SOURCES.json](../ressources-pack/sanctuary/HUD-SOURCES.json).
|
||||
- Menus, panorama, progression, aptitudes et atlas alimentaire existants
|
||||
conservés. La respiration utilise toujours les ressources vanilla.
|
||||
|
||||
Le pack est une base graphique séparée du mod. Aucun code Java, identifiant,
|
||||
format de sauvegarde ni manifeste packwiz ne change dans ce ticket ; aucun
|
||||
nouveau binaire du mod n'est livré, donc aucun compteur beta n'est consommé.
|
||||
Les modifications beta.062/beta.063 déjà présentes restent hors de ce lot.
|
||||
|
||||
## Vérification
|
||||
|
||||
Contrôles effectués sur la base et son export local :
|
||||
|
||||
- Les 53 sprites correspondent octet pour octet au client officiel ; tous
|
||||
mesurent 9 × 9, conservent la transparence et passent le contrôle des CRC
|
||||
PNG et la décompression des données d'image.
|
||||
- Les 19 fichiers JSON et métadonnées se lisent correctement. Le format 97.1
|
||||
correspond au `version.json` du client ; les champs du manifeste suivent
|
||||
son lecteur `PackFormat`.
|
||||
- 121 fichiers préexistants sont inchangés, dont toutes les textures déjà
|
||||
présentes dans le resource pack et les travaux hors périmètre. Les seuls
|
||||
fichiers de ce relevé modifiés sont le manifeste et les guides ciblés.
|
||||
- Le ZIP contient 154 fichiers, avec `pack.mcmeta`, `pack.png` et `assets/`
|
||||
à la racine. Son intégrité et l'égalité de chaque entrée avec sa source
|
||||
sont vérifiées ; aucun fichier Finder `.DS_Store` n'est exporté.
|
||||
|
||||
Export local :
|
||||
[`Sanctuary-Resource-Pack-base-26.3-pre-2.zip`](../ressources-pack/Sanctuary-Resource-Pack-base-26.3-pre-2.zip),
|
||||
3 761 553 octets. SHA-256 :
|
||||
`21b2ce42e9c381191508c93929d5ce02ee70beeeb519cf58a9ae6b3acd35f107`.
|
||||
Reçu local ignoré : `build/resourcepack-hud-base/verification.json`.
|
||||
|
||||
Aucun essai de rendu en jeu ni installation personnelle effectué. Les
|
||||
commandes Gradle ne sont pas lancées pour ce lot de PNG et de documentation :
|
||||
le code et la distribution packwiz sont inchangés. Le contrôle visuel après
|
||||
les futures retouches du créateur est décrit dans le guide HUD.
|
||||
@@ -0,0 +1,192 @@
|
||||
# PLAN-01 — statues et plans légers — beta.072
|
||||
|
||||
Branche `codex/statues-plans-beta072`. Ticket ouvert le 15 septembre 2026.
|
||||
**Implémenté, essais natifs réussis et archives locales beta.072 vérifiées.**
|
||||
|
||||
## Demande et choix retenu
|
||||
|
||||
Extraire les modèles et textures réellement utilisés par Minecraft pour créer
|
||||
des statues en blocs, avec une palette de matériaux choisie par couleur.
|
||||
Le créateur demande un outil Sanctuary léger qui couvre l'essentiel des plans
|
||||
partagés en ligne. Il a retenu une implémentation native compatible avec les
|
||||
fichiers Litematica, sans embarquer Litematica ou MaLiLib.
|
||||
|
||||
Parcours : modèle → hauteur et palette → plan → aperçu positionnable et
|
||||
orientable → placement créatif, ou construction manuelle guidée en survie.
|
||||
Les plans importés partagent ce dernier parcours. Une bibliothèque locale,
|
||||
l'export `.litematic`, les couches et le décompte des blocs complètent l'outil.
|
||||
Le lien de recherche Minecraft Schematics livré en beta.072 est retiré en
|
||||
[beta.075](plans-library-beta075.md). La bibliothèque utilise les fichiers locaux.
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Appuyer sur **K** (raccourci modifiable dans les contrôles).
|
||||
2. Dans **Statue**, choisir une espèce, ou le mob visé au moment d'ouvrir
|
||||
l'écran. Régler la hauteur de 4 à 96 blocs et la palette : mélange, béton
|
||||
ou laine. La géométrie et les textures viennent du rendu natif et des
|
||||
ressources actives ; les pixels transparents ne deviennent pas des blocs.
|
||||
3. Dans **Bibliothèque**, charger un fichier local ou le déposer sur l'écran.
|
||||
Les dossiers `schematics/` et `schematics/sanctuary/` sont consultés.
|
||||
**Ouvrir le dossier** permet d’y déposer des plans ; **Actualiser** relit
|
||||
les fichiers disponibles.
|
||||
4. Dans **Construction**, ancrer le plan au point visé, régler ses coordonnées,
|
||||
tourner de 90°, isoler une couche et consulter les blocs restants.
|
||||
Les blocs corrects disparaissent de l'aperçu ; les conflits sont rouges.
|
||||
5. En créatif, **Placer** puis confirmer dans le jeu. En survie, poser les
|
||||
blocs normalement avec son inventaire ; le compteur suit l'avancement.
|
||||
**Exporter** écrit un `.litematic` dans `schematics/sanctuary/`, sans écraser
|
||||
les exports précédents.
|
||||
|
||||
L'aperçu est local et se ferme au changement de dimension ou à la déconnexion.
|
||||
Exporter avant de quitter conserve le plan. Le serveur Sanctuary est requis
|
||||
pour le collage créatif ; un aperçu seul ne donne aucun droit de placement.
|
||||
|
||||
## Compatibilité implémentée
|
||||
|
||||
- Minecraft **26.3-pre-2**, Java 25, versions Fabric du dépôt.
|
||||
- `.litematic` versions 4 à 7 en lecture, version 6 en écriture ; Sponge
|
||||
`.schem` v2/v3 en lecture ; structures vanilla `.nbt` en lecture. Fichiers
|
||||
NBT compressés gzip. Les palettes classiques `Name`/`Properties` et les clés
|
||||
natives 26.3 `id`/`properties` sont lues ; l'export Litematic conserve le
|
||||
contrat classique, indépendant de la version Minecraft.
|
||||
- Refus explicite des blocs ou propriétés inconnus, des formats non pris en
|
||||
charge et des données d'une version Minecraft plus récente. Aucun remplacement
|
||||
silencieux par de l'air ; aucune migration automatique d'anciens identifiants.
|
||||
- Les anciennes `.schematic` à identifiants numériques nécessitent une
|
||||
conversion externe. Ni archive de monde ni ZIP n'est un plan importable.
|
||||
- Import des blocs uniquement : les inventaires, commandes, entités vivantes,
|
||||
ticks différés et biomes d'un fichier tiers ne sont pas exécutés/importés.
|
||||
|
||||
## Contrat de monde et de sauvegarde
|
||||
|
||||
Les plans sont des fichiers clients indépendants ; aucun format de sauvegarde
|
||||
Sanctuary ni aucune génération n'évolue. Aucun monde personnel n'est ouvert
|
||||
par le développement. La conversion utilise un modèle de rendu, sans faire
|
||||
apparaître ni supprimer un mob. Un aperçu ne modifie pas le monde.
|
||||
|
||||
Le collage créatif demande une action explicite en jeu. Le serveur vérifie le
|
||||
mode réel, la taille, la portée, les chunks déjà chargés, la frontière du monde,
|
||||
les permissions et l'absence de blocs occupés. Un opérateur en survie reste en
|
||||
survie. Les blocs d'air du plan ne servent jamais à effacer le terrain.
|
||||
En survie, les poses et la consommation passent par Minecraft normalement.
|
||||
|
||||
## Limites de cette livraison
|
||||
|
||||
- Les statues sont des enveloppes creuses. Les couleurs sont approchées par
|
||||
celles des textures moyennes des matériaux ; la forme dépend de la hauteur
|
||||
choisie. Les effets, étiquettes, laisses et couches d'objets/blocs ne font pas
|
||||
partie de la capture. Les textures dynamiques absentes des ressources et les
|
||||
UV d'atlas spéciaux demandent encore un adaptateur.
|
||||
- L'aperçu utilise des faces colorées et les formes de sélection natives,
|
||||
sans reproduire toutes les textures et l'éclairage d'un bloc posé. Il dessine
|
||||
à 96 blocs du joueur et recalcule les blocs restants toutes les dix ticks.
|
||||
Le traitement des fichiers et la conversion géométrique passent par un
|
||||
unique fil de travail ; masquer le plan arrête son actualisation périodique.
|
||||
- Plafonds : 256 blocs par axe, 32 768 blocs non vides, 2 097 152 cellules de
|
||||
volume ; fichier compressé de 8 Mio et lecture NBT de 32 Mio au maximum.
|
||||
La capture refuse plus de 16 384 faces et la conversion plus de 8 millions
|
||||
d'échantillons. Un gros build communautaire peut dépasser ces limites.
|
||||
- Collage à moins de 192 blocs, dans des chunks chargés. Pas d'effacement,
|
||||
de remplacement du terrain occupé, de blocs administratifs ou sans objet
|
||||
associé. Le serveur consulte les permissions et le veto Fabric d'utilisation
|
||||
des blocs. La physique et les mises à jour des voisins reprennent ensuite.
|
||||
- Les matériaux comptent des états de blocs. Portes, lits et dalles doubles
|
||||
peuvent donc demander un nombre d'objets différent du nombre affiché.
|
||||
- Aucun import d'inventaire, de NBT de bloc ou d'entité ; aucun miroir, collage
|
||||
avec remplacement, remplissage automatique en survie ou catalogue web intégré.
|
||||
- Aucun benchmark comparatif avec Litematica n'est revendiqué. La réduction
|
||||
de périmètre et l'absence de dépendance supplémentaire sont vérifiables ;
|
||||
elles ne constituent pas une mesure de gain de FPS.
|
||||
|
||||
## Sources de compatibilité
|
||||
|
||||
Vérifiées le 15 septembre 2026 :
|
||||
|
||||
- [Litematica](https://modrinth.com/mod/litematica/versions) publie des versions
|
||||
26.2, sans release annoncée pour la cible exacte du dépôt.
|
||||
- [Source 26.3](https://github.com/sakura-ryoko/litematica/tree/101ac3a23649f916cdcf05f30a5036f72706a096)
|
||||
: cible 26.3-rc-1, rendu encore en chantier. Consultée pour le contrat du
|
||||
format ; aucun JAR tiers supposé compatible n'est ajouté au pack.
|
||||
- [Spécification Sponge v3](https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-3.md).
|
||||
- [Recherche Minecraft Schematics](https://www.minecraft-schematics.com/search/).
|
||||
- [Litemapy](https://github.com/SmylerMC/litemapy), utilisé seulement pour écrire
|
||||
des fixtures synthétiques indépendantes et relire l'export, hors distribution.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Essais sur macOS, Java 25, Minecraft 26.3-pre-2 et Fabric API 0.160.0+26.3.
|
||||
Tous les mondes utilisés sont des mondes de développement jetables.
|
||||
|
||||
- **Client natif réussi** : les 88 espèces à œuf d'apparition ont fourni leur
|
||||
géométrie texturée. Un creeper a parcouru les vrais widgets de création,
|
||||
l'aperçu, les quatre rotations, les matériaux, l'export, la confirmation et
|
||||
le collage réseau. Le serveur et le client retrouvent les mêmes blocs.
|
||||
Résultat : **10 × 32 × 15**, **1 542 blocs**, 22 entrées de palette avec l'air.
|
||||
Seul ce modèle a fait l'objet du parcours complet de statue en jeu ; la
|
||||
capture des 88 espèces ne vaut pas inspection visuelle de 88 statues.
|
||||
- **Survie vérifiée** : un paquet de collage envoyé malgré le mode survie est
|
||||
refusé ; l'interface ne propose pas le collage. Une pose au clic droit natif
|
||||
consomme un objet et fait avancer le compteur. Interfaces FR/EN capturées ;
|
||||
les 95 nouvelles clés de traduction sont présentes dans les deux langues.
|
||||
- **Interopérabilité indépendante** : Litemapy 0.11.0b0 relit le fichier produit
|
||||
en jeu et retrouve dimensions, états et total de 1 542 blocs. Ses trois
|
||||
écrivains ont aussi produit les fixtures `.litematic`, Sponge et vanilla
|
||||
du test serveur. La dépendance Python reste dans un environnement ignoré ;
|
||||
seuls les petits exemples synthétiques en base64 sont versionnés.
|
||||
- **Serveur et formats réussis** : les six GameTests du ticket et le test de
|
||||
contrôle Fabric passent. Ils couvrent l'autorité créatif/survie, le refus
|
||||
complet sur bloc occupé ou permission refusée, les blocs administratifs,
|
||||
la portée, les régions négatives, les palettes traversant les mots de 64 bits,
|
||||
les fichiers indépendants, les entrées invalides, la transparence, les UV et
|
||||
les rotations.
|
||||
- **Build et distribution réussis** : `check build assemblePack assembleTestPack`
|
||||
termine en 3 min 8 s, 130 tâches. Les GameTests sont ciblés sur PLAN-01 dans
|
||||
le harnais plat ; les contrôles Java/Python généraux restent exécutés. Ce
|
||||
ciblage ne revendique pas une nouvelle validation de toute la génération.
|
||||
Un premier lancement avait échoué au démarrage Java du test historique
|
||||
`lostCityPlanner15Smoke` avec un message JavaFX ; le même test isolé puis
|
||||
l'ensemble de la commande réussissent sans modification de ce code.
|
||||
|
||||
Commande du parcours client :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPlans072ClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Commande de contrôle et d'assemblage finale :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack assembleTestPack \
|
||||
-PsanctuaryFocusedTests=plans072 -PsanctuaryAtlasOnly=true \
|
||||
-PsanctuaryClientTests=true -PsanctuaryPlans072ClientTests=true \
|
||||
-PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Puis `packwiz modrinth export --output ../Sanctuary-beta.072.mrpack` dans
|
||||
`build/packwiz/`, et l'équivalent `Sanctuary-Test-beta.072.mrpack` depuis
|
||||
`build/packwiz-test/`.
|
||||
|
||||
Les captures et reçus locaux sont conservés dans
|
||||
`build/statues-research/evidence/`, `client-tests.log` et `interoperability.json`
|
||||
du même dossier de recherche. Ils ne sont pas ajoutés au pack.
|
||||
|
||||
## Archives locales
|
||||
|
||||
Le mod et les manifestes passent à **beta.072** ; les textures restent en
|
||||
**beta.070**. Les 1 671 classes du JAR correspondent exactement aux classes
|
||||
compilées, les 758 sources Java à l'archive des sources, et les 150 ressources
|
||||
graphiques aux fichiers versionnés. Les deux MRpacks contiennent le même JAR
|
||||
Sanctuary ; seul le profil Test ajoute le module de monde plat. Versions,
|
||||
index packwiz, empreintes et intégrité ZIP sont vérifiés.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| [Pack normal](../build/Sanctuary-beta.072.mrpack) | 9 534 082 | `ccadfa614595f4409cb8eab9c795f112e65854c8e84864c8a8fb67dbc2de450e` |
|
||||
| [Monde plat rapide](../build/Sanctuary-Test-beta.072.mrpack) | 9 553 013 | `cb1786ffbf0290fc2cc5437647423e96abee8bca7737537aecbb12de98aee811` |
|
||||
| [Mod Sanctuary](../mods/sanctuary/build/libs/sanctuary-beta.072.jar) | 10 043 650 | `f324eba7046563e06671c27551497ad6254cac80a27b5adbf3887b20998f0325` |
|
||||
|
||||
Reçu : `build/statues-research/artifacts.json`. Les changements déjà présents
|
||||
des beta.062 à beta.071 sont conservés. Aucun canal public, instance Prism ou
|
||||
serveur personnel n'est synchronisé par cette préparation locale.
|
||||
@@ -0,0 +1,60 @@
|
||||
# beta.089 — Crash au contact du super piston
|
||||
|
||||
Branche : `codex/super-piston-collision-beta089`.
|
||||
Signalement : crash beta.088 sous Minecraft 26.3-pre-2, `Ticking entity`,
|
||||
pendant `LocalPlayer.moveTowardsClosestSpace` → `isSuffocating`.
|
||||
|
||||
## Cause et correction
|
||||
|
||||
Les propriétés de la tête et de la tige étaient copiées depuis le socle du
|
||||
piston natif. Son prédicat de suffocation lit `PistonBaseBlock.EXTENDED`.
|
||||
Cette propriété existe sur les socles, mais pas sur les pièces techniques :
|
||||
la recherche d'un espace libre autour du joueur levait une exception lorsque
|
||||
sa boîte de collision rencontrait `sanctuary:super_piston_head`.
|
||||
La tige héritait du même défaut. Le prédicat de visibilité copié capture
|
||||
aussi les propriétés du socle d’origine et relit ce même état : changer
|
||||
uniquement la suffocation ne suffisait pas.
|
||||
|
||||
La tête et la tige utilisent des propriétés neuves, avec des prédicats
|
||||
explicites de suffocation et d’occultation désactivés. Elles conservent leurs collisions physiques de plaque et de tige,
|
||||
leur résistance et leurs règles de mouvement. Les socles gardent le prédicat
|
||||
natif, avec suffocation seulement lorsqu'ils sont rétractés.
|
||||
|
||||
Aucun changement d'identifiant, d'état de bloc, de schéma ou de sauvegarde.
|
||||
Les pistons déjà assemblés restent compatibles : aucune reconstruction,
|
||||
conversion ou suppression de blocs n'est nécessaire. Les textures et le
|
||||
correctif de navigation du Fût beta.088 sont conservés.
|
||||
|
||||
## Validation ciblée
|
||||
|
||||
Le parcours client natif des super pistons est étendu pour contrôler tous
|
||||
les états des deux socles, de la tête et de la tige, côté client et serveur :
|
||||
suffocation, visibilité, support d'apparition et conduction redstone.
|
||||
Le chemin natif `collidesWithSuffocatingBlock` est aussi exercé avec le joueur
|
||||
sur les neuf cases de tête et les deux positions de tige de chaque variante,
|
||||
dans chacune des six orientations.
|
||||
|
||||
Le défaut initial a été reproduit avec la même `IllegalArgumentException`
|
||||
que dans le rapport : `build/piston089-before-client.log`. Le contrôle de
|
||||
visibilité a ensuite révélé la capture des anciennes propriétés, corrigée
|
||||
par les propriétés neuves décrites ci-dessus.
|
||||
|
||||
Le parcours complet passe en 1 min 25 s (`build/piston089-client.log`) :
|
||||
864 états contrôlés côté client et serveur ; contact natif du joueur avec
|
||||
les pièces ; deux variantes et six directions ; charge de 108 et refus de
|
||||
109 blocs ; obstacles, inventaires, slime/miel, transport vertical d'une
|
||||
entité, interruption, sérialisation, retour dans les chunks et démontage.
|
||||
Les essais se déroulent dans un monde de développement neuf, graine 87.
|
||||
|
||||
`check build assemblePack assembleTestPack` réussit en 2 min 35 s
|
||||
(124 tâches, 104 exécutées), avec `:sanctuary:runGameTest` explicitement exclu.
|
||||
Journal : `build/piston089-check.log`. Le reçu `build/piston089-artifact.json`
|
||||
vérifie les sources, le JAR, les archives et l'identité de tous les assets
|
||||
avec beta.088. Les seules classes compilées modifiées sont `SuperPistons`
|
||||
et ses classes internes (décalages de lignes de debug pour ces dernières).
|
||||
|
||||
Archives locales : `build/Sanctuary-beta.089.mrpack` et
|
||||
`build/Sanctuary-Test-beta.089.mrpack`. Les archives beta.088 sont intactes.
|
||||
Aucun déploiement sur Prism ou sur le canal publié, aucun serveur dédié
|
||||
lancé, aucune EULA acceptée. Validation native sur macOS ; pas d'exécution
|
||||
sur le poste Windows du signalement ni d'essai LAN à deux clients distincts.
|
||||
@@ -0,0 +1,110 @@
|
||||
# beta.087 — Super piston et super piston gluant
|
||||
|
||||
## Contrat de sauvegarde avant implémentation
|
||||
|
||||
Assemblage volontaire à la clé dorée d'un carré plein de neuf pistons
|
||||
rétractés, identiques et orientés dans la même direction. Deux clics de
|
||||
confirmation ; les six directions sont admises. Sans cette action, les
|
||||
pistons restent indépendants. Aucun changement de la génération ni des
|
||||
registres de Fourneau et de Fût.
|
||||
|
||||
Les neuf composants deviennent des blocs `sanctuary:super_piston` ou
|
||||
`sanctuary:super_sticky_piston`, identifiés par leur orientation et leur
|
||||
position dans le carré. Le composant central porte une BlockEntity dédiée,
|
||||
schéma 1 : course engagée (0 à 3) et liste des déplacements natifs en cours.
|
||||
Les états intermédiaires utilisent les moving pistons
|
||||
natifs, avec leur état de bloc et progression sérialisés par Minecraft.
|
||||
Les pièces de tête et de tige sont des blocs techniques sans objet récupérable.
|
||||
Aucun inventaire ni copie de charge n'est conservé dans le contrôleur.
|
||||
|
||||
Un signal redstone sur un des neuf composants demande la sortie ; sa
|
||||
coupure demande le retour. Trois pas animés d'un bloc. Chaque pas est planifié
|
||||
entièrement avant mutation : obstacle intermédiaire, hauteur, frontière du
|
||||
monde, charge et présence des chunks. Aucune charge de chunk forcée.
|
||||
L'animation engagée finit avant d'entamer le pas suivant ou d'inverser.
|
||||
Après rechargement, les animations natives finissent et le contrôleur reprend
|
||||
à partir de l'état courant, sans rattrapage temporel ni restitution d'une
|
||||
copie initiale. Les sauvegardes suivent l'atomicité native des chunks ; pas de
|
||||
journal inter-chunks pour un crash brutal pendant une sauvegarde.
|
||||
|
||||
La charge commune conserve les règles natives d'indéplaçabilité et les
|
||||
liaisons slime/miel. Une poussée bloquée suspend l'ensemble de la tête ;
|
||||
une traction impossible laisse la charge en place et permet au gluant de
|
||||
rentrer, comme un piston natif. Les coffres et autres inventaires indéplaçables restent en place. Les blocs
|
||||
que les pistons natifs cassent, notamment les boîtes de Shulker, déposent
|
||||
leur objet avec son contenu.
|
||||
Le piston normal ne tire pas au retour. Les collisions et le déplacement des
|
||||
entités utilisent les moving pistons natifs.
|
||||
|
||||
Maj-clic avec la clé dissocie une machine rétractée et désalimentée. Une
|
||||
machine active demande de couper son alimentation et de la laisser rentrer.
|
||||
Casser une pièce dissocie la structure : les bases restantes redeviennent
|
||||
leurs pistons d'origine, les pièces techniques disparaissent sans butin ;
|
||||
seuls les composants effectivement cassés déposent leur piston. Les charges
|
||||
déjà déplacées restent à leur position actuelle, les animations de charge
|
||||
engagées finissent normalement. Une moitié déchargée attend son chargement
|
||||
pour valider ou nettoyer ses pièces ; aucun bloc étranger n'est écrasé.
|
||||
|
||||
## Valeurs et utilisation
|
||||
|
||||
Charge totale acceptée : **108 blocs distincts**, commune aux neuf cases,
|
||||
liaisons slime/miel incluses. Ce n'est pas une limite de 12 par colonne.
|
||||
Les neuf pistons doivent être du même type, orientés pareil, rétractés et
|
||||
sans alimentation. La clé dorée montre d'abord le carré ; un deuxième clic
|
||||
sous dix secondes confirme l'assemblage. Maj-clic sur un piston indépendant
|
||||
permet de l'orienter vers la face cliquée. Un carré ambigu dans une surface
|
||||
plus grande est refusé plutôt que de sélectionner des composants au hasard.
|
||||
|
||||
Un signal reçu par n'importe lequel des neuf composants fait avancer la tête
|
||||
commune jusqu'à trois blocs. La coupure la fait rentrer ; seule la variante
|
||||
gluante ramène la charge. Trois pas animés utilisent les collisions natives.
|
||||
Une charge en chunk absent suspend aussi le retour gluant, sans charger le
|
||||
chunk et sans l'abandonner comme si elle était un obstacle indéplaçable.
|
||||
|
||||
## Apparence
|
||||
|
||||
Les cinq PNG fournis sont conservés octet pour octet. Les faces 48 × 48 sont
|
||||
réparties sur les neuf composants ; la bande de côté 48 × 16 fait le tour
|
||||
du socle. **Les quatre pixels avant partent avec la tête** : le corps ouvert
|
||||
ne conserve que douze pixels, avec la texture intérieure reculée et une
|
||||
seule tige centrale qui comble l'intervalle. Les UV du côté sont découpés,
|
||||
non étirés. Modèles, collisions et retour animé suivent cette géométrie dans
|
||||
les six orientations, pour les deux variantes.
|
||||
|
||||
La synchronisation utilise les états et données des moving pistons natifs.
|
||||
Le client initialise la BlockEntity des seuls paquets marqués par cette
|
||||
machine avant leur chargement natif : un simple paquet d'état de bloc ne
|
||||
crée pas la BlockEntity d'un moving piston.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Parcours natif dans un monde plat neuf, graine 87, serveur intégré : douze
|
||||
combinaisons direction/variante, assemblage réel à la clé, charge de 108,
|
||||
refus à 109, obstacle au deuxième pas puis reprise, coffre immobile avec son
|
||||
stock, boîte de Shulker cassée avec ses 17 diamants conservés, liaison slime
|
||||
au-delà de la face, séparation slime/miel, retour gluant, transport vertical
|
||||
d'une entité, coupure pendant l'animation, sérialisation des mouvements,
|
||||
déchargement puis retour dans les chunks et démontage après casse.
|
||||
|
||||
Le test vérifie aussi la réception côté client des têtes en mouvement et du
|
||||
retour final, les collisions de 12/4 pixels dans les six directions, le refus
|
||||
hors hauteur et la suspension sur un chunk absent sans le charger.
|
||||
Journal : `build/piston087-client.log` (succès, 1 min 20 s).
|
||||
Captures inspectées : `build/piston087-screenshots/`, notamment
|
||||
`0015_piston087-normal-face-open.png` et `0016_piston087-sticky-face-closed.png`.
|
||||
|
||||
Les essais à deux clients LAN distincts et avec un moteur de shaders tiers
|
||||
restent à réaliser. Aucun serveur dédié démarré et aucune EULA acceptée.
|
||||
|
||||
`check build assemblePack assembleTestPack` a réussi en 2 min 29 s
|
||||
(124 tâches, 104 exécutées), avec `:sanctuary:runGameTest` exclu pour respecter
|
||||
le choix de tests client uniquement. Journal : `build/piston087-check.log`.
|
||||
Le reçu `build/piston087-artifact.json` vérifie les 55 nouveaux assets, les
|
||||
13 nouvelles classes, l'identité des cinq PNG, la conservation des assets
|
||||
précédents, les sources embarquées et les archives. Les autres classes
|
||||
existantes restent identiques, à l'exception du raccord dans `Multiblocks`.
|
||||
|
||||
Archives locales : `build/Sanctuary-beta.087.mrpack` et
|
||||
`build/Sanctuary-Test-beta.087.mrpack`. Les archives beta.086 sont inchangées.
|
||||
Aucun déploiement dans Prism ni sur le canal publié ; aucun monde personnel
|
||||
modifié. Le pack de ressources intégré conserve sa version beta.070.
|
||||
@@ -0,0 +1,87 @@
|
||||
# beta.082 — nouvelles météos dans les commandes opérateur
|
||||
|
||||
Branche `codex/weather-commands-beta082`, Minecraft 26.3-pre-2, textures beta.070.
|
||||
|
||||
Les trois profils Sanctuary rejoignent la commande native :
|
||||
|
||||
- `/weather overcast` : ciel gris sans pluie.
|
||||
- `/weather fog` : brouillard.
|
||||
- `/weather showers` : averses alternant avec des périodes sèches.
|
||||
|
||||
Les six profils sont également disponibles sous le préfixe Sanctuary :
|
||||
|
||||
```text
|
||||
/sanctuary weather clear
|
||||
/sanctuary weather overcast
|
||||
/sanctuary weather fog
|
||||
/sanctuary weather showers
|
||||
/sanctuary weather rain
|
||||
/sanctuary weather thunder
|
||||
```
|
||||
|
||||
Ces choix nécessitent les droits opérateur et, en solo, Allow Commands.
|
||||
Ils sont masqués dans l’arbre transmis aux clients sans ces droits ; le
|
||||
serveur refuse aussi une commande envoyée directement sans autorisation.
|
||||
Le statut `/sanctuary weather` reste consultable normalement.
|
||||
La confirmation de sélection est traduite FR/EN.
|
||||
|
||||
## Durée et sauvegarde
|
||||
|
||||
Le choix porte sur la **journée civile courante**, dans le fuseau du monde.
|
||||
Il active le mode météo Real Time si la météo était en Vanilla, sans changer
|
||||
le mode du soleil. Il n’accepte pas de durée en ticks. Les averses conservent
|
||||
leurs passages de pluie et leurs pauses : les choisir ne force pas un départ
|
||||
immédiat de la pluie pendant un intervalle sec.
|
||||
|
||||
Le choix persiste après déconnexion, rechargement ou redémarrage. À la
|
||||
prochaine date civile, le tirage quotidien existant reprend. Rechoisir le
|
||||
même profil le même jour ne réécrit pas le journal. La sélection utilise
|
||||
exactement le schéma 1 existant (`schema`, `realtime`, `date`, `weather`) ;
|
||||
aucune migration de sauvegarde, dimension ou génération n’est introduite.
|
||||
L’écriture atomique précède la modification météo et sa synchronisation.
|
||||
Un conflit d’écriture refuse la modification sans écraser le fichier externe.
|
||||
|
||||
`/sanctuary weather vanilla` rend la main au cycle Minecraft ; les commandes
|
||||
natives `/weather clear`, `/weather rain` et `/weather thunder`, avec leurs
|
||||
durées habituelles, y fonctionnent comme avant. En mode Real Time, elles
|
||||
conservent leur message de redirection vers Vanilla ; les variantes sous
|
||||
`/sanctuary weather` sélectionnent les profils quotidiens correspondants.
|
||||
Les nouvelles commandes refusent la sélection si la météo Sanctuary est
|
||||
globalement désactivée ou indisponible dans ce monde.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`Weather082ClientChecks` réussit en **49 secondes** : commandes envoyées
|
||||
par la connexion réelle du joueur, six profils, trois alias natifs,
|
||||
autocomplétion masquée puis réactivée avec Allow Commands, refus des commandes
|
||||
sans droits, état natif de pluie/orage et brouillard appliqué au rendu.
|
||||
|
||||
Le même scénario vérifie la sélection depuis Vanilla, l’indépendance du
|
||||
soleil, la conservation au rechargement, l’absence d’écriture pour une
|
||||
sélection identique, le passage à la date civile suivante, le retour aux
|
||||
commandes natives, la désactivation par configuration, le refus d’un conflit
|
||||
de sauvegarde avant mutation météo et les confirmations FR/EN.
|
||||
Journal : `build/weather082-client.log`.
|
||||
|
||||
Le contrôle existant `Weather057Smoke` est étendu aux six choix persistants,
|
||||
à leur relecture, au maintien pour la même date et au tirage du lendemain.
|
||||
`Weather057Smoke` passe **222 084 assertions**. La commande
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
réussit en **2 min 36 s**, 124 tâches (104 exécutées, 20 à jour).
|
||||
Journal : `build/weather082-check.log`.
|
||||
|
||||
Les 1 700 classes du JAR correspondent aux fichiers compilés ; les seules
|
||||
classes différentes de beta.081 sont le service météo, son état et le journal.
|
||||
Un libellé FR/EN ajouté ; textures, données et identifiants réseau inchangés.
|
||||
Sources et packs vérifiés, même JAR embarqué, archives beta.081 préservées.
|
||||
Reçu : `build/weather082-artifact.json`.
|
||||
|
||||
Les essais utilisent uniquement des mondes jetables et le serveur intégré ;
|
||||
aucun serveur dédié, EULA supplémentaire, monde personnel ou déploiement.
|
||||
|
||||
## Archives
|
||||
|
||||
- [Sanctuary-beta.082.mrpack](../build/Sanctuary-beta.082.mrpack).
|
||||
SHA-256 : `a1af14b31045c8e108a0f525098388aa4f3abc78cdb7f5141eb8cb6b9216e770`.
|
||||
- [Sanctuary-Test-beta.082.mrpack](../build/Sanctuary-Test-beta.082.mrpack).
|
||||
SHA-256 : `770952cf5c67af5bd135e4cd4ac093275407c3bd190396c0468bc36d2ca8212d`.
|
||||
+7
-5
@@ -2,13 +2,15 @@ org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Verified 2026-09-08. 26.3 is still a pre-release; do not silently move worlds.
|
||||
minecraft_version=26.3-pre-2
|
||||
# Verified 2026-09-16 against Mojang/Fabric metadata; personal worlds are not migrated.
|
||||
minecraft_version=26.3
|
||||
loader_version=0.19.5
|
||||
loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.061
|
||||
pack_version=beta.061
|
||||
mod_version=beta.092
|
||||
pack_version=beta.092
|
||||
resource_pack_version=beta.090
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Just Enough Items — Sanctuary preview
|
||||
# Just Enough Items — portage Sanctuary
|
||||
|
||||
Source officielle : https://github.com/mezz/JustEnoughItems/tree/26.2
|
||||
Commit : `aae2dfcfb82e6b5bce787ba72bb2eda7339b274f`.
|
||||
Version source 30.32.0 ; fork `30.32.0-sanctuary.2` pour **Minecraft 26.3-pre-2**
|
||||
(Fabric normalise ce nom en `26.3-pre.2`), Loader 0.19.5, API 0.160.0+26.3, Java 25.
|
||||
Version source 30.32.0 ; fork `30.32.0-sanctuary.3` pour **Minecraft 26.3**
|
||||
(finale), Loader 0.19.5, API 0.160.5+26.3, Java 25.
|
||||
Licence MIT, copyright 2014–2015 mezz. `LICENSE.txt` est inclus sans modification.
|
||||
|
||||
Le fork adapte les entrées SDL, les pipelines RenderPearl, les recettes devenues
|
||||
@@ -37,9 +37,13 @@ Le mod JEI est imbriqué dans le JAR Sanctuary avec deux dépendances :
|
||||
Amecs 26.2 sert uniquement à compiler le point d’intégration optionnel historique ;
|
||||
il n’est ni embarqué ni annoncé compatible 26.3. Le client vérifié utilise les touches
|
||||
natives sans Amecs. Les fichiers de licences des dépendances sont ajoutés au JAR JEI.
|
||||
Ce fork expérimental ne revendique pas une validation upstream de cette préversion.
|
||||
Ce fork expérimental ne revendique pas une validation upstream de ce portage.
|
||||
|
||||
La révision Sanctuary.2 retire le bouton de configuration du catalogue lorsque
|
||||
Sanctuary le demande (dessin, infobulle et interactions), et adapte les libellés
|
||||
FR/EN des favoris, de l’historique et des raccourcis. Les attributions, identifiants
|
||||
JEI, formats de réglages et mécanismes de recettes restent conservés.
|
||||
|
||||
La révision Sanctuary.3 cible Minecraft 26.3 finale et Fabric API 0.160.5+26.3.
|
||||
La version Minecraft et les dépendances sont désormais reprises des propriétés
|
||||
communes du projet ; le comportement du catalogue Sanctuary est conservé.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
plugins { id 'net.fabricmc.fabric-loom' }
|
||||
version='30.32.0-sanctuary.2'
|
||||
version=rootProject.jei_version
|
||||
group='mezz.jei'
|
||||
base { archivesName='jei-sanctuary-26.3-pre-2' }
|
||||
base { archivesName="jei-sanctuary-${rootProject.minecraft_version}" }
|
||||
repositories { mavenCentral(); maven { url='https://maven.siphalor.de/' }; maven { url='https://maven.blamejared.com/' } }
|
||||
providers.exec { workingDir rootProject.projectDir; commandLine 'python3', 'scripts/prepare_jei.py' }.result.get()
|
||||
def upstream=rootProject.file('build/jei-fork')
|
||||
@@ -9,9 +9,9 @@ sourceSets.main.java.setSrcDirs(['Common/src/main/java','Common/src/api/java','F
|
||||
sourceSets.main.resources.setSrcDirs(['Common/src/main/resources','Fabric/src/main/resources','Library/src/main/resources','Gui/src/main/resources'].collect{new File(upstream,it)})
|
||||
loom { accessWidenerPath=new File(upstream,'Fabric/src/main/resources/jei.accesswidener') }
|
||||
dependencies {
|
||||
minecraft 'com.mojang:minecraft:26.3-pre-2'
|
||||
implementation 'net.fabricmc:fabric-loader:0.19.5'
|
||||
implementation 'net.fabricmc.fabric-api:fabric-api:0.160.0+26.3'
|
||||
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
|
||||
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
|
||||
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
|
||||
implementation 'org.jetbrains:annotations:26.0.2'
|
||||
implementation 'org.jspecify:jspecify:1.0.0'
|
||||
implementation 'net.mezzdev:baked-substring-index:0.1.0'
|
||||
@@ -23,7 +23,7 @@ dependencies {
|
||||
java { toolchain { languageVersion=JavaLanguageVersion.of(25) } }
|
||||
tasks.withType(JavaCompile).configureEach { options.encoding='UTF-8'; options.compilerArgs+=['-Xmaxerrs','300'] }
|
||||
processResources {
|
||||
def values=[modId:'jei', version:project.version, modName:'Just Enough Items — Sanctuary preview', modDescription:'Experimental source port for Sanctuary, Minecraft 26.3-pre-2.', modAuthor:'mezz', curseHomepageUrl:'https://www.curseforge.com/minecraft/mc-mods/jei', githubUrl:'https://github.com/mezz/JustEnoughItems', fabricLoaderVersionRange:'>=0.19.5', fabricApiVersionRange:'>=0.160.0+26.3', modJavaVersion:'25', amecsVersionFabric:'1.0.2']
|
||||
def values=[modId:'jei', version:project.version, modName:'Just Enough Items — Sanctuary preview', modDescription:"Source port for Sanctuary, Minecraft ${rootProject.minecraft_version}.", minecraftVersion:rootProject.minecraft_version, modAuthor:'mezz', curseHomepageUrl:'https://www.curseforge.com/minecraft/mc-mods/jei', githubUrl:'https://github.com/mezz/JustEnoughItems', fabricLoaderVersionRange:">=${rootProject.loader_version}", fabricApiVersionRange:">=${rootProject.fabric_api_version}", modJavaVersion:'25', amecsVersionFabric:'1.0.2']
|
||||
inputs.properties(values)
|
||||
filesMatching('fabric.mod.json') { expand(values) }
|
||||
duplicatesStrategy=DuplicatesStrategy.EXCLUDE
|
||||
|
||||
@@ -907,7 +907,7 @@
|
||||
"accessWidener" : "jei.accesswidener",
|
||||
|
||||
"depends": {
|
||||
+ "minecraft": "26.3-pre.2",
|
||||
+ "minecraft": "${minecraftVersion}",
|
||||
"fabricloader": "${fabricLoaderVersionRange}",
|
||||
"fabric-api": "${fabricApiVersionRange}",
|
||||
"java": ">=${modJavaVersion}"
|
||||
|
||||
@@ -95,7 +95,7 @@ tasks.named('runGameTest') {
|
||||
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 = [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 = [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 introClient = providers.gradleProperty('sanctuaryIntroClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -114,6 +114,85 @@ tasks.named('processGametestResources') {
|
||||
if (skillsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Skills025ClientChecks') }
|
||||
def fogClient = providers.gradleProperty('sanctuaryFogClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def sky056Client = providers.gradleProperty('sanctuarySky056ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def cloud077Client = providers.gradleProperty('sanctuaryCloud077ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryCloud077ClientTests', cloud077Client)
|
||||
if (cloud077Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Cloud077ClientChecks') }
|
||||
def navigation076Client = providers.gradleProperty('sanctuaryNavigation076ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryNavigation076ClientTests', navigation076Client)
|
||||
if (navigation076Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Navigation076ClientChecks') }
|
||||
def welcome083Client = providers.gradleProperty('sanctuaryWelcome083ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWelcome083ClientTests', welcome083Client)
|
||||
if (welcome083Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Welcome083ClientChecks') }
|
||||
def piston087Client = providers.gradleProperty('sanctuaryPiston087ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPiston087ClientTests', piston087Client)
|
||||
if (piston087Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Piston087ClientChecks') }
|
||||
def fut086Client = providers.gradleProperty('sanctuaryFut086ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryFut086ClientTests', fut086Client)
|
||||
if (fut086Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Fut086ClientChecks') }
|
||||
def fourneau085Client = providers.gradleProperty('sanctuaryFourneau085ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryFourneau085ClientTests', fourneau085Client)
|
||||
if (fourneau085Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Fourneau085ClientChecks') }
|
||||
def pick092Client = providers.gradleProperty('sanctuaryPick092ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPick092ClientTests', pick092Client)
|
||||
if (pick092Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Pick092ClientChecks') }
|
||||
def boatPassengers091Client = providers.gradleProperty('sanctuaryBoatPassengers091ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBoatPassengers091ClientTests', boatPassengers091Client)
|
||||
if (boatPassengers091Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.BoatPassengers091ClientChecks') }
|
||||
def multiblocks084Client = providers.gradleProperty('sanctuaryMultiblocks084ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMultiblocks084ClientTests', multiblocks084Client)
|
||||
if (multiblocks084Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Multiblocks084ClientChecks') }
|
||||
def menus083Client = providers.gradleProperty('sanctuaryMenus083ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMenus083ClientTests', menus083Client)
|
||||
if (menus083Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Menus083ClientChecks') }
|
||||
def weather082Client = providers.gradleProperty('sanctuaryWeather082ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWeather082ClientTests', weather082Client)
|
||||
if (weather082Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Weather082ClientChecks') }
|
||||
def balance080Client = providers.gradleProperty('sanctuaryBalance080ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBalance080ClientTests', balance080Client)
|
||||
if (balance080Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Balance080ClientChecks') }
|
||||
def solo079Client = providers.gradleProperty('sanctuarySolo079ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySolo079ClientTests', solo079Client)
|
||||
if (solo079Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Solo079ClientChecks') }
|
||||
def autonomy074Client = providers.gradleProperty('sanctuaryAutonomy074ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryAutonomy074ClientTests', autonomy074Client)
|
||||
if (autonomy074Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Autonomy074ClientChecks') }
|
||||
def arena073Client = providers.gradleProperty('sanctuaryArena073ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('arena073Client', arena073Client)
|
||||
if (arena073Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Arena073ClientChecks') }
|
||||
def plans072Client = providers.gradleProperty('sanctuaryPlans072ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('plans072Client', plans072Client)
|
||||
if (plans072Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Plans072ClientChecks') }
|
||||
def familiar071Client = providers.gradleProperty('sanctuaryFamiliar071ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('familiar071Client', familiar071Client)
|
||||
if (familiar071Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Familiar071ClientChecks') }
|
||||
def resourcePack070Client = providers.gradleProperty('sanctuaryResourcePack070ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('resourcePack070Client', resourcePack070Client)
|
||||
if (resourcePack070Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ResourcePack070ClientChecks') }
|
||||
def loading068Client = providers.gradleProperty('sanctuaryLoading068ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('loading068Client', loading068Client)
|
||||
if (loading068Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Loading068ClientChecks') }
|
||||
def familiar067Client = providers.gradleProperty('sanctuaryFamiliar067ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('familiar067Client', familiar067Client)
|
||||
if (familiar067Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Familiar067ClientChecks') }
|
||||
def cosmetic066Client = providers.gradleProperty('sanctuaryCosmetic066ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('cosmetic066Client', cosmetic066Client)
|
||||
if (cosmetic066Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Cosmetic066ClientChecks') }
|
||||
def boat065Client = providers.gradleProperty('sanctuaryBoat065ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('boat065Client', boat065Client)
|
||||
if (boat065Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Boat065ClientChecks') }
|
||||
def loading064Client = providers.gradleProperty('sanctuaryLoading064ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('loading064Client', loading064Client)
|
||||
if (loading064Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Loading064ClientChecks') }
|
||||
def gameplay063Client = providers.gradleProperty('sanctuaryGameplay063ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('gameplay063Client', gameplay063Client)
|
||||
if (gameplay063Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Gameplay063ClientChecks') }
|
||||
def loading063Client = providers.gradleProperty('sanctuaryLoading063ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('loading063Client', loading063Client)
|
||||
inputs.property('loading063Entry', 'fr.koka.sanctuary.client.Loading063ClientAudit')
|
||||
if (loading063Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Loading063ClientAudit') }
|
||||
def sunlight062Client = providers.gradleProperty('sanctuarySunlight062ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sunlight062Client', sunlight062Client)
|
||||
if (sunlight062Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Sunlight062ClientChecks') }
|
||||
def arrivalMount061Client = providers.gradleProperty('sanctuaryArrivalMount061ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('arrivalMount061Client', arrivalMount061Client)
|
||||
if (arrivalMount061Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ArrivalMount061ClientChecks') }
|
||||
@@ -311,10 +390,15 @@ tasks.withType(JavaCompile).configureEach {
|
||||
}
|
||||
|
||||
processResources {
|
||||
// The launcher uses 26.3-pre-2; Fabric's runtime normalizes it to 26.3-pre.2.
|
||||
from(rootProject.file('ressources-pack/sanctuary')) {
|
||||
include 'assets/**', 'pack.mcmeta', 'pack.png'
|
||||
exclude '**/.DS_Store'
|
||||
into 'resourcepacks/textures'
|
||||
}
|
||||
// Normalize launcher pre-release IDs when targeting one; final releases retain their exact ID.
|
||||
def values = [version: project.version, minecraft_version: rootProject.minecraft_version,
|
||||
minecraft_dependency_version: rootProject.minecraft_version.replace('-pre-', '-pre.'),
|
||||
loader_version: rootProject.loader_version, fabric_api_version: rootProject.fabric_api_version]
|
||||
loader_version: rootProject.loader_version, fabric_api_version: rootProject.fabric_api_version, jei_version: rootProject.jei_version]
|
||||
inputs.properties(values)
|
||||
filesMatching('fabric.mod.json') { expand(values) }
|
||||
}
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.arena.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.server.network.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real integrated server, synthetic network players, native combat, escrow and FR/EN UI. */
|
||||
public final class Arena073ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
private final List<ServerPlayer> remotes=new ArrayList<>();
|
||||
private Arenas.Match current;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){Starter039TestSupport.selectFirst(screen);screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);}});
|
||||
ServerTickEvents.END_SERVER_TICK.register(s->{for(var p:List.copyOf(remotes))if(p.level().getServer()==s&&!p.isRemoved()){p.tickCount++;CompanionService.tick(p);FamiliarService.tick(p);FamiliarBattle.tick(p);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Arena073 "+UUID.randomUUID());s.setSeed("73");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=host(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.getAbilities().invulnerable=false;s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");p.level().getGameRules().set(net.minecraft.world.level.gamerules.GameRules.PVP,false,s);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=16;z++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int i=0;i<5;i++)remotes.add(remote(p,"Arena073_"+i,i+2));
|
||||
for(var e:all(p)){fresh(e);e.getInventory().setItem(0,new ItemStack(Items.EMERALD,10));}
|
||||
check(Arenas.create(p,new Arenas.Rules(Arenas.Mode.FAMILIARS,false,true,24,180),0,2).equals("created"),"Create familiar arena");current=Arenas.match(p);
|
||||
check(Arenas.join(current,remotes.get(0)).equals("joined")&&Arenas.join(current,remotes.get(1)).equals("joined"),"Third participant accepted");
|
||||
check(Arenas.bet(current,p,p.getUUID(),1).equals("participant_bet"),"Participant cannot speculate");
|
||||
check(Arenas.bet(current,remotes.get(2),p.getUUID(),4).equals("bet_placed"),"First spectator stake");
|
||||
check(Arenas.bet(current,remotes.get(3),p.getUUID(),2).equals("bet_placed"),"Second spectator stake");
|
||||
check(Arenas.bet(current,remotes.get(4),remotes.get(0).getUUID(),3).equals("bet_placed"),"Losing-side spectator stake");
|
||||
check(Arenas.join(current,remotes.get(2)).equals("bettor"),"Spectator cannot join after betting");
|
||||
check(Arenas.start(current,p).equals("not_ready"),"All entrants must confirm immutable rules");
|
||||
check(Arenas.ledger(s).pot(current.id,true)==6&&Arenas.ledger(s).pot(current.id,false)==9,"Separate entry and spectator pots");
|
||||
for(var e:current.entrants.values())Arenas.ready(current,e.player);
|
||||
});
|
||||
c.runOnClient(m->ArenaClient.open(null));c.waitFor(m->m.gui.screen() instanceof ArenaScreen,100);
|
||||
for(var locale:List.of("fr_fr","en_us")){c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new ArenaScreen(null));});c.waitTicks(5);c.takeScreenshot("arena073-board-"+locale);}
|
||||
server.runOnServer(s->ArenaNetwork.request(host(s),new ArenaNetwork.Request("open",current.id.toString(),"",0,-1,0,0,"")));c.waitTicks(5);c.takeScreenshot("arena073-registration");
|
||||
server.runOnServer(s->{var p=host(s);check(Arenas.start(current,p).equals("started"),"Unanimous start");check(Arenas.bet(current,remotes.get(2),p.getUUID(),1).equals("closed"),"Bets close before countdown");current.until=FamiliarBattle.now();Arenas.tick(s);
|
||||
var pet=FamiliarService.active(p);var rival=FamiliarService.active(remotes.get(0));check(FamiliarBattle.enemy(p,rival)&&!FamiliarBattle.enemy(p,remotes.get(0)),"Familiar-only consent boundary");check(!rival.hurtServer(p.level(),p.damageSources().playerAttack(p),10),"Owner cannot attack a rival pet");check(!p.hurtServer(p.level(),p.damageSources().playerAttack(remotes.get(2)),10),"Bystander cannot attack organizer");
|
||||
pet.setPos(.5,1,2);rival.setPos(1.5,1,2);FamiliarBattle.engage(p,rival,false);
|
||||
});c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(65);
|
||||
server.runOnServer(s->{var p=host(s);check(current.phase==Arenas.Phase.FIGHT,"Three-way arena running");check(current.entrants.values().stream().anyMatch(e->FamiliarBattle.bond(e.player).health()<FamiliarBattle.profile(e.player).health()),"Native autonomous attacks cause wounds");
|
||||
for(int i=0;i<2;i++){var rival=FamiliarService.active(remotes.get(i));if(rival!=null){var pet=FamiliarService.active(p);pet.setPos(.5,1,2);rival.setPos(1.5,1,2);var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).initiative(),false,rival,rival.position());for(int hit=0;hit<50&¤t.phase==Arenas.Phase.FIGHT&&!current.entrants.get(remotes.get(i).getUUID()).out;hit++)FamiliarTechniques.hit(cast,rival,4);}}
|
||||
check(current.phase==Arenas.Phase.FINISHED&&p.getUUID().equals(current.winner),"Last living familiar wins");for(var e:all(p))Arenas.ledger(s).claim(e);
|
||||
check(count(p)==14&&count(remotes.get(0))==8&&count(remotes.get(1))==8,"Winner receives exact entry pool");check(count(remotes.get(2))==12&&count(remotes.get(3))==11&&count(remotes.get(4))==7,"Spectator pari-mutuel payout conserves all nine items");
|
||||
Arenas.ledger(s).claim(p);check(count(p)==14,"No duplicate payout");System.out.println("ARENA073_FAMILIARS_PASS: three entrants, native AI, external interference, registration and spectator pools");
|
||||
});c.waitTicks(110);
|
||||
server.runOnServer(s->{var p=host(s);var q=remotes.get(0);var r=remotes.get(1);for(var e:all(p))fresh(e);
|
||||
check(Arenas.create(q,new Arenas.Rules(Arenas.Mode.PLAYERS,false,false,24,180),-1,0).equals("created"),"Players-only arena");current=Arenas.match(q);check(Arenas.join(current,p).equals("joined")&&Arenas.join(current,r).equals("joined"),"Three human entrants");for(var e:current.entrants.values())Arenas.ready(current,e.player);Arenas.start(current,q);current.until=FamiliarBattle.now();Arenas.tick(s);
|
||||
check(p.canHarmPlayer(q),"Consensual arena bypasses global PvP=false");check(!p.canHarmPlayer(remotes.get(2)),"Global PvP remains off for spectators");check(!FamiliarBattle.enemy(p,FamiliarService.active(q)),"Familiars excluded from human-only fight");
|
||||
check(!p.gameMode.destroyBlock(new net.minecraft.core.BlockPos(0,0,0)),"Arena blocks native/grouped mining");
|
||||
check(p.gameMode.useItem(p,p.level(),new ItemStack(Items.COOKED_BEEF),net.minecraft.world.InteractionHand.MAIN_HAND)==net.minecraft.world.InteractionResult.FAIL,"Food disabled by arena rules");
|
||||
int items=count(q);q.setInvulnerableTime(0);check(q.hurtServer(p.level(),p.damageSources().playerAttack(p),100),"Native player damage accepted");check(q.getHealth()<=0&&!q.isAlive()&&count(q)==0&¤t.entrants.get(q.getUUID()).out,"Native lethal hit kills and moves inventory to a grave");
|
||||
check(!q.hurtServer(p.level(),p.damageSources().playerAttack(p),100),"Eliminated players cannot be hit again");r.setInvulnerableTime(0);r.hurtServer(p.level(),p.damageSources().playerAttack(p),100);check(current.phase==Arenas.Phase.FINISHED&&p.getUUID().equals(current.winner),"Player arena resolves by actual death");System.out.println("ARENA073_PLAYERS_PASS: native damage, real death, PvP consent and pet exclusion");
|
||||
});c.waitTicks(110);
|
||||
server.runOnServer(s->{var p=host(s);var q=remotes.get(0);var r=remotes.get(1);for(int i=0;i<2;i++){var old=remotes.get(i);var petOld=FamiliarService.active(old);if(petOld!=null)petOld.discard();old.discard();remotes.set(i,remote(p,"Rejoined073_"+i,3+i));}q=remotes.get(0);r=remotes.get(1);for(var e:all(p))fresh(e);
|
||||
check(Arenas.create(r,new Arenas.Rules(Arenas.Mode.MIXED,true,true,24,180),-1,0).equals("created"),"Mixed duel creation");current=Arenas.match(r);check(Arenas.join(current,p).equals("joined"),"Mixed duel second entrant");check(Arenas.join(current,q).equals("unavailable"),"Only duels cap entry at two");for(var e:current.entrants.values())Arenas.ready(current,e.player);Arenas.start(current,r);current.until=FamiliarBattle.now();Arenas.tick(s);
|
||||
var rival=FamiliarService.active(r);var pet=FamiliarService.active(p);pet.setPos(.5,1,2);rival.setPos(1.5,1,2);check(FamiliarBattle.enemy(p,r)&&FamiliarBattle.enemy(p,rival),"Mixed fight allows both target kinds");check(rival.hurtServer(p.level(),p.damageSources().playerAttack(p),1000),"Players can KO rival pets in mixed fight");check(!current.entrants.get(r.getUUID()).out&¤t.phase==Arenas.Phase.FIGHT,"Pet KO leaves its human in mixed fight");
|
||||
Arenas.eliminate(r,"forfeit");check(p.getUUID().equals(current.winner),"Mixed forfeit settles winner");
|
||||
escrow(s,p,q);System.out.println("ARENA073_MIXED_ESCROW_PASS: two-player duel, mixed combat, refund, restart and item components");
|
||||
});c.waitTicks(110);
|
||||
server.runOnServer(s->{var p=host(s);var q=remotes.get(0);fresh(p);fresh(q);p.teleportTo(.5,1,.5);q.setPos(.5,1,4);check(FamiliarDuels.invite(p,q).equals("duel.sent"),"Legacy duel stays available");var duel=FamiliarDuels.match(p);var board=Arenas.get(s,duel.id);check(board!=null&&board.legacy&&board.entrants.size()==2,"Legacy duel publicly listed");check(Arenas.bet(board,remotes.get(2),p.getUUID(),1).equals("bet_placed"),"Spectators can bet on legacy duels");int before=count(remotes.get(2));FamiliarDuels.finish(duel,-1,"cancelled");Arenas.ledger(s).claim(remotes.get(2));check(count(remotes.get(2))==before+1,"Cancelled legacy duel refunds spectators");
|
||||
fresh(p);check(Arenas.create(p,new Arenas.Rules(Arenas.Mode.PLAYERS,false,true,96,600),-1,0).equals("created"),"Large roster fixture");current=Arenas.match(p);
|
||||
for(int i=0;i<24;i++){var other=remote(p,"Roster073_"+i,2+i%5);remotes.add(other);check(Arenas.join(current,other).equals("joined"),"No artificial arena player cap at entrant "+(i+2));}
|
||||
check(current.entrants.size()==25,"Arena roster exceeds legacy and page limits");var doomed=remotes.getLast();int beforeSize=current.entrants.size();doomed.setInvulnerableTime(0);doomed.hurtServer(p.level(),doomed.damageSources().generic(),100);check(!doomed.isAlive()&¤t.phase==Arenas.Phase.OPEN&¤t.entrants.size()==beforeSize-1,"Death during registration withdraws only that entrant");var replacement=remote(p,"Roster073_new",6);remotes.add(replacement);check(Arenas.join(current,replacement).equals("joined"),"Free place stays open after registration death");ArenaNetwork.request(p,new ArenaNetwork.Request("open",current.id.toString(),"",0,-1,0,1,""));
|
||||
});c.waitTicks(8);c.runOnClient(m->{check(ArenaClient.data.get("total").getAsInt()==25&&ArenaClient.data.getAsJsonObject("match").getAsJsonArray("entrants").size()==12,"Paginated bounded roster payload");});c.takeScreenshot("arena073-roster-page2");
|
||||
server.runOnServer(s->{Arenas.finish(current,null,"cancelled");for(var q:remotes){FamiliarDuels.disconnect(q);FamiliarBattle.release(q);var pet=FamiliarService.active(q);if(pet!=null)pet.discard();q.discard();}remotes.clear();});
|
||||
c.waitTicks(210);server.runOnServer(s->fresh(host(s)));
|
||||
c.runOnClient(m->ArenaClient.open(null));c.waitTicks(8);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.arena.create").onPress(enter()));c.waitTicks(4);c.takeScreenshot("arena073-create");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.arena.create_confirm").onPress(enter()));c.waitTicks(8);
|
||||
server.runOnServer(s->{check(Arenas.match(host(s))!=null,"Real GUI creates event through network");Arenas.finish(Arenas.match(host(s)),null,"cancelled");});c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(110);
|
||||
fr.koka.sanctuary.gametest.Duel054Checks.run(c,server);
|
||||
|
||||
}finally{intro.set(false);remotes.clear();}
|
||||
System.out.println("ARENA073_PASS");
|
||||
}
|
||||
private void escrow(MinecraftServer s,ServerPlayer p,ServerPlayer q){
|
||||
var ledger=Arenas.ledger(s);var id=UUID.randomUUID();p.getInventory().clearContent();q.getInventory().clearContent();p.getInventory().setItem(0,new ItemStack(Items.EMERALD,7));q.getInventory().setItem(0,new ItemStack(Items.EMERALD,5));
|
||||
check(ledger.deposit(id,p,p.getUUID(),true,new ItemStack(Items.EMERALD),7)&&ledger.deposit(id,q,q.getUUID(),true,new ItemStack(Items.EMERALD),5),"Durable entry deposits");
|
||||
for(int slot:CompanionInventory.slots(p,true))p.getInventory().setItem(slot,new ItemStack(Items.STONE,64));ledger.finish(id,p.getUUID());ledger.claim(p);check(ledger.pending(p),"Full inventory retains winnings");
|
||||
p.getInventory().clearContent();ledger.claim(p);check(count(p)==12&&!ledger.pending(p),"Deferred payout exact");
|
||||
var weights=new TreeMap<UUID,Long>();weights.put(new UUID(0,1),1L);weights.put(new UUID(0,2),2L);check(ArenaLedger.split(10,weights).values().stream().mapToLong(Long::longValue).sum()==10,"Integer remainder conserved");
|
||||
var interrupted=UUID.randomUUID();check(ledger.deposit(interrupted,p,p.getUUID(),false,new ItemStack(Items.EMERALD),3),"Restart fixture deposited");
|
||||
try{var recovered=new ArenaLedger(s);recovered.claim(p);check(count(p)==12,"Restart refunds live event");recovered.claim(p);check(count(p)==12,"Restart refund is idempotent");
|
||||
var withdrawal=UUID.randomUUID();p.getInventory().setItem(0,new ItemStack(Items.EMERALD,12));q.getInventory().setItem(0,new ItemStack(Items.EMERALD,5));
|
||||
check(recovered.deposit(withdrawal,q,q.getUUID(),true,new ItemStack(Items.EMERALD),2)&&recovered.deposit(withdrawal,p,q.getUUID(),false,new ItemStack(Items.EMERALD),3),"Withdrawal fixture");recovered.leave(withdrawal,q.getUUID());recovered.claim(p);recovered.claim(q);check(count(p)==12&&count(q)==5,"Withdrawal refunds both entry and bets on withdrawn entrant");
|
||||
// Fresh journal replaces the test-owned ledger only after all old entries are settled.
|
||||
var field=Arenas.class.getDeclaredField("LEDGERS");field.setAccessible(true);((Map<MinecraftServer,ArenaLedger>)field.get(null)).put(s,recovered);
|
||||
var special=new ItemStack(Items.CHEST);special.set(DataComponents.CUSTOM_NAME,Component.literal("Arena archive"));special.set(DataComponents.CONTAINER,net.minecraft.world.item.component.ItemContainerContents.fromItems(List.of(new ItemStack(Items.DIAMOND,3))));p.getInventory().setItem(1,special.copy());var custom=UUID.randomUUID();check(recovered.deposit(custom,p,p.getUUID(),true,special,1),"Component-bearing item deposited");recovered.finish(custom,null);recovered.claim(p);check(CompanionInventory.slots(p,true).stream().anyMatch(slot->ItemStack.matches(special,p.getInventory().getItem(slot))),"Nested container components preserved");
|
||||
var journal=s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("sanctuary/arena-stakes-v1.json");
|
||||
var crash=UUID.randomUUID();check(recovered.deposit(crash,p,p.getUUID(),true,new ItemStack(Items.EMERALD),3),"Crash after native debit fixture");
|
||||
var root=com.google.gson.JsonParser.parseString(java.nio.file.Files.readString(journal)).getAsJsonObject();var entry=root.getAsJsonObject("deposits").entrySet().iterator().next();var template=entry.getValue().getAsJsonObject().deepCopy();entry.getValue().getAsJsonObject().addProperty("state","pending");java.nio.file.Files.writeString(journal,root.toString());
|
||||
recovered=new ArenaLedger(s);recovered.claim(p);check(count(p)==12,"Prepared debit with saved receipt refunds exactly once");
|
||||
root=com.google.gson.JsonParser.parseString(java.nio.file.Files.readString(journal)).getAsJsonObject();template.addProperty("state","pending");root.getAsJsonObject("deposits").add(UUID.randomUUID().toString(),template);java.nio.file.Files.writeString(journal,root.toString());
|
||||
recovered=new ArenaLedger(s);recovered.claim(p);check(count(p)==12,"Prepared debit without receipt never invents a refund");
|
||||
var payment=UUID.randomUUID();check(recovered.deposit(payment,p,p.getUUID(),true,new ItemStack(Items.EMERALD),3),"Crash after payout fixture");recovered.finish(payment,p.getUUID());
|
||||
root=com.google.gson.JsonParser.parseString(java.nio.file.Files.readString(journal)).getAsJsonObject();recovered.claim(p);var due=root.getAsJsonObject("payouts").entrySet().iterator().next().getValue().getAsJsonObject();due.addProperty("transfer",p.getAttachedOrElse(ArenaLedger.RECEIPT,""));due.addProperty("moving",3);java.nio.file.Files.writeString(journal,root.toString());
|
||||
recovered=new ArenaLedger(s);recovered.claim(p);recovered.claim(p);check(count(p)==12,"Saved payout receipt prevents paying the same items again");
|
||||
String valid=java.nio.file.Files.readString(journal);var invalid=com.google.gson.JsonParser.parseString(valid).getAsJsonObject();invalid.addProperty("schema",2);java.nio.file.Files.writeString(journal,invalid.toString());boolean refused=false;try{new ArenaLedger(s);}catch(Exception expected){refused=true;}check(refused&&java.nio.file.Files.readString(journal).equals(invalid.toString()),"Unknown journal schema rejected without overwrite");java.nio.file.Files.writeString(journal,valid);recovered=new ArenaLedger(s);((Map<MinecraftServer,ArenaLedger>)field.get(null)).put(s,recovered);
|
||||
System.out.println("ARENA073_RECOVERY_PASS: before debit, after debit, after payout, unknown schema");
|
||||
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
}
|
||||
private static ServerPlayer remote(ServerPlayer p,String name,int z){var s=p.level().getServer();var profile=new GameProfile(UUID.randomUUID(),name);var other=new ServerPlayer(s,p.level(),profile,ClientInformation.createDefault());var connection=new Connection(PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);other.connection=new ServerGamePacketListenerImpl(s,connection,other,CommonListenerCookie.createInitial(profile,false));other.setPos(.5,1,z);other.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(other,ProgressRecord.INITIAL);other.setHealth(6);other.connection.handleAcceptPlayerLoad(new ServerboundPlayerLoadedPacket());p.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(other)));p.level().addNewPlayer(other);return other;}
|
||||
private List<ServerPlayer> all(ServerPlayer p){var list=new ArrayList<>(remotes);list.addFirst(p);return list;}
|
||||
private static void fresh(ServerPlayer p){p.setHealth(p.getMaxHealth());p.getAbilities().invulnerable=false;p.setInvulnerableTime(0);p.stopRiding();CarryService.detach(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.removeAttached(FamiliarBattle.COMBAT_LOCK);p.removeAttached(FamiliarBattle.LAST_FAMILIAR);var egg=new ItemStack(Items.WOLF_SPAWN_EGG);var profile=FamiliarBattle.world(p.level().getServer()).catalog.forEgg(egg);var id=UUID.randomUUID();FamiliarBattle.world(p.level().getServer()).store.put(FamiliarBond.create(id,profile,0,"test",p.getUUID()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,FamiliarIdentity.assign(egg,id));CompanionService.tick(p);p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);check(FamiliarService.active(p)!=null,"Spawn test familiar");}
|
||||
private static ServerPlayer host(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static int count(ServerPlayer p){return CompanionInventory.slots(p,true).stream().mapToInt(i->p.getInventory().getItem(i).is(Items.EMERALD)?p.getInventory().getItem(i).getCount():0).sum();}
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> widgets(net.minecraft.client.gui.components.events.GuiEventListener root){return java.util.stream.Stream.concat(java.util.stream.Stream.of(root),root instanceof net.minecraft.client.gui.components.events.ContainerEventHandler c?c.children().stream().flatMap(Arena073ClientChecks::widgets):java.util.stream.Stream.empty());}
|
||||
private static Button button(net.minecraft.client.gui.screens.Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.companion.CompanionWorld;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Exercises server decisions, native navigation/damage and actual short/hold keyboard gestures. */
|
||||
public final class Autonomy074ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
var forbidden=new AtomicReference<UUID>();
|
||||
CompanionWorld.ALLOW.register((p,pos,op)->forbidden.get()==null||p.level().getEntity(forbidden.get())==null||!p.level().getEntity(forbidden.get()).blockPosition().equals(pos));
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Autonomy074 "+UUID.randomUUID());s.setSeed("74");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");
|
||||
for(int x=-20;x<=20;x++)for(int z=-20;z<=20;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
for(var personality:FamiliarPersonality.values()){
|
||||
var victim=new AtomicReference<Mob>();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.setDeltaMovement(Vec3.ZERO);equip(p,Items.WOLF_SPAWN_EGG,personality,1000);FamiliarService.active(p).setPos(.5,1,1.5);victim.set(enemy(p,9.5,1,1.5));});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check((FamiliarBattle.target(p)!=null)==(personality==FamiliarPersonality.AGGRESSIVE),"12-block hunt: "+personality);FamiliarBattle.cancel(p);victim.get().setPos(4.5,1,1.5);});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check((FamiliarBattle.target(p)!=null)==(personality==FamiliarPersonality.AGGRESSIVE||personality==FamiliarPersonality.PROTECTIVE),"6-block guard: "+personality+" player="+p.position()+" pet="+FamiliarService.active(p).position()+" victim="+victim.get().position()+" target="+FamiliarBattle.target(p));
|
||||
FamiliarBattle.cancel(p);FamiliarBattle.damageEvent(victim.get(),p.damageSources().playerAttack(p),1);
|
||||
check((FamiliarBattle.target(p)!=null)==(personality==FamiliarPersonality.AGGRESSIVE),"Only aggressive joins unsolicited owner attack: "+personality);
|
||||
FamiliarBattle.cancel(p);p.getAbilities().invulnerable=false;p.setInvulnerableTime(0);p.hurtServer(p.level(),p.damageSources().mobAttack(victim.get()),1);p.getAbilities().invulnerable=true;
|
||||
check((FamiliarBattle.target(p)!=null)==(personality!=FamiliarPersonality.PACIFIST),"Native owner hurt: "+personality);
|
||||
if(personality==FamiliarPersonality.PACIFIST){check(FamiliarBattle.state(p).evadeUntil>p.tickCount,"Pacifist retreats");FamiliarBattle.engage(p,victim.get(),false);}
|
||||
});c.waitTicks(110);
|
||||
server.runOnServer(s->{var p=player(s);check(victim.get().getHealth()<100,"Real autonomous navigation/damage: "+personality+" distance="+FamiliarService.active(p).distanceTo(victim.get()));victim.get().discard();FamiliarBattle.cancel(p);p.setHealth(p.getMaxHealth());
|
||||
System.out.println("AUTONOMY074_POLICY_PASS "+personality);
|
||||
});
|
||||
}
|
||||
// Protective support species can intercept, really strike, then come back to guard.
|
||||
var first=new AtomicReference<Mob>();var next=new AtomicReference<Mob>();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.setDeltaMovement(Vec3.ZERO);equip(p,Items.VILLAGER_SPAWN_EGG,FamiliarPersonality.PROTECTIVE,1000);FamiliarService.active(p).setPos(.5,1,1.5);p.getAbilities().invulnerable=false;first.set(enemy(p,4.5,1,1.5));});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==first.get(),"Guard first engages an idle intruder");next.set(enemy(p,5.5,1,-1.5));next.get().setTarget(p);});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==next.get(),"Guard prioritizes a monster targeting the owner: selected="+FamiliarBattle.target(p)+" threat="+next.get()+" threatTarget="+next.get().getTarget()+" p="+p.position());});c.waitTicks(100);
|
||||
server.runOnServer(s->{var p=player(s);check(next.get().getHealth()<100,"Protective villager really defends without a shield");next.get().discard();});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==first.get(),"Reacquires next intruder after target dies");first.get().setPos(13.5,1,1.5);});c.waitTicks(70);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null,"Guard gives up pursuit beyond ten blocks");check(FamiliarService.active(p).distanceToSqr(p)<25,"Guard returns to owner");first.get().discard();p.getAbilities().invulnerable=true;
|
||||
System.out.println("AUTONOMY074_GUARD_PASS");
|
||||
});
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.VILLAGER_SPAWN_EGG,FamiliarPersonality.PASSIVE,1000);
|
||||
p.getAbilities().invulnerable=false;var bee=EntityTypes.BEE.create(p.level(),EntitySpawnReason.COMMAND);bee.setNoAi(true);bee.setPos(3.5,1,1.5);p.level().addFreshEntity(bee);
|
||||
check(!FamiliarBattle.enemy(p,bee),"Peaceful bee is not an enemy");bee.setTarget(p);check(FamiliarBattle.enemy(p,bee),"An attacking neutral bee can be defended against");
|
||||
FamiliarBattle.damageEvent(p,p.damageSources().mobAttack(bee),1);check(FamiliarBattle.target(p)==bee,"Passive companion defends against a neutral aggressor");bee.discard();p.getAbilities().invulnerable=true;FamiliarBattle.cancel(p);
|
||||
equip(p,Items.VILLAGER_SPAWN_EGG,FamiliarPersonality.PACIFIST,1000);FamiliarBattle.state(p).lastRequest=-100;FamiliarBattle.command(p,"protect");first.set(enemy(p,4.5,1,1.5));
|
||||
});c.waitTicks(90);
|
||||
server.runOnServer(s->{var p=player(s);check(first.get().getHealth()<100,"Pacifist obeys explicit guard order and really strikes");next.set(enemy(p,8.5,1,1.5));FamiliarBattle.engage(p,next.get(),false);
|
||||
FamiliarBattle.automatic(p,first.get(),FamiliarPersonality.Trigger.OWNER_HURT);check(FamiliarBattle.target(p)==next.get()&&FamiliarBattle.state(p).explicitTarget,"Explicit target has priority over another attacker");
|
||||
first.get().discard();next.get().discard();FamiliarBattle.cancel(p);System.out.println("AUTONOMY074_NEUTRAL_AND_ORDERS_PASS");
|
||||
});
|
||||
// A pet's sight is independent from the owner's, but neither sees through walls.
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.WOLF_SPAWN_EGG,FamiliarPersonality.AGGRESSIVE,1000);var pet=FamiliarService.active(p);pet.setPos(.5,1,4.5);pet.setNoAi(true);first.set(enemy(p,8.5,1,4.5));
|
||||
for(int y=1;y<=4;y++)for(int z=0;z<=2;z++)p.level().setBlockAndUpdate(new BlockPos(3,y,z),Blocks.STONE.defaultBlockState());
|
||||
check(!p.hasLineOfSight(first.get())&&pet.hasLineOfSight(first.get()),"Different sightlines fixture");
|
||||
});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==first.get(),"Own sight selects a target hidden from player");FamiliarBattle.cancel(p);for(int y=1;y<=4;y++)for(int z=-3;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(3,y,z),Blocks.STONE.defaultBlockState());});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null,"No acquisition through a solid wall");FamiliarBattle.automatic(p,first.get(),FamiliarPersonality.Trigger.OWNER_HURT);
|
||||
for(int x=7;x<=10;x++)for(int z=3;z<=6;z++)for(int y=1;y<=4;y++)if(x==7||x==10||z==3||z==6||y==4)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.STONE.defaultBlockState());
|
||||
FamiliarService.active(p).setNoAi(false);});c.waitTicks(115);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null,"Loses enclosed target instead of looping at wall: pet="+FamiliarService.active(p).position()+" victim="+first.get().position());check(first.get().getUUID().equals(FamiliarBattle.state(p).unreachable),"Unreachable target has bounded retry delay");first.get().discard();
|
||||
for(int x=7;x<=10;x++)for(int z=3;z<=6;z++)for(int y=1;y<=4;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
for(int y=1;y<=4;y++)for(int z=-3;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(3,y,z),Blocks.AIR.defaultBlockState());
|
||||
FamiliarBattle.cancel(p);first.set(enemy(p,4.5,1,1.5));forbidden.set(first.get().getUUID());
|
||||
});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null,"Permission veto prevents automatic acquisition");forbidden.set(null);FamiliarBattle.state(p).workMode=true;});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null,"Work mode suppresses autonomy");FamiliarBattle.state(p).workMode=false;FamiliarBattle.state(p).lastRequest=-100;FamiliarBattle.command(p,"follow");});c.waitTicks(40);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null,"Follow order has a five-second grace period");});c.waitTicks(85);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==first.get(),"Autonomy resumes after follow grace: p="+p.position()+" pet="+FamiliarService.active(p).position()+" tick="+p.tickCount+" after="+FamiliarBattle.state(p).automaticAfter+" target="+FamiliarBattle.target(p)+" victim="+first.get().position());first.get().discard();FamiliarBattle.cancel(p);System.out.println("AUTONOMY074_SIGHT_WORK_FOLLOW_PASS");});
|
||||
// Server aims the short order; native F stays untouched, and long holds never release an attack.
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.WOLF_SPAWN_EGG,FamiliarPersonality.PACIFIST,1000);p.teleportTo(.5,1,.5);first.set(enemy(p,.5,1,7.5));});c.waitTicks(15);
|
||||
c.getInput().lookAt(new BlockPos(0,2,7));c.waitTicks(5);c.getInput().pressKey(InputConstants.KEY_H);c.waitTicks(10);
|
||||
c.runOnClient(m->check(m.gui.screen()==null,"Short H does not open a screen"));
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==first.get()&&FamiliarBattle.state(p).explicitTarget,"Short H attacks selected mob even with pacifist");});
|
||||
c.getInput().lookAt(new BlockPos(0,20,-8));c.waitTicks(5);c.getInput().pressKey(InputConstants.KEY_H);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==null&&FamiliarBattle.state(p).order==FamiliarBattle.Order.FOLLOW,"Short H without a target returns to follow");});
|
||||
c.getInput().lookAt(new BlockPos(0,2,7));c.waitTicks(5);c.getInput().holdKey(InputConstants.KEY_H);c.waitFor(m->m.gui.screen() instanceof FamiliarOrdersScreen,100);c.getInput().releaseKey(InputConstants.KEY_H);c.waitTicks(5);
|
||||
c.takeScreenshot("autonomy074-orders");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(10);
|
||||
server.runOnServer(s->check(FamiliarBattle.target(player(s))==null,"Releasing long H does not fire a quick command"));
|
||||
c.runOnClient(m->{KeyMapping.get("sanctuary.familiar.orders_key").setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_Y));KeyMapping.resetMapping();});
|
||||
c.getInput().pressKey(InputConstants.KEY_Y);c.waitTicks(10);server.runOnServer(s->check(FamiliarBattle.target(player(s))==first.get(),"Remapped short Y orders work"));
|
||||
c.getInput().holdKey(InputConstants.KEY_Y);c.waitFor(m->m.gui.screen() instanceof FamiliarOrdersScreen,100);c.getInput().releaseKey(InputConstants.KEY_Y);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().setItem(0,new ItemStack(Items.DIAMOND));p.getInventory().setSelectedSlot(0);p.setItemInHand(net.minecraft.world.InteractionHand.OFF_HAND,new ItemStack(Items.GOLD_INGOT));p.inventoryMenu.broadcastChanges();});c.waitTicks(10);c.getInput().pressKey(InputConstants.KEY_F);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getMainHandItem().is(Items.GOLD_INGOT)&&p.getOffhandItem().is(Items.DIAMOND),"F remains vanilla swap hands");first.get().discard();FamiliarBattle.cancel(p);});
|
||||
c.runOnClient(m->{var key=KeyMapping.get("sanctuary.familiar.orders_key");key.setKey(key.getDefaultKey());KeyMapping.resetMapping();});
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());FamiliarClient.open(null);});c.waitTicks(5);c.takeScreenshot("autonomy074-personality-"+locale);c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
System.out.println("AUTONOMY074_INPUT_PASS: native H tap/hold, remapped Y, F swap, FR/EN screens");
|
||||
}finally{intro.set(false);forbidden.set(null);c.getInput().releaseKey(InputConstants.KEY_H);c.getInput().releaseKey(InputConstants.KEY_Y);}
|
||||
System.out.println("AUTONOMY074_PASS");
|
||||
}
|
||||
private static void equip(ServerPlayer p,Item item,FamiliarPersonality personality,int size){
|
||||
p.stopRiding();CarryService.detach(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.removeAttached(FamiliarBattle.COMBAT_LOCK);p.removeAttached(FamiliarBattle.LAST_FAMILIAR);
|
||||
var egg=new ItemStack(item);var scale=new CompoundTag();scale.putInt("schema",1);scale.putInt("per_mille",size);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,scale));
|
||||
var profile=FamiliarBattle.world(p.level().getServer()).catalog.forEgg(egg);UUID id;do{id=UUID.randomUUID();}while(FamiliarPersonality.of(id,profile.id())!=personality);
|
||||
FamiliarBattle.world(p.level().getServer()).store.put(FamiliarBond.create(id,profile,0,"test",p.getUUID()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,FamiliarIdentity.assign(egg,id));
|
||||
CompanionService.tick(p);p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);check(FamiliarService.active(p)!=null,"Familiar spawned: "+item);p.inventoryMenu.broadcastChanges();
|
||||
}
|
||||
private static Mob enemy(ServerPlayer p,double x,double y,double z){var e=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);e.setNoAi(true);e.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);e.setHealth(100);e.setPos(x,y,z);p.level().addFreshEntity(e);return e;}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.building.*;
|
||||
import fr.koka.sanctuary.mining.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.ai.attributes.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Actual input, native prediction, play packets and authoritative blocks in a disposable solo world. */
|
||||
public final class Balance080ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Balance080 "+UUID.randomUUID());s.setSeed("80");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;
|
||||
for(int x=-4;x<=4;x++)for(int z=-2;z<=10;z++){p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.BEDROCK.defaultBlockState());for(int y=1;y<=6;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());}
|
||||
p.teleportTo(.5,1,.75);p.setOnGround(true);curve(p);
|
||||
});
|
||||
// The base skill places nearby, rejects a normal 4.25-block click, and consumes no stock on rejection.
|
||||
setup(c,server,0,0,3,Items.COBBLESTONE,false);use(c);server.waitFor(s->player(s).level().getBlockState(pos(3).north()).is(Blocks.COBBLESTONE),100);
|
||||
setup(c,server,0,0,5,Items.COBBLESTONE,false);use(c);c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(5).north()).isAir()&&p.getMainHandItem().getCount()==64,"Rank zero rejects far placement without consuming stock");});
|
||||
// Construction alone reaches 5.25 blocks; Mining stays at its own rank-zero reach.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult h&&h.getBlockPos().equals(pos(6))&&h.getType()==HitResult.Type.BLOCK,"Extended construction target picked"));
|
||||
c.getInput().holdKey(o->o.keyAttack);c.waitTicks(25);c.getInput().releaseKey(o->o.keyAttack);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos(6)).is(Blocks.STONE),"Construction does not extend mining"));
|
||||
use(c);server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,true);use(c);server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
// beta.081: the newly earned half-block works through the actual pick and play packet.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,.25));
|
||||
c.waitFor(m->Math.abs(m.player.getZ()-.25)<.01,100);c.getInput().lookAt(pos(6));c.waitTicks(5);use(c);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
// Outside six blocks, a forged hit is rejected even within the native packet tolerance.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,-.25));
|
||||
c.waitFor(m->Math.abs(m.player.getZ()+.25)<.01,100);c.getInput().lookAt(pos(6));c.waitTicks(5);
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,6),Direction.NORTH,pos(6),false)));c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(6).north()).isAir()&&p.getMainHandItem().getCount()==64,"Six-block maximum is enforced by the server");});
|
||||
// A deliberately forged out-of-range native packet cannot place or consume an item.
|
||||
setup(c,server,7,7,8,Items.COBBLESTONE,false);
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,8),Direction.NORTH,pos(8),false)));c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(8).north()).isAir()&&p.getMainHandItem().getCount()==64,"Server rejects placement beyond maximum plus native packet tolerance");});
|
||||
// High Mining + low Construction: mine the far stone, but still cannot place at the same distance.
|
||||
setup(c,server,7,0,6,Items.COBBLESTONE,false);use(c);c.waitTicks(12);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos(6).north()).isAir(),"Mining does not extend placement"));
|
||||
setup(c,server,7,0,6,Items.DIAMOND_PICKAXE,false);
|
||||
c.getInput().holdKey(o->o.keyAttack);server.waitFor(s->player(s).level().getBlockState(pos(6)).isAir(),140);c.getInput().releaseKey(o->o.keyAttack);
|
||||
// Low Mining rejects the same block despite the normal interaction highlight.
|
||||
setup(c,server,0,7,5,Items.DIAMOND_PICKAXE,false);c.getInput().holdKey(o->o.keyAttack);c.waitTicks(30);c.getInput().releaseKey(o->o.keyAttack);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos(5)).is(Blocks.STONE),"Starting mining reach is reduced"));
|
||||
// A distant chest is only a building support, not a newly extended container interaction.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).level().setBlockAndUpdate(pos(6),Blocks.CHEST.defaultBlockState()));c.waitTicks(8);use(c);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
server.runOnServer(s->check(player(s).containerMenu==player(s).inventoryMenu,"Far support does not open a container"));
|
||||
// Offhand building reach must not silently extend a tool held in the main hand.
|
||||
setup(c,server,0,7,6,Items.FLINT_AND_STEEL,false);server.runOnServer(s->{player(s).setItemInHand(InteractionHand.OFF_HAND,new ItemStack(Items.COBBLESTONE,64));player(s).inventoryMenu.broadcastChanges();});c.waitTicks(10);use(c);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
server.runOnServer(s->check(player(s).getMainHandItem().getDamageValue()==0,"Offhand reach does not extend flint and steel"));
|
||||
// A full plane still places outside the anchor's range, through the existing grouped contract.
|
||||
setup(c,server,7,7,6,Items.COBBLESTONE,false);server.runOnServer(s->{for(int x=-1;x<=1;x++)for(int y=1;y<=3;y++)player(s).level().setBlockAndUpdate(new BlockPos(x,y,6),Blocks.STONE.defaultBlockState());});c.waitTicks(8);
|
||||
c.getInput().holdKey(InputConstants.KEY_R);c.waitTicks(4);
|
||||
c.runOnClient(m->{for(int i=0;i<6;i++)BuildingClient.scroll(-1);});c.waitFor(m->BuildingClient.previewSize()==9,150);
|
||||
c.getInput().holdKey(o->o.keyUse);server.waitFor(s->player(s).getMainHandItem().getCount()==55,200);c.getInput().releaseKey(o->o.keyUse);c.getInput().releaseKey(InputConstants.KEY_R);
|
||||
server.runOnServer(s->{for(int x=-1;x<=1;x++)for(int y=1;y<=3;y++)check(player(s).level().getBlockState(new BlockPos(x,y,5)).is(Blocks.COBBLESTONE),"All nine grouped cells placed");});
|
||||
// Vein mining starts beyond vanilla reach with Mining, using the same frozen-anchor rules.
|
||||
setup(c,server,7,0,6,Items.DIAMOND_PICKAXE,false);server.runOnServer(s->{for(int x=-1;x<=1;x++)for(int y=1;y<=3;y++)player(s).level().setBlockAndUpdate(new BlockPos(x,y,6),Blocks.STONE.defaultBlockState());});c.waitTicks(8);
|
||||
c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m->MiningClient.previewSize()==9,150);c.getInput().holdKey(o->o.keyAttack);
|
||||
server.waitFor(s->MiningManager.active(player(s)),100);c.getInput().lookAt(new BlockPos(0,4,-3));
|
||||
server.waitFor(s->player(s).level().getBlockState(pos(6)).isAir(),300);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);
|
||||
server.runOnServer(s->{for(int x=-1;x<=1;x++)for(int y=1;y<=3;y++)check(player(s).level().getBlockState(new BlockPos(x,y,6)).isAir(),"Looking away preserves and completes all nine vein blocks");});
|
||||
// Water placement uses its own native raycast and must receive the same reach.
|
||||
setup(c,server,7,7,6,Items.LILY_PAD,false);server.runOnServer(s->{var p=player(s);p.level().setBlockAndUpdate(pos(6),Blocks.AIR.defaultBlockState());p.level().setBlockAndUpdate(new BlockPos(0,1,5),Blocks.WATER.defaultBlockState());});c.waitTicks(8);
|
||||
c.getInput().lookAt(new BlockPos(0,1,5));c.waitTicks(5);use(c);server.waitFor(s->player(s).level().getBlockState(new BlockPos(0,2,5)).is(Blocks.LILY_PAD),100);
|
||||
// Prestige/reset, creative and unbound vanilla players keep coherent defaults.
|
||||
server.runOnServer(s->{var p=player(s);setRanks(p,0,0);check(BuildReach.range(p)==3&&MiningReach.range(p)==3,"Reset immediately restores both starting reaches");p.setGameMode(GameType.CREATIVE);check(BuildReach.range(p)==p.blockInteractionRange()&&MiningReach.range(p)==p.blockInteractionRange(),"Creative retains native ranges");p.setGameMode(GameType.SURVIVAL);ProgressEffects.clear(p);check(BuildReach.range(p)==4.5&&MiningReach.range(p)==4.5,"Non-Sanctuary players keep vanilla ranges");setRanks(p,7,7);});
|
||||
c.waitFor(m->ProgressionClient.progress.mining()==7&&ProgressionClient.progress.building()==7,200);
|
||||
c.takeScreenshot("balance080-complete");System.out.println("BALANCE080_PASS: independent reach, native placement/mining, offhand, forged packet, group, locked vein, water, utility isolation, ranks and modifiers");
|
||||
}
|
||||
}
|
||||
private static void curve(ServerPlayer p) {
|
||||
double[] speed={.75,.85,.95,1,1.075,1.15,1.225,1.30},reach={3,3.5,4,4.5,4.75,5,5.25,5.5},buildReach={3,3.5,4,4.5,4.75,5,5.5,6};
|
||||
var extra=SanctuaryMod.id("test/balance080");var attribute=p.getAttribute(Attributes.BLOCK_BREAK_SPEED);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));setRanks(p,3,3);
|
||||
var state=Blocks.STONE.defaultBlockState();float baseline=state.getDestroyProgress(p,p.level(),new BlockPos(0,0,0));
|
||||
for(int rank=0;rank<=7;rank++) {setRanks(p,rank,rank);near(attribute.getValue(),speed[rank],"Mining speed rank "+rank);near(state.getDestroyProgress(p,p.level(),new BlockPos(0,0,0))/baseline,speed[rank],"Native block breaking progress rank "+rank);near(BuildReach.range(p),buildReach[rank],"Build reach rank "+rank);near(MiningReach.range(p),reach[rank],"Mining reach rank "+rank);}
|
||||
attribute.addTransientModifier(new AttributeModifier(extra,.5,AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL));setRanks(p,7,7);near(attribute.getValue(),1.95,"Mining composes with external boost");attribute.removeModifier(extra);
|
||||
var blockRange=p.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);blockRange.addTransientModifier(new AttributeModifier(extra,2,AttributeModifier.Operation.ADD_VALUE));near(BuildReach.range(p),8,"Construction preserves external range bonuses");near(MiningReach.range(p),7.5,"Mining preserves external range bonuses");blockRange.removeModifier(extra);
|
||||
near(p.blockInteractionRange(),4.5,"Base interactions remain native");near(p.entityInteractionRange(),3,"Entity interactions remain native");
|
||||
}
|
||||
private static void setup(ClientGameTestContext c,TestServerContext server,int mining,int building,int z,Item item,boolean offhand) {
|
||||
server.runOnServer(s->{var p=player(s);MiningManager.forget(p);BuildingManager.forget(p);setRanks(p,mining,building);p.teleportTo(.5,1,.75);
|
||||
for(int x=-1;x<=1;x++)for(int y=1;y<=3;y++)for(int zz=2;zz<=9;zz++)p.level().setBlockAndUpdate(new BlockPos(x,y,zz),Blocks.AIR.defaultBlockState());
|
||||
p.level().setBlockAndUpdate(pos(z),Blocks.STONE.defaultBlockState());
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,offhand?ItemStack.EMPTY:new ItemStack(item,item.getDefaultMaxStackSize()));p.setItemInHand(InteractionHand.OFF_HAND,offhand?new ItemStack(item,64):ItemStack.EMPTY);p.inventoryMenu.broadcastChanges();
|
||||
});c.waitFor(m->ProgressionClient.progress.mining()==mining&&ProgressionClient.progress.building()==building&&m.level.getBlockState(pos(z)).is(Blocks.STONE)&&(offhand?m.player.getOffhandItem():m.player.getMainHandItem()).is(item),200);c.waitTicks(8);c.getInput().lookAt(pos(z));c.waitTicks(5);
|
||||
}
|
||||
private static void use(ClientGameTestContext c){c.getInput().holdKey(o->o.keyUse);c.waitTicks(2);c.getInput().releaseKey(o->o.keyUse);}
|
||||
private static void setRanks(ServerPlayer p,int mining,int building){var r=ProgressRecord.INITIAL;var config=ProgressionService.session(p.level().getServer()).config;for(int i=0;i<mining;i++)r=r.buy("mining",r.cost("mining",config));for(int i=0;i<building;i++)r=r.buy("building",r.cost("building",config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");}
|
||||
private static BlockPos pos(int z){return new BlockPos(0,2,z);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void near(double actual,double expected,String reason){check(Math.abs(actual-expected)<.00001,reason+" actual="+actual+" expected="+expected);}
|
||||
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.sky.VortexClouds;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.client.CloudStatus;
|
||||
import net.minecraft.client.GraphicsPreset;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.level.GameType;
|
||||
|
||||
/** Native cloud passes on a new empty laboratory, plus the single connected/cached shape. */
|
||||
public final class Cloud077ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
for(int diameter:List.of(512,724,1024))geometry(diameter);
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Cloud077 "+UUID.randomUUID());s.setSeed("77");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_gametest:vortex")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1500);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(VortexClouds.settings(p.level())==null,"A flat generator never receives a vortex automatically");p.setGameMode(GameType.SPECTATOR);p.teleportTo(0,300,0);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
});c.waitTicks(12);
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();m.options.cloudRange().set(128);m.options.cloudStatus().set(CloudStatus.FANCY);m.options.fov().set(110);});c.getInput().pressKey(InputConstants.KEY_F1);c.getInput().lookAt(new BlockPos(0,0,0));c.waitTicks(8);
|
||||
c.takeScreenshot("cloud077-before");int before=c.computeOnClient(m->VortexCloudRenderer.draws());
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();ServerPlayNetworking.send(p,new VortexClouds.Settings(p.level().dimension().identifier(),0,0,724));});
|
||||
c.waitFor(m->VortexCloudRenderer.draws()>before,100);c.waitTicks(8);c.takeScreenshot("cloud077-fancy-top");c.runOnClient(m->System.out.println("CLOUD077_CAMERA pos="+m.gameRenderer.mainCamera().position()+" pitch="+m.gameRenderer.mainCamera().xRot()+" player="+m.player.position()+" range="+m.options.cloudRange().get()));
|
||||
int builds=c.computeOnClient(m->VortexCloudRenderer.builds());
|
||||
c.runOnClient(m->m.level.setTimeFromServer(3000));c.waitTicks(2);c.takeScreenshot("cloud077-rotated");
|
||||
c.runOnClient(m->m.options.cloudStatus().set(CloudStatus.FAST));c.waitTicks(6);c.takeScreenshot("cloud077-fast");
|
||||
int oit=c.computeOnClient(m->VortexCloudRenderer.oitDraws());c.runOnClient(m->{m.options.applyGraphicsPreset(GraphicsPreset.FABULOUS);m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.cloudRange().set(128);});c.waitTicks(12);c.takeScreenshot("cloud077-fabulous");check(c.computeOnClient(m->VortexCloudRenderer.oitDraws())>oit,"Fabulous actually uses the OIT cloud passes");
|
||||
check(c.computeOnClient(m->VortexCloudRenderer.builds())==builds,"Rotation and graphics mode keep cached geometry");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(int x=60;x<=140;x++)for(int z=-40;z<=40;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());p.teleportTo(100,-12,0);});c.waitTicks(12);c.getInput().lookAt(new BlockPos(100,1,0));c.waitTicks(4);c.takeScreenshot("cloud077-below");
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(100,1,0));c.waitTicks(12);c.getInput().lookAt(new BlockPos(100,1,100));c.waitTicks(4);c.takeScreenshot("cloud077-inside");
|
||||
c.runOnClient(m->m.options.cloudStatus().set(CloudStatus.OFF));c.waitTicks(5);int off=c.computeOnClient(m->VortexCloudRenderer.draws());c.waitTicks(8);check(c.computeOnClient(m->VortexCloudRenderer.draws())==off,"Clouds off stops both layers");
|
||||
c.runOnClient(m->m.options.cloudStatus().set(CloudStatus.FANCY));server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(20000,300,20000));c.waitTicks(15);int far=c.computeOnClient(m->VortexCloudRenderer.draws());c.waitTicks(8);check(c.computeOnClient(m->VortexCloudRenderer.draws())==far,"Disc does not tile across the world");
|
||||
check(c.computeOnClient(m->VortexCloudRenderer.builds())==builds,"Moving the camera never rebuilds the mesh");c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
System.out.println("CLOUD077_PASS: connected 512/724/1024 geometry, native fancy/fast/Fabulous, rotation, Y0 crossing, clouds off, finite extent and cache");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void geometry(int diameter){
|
||||
Set<Long> all=new HashSet<>();int radius=(int)Math.ceil(diameter/12.0);
|
||||
for(int x=-radius;x<radius;x++)for(int z=-radius;z<radius;z++)if(VortexCloudRenderer.occupied(x,z,diameter))all.add(key(x,z));
|
||||
check(all.size()>2000&&all.size()<25000,"Bounded geometry "+diameter);
|
||||
Set<Long> seen=new HashSet<>();var queue=new ArrayDeque<Long>();queue.add(all.iterator().next());
|
||||
while(!queue.isEmpty()){long id=queue.remove();if(!seen.add(id))continue;int x=(int)(id>>32),z=(int)id;for(int[] d:new int[][]{{1,0},{-1,0},{0,1},{0,-1}}){long next=key(x+d[0],z+d[1]);if(all.contains(next)&&!seen.contains(next))queue.add(next);}}
|
||||
check(seen.size()==all.size(),"Continuous spiral, no disconnected fragments: "+diameter+" "+seen.size()+"/"+all.size());
|
||||
check(!VortexCloudRenderer.occupied(0,0,diameter)&&!VortexCloudRenderer.occupied(radius,0,diameter),"Open eye and finite outer edge");
|
||||
System.out.println("CLOUD077_GEOMETRY "+diameter+" "+all.size()+" cells");
|
||||
}
|
||||
private static long key(int x,int z){return ((long)x<<32)|(z&0xffffffffL);}
|
||||
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.state.EnderDragonRenderState;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.model.monster.dragon.EnderDragonModel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native key events, client/server carry physics and the submitted dragon model's actual head direction. */
|
||||
public final class Familiar067ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Familiar067 "+UUID.randomUUID());s.setSeed("67");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;
|
||||
p.teleportTo(.5,1,.5);for(int x=-10;x<=10;x++)for(int z=-10;z<=20;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND));p.setItemInHand(InteractionHand.OFF_HAND,new ItemStack(Items.GOLD_INGOT));p.inventoryMenu.broadcastChanges();
|
||||
});c.waitTicks(12);
|
||||
c.runOnClient(m->check(orders().getDefaultKey().equals(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_H)),"H is the restored default"));
|
||||
c.getInput().holdKey(InputConstants.KEY_H);c.waitFor(m->m.gui.screen() instanceof FamiliarOrdersScreen,100);c.getInput().releaseKey(InputConstants.KEY_H);c.waitTicks(5);
|
||||
server.runOnServer(s->check(player(s).getMainHandItem().is(Items.DIAMOND)&&player(s).getOffhandItem().is(Items.GOLD_INGOT),"H orders must not swap hands"));
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.getInput().pressKey(InputConstants.KEY_F);c.waitTicks(8);c.runOnClient(m->check(m.gui.screen()==null,"F swaps hands without opening orders"));
|
||||
server.runOnServer(s->check(player(s).getMainHandItem().is(Items.GOLD_INGOT)&&player(s).getOffhandItem().is(Items.DIAMOND),"Native F swaps both hands with default H orders"));
|
||||
c.runOnClient(m->check(KeyMapping.get("sanctuary.companion.key").getDefaultKey().equals(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_G)),"Technique remains on G"));
|
||||
c.runOnClient(m->{orders().setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_K));KeyMapping.resetMapping();});
|
||||
c.getInput().pressKey(InputConstants.KEY_F);c.waitTicks(8);server.runOnServer(s->check(player(s).getMainHandItem().is(Items.DIAMOND)&&player(s).getOffhandItem().is(Items.GOLD_INGOT),"Native F swap works when orders use another key"));
|
||||
c.getInput().holdKey(InputConstants.KEY_K);c.waitFor(m->m.gui.screen() instanceof FamiliarOrdersScreen,100);c.getInput().releaseKey(InputConstants.KEY_K);c.getInput().pressKey(InputConstants.KEY_ESCAPE);
|
||||
c.runOnClient(m->{orders().setKey(orders().getDefaultKey());KeyMapping.resetMapping();orientation(m);});
|
||||
System.out.println("FAMILIAR067_KEYS_ORIENTATION_PASS: real H/F/K input, native hand swap restored, native dragon head faces travel in four directions");
|
||||
for(int size:new int[]{350,650,1500}){
|
||||
server.runOnServer(s->{var p=player(s);CarryService.detach(p);p.stopRiding();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);
|
||||
p.teleportTo(.5,1,.5);p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);p.setShiftKeyDown(false);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(size));p.inventoryMenu.broadcastChanges();});c.waitTicks(30);
|
||||
int id=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Equipped dragon spawns");pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));pet.setDeltaMovement(Vec3.ZERO);return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(id)!=null&&m.player.getMainHandItem().isEmpty(),100);c.getInput().holdKey(o->o.keyShift);c.waitTicks(4);click(c,id);c.waitTicks(5);c.getInput().releaseKey(o->o.keyShift);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);check(CarryService.carried(FamiliarService.active(p)),"Dragon is carried via native sneak-click: "+size);p.teleportTo(.5,8,.5);p.setDeltaMovement(0,-.5,0);p.setOnGround(false);p.fallDistance=12;});
|
||||
c.waitTicks(20);
|
||||
c.runOnClient(m->{check(CarryGlide.active(m.player),"Local player glides with dragon: "+size);check(m.player.getDeltaMovement().y>=-CarryGlide.DESCENT-.01,"Client descent bounded: "+m.player.getDeltaMovement());check(m.player.fallDistance<.2,"Accumulated fall distance cleared on client");});
|
||||
server.runOnServer(s->{check(CarryGlide.supported(player(s)),"Server agrees on glide support");check(player(s).fallDistance<.2,"Server fall distance cleared");});
|
||||
if(size==1500){
|
||||
double base=server.computeOnServer(s->player(s).getY());c.getInput().holdKey(o->o.keyJump);c.waitTicks(100);
|
||||
server.runOnServer(s->{check(player(s).getY()>base+8,"Holding jump flies upward with large dragon: "+player(s).getY()+" from "+base);check(player(s).connection.isAcceptingMessages(),"Sustained supported flight is not rejected as floating");});
|
||||
c.getInput().releaseKey(o->o.keyJump);c.waitTicks(25);double top=server.computeOnServer(s->player(s).getY());c.waitTicks(15);server.runOnServer(s->check(player(s).getY()<top-.5,"Releasing jump resumes a gentle glide"));
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.THIRD_PERSON_BACK);});c.waitTicks(3);c.takeScreenshot("familiar067-large-dragon-flight");
|
||||
server.runOnServer(s->{var p=player(s);int y=(int)Math.ceil(FamiliarService.active(p).getBoundingBox().maxY)+1;for(int x=-2;x<=2;x++)for(int z=-2;z<=2;z++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.STONE.defaultBlockState());});
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(40);c.getInput().releaseKey(o->o.keyJump);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Lift cannot push the player through a ceiling");});
|
||||
}else{
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(6);c.getInput().releaseKey(o->o.keyJump);
|
||||
c.runOnClient(m->check(m.player.getDeltaMovement().y<=0,"Small dragon glides but cannot lift"));
|
||||
}
|
||||
server.runOnServer(s->{var p=player(s);CarryService.detach(p);p.teleportTo(.5,1,.5);});c.waitTicks(5);
|
||||
System.out.println("FAMILIAR067_CARRY_PASS size="+size);
|
||||
}
|
||||
// The existing colossal riding path must still work for the dragon itself.
|
||||
server.runOnServer(s->{var p=player(s);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(3500));p.teleportTo(5.5,1,.5);});c.waitTicks(30);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);pet.setPos(p.position().add(0,1,2));p.setShiftKeyDown(false);check(CarryService.interact(p,pet),"Colossal dragon mounts instead of being carried");});c.waitTicks(5);
|
||||
double base=server.computeOnServer(s->FamiliarService.active(player(s)).getY());c.getInput().holdKey(o->o.keyJump);c.waitTicks(20);c.getInput().releaseKey(o->o.keyJump);
|
||||
server.runOnServer(s->check(FamiliarService.active(player(s)).getY()>base+2,"Colossal dragon flies with native jump input"));
|
||||
System.out.println("FAMILIAR067_PASS: native shortcuts, model orientation, tiny/small glide, large carried flight and colossal riding");
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyShift);c.getInput().releaseKey(o->o.keyJump);}
|
||||
}
|
||||
private static void orientation(Minecraft mc){
|
||||
for(int yaw:new int[]{0,90,180,270}){
|
||||
var pet=FamiliarService.TYPE.create(mc.level,EntitySpawnReason.COMMAND);pet.setId(300000+yaw);pet.bind(null,egg(1000));pet.setPos(mc.player.position().add(0,5,8));pet.setYRot(yaw);pet.yRotO=yaw;pet.yBodyRot=pet.yBodyRotO=pet.yHeadRot=pet.yHeadRotO=yaw;
|
||||
var renderer=(FamiliarRenderer)mc.getEntityRenderDispatcher().getRenderer(pet);var state=renderer.createRenderState(pet,1);var checked=new AtomicBoolean();
|
||||
SubmitNodeCollector collector=(SubmitNodeCollector)Proxy.newProxyInstance(SubmitNodeCollector.class.getClassLoader(),new Class<?>[]{SubmitNodeCollector.class},(proxy,method,args)->{
|
||||
if(method.getName().equals("order"))return proxy;
|
||||
if(method.getName().equals("submitModel")&&args[0] instanceof EnderDragonModel dragon&&checked.compareAndSet(false,true)){
|
||||
dragon.setupAnim((EnderDragonRenderState)args[1]);var pose=new PoseStack();pose.mulPose(((PoseStack)args[2]).last().pose());dragon.root().translateAndRotate(pose);
|
||||
var head=dragon.root().getChild("head");var body=dragon.root().getChild("body");
|
||||
var a=pose.last().pose().transformPosition(new org.joml.Vector3f(head.x/16,head.y/16,head.z/16));var b=pose.last().pose().transformPosition(new org.joml.Vector3f(body.x/16,body.y/16,body.z/16));
|
||||
var forward=Vec3.directionFromRotation(0,yaw);double dot=(a.x-b.x)*forward.x+(a.z-b.z)*forward.z;
|
||||
check(dot>0,"Submitted dragon head points forwards at yaw "+yaw+", dot="+dot);
|
||||
}return null;
|
||||
});
|
||||
renderer.submit(state,new PoseStack(),collector,new CameraRenderState());check(checked.get(),"Actual native dragon model was submitted");
|
||||
}
|
||||
}
|
||||
private static ItemStack egg(int size){var stack=new ItemStack(Items.ENDER_DRAGON_SPAWN_EGG);var tag=new CompoundTag();tag.putInt("schema",1);tag.putInt("per_mille",size);CustomData.update(DataComponents.CUSTOM_DATA,stack,t->t.put(FamiliarSize.KEY,tag));return stack;}
|
||||
private static KeyMapping orders(){return KeyMapping.get("sanctuary.familiar.orders_key");}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void click(ClientGameTestContext c,int id){c.runOnClient(m->{var e=m.level.getEntity(id);m.gameMode.interact(m.player,e,new EntityHitResult(e,e.getEyePosition()),InteractionHand.MAIN_HAND);});}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native server AI and mount movement in an isolated client-created flat world. */
|
||||
public final class Familiar071ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave save;
|
||||
var savedId=new AtomicReference<UUID>();var savedPersonality=new AtomicReference<FamiliarPersonality>();
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Familiar071 "+UUID.randomUUID());s.setSeed("71");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
save=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=16;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
distributions(s);
|
||||
});
|
||||
for(var personality:FamiliarPersonality.values()){
|
||||
var victim=new AtomicReference<Mob>();
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.WOLF_SPAWN_EGG,personality,1000);FamiliarService.active(p).setPos(.5,1,1.5);victim.set(enemy(p,7.5,1,1.5));});c.waitTicks(25);
|
||||
server.runOnServer(s->{var p=player(s);var target=victim.get();check((FamiliarBattle.target(p)!=null)==(personality==FamiliarPersonality.AGGRESSIVE),"Proximity reaction: "+personality);
|
||||
FamiliarBattle.cancel(p);FamiliarBattle.state(p).automaticAfter=0;
|
||||
FamiliarBattle.damageEvent(target,p.damageSources().playerAttack(p),1);
|
||||
check((FamiliarBattle.target(p)!=null)==(personality==FamiliarPersonality.AGGRESSIVE),"Owner attack reaction: "+personality);
|
||||
check(FamiliarBattle.fighting(p),"Combat lock remains active even if personality refuses to attack: "+personality);
|
||||
FamiliarBattle.cancel(p);FamiliarBattle.damageEvent(p,p.damageSources().mobAttack(target),1);
|
||||
check((FamiliarBattle.target(p)!=null)==(personality!=FamiliarPersonality.PACIFIST),"Owner hurt reaction: "+personality);
|
||||
FamiliarBattle.cancel(p);FamiliarService.active(p).hurtServer(p.level(),p.damageSources().mobAttack(target),1);
|
||||
check((FamiliarBattle.target(p)!=null)==(personality!=FamiliarPersonality.PACIFIST),"Pet hurt reaction: "+personality);
|
||||
if(personality==FamiliarPersonality.PACIFIST)check(FamiliarBattle.state(p).evadeUntil>p.tickCount,"Pacifist chooses retreat");
|
||||
FamiliarBattle.engage(p,target,false);check(FamiliarBattle.state(p).explicitTarget,"Manual attack is explicit: "+personality);
|
||||
});c.waitTicks(100);
|
||||
server.runOnServer(s->{var p=player(s);check(victim.get().getHealth()<100,"Actual navigation and damage on explicit order: "+personality+" distance="+FamiliarService.active(p).distanceTo(victim.get()));
|
||||
System.out.println("FAMILIAR071_PERSONALITY_PASS "+personality+" targetHealth="+victim.get().getHealth()+" pursuit="+personality.pursuitSpeed());victim.get().discard();FamiliarBattle.cancel(p);
|
||||
});
|
||||
}
|
||||
// Support species must really attack, rather than repeatedly trying a shield on an enemy.
|
||||
var villagerTarget=new AtomicReference<Mob>();
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.VILLAGER_SPAWN_EGG,FamiliarPersonality.AGGRESSIVE,1000);FamiliarService.active(p).setPos(.5,1,1.5);villagerTarget.set(enemy(p,6.5,1,1.5));});c.waitTicks(120);
|
||||
server.runOnServer(s->{var p=player(s);check(villagerTarget.get().getHealth()<100,"Aggressive villager initiates and really strikes without shield supply");check(!FamiliarBattle.state(p).explicitTarget,"Automatic technique remains automatic");
|
||||
var second=enemy(p,1.5,1,.5);FamiliarBattle.engage(p,villagerTarget.get(),false);FamiliarBattle.automatic(p,second,FamiliarPersonality.Trigger.OWNER_HURT);check(FamiliarBattle.target(p)==villagerTarget.get(),"Explicit target is not overwritten by another attacker");second.discard();villagerTarget.get().discard();FamiliarBattle.cancel(p);
|
||||
savedId.set(FamiliarBattle.bond(p).id());savedPersonality.set(FamiliarPersonality.of(FamiliarBattle.bond(p)));
|
||||
var egg=FamiliarBattle.egg(p);egg.set(DataComponents.CUSTOM_NAME,Component.literal("Mistral"));FamiliarBattle.update(p,FamiliarBattle.bond(p).charge(100,FamiliarBattle.profile(p).health()));FamiliarBattle.flush(s);
|
||||
});c.waitTicks(25);
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());FamiliarClient.open(null);});c.waitTicks(5);c.takeScreenshot("familiar071-personality-"+locale);c.runOnClient(m->m.gui.setScreen(null));
|
||||
}
|
||||
System.out.println("FAMILIAR071_VILLAGER_PASS: automatic melee, manual target priority and FR/EN character sheet");
|
||||
// Native input drives two climb-capable mounts, with a ceiling and no open-air flight.
|
||||
for(var item:List.of(Items.SPIDER_SPAWN_EGG,Items.CAVE_SPIDER_SPAWN_EGG)){
|
||||
server.runOnServer(s->{var p=player(s);equip(p,item,FamiliarPersonality.PACIFIST,3500);p.teleportTo(.5,1,.5);
|
||||
for(int x=-3;x<=3;x++)for(int y=1;y<=12;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,4),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-3;x<=3;x++)for(int z=-1;z<=4;z++)p.level().setBlockAndUpdate(new BlockPos(x,10,z),Blocks.STONE.defaultBlockState());
|
||||
});c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);pet.setPos(.5,1,1.5);pet.setDeltaMovement(Vec3.ZERO);p.setShiftKeyDown(false);check(CarryService.interact(p,pet)&&p.getVehicle()==pet,"Mount colossal spider");});c.waitTicks(5);
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});c.getInput().holdKey(o->o.keyUp);c.waitTicks(70);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>3,"Spider climbs real wall: "+item+" pos="+pet.position());check(p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Rider cannot enter ceiling");check(p.level().noBlockCollision(pet,pet.getBoundingBox().deflate(.02)),"Mount cannot enter wall");System.out.println("FAMILIAR071_CLIMB_PASS "+item+" y="+pet.getY());});
|
||||
c.takeScreenshot("familiar071-climb-"+item);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);pet.setPos(8.5,1,1.5);pet.setDeltaMovement(Vec3.ZERO);});c.waitTicks(5);c.getInput().holdKey(o->o.keyUp);c.waitTicks(25);c.getInput().releaseKey(o->o.keyUp);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarService.active(p).getY()<1.5,"Climbing never lifts in open air");p.stopRiding();});
|
||||
}
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);equip(p,Items.STRIDER_SPAWN_EGG,FamiliarPersonality.PACIFIST,3500);
|
||||
for(int x=-14;x<=-4;x++)for(int z=-4;z<=12;z++)for(int y=1;y<=3;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.LAVA.defaultBlockState());
|
||||
var pet=FamiliarService.active(p);pet.setPos(-9.5,3.5,.5);pet.setDeltaMovement(Vec3.ZERO);p.teleportTo(-9.5,5.5,.5);p.setShiftKeyDown(false);check(CarryService.interact(p,pet)&&p.getVehicle()==pet,"Mount colossal Strider");p.getAbilities().invulnerable=false;
|
||||
});c.waitTicks(20);c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});c.getInput().holdKey(o->o.keyUp);c.waitTicks(20);c.getInput().releaseKey(o->o.keyUp);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>=3.4,"Strider stays on lava surface: "+pet.position());check(pet.getZ()>2,"Strider actually moves across lava");check(!p.isOnFire()&&!p.isInLava(),"Rider stays safely above lava without fire immunity buff");check(!p.hasEffect(net.minecraft.world.effect.MobEffects.FIRE_RESISTANCE),"No invented rider fire resistance");p.getAbilities().invulnerable=true;p.stopRiding();p.teleportTo(.5,1,.5);
|
||||
System.out.println("FAMILIAR071_STRIDER_PASS: native lava support and real directional input");
|
||||
});
|
||||
for(var item:List.of(Items.NAUTILUS_SPAWN_EGG,Items.ZOMBIE_NAUTILUS_SPAWN_EGG))for(int size:new int[]{2500,6000}){
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.removeEffect(net.minecraft.world.effect.MobEffects.BREATH_OF_THE_NAUTILUS);p.teleportTo(10.5,1,10.5);equip(p,item,FamiliarPersonality.PACIFIST,size);
|
||||
for(int x=6;x<=15;x++)for(int z=6;z<=15;z++)for(int y=1;y<=12;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
check(!p.hasEffect(net.minecraft.world.effect.MobEffects.BREATH_OF_THE_NAUTILUS),"Equipping alone does not grant mount breath");
|
||||
var pet=FamiliarService.active(p);pet.setPos(10.5,2,10.5);pet.setDeltaMovement(Vec3.ZERO);p.setShiftKeyDown(false);check(CarryService.interact(p,pet)&&p.getVehicle()==pet,"Mount Nautilus "+size);
|
||||
check(p.hasEffect(net.minecraft.world.effect.MobEffects.BREATH_OF_THE_NAUTILUS),"Native effect applies immediately on mount");
|
||||
});c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=player(s);check(p.isUnderWater(),"Rider is actually submerged");p.getAbilities().invulnerable=false;p.setAirSupply(40);});c.waitTicks(90);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getVehicle()==FamiliarService.active(p),"Underwater rider stays mounted");check(p.getAirSupply()==40,"Native breath freezes air across multiple refreshes: "+item+" size="+size+" air="+p.getAirSupply());
|
||||
check(!p.hasEffect(net.minecraft.world.effect.MobEffects.WATER_BREATHING),"Uses Breath of the Nautilus, not regenerating Water Breathing");p.stopRiding();p.teleportTo(10.5,2,10.5);
|
||||
});c.waitTicks(72);
|
||||
server.runOnServer(s->{var p=player(s);check(!p.hasEffect(net.minecraft.world.effect.MobEffects.BREATH_OF_THE_NAUTILUS),"Effect expires naturally after dismount");check(p.getAirSupply()<40,"Air consumption resumes after dismount");p.getAbilities().invulnerable=true;p.setAirSupply(p.getMaxAirSupply());p.teleportTo(.5,1,.5);
|
||||
System.out.println("FAMILIAR071_NAUTILUS_PASS "+item+" size="+size+" native frozen air and dismount expiry");
|
||||
});
|
||||
}
|
||||
server.runOnServer(s->{var p=player(s);equip(p,Items.VILLAGER_SPAWN_EGG,FamiliarPersonality.AGGRESSIVE,1000);savedId.set(FamiliarBattle.bond(p).id());savedPersonality.set(FamiliarPersonality.of(FamiliarBattle.bond(p)));FamiliarBattle.flush(s);s.saveEverything(true,true,true);});
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyUp);}
|
||||
try(var world=save.open()){
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var b=FamiliarBattle.bond(player(s));check(savedId.get().equals(b.id())&&savedPersonality.get()==FamiliarPersonality.of(b),"Individual and personality survive a real world reopen");});
|
||||
}
|
||||
System.out.println("FAMILIAR071_PASS: four automatic response policies, explicit damage, villager exceptions, 88 species distributions, native spider/cave-spider climbing, Nautilus/Zombie Nautilus rider breath at both size limits, Strider lava and persistence");
|
||||
}
|
||||
private static void distributions(MinecraftServer s){
|
||||
var random=new Random(71);var profiles=FamiliarBattle.world(s).catalog.profiles();check(profiles.size()==88,"88 profiles remain loaded");
|
||||
for(var profile:profiles){var counts=new EnumMap<FamiliarPersonality,Integer>(FamiliarPersonality.class);
|
||||
for(int i=0;i<4000;i++){var id=new UUID(random.nextLong(),random.nextLong());var p=FamiliarPersonality.of(id,profile.id());counts.merge(p,1,Integer::sum);
|
||||
var bond=FamiliarBond.create(id,profile,0,"test",UUID.randomUUID());check(p==FamiliarPersonality.of(bond.charge(100,profile.health()).remember("journey","test")),"Levels and memories preserve personality");
|
||||
}
|
||||
check(counts.size()==4,"Every species allows exceptions: "+profile.id());
|
||||
if(profile.id().equals("sanctuary:villager"))check(counts.get(FamiliarPersonality.PACIFIST)>1800,"Villagers tend to be pacifist");
|
||||
if(profile.id().equals("sanctuary:vindicator"))check(counts.get(FamiliarPersonality.AGGRESSIVE)>2500,"Vindicators tend to be aggressive");
|
||||
}
|
||||
System.out.println("FAMILIAR071_DISTRIBUTIONS_PASS: 352000 deterministic identities across 88 species");
|
||||
}
|
||||
private static void equip(ServerPlayer p,Item item,FamiliarPersonality personality,int size){
|
||||
p.stopRiding();CarryService.detach(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.removeAttached(FamiliarBattle.COMBAT_LOCK);p.removeAttached(FamiliarBattle.LAST_FAMILIAR);
|
||||
var egg=new ItemStack(item);var scale=new CompoundTag();scale.putInt("schema",1);scale.putInt("per_mille",size);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,scale));
|
||||
var profile=FamiliarBattle.world(p.level().getServer()).catalog.forEgg(egg);UUID id;do{id=UUID.randomUUID();}while(FamiliarPersonality.of(id,profile.id())!=personality);
|
||||
FamiliarBattle.world(p.level().getServer()).store.put(FamiliarBond.create(id,profile,0,"test",p.getUUID()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,FamiliarIdentity.assign(egg,id));
|
||||
CompanionService.tick(p);p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);check(FamiliarService.active(p)!=null,"Familiar spawned: "+item);p.inventoryMenu.broadcastChanges();
|
||||
}
|
||||
private static Mob enemy(ServerPlayer p,double x,double y,double z){var e=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);e.setNoAi(true);e.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);e.setHealth(100);e.setPos(x,y,z);p.level().addFreshEntity(e);return e;}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Fourneau085ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos ORIGIN=new BlockPos(15,80,15);
|
||||
private static MultiblockData.Machine oven;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Fourneau085 "+UUID.randomUUID());s.setSeed("85");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();
|
||||
p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
for(int x=8;x<25;x++)for(int z=8;z<25;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=0;x<3;x++)for(int y=0;y<3;y++)for(int z=0;z<3;z++)l.setBlockAndUpdate(ORIGIN.offset(x,y,z),Blocks.FURNACE.defaultBlockState());
|
||||
((FurnaceBlockEntity)l.getBlockEntity(ORIGIN)).setItem(0,new ItemStack(Items.DIAMOND,17));
|
||||
p.teleportTo(22.5,83,9.5);
|
||||
});
|
||||
c.waitFor(m->m.level.getBlockState(ORIGIN).is(Blocks.FURNACE)&&m.player.getY()>80,200);
|
||||
c.runOnClient(m->{if(!m.gui.hud.isHidden())m.gui.hud.toggle();m.options.bobView().set(false);m.options.fov().set(60);m.getWindow().setWindowed(1280,800);});
|
||||
c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(15);c.takeScreenshot("fourneau085-before");
|
||||
for(var facing:Direction.Plane.HORIZONTAL){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();
|
||||
if(oven!=null)Multiblocks.dissolve(l,oven);
|
||||
oven=new MultiblockData.Machine(UUID.randomUUID(),ORIGIN,true,facing);
|
||||
check(Multiblocks.assemble(p,oven),"Assembly for "+facing);
|
||||
var right=facing.getCounterClockWise();
|
||||
p.teleportTo(16.5+facing.getStepX()*7+right.getStepX()*5,83,16.5+facing.getStepZ()*7+right.getStepZ()*5);
|
||||
});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null&&FourneauModels.modelAt(ORIGIN).getPath().contains(facing.getName()),200);
|
||||
c.waitTicks(8);c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(15);
|
||||
c.runOnClient(m->{for(int x=0;x<3;x++)for(int y=0;y<3;y++)for(int z=0;z<3;z++)check(FourneauModels.modelAt(ORIGIN.offset(x,y,z))!=null,"All 27 components synchronized across four chunks");});
|
||||
c.takeScreenshot("fourneau085-"+facing.getName());
|
||||
}
|
||||
// Resource reload must retain the synchronized appearance and rebuild native models.
|
||||
var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(10);
|
||||
c.takeScreenshot("fourneau085-reload");
|
||||
c.runOnClient(m->{var before=FourneauModels.modelAt(ORIGIN);
|
||||
FourneauModels.apply(new FourneauAppearance.Update(net.minecraft.resources.Identifier.withDefaultNamespace("the_nether"),net.minecraft.world.level.ChunkPos.containing(ORIGIN),ORIGIN,Direction.NORTH,FourneauAppearance.Action.CLEAR_CHUNK));
|
||||
check(before.equals(FourneauModels.modelAt(ORIGIN)),"Other dimension cannot erase the current facade");});
|
||||
server.runOnServer(s->{var l=s.getPlayerList().getPlayers().getFirst().level();var furnace=(FurnaceBlockEntity)l.getBlockEntity(ORIGIN.offset(1,0,0));
|
||||
furnace.setItem(0,new ItemStack(Items.RAW_IRON,64));furnace.setItem(1,new ItemStack(Items.COAL,64));});
|
||||
c.waitFor(m->m.level.getBlockState(ORIGIN.offset(1,0,0)).getValue(AbstractFurnaceBlock.LIT),200);c.waitTicks(10);
|
||||
c.runOnClient(m->check(FourneauModels.modelAt(ORIGIN)!=null,"Lit updates retain assembled model"));c.takeScreenshot("fourneau085-lit");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();Multiblocks.dissolve(l,oven);
|
||||
check(((FurnaceBlockEntity)l.getBlockEntity(ORIGIN)).getItem(0).getCount()==17,"Dissociation retains original furnace contents");});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null,200);c.waitTicks(15);c.takeScreenshot("fourneau085-separated");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(Multiblocks.assemble(p,oven),"Reassemble");});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null,200);
|
||||
// Real chunk unload/re-entry exercises native PlayerChunkSender synchronization.
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(400.5,83,400.5));
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null,500);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(22.5,83,9.5));
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null&&m.level.getBlockState(ORIGIN).is(Blocks.FURNACE),500);
|
||||
c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(15);c.takeScreenshot("fourneau085-return");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.level().destroyBlock(ORIGIN.offset(2,2,2),true);});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null,200);c.waitTicks(10);c.takeScreenshot("fourneau085-broken");
|
||||
System.out.println("FOURNEAU085_PASS: four orientations, four chunks, native models, resource reload, unload/re-entry, stock preservation, dismantle and break");
|
||||
}
|
||||
}
|
||||
private static net.minecraft.client.input.KeyEvent enter(){return new net.minecraft.client.input.KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Fut086ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos ORIGIN=new BlockPos(15,84,15),OVEN=new BlockPos(23,84,15);
|
||||
private static MultiblockData.Machine cask,oven;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Fut086 "+UUID.randomUUID());s.setSeed("86");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
|
||||
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();
|
||||
p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
for(int x=8;x<31;x++)for(int z=8;z<25;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
cask=new MultiblockData.Machine(UUID.randomUUID(),ORIGIN,false,Direction.NORTH);
|
||||
oven=new MultiblockData.Machine(UUID.randomUUID(),OVEN,true,Direction.NORTH);
|
||||
for(var pos:cask.members())l.setBlockAndUpdate(pos,Blocks.BARREL.defaultBlockState());
|
||||
for(var pos:oven.members())l.setBlockAndUpdate(pos,Blocks.FURNACE.defaultBlockState());
|
||||
var stock=new MultiblockContainer(l,cask);stock.setItem(0,new ItemStack(Items.DIAMOND,17));stock.setItem(728,new ItemStack(Items.EMERALD,23));
|
||||
p.teleportTo(22.5,87,9.5);
|
||||
});
|
||||
c.waitFor(m->m.level.getBlockState(ORIGIN).is(Blocks.BARREL)&&m.player.getY()>84,200);
|
||||
c.runOnClient(m->{if(!m.gui.hud.isHidden())m.gui.hud.toggle();m.options.bobView().set(false);m.options.fov().set(60);m.getWindow().setWindowed(1280,800);});
|
||||
c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(15);c.takeScreenshot("fut086-before");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(Multiblocks.assemble(p,cask),"Cask assembles");check(Multiblocks.assemble(p,oven),"Furnace still assembles");});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null&&FourneauModels.modelAt(OVEN)!=null,200);c.waitTicks(15);
|
||||
c.runOnClient(m->{check(FourneauModels.modelAt(ORIGIN).getPath().startsWith("block/fut/"),"Cask packet selects its own textures");check(FourneauModels.modelAt(OVEN).getPath().startsWith("block/fourneau/"),"Furnace keeps its original textures");});
|
||||
c.takeScreenshot("fut086-top-side");
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(22.5,79,9.5));
|
||||
c.waitFor(m->m.player.getY()<81,200);c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(12);c.takeScreenshot("fut086-bottom-side");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(16.5,84,12.5);Multiblocks.open(p,cask,ORIGIN,0,"",0);});
|
||||
c.waitFor(m->m.gui.screen() instanceof MultiblockScreen,200);c.waitTicks(5);c.takeScreenshot("fut086-inventory");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var menu=(MultiblockMenu)p.containerMenu;
|
||||
check(menu.getSlot(0).remove(7).getCount()==7,"Shared inventory remains accessible");
|
||||
var stock=new MultiblockContainer(p.level(),cask);check(stock.getContainerSize()==729&&stock.getItem(0).getCount()==10&&stock.getItem(728).getCount()==23,"Native stock spans all 729 cells");
|
||||
p.closeContainer();p.teleportTo(22.5,87,9.5);});
|
||||
c.waitFor(m->m.gui.screen()==null&&m.player.getY()>84,200);c.getInput().lookAt(ORIGIN.offset(1,1,1));
|
||||
var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(10);c.takeScreenshot("fut086-reload");
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(400.5,87,400.5));
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null&&FourneauModels.modelAt(OVEN)==null,500);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(22.5,87,9.5));
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null&&FourneauModels.modelAt(OVEN)!=null&&m.level.getBlockState(ORIGIN).is(Blocks.BARREL),500);
|
||||
c.runOnClient(m->{for(var pos:cask.members())check(FourneauModels.modelAt(pos)!=null,"All 27 cask tiles return across four chunks");});
|
||||
c.getInput().lookAt(ORIGIN.offset(1,1,1));c.waitTicks(12);c.takeScreenshot("fut086-return");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();Multiblocks.dissolve(p.level(),cask);
|
||||
var stock=new MultiblockContainer(p.level(),cask);check(stock.getItem(0).getCount()==10&&stock.getItem(728).getCount()==23,"Dismantle preserves real stock");});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null,200);c.waitTicks(12);c.takeScreenshot("fut086-separated");
|
||||
c.runOnClient(m->check(FourneauModels.modelAt(OVEN)!=null,"Dismantling cask does not erase neighboring oven facade"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(Multiblocks.assemble(p,cask),"Reassemble");});
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)!=null,200);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().level().destroyBlock(ORIGIN.offset(2,2,2),true));
|
||||
c.waitFor(m->FourneauModels.modelAt(ORIGIN)==null,200);c.runOnClient(m->check(FourneauModels.modelAt(OVEN)!=null,"Break stays scoped to its machine"));
|
||||
System.out.println("FUT086_PASS: complete top/side/bottom, mixed furnace and cask sync, resource reload, native chunk re-entry, 729-slot inventory, dismantle and break");
|
||||
}
|
||||
}
|
||||
private static net.minecraft.client.input.KeyEvent enter(){return new net.minecraft.client.input.KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import jdk.jfr.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Observation only: fixed seed, full native world creation, same JVM and fresh development saves. */
|
||||
public final class Loading063ClientAudit implements FabricClientGameTest {
|
||||
private static volatile long origin;
|
||||
private static volatile String scenario="menu";
|
||||
private static final List<String> EVENTS=new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
private static void mark(String event){String row=scenario+"\t"+((System.nanoTime()-origin)/1_000_000)+"\t"+event;EVENTS.add(row);System.out.println("LOADING063\t"+row);}
|
||||
public void runTest(ClientGameTestContext c){
|
||||
origin=System.nanoTime();mark("test starts; JVM max heap="+Runtime.getRuntime().maxMemory()+"; processors="+Runtime.getRuntime().availableProcessors()+"; launch="+ProcessHandle.current().info().startInstant().orElse(null));
|
||||
var observing=new AtomicBoolean(true);var last=new AtomicReference<>("");var hello=new AtomicBoolean();var introFinished=new AtomicBoolean();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(!observing.get())return;String screen=m.gui.screen()==null?"world":m.gui.screen().getClass().getSimpleName();if(!screen.equals(last.getAndSet(screen)))mark("screen "+screen);
|
||||
if(m.gui.screen() instanceof IntroScreen intro&&intro.isFinished()&&introFinished.compareAndSet(false,true))mark("intro finished; waiting under flash");
|
||||
if(m.gui.screen() instanceof HelloWorldScreen s&&hello.compareAndSet(false,true)){
|
||||
mark("hello ready");IntroClient.setQuickTestIntro(false);Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}
|
||||
});
|
||||
ServerLifecycleEvents.SERVER_STARTING.register(s->{if(observing.get())mark("server starting; initialized="+s.getWorldData().overworldData().isInitialized());});
|
||||
ServerLevelEvents.LOAD.register((s,l)->{if(observing.get())mark("level loaded "+l.dimension().identifier());});
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(s->{if(observing.get())mark("server started");});
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;});
|
||||
TestWorldSave island=null;
|
||||
try{
|
||||
Files.createDirectories(Path.of("loading063"));
|
||||
for(String preset:List.of("sanctuary:sanctuary","minecraft:flat","minecraft:normal")){
|
||||
scenario=preset.replace(':','-');hello.set(false);introFinished.set(false);last.set("");origin=System.nanoTime();mark("create requested; seed=42; structures=true; render=8; simulation=8");
|
||||
try(var recording=new Recording(Configuration.getConfiguration("profile"))){recording.start();
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Loading063 "+UUID.randomUUID());s.setSeed("42");s.setAllowCommands(true);s.setGenerateStructures(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals(preset)).findFirst().orElseThrow());}).create()){
|
||||
if(preset.startsWith("sanctuary:"))island=world.getWorldSave();
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),3000);mark("playable");
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;mark("mode="+(fr.koka.sanctuary.progression.ProgressionService.session(s)!=null)+"; generator="+s.overworld().getChunkSource().getGenerator().getClass().getSimpleName());});
|
||||
c.waitTicks(40);c.takeScreenshot("loading063-"+scenario);
|
||||
}recording.stop();recording.dump(Path.of("loading063",scenario+".jfr"));
|
||||
}
|
||||
if(preset.startsWith("sanctuary:")){
|
||||
scenario="sanctuary-reopen";hello.set(false);introFinished.set(false);last.set("");origin=System.nanoTime();mark("open requested");
|
||||
try(var recording=new Recording(Configuration.getConfiguration("profile"))){recording.start();
|
||||
try(var world=island.open()){c.waitFor(m->m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),3000);mark("playable");c.waitTicks(20);}
|
||||
recording.stop();recording.dump(Path.of("loading063",scenario+".jfr"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Files.write(Path.of("loading063","timings.tsv"),EVENTS);System.out.println("LOADING063_AUDIT_PASS");
|
||||
}catch(Exception e){throw new AssertionError(e);}finally{observing.set(false);}
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.loading.*;
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.GenericMessageScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class Loading064ClientChecks implements FabricClientGameTest {
|
||||
private record Fixture(int value) {}
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
cacheFailures();
|
||||
var observing=new AtomicBoolean(true);var lastHello=new AtomicReference<HelloWorldScreen>();
|
||||
var phases=new java.util.HashSet<String>();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(!observing.get())return;
|
||||
if(m.gui.screen() instanceof HelloWorldScreen s&&lastHello.getAndSet(s)!=s){
|
||||
IntroClient.setQuickTestIntro(false);Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}
|
||||
});
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;});
|
||||
var baseline=new AtomicReference<Evidence>();var directory=new AtomicReference<Path>();TestWorldSave save;
|
||||
long began=System.nanoTime();
|
||||
try {
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Loading064 "+UUID.randomUUID());s.setSeed("42");s.setAllowCommands(true);s.setGenerateStructures(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary:sanctuary")).findFirst().orElseThrow());}).create()) {
|
||||
save=world.getWorldSave();
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),3000);
|
||||
System.out.println("LOADING064_COLD_PLAYABLE_MS="+(System.nanoTime()-began)/1_000_000);
|
||||
world.getServer().runOnServer(s->{var g=(SanctuaryChunkGenerator)s.overworld().getChunkSource().getGenerator();
|
||||
check(g.planCache().writes()>0,"Cold world saved derived plans");directory.set(g.planCache().directory());baseline.set(evidence(g));
|
||||
s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true;
|
||||
});
|
||||
phases.addAll(PreloadProgress.observedPhases());
|
||||
c.takeScreenshot("loading064-arrival");
|
||||
c.runOnClient(m->{PreloadProgress.begin(m.getSingleplayerServer());PreloadProgress.stage(m.getSingleplayerServer(),"water");m.gui.setScreen(new GenericMessageScreen(Component.translatable("menu.preparingSpawn")));});
|
||||
c.waitTicks(3);c.takeScreenshot("loading064-preload-en");
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());PreloadProgress.stage(m.getSingleplayerServer(),"heritage");});
|
||||
c.waitTicks(3);c.takeScreenshot("loading064-preload-fr");
|
||||
c.runOnClient(m->{m.gui.setScreen(null);PreloadProgress.clear(m.getSingleplayerServer());});
|
||||
}
|
||||
for(int pass=0;pass<2;pass++) {
|
||||
if(pass==1)try(var files=Files.list(directory.get())) {
|
||||
Path core=files.filter(p->p.getFileName().toString().startsWith("core-")).findFirst().orElseThrow();Files.writeString(core,"interrupted cache write");
|
||||
}
|
||||
began=System.nanoTime();
|
||||
try(var world=save.open()) {
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),3000);
|
||||
System.out.println("LOADING064_"+(pass==0?"WARM":"RECOVERED")+"_PLAYABLE_MS="+(System.nanoTime()-began)/1_000_000);
|
||||
final int index=pass;
|
||||
world.getServer().runOnServer(s->{var g=(SanctuaryChunkGenerator)s.overworld().getChunkSource().getGenerator();
|
||||
check(g.planCache().hits()>0,"Reopened world read disk cache");
|
||||
if(index==0)check(g.planCache().misses()==0&&g.planCache().writes()==0,"Warm reopen restores all plans without recomputing");
|
||||
if(index==1)check(g.planCache().misses()>0&&g.planCache().writes()>0,"Corrupted core cache recomputed and replaced");
|
||||
var now=evidence(g);check(baseline.get().equals(now),"Same plans, fluid geometry, block writes, loot and protection after restore/recovery");
|
||||
System.out.println("LOADING064_CACHE_STATS hits="+g.planCache().hits()+" misses="+g.planCache().misses()+" writes="+g.planCache().writes()+" blockWrites="+now.writes()+" blockHash="+now.blocks());
|
||||
});
|
||||
check(PreloadProgress.current()==null,"Progress cleared after admission");
|
||||
}
|
||||
}
|
||||
check(phases.containsAll(Set.of("cache_check","water","transit","heritage","village")),"Real phases observed: "+phases);
|
||||
for(String lang:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());for(String phase:phases)check(!Component.translatable("sanctuary.preload."+phase).getString().startsWith("sanctuary."),"Localized phase "+lang+" "+phase);});
|
||||
System.out.println("LOADING064_PASS phases="+phases);
|
||||
}catch(Exception e){throw new AssertionError(e);}finally{observing.set(false);}
|
||||
}
|
||||
private record Evidence(com.google.gson.JsonElement core,PopulationHydrology.Plan.Snapshot water,PopulationLavaDeposit.Plan.Snapshot lava,long blocks,long writes,long protection) {}
|
||||
private static Evidence evidence(SanctuaryChunkGenerator g) {
|
||||
var root=g.source().islands().stream().filter(fr.koka.sanctuary.expansion.ExpansionIsland::origin).findFirst().orElseThrow();
|
||||
var region=PopulationHydrologyRuntime.regionPlan(g.delegate(root),g.randomState(root),new IslandCapacity.Region(0,0));
|
||||
// Reconstructed fluid indexes must agree with every retained geometric cell.
|
||||
for(var cell:region.water().cells())check(region.water().cellsAt(cell.x(),cell.z()).contains(cell)&®ion.water().cellsInChunk(cell.x()>>4,cell.z()>>4).contains(cell),"Hydrology indexes retain cells");
|
||||
var writer=new Writer();
|
||||
g.transitPlan21().modules().forEach(m->fr.koka.sanctuary.worldgen.transit.TransitRenderer21.render(m,writer));
|
||||
g.ruinsPlan17().networks().forEach(n->n.rooms().forEach(r->fr.koka.sanctuary.worldgen.ruins.RuinsRenderer17.render(r,writer)));
|
||||
g.heritagePlan19().sites().forEach(s->fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19.render(s,writer));
|
||||
g.heritagePlan19().segments().forEach(s->fr.koka.sanctuary.worldgen.heritage.HeritageRenderer19.renderSegment(s,writer));
|
||||
if(g.mineralSanctuary23().chamber()!=null)fr.koka.sanctuary.worldgen.mineral.MineralSanctuaryRenderer23.render(g.mineralSanctuary23().chamber(),writer);
|
||||
long protection=1;
|
||||
for(int x=-352;x<=352;x+=16)for(int z=-352;z<=352;z+=16)for(int y=16;y<384;y+=8)protection=31*protection+(g.protectsCityDecoration(new BlockPos(x,y,z))?1:0);
|
||||
return new Evidence(JsonParser.parseString(g.corePlanSignature()),region.water().snapshot(),region.lava().snapshot(),writer.hash,writer.writes,protection);
|
||||
}
|
||||
private static final class Writer implements fr.koka.sanctuary.worldgen.city.LostCityRenderer.BlockWriter {
|
||||
long hash=0xcbf29ce484222325L,writes;
|
||||
void add(long v){hash=(hash^v)*0x100000001b3L;}
|
||||
public void set(int x,int y,int z,BlockState state){add(x);add(y);add(z);add(Block.getId(state));writes++;}
|
||||
public void chest(int x,int y,int z,net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> table,long seed){add(x);add(y);add(z);add(table.identifier().toString().hashCode());add(seed);writes++;}
|
||||
}
|
||||
private static void cacheFailures() {
|
||||
try {
|
||||
Path root=Files.createTempDirectory("loading064-cache-");var cache=new DerivedPlanCache(root,"one");
|
||||
check(cache.read("test",Fixture.class,x->x)==null,"Missing cache is a miss");cache.write("test",new Fixture(7));
|
||||
check(cache.read("test",Fixture.class,x->x).value()==7,"Round trip");
|
||||
var changed=new DerivedPlanCache(root,"two");check(changed.read("test",Fixture.class,x->x)==null,"Changed identity invalidates");
|
||||
Files.createDirectories(changed.directory());Files.copy(cache.directory().resolve("test.json.gz"),changed.directory().resolve("test.json.gz"));
|
||||
check(changed.read("test",Fixture.class,x->x)==null,"Even copied cache verifies embedded identity");
|
||||
var file=cache.directory().resolve("test.json.gz");
|
||||
com.google.gson.JsonObject envelope;
|
||||
try(var in=new java.util.zip.GZIPInputStream(Files.newInputStream(file))){envelope=JsonParser.parseString(new String(in.readAllBytes(),java.nio.charset.StandardCharsets.UTF_8)).getAsJsonObject();}
|
||||
envelope.addProperty("payload","{\"value\":8}");
|
||||
try(var out=new java.util.zip.GZIPOutputStream(Files.newOutputStream(file))){out.write(envelope.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));}
|
||||
check(cache.read("test",Fixture.class,x->x)==null,"Payload checksum verified before decoding");cache.write("test",new Fixture(7));
|
||||
check(cache.read("test",Fixture.class,x->{throw new IllegalArgumentException("invalid geometry");})==null,"Validation failure falls back");
|
||||
Files.writeString(cache.directory().resolve("test.json.gz"),"truncated");check(cache.read("test",Fixture.class,x->x)==null,"Truncated gzip falls back");
|
||||
cache.write("test",new Fixture(9));check(cache.read("test",Fixture.class,x->x).value()==9,"Bad cache replaceable");
|
||||
try(var files=Files.walk(root)){for(Path p:files.sorted(Comparator.reverseOrder()).toList())Files.delete(p);}
|
||||
}catch(Exception e){throw new AssertionError(e);}
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String msg){if(!ok)throw new AssertionError(msg);}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.loading.PreloadProgress;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.screens.GenericMessageScreen;
|
||||
import net.minecraft.client.gui.screens.LevelLoadingScreen;
|
||||
import net.minecraft.client.multiplayer.LevelLoadTracker;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.progress.ChunkLoadStatusView;
|
||||
import net.minecraft.server.level.progress.LevelLoadListener;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
|
||||
/** Render the actual mixins without creating a world or changing its loading lifecycle. */
|
||||
public final class Loading068ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var previous=c.computeOnClient(m->m.gui.screen());
|
||||
int scale=c.computeOnClient(m->m.options.guiScale().get());
|
||||
String language=c.computeOnClient(m->m.getLanguageManager().getSelected());
|
||||
var progress=progress();var old=progress.get();
|
||||
var state=new PreloadProgress.State(null,"cache_read",System.nanoTime());
|
||||
try {
|
||||
progress.set(state);
|
||||
c.runOnClient(m->{
|
||||
m.options.guiScale().set(3);m.resizeGui();
|
||||
m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.gui.setScreen(new GenericMessageScreen(Component.translatable("menu.preparingSpawn")) {
|
||||
@Override public void tick() {} // No tick-based animation can pass this fixture.
|
||||
});
|
||||
});
|
||||
for(int frame=0;frame<8;frame++) {
|
||||
long until=System.nanoTime()+350_000_000L;
|
||||
c.waitFor(m->System.nanoTime()>=until,1200);
|
||||
c.takeScreenshot("loading068-animation-"+frame);
|
||||
check(progress.get()==state,"Rendering must not invent or advance a server phase");
|
||||
}
|
||||
for(int gui:new int[]{2,4}) {
|
||||
c.runOnClient(m->{m.options.guiScale().set(gui);m.resizeGui();});
|
||||
c.waitTicks(3);c.takeScreenshot("loading068-fr-scale-"+gui);
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
m.options.guiScale().set(3);m.resizeGui();
|
||||
m.getLanguageManager().setSelected("en_us");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
progress.set(new PreloadProgress.State(null,"water",state.started()));
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("loading068-en-water");
|
||||
progress.set(null);
|
||||
c.runOnClient(m->m.gui.setScreen(new GenericMessageScreen(Component.literal("Native message outside preparation"))));
|
||||
c.waitTicks(3);c.takeScreenshot("loading068-native-message");
|
||||
for(boolean grid:new boolean[]{false,true}) {
|
||||
c.runOnClient(m->{
|
||||
var tracker=new LevelLoadTracker();
|
||||
tracker.start(LevelLoadListener.Stage.LOAD_INITIAL_CHUNKS,100);
|
||||
tracker.update(LevelLoadListener.Stage.LOAD_INITIAL_CHUNKS,42,100);
|
||||
if(grid)tracker.setServerChunkStatusView(new ChunkLoadStatusView() {
|
||||
public void moveTo(ResourceKey<Level> level,ChunkPos pos) {}
|
||||
public int radius(){return 11;}
|
||||
public ChunkStatus get(int x,int z){return Math.abs(x)+Math.abs(z)<9?ChunkStatus.FULL:ChunkStatus.TERRAIN;}
|
||||
});
|
||||
m.gui.setScreen(new LevelLoadingScreen(tracker,LevelLoadingScreen.Reason.OTHER));
|
||||
});
|
||||
c.waitTicks(12);c.takeScreenshot("loading068-terrain-"+grid);
|
||||
}
|
||||
System.out.println("LOADING068_PASS: native FR/EN, GUI 2/3/4, eight frames without screen ticks, real phase unchanged, native chunk progress retained");
|
||||
} finally {
|
||||
progress.set(old);
|
||||
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(previous);});
|
||||
}
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
private static AtomicReference<PreloadProgress.State> progress() {
|
||||
try {var field=PreloadProgress.class.getDeclaredField("CURRENT");field.setAccessible(true);return (AtomicReference<PreloadProgress.State>)field.get(null);}
|
||||
catch(ReflectiveOperationException e){throw new AssertionError(e);}
|
||||
}
|
||||
private static void check(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.cycle.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import net.minecraft.client.gui.screens.advancements.*;
|
||||
import fr.koka.sanctuary.mixin.client.NotificationAdvancementTabAccess;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Real native routes and server-confirmed actions, including while the integrated world is paused. */
|
||||
public final class Menus083ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Menus083 "+UUID.randomUUID());s.setSeed("83");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.giveExperienceLevels(30);ProgressionService.send(p,"");});c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().setItem(0,new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.STONE));p.awardStat(net.minecraft.stats.Stats.ITEM_PICKED_UP.get(net.minecraft.world.item.Items.BREAD),3);p.awardStat(net.minecraft.stats.Stats.ENTITY_KILLED.get(net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getValue(net.minecraft.resources.Identifier.withDefaultNamespace("zombie"))),2);p.awardStat(net.minecraft.stats.Stats.JUMP,12);});
|
||||
server.runOnServer(s->{var p=player(s);for(String id:List.of("husbandry/root","husbandry/plant_seed")){var a=s.getAdvancements().get(Identifier.withDefaultNamespace(id));for(String key:a.value().criteria().keySet())p.getAdvancements().award(a,key);}p.getAdvancements().award(s.getAdvancements().get(Identifier.withDefaultNamespace("husbandry/balanced_diet")),"rotten_flesh");});
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3)){
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});c.waitTicks(3);
|
||||
c.runOnClient(m->{var b=button(m.gui.screen(),"sanctuary.arena.title");check(b.getY()>0&&b.getY()+b.getHeight()<m.gui.screen().height,"Combat button in viewport");});
|
||||
c.takeScreenshot("menus083-pause-"+locale+"-"+scale);
|
||||
c.runOnClient(m->{check(find(m.gui.screen(),"gui.advancements")==null&&find(m.gui.screen(),"gui.stats")==null,"Grouped native pause pages");check(button(m.gui.screen(),"sanctuary.cycle.faction").getY()==button(m.gui.screen(),"sanctuary.arena.title").getY(),"Faction and battles share row");});
|
||||
click(c,"sanctuary.progression.blocks");c.waitTicks(10);c.takeScreenshot("menus083-discovery-"+locale+"-"+scale);
|
||||
click(c,"sanctuary.discovery.general");c.waitFor(m->hasText(m.gui.screen(),"sanctuary.discovery.count"),100);c.takeScreenshot("menus083-general-"+locale+"-"+scale);
|
||||
click(c,"sanctuary.discovery.items");c.waitFor(m->hasText(m.gui.screen(),"sanctuary.discovery.count"),100);
|
||||
c.runOnClient(m->{check(find(m.gui.screen(),"item.minecraft.bread")==null,"Unowned item hidden initially");check(!button(m.gui.screen(),"sanctuary.discovery.owned").active,"Catalogue required for historical items");button(m.gui.screen(),"block.minecraft.stone");});
|
||||
c.takeScreenshot("menus083-items-"+locale+"-"+scale);
|
||||
click(c,"sanctuary.discovery.mobs");c.waitFor(m->hasText(m.gui.screen(),"sanctuary.discovery.count"),100);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"entity.minecraft.zombie");check(find(m.gui.screen(),"entity.minecraft.creeper")==null,"Unknown mob hidden");});
|
||||
click(c,"entity.minecraft.zombie");c.takeScreenshot("menus083-mob-detail-"+locale+"-"+scale);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
click(c,"sanctuary.discovery.recipes");c.takeScreenshot("menus083-recipes-"+locale+"-"+scale);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Sibling discovery tabs return straight to pause"));
|
||||
click(c,"sanctuary.progression.history");c.runOnClient(m->check(m.gui.screen() instanceof SanctuaryAtlasScreen,"World opens map directly"));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
click(c,"sanctuary.progression.identity");c.runOnClient(m->check(m.gui.screen() instanceof ProgressionScreen,"Inhabitant opens personal page"));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
click(c,"sanctuary.cycle.faction");c.waitFor(m->CycleClient.view!=null,100);c.waitTicks(5);c.takeScreenshot("menus083-faction-"+locale+"-"+scale);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
click(c,"sanctuary.arena.title");c.waitFor(m->m.gui.screen() instanceof ArenaScreen&&ArenaClient.view!=null,100);
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Arena returns to pause"));
|
||||
c.runOnClient(m->{ArenaClient.open(m.gui.screen());m.gui.screen().onClose();});c.waitTicks(8);c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Late arena response does not reopen it"));
|
||||
click(c,"sanctuary.progression.progress");c.runOnClient(m->{check(find(m.gui.screen(),"sanctuary.arena.title")==null,"No combat in progression");check(find(m.gui.screen(),"sanctuary.cycle.faction")==null,"No faction in progression");for(String tab:List.of("cycle","log"))button(m.gui.screen(),"sanctuary.cycle."+tab);button(m.gui.screen(),"gui.advancements");});
|
||||
c.takeScreenshot("menus083-progress-"+locale+"-"+scale);
|
||||
c.runOnClient(m->{var screen=(ProgressionScreen)m.gui.screen();check(button(screen,"sanctuary.cycle.cycle").getY()>=screen.contentTop+12,"XP has separate line");check(button(screen,"gui.advancements").getX()<screen.width/2,"Advancements above left skills");});
|
||||
click(c,"gui.advancements");trackDiet(c);c.runOnClient(m->check(m.gui.screen() instanceof net.minecraft.client.gui.screens.advancements.AdvancementsScreen,"Full native advancements"));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof ProgressionScreen,"Advancements returns to progression"));c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});
|
||||
click(c,"sanctuary.progression.blocks");click(c,"sanctuary.discovery.recipes");click(c,"sanctuary.recipes.buy");c.waitFor(m->RecipeClient.knowledge!=null&&RecipeClient.knowledge.learned(),100);
|
||||
click(c,"sanctuary.discovery.items");c.waitFor(m->hasText(m.gui.screen(),"sanctuary.discovery.count"),100);click(c,"sanctuary.discovery.owned");
|
||||
c.runOnClient(m->{button(m.gui.screen(),"item.minecraft.bread");check(find(m.gui.screen(),"item.minecraft.diamond")==null,"Catalogue still hides unknown items");});
|
||||
click(c,"item.minecraft.bread");c.takeScreenshot("menus083-item-detail");c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
click(c,"sanctuary.progression.progress");click(c,"sanctuary.cycle.cycle");c.waitFor(m->CycleClient.view!=null,100);c.waitTicks(5);
|
||||
c.runOnClient(m->check(!button(m.gui.screen(),"sanctuary.cycle.ngp").active,"Incomplete prestige disabled"));click(c,"sanctuary.cycle.cycle_progress");
|
||||
c.runOnClient(m->check(m.gui.screen() instanceof ProgressionScreen,"Prestige link returns to skills"));
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.active&&b.visible&&b.getMessage().getString().equals("+")).findFirst().orElseThrow().onPress(enter()));
|
||||
c.waitFor(m->ProgressionClient.progress.vitality()==1,100);
|
||||
c.runOnClient(m->check(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->b.visible&&b.getMessage().getString().equals("−")),"Refund invisible in normal play"));
|
||||
toggleOperator(c);c.waitFor(m->ProgressionClient.operator,150);
|
||||
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(new PauseScreen(true))));c.waitTicks(4);glyph(c,"−");
|
||||
c.waitFor(m->ProgressionClient.progress.vitality()==0,100);c.runOnClient(m->check(m.player.experienceLevel==26,"Operator refund returns last purchase cost"));
|
||||
c.takeScreenshot("menus083-refund");glyph(c,"+");c.waitFor(m->ProgressionClient.progress.vitality()==1,100);
|
||||
toggleOperator(c);c.waitFor(m->!ProgressionClient.operator,150);c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(new PauseScreen(true))));c.waitTicks(4);
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);click(c,"sanctuary.cycle.faction");c.waitTicks(8);
|
||||
c.runOnClient(m->{check(!button(m.gui.screen(),"sanctuary.cycle.create").active,"Blank faction name disabled");edit(m.gui.screen()).setValue("A");check(!button(m.gui.screen(),"sanctuary.cycle.create").active,"Short name disabled");edit(m.gui.screen()).setValue("Navigateurs");});
|
||||
click(c,"sanctuary.cycle.create");click(c,"sanctuary.cycle.cancel");c.runOnClient(m->check(CycleClient.view.faction()==null,"Cancel does not found"));
|
||||
click(c,"sanctuary.cycle.create");click(c,"sanctuary.cycle.confirm");c.waitFor(m->CycleClient.view.faction()!=null,100);
|
||||
c.runOnClient(m->{check(CycleClient.view.faction().capacity()==2,"Two founding places");check(!button(m.gui.screen(),"sanctuary.cycle.invite").active,"Blank invitation disabled");check(m.player.experienceLevel==15,"Catalogue, skill and faction paid exactly once");});
|
||||
click(c,"sanctuary.cycle.faction_log");c.runOnClient(m->{check(hasText(m.gui.screen(),"sanctuary.cycle.log_line"),"Faction journal has founding event");check(!hasText(m.gui.screen(),"sanctuary.progression.history_purchase"),"No skill purchases in faction journal");check(find(m.gui.screen(),"sanctuary.cycle.cycle")==null,"No prestige in faction navigation");});
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);click(c,"sanctuary.progression.progress");click(c,"sanctuary.cycle.log");c.waitTicks(5);c.runOnClient(m->{check(hasText(m.gui.screen(),"sanctuary.cycle.log_line"),"Faction event visible");check(hasText(m.gui.screen(),"sanctuary.progression.history_purchase"),"Purchase visible in history");check(hasText(m.gui.screen(),"sanctuary.progression.history_refund"),"Refund visible in history");});c.takeScreenshot("menus083-history-fr");
|
||||
click(c,"sanctuary.cycle.refresh");c.waitTicks(8);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=player(s);var r=ProgressEffects.get(p).buy("names",4);for(String skill:ProgressRecord.SKILLS)while(r.rank(skill)<ProgressRecord.cap(skill))r=r.buy(skill,1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});c.waitFor(m->ProgressionClient.progress.complete(),100);
|
||||
click(c,"sanctuary.cycle.cycle");c.waitTicks(8);click(c,"sanctuary.cycle.ngp");click(c,"sanctuary.cycle.cancel");click(c,"sanctuary.cycle.ngp");click(c,"sanctuary.cycle.confirm");
|
||||
c.waitFor(m->CycleClient.view.prestige()==1&&ProgressionClient.progress.cycle()==1,100);
|
||||
c.runOnClient(m->{check(ProgressionClient.progress.inventoryRows()==1&&ProgressionClient.progress.names(),"Prestige resets skills and keeps ability");check(CycleClient.view.faction().name().equals("Navigateurs")&&CycleClient.view.charges()==1,"Faction and prestige charge preserved");check(!button(m.gui.screen(),"sanctuary.cycle.ngp").active,"Cannot repeat prestige");});
|
||||
click(c,"sanctuary.cycle.log");c.waitTicks(5);c.takeScreenshot("menus083-prestige-history");c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(5);
|
||||
System.out.println("MENUS083_PASS: FR/EN GUI 2/3, six pause destinations, independent categories, native statistics, knowledge gates, advancements, buy/refund, faction journal, history, prestige, late arena close");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void glyph(ClientGameTestContext c,String glyph){c.runOnClient(m->widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.active&&b.visible&&b.getMessage().getString().equals(glyph)).findFirst().orElseThrow().onPress(enter()));c.waitTicks(5);}
|
||||
private static void toggleOperator(ClientGameTestContext c){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected("en_us");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new WorldOptionsScreen(m.gui.screen(),m.level));});c.waitTicks(3);
|
||||
for(String prefix:List.of("Allow Commands","Apply Changes"))c.runOnClient(m->{var w=widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(b->b.getMessage().getString().startsWith(prefix)).findFirst().orElseThrow();check(w.visible&&w.active,"Operator toggle accessible");if(w instanceof Button b)b.onPress(enter());else if(w instanceof CycleButton<?> b)b.onPress(enter());});
|
||||
c.waitTicks(5);
|
||||
}
|
||||
private static void trackDiet(ClientGameTestContext c){
|
||||
var id=Identifier.withDefaultNamespace("husbandry/balanced_diet");
|
||||
c.runOnClient(m->{var adv=m.getConnection().getAdvancements();var root=adv.progress().keySet().stream().filter(a->a.id().toString().equals("minecraft:husbandry/root")).findFirst().orElseThrow();adv.setSelectedTab(root,true);});c.waitTicks(3);
|
||||
c.runOnClient(m->{var screen=(AdvancementsScreen)m.gui.screen();var tab=(AdvancementTab)field(screen,"selectedTab");var access=(NotificationAdvancementTabAccess)tab;var w=access.sanctuary$widgets().values().stream().filter(a->a.getAdvancement().id().equals(id)).findFirst().orElseThrow();tab.scroll(100-w.getX()-access.sanctuary$scrollX(),45-w.getY()-access.sanctuary$scrollY());});c.waitTicks(2);
|
||||
c.runOnClient(m->{var screen=(AdvancementsScreen)m.gui.screen();var access=(NotificationAdvancementTabAccess)field(screen,"selectedTab");var w=access.sanctuary$widgets().values().stream().filter(a->a.getAdvancement().id().equals(id)).findFirst().orElseThrow();var event=new MouseButtonEvent((int)field(screen,"leftPos")+9+Math.floor(access.sanctuary$scrollX())+w.getX()+12,(int)field(screen,"topPos")+18+Math.floor(access.sanctuary$scrollY())+w.getY()+12,new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0));boolean before=NotificationClient.isPinned(id);screen.mouseClicked(event,false);screen.mouseReleased(event);check(NotificationClient.isPinned(id)!=before,"Native click toggles advancement tracking through new route");});
|
||||
}
|
||||
private static Object field(AdvancementsScreen screen,String name){try{var f=AdvancementsScreen.class.getDeclaredField(name);f.setAccessible(true);return f.get(screen);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> widgets(net.minecraft.client.gui.components.events.GuiEventListener e){
|
||||
return java.util.stream.Stream.concat(java.util.stream.Stream.of(e),e instanceof net.minecraft.client.gui.components.events.ContainerEventHandler h?h.children().stream().flatMap(Menus083ClientChecks::widgets):java.util.stream.Stream.empty());
|
||||
}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static Button find(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElse(null);}
|
||||
private static Button button(Screen s,String key){return Objects.requireNonNull(find(s,key),"Missing button "+key+" on "+s.getClass().getSimpleName());}
|
||||
private static void press(Screen s,String key){var b=button(s,key);check(b.active,"Disabled "+key);b.onPress(enter());}
|
||||
private static void click(ClientGameTestContext c,String key){c.runOnClient(m->press(m.gui.screen(),key));c.waitTicks(5);}
|
||||
private static EditBox edit(Screen s){return widgets(s).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow();}
|
||||
private static boolean hasText(Screen s,String key){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w->contains(w.getMessage(),key));}
|
||||
private static boolean contains(Component c,String key){if(c.getContents() instanceof TranslatableContents t&&t.getKey().equals(key))return true;return c.getSiblings().stream().anyMatch(s->contains(s,key));}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Multiblocks084ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos BARREL=new BlockPos(15,80,0),FURNACE=new BlockPos(15,80,7);
|
||||
private static MultiblockData.Machine cask,oven;
|
||||
private static io.netty.channel.embedded.EmbeddedChannel guestChannel;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Multiblocks084 "+UUID.randomUUID());s.setSeed("84");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;var l=p.level();
|
||||
var progress=ProgressRecord.INITIAL;var config=ProgressionService.session(s).config;for(int i=0;i<5;i++)progress=progress.buy("inventory",progress.cost("inventory",config));p.setAttached(ProgressionService.RECORD,progress.json());ProgressEffects.apply(p,progress);ProgressionService.send(p,"");
|
||||
for(int x=10;x<23;x++)for(int z=-4;z<14;z++)l.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState());
|
||||
p.teleportTo(l,13.5,80,1.5,Set.of(),-90,0,true);
|
||||
cask=structure(l,BARREL,false);oven=structure(l,FURNACE,true);
|
||||
check(MultiblockData.get(l).at(BARREL)==null,"No automatic assembly");
|
||||
var stock=new MultiblockContainer(l,cask);stock.setItem(0,new ItemStack(Items.STONE,64));
|
||||
var diamond=new ItemStack(Items.DIAMOND,17);diamond.set(DataComponents.CUSTOM_NAME,Component.literal("Souvenir 084"));stock.setItem(728,diamond);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Multiblocks.WRENCH));
|
||||
wrench(p,BARREL);check(MultiblockData.get(l).at(BARREL)==null,"First click previews only");wrench(p,BARREL);
|
||||
cask=MultiblockData.get(l).at(BARREL);check(cask!=null,"Second click assembles");
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.HOPPER));p.setShiftKeyDown(true);
|
||||
check(UseBlockCallback.EVENT.invoker().interact(p,l,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(BARREL),Direction.WEST,BARREL,false))==InteractionResult.PASS,"Sneaking permits attaching blocks to machine");
|
||||
p.setShiftKeyDown(false);p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Multiblocks.WRENCH));
|
||||
check(!MultiblockData.get(l).add(new MultiblockData.Machine(UUID.randomUUID(),BARREL,false,Direction.NORTH)),"Overlap rejected");
|
||||
check(stock.getItem(728).getCount()==17,"Existing component data retained");
|
||||
var data=MultiblockData.get(l);var json=MultiblockData.CODEC.encodeStart(JsonOps.INSTANCE,data).getOrThrow();var reloaded=MultiblockData.CODEC.parse(JsonOps.INSTANCE,json).getOrThrow();
|
||||
check(reloaded.at(BARREL.offset(2,2,2)).id().equals(cask.id()),"Persistence reconstructs all members across chunks");
|
||||
try{
|
||||
var folder=java.nio.file.Files.createTempDirectory("sanctuary-multiblocks084-");
|
||||
try(var disk=new net.minecraft.world.level.storage.SavedDataStorage(folder,s.getFixerUpper(),l.registryAccess())){var record=MultiblockData.read(disk);record.add(cask);disk.saveAndJoin();}
|
||||
try(var disk=new net.minecraft.world.level.storage.SavedDataStorage(folder,s.getFixerUpper(),l.registryAccess())){check(MultiblockData.read(disk).at(BARREL.offset(2,2,2)).id().equals(cask.id()),"Actual SavedData file reload retains assembly");}
|
||||
try(var files=java.nio.file.Files.walk(folder)){for(var file:files.sorted(java.util.Comparator.reverseOrder()).toList())java.nio.file.Files.delete(file);}
|
||||
}catch(java.io.IOException error){throw new AssertionError(error);}
|
||||
json.getAsJsonObject().addProperty("schema",99);check(MultiblockData.CODEC.parse(JsonOps.INSTANCE,json).error().isPresent(),"Unknown schema rejected");
|
||||
var common=HopperBlockEntity.getContainerAt(l,BARREL);check(common.getContainerSize()==729,"Hopper sees whole cask");
|
||||
check(HopperBlockEntity.addItem(null,common,new ItemStack(Items.COBBLESTONE,64),Direction.UP).isEmpty(),"Native hopper insertion into common cask");
|
||||
l.setBlockAndUpdate(BARREL.below(),Blocks.HOPPER.defaultBlockState());var hopper=(HopperBlockEntity)l.getBlockEntity(BARREL.below());
|
||||
check(HopperBlockEntity.suckInItems(l,hopper)&&hopper.getItem(0).is(Items.STONE),"Native hopper extracts from common cask");
|
||||
l.setBlockAndUpdate(BARREL.below(),Blocks.STONE.defaultBlockState());
|
||||
var remote=guest(p);remote.setPos(13.5,80,2.5);Multiblocks.open(p,cask,BARREL,13,"",0);Multiblocks.open(remote,cask,BARREL,13,"",0);
|
||||
var a=(MultiblockMenu)p.containerMenu;var b=(MultiblockMenu)remote.containerMenu;
|
||||
int overflow=a.slots.size()-1;check(overflow>=90,"Six-row inventory includes overflow slots");a.getSlot(overflow).set(new ItemStack(Items.STICK,23));check(!a.quickMoveStack(p,overflow).isEmpty()&&a.getSlot(overflow).getItem().isEmpty(),"Shift deposit from sixth inventory row reaches whole cask");
|
||||
check(a.view.indices().length==27&&a.getSlot(26).getItem().getCount()==17,"Last page has 27 real cells");
|
||||
a.getSlot(26).remove(7);check(b.getSlot(26).getItem().getCount()==10,"Second viewer sees same physical stack");
|
||||
common.removeItem(728,3);check(a.getSlot(26).getItem().getCount()==7,"Hopper and players share stock");
|
||||
var result=b.quickMoveStack(remote,26);check(result.getCount()==7&&stock.getItem(728).isEmpty(),"Shift take exactly once");
|
||||
for(int i=0;i<remote.getInventory().getContainerSize();i++)remote.getInventory().setItem(i,new ItemStack(Items.DIRT,64));
|
||||
stock.setItem(728,new ItemStack(Items.DIAMOND));check(b.quickMoveStack(remote,26).isEmpty()&&stock.getItem(728).getCount()==1,"Full player inventory leaves source intact");stock.setItem(728,ItemStack.EMPTY);
|
||||
check(a.quickMoveStack(p,26).isEmpty(),"Other viewer cannot duplicate consumed stack");
|
||||
var restored=new ItemStack(Items.DIAMOND,17);restored.set(DataComponents.CUSTOM_NAME,Component.literal("Souvenir 084"));stock.setItem(728,restored);
|
||||
guestChannel.outboundMessages().clear();int oldRemoteId=remote.containerMenu.containerId;
|
||||
((MultiblockMenu)remote.containerMenu).change(remote,0,"",0);guestChannel.runPendingTasks();
|
||||
check(remote.containerMenu.containerId!=oldRemoteId,"Navigation still allocates a fresh menu ID");
|
||||
check(guestChannel.outboundMessages().stream().noneMatch(packet->packet instanceof net.minecraft.network.protocol.game.ClientboundContainerClosePacket),"Navigation sends no intermediate close packet");
|
||||
remote.closeContainer();guestChannel.runPendingTasks();
|
||||
check(guestChannel.outboundMessages().stream().anyMatch(packet->packet instanceof net.minecraft.network.protocol.game.ClientboundContainerClosePacket),"Actual close still sends close packet");
|
||||
remote.openMenu(new SimpleMenuProvider((id,inv,player)->ChestMenu.threeRows(id,inv),Component.literal("Unrelated")));
|
||||
Multiblocks.closeViewers(l,cask);check(remote.containerMenu!=remote.inventoryMenu,"Machine changes leave unrelated nearby menus open");
|
||||
remote.closeContainer();remote.discard();
|
||||
Multiblocks.open(p,cask,BARREL,0,"",0);
|
||||
// Same native block-entity serialization used when a world unloads/reloads.
|
||||
var last=l.getBlockEntity(BARREL.offset(2,2,2));var tag=last.saveWithFullMetadata(l.registryAccess());
|
||||
var restoredBe=(BarrelBlockEntity)BlockEntity.loadStatic(last.getBlockPos(),last.getBlockState(),tag,l.registryAccess());
|
||||
check(ItemStack.matches(restoredBe.getItem(26),restored),"Native persistence retains custom stack components");
|
||||
});
|
||||
c.waitFor(m->m.gui.screen() instanceof MultiblockScreen,200);c.waitTicks(5);
|
||||
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3)){
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});c.waitTicks(3);c.takeScreenshot("multiblocks084-cask-"+language+"-"+scale);
|
||||
c.runOnClient(m->{var screen=m.gui.screen();for(var child:screen.children())if(child instanceof AbstractWidget w)check(w.getX()>=0&&w.getX()+w.getWidth()<=screen.width&&w.getY()>=0&&w.getY()+w.getHeight()<=screen.height,"Controls fit GUI scale "+scale);});
|
||||
}
|
||||
// Regression beta.088: click several pages at the same physical cursor position.
|
||||
double[] cursor=hoverButton(c,">");
|
||||
for(int page=1;page<=3;page++){int expected=page;clickMouse(c);c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.page()==expected,200);c.waitTicks(4);assertCursor(c,cursor,"Next page "+page);}
|
||||
cursor=hoverButton(c,"<");
|
||||
for(int page=2;page>=1;page--){int expected=page;clickMouse(c);c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.page()==expected,200);c.waitTicks(4);assertCursor(c,cursor,"Previous page "+page);}
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.screen().children().stream().filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow().setValue("Roche");});
|
||||
cursor=hoverButton(c,c.computeOnClient(m->Multiblocks.text("search").getString()));clickMouse(c);
|
||||
c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.query().equals("Roche"),200);c.waitTicks(4);
|
||||
assertCursor(c,cursor,"Search");
|
||||
c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.view.indices().length>0&&menu.getSlot(0).getItem().is(Items.STONE),"Localized French search maps to server-owned items");});
|
||||
int previous=c.computeOnClient(m->m.player.containerMenu.containerId);
|
||||
c.runOnClient(m->ClientPlayNetworking.send(new MultiblockMenu.Change(previous,0,"Souvenir",0)));
|
||||
c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.containerId!=previous&&menu.view.query().equals("Souvenir"),200);
|
||||
c.waitTicks(3);c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.view.indices().length==1&&menu.getSlot(0).getItem().is(Items.DIAMOND),"Native filtered contents synchronized");});c.takeScreenshot("multiblocks084-search");
|
||||
c.runOnClient(m->ClientPlayNetworking.send(new MultiblockMenu.Change(previous,13,"",0)));c.waitTicks(4);
|
||||
c.runOnClient(m->check(((MultiblockMenu)m.player.containerMenu).view.query().equals("Souvenir"),"Stale page request rejected"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var menu=(MultiblockMenu)p.containerMenu;int old=menu.containerId;
|
||||
menu.setCarried(new ItemStack(Items.STICK));menu.change(p,1,"",0);check(p.containerMenu.containerId==old,"Cursor blocks page change");menu.setCarried(ItemStack.EMPTY);
|
||||
var l=p.level();var before=new MultiblockContainer(l,cask).getItem(728).copy();Multiblocks.dissolve(l,cask);
|
||||
check(((BarrelBlockEntity)l.getBlockEntity(BARREL.offset(2,2,2))).getItem(26).equals(before)||ItemStack.matches(((BarrelBlockEntity)l.getBlockEntity(BARREL.offset(2,2,2))).getItem(26),before),"Disassembly preserves named stack");
|
||||
check(Multiblocks.assemble(p,cask),"Can reassemble current stock");
|
||||
l.destroyBlock(BARREL,true);check(!Multiblocks.ready(l,cask),"Broken component invalidates menus immediately");
|
||||
check(new MultiblockContainer(l,cask).getItem(728).getCount()==17,"Breaking other cell preserves remainder");
|
||||
check(MultiblockData.get(l).at(BARREL.offset(2,2,2))==null,"Break dissolves membership immediately");
|
||||
explosionCheck(p);
|
||||
p.teleportTo(l,13.5,80,8.5,Set.of(),-90,0,true);check(Multiblocks.assemble(p,oven),"Assemble furnace");
|
||||
cookChecks(p);Multiblocks.open(p,oven,FURNACE,0,"",2);
|
||||
});
|
||||
c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.furnace(),200);c.waitTicks(5);c.takeScreenshot("multiblocks084-furnace");
|
||||
for(int section=0;section<3;section++){int expected=section;String key=new String[]{"inputs","fuel","outputs"}[section];cursor=hoverButton(c,c.computeOnClient(m->Multiblocks.text(key).getString()));clickMouse(c);c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.section()==expected,200);c.waitTicks(4);assertCursor(c,cursor,"Furnace section "+section);}
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitForScreen(null);
|
||||
server.waitFor(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.containerMenu==p.inventoryMenu;},200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var l=p.level();var stock=new MultiblockContainer(l,oven);int before=items(stock);Multiblocks.dissolve(l,oven);check(items(stock)==before,"Furnace disassembly preserves current fuel/outputs");
|
||||
// An absent distant chunk suspends membership, without creating or loading that chunk.
|
||||
var far=new MultiblockData.Machine(UUID.randomUUID(),new BlockPos(300000,80,300000),true,Direction.NORTH);check(!l.hasChunkAt(far.origin()),"Fixture starts unloaded");MultiblockData.get(l).add(far);check(Multiblocks.state(l,far)==0&&!l.hasChunkAt(far.origin()),"Validation never loads remote chunks");MultiblockData.get(l).remove(far);
|
||||
p.closeContainer();System.out.println("MULTIBLOCKS084_PASS: explicit preview/assembly, cross-chunk membership, schema, 729 slots, 2 server players, native hopper view, pages/search/new IDs/cursor, stationary mouse paging and furnace tabs, Escape closure, component preservation, break, shared native smelting/fuel/remainders/XP, no forced chunk loads, FR/EN GUI2/3");
|
||||
});
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static double[] hoverButton(ClientGameTestContext c,String text){
|
||||
return c.computeOnClient(m->{var button=m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(text)).findFirst().orElseThrow();var w=m.getWindow();
|
||||
double x=button.getX()+button.getWidth()/2.0,y=button.getY()+button.getHeight()/2.0;
|
||||
double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();
|
||||
org.lwjgl.sdl.SDLMouse.SDL_WarpMouseInWindow(w.handle(),(float)(x*sx),(float)(y*sy));
|
||||
m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();return new double[]{x,y};});
|
||||
}
|
||||
private static void clickMouse(ClientGameTestContext c){
|
||||
c.runOnClient(m->{var button=new net.minecraft.client.input.MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0);m.mouseHandler.onButton(m.getWindow().handle(),button,1);m.mouseHandler.onButton(m.getWindow().handle(),button,0);});
|
||||
}
|
||||
private static void assertCursor(ClientGameTestContext c,double[] cursor,String step){
|
||||
c.runOnClient(m->{check(Math.abs(m.mouseHandler.getScaledXPos(m.getWindow())-cursor[0])<1&&Math.abs(m.mouseHandler.getScaledYPos(m.getWindow())-cursor[1])<1,step+" keeps mouse over its button: expected "+Arrays.toString(cursor)+", got "+m.mouseHandler.getScaledXPos(m.getWindow())+","+m.mouseHandler.getScaledYPos(m.getWindow()));});
|
||||
}
|
||||
private static void cookChecks(ServerPlayer p){
|
||||
var l=p.level();var stock=new MultiblockContainer(l,oven);
|
||||
stock.setItem(0,new ItemStack(Items.RAW_IRON,8));stock.setItem(3,new ItemStack(Items.RAW_GOLD,8));stock.setItem(79,new ItemStack(Items.COAL,2));
|
||||
var nativeBe=(AbstractFurnaceBlockEntity)stock.cell(0);
|
||||
AbstractFurnaceBlockEntity.serverTick(l,nativeBe.getBlockPos(),nativeBe.getBlockState(),nativeBe);
|
||||
check(((MultiblockFurnaceAccess)nativeBe).sanctuary$progress()==0,"Native tick cannot double-cook an assembled lane");
|
||||
for(int tick=0;tick<201;tick++)MultiblockCooking.tick(l,oven);
|
||||
check(stock.getItem(2).is(Items.IRON_INGOT)&&stock.getItem(5).is(Items.GOLD_INGOT),"Two lanes smelt with distant common fuel");
|
||||
int coal=0;for(int i=1;i<81;i+=3)if(stock.getItem(i).is(Items.COAL))coal+=stock.getItem(i).getCount();check(coal==1,"Only one shared coal consumed");
|
||||
int heat=0;for(int i=0;i<81;i+=3)heat+=((MultiblockFurnaceAccess)stock.cell(i)).sanctuary$heat();check(heat>=1180&&heat<=1210,"Heat cost reflects both lanes: "+heat);
|
||||
var physical=(AbstractFurnaceBlockEntity)stock.cell(0);var saved=physical.saveWithFullMetadata(l.registryAccess());
|
||||
var restored=BlockEntity.loadStatic(physical.getBlockPos(),physical.getBlockState(),saved,l.registryAccess());
|
||||
check(((MultiblockFurnaceAccess)restored).sanctuary$heat()==((MultiblockFurnaceAccess)physical).sanctuary$heat(),"Native save preserves remaining shared heat");
|
||||
check(((MultiblockFurnaceAccess)restored).sanctuary$progress()==((MultiblockFurnaceAccess)physical).sanctuary$progress(),"Native save preserves work in progress");
|
||||
Multiblocks.open(p,oven,FURNACE,0,"",2);var menu=(MultiblockMenu)p.containerMenu;check(!menu.getSlot(0).mayPlace(new ItemStack(Items.STONE)),"Outputs reject deposits");check(!menu.quickMoveStack(p,0).isEmpty(),"Native result slot awards cooking XP/recipes");
|
||||
stock.clearContent();for(int i=0;i<81;i+=3){var a=(MultiblockFurnaceAccess)stock.cell(i);a.sanctuary$heat(0);a.sanctuary$progress(0);}
|
||||
stock.setItem(0,new ItemStack(Items.WET_SPONGE));stock.setItem(79,new ItemStack(Items.LAVA_BUCKET));
|
||||
for(int tick=0;tick<205;tick++)MultiblockCooking.tick(l,oven);
|
||||
check(stock.getItem(2).is(Items.SPONGE)&&stock.getItem(1).is(Items.WATER_BUCKET),"Native lava remainder and wet sponge interaction preserved");
|
||||
stock.clearContent();for(int i=0;i<81;i+=3){var a=(MultiblockFurnaceAccess)stock.cell(i);a.sanctuary$heat(0);a.sanctuary$progress(0);}
|
||||
stock.setItem(0,new ItemStack(Items.RAW_IRON));stock.setItem(2,new ItemStack(Items.IRON_INGOT,64));stock.setItem(79,new ItemStack(Items.COAL));
|
||||
for(int tick=0;tick<30;tick++)MultiblockCooking.tick(l,oven);check(stock.getItem(79).getCount()==1&&stock.getItem(0).getCount()==1,"Full output never consumes new fuel or ingredient");
|
||||
// One coal cannot finish 27 simultaneous recipes, but adding it in stages must not erase paid work.
|
||||
stock.clearContent();for(int i=0;i<81;i+=3){var a=(MultiblockFurnaceAccess)stock.cell(i);a.sanctuary$heat(0);a.sanctuary$progress(0);stock.setItem(i,new ItemStack(Items.RAW_IRON));}
|
||||
stock.setItem(79,new ItemStack(Items.COAL));for(int tick=0;tick<90;tick++)MultiblockCooking.tick(l,oven);
|
||||
int partial=((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress();check(partial>0&&partial<200,"Batch pauses midway when fuel runs out");
|
||||
for(int tick=0;tick<30;tick++)MultiblockCooking.tick(l,oven);
|
||||
check(((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress()==partial,"Starved batch retains paid work");
|
||||
stock.setItem(79,new ItemStack(Items.COAL,4));for(int tick=0;tick<205;tick++)MultiblockCooking.tick(l,oven);
|
||||
int produced=0;for(int i=2;i<81;i+=3)produced+=stock.getItem(i).getCount();check(produced==27,"Refuelling completes all 27 lanes once: "+produced);
|
||||
var hopper=(WorldlyContainer)HopperBlockEntity.getContainerAt(l,FURNACE);check(hopper.getSlotsForFace(Direction.UP).length==27&&hopper.getSlotsForFace(Direction.DOWN).length==54,"Native sided hopper slots aggregated");
|
||||
}
|
||||
private static void explosionCheck(ServerPlayer p){
|
||||
var l=p.level();var origin=new BlockPos(48,80,0);var m=structure(l,origin,false);
|
||||
check(Multiblocks.assemble(p,m),"Explosion fixture assembled");var stock=new MultiblockContainer(l,m);stock.setItem(0,new ItemStack(Items.DIAMOND,23));
|
||||
l.explode(null,48.5,80.5,.5,4,false,net.minecraft.world.level.Level.ExplosionInteraction.TNT);
|
||||
check(MultiblockData.get(l).at(origin)==null,"Explosion dissolves machine");
|
||||
int loose=l.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,new AABB(origin).inflate(12)).stream().filter(e->e.getItem().is(Items.DIAMOND)).mapToInt(e->e.getItem().getCount()).sum();
|
||||
int stored=0;for(var pos:m.members())if(l.getBlockEntity(pos) instanceof BarrelBlockEntity be)for(int i=0;i<27;i++)if(be.getItem(i).is(Items.DIAMOND))stored+=be.getItem(i).getCount();
|
||||
check(loose+stored==23,"Explosion conserves container contents exactly once: "+loose+"/"+stored);
|
||||
}
|
||||
private static MultiblockData.Machine structure(ServerLevel l,BlockPos origin,boolean furnace){var m=new MultiblockData.Machine(UUID.randomUUID(),origin,furnace,Direction.NORTH);for(var pos:m.members())l.setBlockAndUpdate(pos,(furnace?Blocks.FURNACE:Blocks.BARREL).defaultBlockState());return m;}
|
||||
private static int items(Container c){int n=0;for(int i=0;i<c.getContainerSize();i++)n+=c.getItem(i).getCount();return n;}
|
||||
private static void wrench(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.WEST,pos,false));}
|
||||
private static ServerPlayer guest(ServerPlayer host){var s=host.level().getServer();var profile=new com.mojang.authlib.GameProfile(UUID.randomUUID(),"Visitor084");var p=new ServerPlayer(s,host.level(),profile,ClientInformation.createDefault());var connection=new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);guestChannel=new io.netty.channel.embedded.EmbeddedChannel(connection);p.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(s,connection,p,net.minecraft.server.network.CommonListenerCookie.createInitial(profile,false));p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());host.level().addNewPlayer(p);return p;}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cycle.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Real native routes and server-confirmed actions, including while the integrated world is paused. */
|
||||
public final class Navigation076ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Navigation076 "+UUID.randomUUID());s.setSeed("76");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.giveExperienceLevels(30);ProgressionService.send(p,"");});c.waitTicks(5);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3)){
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});c.waitTicks(3);
|
||||
c.runOnClient(m->{var b=button(m.gui.screen(),"sanctuary.arena.title");check(b.getY()>0&&b.getY()+b.getHeight()<m.gui.screen().height,"Combat button in viewport");});
|
||||
c.takeScreenshot("navigation076-pause-"+locale+"-"+scale);
|
||||
click(c,"sanctuary.arena.title");c.waitFor(m->m.gui.screen() instanceof ArenaScreen&&ArenaClient.view!=null,100);
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Arena returns to pause"));
|
||||
c.runOnClient(m->{ArenaClient.open(m.gui.screen());m.gui.screen().onClose();});c.waitTicks(8);c.runOnClient(m->check(m.gui.screen() instanceof PauseScreen,"Late arena response does not reopen it"));
|
||||
click(c,"sanctuary.progression.progress");c.runOnClient(m->{check(find(m.gui.screen(),"sanctuary.arena.title")==null,"No combat in progression");for(String tab:List.of("cycle","faction","log"))button(m.gui.screen(),"sanctuary.cycle."+tab);});
|
||||
c.takeScreenshot("navigation076-progress-"+locale+"-"+scale);c.getInput().pressKey(InputConstants.KEY_ESCAPE);
|
||||
}
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());});
|
||||
click(c,"sanctuary.progression.progress");click(c,"sanctuary.cycle.cycle");c.waitFor(m->CycleClient.view!=null,100);c.waitTicks(5);
|
||||
c.runOnClient(m->check(!button(m.gui.screen(),"sanctuary.cycle.ngp").active,"Incomplete prestige disabled"));click(c,"sanctuary.cycle.cycle_progress");
|
||||
c.runOnClient(m->check(m.gui.screen() instanceof ProgressionScreen,"Prestige link returns to skills"));
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.active&&b.visible&&b.getMessage().getString().equals("+")).findFirst().orElseThrow().onPress(enter()));
|
||||
c.waitFor(m->ProgressionClient.progress.vitality()==1,100);
|
||||
click(c,"sanctuary.cycle.faction");c.waitTicks(8);
|
||||
c.runOnClient(m->{check(!button(m.gui.screen(),"sanctuary.cycle.create").active,"Blank faction name disabled");edit(m.gui.screen()).setValue("A");check(!button(m.gui.screen(),"sanctuary.cycle.create").active,"Short name disabled");edit(m.gui.screen()).setValue("Navigateurs");});
|
||||
click(c,"sanctuary.cycle.create");click(c,"sanctuary.cycle.cancel");c.runOnClient(m->check(CycleClient.view.faction()==null,"Cancel does not found"));
|
||||
click(c,"sanctuary.cycle.create");click(c,"sanctuary.cycle.confirm");c.waitFor(m->CycleClient.view.faction()!=null,100);
|
||||
c.runOnClient(m->{check(CycleClient.view.faction().capacity()==2,"Two founding places");check(!button(m.gui.screen(),"sanctuary.cycle.invite").active,"Blank invitation disabled");check(m.player.experienceLevel==19,"Skill plus faction paid exactly once");});
|
||||
click(c,"sanctuary.cycle.log");c.waitTicks(5);c.runOnClient(m->{check(hasText(m.gui.screen(),"sanctuary.cycle.log_line"),"Faction event visible");check(hasText(m.gui.screen(),"sanctuary.progression.history_purchase"),"Purchase visible in history");});c.takeScreenshot("navigation076-history-fr");
|
||||
click(c,"sanctuary.cycle.refresh");c.waitTicks(8);c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=player(s);var r=ProgressEffects.get(p).buy("names",4);for(String skill:ProgressRecord.SKILLS)while(r.rank(skill)<ProgressRecord.cap(skill))r=r.buy(skill,1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});c.waitFor(m->ProgressionClient.progress.complete(),100);
|
||||
click(c,"sanctuary.cycle.cycle");c.waitTicks(8);click(c,"sanctuary.cycle.ngp");click(c,"sanctuary.cycle.cancel");click(c,"sanctuary.cycle.ngp");click(c,"sanctuary.cycle.confirm");
|
||||
c.waitFor(m->CycleClient.view.prestige()==1&&ProgressionClient.progress.cycle()==1,100);
|
||||
c.runOnClient(m->{check(ProgressionClient.progress.inventoryRows()==1&&ProgressionClient.progress.names(),"Prestige resets skills and keeps ability");check(CycleClient.view.faction().name().equals("Navigateurs")&&CycleClient.view.charges()==1,"Faction and prestige charge preserved");check(!button(m.gui.screen(),"sanctuary.cycle.ngp").active,"Cannot repeat prestige");});
|
||||
click(c,"sanctuary.cycle.log");c.waitTicks(5);c.takeScreenshot("navigation076-prestige-history");c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(5);
|
||||
c.runOnClient(m->FamiliarClient.open(null));c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.bond!=null,100);c.waitTicks(5);
|
||||
c.runOnClient(m->check(!button(m.gui.screen(),"sanctuary.familiar.work").active,"Utility requires ability for normal player"));
|
||||
server.runOnServer(s->{var p=player(s);FamiliarBattle.state(p).lastRequest=-100;FamiliarBattle.command(p,"work");check(!FamiliarBattle.state(p).workMode,"Server also refuses locked utility");CompanionService.save(p,CompanionService.record(p).purchase(4,System.currentTimeMillis()));CompanionService.send(p,"");FamiliarBattle.send(p,"");});c.waitTicks(10);
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow().setScrollAmount(100));
|
||||
click(c,"sanctuary.familiar.work");c.waitFor(m->FamiliarClient.view.work(),100);
|
||||
c.runOnClient(m->check(widgets(m.gui.screen()).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow().scrollAmount()>=90,"Changing utility preserves scroll position"));
|
||||
c.runOnClient(m->{check(!button(m.gui.screen(),"sanctuary.familiar.work_selected").active,"Selected utility visibly selected");check(hasText(m.gui.screen(),"sanctuary.familiar.selected_power"),"Selected power named");});
|
||||
c.takeScreenshot("navigation076-utility");
|
||||
c.runOnClient(m->{String name=FamiliarClient.name(FamiliarClient.profile.signature()).getString();widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(name)).findFirst().orElseThrow().onPress(enter());});c.waitFor(m->!FamiliarClient.view.work(),100);c.waitTicks(6);
|
||||
click(c,"sanctuary.familiar.work");c.waitFor(m->FamiliarClient.view.work(),100);c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);CompanionService.save(p,CompanionService.record(p).cooldown(System.currentTimeMillis()+60000));});c.getInput().pressKey(InputConstants.KEY_G);
|
||||
c.waitFor(m->FamiliarClient.view.message().equals("work.cooldown"),100);
|
||||
c.runOnClient(m->check(FamiliarClient.feedback(FamiliarClient.view.message()).getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.companion.cooldown"),"Utility feedback uses correct translations"));
|
||||
System.out.println("NAVIGATION076_PASS: FR/EN scale 2/3, pause/arena/back, late close, purchase history, faction, prestige, locked/selected utility, G feedback");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static java.util.stream.Stream<net.minecraft.client.gui.components.events.GuiEventListener> widgets(net.minecraft.client.gui.components.events.GuiEventListener e){
|
||||
return java.util.stream.Stream.concat(java.util.stream.Stream.of(e),e instanceof net.minecraft.client.gui.components.events.ContainerEventHandler h?h.children().stream().flatMap(Navigation076ClientChecks::widgets):java.util.stream.Stream.empty());
|
||||
}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static Button find(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElse(null);}
|
||||
private static Button button(Screen s,String key){return Objects.requireNonNull(find(s,key),"Missing button "+key+" on "+s.getClass().getSimpleName());}
|
||||
private static void press(Screen s,String key){var b=button(s,key);check(b.active,"Disabled "+key+" view="+FamiliarClient.view+" record="+CompanionClient.record);b.onPress(enter());}
|
||||
private static void click(ClientGameTestContext c,String key){c.runOnClient(m->press(m.gui.screen(),key));c.waitTicks(5);}
|
||||
private static EditBox edit(Screen s){return widgets(s).filter(EditBox.class::isInstance).map(EditBox.class::cast).findFirst().orElseThrow();}
|
||||
private static boolean hasText(Screen s,String key){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w->contains(w.getMessage(),key));}
|
||||
private static boolean contains(Component c,String key){if(c.getContents() instanceof TranslatableContents t&&t.getKey().equals(key))return true;return c.getSiblings().stream().anyMatch(s->contains(s,key));}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Piston087ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos C=new BlockPos(15,85,15);
|
||||
private static final AtomicBoolean movingSeen=new AtomicBoolean(),returnSeen=new AtomicBoolean();
|
||||
private static net.minecraft.world.entity.animal.pig.Pig pig;
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Piston087 "+UUID.randomUUID());s.setSeed("87");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
|
||||
|
||||
|
||||
var server=world.getServer();
|
||||
c.runOnClient(m->{if(!m.gui.hud.isHidden())m.gui.hud.toggle();m.options.bobView().set(false);m.options.fov().set(70);m.getWindow().setWindowed(1280,800);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(m.level==null)return;for(var f:Direction.values())for(int d=0;d<4;d++)if(m.level.getBlockEntity(C.relative(f,d)) instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity be){
|
||||
if(be.getMovedState().is(SuperPistons.HEAD))movingSeen.set(true);if(be.getMovedState().getBlock() instanceof SuperPistonBlocks.Base&&be.isSourcePiston())returnSeen.set(true);
|
||||
}});
|
||||
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.CREATIVE);p.getAbilities().flying=true;p.onUpdateAbilities();p.teleportTo(24,91,5);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
// beta.089 regression: inherited piston predicates must never read absent EXTENDED on helper pieces.
|
||||
c.runOnClient(m->verifyPredicates(m.level));
|
||||
server.runOnServer(s->verifyPredicates(player(s).level()));
|
||||
// Real wrench confirmation on vanilla components; then six orientations for both variants.
|
||||
for(boolean sticky:new boolean[]{false,true})for(var facing:Direction.values()){
|
||||
server.runOnServer(s->{var p=player(s);setup(p,facing,sticky,1);var l=p.level();
|
||||
check(!(l.getBlockEntity(C) instanceof SuperPistonController),"Placing nine pistons does not assemble them");
|
||||
wrench(p,C);check(!(l.getBlockEntity(C) instanceof SuperPistonController),"First click only previews");wrench(p,C);
|
||||
check(l.getBlockEntity(C) instanceof SuperPistonController,"Second click creates controller");
|
||||
camera(p,facing);
|
||||
});
|
||||
c.waitTicks(8);c.getInput().lookAt(C.relative(facing));c.waitTicks(5);
|
||||
if(facing==Direction.NORTH)c.takeScreenshot("piston087-"+(sticky?"sticky":"normal")+"-closed");
|
||||
server.runOnServer(s->power(player(s).level(),facing,true));
|
||||
server.waitFor(s->idle(player(s).level(),3),200);
|
||||
server.runOnServer(s->{var l=player(s).level();for(var pos:SuperPistons.tiles(C,facing,4))check(l.getBlockState(pos).is(Blocks.STONE),"Entire nine-block load moved exactly three blocks: "+facing);
|
||||
for(var pos:SuperPistons.tiles(C,facing,3))check(l.getBlockState(pos).is(SuperPistons.HEAD),"Common head at depth three");});
|
||||
c.waitTicks(7);c.takeScreenshot("piston087-"+(sticky?"sticky":"normal")+"-"+facing.getName());
|
||||
c.runOnClient(m->{
|
||||
for(var pos:SuperPistons.tiles(C,facing,3))check(!m.level.collidesWithSuffocatingBlock(m.player,new AABB(pos)),"Player contact with every head tile is safe: "+facing);
|
||||
for(int depth=1;depth<3;depth++)check(!m.level.collidesWithSuffocatingBlock(m.player,new AABB(C.relative(facing,depth))),"Player contact with rod is safe: "+facing);
|
||||
});
|
||||
server.runOnServer(s->power(player(s).level(),facing,false));server.waitFor(s->idle(player(s).level(),0),200);
|
||||
server.runOnServer(s->{var l=player(s).level();for(var pos:SuperPistons.tiles(C,facing,sticky?1:4))check(l.getBlockState(pos).is(Blocks.STONE),"Correct normal/sticky return: "+facing);
|
||||
for(int depth=1;depth<=3;depth++)for(var pos:SuperPistons.tiles(C,facing,depth))check(!(l.getBlockState(pos).getBlock() instanceof SuperPistonBlocks.Part),"No leftover helper pieces");});
|
||||
}
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,false,12);check(SuperPistons.assemble(p.level(),C,Direction.NORTH,false),"108-load fixture");power(p.level(),Direction.NORTH,true);});
|
||||
server.waitFor(s->idle(player(s).level(),3),200);
|
||||
server.runOnServer(s->{var l=player(s).level();int n=0;for(int d=1;d<=15;d++)for(var pos:SuperPistons.tiles(C,Direction.NORTH,d))if(l.getBlockState(pos).is(Blocks.STONE))n++;check(n==108,"108 blocks conserved after three steps");for(var pos:SuperPistons.tiles(C,Direction.NORTH,15))check(l.getBlockState(pos).is(Blocks.STONE),"Full 12-block deep load moves");});
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,false,12);p.level().setBlockAndUpdate(SuperPistons.tile(C,Direction.NORTH,0).relative(Direction.NORTH,13),Blocks.STONE.defaultBlockState());SuperPistons.assemble(p.level(),C,Direction.NORTH,false);power(p.level(),Direction.NORTH,true);});
|
||||
c.waitTicks(25);server.runOnServer(s->{var l=player(s).level();check(idle(l,0),"109 blocks reject the whole stroke");});
|
||||
// Obstacle at the second step: all nine stop, then resume after removal.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,false,0);SuperPistons.assemble(p.level(),C,Direction.NORTH,false);p.level().setBlockAndUpdate(SuperPistons.tile(C,Direction.NORTH,0).relative(Direction.NORTH,2),Blocks.OBSIDIAN.defaultBlockState());power(p.level(),Direction.NORTH,true);});
|
||||
server.waitFor(s->idle(player(s).level(),1),200);c.waitTicks(15);server.runOnServer(s->{var l=player(s).level();check(idle(l,1),"Obstacle stops every tile at step one");for(var pos:SuperPistons.tiles(C,Direction.NORTH,1))check(l.getBlockState(pos).is(SuperPistons.HEAD),"Head remains aligned");l.removeBlock(SuperPistons.tile(C,Direction.NORTH,0).relative(Direction.NORTH,2),false);});
|
||||
server.waitFor(s->idle(player(s).level(),3),200);
|
||||
// A chest remains untouched and keeps its inventory.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,true,0);var l=p.level();l.setBlockAndUpdate(C.north(),Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(C.north())).setItem(0,new ItemStack(Items.DIAMOND,17));SuperPistons.assemble(l,C,Direction.NORTH,true);power(l,Direction.NORTH,true);});
|
||||
c.waitTicks(20);server.runOnServer(s->{var l=player(s).level();check(idle(l,0)&&((ChestBlockEntity)l.getBlockEntity(C.north())).getItem(0).getCount()==17,"Container remains immovable with its exact contents");});
|
||||
// Adhesion beyond the face, with slime/honey incompatibility.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,true,0);var l=p.level();var corner=SuperPistons.tile(C,Direction.NORTH,8).north();l.setBlockAndUpdate(corner,Blocks.SLIME_BLOCK.defaultBlockState());l.setBlockAndUpdate(corner.east(),Blocks.STONE.defaultBlockState());l.setBlockAndUpdate(corner.above(),Blocks.HONEY_BLOCK.defaultBlockState());SuperPistons.assemble(l,C,Direction.NORTH,true);power(l,Direction.NORTH,true);});
|
||||
server.waitFor(s->idle(player(s).level(),3),200);
|
||||
server.runOnServer(s->{var l=player(s).level();var corner=SuperPistons.tile(C,Direction.NORTH,8).north();check(l.getBlockState(corner.north(3)).is(Blocks.SLIME_BLOCK)&&l.getBlockState(corner.east().north(3)).is(Blocks.STONE),"Slime pulls an adjacent load outside the 3x3 face");check(l.getBlockState(corner.above()).is(Blocks.HONEY_BLOCK),"Honey does not adhere to slime");power(l,Direction.NORTH,false);});
|
||||
server.waitFor(s->idle(player(s).level(),0),200);
|
||||
server.runOnServer(s->{var l=player(s).level();var corner=SuperPistons.tile(C,Direction.NORTH,8).north();check(l.getBlockState(corner).is(Blocks.SLIME_BLOCK)&&l.getBlockState(corner.east()).is(Blocks.STONE),"Sticky return restores attached load");});
|
||||
// Moving platform transports a real entity using vanilla collision.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.UP,false,1);var l=p.level();pig=new net.minecraft.world.entity.animal.pig.Pig(net.minecraft.world.entity.EntityTypes.PIG,l);pig.setNoAi(true);pig.setPos(C.getX()+.5,C.getY()+2,C.getZ()+.5);l.addFreshEntity(pig);SuperPistons.assemble(l,C,Direction.UP,false);power(l,Direction.UP,true);});
|
||||
server.waitFor(s->idle(player(s).level(),3),200);c.waitTicks(5);
|
||||
server.runOnServer(s->{check(pig.getY()>=C.getY()+4.8,"Real entity carried upward three blocks: "+pig.getY());pig.discard();});
|
||||
// Cut power during a step, preserving native in-flight serialization.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,true,1);SuperPistons.assemble(p.level(),C,Direction.NORTH,true);power(p.level(),Direction.NORTH,true);});
|
||||
server.waitFor(s->player(s).level().getBlockEntity(C) instanceof SuperPistonController be&&be.stroke==1&&!be.pending.isEmpty(),200);
|
||||
server.runOnServer(s->{var l=player(s).level();var be=(SuperPistonController)l.getBlockEntity(C);var copy=BlockEntity.loadStatic(C,be.getBlockState(),be.saveWithFullMetadata(l.registryAccess()),l.registryAccess());check(copy instanceof SuperPistonController saved&&saved.stroke==1&&saved.pending.equals(be.pending),"Controller serializes in-flight progress and positions");
|
||||
for(var pos:be.pending)if(l.getBlockEntity(pos) instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity motion){var saved=BlockEntity.loadStatic(pos,motion.getBlockState(),motion.saveWithFullMetadata(l.registryAccess()),l.registryAccess());check(saved instanceof net.minecraft.world.level.block.piston.PistonMovingBlockEntity m&&m.getMovedState().equals(motion.getMovedState()),"Native moving block state serialized");}
|
||||
power(l,Direction.NORTH,false);});
|
||||
server.waitFor(s->idle(player(s).level(),0),200);
|
||||
server.runOnServer(s->{for(var pos:SuperPistons.tiles(C,Direction.NORTH,1))check(player(s).level().getBlockState(pos).is(Blocks.STONE),"Interrupted sticky cycle keeps cargo");});
|
||||
// Unloaded loads suspend instead of detaching a sticky piston; no forced chunk request.
|
||||
server.runOnServer(s->{var l=player(s).level();var far=new BlockPos(300000,85,300000);boolean suspended=false;
|
||||
try{SuperPistonPlan.resolve(l,List.of(far),Direction.NORTH,false,Set.of());}catch(SuperPistonPlan.Suspended expected){suspended=true;}
|
||||
check(suspended&&!l.hasChunkAt(far),"Unloaded pull suspends without loading chunks");
|
||||
check(SuperPistonPlan.resolve(l,List.of(C.atY(l.getMaxY()+1)),Direction.UP,true,Set.of())==null,"World height blocks movement");});
|
||||
// Native breakable container drops one box with its inventory, never loose duplicate cargo.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,false,0);var l=p.level();l.setBlockAndUpdate(C.north(),Blocks.SHULKER_BOX.defaultBlockState());((ShulkerBoxBlockEntity)l.getBlockEntity(C.north())).setItem(0,new ItemStack(Items.DIAMOND,17));SuperPistons.assemble(l,C,Direction.NORTH,false);power(l,Direction.NORTH,true);});
|
||||
server.waitFor(s->idle(player(s).level(),3),200);
|
||||
server.runOnServer(s->{var l=player(s).level();var boxes=l.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,new AABB(C).inflate(20),e->e.getItem().is(Items.SHULKER_BOX));check(boxes.size()==1,"One broken shulker box dropped");var contents=boxes.getFirst().getItem().get(DataComponents.CONTAINER);check(contents!=null&&contents.nonEmptyItemCopyStream().filter(i->i.is(Items.DIAMOND)).mapToInt(ItemStack::getCount).sum()==17,"Shulker contents preserved");boxes.forEach(net.minecraft.world.entity.Entity::discard);});
|
||||
// The four-pixel front belongs to the moving head, including all six collision orientations.
|
||||
server.runOnServer(s->{var l=player(s).level();for(var f:Direction.values()){
|
||||
var state=SuperPistons.base(f,0,false,true);var bounds=state.getCollisionShape(l,C).bounds();double thickness=bounds.max(f.getAxis())-bounds.min(f.getAxis());check(Math.abs(thickness-.75)<.00001,"Extended base keeps twelve pixels: "+f);
|
||||
var head=SuperPistons.headForBase(state,1).getCollisionShape(l,C).bounds();check(Math.abs(head.max(f.getAxis())-head.min(f.getAxis())-.25)<.00001,"Moving plate has four pixels: "+f);
|
||||
}});
|
||||
for(boolean sticky:new boolean[]{false,true}){
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,sticky,0);SuperPistons.assemble(p.level(),C,Direction.NORTH,sticky);p.teleportTo(C.getX()+6.5,C.getY()+3,C.getZ()-6.5);});
|
||||
c.waitTicks(8);c.getInput().lookAt(C);c.waitTicks(4);c.takeScreenshot("piston087-"+(sticky?"sticky":"normal")+"-face-closed");
|
||||
server.runOnServer(s->power(player(s).level(),Direction.NORTH,true));server.waitFor(s->idle(player(s).level(),3),200);c.waitTicks(6);c.takeScreenshot("piston087-"+(sticky?"sticky":"normal")+"-face-open");
|
||||
}
|
||||
// Loaded structure after real chunk unload and return; break/dismantle cleanup.
|
||||
server.runOnServer(s->{var p=player(s);setup(p,Direction.NORTH,false,0);SuperPistons.assemble(p.level(),C,Direction.NORTH,false);power(p.level(),Direction.NORTH,true);camera(p,Direction.NORTH);});
|
||||
server.waitFor(s->idle(player(s).level(),3),200);
|
||||
server.runOnServer(s->player(s).teleportTo(400.5,90,400.5));c.waitFor(m->m.level.getChunkSource().getChunkNow(C.getX()>>4,C.getZ()>>4)==null,600);
|
||||
server.runOnServer(s->camera(player(s),Direction.NORTH));c.waitFor(m->m.level.getChunkSource().getChunkNow(C.getX()>>4,C.getZ()>>4)!=null&&m.level.getBlockState(C).is(SuperPistons.NORMAL),600);server.waitFor(s->idle(player(s).level(),3),200);
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(SuperPistons.tile(C,Direction.NORTH,0),true);});c.waitTicks(15);
|
||||
server.runOnServer(s->{var l=player(s).level();for(int i=1;i<9;i++)check(l.getBlockState(SuperPistons.tile(C,Direction.NORTH,i)).is(Blocks.PISTON),"Broken structure restores remaining eight components");for(int d=1;d<=3;d++)for(var pos:SuperPistons.tiles(C,Direction.NORTH,d))check(l.getBlockState(pos).isAir(),"Head and rod removed after break");});
|
||||
check(movingSeen.get(),"Client receives moving head data");check(returnSeen.get(),"Client receives final retraction data");
|
||||
System.out.println("PISTON087_PASS: all-state suffocation/view/spawn/conductor predicates, client player collision on every head tile and rod, six directions x two variants, 108/109 load, obstacles, inventories, adhesion, entity collision, interruption, serialization, chunk reload and dismantle");
|
||||
}
|
||||
}
|
||||
private static void verifyPredicates(net.minecraft.world.level.BlockGetter level){
|
||||
for(var block:List.of(SuperPistons.NORMAL,SuperPistons.GLUANT,SuperPistons.HEAD,SuperPistons.ROD))for(var state:block.getStateDefinition().getPossibleStates()){
|
||||
boolean closed=state.getBlock() instanceof SuperPistonBlocks.Base&&!state.getValue(net.minecraft.world.level.block.piston.PistonBaseBlock.EXTENDED);
|
||||
check(state.isSuffocating(level,C)==closed,"Only retracted bases suffocate: "+state);
|
||||
check(!state.isRedstoneConductor(level,C),"Piston pieces do not conduct through their volume");
|
||||
state.isViewBlocking(level,C,new AABB(C));state.isValidSpawn(level,C,net.minecraft.world.entity.EntityTypes.PIG);
|
||||
}
|
||||
}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void camera(ServerPlayer p,Direction f){var u=SuperPistons.u(f);var v=SuperPistons.v(f);p.teleportTo(C.getX()+.5+f.getStepX()*9+u.getStepX()*7+v.getStepX()*5,C.getY()+.5+f.getStepY()*9+u.getStepY()*7+v.getStepY()*5,C.getZ()+.5+f.getStepZ()*9+u.getStepZ()*7+v.getStepZ()*5);}
|
||||
private static void power(ServerLevel l,Direction f,boolean on){l.setBlockAndUpdate(SuperPistons.tile(C,f,0).relative(f,-1),(on?Blocks.REDSTONE_BLOCK:Blocks.AIR).defaultBlockState());}
|
||||
private static boolean idle(ServerLevel l,int n){return l.getBlockEntity(C) instanceof SuperPistonController be&&be.stroke==n&&be.pending.isEmpty();}
|
||||
private static void setup(ServerPlayer p,Direction f,boolean sticky,int layers){
|
||||
var l=p.level();p.closeContainer();p.setShiftKeyDown(false);p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Multiblocks.WRENCH));
|
||||
for(var d:Direction.values())for(boolean glue:new boolean[]{false,true})SuperPistons.dismantle(l,C,d,glue);
|
||||
for(var d:Direction.values())for(int depth=-16;depth<=16;depth++)for(var pos:SuperPistons.tiles(C,d,depth))if(!l.getBlockState(pos).isAir())l.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());
|
||||
for(int i=0;i<9;i++){var pos=SuperPistons.tile(C,f,i);l.setBlockAndUpdate(pos,(sticky?Blocks.STICKY_PISTON:Blocks.PISTON).defaultBlockState().setValue(net.minecraft.world.level.block.piston.PistonBaseBlock.FACING,f));}
|
||||
for(int depth=1;depth<=layers;depth++)for(var pos:SuperPistons.tiles(C,f,depth))l.setBlockAndUpdate(pos,Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
private static void wrench(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
|
||||
private static net.minecraft.client.input.KeyEvent enter(){return new net.minecraft.client.input.KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import java.util.stream.Stream;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real client resource/model capture, widgets, previews and creative upload in a new flat world. */
|
||||
public final class Plans072ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Plans072 "+UUID.randomUUID());s.setSeed("72");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1800);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(GameType.CREATIVE);p.teleportTo(0.5,10,0.5);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set day");
|
||||
});
|
||||
c.waitFor(m->m.player.isCreative(),100);c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
long started=System.nanoTime();var supported=new ArrayList<String>();var refused=new ArrayList<String>();
|
||||
c.runOnClient(m->{
|
||||
var palette=new StatuePaletteUnchecked();
|
||||
require(palette.value.nearest(0xFFFFFFFF)!=null,"Active resource textures produce a block palette");
|
||||
for(var item:BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem){
|
||||
var type=SpawnEggItem.getType(item.getDefaultInstance());String id=BuiltInRegistries.ENTITY_TYPE.getKey(type).toString();
|
||||
if(supported.contains(id)||refused.stream().anyMatch(v->v.startsWith(id+" ")))continue;
|
||||
try{
|
||||
var entity=type.create(m.level,EntitySpawnReason.MOB_SUMMONED);entity.setPos(m.player.position());entity.setId(Integer.MIN_VALUE+73);
|
||||
var faces=StatueModels.capture(entity);require(faces.size()>=6,"Native geometry exists: "+id);supported.add(id);
|
||||
}catch(Exception e){refused.add(id+" "+e.getMessage());}
|
||||
}
|
||||
});
|
||||
require(supported.containsAll(List.of("minecraft:creeper","minecraft:cow","minecraft:pig","minecraft:chicken","minecraft:zombie","minecraft:sheep")),"Common native models are supported");
|
||||
System.out.println("PLANS072 models="+supported.size()+" refused="+refused+" capture_ms="+(System.nanoTime()-started)/1_000_000);
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);
|
||||
c.takeScreenshot("plans072-create-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter()));
|
||||
c.waitFor(m->PlanClient.plan!=null&&!PlanClient.busy,400);
|
||||
c.runOnClient(m->{require(PlanClient.plan.height()==32,"Requested model height");require(PlanClient.plan.cells().size()>100,"Detailed geometry becomes real material cells");
|
||||
PlanClient.origin=new BlockPos(10,2,10);PlanClient.refresh();
|
||||
});
|
||||
c.takeScreenshot("plans072-plan-en");
|
||||
var expected=new AtomicReference<BuildPlan>();c.runOnClient(m->expected.set(PlanClient.plan));
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.rotate").onPress(enter()));
|
||||
c.runOnClient(m->{for(int i=0;i<3;i++)PlanClient.rotate();require(PlanClient.plan.equals(expected.get()),"UI and native rotations return to exact original plan");});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.materials").onPress(enter()));c.takeScreenshot("plans072-materials-en");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.back_plan").onPress(enter()));
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.export").onPress(enter()));c.waitFor(m->!PlanClient.busy,200);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(15.5,15,55.5));
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitFor(m->m.player.distanceToSqr(15.5,15,55.5)<2,100);
|
||||
c.getInput().lookAt(new BlockPos(15,18,17));c.waitTicks(15);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);c.takeScreenshot("plans072-ghost-creeper");c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.paste").onPress(enter()));c.waitForScreen(ConfirmScreen.class);
|
||||
c.clickScreenButton("gui.yes");
|
||||
c.waitFor(m->PlanClient.completed==PlanClient.plan.cells().size(),400);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(var cell:expected.get().cells())require(p.level().getBlockState(new BlockPos(10,2,10).offset(cell.pos()))==cell.state(),"Server pasted every exact material");});
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.getInput().pressKey(InputConstants.KEY_F1);c.waitTicks(10);c.takeScreenshot("plans072-built-creeper");c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
// A real survival connection must reject an upload, even when sent without the GUI.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(GameType.SURVIVAL);p.getAbilities().invulnerable=true;});
|
||||
c.waitFor(m->!m.player.isCreative(),100);
|
||||
c.runOnClient(m->{var tiny=new BuildPlan("Survival",1,1,1,List.of(new BuildPlan.Cell(BlockPos.ZERO,Blocks.STONE.defaultBlockState())));PlanClient.select(tiny,Map.of());PlanClient.origin=new BlockPos(4,2,4);PlanClient.refresh();
|
||||
ClientPlayNetworking.send(new PlanPayload(UUID.randomUUID(),PlanClient.origin,1,1,1,1,0,tiny.cells()));});
|
||||
c.waitTicks(30);server.runOnServer(s->require(s.getPlayerList().getPlayers().getFirst().level().getBlockState(new BlockPos(4,2,4)).isAir(),"Survival spoofed paste leaves the world untouched"));
|
||||
c.getInput().pressKey(InputConstants.KEY_K);c.waitForScreen(PlansScreen.class);
|
||||
c.runOnClient(m->require(widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).equals("sanctuary.plans.paste")),"Survival UI only offers construction guidance"));
|
||||
language(c,"fr_fr");c.takeScreenshot("plans072-survie-fr");
|
||||
c.runOnClient(m->{PlanClient.layer=0;PlanClient.refresh();require(PlanClient.completed==0,"Free preview does not complete a block");});
|
||||
// Ordinary native placement updates the plan and consumes the held material.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Items.STONE,2));
|
||||
for(int x=3;x<=5;x++)for(int z=2;z<=5;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
p.level().setBlockAndUpdate(new BlockPos(4,1,4),Blocks.STONE.defaultBlockState());p.teleportTo(4.5,1,3.5);});
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
c.waitFor(m->m.player.distanceToSqr(4.5,1,3.5)<1&&m.player.getMainHandItem().is(Items.STONE),150);
|
||||
c.getInput().lookAt(new BlockPos(4,1,4));c.waitTicks(3);
|
||||
c.runOnClient(m->System.out.println("PLANS072 survival hit="+m.hitResult+" player="+m.player.position()+" origin="+PlanClient.origin+" held="+m.player.getMainHandItem()));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
|
||||
c.waitFor(m->PlanClient.completed==1,150);
|
||||
server.runOnServer(s->require(s.getPlayerList().getPlayers().getFirst().getMainHandItem().getCount()==1,"Survival native placement consumes exactly one block"));
|
||||
c.runOnClient(m->{PlanClient.clear();m.gui.setScreen(new PlansScreen(null));});c.takeScreenshot("plans072-create-fr");
|
||||
language(c,"en_us");
|
||||
System.out.println("PLANS072 passed: native models + UV textures + active palette, creation widgets, 32-block statue, rotations, materials, gzip export, colored preview, real creative network paste, survival spoof rejection and paid native placement, FR/EN.");
|
||||
}
|
||||
}
|
||||
private static void language(ClientGameTestContext c,String id){
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected(id);return m.reloadResourcePacks();});
|
||||
c.waitFor(m->reload.isDone(),600);reload.join();c.waitFor(m->m.gui.overlay()==null,400);c.waitTicks(8);
|
||||
}
|
||||
private static final class StatuePaletteUnchecked {final StatuePalette value;StatuePaletteUnchecked(){try{value=new StatuePalette("mixed");}catch(Exception e){throw new IllegalStateException(e);}}}
|
||||
private static Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler child?Stream.concat(Stream.of(w),widgets(child)):Stream.of(w));}
|
||||
private static String key(Button b){return b.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static Button button(Screen s,String key){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals(key)).findFirst().orElseThrow(()->new AssertionError("Button missing: "+key));}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void require(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.Arrays;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.packs.PackSelectionScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.repository.PackCompatibility;
|
||||
|
||||
/** Native default selection, actual resource priority, GUI atlas rendering and player opt-out. */
|
||||
public final class ResourcePack070ClientChecks implements FabricClientGameTest {
|
||||
private static final String ID="sanctuary:textures";
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var previous=c.computeOnClient(m->m.gui.screen());
|
||||
c.runOnClient(m->{
|
||||
var repo=m.getResourcePackRepository();
|
||||
check(repo.getAvailableIds().contains(ID),"Built-in pack available: "+repo.getAvailableIds());
|
||||
check(repo.getSelectedIds().contains(ID),"Pack enabled at startup without changing options");
|
||||
check(repo.getPack(ID).getCompatibility()==PackCompatibility.COMPATIBLE,"Format accepted by exact Minecraft version");
|
||||
for(String state:new String[]{"empty","half","full","empty_hunger","half_hunger","full_hunger"})
|
||||
resource(m,"minecraft:textures/gui/sprites/hud/food_"+state+".png");
|
||||
resource(m,"sanctuary:textures/gui/food_details.png");
|
||||
check(!m.getResourceManager().getResource(Identifier.withDefaultNamespace("textures/gui/sprites/hud/air.png")).orElseThrow().sourcePackId().equals(ID),"Breathing keeps native resources");
|
||||
});
|
||||
c.takeScreenshot("resourcepack070-title-default");
|
||||
c.runOnClient(m->m.gui.setScreen(new PackSelectionScreen(m.getResourcePackRepository(),repo->{},m.getResourcePackDirectory(),Component.translatable("resourcePack.title"))));
|
||||
c.waitTicks(4);c.takeScreenshot("resourcepack070-selected");
|
||||
c.runOnClient(m->m.gui.setScreen(new Screen(Component.literal("Resource pack preview")) {
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g,int x,int y,float delta) {
|
||||
g.fill(0,0,width,height,0xff171717);
|
||||
g.centeredText(font,Component.translatable("sanctuary.resource_pack.description","beta.070"),width/2,35,0xffffffff);
|
||||
String[] states={"empty","half","full","empty_hunger","half_hunger","full_hunger"};
|
||||
for(int i=0;i<states.length;i++)g.blitSprite(RenderPipelines.GUI_TEXTURED,Identifier.withDefaultNamespace("hud/food_"+states[i]),width/2-63+i*22,75,18,18);
|
||||
for(int i=0;i<4;i++)g.blit(RenderPipelines.GUI_TEXTURED,Identifier.parse("sanctuary:textures/gui/food_details.png"),width/2-36+i*22,115,i*9,0,18,18,9,9,256,256);
|
||||
}
|
||||
}));
|
||||
c.waitTicks(4);c.takeScreenshot("resourcepack070-food-native-atlas");
|
||||
c.runOnClient(m->{
|
||||
var repo=m.getResourcePackRepository();repo.removePack(ID);m.options.updateResourcePacks(repo);
|
||||
});
|
||||
c.waitFor(m->!source(m).equals(ID),1000);
|
||||
c.runOnClient(m->{
|
||||
var repo=m.getResourcePackRepository();repo.reload();m.options.loadSelectedResourcePacks(repo);
|
||||
check(!repo.getSelectedIds().contains(ID),"Explicit disable survives pack rediscovery");
|
||||
repo.addPack(ID);m.options.updateResourcePacks(repo);
|
||||
});
|
||||
c.waitFor(m->source(m).equals(ID),1000);
|
||||
c.runOnClient(m->m.gui.setScreen(previous));
|
||||
System.out.println("RESOURCEPACK070_PASS: default activation, exact imported textures override native assets, format compatible, breathing vanilla, disable retained and re-enable works");
|
||||
}
|
||||
private static String source(Minecraft m) {
|
||||
return m.getResourceManager().getResource(Identifier.withDefaultNamespace("textures/gui/sprites/hud/food_full.png")).orElseThrow().sourcePackId();
|
||||
}
|
||||
private static void resource(Minecraft m,String name) {
|
||||
var id=Identifier.parse(name);var resource=m.getResourceManager().getResource(id).orElseThrow();
|
||||
check(resource.sourcePackId().equals(ID),"Active resource priority: "+name+" from "+resource.sourcePackId());
|
||||
var path=FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow().findPath("resourcepacks/textures/assets/"+id.getNamespace()+"/"+id.getPath()).orElseThrow();
|
||||
try(var in=resource.open()) {check(Arrays.equals(in.readAllBytes(),java.nio.file.Files.readAllBytes(path)),"Resolved texture bytes: "+name);}
|
||||
catch(java.io.IOException error){throw new AssertionError(error);}
|
||||
}
|
||||
private static void check(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.companion.CompanionService;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.ProgressionService;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real single-player orders without operator permissions or purchased familiar abilities. */
|
||||
public final class Solo079ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
var messages=ConcurrentHashMap.<String>newKeySet();
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(FamiliarClient.view!=null&&!FamiliarClient.view.message().isEmpty())messages.add(FamiliarClient.view.message());
|
||||
if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
button(screen,"sanctuary.progression.enter").onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Solo079 "+UUID.randomUUID());s.setSeed("79");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();var target=new AtomicReference<Mob>();
|
||||
server.runOnServer(s->{var p=player(s);check(!ProgressionService.canOperate(p),"Normal solo has no operator permissions");check(!CompanionService.record(p).unlocked(),"No familiar active ability bought");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set night");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"difficulty normal");
|
||||
for(int x=-18;x<=18;x++)for(int z=-18;z<=18;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
p.getInventory().clearContent();p.teleportTo(.5,1,.5);equip(p,Items.WOLF_SPAWN_EGG,FamiliarPersonality.PASSIVE);
|
||||
});c.waitTicks(15);
|
||||
// The actual held-H menu sends recall and follow through the play connection.
|
||||
order(c,"recall");c.waitTicks(12);
|
||||
server.runOnServer(s->check(FamiliarBattle.bond(player(s)).recalled()&&FamiliarService.active(player(s))==null,"Held H -> Recall actually removes the familiar"));
|
||||
order(c,"follow");c.waitTicks(20);
|
||||
server.runOnServer(s->check(!FamiliarBattle.bond(player(s)).recalled()&&FamiliarService.active(player(s))!=null,"Follow summons the recalled familiar"));
|
||||
order(c,"protect");c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.state(p).order==FamiliarBattle.Order.PROTECT&&p.getUUID().equals(FamiliarBattle.state(p).protectedEntity),"Held H -> Protect establishes a personal guard without operator rights");});
|
||||
for(var item:new Item[]{Items.WOLF_SPAWN_EGG,Items.SLIME_SPAWN_EGG,Items.CHICKEN_SPAWN_EGG}) {
|
||||
server.runOnServer(s->{var p=player(s);equip(p,item,FamiliarPersonality.PASSIVE);p.teleportTo(.5,1,.5);FamiliarService.active(p).setPos(1.5,1,.5);target.set(enemy(p,.5,1,6.5));});c.waitTicks(15);
|
||||
c.getInput().lookAt(new BlockPos(0,2,6));c.waitTicks(5);order(c,"engage");c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarBattle.target(p)==target.get()&&FamiliarBattle.state(p).explicitTarget,"Held H -> Attack gives a target: "+item);});
|
||||
c.waitTicks(200);
|
||||
server.runOnServer(s->{var p=player(s);var st=FamiliarBattle.state(p);var pet=FamiliarService.active(p);check(target.get().getHealth()<100&&target.get().getLastHurtByMob()==pet,"Commanded familiar actually damages enemy: "+item+" pet="+pet.position()+" target="+target.get().position()+" current="+st.target+" next="+st.nextInitiative+" tick="+p.tickCount+" cast="+(st.cast==null?"none":st.cast.age)+" sight="+pet.hasLineOfSight(target.get()));target.get().discard();FamiliarBattle.cancel(p);});
|
||||
}
|
||||
// Follow pauses spontaneous pursuit, never an immediate response to real damage.
|
||||
for(var item:new Item[]{Items.WOLF_SPAWN_EGG,Items.SLIME_SPAWN_EGG,Items.CHICKEN_SPAWN_EGG}) {
|
||||
server.runOnServer(s->{var p=player(s);equip(p,item,FamiliarPersonality.PASSIVE);p.setHealth(p.getMaxHealth());p.teleportTo(.5,1,.5);FamiliarService.active(p).setPos(1.5,1,.5);target.set(enemy(p,5.5,1,.5));
|
||||
FamiliarBattle.command(p,"follow");
|
||||
p.hurtServer(p.level(),p.damageSources().mobAttack(target.get()),1);
|
||||
check(FamiliarBattle.target(p)==target.get(),"Passive familiar defends non-operator solo player immediately after Follow: "+item);
|
||||
});c.waitTicks(200);
|
||||
server.runOnServer(s->{check(target.get().getHealth()<100&&target.get().getLastHurtByMob()==FamiliarService.active(player(s)),"Autonomous defence really lands a hit: "+item);target.get().discard();});
|
||||
}
|
||||
check(messages.containsAll(java.util.Set.of("recalled","order.following","order.attacking","order.protecting")),"Accepted menu orders have client feedback: "+messages);
|
||||
c.takeScreenshot("solo079-defence");System.out.println("SOLO079_PASS: no operator, no purchased ability, held-H menu, wolf/slime/chicken damage, real defence");
|
||||
} finally {intro.set(false);c.getInput().releaseKey(InputConstants.KEY_H);}
|
||||
}
|
||||
private static void order(ClientGameTestContext c,String action) {
|
||||
c.getInput().holdKey(InputConstants.KEY_H);c.waitFor(m->m.gui.screen() instanceof FamiliarOrdersScreen,100);c.getInput().releaseKey(InputConstants.KEY_H);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.familiar."+action).onPress(enter()));
|
||||
}
|
||||
private static Button button(net.minecraft.client.gui.screens.Screen screen,String key) {
|
||||
return screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();
|
||||
}
|
||||
private static void equip(ServerPlayer p,Item item,FamiliarPersonality personality) {
|
||||
p.stopRiding();CarryService.detach(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.removeAttached(FamiliarBattle.COMBAT_LOCK);p.removeAttached(FamiliarBattle.LAST_FAMILIAR);
|
||||
var egg=new ItemStack(item);var scale=new CompoundTag();scale.putInt("schema",1);scale.putInt("per_mille",1000);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,scale));
|
||||
var profile=FamiliarBattle.world(p.level().getServer()).catalog.forEgg(egg);UUID id;do{id=UUID.randomUUID();}while(FamiliarPersonality.of(id,profile.id())!=personality);
|
||||
FamiliarBattle.world(p.level().getServer()).store.put(FamiliarBond.create(id,profile,0,"test",p.getUUID()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,FamiliarIdentity.assign(egg,id));
|
||||
CompanionService.tick(p);p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);check(FamiliarService.active(p)!=null,"Familiar spawned: "+item);p.inventoryMenu.broadcastChanges();
|
||||
}
|
||||
private static Mob enemy(ServerPlayer p,double x,double y,double z){var e=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);e.setNoAi(true);e.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);e.setHealth(100);e.setPos(x,y,z);p.level().addFreshEntity(e);return e;}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean condition,String message){if(!condition)throw new AssertionError(message);}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.realtime.RealtimeService;
|
||||
import fr.koka.sanctuary.weather.*;
|
||||
import java.nio.file.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Operator command tree, actual chat packets, daily persistence and native atmosphere. */
|
||||
public final class Weather082ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);byte[] prior=readConfig();
|
||||
checked(()->{Files.createDirectories(WeatherService.CONFIG.getParent());Files.writeString(WeatherService.CONFIG,WeatherConfig.DEFAULT.json());});
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Weather082 "+UUID.randomUUID());s.setSeed("82");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->checked(()->{
|
||||
at(s,Instant.parse("2026-09-16T10:00:00Z"));WeatherService.reload(s);
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;
|
||||
var commands=s.getCommands().getDispatcher();var root=commands.getRoot();var sanctuary=root.getChild("sanctuary").getChild("weather");var nativeWeather=root.getChild("weather");
|
||||
s.getWorldData().setAllowCommands(false);var source=p.createCommandSourceStack();var before=WeatherService.session(s).state();
|
||||
for(var kind:DailyWeather.Kind.values()) {check(!sanctuary.getChild(kind.key()).canUse(source),"Operator choice hidden without Allow Commands: "+kind);refused(()->commands.execute("sanctuary weather "+kind.key(),source));}
|
||||
for(var kind:List.of(DailyWeather.Kind.OVERCAST,DailyWeather.Kind.FOG,DailyWeather.Kind.SHOWERS)){check(!nativeWeather.getChild(kind.key()).canUse(source),"Native alias hidden for non-operator");refused(()->commands.execute("weather "+kind.key(),source));}
|
||||
check(WeatherService.session(s).state().equals(before),"Rejected commands cannot alter weather");
|
||||
s.getCommands().sendCommands(p);
|
||||
}));
|
||||
c.waitFor(m->m.player.connection.getCommands().getRoot().getChild("sanctuary").getChild("weather").getChild("fog")==null,200);
|
||||
server.runOnServer(s->{s.getWorldData().setAllowCommands(true);s.getCommands().sendCommands(s.getPlayerList().getPlayers().getFirst());});
|
||||
c.waitFor(m->{var weather=m.player.connection.getCommands().getRoot().getChild("weather");return weather!=null&&weather.getChild("fog")!=null;},200);
|
||||
c.runOnClient(m->{var root=m.player.connection.getCommands().getRoot();for(String key:List.of("clear","rain","thunder","overcast","fog","showers"))check(root.getChild("weather").getChild(key)!=null&&root.getChild("sanctuary").getChild("weather").getChild(key)!=null,"All six profiles available to operator completion: "+key);});
|
||||
server.runOnServer(s->checked(()->{s.getCommands().getDispatcher().execute("sanctuary time vanilla",s.createCommandSourceStack());WeatherService.setMode(s,false);}));
|
||||
c.waitFor(m->!WeatherClient.state().active(),200);
|
||||
// All six choices go over the player's real chat-command connection.
|
||||
for(var kind:List.of(DailyWeather.Kind.OVERCAST,DailyWeather.Kind.FOG,DailyWeather.Kind.SHOWERS,DailyWeather.Kind.RAIN,DailyWeather.Kind.THUNDER,DailyWeather.Kind.CLEAR)) {
|
||||
c.runOnClient(m->m.player.connection.sendCommand("sanctuary weather "+kind.key()));
|
||||
c.waitFor(m->WeatherClient.state().active()&&WeatherClient.state().kind()==kind,200);
|
||||
server.runOnServer(s->{var now=RealtimeService.civilTime(s);var state=WeatherService.session(s).state();var weather=s.overworld().getWeatherData();check(state.weather()==kind&&state.realtime()&&state.date()==now.toLocalDate().toEpochDay(),"Chosen civil-day profile committed: "+kind);check(weather.isRaining()==DailyWeather.raining(kind,s.overworld().getSeed(),now)&&weather.isThundering()==kind.thundering(),"Native precipitation matches "+kind);check(!RealtimeService.active(s.overworld()),"Weather choice never changes solar mode");});
|
||||
}
|
||||
for(var kind:List.of(DailyWeather.Kind.OVERCAST,DailyWeather.Kind.FOG,DailyWeather.Kind.SHOWERS)){
|
||||
c.runOnClient(m->m.player.connection.sendCommand("weather "+kind.key()));c.waitFor(m->WeatherClient.state().kind()==kind,200);
|
||||
if(kind==DailyWeather.Kind.FOG){c.waitTicks(205);c.runOnClient(m->check(m.level.environmentAttributes().getDimensionValue(EnvironmentAttributes.FOG_END_DISTANCE)<=80.01,"Commanded fog reaches native rendering"));}
|
||||
}
|
||||
server.runOnServer(s->checked(()->{
|
||||
var path=s.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-weather.json");var before=WeatherService.session(s).state();byte[] bytes=Files.readAllBytes(path);
|
||||
WeatherService.reload(s);check(WeatherService.session(s).state().equals(before)&&Arrays.equals(bytes,Files.readAllBytes(path)),"Reload retains chosen profile without rewriting it");
|
||||
check(s.getCommands().getDispatcher().execute("weather showers",s.createCommandSourceStack())==1&&Arrays.equals(bytes,Files.readAllBytes(path)),"Console alias is idempotent");
|
||||
var current=RealtimeService.civilTime(s);var midnight=current.toLocalDate().plusDays(1).atStartOfDay(current.getZone());at(s,midnight.plusSeconds(1).toInstant());
|
||||
}));server.waitFor(s->WeatherService.session(s).state().date()==RealtimeService.civilTime(s).toLocalDate().toEpochDay(),200);
|
||||
server.runOnServer(s->checked(()->{
|
||||
var now=RealtimeService.civilTime(s);check(WeatherService.session(s).state().date()==now.toLocalDate().toEpochDay()&&WeatherService.session(s).state().weather()==DailyWeather.at(s.overworld().getSeed(),now.toLocalDate(),WeatherConfig.DEFAULT),"Daily forecast resumes on the next civil date");
|
||||
var commands=s.getCommands().getDispatcher();var source=s.createCommandSourceStack();commands.execute("sanctuary weather vanilla",source);commands.execute("weather rain 600",source);check(s.overworld().getWeatherData().isRaining(),"Native weather commands remain available in vanilla mode");
|
||||
commands.execute("weather fog",source);check(WeatherService.active(s.overworld())&&WeatherService.session(s).state().weather()==DailyWeather.Kind.FOG,"Alias activates weather from vanilla mode");
|
||||
Files.writeString(WeatherService.CONFIG,new WeatherConfig(false,15,5,10,3,2).json());WeatherService.reload(s);var before=WeatherService.session(s).state();
|
||||
check(commands.execute("weather overcast",source)==0&&WeatherService.session(s).state().equals(before)&&!WeatherService.active(s.overworld()),"Disabled weather configuration refuses choices");
|
||||
Files.writeString(WeatherService.CONFIG,WeatherConfig.DEFAULT.json());WeatherService.reload(s);
|
||||
var path=s.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-weather.json");byte[] bytes=Files.readAllBytes(path);Files.writeString(path,"{external-conflict");var retained=WeatherService.session(s).state();
|
||||
check(commands.execute("weather overcast",source)==0&&WeatherService.session(s).state().equals(retained)&&Files.readString(path).equals("{external-conflict"),"Save conflict refuses weather change before native side effects");Files.write(path,bytes);
|
||||
}));
|
||||
for(String language:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());check(!WeatherService.label("selected",WeatherService.label("fog"),"2026-09-16","Europe/Paris").getString().contains("sanctuary.weather."),"Localized confirmation "+language);});
|
||||
System.out.println("WEATHER082_PASS: player commands, operator completion and denials, six choices, native aliases, client fog, current-day persistence, midnight rollover, vanilla return, config disable, atomic save refusal, FR/EN");
|
||||
}finally{intro.set(false);checked(()->{if(prior==null)Files.deleteIfExists(WeatherService.CONFIG);else Files.write(WeatherService.CONFIG,prior);});}
|
||||
}
|
||||
private static byte[] readConfig(){try{return Files.exists(WeatherService.CONFIG)?Files.readAllBytes(WeatherService.CONFIG):null;}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void at(MinecraftServer s,Instant instant)throws Exception{var field=RealtimeService.Session.class.getDeclaredField("clock");field.setAccessible(true);field.set(RealtimeService.session(s),Clock.fixed(instant,ZoneOffset.UTC));RealtimeService.tick(s);}
|
||||
private interface Checked{void run()throws Exception;}
|
||||
private static void checked(Checked task){try{task.run();}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void refused(Checked task)throws Exception{try{task.run();throw new AssertionError("Non-operator command accepted");}catch(CommandSyntaxException expected){}}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.progression.ProgressionPayloads;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
|
||||
/** Native background captures, with the complete original score after a separate welcome fade. */
|
||||
public final class Welcome083ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var previous=c.computeOnClient(m->m.gui.screen());
|
||||
var clock=new AtomicLong();var completed=new AtomicInteger();
|
||||
var uuid=UUID.fromString("01234567-89ab-cdef-fedc-ba9876543210");
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,800);m.options.guiScale().set(2);m.resizeGui();
|
||||
m.gui.setScreen(new HelloWorldScreen(new ProgressionPayloads.Hello("Sanctuary",List.of(0x55AAFF,0xFFAA55),""),()->{}));});
|
||||
c.waitTicks(3);c.takeScreenshot("welcome083-gray");
|
||||
c.runOnClient(m->m.gui.setScreen(new IntroScreen(uuid,skipped->completed.incrementAndGet(),clock::get,true)));
|
||||
for(int millis:new int[]{0,250,500,750,1000,4000,24500,30000}) {
|
||||
clock.set(millis*1_000_000L);c.waitTicks(2);c.takeScreenshot("welcome083-intro-"+millis);
|
||||
if(millis<24500)c.runOnClient(m->check(((IntroScreen)m.gui.screen()).waitingForMusic(),"Music still waits for portal reveal"));
|
||||
if(millis>=24500)c.runOnClient(m->check(!((IntroScreen)m.gui.screen()).waitingForMusic(),"Music begins with portal"));
|
||||
check(completed.get()==0,"Original 29.5-second intro is not shortened by welcome fade");
|
||||
}
|
||||
clock.set(30_600_000_000L);c.waitTicks(3);
|
||||
check(completed.get()==1,"Completion exactly once after fade plus full sequence");
|
||||
c.runOnClient(m->m.gui.screen().onClose());check(completed.get()==1,"Closing cannot duplicate completion");
|
||||
clock.set(0);c.runOnClient(m->m.gui.setScreen(new IntroScreen(uuid,skipped->completed.incrementAndGet(),clock::get)));
|
||||
c.waitTicks(2);c.takeScreenshot("welcome083-replay-black");
|
||||
c.runOnClient(m->m.gui.setScreen(previous));check(completed.get()==1,"Replacing replay never acknowledges admission");
|
||||
System.out.println("WELCOME083_PASS: plain welcome gray, monotonic one-second gray-to-black fade, complete intro, portal music, black replay, no duplicate completion");
|
||||
}
|
||||
private static void check(boolean value,String reason) {if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.boats.*;
|
||||
import fr.koka.sanctuary.client.TiledBoatModel;
|
||||
import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.player.Input;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Actual native block shapes, vehicle movement and networked client controls in a development world. */
|
||||
public final class Boat065ClientChecks implements FabricClientGameTest {
|
||||
private static final double EPS=1.0E-5;
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Boat065 "+UUID.randomUUID());s.setSeed("65");s.setAllowCommands(true);s.getGameRules().set(SanctuaryGameplay.ENABLED,false,null);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
var level=s.overworld();walls(level,Blocks.STONE.defaultBlockState());
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;
|
||||
int moves=0,turns=0;
|
||||
for(var variant:CrewBoats.variants()) {
|
||||
var boat=variant.type().create(level,EntitySpawnReason.COMMAND);
|
||||
for(int angle=0;angle<360;angle+=45)for(int axis=0;axis<2;axis++)for(int sign:new int[]{-1,1}) {
|
||||
boat.setYRot(angle);boat.setPos(0,20,0);boat.setDeltaMovement(Vec3.ZERO);
|
||||
var motion=axis==0?new Vec3(sign*20,0,0):new Vec3(0,0,sign*20);
|
||||
boat.move(MoverType.SELF,motion);inside(boat);check(boat.horizontalCollision,"Native collision reports contact");
|
||||
check(boat.position().horizontalDistance()>2,"Craft moved before the wall");
|
||||
Vec3 before=boat.position();boat.move(MoverType.SELF,motion.scale(-.075));
|
||||
check(Math.abs(boat.position().distanceTo(before)-1.5)<EPS,"Can back away from contact");inside(boat);moves++;
|
||||
}
|
||||
for(int sx:new int[]{-1,1})for(int sz:new int[]{-1,1}) {
|
||||
boat.setYRot(33);boat.setPos(0,20,0);boat.move(MoverType.SELF,new Vec3(20*sx,0,20*sz));inside(boat);moves++;
|
||||
}
|
||||
check(p.startRiding(boat,true,true),"Pilot mounted");
|
||||
p.setLastClientInput(new Input(false,false,false,true,false,false,false));
|
||||
for(int angle:new int[]{0,90,180,270}) {
|
||||
boat.setYRot(angle);boat.setPos(8-boat.getBoundingBox().getXsize()/2-.01,20,0);
|
||||
check(level.noCollision(boat,boat.getBoundingBox().deflate(EPS)),"Rotation fixture starts clear");
|
||||
for(int tick=0;tick<20;tick++){boat.control(Vec3.ZERO);boat.move(MoverType.SELF,new Vec3(.35,0,0));inside(boat);}
|
||||
turns++;
|
||||
}
|
||||
// Both endpoint envelopes fit; only the middle of this five-degree turn hits the wall.
|
||||
boat.setPos(0,20,0);boat.setYRot(0);
|
||||
for(int tick=0;tick<160;tick++)boat.control(Vec3.ZERO); // Native angular inertia reaches its 5° target in free space.
|
||||
check(Math.abs(boat.getYRot())>90,"Free turning retained");
|
||||
double halfWidth=(variant.raft()?variant.columns()*1.25:variant.columns()+.25)/2;
|
||||
double halfLength=(variant.rows()*1.75+(variant.raft()?0:.25))/2;
|
||||
float from=(float)Math.toDegrees(Math.atan2(halfLength,halfWidth))-2.5f;
|
||||
boat.setYRot(from);boat.setPos(8-Math.hypot(halfWidth,halfLength)+.001,20,0);
|
||||
check(level.noCollision(boat,boat.getBoundingBox().deflate(EPS)),"Arc starts clear");
|
||||
boat.setYRot(from+5);check(level.noCollision(boat,boat.getBoundingBox().deflate(EPS)),"Arc ends clear");boat.setYRot(from);
|
||||
boat.control(Vec3.ZERO);check(boat.getYRot()==from,"Swept corner arc rejected before touching wall");inside(boat);turns++;
|
||||
p.stopRiding();p.setLastClientInput(Input.EMPTY);boat.discard();
|
||||
}
|
||||
// Fences and panes must use their native voxel collision shapes, even at high displacement.
|
||||
int thin=0;
|
||||
for(var block:List.of(Blocks.OAK_FENCE,Blocks.GLASS_PANE)) {
|
||||
walls(level,block.defaultBlockState());
|
||||
for(String wood:List.of("oak","bamboo"))for(var shape:CrewBoats.SHAPES)for(int axis=0;axis<2;axis++)for(int sign:new int[]{-1,1}) {
|
||||
var boat=CrewBoats.variant(wood,shape.columns(),shape.rows()).type().create(level,EntitySpawnReason.COMMAND);
|
||||
boat.setYRot(27);boat.setPos(0,20,0);boat.move(MoverType.SELF,axis==0?new Vec3(sign*20,0,0):new Vec3(0,0,sign*20));
|
||||
check(level.noCollision(boat,boat.getBoundingBox().deflate(EPS)),"Thin obstacle not intersected");
|
||||
check(Math.abs(boat.getX())<8&&Math.abs(boat.getZ())<8&&boat.horizontalCollision,"No tunneling through thin obstacle");boat.discard();thin++;
|
||||
}
|
||||
}
|
||||
walls(level,Blocks.STONE.defaultBlockState());
|
||||
for(int x=-8;x<8;x++)for(int z=-8;z<8;z++) {
|
||||
level.setBlockAndUpdate(new BlockPos(x,18,z),Blocks.STONE.defaultBlockState());
|
||||
level.setBlockAndUpdate(new BlockPos(x,19,z),Blocks.WATER.defaultBlockState());
|
||||
}
|
||||
p.teleportTo(0,25,0);
|
||||
System.out.println("BOAT065_MATRIX_PASS variants="+CrewBoats.variants().size()+" moves="+moves+" turns="+turns+" thin="+thin);
|
||||
});
|
||||
// Compare real mesh vertices (including rails, excluding decorative moving paddles) against each collision box.
|
||||
c.runOnClient(m->{
|
||||
for(var v:CrewBoats.variants())for(int yaw=0;yaw<360;yaw+=45) {
|
||||
var boat=v.type().create(m.level,EntitySpawnReason.COMMAND);boat.setYRot(yaw);boat.setPos(0,20,0);var bounds=boat.getBoundingBox();
|
||||
var pose=new com.mojang.blaze3d.vertex.PoseStack();pose.rotateDegrees(com.mojang.math.Axis.YP,180-yaw);pose.scale(-1,-1,1);pose.rotateDegrees(com.mojang.math.Axis.YP,90);
|
||||
TiledBoatModel.hull(v.columns(),v.rows(),v.raft()).bakeRoot().visit(pose,(at,path,index,cube)->{
|
||||
if(path.contains("paddle"))return;
|
||||
for(float x:new float[]{cube.minX,cube.maxX})for(float y:new float[]{cube.minY,cube.maxY})for(float z:new float[]{cube.minZ,cube.maxZ}) {
|
||||
var vertex=at.pose().transformPosition(new org.joml.Vector3f(x/16,y/16,z/16));
|
||||
check(vertex.x>=bounds.minX-EPS&&vertex.x<=bounds.maxX+EPS&&vertex.z>=bounds.minZ-EPS&&vertex.z<=bounds.maxZ+EPS,"Rendered hull inside collision: "+v.wood()+" "+v.columns()+"x"+v.rows()+" "+path);
|
||||
}
|
||||
});boat.discard();
|
||||
}
|
||||
});
|
||||
c.runOnClient(m->m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK));
|
||||
int live=0;
|
||||
for(String wood:List.of("oak","bamboo"))for(var shape:CrewBoats.SHAPES) {
|
||||
final float yaw=(live%4)*90;final boolean flying=live%2==0;live++;
|
||||
var id=new java.util.concurrent.atomic.AtomicInteger();
|
||||
server.runOnServer(s->{
|
||||
cleanup(s);var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var boat=CrewBoats.variant(wood,shape.columns(),shape.rows()).type().create(s.overworld(),EntitySpawnReason.COMMAND);
|
||||
boat.setPos(0,flying?25:20,0);boat.setYRot(yaw);s.overworld().addFreshEntity(boat);id.set(boat.getId());
|
||||
if(flying){var motor=EntityTypes.ALLAY.create(s.overworld(),EntitySpawnReason.COMMAND);motor.setPos(boat.position());motor.setNoAi(true);s.overworld().addFreshEntity(motor);motor.startRiding(boat);}
|
||||
p.teleportTo(boat.getX(),boat.getY(),boat.getZ());p.startRiding(boat,true,true);
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle()!=null&&m.player.getVehicle().getId()==id.get(),200);
|
||||
c.getInput().lookAt(yaw,25);c.getInput().holdKey(o->o.keyUp);if(flying)c.getInput().holdKey(o->o.keyJump);c.waitTicks(65);
|
||||
c.getInput().holdKey(o->o.keyRight);c.waitTicks(15);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyUp);
|
||||
server.runOnServer(s->{var b=(CrewBoat)s.overworld().getEntity(id.get());check(b!=null&&b.getPassengers().stream().anyMatch(net.minecraft.server.level.ServerPlayer.class::isInstance),"Live pilot stays seated");inside(b);check(b.position().horizontalDistance()>2,"Native keys moved vessel");});
|
||||
c.waitTicks(5);c.runOnClient(m->inside((CrewBoat)m.player.getVehicle()));
|
||||
if(shape.columns()==3&&shape.rows()==3)c.takeScreenshot("boat065-"+wood+"-wall-contact");
|
||||
var before=new AtomicReference<Vec3>();server.runOnServer(s->before.set(s.overworld().getEntity(id.get()).position()));
|
||||
c.getInput().holdKey(o->o.keyDown);c.waitTicks(25);c.getInput().releaseKey(o->o.keyDown);if(flying)c.getInput().releaseKey(o->o.keyJump);
|
||||
server.runOnServer(s->{var b=(CrewBoat)s.overworld().getEntity(id.get());inside(b);check(b.position().subtract(before.get()).horizontalDistance()>.15,"Native reverse input frees craft");});
|
||||
System.out.println("BOAT065_LIVE_PASS "+wood+" "+shape.columns()+"x"+shape.rows()+" yaw="+yaw+" flying="+flying);
|
||||
}
|
||||
server.runOnServer(Boat065ClientChecks::cleanup);
|
||||
System.out.println("BOAT065_PASS native collision, meshes and "+live+" piloted water/flight cases");
|
||||
} finally {
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyDown);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);
|
||||
}
|
||||
}
|
||||
private static void cleanup(MinecraftServer server) {
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var old=p.getVehicle();p.stopRiding();
|
||||
if(old!=null)old.getSelfAndPassengers().toList().reversed().forEach(Entity::discard);
|
||||
}
|
||||
private static void walls(ServerLevel level,net.minecraft.world.level.block.state.BlockState state) {
|
||||
for(int y=18;y<=70;y++)for(int i=-9;i<=8;i++) {
|
||||
level.setBlockAndUpdate(new BlockPos(-9,y,i),state);level.setBlockAndUpdate(new BlockPos(8,y,i),state);
|
||||
level.setBlockAndUpdate(new BlockPos(i,y,-9),state);level.setBlockAndUpdate(new BlockPos(i,y,8),state);
|
||||
}
|
||||
}
|
||||
private static void inside(CrewBoat b) {
|
||||
var box=b.getBoundingBox();check(box.minX>=-8-EPS&&box.maxX<=8+EPS&&box.minZ>=-8-EPS&&box.maxZ<=8+EPS,"Craft remains inside walls: "+b.position()+" "+box);
|
||||
check(b.level().noBlockCollision(b,box.deflate(EPS)),"Hull does not overlap blocks");
|
||||
}
|
||||
private static void check(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.boats.*;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.inventory.ContainerScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native cursor and mouse packets, including actual mounted mob damage and head dispenser activation. */
|
||||
public final class BoatPassengers091ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(m.gui.screen() instanceof IntroScreen s)s.onClose();
|
||||
if(m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}
|
||||
});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("BoatPassengers091 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().clearContent();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);
|
||||
for(int x=-8;x<=8;x++)for(int z=-8;z<=8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
var types=new ArrayList<EntityType<? extends AbstractBoat>>();types.add(EntityTypes.OAK_BOAT);types.add(EntityTypes.BAMBOO_RAFT);
|
||||
for(var shape:CrewBoats.SHAPES)types.add(CrewBoats.variant("oak",shape.columns(),shape.rows()).type());
|
||||
types.add(CrewBoats.variant("bamboo",3,3).type());types.add(EntityTypes.OAK_BOAT);types.add(EntityTypes.OAK_BOAT);
|
||||
for(int index=0;index<types.size();index++){
|
||||
var type=types.get(index);boolean familiar=index==types.size()-1,baby=index==types.size()-2;
|
||||
int[] ids=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.stopRiding();p.setPos(0,2,0);
|
||||
var boat=type.create(p.level(),EntitySpawnReason.COMMAND);boat.setPos(0,1,0);p.level().addFreshEntity(boat);
|
||||
Mob mob;
|
||||
if(familiar){var pet=FamiliarService.TYPE.create(p.level(),EntitySpawnReason.COMMAND);pet.bind(p,new ItemStack(Items.CHICKEN_SPAWN_EGG));mob=pet;}
|
||||
else{var chicken=EntityTypes.CHICKEN.create(p.level(),EntitySpawnReason.COMMAND);chicken.setBaby(baby);mob=chicken;}
|
||||
mob.setPos(boat.position());mob.setNoAi(true);mob.getAttribute(Attributes.MAX_HEALTH).setBaseValue(40);mob.setHealth(40);p.level().addFreshEntity(mob);
|
||||
boolean boarded=boat instanceof CrewBoat ? p.startRiding(boat,true,true)&&mob.startRiding(boat,true,true) : mob.startRiding(boat,true,true)&&p.startRiding(boat,true,true);
|
||||
check(boarded,"Both passengers board "+type);
|
||||
HeadEquipment.set(mob,new ItemStack(Items.CHEST));((Container)HeadBlockService.session(mob).entity).setItem(0,new ItemStack(Items.DIAMOND,7));
|
||||
p.inventoryMenu.broadcastChanges();return new int[]{boat.getId(),mob.getId()};
|
||||
});
|
||||
c.waitFor(m->m.player.getVehicle()!=null&&m.player.getVehicle().getId()==ids[0]&&m.level.getEntity(ids[1])!=null&&m.level.getEntity(ids[1]).getVehicle()==m.player.getVehicle(),200);c.waitTicks(15);
|
||||
aim(c,ids[1]);assertPick(c,ids[1]);
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_RIGHT);
|
||||
c.waitFor(m->m.gui.screen() instanceof ContainerScreen,100);
|
||||
server.runOnServer(s->check(s.getPlayerList().getPlayers().getFirst().containerMenu.getSlot(0).getItem().is(Items.DIAMOND),"Right click opens the seated mob's actual chest"));
|
||||
c.runOnClient(m->m.player.closeContainer());c.waitTicks(8);
|
||||
server.runOnServer(s->{var mob=mob(s,ids[1]);HeadEquipment.set(mob,new ItemStack(Items.DISPENSER));((Container)HeadBlockService.session(mob).entity).setItem(0,new ItemStack(Items.ARROW,4));});
|
||||
c.waitTicks(8);aim(c,ids[1]);assertPick(c,ids[1]);c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_LEFT);
|
||||
server.waitFor(s->(familiar?mob(s,ids[1]).getHealth()==40:mob(s,ids[1]).getHealth()<40)&&((Container)HeadBlockService.session(mob(s,ids[1])).entity).getItem(0).getCount()==3,100);
|
||||
server.runOnServer(s->{check(mob(s,ids[1]).getVehicle()!=null,"Hit does not unseat the animal");check(!p(s).level().getEntitiesOfClass(net.minecraft.world.entity.projectile.arrow.Arrow.class,new AABB(-12,-4,-12,12,10,12)).isEmpty(),"A real projectile is fired");});
|
||||
c.runOnClient(m->{var other=EntityTypes.CHICKEN.create(m.level,EntitySpawnReason.COMMAND);check(!BoatPassengerInteractions.aboardTogether(m.player,other),"Unrelated mob excluded from exception");check(!BoatPassengerInteractions.aboardTogether(m.player,m.player),"Player seats excluded from exception");});
|
||||
if(familiar)server.runOnServer(s->{
|
||||
var pet=(FamiliarEntity)mob(s,ids[1]);check(!CarryService.pair(p(s),pet),"Seated familiar accepts taps");
|
||||
var ammo=(Container)HeadBlockService.session(pet).entity;
|
||||
pet.stopRiding();pet.setAttached(CarryService.CARRIED,true);check(pet.startRiding(p(s),true,true),"Familiar can be carried for protection check");
|
||||
check(CarryService.pair(p(s),pet)&&CarryService.pair(pet,p(s)),"Head carry protection is symmetric and preserved");
|
||||
pet.tap(p(s));check(pet.getHealth()==40&&ammo.getItem(0).getCount()==3,"Carried familiar remains protected from equipment pulses");
|
||||
pet.setAttached(CarryService.CARRIED,false);
|
||||
});
|
||||
System.out.println("BOAT091_SEAT_PASS "+type+" familiar="+familiar+" baby="+baby);
|
||||
server.runOnServer(s->{p(s).stopRiding();HeadEquipment.set(mob(s,ids[1]),ItemStack.EMPTY);mob(s,ids[1]).discard();p(s).level().getEntity(ids[0]).discard();});c.waitTicks(8);
|
||||
}
|
||||
System.out.println("BOAT091_PASS: vanilla boat/raft, every tiled shape, native targeting, right-click storage, left-click damage and real dispenser shots, baby/familiar passengers and preserved carry protection");
|
||||
}
|
||||
}
|
||||
private static net.minecraft.server.level.ServerPlayer p(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static Mob mob(MinecraftServer s,int id){return (Mob)p(s).level().getEntity(id);}
|
||||
private static void aim(ClientGameTestContext c,int id){var d=c.computeOnClient(m->m.level.getEntity(id).getBoundingBox().getCenter().subtract(m.player.getEyePosition()));c.getInput().lookAt((float)Math.toDegrees(Math.atan2(-d.x,d.z)),(float)-Math.toDegrees(Math.atan2(d.y,Math.sqrt(d.x*d.x+d.z*d.z))));c.waitTicks(4);}
|
||||
private static void assertPick(ClientGameTestContext c,int id){c.runOnClient(m->{var h=m.player.raycastHitResult(1,m.player);check(h instanceof EntityHitResult e&&e.getEntity().getId()==id,"Native crosshair must pick seated animal: "+h+" eye="+m.player.getEyePosition()+" view="+m.player.getViewVector(1)+" mob="+m.level.getEntity(id).getBoundingBox()+" yaw="+m.player.getYRot()+" eligible="+BoatPassengerInteractions.aboardTogether(m.player,m.level.getEntity(id)));});}
|
||||
private static void check(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
}
|
||||
+4
-4
@@ -51,11 +51,11 @@ public final class Collections035ClientChecks implements FabricClientGameTest {
|
||||
m.gui.setScreen(new InventoryScreen(m.player));});c.waitTicks(5);c.takeScreenshot("sanctuary-beta035-personal-catalogue");
|
||||
hover(c,36);c.getInput().pressKey(InputConstants.KEY_U);c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,60);
|
||||
c.runOnClient(m->{m.gui.setScreen(new RecipeScreen(null));button(m.gui.screen(),"sanctuary.recipes.open").onPress(enter());});c.waitFor(m->m.gui.screen() instanceof mezz.jei.gui.recipes.RecipesGui,60);c.waitTicks(5);c.takeScreenshot("sanctuary-beta035-wood-recipes");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().op(p.nameAndId());RecipeService.refresh(p);});c.waitFor(m->RecipeClient.operatorAllowed,120);
|
||||
c.runOnClient(m->{require(!RecipeClient.operator,"Operator status alone does not enable full view");m.gui.setScreen(new RecipeScreen(null));RecipeClient.request("operator");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getWorldData().setAllowCommands(true);s.getPlayerList().op(p.nameAndId());RecipeService.refresh(p);});c.waitFor(m->RecipeClient.operatorAllowed,120);
|
||||
c.runOnClient(m->{require(RecipeClient.operator,"Allow Commands enables the operator catalogue");m.gui.setScreen(new RecipeScreen(null));});
|
||||
c.waitFor(m->RecipeClient.operator,120);c.waitTicks(3);c.takeScreenshot("sanctuary-beta035-operator-view");
|
||||
c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().anyMatch(i->i.is(Items.DIAMOND_BLOCK)),"Explicit operator sees unknown items"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getPlayerList().deop(p.nameAndId());RecipeService.refresh(p);});
|
||||
c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().anyMatch(i->i.is(Items.DIAMOND_BLOCK)),"Operator sees unknown items"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.getWorldData().setAllowCommands(false);s.getPlayerList().deop(p.nameAndId());RecipeService.refresh(p);});
|
||||
c.waitFor(m->!RecipeClient.operatorAllowed&&!RecipeClient.operator,120);c.runOnClient(m->require(SanctuaryJeiPlugin.runtime().getIngredientFilter().getFilteredItemStacks().stream().noneMatch(i->i.is(Items.DIAMOND_BLOCK)),"Permission revocation removes unknown items"));
|
||||
// Use JEI's real transfer packet with ingredients available only in the final added row.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var progress=ProgressEffects.get(p);for(int i=progress.inventory();i<5;i++)progress=progress.buy("inventory",1);
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.progression.SanctuaryGameplay;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.state.*;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.entity.CampfireBlockEntity;
|
||||
|
||||
/** Native renderer submissions and synchronized animation, in a disposable integrated world. */
|
||||
public final class Cosmetic066ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(6);m.options.pauseOnLostFocus=false;});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Cosmetic066 "+UUID.randomUUID());s.setSeed("66");s.setAllowCommands(true);s.getGameRules().set(SanctuaryGameplay.ENABLED,false,null);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
world.getConnection().waitForChunksDownload();var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().clearContent();s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.THIRD_PERSON_FRONT);m.options.fov().set(40);m.getWindow().setWindowed(1280,800);m.resizeGui();});c.getInput().lookAt(0,-12);
|
||||
for(var item:List.of(Items.CHEST,Items.TRAPPED_CHEST,Items.ENDER_CHEST,Items.SHULKER_BOX)) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();HeadEquipment.set(p,new ItemStack(item));HeadBlockService.session(p).event(1,1);});
|
||||
c.waitTicks(18);
|
||||
c.takeScreenshot("cosmetic066-open-"+item);
|
||||
c.runOnClient(m->{var h=head(m,m.player);var calls=draw(h);System.out.println("COSMETIC066_OPEN "+item+" "+calls);
|
||||
check(calls.models()==1,"Exactly one animated container, no closed duplicate: "+item+" "+calls);
|
||||
if(h.blockEntity instanceof ChestRenderState chest)check(chest.open>.7,"Synced chest lid opens");
|
||||
else check(h.blockEntity instanceof ShulkerBoxRenderState box&&box.progress>.9,"Synced Shulker lid opens");
|
||||
});
|
||||
server.runOnServer(s->HeadBlockService.session(s.getPlayerList().getPlayers().getFirst()).event(1,0));c.waitTicks(18);
|
||||
c.runOnClient(m->{var h=head(m,m.player);check(draw(h).models()==1,"One closed model");if(h.blockEntity instanceof ChestRenderState chest)check(chest.open<.05,"Chest closes");else check(((ShulkerBoxRenderState)h.blockEntity).progress<.05,"Shulker closes");});
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();HeadEquipment.set(p,new ItemStack(Items.BELL));HeadBlockService.session(p).event(1,Direction.NORTH.get3DDataValue());});
|
||||
c.waitTicks(7);c.takeScreenshot("cosmetic066-bell-ringing");
|
||||
c.runOnClient(m->{var h=head(m,m.player);var calls=draw(h);check(calls.models()==1&&calls.blocks==1,"One moving bell and its static support: "+calls);check(h.blockEntity instanceof BellRenderState bell&&bell.shakeDirection!=null&&bell.ticks>1,"Bell shakes from the server event");});
|
||||
c.waitTicks(50);c.runOnClient(m->check(((BellRenderState)head(m,m.player).blockEntity).shakeDirection==null,"Bell settles normally"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();HeadEquipment.set(p,new ItemStack(Items.CAMPFIRE));var session=HeadBlockService.session(p);HeadBlockService.scoped(session,()->{((CampfireBlockEntity)session.entity).placeFood(p.level(),p,new ItemStack(Items.BEEF));return null;});});
|
||||
c.waitTicks(15);c.runOnClient(m->{var h=head(m,m.player);check(draw(h).blocks==1,"Campfire body remains");check(h.blockEntity instanceof CampfireRenderState f&&f.items.stream().anyMatch(i->!i.isEmpty()),"Campfire still renders synchronized food");});c.takeScreenshot("cosmetic066-campfire-food");
|
||||
// The same renderer must retain partial static bodies and complete models that have no live renderer.
|
||||
c.runOnClient(m->{
|
||||
var original=HeadCosmeticService.appearance(m.player).copy();
|
||||
for(var item:List.of(Items.ENCHANTING_TABLE,Items.LECTERN,Items.OAK_SIGN,Items.BANNER.white(),Items.PLAYER_HEAD,Items.BED.red(),Items.OAK_DOOR,Items.FURNACE,Items.OAK_SHELF)) {
|
||||
m.player.setAttached(AccessoryService.APPEARANCE,new AccessoryService.Appearance(ItemStack.EMPTY,new ItemStack(item)));
|
||||
var h=head(m,m.player);var calls=draw(h);System.out.println("COSMETIC066_RETAINED "+item+" "+calls);
|
||||
check(calls.models()+calls.blocks>0,"Head visual retained: "+item);
|
||||
if(item==Items.ENCHANTING_TABLE)check(calls.models()==1&&calls.blocks==1,"One book on enchanting table body");
|
||||
if(item==Items.OAK_SIGN)check(calls.blocks==1&&calls.models()==0,"One sign board mesh");
|
||||
if(item==Items.BANNER.white())check(Collections.frequency(calls.modelNames,"BannerModel")==1,"One banner support, with native fabric texture passes");
|
||||
if(item==Items.PLAYER_HEAD)check(calls.models()==1,"One skull model");
|
||||
if(item==Items.BED.red()||item==Items.OAK_DOOR)check(!h.second.isEmpty(),"Both bed/door halves retained");
|
||||
}
|
||||
int nextId=200000;
|
||||
for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.COW,EntityTypes.ZOMBIE))for(boolean baby:List.of(false,true)) {
|
||||
var mob=(Mob)type.create(m.level,EntitySpawnReason.COMMAND);mob.setId(nextId++);mob.setPos(m.player.position());mob.setBaby(baby);
|
||||
for(var item:List.of(Items.CHEST,Items.BELL)) {
|
||||
mob.setAttached(AccessoryService.APPEARANCE,new AccessoryService.Appearance(ItemStack.EMPTY,new ItemStack(item)));
|
||||
var h=head(m,mob);var calls=draw(h);check(calls.models()==1,"Mob hat has one model: "+type+" baby="+baby+" "+item+" "+calls);
|
||||
if(item==Items.BELL)check(calls.blocks==1,"Mob bell keeps its support");
|
||||
}
|
||||
}
|
||||
m.player.setAttached(AccessoryService.APPEARANCE,new AccessoryService.Appearance(ItemStack.EMPTY,original));
|
||||
});
|
||||
System.out.println("COSMETIC066_PASS: native submissions, four synced container animations, bell ring/settle, static supports, food and twelve mob hat cases");
|
||||
}
|
||||
}
|
||||
private static HeadCosmeticState head(Minecraft m,LivingEntity entity) {
|
||||
var state=m.getEntityRenderDispatcher().getRenderer(entity).createRenderState(entity,1);
|
||||
return entity==m.player?((HeadCosmeticState.Access)state).sanctuary$headCosmetic():((MobHeadState.Access)state).sanctuary$mobHat().head;
|
||||
}
|
||||
private static final class Calls {
|
||||
final List<String> modelNames=new ArrayList<>();int blocks;
|
||||
int models(){return modelNames.size();}
|
||||
public String toString(){return "models="+modelNames+", block meshes="+blocks;}
|
||||
}
|
||||
private static Calls draw(HeadCosmeticState head) {
|
||||
var calls=new Calls();
|
||||
SubmitNodeCollector collector=(SubmitNodeCollector)Proxy.newProxyInstance(SubmitNodeCollector.class.getClassLoader(),new Class<?>[]{SubmitNodeCollector.class},(proxy,method,args)->{
|
||||
if(method.getName().equals("order"))return proxy;
|
||||
if(method.getName().equals("submitModel"))calls.modelNames.add(args[0].getClass().getSimpleName());
|
||||
if(method.getName().equals("submitBlockModel"))calls.blocks++;
|
||||
return null;
|
||||
});
|
||||
HeadCosmeticLayer.submitAtHead(new PoseStack(),collector,15728880,0,head);return calls;
|
||||
}
|
||||
private static void check(boolean yes,String message){if(!yes)throw new AssertionError(message);}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
|
||||
/** Creation toggles and saved-world reopen, using ordinary vanilla presets and actual admission. */
|
||||
public final class Gameplay063ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var observing=new AtomicBoolean(true);var hello=new AtomicInteger();var lastHello=new AtomicReference<HelloWorldScreen>();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(observing.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
if(lastHello.getAndSet(s)==s)return;
|
||||
hello.incrementAndGet();IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}});
|
||||
c.runOnClient(m->{m.options.renderDistance().set(4);m.options.simulationDistance().set(4);m.options.pauseOnLostFocus=false;});
|
||||
try{
|
||||
for(String preset:List.of("minecraft:flat","minecraft:normal"))for(boolean enabled:List.of(true,false)){
|
||||
int before=hello.get();net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
var identity=new AtomicReference<UUID>();
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Gameplay063 "+UUID.randomUUID());s.setSeed("63");s.setAllowCommands(true);s.setGenerateStructures(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals(preset)).findFirst().orElseThrow());s.getGameRules().set(SanctuaryGameplay.ENABLED,enabled,null);}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),2400);c.waitTicks(25);
|
||||
check((hello.get()>before)==enabled,"Hello World only when enabled: "+preset+" "+enabled);
|
||||
check(c.computeOnClient(m->ProgressionClient.active())==enabled,"Client knows selected mode");
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;
|
||||
check(SanctuaryGameplay.supported(s)==enabled&&(ProgressionService.session(s)!=null)==enabled,"Server uses the creation choice");
|
||||
check((s.overworld().getChunkSource().getGenerator() instanceof net.minecraft.world.level.levelgen.FlatLevelSource)==preset.endsWith("flat"),"Selected native generator preserved");
|
||||
try{fr.koka.sanctuary.expansion.ExpansionRuntime.session(s.overworld());throw new AssertionError("Native terrain must not open an expansion session");}
|
||||
catch(IllegalArgumentException expected){check(expected.getMessage().equals("laboratory_required"),"No island planning or expansion system on native terrain");}
|
||||
check(SanctuaryInventoryCapacity.unlockedRows(p)==(enabled?1:4),"Correct inventory capacity");
|
||||
check(p.getAttributeValue(Attributes.MAX_HEALTH)==(enabled?6:20),"Correct initial health");
|
||||
check(fr.koka.sanctuary.realtime.RealtimeService.active(s.overworld())==enabled&&fr.koka.sanctuary.weather.WeatherService.active(s.overworld())==enabled,"Clock and weather follow gameplay choice");
|
||||
if(enabled){check(FamiliarService.active(p)!=null,"Starter familiar enters native terrain");identity.set(fr.koka.sanctuary.familiar.FamiliarBattle.bond(p).id());}
|
||||
check(s.getGlobalGameRules().get(SanctuaryGameplay.ENABLED)==enabled,"Rule retained");
|
||||
s.overworld().getGameRules().set(SanctuaryGameplay.ENABLED,!enabled,s);check(s.overworld().getGameRules().get(SanctuaryGameplay.ENABLED)==enabled,"Live dimension rule flip rejected before mutation");
|
||||
s.getGlobalGameRules().set(SanctuaryGameplay.ENABLED,!enabled,s);check(s.getGlobalGameRules().get(SanctuaryGameplay.ENABLED)==enabled,"Live global rule flip rejected");
|
||||
try{s.getCommands().getDispatcher().execute("gamerule sanctuary:use_sanctuary "+!enabled,s.createCommandSourceStack());throw new AssertionError("Command must be rejected");}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){check(expected.getRawMessage().getString().contains("Sanctuary"),"Command explains creation-only choice");}
|
||||
s.overworld().getGameRules().set(GameRules.KEEP_INVENTORY,true,s);check(s.overworld().getGameRules().get(GameRules.KEEP_INVENTORY),"Other game rules remain editable");
|
||||
});
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),2400);c.waitTicks(20);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(s.getWorldData().overworldData().isInitialized(),"Reopened saved world");check(SanctuaryGameplay.supported(s)==enabled&&(ProgressionService.session(s)!=null)==enabled,"Mode survives reopening");
|
||||
if(enabled)check(identity.get().equals(fr.koka.sanctuary.familiar.FamiliarBattle.bond(p).id()),"Starter identity preserved across reopen");
|
||||
});
|
||||
check(hello.get()==before+(enabled?1:0),"Reopen does not repeat character creation");
|
||||
}
|
||||
System.out.println("GAMEPLAY063_CASE_PASS: "+preset+" enabled="+enabled+" creation and reopen");
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());check(!Component.translatable(SanctuaryGameplay.ENABLED.getDescriptionId()).getString().contains("gamerule."),"Native gamerule name localized: "+locale);check(!Component.translatable(SanctuaryGameplay.ENABLED.getDescriptionId()+".description").getString().contains("gamerule."),"Native tooltip localized: "+locale);});
|
||||
System.out.println("GAMEPLAY063_PASS");
|
||||
}finally{observing.set(false);}
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.network.protocol.game.ServerboundPickItemFromBlockPacket;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.piston.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class Pick092ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos C=new BlockPos(0,50,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(m.gui.screen() instanceof IntroScreen s)s.onClose();
|
||||
if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen s){Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}
|
||||
});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Pick092 "+UUID.randomUUID());s.setSeed("42");s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null,1200);
|
||||
var server=world.getServer();server.runOnServer(s->{creative(p(s));verifyStates(p(s).level());});c.runOnClient(m->verifyStates(m.level));
|
||||
for(boolean sticky:new boolean[]{false,true})for(var f:Direction.values()){
|
||||
Item item=sticky?Items.STICKY_PISTON:Items.PISTON;
|
||||
server.runOnServer(s->{var l=p(s).level();for(var facing:Direction.values())for(boolean glue:new boolean[]{false,true})SuperPistons.dismantle(l,C,facing,glue);
|
||||
for(int x=-6;x<=6;x++)for(int y=-6;y<=6;y++)for(int z=-6;z<=6;z++)l.setBlockAndUpdate(C.offset(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
for(int i=0;i<9;i++)l.setBlockAndUpdate(SuperPistons.tile(C,f,i),(sticky?Blocks.STICKY_PISTON:Blocks.PISTON).defaultBlockState().setValue(PistonBaseBlock.FACING,f));
|
||||
check(SuperPistons.assemble(l,C,f,sticky),"Assemble real piston");
|
||||
});c.waitTicks(5);
|
||||
pick(c,server,C,f,item,false);
|
||||
// Ctrl-pick uses the actual server handler; its result must still be an ordinary piston.
|
||||
server.runOnServer(s->{p(s).getInventory().clearContent();p(s).connection.handlePickItemFromBlock(new ServerboundPickItemFromBlockPacket(C,true));check(ItemStack.matches(p(s).getMainHandItem(),new ItemStack(item)),"Ctrl-pick excludes controller data: "+p(s).getMainHandItem()+" components="+p(s).getMainHandItem().getComponents()+" eye="+p(s).getEyePosition());});
|
||||
server.runOnServer(s->p(s).level().setBlockAndUpdate(SuperPistons.tile(C,f,0).relative(f,-1),Blocks.REDSTONE_BLOCK.defaultBlockState()));
|
||||
server.waitFor(s->p(s).level().getBlockEntity(C) instanceof SuperPistonController be&&be.stroke==3&&be.pending.isEmpty(),200);c.waitTicks(5);
|
||||
pick(c,server,C.relative(f,3),f,item,false);
|
||||
pick(c,server,C.relative(f),SuperPistons.u(f),item,false);
|
||||
pick(c,server,C.relative(f,2),SuperPistons.u(f),item,false);
|
||||
if(f==Direction.NORTH){
|
||||
server.runOnServer(s->{var p=p(s);p.setGameMode(GameType.SURVIVAL);p.getAbilities().mayfly=true;p.getAbilities().flying=true;p.onUpdateAbilities();});
|
||||
pick(c,server,C.relative(f,3),f,item,true);
|
||||
server.runOnServer(s->{p(s).getInventory().clearContent();p(s).inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getMainHandItem().isEmpty(),100);c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_MIDDLE);c.waitTicks(8);
|
||||
server.runOnServer(s->{check(p(s).getInventory().isEmpty(),"Survival pick creates no item");creative(p(s));});
|
||||
}
|
||||
System.out.println("PICK092_DIRECTION_PASS sticky="+sticky+" facing="+f);
|
||||
}
|
||||
server.runOnServer(s->moving(p(s)));
|
||||
System.out.println("PICK092_PASS: 864 states on client/server; real middle clicks for bases, heads, both rods, six directions and variants; survival and moving/Ctrl-pick");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void pick(ClientGameTestContext c,TestServerContext server,BlockPos pos,Direction from,Item item,boolean survival){
|
||||
server.runOnServer(s->{var p=p(s);p.getInventory().clearContent();p.getInventory().setSelectedSlot(0);if(survival)p.getInventory().setItem(4,new ItemStack(item,5));p.inventoryMenu.broadcastChanges();
|
||||
p.teleportTo(pos.getX()+.5+from.getStepX()*2.5,pos.getY()+.5+from.getStepY()*2.5-p.getEyeHeight(),pos.getZ()+.5+from.getStepZ()*2.5);p.setDeltaMovement(Vec3.ZERO);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
});c.waitFor(m->m.player.getMainHandItem().isEmpty(),100);c.waitTicks(5);c.runOnClient(m->{m.player.getAbilities().flying=true;m.player.setDeltaMovement(Vec3.ZERO);});c.getInput().lookAt(pos);c.waitTicks(4);
|
||||
c.runOnClient(m->{var hit=m.player.raycastHitResult(1,m.player);check(hit instanceof BlockHitResult b&&b.getType()==HitResult.Type.BLOCK&&b.getBlockPos().equals(pos),"Native cursor targets the requested part: "+pos+" got "+hit.getLocation());});
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_MIDDLE);
|
||||
c.waitFor(m->m.player.getMainHandItem().is(item),100);
|
||||
server.runOnServer(s->check(ItemStack.matches(p(s).getMainHandItem(),new ItemStack(item,survival?5:1)),"Exact source item and count, no technical data"));
|
||||
}
|
||||
private static void verifyStates(LevelReader l){int count=0;for(var b:List.of(SuperPistons.NORMAL,SuperPistons.GLUANT,SuperPistons.HEAD,SuperPistons.ROD))for(var s:b.getStateDefinition().getPossibleStates()){
|
||||
boolean sticky=s.getBlock() instanceof SuperPistonBlocks.Base base?base.sticky:s.getValue(SuperPistonBlocks.STICKY);
|
||||
for(boolean data:new boolean[]{false,true})check(ItemStack.matches(s.getCloneItemStack(l,C,data),new ItemStack(sticky?Items.STICKY_PISTON:Items.PISTON)),"Source for "+s);count++;
|
||||
}check(count==864,"Every technical state checked");}
|
||||
private static void moving(ServerPlayer p){var l=p.level();var pos=C.offset(5,0,0);p.teleportTo(pos.getX()+.5,pos.getY(),pos.getZ()+2);creative(p);
|
||||
for(boolean sticky:new boolean[]{false,true})for(boolean extending:new boolean[]{false,true}){
|
||||
var carried=extending?SuperPistons.headForBase(SuperPistons.base(Direction.NORTH,4,sticky,true),2):SuperPistons.base(Direction.NORTH,4,sticky,true);
|
||||
var state=Blocks.MOVING_PISTON.defaultBlockState().setValue(MovingPistonBlock.FACING,Direction.NORTH);l.setBlock(pos,state,324);l.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos,state,carried,Direction.NORTH,extending,true));
|
||||
p.getInventory().clearContent();p.connection.handlePickItemFromBlock(new ServerboundPickItemFromBlockPacket(pos,true));check(ItemStack.matches(p.getMainHandItem(),new ItemStack(sticky?Items.STICKY_PISTON:Items.PISTON)),"Moving source picks ordinary piston without motion NBT");
|
||||
l.removeBlockEntity(pos);
|
||||
}
|
||||
var state=Blocks.MOVING_PISTON.defaultBlockState();l.setBlockEntity(MovingPistonBlock.newMovingBlockEntity(pos,state,Blocks.STONE.defaultBlockState(),Direction.NORTH,true,false));check(state.getCloneItemStack(l,pos,false).isEmpty(),"Vanilla moving cargo behaviour unchanged");l.removeBlockEntity(pos);l.setBlock(pos,Blocks.AIR.defaultBlockState(),324);
|
||||
for(var block:List.of(Blocks.FURNACE,Blocks.BARREL))check(block.defaultBlockState().getCloneItemStack(l,pos,false).is(block.asItem()),"Other multiblock source blocks still pick normally");
|
||||
}
|
||||
private static void creative(ServerPlayer p){p.setGameMode(GameType.CREATIVE);p.setNoGravity(true);p.getAbilities().flying=true;p.onUpdateAbilities();}
|
||||
private static ServerPlayer p(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean v,String message){if(!v)throw new AssertionError(message);}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native solar engine, live equipment packets and familiar combat health on a fresh flat world. */
|
||||
public final class Sunlight062ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sunlight062 "+UUID.randomUUID());s.setSeed("62");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.getInventory().clearContent();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);
|
||||
command(s,"sanctuary time vanilla");command(s,"sanctuary weather vanilla");command(s,"weather clear 100000");command(s,"time set noon");
|
||||
p.teleportTo(.5,1,.5);for(int x=-9;x<=9;x++)for(int z=-9;z<=9;z++)for(int y=0;y<=7;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),(y==0?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
});c.waitTicks(30);
|
||||
server.runOnServer(s->{species(player(s));nativeHats(player(s));command(s,"time set midnight");});c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=probe(p,Items.ZOMBIE_SPAWN_EGG);expose(pet);check(!pet.isOnFire(),"Night prevents ignition");pet.discard();
|
||||
fresh(p,Items.ZOMBIE_SPAWN_EGG);p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND));p.inventoryMenu.broadcastChanges();});c.waitTicks(25);
|
||||
int id=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Equipped zombie egg spawns");pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));pet.setDeltaMovement(Vec3.ZERO);return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(id)!=null&&m.player.getMainHandItem().is(Items.DIAMOND),100);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);click(c,id);c.waitTicks(8);c.getInput().releaseKey(o->o.keyShift);
|
||||
float healthy=server.computeOnServer(s->{var pet=FamiliarService.active(player(s));check(HeadEquipment.get(pet).is(Items.DIAMOND),"Real sneak-click equips a non-block sun hat");command(s,"time set noon");return pet.getHealth();});
|
||||
c.waitTicks(100);server.runOnServer(s->{var pet=FamiliarService.active(player(s));check(!pet.isOnFire()&&pet.getHealth()==healthy,"Hat protects familiar during real server ticks");expose(pet);check(!pet.isOnFire(),"Hat also withstands a long solar exposure");});
|
||||
c.runOnClient(m->{var pet=(FamiliarEntity)m.level.getEntity(id);var state=(FamiliarRenderer.State)m.getEntityRenderDispatcher().getRenderer(pet).createRenderState(pet,1);check(((MobHeadState.Access)state.appearance).sanctuary$mobHat().active,"Synced hat renders on the familiar model");});
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);click(c,id);c.waitTicks(8);c.getInput().releaseKey(o->o.keyShift);
|
||||
server.runOnServer(s->{var pet=FamiliarService.active(player(s));check(HeadEquipment.get(pet).isEmpty(),"Empty-hand sneak-click removes hat");expose(pet);check(pet.isOnFire(),"Removing hat exposes familiar to the sun again");});
|
||||
c.waitTicks(45);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getHealth()<healthy&&FamiliarBattle.bond(p).health()==pet.getHealth(),"Native on-fire ticks lower persisted familiar health");});
|
||||
c.waitFor(m->m.level.getEntity(id)!=null&&m.level.getEntity(id).isOnFire(),100);
|
||||
c.runOnClient(m->{var pet=(FamiliarEntity)m.level.getEntity(id);var state=(FamiliarRenderer.State)m.getEntityRenderDispatcher().getRenderer(pet).createRenderState(pet,1);check(state.displayFireAnimation&&state.boundingBoxHeight==pet.visualHeight(),"Synced native fire follows visible baby size");});
|
||||
c.getInput().lookAt(0,12);c.takeScreenshot("sanctuary-beta062-sunburn");
|
||||
// A hat prevents new ignition; it must never grant blanket fire immunity.
|
||||
server.runOnServer(s->{var pet=FamiliarService.active(player(s));HeadEquipment.set(pet,new ItemStack(Items.STONE));pet.clearFire();pet.igniteForSeconds(2);check(pet.isOnFire(),"A hat does not extinguish an external flame");});c.waitTicks(45);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(!pet.isOnFire(),"With hat, previous fire expires without solar reignition");
|
||||
HeadEquipment.set(pet,ItemStack.EMPTY);roof(p,true);pet.clearFire();});c.waitTicks(8);
|
||||
server.runOnServer(s->{var pet=FamiliarService.active(player(s));expose(pet);check(!pet.isOnFire(),"Solid roof prevents ignition");roof(player(s),false);
|
||||
var p=player(s);for(int x=-1;x<=1;x++)for(int z=1;z<=3;z++)p.level().setBlockAndUpdate(new BlockPos(x,1,z),Blocks.WATER.defaultBlockState());pet.igniteForSeconds(8);});c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.isInWater()&&!pet.isOnFire(),"Real water extinguishes existing familiar fire");expose(pet);check(!pet.isOnFire(),"Immersion prevents solar ignition");
|
||||
for(int x=-9;x<=9;x++)for(int z=-9;z<=9;z++)for(int y=1;y<=2;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
pet.setPos(.5,1,2.5);pet.setDeltaMovement(Vec3.ZERO);HeadEquipment.set(pet,new ItemStack(Items.STONE));command(s,"weather rain 100000");});c.waitTicks(130);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);HeadEquipment.set(pet,ItemStack.EMPTY);pet.clearFire();check(pet.isInWaterOrRain(),"Rain fixture actually wets the familiar");expose(pet);check(!pet.isOnFire(),"Rain protects familiar under open sky");HeadEquipment.set(pet,new ItemStack(Items.STONE));command(s,"weather clear 100000");});c.waitTicks(130);
|
||||
for(String locale:List.of("fr_fr","en_us"))c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());check(!Component.translatable("sanctuary.familiar.sunlight.hint").getString().contains("sanctuary."),"Sunlight advice localized in "+locale);});
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);var egg=AccessoryAccess.of(p).getItem(AccessoryContainer.FAMILIAR).copy();
|
||||
HeadEquipment.set(pet,ItemStack.EMPTY);pet.clearFire();pet.setHealth(.5F);expose(pet);check(pet.isOnFire(),"Bare familiar ignites after clear weather resumes");
|
||||
// A final native baseTick delivers on-fire damage, not a direct test-only damage call.
|
||||
pet.setRemainingFireTicks(160);pet.baseTick();check(pet.isRemoved()&&FamiliarBattle.bond(p).knockedOutUntil()>FamiliarBattle.now(),"Sunfire causes ordinary familiar KO");
|
||||
check(ItemStack.isSameItemSameComponents(egg,AccessoryAccess.of(p).getItem(AccessoryContainer.FAMILIAR)),"KO preserves the equipped egg and identity");
|
||||
command(s,"time set midnight");duelSunfire(p);
|
||||
});
|
||||
System.out.println("SUNLIGHT062_PASS: species, real hat packets, native sunlight/fire/health/KO, roofs, night, water, rain, flame size, duels and FR/EN");
|
||||
}finally{onboarding.set(false);c.getInput().releaseKey(o->o.keyShift);}
|
||||
}
|
||||
private static void species(ServerPlayer p){
|
||||
var expected=Set.of("zombie","zombie_villager","drowned","zombie_horse","zombie_nautilus","skeleton","stray","bogged","phantom");var actual=new HashSet<String>();int count=0;
|
||||
for(var item:BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem){
|
||||
var stack=new ItemStack(item);String name=BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(stack)).getPath();
|
||||
var pet=probe(p,item);expose(pet);if(pet.isOnFire())actual.add(name);
|
||||
check(pet.isOnFire()==expected.contains(name),"Native daylight species: "+name);pet.discard();count++;
|
||||
}
|
||||
check(actual.equals(expected),"All nine sun-vulnerable egg species tested; fire-immune wither skeleton exempt");
|
||||
System.out.println("SUNLIGHT062_SPECIES_PASS: "+count+" spawn eggs, burning="+actual);
|
||||
}
|
||||
private static void nativeHats(ServerPlayer p){
|
||||
var mob=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,2.5);mob.setNoAi(true);expose(mob);check(mob.isOnFire(),"Native bare zombie burns");
|
||||
for(var item:List.of(Items.STONE,Items.DIAMOND,Items.STICK,Items.TORCH)){
|
||||
mob.clearFire();HeadEquipment.set(mob,new ItemStack(item));expose(mob);check(!mob.isOnFire(),"Any Sanctuary head item shades a normal mob: "+item);
|
||||
}
|
||||
HeadEquipment.set(mob,ItemStack.EMPTY);var helmet=new ItemStack(Items.DIAMOND_HELMET);mob.setItemSlot(EquipmentSlot.HEAD,helmet);mob.clearFire();expose(mob);
|
||||
check(!mob.isOnFire()&&helmet.getDamageValue()>0,"Native head armour still protects and wears");
|
||||
mob.setItemSlot(EquipmentSlot.HEAD,ItemStack.EMPTY);expose(mob);check(mob.isOnFire(),"Uncovered native mob burns again");mob.discard();
|
||||
}
|
||||
private static FamiliarEntity probe(ServerPlayer p,Item item){var pet=FamiliarService.TYPE.create(p.level(),EntitySpawnReason.COMMAND);pet.bind(p,egg(item));pet.setPos(.5,1,2.5);pet.setNoAi(true);return pet;}
|
||||
private static void expose(Mob mob){for(int i=0;i<400;i++)mob.aiStep();}
|
||||
private static void roof(ServerPlayer p,boolean present){for(int x=-3;x<=3;x++)for(int z=-1;z<=5;z++)p.level().setBlockAndUpdate(new BlockPos(x,4,z),(present?Blocks.STONE:Blocks.AIR).defaultBlockState());}
|
||||
private static void fresh(ServerPlayer p,Item item){FamiliarBattle.cancel(p);p.setAttached(FamiliarBattle.COMBAT_LOCK,0L);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(item));p.tickCount+=10-p.tickCount%10;FamiliarService.tick(p);}
|
||||
private static void duelSunfire(ServerPlayer p){
|
||||
var s=p.level().getServer();fresh(p,Items.ZOMBIE_SPAWN_EGG);
|
||||
var profile=new com.mojang.authlib.GameProfile(UUID.randomUUID(),"Shade062");
|
||||
var q=new ServerPlayer(s,p.level(),profile,net.minecraft.server.level.ClientInformation.createDefault());
|
||||
var connection=new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);new io.netty.channel.embedded.EmbeddedChannel(connection);
|
||||
q.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(s,connection,q,net.minecraft.server.network.CommonListenerCookie.createInitial(profile,false));q.setPos(p.position().add(0,0,4));
|
||||
q.setAttached(fr.koka.sanctuary.progression.ProgressionService.RECORD,fr.koka.sanctuary.progression.ProgressRecord.INITIAL.json());fr.koka.sanctuary.progression.ProgressEffects.apply(q,fr.koka.sanctuary.progression.ProgressRecord.INITIAL);fresh(q,Items.COW_SPAWN_EGG);
|
||||
check(FamiliarDuels.invite(p,q).equals("duel.sent"),"Sunlight duel accepted");request(q,"accept");request(p,"ready");request(q,"ready");
|
||||
var match=FamiliarDuels.match(p);check(match.running(),"Sunlight duel started");match.phase=FamiliarDuels.Phase.FIGHT;
|
||||
var pet=FamiliarService.active(p);float health=pet.getHealth();
|
||||
pet.hurtServer(p.level(),p.damageSources().playerAttack(q),2);check(pet.getHealth()==health,"Player interference is still blocked in a duel");
|
||||
pet.hurtServer(p.level(),p.damageSources().generic(),2);check(pet.getHealth()==health,"Unrelated environmental damage is still blocked in a duel");
|
||||
pet.setHealth(.5F);pet.setRemainingFireTicks(160);pet.baseTick();check(pet.isRemoved()&&FamiliarDuels.match(p)==null&&FamiliarBattle.bond(p).knockedOutUntil()>FamiliarBattle.now(),"On-fire KO finishes a duel normally");
|
||||
AccessoryAccess.of(q).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(q);q.discard();
|
||||
}
|
||||
private static void request(ServerPlayer p,String action){p.tickCount+=4;var m=FamiliarDuels.match(p);FamiliarDuels.request(p,m.id.toString(),m.revision,action,-1,0);}
|
||||
private static ItemStack egg(Item item){var egg=new ItemStack(item);var data=new CompoundTag();data.putInt("schema",1);data.putInt("per_mille",1000);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,data));return egg;}
|
||||
private static void command(MinecraftServer s,String cmd){s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),cmd);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void click(ClientGameTestContext c,int id){c.runOnClient(m->{var e=m.level.getEntity(id);m.gameMode.interact(m.player,e,new EntityHitResult(e,e.getEyePosition()),InteractionHand.MAIN_HAND);});}
|
||||
private static void check(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+3
-1
@@ -88,7 +88,9 @@ public final class BoatsHead049GameTests {
|
||||
h.assertTrue(primary.size()==v.tiles(),"Primary places occupy every module before secondaries");
|
||||
if(v.columns()!=v.rows()){
|
||||
b.setYRot(0);double x=b.getBoundingBox().getXsize(),z=b.getBoundingBox().getZsize();
|
||||
h.assertTrue((x>z)==(v.columns()>v.rows()),"Dimensions distinguish transposed recipes");
|
||||
double expectedX=v.raft()?v.columns()*1.25:v.columns()+.25;
|
||||
double expectedZ=v.rows()*1.75+(v.raft()?0:.25);
|
||||
h.assertTrue(Math.abs(x-expectedX)<.0001&&Math.abs(z-expectedZ)<.0001,"Bounds cover tiled hull and exterior rails");
|
||||
b.setYRot(90);h.assertTrue(Math.abs(x-b.getBoundingBox().getZsize())<.0001&&Math.abs(z-b.getBoundingBox().getXsize())<.0001,"Collision bounds follow orientation");
|
||||
var e=FlightBalance.Engines.NONE.add(FlightBalance.Kind.ALLAY,false);
|
||||
h.assertTrue(FlightBalance.profile(e,2,v.columns(),v.rows()).equals(FlightBalance.profile(e,2,v.rows(),v.columns())),"Transpose has identical engine balance");
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public final class Building009GameTests {
|
||||
h.assertTrue(ProgressEffects.get(p).building()==rank && BuildingLimits.diameter(rank)==1+2*rank,"Each rank grows the diameter");
|
||||
}
|
||||
h.assertTrue(p.experienceLevel==0 && !ProgressionService.purchase(p,"building",7),"111 levels pay seven ranks, no eighth purchase");
|
||||
h.assertTrue(p.blockInteractionRange()==reach && p.entityInteractionRange()==attack,"Neither block nor attack reach is changed");
|
||||
h.assertTrue(p.blockInteractionRange()==reach && p.entityInteractionRange()==attack,"Native block interaction and attack attributes are unchanged; placement has its own skill range");
|
||||
for (Direction face:Direction.values()) for (Direction look:List.of(Direction.NORTH,Direction.EAST,Direction.SOUTH,Direction.WEST)) {
|
||||
var plane=BuildPlane.positions(BlockPos.ZERO,face,look,7);
|
||||
h.assertTrue(plane.size()==225 && new HashSet<>(plane).size()==225 && plane.getFirst().equals(BlockPos.ZERO),"Every orientation makes 225 unique positions, centre first");
|
||||
|
||||
@@ -33,7 +33,7 @@ public final class Mining008GameTests {
|
||||
@GameTest
|
||||
public void purchaseRanksAndNativeSpeed(GameTestHelper h) {
|
||||
var p=player(h,0);p.giveExperienceLevels(111);
|
||||
h.assertTrue(p.getAttributeValue(Attributes.BLOCK_BREAK_SPEED)==1,"Rank zero keeps vanilla mining speed");
|
||||
h.assertTrue(p.getAttributeValue(Attributes.BLOCK_BREAK_SPEED)==.75,"Rank zero starts at 75 percent of vanilla mining speed");
|
||||
var other=net.minecraft.resources.Identifier.fromNamespaceAndPath("test","other_mining_modifier");
|
||||
p.getAttribute(Attributes.BLOCK_BREAK_SPEED).addTransientModifier(new AttributeModifier(other,.5,AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL));
|
||||
for (int i=1;i<=7;i++) {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
|
||||
public final class Plans072GameTests {
|
||||
private static final Set<UUID> DENIED=new HashSet<>();
|
||||
static { UseBlockCallback.EVENT.register((p,l,h,hit)->DENIED.contains(p.getUUID())?InteractionResult.FAIL:InteractionResult.PASS); }
|
||||
@GameTest public void creativePasteAndSurvivalAuthority(GameTestHelper h){
|
||||
var player=Progression003GameTests.player(h,GameType.SURVIVAL);player.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(-4,4,-4)));
|
||||
var origin=h.absolutePos(new BlockPos(1,4,1));var plan=fixture();
|
||||
for(var c:plan.cells())h.getLevel().setBlockAndUpdate(origin.offset(c.pos()),Blocks.AIR.defaultBlockState());
|
||||
h.assertTrue(PlanService.paste(player,plan,origin).equals("creative_only"),"Survival operator cannot paste");
|
||||
h.assertTrue(h.getLevel().getBlockState(origin).isAir(),"Survival never modifies the world");
|
||||
// GameTest's mock isCreative() is fixed to its creation mode.
|
||||
var creative=Progression003GameTests.player(h,GameType.CREATIVE);creative.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(-4,4,-4)));
|
||||
String result=PlanService.paste(creative,plan,origin);
|
||||
h.assertTrue(result.equals("placed"),"Creative paste succeeds in a loaded clear area: "+result);
|
||||
for(var c:plan.cells())h.assertTrue(h.getLevel().getBlockState(origin.offset(c.pos())).getBlock()==c.state().getBlock(),"Native world contains expected blocks");
|
||||
h.assertTrue(PlanService.paste(creative,plan,origin).equals("placed"),"Already matching blocks are not replaced");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void preflightPreservesOccupiedBlocksAndPermissions(GameTestHelper h){
|
||||
var player=Progression003GameTests.player(h,GameType.CREATIVE);player.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(-4,4,-4)));
|
||||
var origin=h.absolutePos(new BlockPos(1,4,1));var plan=fixture();
|
||||
for(var c:plan.cells())h.getLevel().setBlockAndUpdate(origin.offset(c.pos()),Blocks.AIR.defaultBlockState());
|
||||
var conflict=origin.offset(plan.cells().getLast().pos());h.getLevel().setBlockAndUpdate(conflict,Blocks.CHEST.defaultBlockState());
|
||||
var chest=h.getLevel().getBlockEntity(conflict);
|
||||
h.assertTrue(PlanService.paste(player,plan,origin).equals("occupied"),"One conflict rejects the complete paste");
|
||||
h.assertTrue(h.getLevel().getBlockState(origin).isAir()&&h.getLevel().getBlockEntity(conflict)==chest,"No partial placement and original chest retained");
|
||||
h.getLevel().setBlockAndUpdate(conflict,Blocks.AIR.defaultBlockState());DENIED.add(player.getUUID());
|
||||
try{h.assertTrue(PlanService.paste(player,plan,origin).equals("denied"),"Fabric permission callback vetoes paste");}
|
||||
finally{DENIED.remove(player.getUUID());}
|
||||
for(var c:plan.cells())h.assertTrue(h.getLevel().getBlockState(origin.offset(c.pos())).isAir(),"Veto leaves all destinations empty");
|
||||
var restricted=new BuildPlan("Restricted",1,1,1,List.of(new BuildPlan.Cell(BlockPos.ZERO,Blocks.COMMAND_BLOCK.defaultBlockState())));
|
||||
h.assertTrue(PlanService.paste(player,restricted,origin).equals("restricted_block"),"No command block through plan import");
|
||||
h.assertTrue(PlanService.paste(player,plan,origin.offset(1000,0,0)).equals("distance"),"Far imports cannot generate chunks");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void litematicCrossWordPaletteAndNegativeRegions(GameTestHelper h) throws Exception {
|
||||
var cells=new ArrayList<BuildPlan.Cell>();
|
||||
var materials=new ArrayList<Block>();
|
||||
for(var dye:net.minecraft.world.item.DyeColor.values())materials.add(BuiltInRegistries.BLOCK.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(dye.getName()+"_wool")));
|
||||
materials.add(Blocks.STONE);materials.add(Blocks.BRICKS);
|
||||
for(int x=0;x<31;x++)cells.add(new BuildPlan.Cell(new BlockPos(x,0,0),materials.get(x%materials.size()).defaultBlockState()));
|
||||
var input=new BuildPlan("Palette",31,1,1,cells);var encoded=SchematicIO.encode(input);var output=SchematicIO.decode(encoded,"Palette");
|
||||
h.assertTrue(output.equals(input),"Blockstates survive 5-bit packed palette boundaries");
|
||||
// Independent 5-bit witness: index 12 straddles bit 60 of the first long.
|
||||
long[] words={17L<<60,1};h.assertTrue(SchematicIO.unpack(words,5,12)==17,"Bitstream crosses a 64-bit word without padding");
|
||||
var region=encoded.getCompoundOrEmpty("Regions").getCompoundOrEmpty("Main");
|
||||
var size=region.getCompoundOrEmpty("Size");size.putInt("x",-31);region.put("Size",size);
|
||||
var origin=region.getCompoundOrEmpty("Position");origin.putInt("x",50);region.put("Position",origin);
|
||||
h.assertTrue(SchematicIO.decode(encoded,"Palette").equals(input),"Negative region size moves the minimum, does not reverse indices");
|
||||
var dir=java.nio.file.Files.createTempDirectory("sanctuary-plan072-");
|
||||
try{
|
||||
var file=SchematicIO.write(input,dir);h.assertTrue(SchematicIO.read(file).equals(input),"Compressed disk export opens as a plan");
|
||||
var again=SchematicIO.write(input,dir);h.assertTrue(!file.equals(again),"Repeated export preserves earlier file");
|
||||
java.nio.file.Files.delete(file);java.nio.file.Files.delete(again);
|
||||
}finally{java.nio.file.Files.delete(dir);}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void sharedFormatsAndStrictInvalidInput(GameTestHelper h) throws Exception {
|
||||
for(int version:List.of(2,3)){
|
||||
var root=new CompoundTag();root.putInt("Version",version);root.putInt("DataVersion",0);root.putShort("Width",(short)2);root.putShort("Height",(short)1);root.putShort("Length",(short)1);
|
||||
var palette=new CompoundTag();palette.putInt("minecraft:stone",0);palette.putInt("minecraft:oak_stairs[facing=east,half=top,shape=straight,waterlogged=false]",1);
|
||||
var data=new CompoundTag();data.put("Palette",palette);data.putByteArray(version==3?"Data":"BlockData",new byte[]{0,1});
|
||||
if(version==3){root.put("Blocks",data);var outer=new CompoundTag();outer.put("Schematic",root);root=outer;}else root.merge(data);
|
||||
var plan=SchematicIO.decode(root,"Sponge");
|
||||
h.assertTrue(plan.cells().size()==2&&plan.cells().get(1).state().getValue(BlockStateProperties.HORIZONTAL_FACING)==Direction.EAST,"Sponge v"+version+" reads native state properties");
|
||||
}
|
||||
var vanilla=new CompoundTag();vanilla.put("size",ints(1,1,1));
|
||||
var palette=new ListTag();palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState()));vanilla.put("palette",palette);
|
||||
var block=new CompoundTag();block.put("pos",ints(0,0,0));block.putInt("state",0);var entries=new ListTag();entries.add(block);vanilla.put("blocks",entries);
|
||||
h.assertTrue(SchematicIO.decode(vanilla,"Vanilla").cells().size()==1,"Vanilla structure imports");
|
||||
var bad=SchematicIO.encode(fixture());bad.getCompoundOrEmpty("Regions").getCompoundOrEmpty("Main").putLongArray("BlockStates",new long[0]);reject(h,()->SchematicIO.decode(bad,"Bad"));
|
||||
reject(h,()->SchematicIO.parseState("minecraft:no_such_block"));
|
||||
reject(h,()->SchematicIO.parseState("minecraft:oak_stairs[facing=garbage]"));
|
||||
reject(h,()->SchematicIO.parseState("minecraft:stone{foo:1}"));
|
||||
var newer=SchematicIO.encode(fixture());newer.putInt("MinecraftDataVersion",Integer.MAX_VALUE);reject(h,()->SchematicIO.decode(newer,"Future"));
|
||||
var huge=SchematicIO.encode(fixture());huge.getCompoundOrEmpty("Regions").getCompoundOrEmpty("Main").getCompoundOrEmpty("Size").putInt("x",Integer.MIN_VALUE);reject(h,()->SchematicIO.decode(huge,"Huge"));
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void independentlyWrittenCommunityFormats(GameTestHelper h)throws Exception{
|
||||
for(String extension:List.of("litematic","schem","nbt")){
|
||||
try(var stream=getClass().getResourceAsStream("/fixtures/plans072/independent."+extension+".base64.txt")){
|
||||
h.assertTrue(stream!=null,"Independent fixture packaged: "+extension);
|
||||
var compressed=new java.io.ByteArrayInputStream(Base64.getMimeDecoder().decode(stream.readAllBytes()));
|
||||
var plan=SchematicIO.decode(NbtIo.readCompressed(compressed,NbtAccounter.create(SchematicIO.MAX_NBT)),"Independent");
|
||||
h.assertTrue(plan.width()==5&&plan.height()==2&&plan.depth()==3&&plan.cells().size()==3,"Independent dimensions and sparse block count: "+extension);
|
||||
var blocks=new HashMap<BlockPos,BlockState>();plan.cells().forEach(c->blocks.put(c.pos(),c.state()));
|
||||
h.assertTrue(blocks.get(BlockPos.ZERO).is(Blocks.WOOL.red()),"Independent red wool at origin: "+extension);
|
||||
h.assertTrue(blocks.get(new BlockPos(4,1,2)).getValue(BlockStateProperties.HORIZONTAL_FACING)==Direction.WEST,"Independent UV-free oriented stairs: "+extension);
|
||||
}
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void modelGeometryAlphaColorAndRotation(GameTestHelper h){
|
||||
var texture=new StatueVoxelizer.Texture(2,1,new int[]{0xFFFF0000,0x00000000});
|
||||
var face=new StatueVoxelizer.Face(new StatueVoxelizer.Vertex(0,0,0,0,0),new StatueVoxelizer.Vertex(1,0,0,1,0),new StatueVoxelizer.Vertex(1,1,0,1,1),new StatueVoxelizer.Vertex(0,1,0,0,1),texture,-1);
|
||||
var voxels=StatueVoxelizer.convert(List.of(face),16);
|
||||
h.assertTrue(voxels.keySet().stream().mapToInt(BlockPos::getY).max().orElseThrow()==15,"Requested height preserved");
|
||||
h.assertTrue(voxels.keySet().stream().noneMatch(p->p.getX()>8),"Transparent texture half never becomes blocks");
|
||||
h.assertTrue(voxels.values().stream().allMatch(c->c==0xFFFF0000),"UV color sampling is exact without background colors");
|
||||
var plan=fixture();h.assertTrue(plan.rotate().rotate().rotate().rotate().equals(plan),"Four rotations preserve coordinates and native blockstate direction");
|
||||
h.succeed();
|
||||
}
|
||||
private static ListTag ints(int...values){var list=new ListTag();for(int n:values)list.add(IntTag.valueOf(n));return list;}
|
||||
@FunctionalInterface private interface Checked {void run()throws Exception;}
|
||||
private static void reject(GameTestHelper h,Checked work)throws Exception{boolean refused=false;try{work.run();}catch(IOException|IllegalArgumentException e){refused=true;}h.assertTrue(refused,"Invalid input rejected explicitly");}
|
||||
private static BuildPlan fixture(){return new BuildPlan("Fixture",3,1,1,List.of(new BuildPlan.Cell(BlockPos.ZERO,Blocks.STONE.defaultBlockState()),new BuildPlan.Cell(new BlockPos(1,0,0),Blocks.WOOL.red().defaultBlockState()),new BuildPlan.Cell(new BlockPos(2,0,0),Blocks.OAK_STAIRS.defaultBlockState().setValue(BlockStateProperties.HORIZONTAL_FACING,Direction.EAST))));}
|
||||
}
|
||||
+1
-1
@@ -97,7 +97,7 @@ public final class TerrainAtlas305GameTests {
|
||||
var report = new LinkedHashMap<String, Object>();
|
||||
report.put("schema", 1); report.put("version", version);
|
||||
report.put("revision", System.getProperty("sanctuary.test.atlasRevision", "working-tree"));
|
||||
report.put("minecraft", "26.3-pre-2"); report.put("seed", Long.toString(seed));
|
||||
report.put("minecraft", net.minecraft.SharedConstants.getCurrentVersion().id()); report.put("seed", Long.toString(seed));
|
||||
report.put("diameter", radius * 2); report.put("process_id", ProcessHandle.current().pid());
|
||||
report.put("domain_y", List.of(0, 419)); report.put("build_ceiling_exclusive", 640);
|
||||
report.put("map_step", 12); report.put("vertical_step", 2); report.put("density_scale", 10000);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"sanctuary_gametest:vortex"
|
||||
]
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"dimensions": {
|
||||
"minecraft:overworld": {
|
||||
"type": "sanctuary:sanctuary_640",
|
||||
"generator": {
|
||||
"type": "minecraft:flat",
|
||||
"settings": {
|
||||
"biome": "minecraft:plains",
|
||||
"features": false,
|
||||
"lakes": false,
|
||||
"layers": [
|
||||
{
|
||||
"block": "minecraft:bedrock",
|
||||
"height": 1
|
||||
}
|
||||
],
|
||||
"structure_overrides": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"minecraft:the_end": {
|
||||
"type": "minecraft:the_end",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:the_end"
|
||||
},
|
||||
"settings": "minecraft:end"
|
||||
}
|
||||
},
|
||||
"minecraft:the_nether": {
|
||||
"type": "minecraft:the_nether",
|
||||
"generator": {
|
||||
"type": "minecraft:noise",
|
||||
"biome_source": {
|
||||
"type": "minecraft:multi_noise",
|
||||
"preset": "minecraft:nether"
|
||||
},
|
||||
"settings": "minecraft:nether"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"entrypoints": {
|
||||
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
|
||||
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
|
||||
"fabric-gametest": ["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"],
|
||||
"fabric-gametest": ["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"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Independent schematic fixtures
|
||||
|
||||
Synthetic three-block region, authored for Sanctuary on 2026-09-15.
|
||||
Generated with Litemapy **0.11.0b0**, using its own Litematica, Sponge and vanilla
|
||||
writers. No downloaded build or Minecraft world is included.
|
||||
The gzip files are stored as base64 text because this repository's test resource
|
||||
task applies line filters. The test decodes their exact bytes before parsing.
|
||||
|
||||
- Dimensions: 5 × 2 × 3.
|
||||
- `(0,0,0)` = `minecraft:red_wool`.
|
||||
- `(1,0,0)` = `minecraft:stone`.
|
||||
- `(4,1,2)` = `minecraft:oak_stairs[facing=west,half=top,shape=straight,waterlogged=false]`.
|
||||
- All other cells are air; no entities or block entity contents.
|
||||
|
||||
These exercise an independently implemented writer, in addition to Sanctuary's
|
||||
round-trip and malformed-input tests. Litemapy is a test-fixture generation tool;
|
||||
it is not a build dependency and is not shipped in Sanctuary.
|
||||
|
||||
Source: https://github.com/SmylerMC/litemapy
|
||||
+1
@@ -0,0 +1 @@
|
||||
H4sICBRyqWoC/2luZGVwZW5kZW50LmxpdGVtYXRpYwB9kc9O20AQxie1A44dVFKhHiueAIVrby1QCampLBJxjSb22Fmx3o12xw3hZeilj9b36KybxBKH+rLy92n+/b4UIILTR3JeWQMAJxGk83bVC4MILmbKUOGw4ltkPFrZawrJjBhLUVM4uzOFtl6Zeq5eKILBs1QP5d3J+07eFwizEjj50vLaOvg4R1Nwi253yeT5slLP3DpKILslXzi14W5MAvEPbAjO7w2TsxtyuFJa8S6BZG4r3qIjmHxXTA1udsvp1fX11XQ1jSB7oFo63NjWcDgkhmyhGrpxhEylCL9+/3ldxDAO6syWqlK9LOULy6i/als8+bD5QXm0upV1AD5lcJ47+qloe99gTQEOhC+F03+jfQqjB6rIkSlIYOWCJxy1hwN7OLCHEyrj/8IbwaRbaM5yQo6amKUvQHyAdNYcovqMyh3ZfehlR+Vya60+eu97z7M1dDQuesPi09KzdJSL0rwLgRV5ybLCQhKHeCsJSuEadQUR200CQ7/GjXTz7FDVa3GzrWzttK1r4TysUHvBOILkzrAK7cIl8i95aHqjTXIypQzqrl8oieSN8U23quyNMWQ9qBDfYNwBlgqAv/7b+CT0AgAA
|
||||
@@ -0,0 +1 @@
|
||||
H4sICBRyqWoC/2luZGVwZW5kZW50Lm5idACNkM9qhDAQhycmbv1DKZQ990F67rn01OsydUcN6xpJBoS+TF+1sbBaitYRAnHmm/n5WQDkYIL9JA0A00njSaa7hvIFGd/JB+t6gPIrh4x6tmwpFJGIk3cDdsRM06vJwLzileD+anuqPNb8jNbDrfy4lD2dT6Nz3dx7WHqBXU9z47g0HF5OgePGGF68eTeQn74kg0ONle0bMCMFjoMtdjVodkMGaWhxiNsCe7RNG7vliEy+c01DZ0hr7AJNIoePzlWXH62nHPTgwu1/zI+O2zgOx6uCdUb9YmCDSXYY9Sdri1EC5r8steKVbDBKsGcva89LCbyUwCtZ8dpilIDZy1KCLCXI2vPSAi8t8NICLy3w0gIvLfAyAi8j8DICLyPwMgIvs+IV698vYo2hTQUAAA==
|
||||
@@ -0,0 +1 @@
|
||||
H4sICBRyqWoC/2luZGVwZW5kZW50LnNjaGVtAHWQ0UrDQBBFb7M2aROK4Af4BfkCIS9FQVDRJ30QKUMyu1m67pbdgfg3/qpJK0YfnLd7Zrgzd0pAobomoWeOyQYPVJ8KxY9ClmH5YjvpscyQ37I1vUwwv2dvRqoq5I9aJxZMXr9qjdWNFyuWU3nSm60L7f4PVCifyLEIP9DHqM9KFN9AYfNuPbeRtFyRjafxi5lF7nZDCG7kC4XzuZEkeJ5uV7ibaaD9LslolF41tdabZuAkdU9ONxIOderpwE2SSFPGeiDh6IIx3DWaXOK34/4C62OK6WcjuFxk+L8UvgDKJuEMYQEAAA==
|
||||
@@ -43,6 +43,9 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
fr.koka.sanctuary.sky.VortexClouds.register();
|
||||
fr.koka.sanctuary.loading.PreloadProgress.register();
|
||||
fr.koka.sanctuary.progression.SanctuaryGameplay.register();
|
||||
fr.koka.sanctuary.cosmetics.AccessoryService.register();
|
||||
fr.koka.sanctuary.progression.ProgressionService.register();
|
||||
fr.koka.sanctuary.operator.OperatorService.register();
|
||||
@@ -66,6 +69,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
fr.koka.sanctuary.notifications.DiscoveryNotifications.register();
|
||||
fr.koka.sanctuary.realtime.RealtimeService.register();
|
||||
fr.koka.sanctuary.weather.WeatherService.register();
|
||||
fr.koka.sanctuary.plans.PlanService.register();
|
||||
fr.koka.sanctuary.food.FoodSync.register();
|
||||
fr.koka.sanctuary.food.FoodKnowledgeService.register();
|
||||
fr.koka.sanctuary.grave.GraveService.register();
|
||||
@@ -130,6 +134,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_groves"), CavernGrovesFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_decorations"), CavernDecorationsFeature.CODEC);
|
||||
Registry.register(BuiltInRegistries.FEATURE_TYPE, id("cavern_shore_sugar_cane"), CavernShoreSugarCaneFeature.CODEC);
|
||||
fr.koka.sanctuary.multiblock.Multiblocks.register();
|
||||
LOGGER.info("Sanctuary initialized: the Sanctuary world preset is available.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package fr.koka.sanctuary.arena;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.companion.CompanionInventory;
|
||||
import fr.koka.sanctuary.familiar.FamiliarCatalog;
|
||||
import fr.koka.sanctuary.progression.AtomicJson;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
/** Separate additive escrow journal. Each inventory change has a durable native player receipt. */
|
||||
public final class ArenaLedger {
|
||||
public static final AttachmentType<String> RECEIPT=AttachmentRegistry.create(SanctuaryMod.id("arena_receipt"),b->b.persistent(com.mojang.serialization.Codec.STRING).copyOnDeath());
|
||||
private static final int LIMIT=32*1024*1024;
|
||||
private final MinecraftServer server;
|
||||
private final Path path;
|
||||
private byte[] previous;
|
||||
private JsonObject deposits=new JsonObject(),payouts=new JsonObject();
|
||||
private boolean healthy=true;
|
||||
public ArenaLedger(MinecraftServer server)throws Exception{
|
||||
this.server=server;path=server.getWorldPath(LevelResource.ROOT).resolve("sanctuary/arena-stakes-v1.json");previous=AtomicJson.read(path,LIMIT);
|
||||
if(previous==null)return;
|
||||
var root=AtomicJson.parse(new String(previous,StandardCharsets.UTF_8)).getAsJsonObject();AtomicJson.fields(root,"schema","deposits","payouts");
|
||||
if(root.get("schema").getAsBigDecimal().intValueExact()!=1)throw new IllegalArgumentException("Unknown arena journal");
|
||||
deposits=root.getAsJsonObject("deposits");payouts=root.getAsJsonObject("payouts");
|
||||
for(var e:deposits.entrySet()){
|
||||
UUID.fromString(e.getKey());var d=e.getValue().getAsJsonObject();AtomicJson.fields(d,"player","item","count","match","target","entry","state");validate(d);if(d.get("count").getAsLong()<1||d.get("count").getAsLong()>3456||!d.get("entry").isJsonPrimitive()||!d.get("entry").getAsJsonPrimitive().isBoolean())throw new IllegalArgumentException("Invalid deposit");UUID.fromString(d.get("match").getAsString());UUID.fromString(d.get("target").getAsString());
|
||||
if(!Set.of("pending","held","refund").contains(d.get("state").getAsString()))throw new IllegalArgumentException("Invalid deposit state");
|
||||
// Live combats do not survive restart. Decided payouts are already separate and remain authoritative.
|
||||
d.addProperty("state",d.get("state").getAsString().equals("pending")?"pending":"refund");
|
||||
}
|
||||
for(var e:payouts.entrySet()){UUID.fromString(e.getKey());var d=e.getValue().getAsJsonObject();validate(d);
|
||||
if(d.has("transfer")){AtomicJson.fields(d,"player","item","count","transfer","moving");long moving=d.get("moving").getAsBigDecimal().longValueExact();if(moving<1||moving>3456||moving>d.get("count").getAsLong()||d.get("transfer").getAsString().length()>64)throw new IllegalArgumentException("Invalid transfer");}
|
||||
else AtomicJson.fields(d,"player","item","count");
|
||||
}
|
||||
write();
|
||||
}
|
||||
private void validate(JsonObject d){UUID.fromString(d.get("player").getAsString());if(decode(d.get("item")).getCount()!=1)throw new IllegalArgumentException("Invalid unit item");if(d.get("count").getAsBigDecimal().longValueExact()<0)throw new IllegalArgumentException("Negative stake");}
|
||||
public boolean healthy(){return healthy;}
|
||||
public boolean pending(ServerPlayer p){return payouts.asMap().values().stream().anyMatch(v->v.getAsJsonObject().get("player").getAsString().equals(p.getStringUUID()))||deposits.asMap().values().stream().anyMatch(v->{var d=v.getAsJsonObject();return d.get("player").getAsString().equals(p.getStringUUID())&&!d.get("state").getAsString().equals("held");});}
|
||||
public boolean deposit(UUID match,ServerPlayer p,UUID target,boolean entry,ItemStack item,int count){
|
||||
if(!healthy||!p.isAlive()||p.isRemoved()||item.isEmpty()||count<1||count>3456)return false;
|
||||
claim(p);if(!healthy||pending(p))return false;
|
||||
var plan=new CompanionInventory.Plan(p);if(!plan.take(s->ItemStack.isSameItemSameComponents(s,item),count))return false;
|
||||
try{
|
||||
var id=UUID.randomUUID().toString();var d=amount(p.getUUID(),item,count);d.addProperty("match",match.toString());d.addProperty("target",target.toString());d.addProperty("entry",entry);d.addProperty("state","pending");deposits.add(id,d);write();
|
||||
plan.commit();checkpoint(p,id+"/paid");d.addProperty("state","held");write();return true;
|
||||
}catch(Exception e){fail(e);return false;}
|
||||
}
|
||||
public long pot(UUID match,boolean entry){long n=0;for(var v:deposits.asMap().values()){var d=v.getAsJsonObject();if(d.get("match").getAsString().equals(match.toString())&&d.get("entry").getAsBoolean()==entry&&d.get("state").getAsString().equals("held"))n=Math.addExact(n,d.get("count").getAsLong());}return n;}
|
||||
public boolean hasBet(UUID match,UUID player){return deposits.asMap().values().stream().anyMatch(v->{var d=v.getAsJsonObject();return d.get("match").getAsString().equals(match.toString())&&d.get("player").getAsString().equals(player.toString())&&!d.get("entry").getAsBoolean();});}
|
||||
public void leave(UUID match,UUID player){
|
||||
if(!healthy)return;
|
||||
try{for(var id:new ArrayList<>(deposits.keySet())){var d=deposits.getAsJsonObject(id);
|
||||
if(!d.get("match").getAsString().equals(match.toString())||!d.get("target").getAsString().equals(player.toString()))continue;
|
||||
if(!d.get("state").getAsString().equals("held"))throw new IllegalStateException("Unresolved withdrawn stake");
|
||||
credit(UUID.fromString(d.get("player").getAsString()),decode(d.get("item")),d.get("count").getAsLong());deposits.remove(id);
|
||||
}write();}catch(Exception e){fail(e);}
|
||||
}
|
||||
/** The complete allocation is committed in one write before any player can collect. */
|
||||
public void finish(UUID match,UUID winner){
|
||||
if(!healthy)return;
|
||||
try{
|
||||
var chosen=new LinkedHashMap<String,JsonObject>();for(var e:deposits.entrySet())if(e.getValue().getAsJsonObject().get("match").getAsString().equals(match.toString()))chosen.put(e.getKey(),e.getValue().getAsJsonObject());
|
||||
if(chosen.values().stream().anyMatch(d->!d.get("state").getAsString().equals("held"))){for(var d:chosen.values())if(d.get("state").getAsString().equals("held"))d.addProperty("state","refund");write();return;}
|
||||
if(winner==null){for(var d:chosen.values())credit(UUID.fromString(d.get("player").getAsString()),decode(d.get("item")),d.get("count").getAsLong());}
|
||||
else{
|
||||
for(boolean entry:List.of(true,false)){
|
||||
var pool=chosen.values().stream().filter(d->d.get("entry").getAsBoolean()==entry).toList();if(pool.isEmpty())continue;
|
||||
long total=0;var weights=new TreeMap<UUID,Long>();
|
||||
for(var d:pool){long n=d.get("count").getAsLong();total=Math.addExact(total,n);if(d.get("target").getAsString().equals(winner.toString()))weights.merge(UUID.fromString(d.get("player").getAsString()),n,Math::addExact);}
|
||||
var item=decode(pool.getFirst().get("item"));
|
||||
if(entry)credit(winner,item,total);
|
||||
else if(weights.isEmpty())for(var d:pool)credit(UUID.fromString(d.get("player").getAsString()),item,d.get("count").getAsLong());
|
||||
else for(var allocation:split(total,weights).entrySet())credit(allocation.getKey(),item,allocation.getValue());
|
||||
}
|
||||
}
|
||||
for(var id:chosen.keySet())deposits.remove(id);write();
|
||||
}catch(Exception e){fail(e);}
|
||||
}
|
||||
/** Exact pari-mutuel division; remainder follows stable UUID order. No items are created or rounded away. */
|
||||
public static Map<UUID,Long> split(long total,SortedMap<UUID,Long> weights){
|
||||
long sum=0;for(long n:weights.values())sum=Math.addExact(sum,n);if(sum<=0||total<0)throw new IllegalArgumentException("Invalid pot");
|
||||
var result=new LinkedHashMap<UUID,Long>();long used=0;
|
||||
for(var e:weights.entrySet()){long n=BigInteger.valueOf(total).multiply(BigInteger.valueOf(e.getValue())).divide(BigInteger.valueOf(sum)).longValueExact();result.put(e.getKey(),n);used=Math.addExact(used,n);}
|
||||
for(var id:result.keySet()){if(used==total)break;result.put(id,result.get(id)+1);used++;}return result;
|
||||
}
|
||||
public void claim(ServerPlayer p){
|
||||
if(!healthy||!p.isAlive()||p.isRemoved())return;
|
||||
try{
|
||||
for(var id:new ArrayList<>(deposits.keySet())){
|
||||
var d=deposits.getAsJsonObject(id);if(!d.get("player").getAsString().equals(p.getStringUUID()))continue;String state=d.get("state").getAsString();if(state.equals("held"))continue;
|
||||
if(state.equals("refund")||p.getAttachedOrElse(RECEIPT,"").equals(id+"/paid"))credit(p.getUUID(),decode(d.get("item")),d.get("count").getAsLong());
|
||||
deposits.remove(id);write();
|
||||
}
|
||||
for(var id:new ArrayList<>(payouts.keySet())){
|
||||
var d=payouts.getAsJsonObject(id);if(!d.get("player").getAsString().equals(p.getStringUUID()))continue;
|
||||
if(d.has("transfer")){
|
||||
if(p.getAttachedOrElse(RECEIPT,"").equals(d.get("transfer").getAsString())){checkpoint(p,d.get("transfer").getAsString());d.addProperty("count",d.get("count").getAsLong()-d.get("moving").getAsLong());}
|
||||
d.remove("transfer");d.remove("moving");write();
|
||||
}
|
||||
long left=d.get("count").getAsLong();if(left==0){payouts.remove(id);write();continue;}
|
||||
var item=decode(d.get("item"));var plan=new CompanionInventory.Plan(p);int capacity=0;
|
||||
for(int slot:CompanionInventory.slots(p,true)){var at=p.getInventory().getItem(slot);if(at.isEmpty())capacity+=item.getMaxStackSize();else if(ItemStack.isSameItemSameComponents(at,item))capacity+=Math.max(0,item.getMaxStackSize()-at.getCount());}
|
||||
int moved=(int)Math.min(left,Math.min(capacity,3456));
|
||||
if(moved>0&&!plan.give(item.copyWithCount(moved)))throw new IllegalStateException("Payout capacity changed");
|
||||
left-=moved;
|
||||
if(moved==0)continue;
|
||||
String receipt=UUID.randomUUID()+"/received";d.addProperty("transfer",receipt);d.addProperty("moving",moved);write();plan.commit();checkpoint(p,receipt);
|
||||
d.addProperty("count",left);d.remove("transfer");d.remove("moving");if(left==0)payouts.remove(id);write();
|
||||
}
|
||||
}catch(Exception e){fail(e);}
|
||||
}
|
||||
private JsonObject amount(UUID player,ItemStack item,long count){var d=new JsonObject();d.addProperty("player",player.toString());d.add("item",encode(item.copyWithCount(1)));d.addProperty("count",count);return d;}
|
||||
private void credit(UUID player,ItemStack item,long count){if(count>0)payouts.add(UUID.randomUUID().toString(),amount(player,item,count));}
|
||||
private JsonElement encode(ItemStack stack){return ItemStack.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE,server.registryAccess()),stack).getOrThrow();}
|
||||
private ItemStack decode(JsonElement value){return ItemStack.CODEC.parse(RegistryOps.create(JsonOps.INSTANCE,server.registryAccess()),value).getOrThrow();}
|
||||
private void write()throws Exception{var root=new JsonObject();root.addProperty("schema",1);root.add("deposits",deposits);root.add("payouts",payouts);previous=AtomicJson.write(path,previous,FamiliarCatalog.JSON.toJson(root),LIMIT);}
|
||||
private void checkpoint(ServerPlayer p,String receipt)throws Exception{
|
||||
p.setAttached(RECEIPT,receipt);((fr.koka.sanctuary.mixin.FamiliarPlayerSaveAccess)server.getPlayerList()).sanctuary$saveDuelPlayer(p);
|
||||
var file=server.getWorldPath(LevelResource.PLAYER_DATA_DIR).resolve(p.getStringUUID()+".dat");
|
||||
if(!contains(NbtIo.readCompressed(file,NbtAccounter.create(32L*1024*1024)),receipt))throw new IllegalStateException("Arena receipt not saved");
|
||||
}
|
||||
private boolean contains(Tag tag,String receipt){if(tag instanceof CompoundTag c){if(c.getString("sanctuary:arena_receipt").orElse("").equals(receipt))return true;for(var key:c.keySet())if(contains(c.get(key),receipt))return true;}else if(tag instanceof ListTag list)for(var child:list)if(contains(child,receipt))return true;return false;}
|
||||
private void fail(Exception e){healthy=false;SanctuaryMod.LOGGER.error("Arena items paused; journal retained for recovery",e);}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package fr.koka.sanctuary.arena;
|
||||
|
||||
import com.google.gson.*;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.companion.CompanionInventory;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.networking.v1.*;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class ArenaNetwork {
|
||||
public static final int PAGE_SIZE=12;
|
||||
public record Request(String action,String id,String target,int revision,int slot,int value,int page,String options) implements CustomPacketPayload {
|
||||
public static final Type<Request> TYPE=new Type<>(SanctuaryMod.id("arena_request"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Request> CODEC=CustomPacketPayload.codec((p,b)->{b.writeUtf(p.action,16);b.writeUtf(p.id,36);b.writeUtf(p.target,36);b.writeVarInt(p.revision);b.writeVarInt(p.slot);b.writeVarInt(p.value);b.writeVarInt(p.page);b.writeUtf(p.options,96);},b->new Request(b.readUtf(16),b.readUtf(36),b.readUtf(36),b.readVarInt(),b.readVarInt(),b.readVarInt(),b.readVarInt(),b.readUtf(96)));
|
||||
public Type<Request> type(){return TYPE;}
|
||||
}
|
||||
public record View(String data,ItemStack currency,List<Integer> slots,List<ItemStack> items,boolean open) implements CustomPacketPayload {
|
||||
public static final Type<View> TYPE=new Type<>(SanctuaryMod.id("arena_view"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,View> CODEC=CustomPacketPayload.codec((p,b)->{b.writeUtf(p.data,32767);ItemStack.OPTIONAL_STREAM_CODEC.encode(b,p.currency);b.writeVarInt(p.slots.size());for(int i=0;i<p.slots.size();i++){b.writeVarInt(p.slots.get(i));ItemStack.OPTIONAL_STREAM_CODEC.encode(b,p.items.get(i));}b.writeBoolean(p.open);},b->{var data=b.readUtf(32767);var currency=ItemStack.OPTIONAL_STREAM_CODEC.decode(b);int n=b.readVarInt();if(n<0||n>54)throw new IllegalArgumentException("Invalid arena inventory");var slots=new ArrayList<Integer>();var items=new ArrayList<ItemStack>();for(int i=0;i<n;i++){slots.add(b.readVarInt());items.add(ItemStack.OPTIONAL_STREAM_CODEC.decode(b));}return new View(data,currency,List.copyOf(slots),List.copyOf(items),b.readBoolean());});
|
||||
public Type<View> type(){return TYPE;}
|
||||
}
|
||||
private record Watch(String id,int page,long tick){}
|
||||
private static final Map<ServerPlayer,Watch> WATCHERS=new WeakHashMap<>();
|
||||
private static final Map<ServerPlayer,Integer> REQUESTS=new WeakHashMap<>();
|
||||
private ArenaNetwork(){}
|
||||
public static void register(){
|
||||
PayloadTypeRegistry.serverboundPlay().register(Request.TYPE,Request.CODEC);PayloadTypeRegistry.clientboundPlay().register(View.TYPE,View.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(Request.TYPE,(r,c)->c.server().execute(()->request(c.player(),r)));
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((h,s)->{WATCHERS.remove(h.player);REQUESTS.remove(h.player);});
|
||||
}
|
||||
public static void request(ServerPlayer p,Request r){
|
||||
if(r.action.equals("close")){WATCHERS.remove(p);return;}
|
||||
int tick=p.level().getServer().getTickCount();if(tick-REQUESTS.getOrDefault(p,-100)<3)return;REQUESTS.put(p,tick);
|
||||
if(p.isSpectator()&&!Set.of("open","refresh","claim").contains(r.action))return;
|
||||
UUID id=null;try{if(!r.id.isEmpty())id=UUID.fromString(r.id);}catch(IllegalArgumentException e){return;}
|
||||
var m=id==null?null:Arenas.get(p.level().getServer(),id);String message="";boolean open=r.action.equals("open");
|
||||
if(m!=null&&!Set.of("open","refresh","claim","bet").contains(r.action)&&r.revision!=m.revision)message="changed";
|
||||
else try{
|
||||
message=switch(r.action){
|
||||
case "open","refresh"->"";
|
||||
case "create"->{String[] values=r.options.split(",");if(values.length!=5)yield "unavailable";var rules=new Arenas.Rules(Arenas.Mode.valueOf(values[0]),Boolean.parseBoolean(values[1]),Boolean.parseBoolean(values[2]),Integer.parseInt(values[3]),Integer.parseInt(values[4]));yield Arenas.create(p,rules,r.slot,r.value);}
|
||||
case "join"->Arenas.join(m,p);
|
||||
case "ready"->Arenas.ready(m,p);
|
||||
case "start"->Arenas.start(m,p);
|
||||
case "leave"->{if(m==null||m!=Arenas.match(p))yield "unavailable";Arenas.withdraw(p);yield "updated";}
|
||||
case "cancel"->{if(m==null||m.legacy||!m.host.equals(p.getUUID())||m.phase!=Arenas.Phase.OPEN)yield "unavailable";Arenas.finish(m,null,"cancelled");yield "updated";}
|
||||
case "bet"->Arenas.bet(m,p,UUID.fromString(r.target),r.value);
|
||||
case "claim"->{var ledger=Arenas.ledger(p.level().getServer());if(ledger!=null)ledger.claim(p);yield "updated";}
|
||||
default->"unavailable";
|
||||
};
|
||||
}catch(IllegalArgumentException e){message="unavailable";}
|
||||
if(message.equals("created"))m=Arenas.match(p);
|
||||
WATCHERS.put(p,new Watch(m==null?"":m.id.toString(),Math.max(0,Math.min(r.page,1000000)),tick));send(p,message,open);
|
||||
}
|
||||
public static void tick(MinecraftServer s){
|
||||
if(s.getTickCount()%20!=0)return;
|
||||
for(var p:List.copyOf(WATCHERS.keySet()))if(p.level().getServer()==s){if(s.getTickCount()-WATCHERS.get(p).tick>140||p.isRemoved())WATCHERS.remove(p);else send(p,"",false);}
|
||||
}
|
||||
private static String clip(String value){return value.length()>160?value.substring(0,160):value;}
|
||||
private static JsonObject summary(Arenas.Match m){var d=new JsonObject();d.addProperty("id",m.id.toString());d.addProperty("name",m.name);d.addProperty("phase",m.phase.name());d.addProperty("mode",m.rules.mode().name());d.addProperty("duel",m.rules.duel());d.addProperty("size",m.entrants.size());d.addProperty("entry",m.entry);return d;}
|
||||
public static void send(ServerPlayer p,String message,boolean open){
|
||||
if(!ServerPlayNetworking.canSend(p,View.TYPE))return;var watch=WATCHERS.getOrDefault(p,new Watch("",0,0));var data=new JsonObject();data.addProperty("message",message);
|
||||
var ledger=Arenas.ledger(p.level().getServer());data.addProperty("pending",ledger!=null&&ledger.pending(p));data.addProperty("healthy",ledger!=null&&ledger.healthy());data.addProperty("enrolled",Arenas.enrolled(p));
|
||||
var events=new ArrayList<>(Arenas.list(p.level().getServer()));events.sort(Comparator.comparingInt(m->m.phase==Arenas.Phase.FINISHED?1:0));
|
||||
Arenas.Match m=watch.id.isEmpty()?null:Arenas.get(p.level().getServer(),UUID.fromString(watch.id));var currency=ItemStack.EMPTY;
|
||||
if(m==null){int page=Math.min(watch.page,Math.max(0,(events.size()-1)/PAGE_SIZE));data.addProperty("page",page);data.addProperty("total",events.size());var list=new JsonArray();events.stream().skip((long)page*PAGE_SIZE).limit(PAGE_SIZE).forEach(e->list.add(summary(e)));data.add("events",list);}
|
||||
else{
|
||||
var d=summary(m);currency=m.currency.copy();d.addProperty("legacy",m.legacy);d.addProperty("revision",m.revision);d.addProperty("host",m.host.toString());d.addProperty("radius",m.rules.radius());d.addProperty("seconds",m.rules.seconds());d.addProperty("supplies",m.rules.supplies());d.addProperty("remaining",Math.max(0,(m.until-fr.koka.sanctuary.familiar.FamiliarBattle.now()+999)/1000));d.addProperty("dimension",m.level.dimension().identifier().toString());d.addProperty("x",(int)m.center.x);d.addProperty("y",(int)m.center.y);d.addProperty("z",(int)m.center.z);d.addProperty("winner",m.winner==null?"":m.entrants.get(m.winner).name);d.addProperty("reason",m.reason);
|
||||
d.addProperty("entryPot",m.phase==Arenas.Phase.FINISHED?m.entryPot:ledger==null?0:ledger.pot(m.id,true));d.addProperty("betPot",m.phase==Arenas.Phase.FINISHED?m.betPot:ledger==null?0:ledger.pot(m.id,false));
|
||||
var own=m.entrants.get(p.getUUID());d.addProperty("participant",own!=null);d.addProperty("ready",own!=null&&own.ready);d.addProperty("out",own!=null&&own.out);
|
||||
int page=Math.min(watch.page,Math.max(0,(m.entrants.size()-1)/PAGE_SIZE));data.addProperty("page",page);data.addProperty("total",m.entrants.size());var entrants=new JsonArray();m.entrants.values().stream().skip((long)page*PAGE_SIZE).limit(PAGE_SIZE).forEach(e->{var row=new JsonObject();row.addProperty("id",e.id.toString());row.addProperty("name",e.name.length()>160?e.name.substring(0,160):e.name);row.addProperty("ready",e.ready);row.addProperty("out",e.out);row.addProperty("damage",Math.round(e.damage*10)/10F);var bond=fr.koka.sanctuary.familiar.FamiliarBattle.bond(e.player);row.addProperty("pet",bond==null?"":clip(fr.koka.sanctuary.familiar.FamiliarBattle.egg(e.player).getHoverName().getString()));row.addProperty("health",Math.round(e.player.getHealth()*10)/10F);row.addProperty("petHealth",Math.round(fr.koka.sanctuary.familiar.FamiliarLevels.current(bond)*10)/10F);entrants.add(row);});d.add("entrants",entrants);data.add("match",d);
|
||||
}
|
||||
var slots=CompanionInventory.slots(p,true);ServerPlayNetworking.send(p,new View(data.toString(),currency,slots,slots.stream().map(i->p.getInventory().getItem(i).copy()).toList(),open));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package fr.koka.sanctuary.arena;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.companion.CompanionInventory;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.*;
|
||||
import net.fabricmc.fabric.api.event.player.*;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Server-owned free-for-all events. Registration is consent to immutable, publicly visible rules. */
|
||||
public final class Arenas {
|
||||
public enum Mode { FAMILIARS, PLAYERS, MIXED }
|
||||
public enum Phase { OPEN, COUNTDOWN, FIGHT, FINISHED }
|
||||
public record Rules(Mode mode,boolean duel,boolean supplies,int radius,int seconds) {
|
||||
public Rules { if(!Set.of(24,48,96).contains(radius)||!Set.of(180,300,600).contains(seconds))throw new IllegalArgumentException("Invalid arena rules"); }
|
||||
}
|
||||
public static final class Entrant {
|
||||
public final UUID id,individual;
|
||||
public final String name;
|
||||
public final ServerPlayer player;
|
||||
public boolean ready,out;
|
||||
public float damage;
|
||||
Entrant(ServerPlayer p){player=p;id=p.getUUID();name=p.getDisplayName().getString();individual=FamiliarIdentity.get(FamiliarBattle.egg(p));}
|
||||
}
|
||||
public static final class Match {
|
||||
public final UUID id,host;
|
||||
public final String name;
|
||||
public final ServerLevel level;
|
||||
public final Vec3 center;
|
||||
public final Rules rules;
|
||||
public final ItemStack currency;
|
||||
public final int entry;
|
||||
public final LinkedHashMap<UUID,Entrant> entrants=new LinkedHashMap<>();
|
||||
public Phase phase=Phase.OPEN;
|
||||
public int revision=1;
|
||||
public long until,entryPot,betPot;
|
||||
public UUID winner;
|
||||
public String reason="";
|
||||
public boolean legacy;
|
||||
Match(UUID id,ServerPlayer p,Rules rules,ItemStack currency,int entry){this.id=id;host=p.getUUID();name=p.getDisplayName().getString();level=p.level();center=p.position();this.rules=rules;this.currency=currency.copyWithCount(1);this.entry=entry;until=FamiliarBattle.now()+600000;}
|
||||
public boolean running(){return phase==Phase.COUNTDOWN||phase==Phase.FIGHT;}
|
||||
}
|
||||
private static final Map<MinecraftServer,LinkedHashMap<UUID,Match>> WORLDS=new WeakHashMap<>();
|
||||
private static final Map<MinecraftServer,ArenaLedger> LEDGERS=new WeakHashMap<>();
|
||||
private static final Map<ServerPlayer,Match> ENROLLED=new WeakHashMap<>();
|
||||
private static final Map<ServerPlayer,Long> CREATED=new WeakHashMap<>();
|
||||
private Arenas(){}
|
||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.arena."+key,args);}
|
||||
public static ArenaLedger ledger(MinecraftServer s){return LEDGERS.get(s);}
|
||||
public static Collection<Match> list(MinecraftServer s){return WORLDS.computeIfAbsent(s,k->new LinkedHashMap<>()).values();}
|
||||
public static Match get(MinecraftServer s,UUID id){return WORLDS.computeIfAbsent(s,k->new LinkedHashMap<>()).get(id);}
|
||||
public static Match match(ServerPlayer p){return ENROLLED.get(p);}
|
||||
public static boolean enrolled(ServerPlayer p){return match(p)!=null;}
|
||||
public static boolean running(ServerPlayer p){var m=match(p);return m!=null&&!m.legacy&&m.running();}
|
||||
public static UUID combatId(ServerPlayer p){var m=match(p);return running(p)?m.id:null;}
|
||||
public static void register(){
|
||||
ArenaNetwork.register();
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(s->{try{LEDGERS.put(s,new ArenaLedger(s));}catch(Exception e){SanctuaryMod.LOGGER.error("Arena journal unavailable; file preserved",e);}});
|
||||
ServerTickEvents.END_SERVER_TICK.register(Arenas::tick);
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register(s->{for(var m:List.copyOf(list(s)))finish(m,null,"interrupted");});
|
||||
ServerLifecycleEvents.SERVER_STOPPED.register(s->{WORLDS.remove(s);LEDGERS.remove(s);ENROLLED.keySet().removeIf(p->p.level().getServer()==s);CREATED.keySet().removeIf(p->p.level().getServer()==s);});
|
||||
ServerLifecycleEvents.END_DATA_PACK_RELOAD.register((s,r,ok)->{if(ok)for(var m:List.copyOf(list(s)))if(!m.legacy)finish(m,null,"interrupted");});
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((h,s)->{var m=match(h.player);if(m!=null&&!m.legacy){var e=m.entrants.get(h.player.getUUID());if(e!=null&&!e.out)withdraw(h.player);}});
|
||||
UseBlockCallback.EVENT.register((p,l,h,hit)->p instanceof ServerPlayer s&&running(s)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
AttackBlockCallback.EVENT.register((p,l,h,pos,face)->p instanceof ServerPlayer s&&running(s)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
UseItemCallback.EVENT.register((p,l,h)->p instanceof ServerPlayer s&&running(s)&&(!match(s).rules.supplies()&&consumable(p.getItemInHand(h))||p.getItemInHand(h).getItem() instanceof BlockItem)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
}
|
||||
public static String create(ServerPlayer p,Rules rules,int slot,int count){
|
||||
if(enrolled(p)||FamiliarDuels.match(p)!=null||!available(p,rules)||FamiliarBattle.fighting(p))return "unavailable";
|
||||
if(CREATED.getOrDefault(p,0L)>FamiliarBattle.now())return "wait";
|
||||
var ledger=ledger(p.level().getServer());if(ledger==null||!ledger.healthy())return "storage";
|
||||
ItemStack currency=new ItemStack(Items.EMERALD);
|
||||
if(slot>=0){if(!CompanionInventory.slots(p,true).contains(slot))return "missing";currency=p.getInventory().getItem(slot);if(currency.isEmpty())return "missing";}
|
||||
if(count<0||count>64)return "missing";
|
||||
var m=new Match(UUID.randomUUID(),p,rules,currency,count);
|
||||
if(count>0&&!ledger.deposit(m.id,p,p.getUUID(),true,m.currency,count))return "missing";
|
||||
m.entrants.put(p.getUUID(),new Entrant(p));WORLDS.computeIfAbsent(p.level().getServer(),k->new LinkedHashMap<>()).put(m.id,m);ENROLLED.put(p,m);CREATED.put(p,FamiliarBattle.now()+10000);announce(m,"announced",p.getDisplayName());return "created";
|
||||
}
|
||||
public static boolean consumable(ItemStack stack){return stack.has(net.minecraft.core.component.DataComponents.CONSUMABLE)||stack.is(Items.SPLASH_POTION)||stack.is(Items.LINGERING_POTION)||stack.is(Items.ENDER_PEARL);}
|
||||
private static boolean available(ServerPlayer p,Rules r){return fr.koka.sanctuary.progression.ProgressEffects.get(p)!=null&&p.isAlive()&&!p.isRemoved()&&!p.isSpectator()&&!p.isCreative()&&(r.mode()==Mode.PLAYERS||FamiliarDuels.ready(p));}
|
||||
public static boolean inside(Match m,ServerPlayer p){return p.level()==m.level&&p.position().distanceToSqr(m.center)<=m.rules.radius()*m.rules.radius();}
|
||||
public static String join(Match m,ServerPlayer p){
|
||||
var ledger=ledger(p.level().getServer());
|
||||
if(m==null||m.legacy||m.phase!=Phase.OPEN||enrolled(p)||m.entrants.containsKey(p.getUUID())||FamiliarDuels.match(p)!=null||!available(p,m.rules)||FamiliarBattle.fighting(p)||!inside(m,p)||m.rules.duel()&&m.entrants.size()>=2)return "unavailable";
|
||||
if(ledger==null||ledger.hasBet(m.id,p.getUUID()))return "bettor";
|
||||
if(m.entry>0&&!ledger.deposit(m.id,p,p.getUUID(),true,m.currency,m.entry))return "missing";
|
||||
m.entrants.put(p.getUUID(),new Entrant(p));for(var e:m.entrants.values())e.ready=false;ENROLLED.put(p,m);m.revision++;return "joined";
|
||||
}
|
||||
public static String ready(Match m,ServerPlayer p){if(m==null||m.legacy||m.phase!=Phase.OPEN||!m.entrants.containsKey(p.getUUID()))return "unavailable";var e=m.entrants.get(p.getUUID());e.ready=!e.ready;m.revision++;return "updated";}
|
||||
public static String start(Match m,ServerPlayer p){
|
||||
if(m==null||m.legacy||m.phase!=Phase.OPEN||!m.host.equals(p.getUUID())||m.entrants.size()<2)return "unavailable";
|
||||
for(var e:m.entrants.values())if(!e.ready||!available(e.player,m.rules)||!inside(m,e.player)||FamiliarBattle.fighting(e.player)||m.rules.mode()!=Mode.PLAYERS&&!Objects.equals(e.individual,FamiliarIdentity.get(FamiliarBattle.egg(e.player))))return "not_ready";
|
||||
m.phase=Phase.COUNTDOWN;m.until=FamiliarBattle.now()+10000;m.revision++;
|
||||
for(var e:m.entrants.values()){e.player.stopUsingItem();e.player.stopRiding();e.player.ejectPassengers();fr.koka.sanctuary.mining.MiningManager.interrupt(e.player);fr.koka.sanctuary.building.BuildingManager.interrupt(e.player);FamiliarBattle.cancel(e.player);FamiliarBattle.state(e.player).workMode=false;FamiliarBattle.combat(e.player);}
|
||||
announce(m,"starting",text("mode."+m.rules.mode().name().toLowerCase(Locale.ROOT)));return "started";
|
||||
}
|
||||
public static String bet(Match m,ServerPlayer p,UUID target,int count){
|
||||
if(m==null||m.phase!=Phase.OPEN||count<1||count>3456||!m.entrants.containsKey(target))return "closed";
|
||||
if(m.entrants.containsKey(p.getUUID()))return "participant_bet";
|
||||
var ledger=ledger(p.level().getServer());if(ledger==null||!ledger.deposit(m.id,p,target,false,m.currency,count))return "missing";
|
||||
return "bet_placed";
|
||||
}
|
||||
public static void withdraw(ServerPlayer p){var m=match(p);if(m==null||m.legacy)return;if(m.phase==Phase.OPEN){
|
||||
if(m.host.equals(p.getUUID())){finish(m,null,"cancelled");return;}
|
||||
var ledger=ledger(m.level.getServer());if(ledger!=null){ledger.leave(m.id,p.getUUID());if(!ledger.healthy())return;}m.entrants.remove(p.getUUID());ENROLLED.remove(p);for(var e:m.entrants.values())e.ready=false;m.revision++;if(ledger!=null)for(var q:m.level.getServer().getPlayerList().getPlayers())ledger.claim(q);
|
||||
}else eliminate(p,"forfeit");}
|
||||
public static void knockedOut(ServerPlayer p){var m=match(p);if(m!=null&&!m.legacy&&m.rules.mode()==Mode.FAMILIARS)eliminate(p,"ko");}
|
||||
public static void eliminate(ServerPlayer p,String reason){
|
||||
var m=match(p);if(m==null||m.legacy||!m.running())return;var e=m.entrants.get(p.getUUID());if(e.out)return;e.out=true;m.revision++;FamiliarBattle.cancel(p);p.sendSystemMessage(text("eliminated",text(reason)));
|
||||
var alive=m.entrants.values().stream().filter(x->!x.out).toList();if(alive.size()<=1)finish(m,alive.isEmpty()?null:alive.getFirst().id,reason);
|
||||
}
|
||||
public static void finish(Match m,UUID winner,String reason){
|
||||
if(m==null||m.phase==Phase.FINISHED)return;m.phase=Phase.FINISHED;m.winner=winner;m.reason=reason;m.until=FamiliarBattle.now()+300000;m.revision++;
|
||||
var ledger=ledger(m.level.getServer());if(ledger!=null){m.entryPot=ledger.pot(m.id,true);m.betPot=ledger.pot(m.id,false);ledger.finish(m.id,winner);}
|
||||
for(var e:m.entrants.values()){ENROLLED.remove(e.player,m);FamiliarBattle.cancel(e.player);FamiliarBattle.state(e.player).order=FamiliarBattle.Order.FOLLOW;}
|
||||
announce(m,"result",winner==null?text("nobody"):Component.literal(m.entrants.get(winner).name));
|
||||
if(ledger!=null)for(var p:m.level.getServer().getPlayerList().getPlayers())ledger.claim(p);
|
||||
}
|
||||
public static Boolean enemy(ServerPlayer p,LivingEntity target){
|
||||
var m=match(p);var owner=owner(target);var other=owner==null?null:match(owner);
|
||||
if(m==null||m.legacy)return other!=null&&!other.legacy&&other.running()?false:null;
|
||||
if(!m.running())return null;
|
||||
return m.phase==Phase.FIGHT&&m.rules.mode()!=Mode.PLAYERS&&owner!=p&&other==m&&!m.entrants.get(p.getUUID()).out&&!m.entrants.get(owner.getUUID()).out&&(target instanceof FamiliarEntity||m.rules.mode()==Mode.MIXED);
|
||||
}
|
||||
public static ServerPlayer owner(Entity e){if(e instanceof FamiliarEntity f)return f.owner();if(e instanceof ServerPlayer p)return p;if(e instanceof net.minecraft.world.entity.projectile.Projectile shot)return owner(shot.getOwner());return null;}
|
||||
public static Boolean playerEnemy(ServerPlayer p,Player q){
|
||||
var m=match(p);var other=q instanceof ServerPlayer s?match(s):null;
|
||||
if((m==null||m.legacy||!m.running())&&(other==null||other.legacy||!other.running()))return null;
|
||||
return m!=null&&m==other&&!m.legacy&&m.phase==Phase.FIGHT&&m.rules.mode()!=Mode.FAMILIARS&&p!=q&&!m.entrants.get(p.getUUID()).out&&!m.entrants.get(q.getUUID()).out;
|
||||
}
|
||||
/** null = ordinary world rules. No damage can cross the consent boundary. */
|
||||
public static Boolean damageAllowed(LivingEntity target,DamageSource source){
|
||||
var victim=owner(target);var attacker=owner(source.getEntity());var m=victim==null?null:match(victim);var a=attacker==null?null:match(attacker);
|
||||
boolean v=m!=null&&!m.legacy&&m.running(),s=a!=null&&!a.legacy&&a.running();
|
||||
if(!v&&!s)return null;
|
||||
if(!v)return false;
|
||||
if(m.phase!=Phase.FIGHT||m.entrants.get(victim.getUUID()).out)return false;
|
||||
if(target instanceof Player&&m.rules.mode()==Mode.FAMILIARS)return false;
|
||||
if(target instanceof FamiliarEntity&&m.rules.mode()==Mode.PLAYERS)return false;
|
||||
if(attacker==null)return true;
|
||||
if(!s||a!=m||m.entrants.get(attacker.getUUID()).out||attacker==victim)return false;
|
||||
if(m.rules.mode()==Mode.FAMILIARS&&!(source.getEntity() instanceof FamiliarEntity))return false;
|
||||
if(m.rules.mode()==Mode.PLAYERS&&source.getEntity() instanceof FamiliarEntity)return false;
|
||||
return true;
|
||||
}
|
||||
/** Count real damage after vanilla armor and absorption. Death remains entirely native. */
|
||||
public static void afterDamage(LivingEntity entity,DamageSource source,float lost){
|
||||
var attacker=owner(source.getEntity());if(attacker!=null){var m=match(attacker);if(m!=null&&!m.legacy&&m.phase==Phase.FIGHT)m.entrants.get(attacker.getUUID()).damage+=Math.max(0,lost);}
|
||||
}
|
||||
public static void died(ServerPlayer p){var m=match(p);if(m==null||m.legacy)return;if(m.phase==Phase.OPEN)withdraw(p);else if(m.running())eliminate(p,"death");}
|
||||
public static boolean castAllowed(FamiliarTechniques.Cast cast){var m=match(cast.owner);return m==null||m.legacy||!m.running()||m.phase==Phase.FIGHT&&m.rules.mode()!=Mode.PLAYERS&&!m.entrants.get(cast.owner.getUUID()).out;}
|
||||
public static void tick(MinecraftServer server){
|
||||
long now=FamiliarBattle.now();
|
||||
for(var m:List.copyOf(list(server))){
|
||||
if(m.phase==Phase.FINISHED){if(now>=m.until)WORLDS.get(server).remove(m.id);continue;}if(m.legacy)continue;
|
||||
if(now>=m.until){if(m.phase==Phase.COUNTDOWN){m.phase=Phase.FIGHT;m.until=now+m.rules.seconds()*1000L;m.revision++;announce(m,"fight",text("mode."+m.rules.mode().name().toLowerCase(Locale.ROOT)));}else{finish(m,null,m.phase==Phase.OPEN?"expired":"draw");continue;}}
|
||||
if(!m.running())continue;
|
||||
for(var e:List.copyOf(m.entrants.values())){
|
||||
if(!m.running())break;if(e.out)continue;var p=e.player;
|
||||
if(!inside(m,p)||p.isRemoved()||!p.isAlive()||p.isCreative()||p.isSpectator()){eliminate(p,"left");continue;}
|
||||
if(m.rules.mode()!=Mode.PLAYERS&&(!Objects.equals(e.individual,FamiliarIdentity.get(FamiliarBattle.egg(p)))||FamiliarBattle.bond(p)!=null&&FamiliarBattle.bond(p).recalled())){eliminate(p,"forfeit");continue;}
|
||||
if(m.rules.mode()==Mode.FAMILIARS&&!FamiliarDuels.ready(p)){eliminate(p,"ko");continue;}
|
||||
if(server.getTickCount()%20==0){
|
||||
FamiliarBattle.combat(p);
|
||||
if(m.phase==Phase.FIGHT&&m.rules.mode()!=Mode.PLAYERS&&FamiliarBattle.target(p)==null){
|
||||
LivingEntity nearest=null;double best=24*24;
|
||||
for(var rival:m.entrants.values())if(!rival.out&&rival!=e){LivingEntity target=FamiliarService.active(rival.player);if(target==null&&m.rules.mode()==Mode.MIXED)target=rival.player;if(target!=null&&target.isAlive()&&enemy(p,target)==Boolean.TRUE&&p.distanceToSqr(target)<best){nearest=target;best=p.distanceToSqr(target);}}
|
||||
if(nearest!=null)FamiliarBattle.engage(p,nearest,false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ArenaNetwork.tick(server);
|
||||
}
|
||||
public static void legacyCreated(FamiliarDuels.Match duel){
|
||||
var p=duel.players[0];var m=new Match(duel.id,p,new Rules(Mode.FAMILIARS,true,true,24,180),new ItemStack(Items.EMERALD),0);m.legacy=true;
|
||||
for(var e:duel.players)m.entrants.put(e.getUUID(),new Entrant(e));WORLDS.computeIfAbsent(p.level().getServer(),k->new LinkedHashMap<>()).put(m.id,m);announce(m,"announced",p.getDisplayName());
|
||||
}
|
||||
public static void legacyStarted(FamiliarDuels.Match duel){var m=get(duel.players[0].level().getServer(),duel.id);if(m!=null){m.phase=Phase.COUNTDOWN;m.until=duel.until;announce(m,"starting",text("mode.familiars"));}}
|
||||
public static void legacyFight(FamiliarDuels.Match duel){var m=get(duel.players[0].level().getServer(),duel.id);if(m!=null){m.phase=Phase.FIGHT;m.until=duel.until;}}
|
||||
public static void legacyFinished(FamiliarDuels.Match duel,int winner,String reason){finish(get(duel.players[0].level().getServer(),duel.id),winner<0?null:duel.players[winner].getUUID(),reason);}
|
||||
private static void announce(Match m,String key,Component detail){m.level.getServer().getPlayerList().broadcastSystemMessage(text(key,text(m.rules.duel()?"duel":"arena"),m.id.toString().substring(0,8),detail),false);}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka.sanctuary.boats;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
|
||||
/** Boat seats remain usable; passengers carried on somebody's head keep their protection. */
|
||||
public final class BoatPassengerInteractions {
|
||||
private BoatPassengerInteractions() {}
|
||||
|
||||
public static boolean aboardTogether(Entity actor, Entity target) {
|
||||
return actor instanceof Player && target instanceof Mob && actor.getVehicle() != target
|
||||
&& target.getVehicle() instanceof AbstractBoat boat && actor.getRootVehicle() == boat;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** One native vehicle with a fixed rectangular seating plan. Its movement is always simulated by the server. */
|
||||
public final class CrewBoat extends AbstractBoat {
|
||||
@@ -44,15 +45,43 @@ public final class CrewBoat extends AbstractBoat {
|
||||
return seatOffset(Math.max(0, getPassengers().indexOf(passenger))).yRot((float)Math.toRadians(-getYRot()));
|
||||
}
|
||||
|
||||
@Override protected net.minecraft.world.phys.AABB makeBoundingBox(Vec3 at) {
|
||||
if(columns < 1 || rows < 1 || columns == rows)return super.makeBoundingBox(at);
|
||||
// TiledBoatModel repeats 28px longitudinal floors, 16px wooden / 20px raft
|
||||
// floors across. Wooden exterior rails add 2px per side; paddles are decorative.
|
||||
private double halfHullWidth() { return wood.equals("bamboo") ? columns * 10.0 / 16 : (columns * 8.0 + 2) / 16; }
|
||||
private double halfHullLength() { return (rows * 14.0 + (wood.equals("bamboo") ? 0 : 2)) / 16; }
|
||||
@Override protected AABB makeBoundingBox(Vec3 at) {
|
||||
if(columns < 1 || rows < 1)return super.makeBoundingBox(at); // Entity constructor precedes our fields.
|
||||
double angle=Math.toRadians(getYRot()), c=Math.abs(Math.cos(angle)), s=Math.abs(Math.sin(angle));
|
||||
double x=.6875*(columns*c+rows*s), z=.6875*(rows*c+columns*s);
|
||||
return new net.minecraft.world.phys.AABB(at.x-x,at.y,at.z-z,at.x+x,at.y+getBbHeight(),at.z+z);
|
||||
return hullBox(at,halfHullWidth()*c+halfHullLength()*s,halfHullLength()*c+halfHullWidth()*s);
|
||||
}
|
||||
private AABB hullBox(Vec3 at,double x,double z) {
|
||||
return new AABB(at.x-x,at.y,at.z-z,at.x+x,at.y+getBbHeight(),at.z+z);
|
||||
}
|
||||
@Override public void setYRot(float yaw) {
|
||||
super.setYRot(yaw);
|
||||
if(columns>0 && rows>0 && columns!=rows)setBoundingBox(makeBoundingBox(position()));
|
||||
if(columns>0 && rows>0)setBoundingBox(makeBoundingBox(position()));
|
||||
}
|
||||
|
||||
/** Translation collides natively, but changing yaw must not start that move inside a wall. */
|
||||
private void turnWithCollisions() {
|
||||
float target=getYRot()+turn;
|
||||
if(target==getYRot())return;
|
||||
double from=Math.toRadians(net.minecraft.util.Mth.wrapDegrees(getYRot()));
|
||||
double to=from+Math.toRadians(target-getYRot());
|
||||
var swept=hullBox(position(),sweptExtent(halfHullWidth(),halfHullLength(),from,to),
|
||||
sweptExtent(halfHullLength(),halfHullWidth(),from,to));
|
||||
if(level().noCollision(this,swept.deflate(1.0E-7)))setYRot(target);
|
||||
else turn=0; // Stop angular inertia at contact; keep native tangential/reverse movement.
|
||||
}
|
||||
private static double sweptExtent(double a,double b,double from,double to) {
|
||||
double low=Math.min(from,to),high=Math.max(from,to);
|
||||
double maximum=Math.max(a*Math.abs(Math.cos(from))+b*Math.abs(Math.sin(from)),
|
||||
a*Math.abs(Math.cos(to))+b*Math.abs(Math.sin(to)));
|
||||
// Include extrema between endpoints: checking the target alone misses a corner's arc.
|
||||
double peak=Math.atan2(b,a);
|
||||
for(int k=-2;k<=2;k++)for(double angle:new double[]{k*Math.PI+peak,k*Math.PI-peak})
|
||||
if(angle>=low&&angle<=high)return Math.hypot(a,b);
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public record Orders(int pilots, double forward, double steering, double lift) { }
|
||||
@@ -76,7 +105,7 @@ public final class CrewBoat extends AbstractBoat {
|
||||
var profile = engines.count() == 0 ? null : FlightBalance.profile(engines, FlightMotors.load(this), columns, rows);
|
||||
double response = profile == null ? .1 : profile.response();
|
||||
turn += (float)((orders.steering() * 5 - turn) * response);
|
||||
setYRot(getYRot() + turn);
|
||||
turnWithCollisions();
|
||||
double heading = Math.toRadians(getYRot()), throttle = orders.forward();
|
||||
entityData.set(ROWING, (byte)((throttle != 0 || orders.steering() > 0 ? 1 : 0) | (throttle != 0 || orders.steering() < 0 ? 2 : 0)));
|
||||
if (profile == null) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package fr.koka.sanctuary.building;
|
||||
|
||||
import fr.koka.sanctuary.progression.ProgressEffects;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Placement-only reach: independent mining, native containers/combat and external range bonuses. */
|
||||
public final class BuildReach {
|
||||
private BuildReach() { }
|
||||
public static double range(Player player) {
|
||||
double vanilla=player.blockInteractionRange();
|
||||
var progress=ProgressEffects.get(player);
|
||||
return progress==null || player.isCreative() || player.isSpectator() ? vanilla
|
||||
: Math.max(0,vanilla+BuildingLimits.reach(progress.building())-4.5);
|
||||
}
|
||||
public static boolean within(Player player,BlockPos support,double buffer) {
|
||||
double range=range(player)+buffer;
|
||||
return new AABB(support).distanceToSqr(player.getEyePosition())<range*range;
|
||||
}
|
||||
public static double pickRange(Player player) {
|
||||
double base=Math.max(player.blockInteractionRange(),fr.koka.sanctuary.mining.MiningReach.range(player));
|
||||
return player.getMainHandItem().getItem() instanceof BlockItem || player.getOffhandItem().getItem() instanceof BlockItem
|
||||
? Math.max(base,range(player)) : base;
|
||||
}
|
||||
public static boolean grouped(Player player) {
|
||||
return player instanceof ServerPlayer server && BuildingManager.placing(server);
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,14 @@ public final class BuildSelection {
|
||||
}
|
||||
public static BlockHitResult aimed(ServerPlayer player,BlockPos origin) {
|
||||
if (!player.isAlive() || player.isSpectator() || !usable(player) || !permitted(player,origin)
|
||||
|| !player.isWithinBlockInteractionRange(origin,0)) return null;
|
||||
var hit=player.pick(player.blockInteractionRange(),1,false);
|
||||
|| !BuildReach.within(player,origin,0)) return null;
|
||||
var hit=player.pick(BuildReach.range(player),1,false);
|
||||
return hit instanceof BlockHitResult block && hit.getType()!=HitResult.Type.MISS && block.getBlockPos().equals(origin) ? block : null;
|
||||
}
|
||||
public static Plan plan(Player player,BlockHitResult hit,int rank) {
|
||||
if (!usable(player) || rank<1 || rank>BuildingLimits.MAX_RANK || hit.getType()==HitResult.Type.MISS
|
||||
|| !loaded(player.level(),hit.getBlockPos())) return null;
|
||||
|| !loaded(player.level(),hit.getBlockPos())
|
||||
|| player.getEyePosition().distanceToSqr(hit.getLocation())>Math.pow(BuildReach.range(player),2)) return null;
|
||||
var base=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,player.getMainHandItem(),hit);
|
||||
BlockPos anchor=base.getClickedPos();
|
||||
var view=PlacementOrientation.View.capture(player);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package fr.koka.sanctuary.building;
|
||||
|
||||
/** Diameter is independent from the player's native interaction/attack reach. */
|
||||
/** Surface size and placement reach progress independently; entity reach stays native. */
|
||||
public final class BuildingLimits {
|
||||
public static final int MAX_RANK=7;
|
||||
private static final double[] REACH={3,3.5,4,4.5,4.75,5,5.5,6};
|
||||
private BuildingLimits() { }
|
||||
public static double reach(int rank) {return REACH[Math.clamp(rank,0,MAX_RANK)];}
|
||||
public static int diameter(int rank) {return 1+2*Math.clamp(rank,0,MAX_RANK);}
|
||||
public static int area(int rank) {int d=diameter(rank);return d*d;}
|
||||
public static int requested(int requested,int earned) {return Math.clamp(requested,0,Math.clamp(earned,0,MAX_RANK));}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class BuildingSession {
|
||||
|| player.gameMode.getGameModeForPlayer()!=mode || player.containerMenu!=player.inventoryMenu
|
||||
|| level.getServer().getTickCount()-heartbeat>12 || !sameStack()
|
||||
|| !BuildSelection.permitted(player,plan.hit().getBlockPos())
|
||||
|| !player.isWithinBlockInteractionRange(plan.hit().getBlockPos(),0)) return false;
|
||||
|| !BuildReach.within(player,plan.hit().getBlockPos(),0)) return false;
|
||||
if (!player.isCreative() && ++ticks%2!=0) return true;
|
||||
int budget=player.isCreative() ? 16 : 1;
|
||||
for (int processed=0;index<plan.targets().size() && processed<budget;processed++) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user