Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74dc1265a8 | ||
|
|
678d77fb91 | ||
|
|
97c601cd7f | ||
|
|
a93e261120 | ||
|
|
72f1cf50ae | ||
|
|
6821f6ee12 | ||
|
|
85d274f2ef | ||
|
|
235e3c08b7 | ||
|
|
697e4a205f | ||
|
|
c51e611f4f | ||
|
|
f4d3dad1ca | ||
|
|
653a063b15 | ||
|
|
9d3bc0cee2 |
@@ -2,6 +2,9 @@
|
||||
|
||||
Ce dépôt contient le pack Sanctuary et ses mods. Lire `README.md`, puis le ticket concerné.
|
||||
La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités futures.
|
||||
Pour développer/livrer le site, packwiz, la Beta dédiée et Prism local, lire
|
||||
`docs/updates.md`. La compétence personnelle `sanctuary-updates` mémorise les
|
||||
chemins et accès propres au poste ; ne pas déduire une publication d'un simple push.
|
||||
|
||||
- Avant une modification, lire l'état Git et préserver les changements existants.
|
||||
- Un ticket correspond à un résultat jouable ou vérifiable et à une branche `codex/<sujet>`.
|
||||
|
||||
+113
@@ -1,3 +1,116 @@
|
||||
# beta.293 — Happy Creepers, logo packwiz et mods facultatifs
|
||||
|
||||
- Intégration de la branche `codex/happy-creepers` avec les baleines beta.290.
|
||||
- Logo Sanctuary pendant packwiz et outils prêts dans le ZIP Prism.
|
||||
- Simple Voice Chat, Bobby facultatif, Lithium et Entity Culling épinglés pour 26.3.
|
||||
- Même canal et même instance Prism, profils client/serveur et sauvegardes conservés.
|
||||
- [Contrat et reçu de livraison](docs/release-beta293.md).
|
||||
|
||||
# beta.290 — Baleine fantôme volante
|
||||
|
||||
- Import de l’IA, du modèle animé, de la texture et des effets de la baleine Sanctuary 26.2.
|
||||
- Deux baleines chargées pour le serveur entier, toutes dimensions et sources d’apparition comprises.
|
||||
- Œuf créatif personnalisé depuis la texture native `egg.png` de Minecraft 26.3, silhouette et ombres conservées.
|
||||
- Refus d’un troisième œuf avant consommation, libellés FR/EN et calendrier propre à chaque serveur.
|
||||
- [Contrat et vérifications](docs/flying-whale-beta290.md).
|
||||
|
||||
# beta.278 — Duel Battle pour un claim consenti
|
||||
|
||||
- Proposition depuis le clic droit d’un claim déjà revendiqué ; propriétaire et challenger valident le même enjeu.
|
||||
- Challenger ayant habité le chunk, propriétaire connecté, Carte active et disponibilité Battle contrôlés côté serveur.
|
||||
- Battle privé à deux joueurs : consentement sur le claim, puis confirmations Prêt natives ; aucun tiers admis.
|
||||
- Transfert du seul claim au vainqueur, avec l’échéance de protection conservée et actualisation de la carte/minage.
|
||||
- Refus, annulation avant lancement, égalité et interruption sans résultat durable ne transfèrent rien.
|
||||
- Journal additif de consentement/résultat, application atomique et reprise idempotente ; anciens formats conservés.
|
||||
- Interface de lecture et acceptation FR/EN, enjeu explicite dans Battle, navigation corrigée entre propositions.
|
||||
- [Contrat et vérifications](docs/claim-battle-beta278.md).
|
||||
|
||||
# beta.277 — Claims liés à Demeure
|
||||
|
||||
- Habitation Demeure obligatoire avant de revendiquer un chunk libre ; simple traversée insuffisante.
|
||||
- Priorité à l’habitant le plus influent, avec départage par l’ordre des premières traces en cas d’égalité.
|
||||
- Un habitant plus influent peut prendre le spot tant qu’il n’a pas été revendiqué ; aucun transfert automatique après acquisition.
|
||||
- Vérification serveur avant réservation ou débit, et sauvegarde des traces avant achat.
|
||||
- Carte : condition expliquée, nom/couleur du joueur prioritaire, action bloquée quand nécessaire et actualisation du terrain libre.
|
||||
- Claims antérieurs, protections, tarifs et formats de sauvegarde conservés ; aucun délai supplémentaire ni duel ajouté.
|
||||
- [Règles et vérifications](docs/claim-demeure-beta277.md).
|
||||
|
||||
# beta.276 — Contours CTM et nouvelle protection des claims
|
||||
|
||||
- Propriétés sans protection visibles par leur contour, aux couleurs de leur propriétaire.
|
||||
- Contours raccordés sur les côtés et angles, sans frontières entre chunks d’un même joueur.
|
||||
- Fond coloré sur les chunks protégés ; contour conservé après expiration.
|
||||
- Actualisation des contours voisins après un achat, dans l’Atlas et l’aperçu de Pause ; masquage conservé.
|
||||
- Protection : 8 saphirs par chunk pour trois jours de serveur en marche ; renouvellements ajoutés au temps restant.
|
||||
- Durées déjà achetées et reçus de sept jours en attente conservés, sans changement de schéma.
|
||||
- Libellés FR/EN et légende incluant aussi les propriétés sans protection.
|
||||
- [Contrat et vérifications](docs/claim-contours-beta276.md).
|
||||
|
||||
# beta.275 — Habitant et menu Pause centrés sur l’Atlas
|
||||
|
||||
- Retrait du conseil d’arrivée « première propriété gratuite » et du bouton Propriété d’Habitant.
|
||||
- Propriétés verrouillées côté serveur et client avant l’aptitude Carte achetée et active ; ancien accès carte locale bloqué.
|
||||
- Habitant : statistiques sans fond à gauche, personnage centré, biographie à droite ; navigation Factions / Familier.
|
||||
- Activité et Historique retirés de cette fiche ; données conservées.
|
||||
- Durée de jeu en jours, heures, minutes et secondes entières, sans minutes décimales.
|
||||
- Pause sans Carte conservée ; avec Carte, Gazette et Tableau dans leurs colonnes latérales, carte au centre.
|
||||
- Aventure à gauche et Communauté à droite au-dessus de la carte, y compris en grande vue ; Partie et ses actions natives dessous.
|
||||
- Mise à jour automatique de Pause lors du déblocage ou de la désactivation de Carte ; droits et protections acquis conservés.
|
||||
- [Contrat et vérifications](docs/menu-atlas-beta275.md).
|
||||
|
||||
# beta.274 — Calques de territoire continus
|
||||
|
||||
- Fiche Habitant conservée et claims actuels accessibles, selon la clarification du créateur.
|
||||
- Refonte des règles de propriété, habitation préalable, départage par Battle et nouveau tarif consignés pour la reprise.
|
||||
- Demeure sans cadres de chunks ; intensité liée à la part relative d’influence et actualisée quand celle-ci change.
|
||||
- Protections actives rendues en couleur unie dans la même texture de pixels que Demeure, sans bouclier ni pointillés.
|
||||
- Zones protégées adjacentes d’un propriétaire raccordées ; légende compacte couleur / personnage, avec masquage conservé.
|
||||
- Aperçu de Pause teinté directement dans ses pixels, avec brouillard et vide conservés.
|
||||
- Infobulle Demeure « Présence et influence d’habitation » en FR/EN ; retrait de « aucun claim ni protection ».
|
||||
- Aucun changement des données, droits, prix ou durées ; aucune attribution depuis Demeure ni aucun transfert par duel automatique.
|
||||
- [Contrat et vérifications](docs/demeure-overlay-beta274.md).
|
||||
|
||||
# beta.273 — Claims aux couleurs de leurs propriétaires
|
||||
|
||||
- Teinte et bordure utilisent la couleur persistante Hello World du propriétaire, y compris déconnecté.
|
||||
- Protection active : bordure pleine et bouclier ; sans protection : bordure en pointillés.
|
||||
- Bouton Propriétés : oui/non, partagé avec l’aperçu de Pause ; clic droit conservé quand les claims sont masqués.
|
||||
- Apparition automatique avec les tuiles de la carte proche, de l’Atlas et de l’aperçu de Pause.
|
||||
- Achats actualisant le claim visible ; expiration modifiant le style sans retirer la propriété.
|
||||
- Légende et survol FR/EN avec nom et durée restante ; terrain inconnu non révélé.
|
||||
- Aucun changement du registre, des prix, de la durée ou des droits.
|
||||
- [Contrat et vérifications](docs/claim-map-beta273.md).
|
||||
|
||||
# beta.272 — Claims et protection sur la carte
|
||||
|
||||
- Clic droit sur un chunk : panneau de revendication ou de protection sur la carte, sans grille ni changement de page.
|
||||
- Propriété ouvre directement la carte ; terrain proche visible dès Hello World, Atlas complet conservant ses prérequis.
|
||||
- Cinq chunks gratuits par joueur, puis 100 points d’XP supplémentaires à chaque nouveau claim.
|
||||
- 64 saphirs par chunk ajoutent sept jours de serveur en marche ; aucun temps décompté pendant l’arrêt.
|
||||
- Minage ×2 pour le propriétaire et ×0,5 pour les autres pendant la couverture ; pose et interactions ordinaires.
|
||||
- Nouveau registre séparé, achats avec reçu natif et migration volontaire des anciens volumes remboursant leurs extensions.
|
||||
- Stabilisation de la sauvegarde des statistiques à la fermeture face au nettoyage de déconnexion.
|
||||
- [Contrat, migration et vérifications](docs/chunk-claims-beta272.md).
|
||||
|
||||
# beta.271 — fiche Habitant, statistiques et activité
|
||||
|
||||
- Combats rétabli dans Communauté sous Habitant ; Story retiré de Pause.
|
||||
- Navigation Propriété, Factions et Familier en haut ; personnage aéré à gauche et biographie à droite.
|
||||
- Six compteurs personnels sous la biographie, lus dans Minecraft et le Blocodex.
|
||||
- Activité sous le personnage jusqu’en bas, avec défilement et bouton Historique dans son pied de panneau.
|
||||
- Publications personnelles, progrès et progression réunis sans nouveau format de sauvegarde.
|
||||
- Aperçu du familier retiré d'Habitant ; réglages et consentement regroupés dans sa page dédiée.
|
||||
- [Contrat, sources et vérifications](docs/inhabitant-menu-beta271.md).
|
||||
|
||||
# beta.270 — première propriété d’habitant
|
||||
|
||||
- Faction et Battle déplacés de Pause vers la fiche Habitant, avec un accès Propriété.
|
||||
- Une réservation gratuite après Hello World, dans un volume fini indépendant des chunks.
|
||||
- Quatre extensions de 100 à 400 points d’XP, sauvegardées avec un reçu natif.
|
||||
- Contrôles serveur du minage, de la pose et des interactions des autres joueurs ; terrains libres jouables.
|
||||
- Menu natif FR/EN, commandes `/sanctuary property`, `propriete` et `propriété`, aperçu sans écriture de terrain.
|
||||
- [Contrat, sauvegarde additive et limites de protection](docs/property-beta270.md).
|
||||
|
||||
# beta.172 — cartes de horde utilisables en jeu
|
||||
|
||||
- Suppression de la restriction au cercle laboratoire : une carte peut ouvrir
|
||||
|
||||
@@ -6,6 +6,338 @@ Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
|
||||
correspondantes. Le module Sanctuary Test n’est pas requis.
|
||||
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
|
||||
|
||||
|
||||
## beta.293 — mise à jour complète
|
||||
|
||||
[Livraison complète publiée](docs/release-beta293.md) : Happy Creepers de la
|
||||
branche dédiée, baleines fantômes, logo packwiz et quatre mods facultatifs.
|
||||
Serveur Beta, téléchargement Web et même instance Prism synchronisés en
|
||||
**beta.293**, sauvegardes et réglages conservés. Build et 79 tests serveur
|
||||
réussis ; assertions Vulkan réussies, blocage de fermeture de l'outil de test
|
||||
consigné. Réseau vocal contrôlé, essai audible à deux joueurs encore ouvert.
|
||||
|
||||
## beta.292 — voix et mods facultatifs (préparation locale)
|
||||
|
||||
[Simple Voice Chat, Bobby, Lithium et Entity Culling rejoignent le pack](docs/optional-mods-beta292.md).
|
||||
Les quatre restent décochables côté client ; Lithium et la voix rejoignent
|
||||
aussi le profil serveur. Logo packwiz conservé, 46 tests serveur ciblés et
|
||||
parcours Vulkan avec et sans Sodium réussis. Préparation locale, sans publication ni
|
||||
modification de l'instance personnelle. Dozei Stream et Only Fun restent en tickets.
|
||||
|
||||
## beta.291 — logo Sanctuary dans packwiz (préparation locale)
|
||||
|
||||
[Le bandeau fourni s'affiche pendant les téléchargements packwiz](docs/packwiz-logo-beta291.md),
|
||||
dans l'instance Prism préparée. L'installateur embarque le logo et conserve
|
||||
le moteur packwiz officiel. Les [tickets des prochaines intégrations](docs/integrations-mods-next.md)
|
||||
couvrent Simple Voice Chat, Bobby facultatif, Lithium, Entity Culling,
|
||||
Dozei Stream et Only Fun ; ces mods ne sont pas ajoutés dans cette version.
|
||||
Fenêtre FR/EN, contrôles d'archive et deux synchronisations isolées réussis.
|
||||
Construction ciblée avec 13 tests serveur réussie ; un test hérité du quota
|
||||
de baleines reste en échec, détaillé dans le contrat. Aucune publication.
|
||||
|
||||
## beta.290 — baleine fantôme volante (branche locale)
|
||||
|
||||
[Import direct de la baleine 26.2](docs/flying-whale-beta290.md), avec son IA,
|
||||
son rendu nuageux animé et son œuf personnalisé à partir de `egg.png` de
|
||||
Minecraft. Deux baleines chargées au maximum pour le serveur, œuf refusé
|
||||
sans consommation si les places sont prises. Construction, 19 tests serveur
|
||||
et parcours Vulkan avec Sodium réussis. Préparée localement, sans publication
|
||||
ni mise à jour d’installation.
|
||||
|
||||
## beta.289 — mise à jour complète
|
||||
|
||||
[Livraison des zombies et de la lumière portée](docs/release-beta289.md), avec
|
||||
JEI intégré au même JAR pour le client et le serveur. Le téléchargement du
|
||||
site conserve l’instance Prism prête à suivre le canal stable.
|
||||
**Publiée** : serveur Beta, téléchargement Web et instance Prism personnelle
|
||||
synchronisés en beta.289, avec sauvegardes et réglages conservés.
|
||||
[Procédure de développement et de publication](docs/updates.md).
|
||||
Construction client/serveur, **31 tests serveur** et parcours des zombies et
|
||||
familiers sous **Vulkan avec Sodium** réussis pour cette livraison.
|
||||
|
||||
## beta.288 — correction des lumières portées intégrée
|
||||
|
||||
[Le correctif des Dynamic Lights rejoint le pack courant](docs/dynamic-hand-light-beta288.md).
|
||||
Les torches et lanternes tenues en main éclairent le terrain avec Sodium,
|
||||
avec ou sans PBR. Les zombies et les neuf tenues de beta.287 sont conservés.
|
||||
Build, **19 tests serveur** et trois parcours **Vulkan** réussis : éclairage
|
||||
avec/sans Sodium, puis zombies et familiers avec Sodium.
|
||||
[Pack local beta.288](build/Sanctuary-beta.288.mrpack).
|
||||
|
||||
## beta.287 — tenues de zombies et postures
|
||||
|
||||
[Neuf tenues aléatoires et Hunter accroupi](docs/zombie-appearances-beta287.md)
|
||||
pour le zombie classique et les six infectés. Les PNG fournis par le créateur
|
||||
sont choisis aléatoirement par individu et gardés au rechargement, y compris
|
||||
pour les familiers. Tous gardent les bras classiques en avant ; le Hunter
|
||||
conserve sa marche accroupie.
|
||||
Les familiers infectés conservent leur modèle adulte miniature à taille variable.
|
||||
Build, **77 régressions, 19 tests Smoker et 16 tests Hunter** en sessions
|
||||
séparées, ainsi que le parcours **Vulkan**, réussis. Neuf tenues par type,
|
||||
bébé classique, 21 combinaisons de familiers, rééquipement stable et marche/bond
|
||||
du Hunter vérifiés. [Pack local beta.287](build/Sanctuary-beta.287.mrpack).
|
||||
|
||||
## beta.286 — Hunter et bond offensif
|
||||
|
||||
[Sixième infecté](docs/hunter-beta286.md) : préparation accroupie, bond physique
|
||||
vers le joueur et frappe unique au contact. Sa trajectoire reste fixe après
|
||||
le départ : une esquive, un mur ou un plafond peuvent faire rater l’attaque.
|
||||
T-shirt bleu et pantalon anthracite, œuf créatif et familier adulte miniature
|
||||
à taille variable, avec son propre bond offensif sur G.
|
||||
Build, **16 tests Hunter, 19 tests Smoker et 77 régressions**, en sessions séparées,
|
||||
ainsi que le parcours **Vulkan FR/EN GUI 2/3/4**, réussis. Contact contre un vrai
|
||||
joueur, esquive au clavier, atterrissage et trois tailles vérifiés.
|
||||
[Pack local beta.286](build/Sanctuary-beta.286.mrpack).
|
||||
|
||||
## beta.285 — Smoker et lasso
|
||||
|
||||
[Cinquième infecté](docs/smoker-beta285.md) : un lasso physique qui peut rater.
|
||||
Il doit toucher sa cible avant de la ramener vers le Smoker à la vitesse des
|
||||
tyroliennes. Les obstacles sur le corps arrêtent le trajet ; blesser la source
|
||||
ou couper le crochet libère la victime. Tenue gris clair et vert olive, œuf
|
||||
créatif et familier adulte miniature à taille variable, avec son propre lasso.
|
||||
Build, **19 tests Smoker et 77 régressions** en sessions séparées, ainsi que
|
||||
le parcours **Vulkan FR/EN GUI 2/3/4**, réussis. Esquive au clavier, touche
|
||||
réelle, traction imposée du joueur et arrivée vérifiées.
|
||||
[Pack local beta.285](build/Sanctuary-beta.285.mrpack).
|
||||
|
||||
## beta.284 — Boomer et bile
|
||||
|
||||
[Quatrième infecté](docs/boomer-beta284.md) : gerbe de bile en cône,
|
||||
diversion temporaire des zombies déjà présents et éclaboussure à la mort,
|
||||
sans destruction du terrain. Tenue violette, pantalon sable, œuf créatif et
|
||||
familier adulte miniature à taille variable, avec une gerbe qui affaiblit
|
||||
les ennemis et une éclaboussure G qui garde le familier vivant.
|
||||
Build, **68 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4**
|
||||
réussis, avec G, recrutement sauvage, mort et trois tailles vérifiés.
|
||||
[Pack local beta.284](build/Sanctuary-beta.284.mrpack).
|
||||
|
||||
## beta.283 — Spitter et flaque d’acide
|
||||
|
||||
[Troisième zombie spécial](docs/spitter-beta283.md) : le crachat dépose une
|
||||
zone verte persistante au sol, comme une potion persistante. Les dégâts cessent
|
||||
en sortant ; aucun poison ne suit la victime. Tenue orange, pantalon bleu
|
||||
ardoise, œuf créatif et familier adulte miniature à taille variable.
|
||||
Build, **54 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4**
|
||||
réussis, avec G, dégâts dans la zone, sortie et trois tailles vérifiés.
|
||||
[Pack local beta.283](build/Sanctuary-beta.283.mrpack).
|
||||
|
||||
## beta.282 — Chargeur
|
||||
|
||||
[Deuxième zombie spécial](docs/charger-beta282.md) : préparation visible, charge
|
||||
en ligne droite, choc et récupération. Tenue verte, pantalon anthracite, œuf
|
||||
créatif et familier adulte miniature à taille variable, avec sa propre charge.
|
||||
Build, **45 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis,
|
||||
avec appui réel sur G, choc et recharge vérifiés.
|
||||
[Pack local beta.282](build/Sanctuary-beta.282.mrpack).
|
||||
|
||||
## beta.281 — Infectés adultes en miniature
|
||||
|
||||
[Les infectés abandonnent la forme bébé](docs/infected-adults-beta281.md).
|
||||
Le familier reprend les proportions adultes en miniature, avec son trait de
|
||||
taille variable conservé. La règle est commune aux futures variantes d'infectés.
|
||||
Build, **34 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis,
|
||||
avec trois tailles réelles de miniature vérifiées.
|
||||
[Pack local beta.281](build/Sanctuary-beta.281.mrpack).
|
||||
|
||||
## beta.280 — Premier infecté
|
||||
|
||||
[Un nouveau zombie et son familier](docs/infected-beta280.md) : modèle vanilla,
|
||||
t-shirt rouge, pantalon brun, 10 PV et vitesse légèrement supérieure au zombie.
|
||||
Œuf créatif `sanctuary:infected_spawn_egg`, apparitions peu fréquentes dans les
|
||||
habitats de zombies de l'Overworld et familier bébé avec son profil de combat.
|
||||
Texture vanilla recolorée pixel par pixel, sans déplacement UV. Build,
|
||||
**33 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.280](build/Sanctuary-beta.280.mrpack).
|
||||
|
||||
## beta.279 — Carte des îles et grand dézoom
|
||||
|
||||
[Vue d'ensemble de l'archipel et relevés prolongés](docs/map-islands-zoom-beta279.md).
|
||||
La carte dézoome jusqu'à l'échelle du monde avec des niveaux de détail adaptés,
|
||||
et reçoit les nouveaux chunks lointains en gardant son centre fixe. Capacité
|
||||
portée de 16 384 à **131 072 chunks par couche et habitant**, sans effacer les
|
||||
relevés existants. Les propriétés et Demeure retrouvent leurs détails en zoomant.
|
||||
Build, **20 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.279](build/Sanctuary-beta.279.mrpack).
|
||||
|
||||
## beta.278 — Disputer une propriété dans Battle
|
||||
|
||||
[Duel consenti pour un claim déjà revendiqué](docs/claim-battle-beta278.md).
|
||||
Clic droit sur la propriété → Disputer via Battle. Le challenger propose et
|
||||
le propriétaire accepte explicitement le chunk mis en jeu ; les deux confirment
|
||||
ensuite Prêt dans Battle. Le vainqueur reçoit ce seul claim avec sa protection
|
||||
restante. Refus, annulation avant lancement ou combat sans vainqueur ne changent
|
||||
pas le propriétaire. Résultat et transfert disposent d’un journal de reprise.
|
||||
Build, **40 tests serveur ciblés**, **26 contrôles du journal** et parcours
|
||||
**Vulkan FR/EN GUI 2/3/4** réussis. [Pack local beta.278](build/Sanctuary-beta.278.mrpack).
|
||||
|
||||
## beta.277 — Habiter avant de revendiquer
|
||||
|
||||
[Claims reliés aux influences Demeure](docs/claim-demeure-beta277.md).
|
||||
Une simple traversée ne suffit plus. Sur un terrain libre, l’habitant le plus
|
||||
influent peut revendiquer ; à égalité, la première trace d’habitation départage.
|
||||
Un autre habitant peut donc prendre la priorité avant le claim en dépassant
|
||||
l’influence du premier. Les propriétés acquises restent attribuées, avec les
|
||||
mêmes prix et protections. Le clic droit indique la condition ou le nom prioritaire.
|
||||
Build, **34 tests serveur ciblés**, contrôles Demeure et parcours **Vulkan
|
||||
FR/EN GUI 2/3/4** réussis. [Pack local beta.277](build/Sanctuary-beta.277.mrpack).
|
||||
|
||||
## beta.276 — Contours des propriétés et protection à huit saphirs
|
||||
|
||||
[Contours CTM aux couleurs des propriétaires](docs/claim-contours-beta276.md),
|
||||
visibles aussi sans protection. Les chunks voisins d’un même joueur se raccordent ;
|
||||
les chunks protégés ajoutent un fond coloré. **8 saphirs par chunk pour trois jours
|
||||
de serveur en marche**, avec conservation des durées déjà achetées.
|
||||
Build, **30 tests serveur ciblés**, **63 vérifications du registre** et parcours
|
||||
**Vulkan FR/EN GUI 2/3/4** réussis. [Pack local beta.276](build/Sanctuary-beta.276.mrpack).
|
||||
|
||||
## beta.275 — Habitant et Pause autour de la carte
|
||||
|
||||
[Habitant en trois colonnes et Pause centrée sur l’Atlas](docs/menu-atlas-beta275.md).
|
||||
Statistiques transparentes à gauche, personnage au centre, biographie à droite ;
|
||||
Factions et Familier restent seuls en haut. Le temps joué utilise jours, heures,
|
||||
minutes et secondes. Le message d’arrivée Propriété et l’activité de la fiche
|
||||
sont retirés. Les propriétés exigent la Carte débloquée et active côté serveur.
|
||||
Après déblocage, Aventure et Communauté forment deux colonnes au-dessus de la
|
||||
carte ; Partie et ses options restent dessous, Gazette à gauche et Tableau à droite.
|
||||
Build, **30 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.275](build/Sanctuary-beta.275.mrpack).
|
||||
|
||||
## beta.274 — Calques continus et propriétés conservées
|
||||
|
||||
[Demeure et les protections utilisent des couleurs par pixel sans cadres de chunks](docs/demeure-overlay-beta274.md).
|
||||
Les protections voisines de même propriétaire se raccordent, avec une légende
|
||||
couleur / personnage ; les boucliers et pointillés sont retirés. Le bouton
|
||||
Propriétés conserve le masquage. L’aide de Demeure décrit l’habitation et son
|
||||
influence. La fiche Habitant et les claims actuels restent accessibles.
|
||||
[La refonte des règles, de l’habitation préalable, des duels et du tarif est mise en attente](docs/property-demeure-refonte.md).
|
||||
Build, **29 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.274](build/Sanctuary-beta.274.mrpack).
|
||||
|
||||
## beta.273 — Couleurs et protection des claims
|
||||
|
||||
[Les claims portent la couleur de leur propriétaire sur la carte](docs/claim-map-beta273.md),
|
||||
même hors ligne. Bordure pleine et bouclier pour les chunks protégés ; pointillés
|
||||
pour les autres. Affichage dès l’ouverture, actualisation après achat et état
|
||||
au survol, dans la carte proche, l’Atlas et son aperçu de Pause. Le bouton
|
||||
**Propriétés : oui/non** permet de les masquer et de les réafficher.
|
||||
Build, **26 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.273](build/Sanctuary-beta.273.mrpack).
|
||||
|
||||
## beta.272 — Claims sur la carte
|
||||
|
||||
[Clic droit sur le terrain pour revendiquer un chunk](docs/chunk-claims-beta272.md).
|
||||
Les cinq premiers sont gratuits, puis 100, 200, 300 points d’XP, etc.
|
||||
Chaque chunk peut être protégé pour **64 saphirs et sept jours de serveur en
|
||||
marche** : minage ×2 pour son propriétaire, ×0,5 pour les autres. L’arrêt du
|
||||
serveur suspend la durée ; le claim reste acquis après expiration.
|
||||
Migration volontaire des anciens volumes, avec remboursement des extensions.
|
||||
Build, **25 tests serveur ciblés**, **58 vérifications du registre** et parcours
|
||||
**Vulkan FR/EN GUI 2/3/4** réussis. [Pack local beta.272](build/Sanctuary-beta.272.mrpack).
|
||||
|
||||
## beta.271 — Habitant, statistiques et activité
|
||||
|
||||
[Fiche en deux colonnes et deux rangées](docs/inhabitant-menu-beta271.md).
|
||||
Propriété, Factions et Familier forment une barre en haut. Le personnage
|
||||
respire en haut à gauche, avec son activité jusqu’en bas et Historique dessous.
|
||||
La biographie et les statistiques occupent la droite. Le familier et ses
|
||||
réglages restent dans sa page dédiée. Combats revient dans Communauté ;
|
||||
Story quitte Pause. Build, **15 tests serveur ciblés** et parcours **Vulkan
|
||||
FR/EN GUI 2/3/4** réussis. [Pack local beta.271](build/Sanctuary-beta.271.mrpack).
|
||||
|
||||
## beta.270 — Propriété personnelle et fiche Habitant
|
||||
|
||||
[Première propriété gratuite après Hello World](docs/property-beta270.md) :
|
||||
volume initial de 17 × 17 × 17 blocs, emplacement choisi dans le monde principal,
|
||||
quatre agrandissements en points d’XP et protection des actions directes des
|
||||
autres joueurs. Accès par **Habitant → Propriété** ou `/sanctuary property`.
|
||||
Faction et Battle rejoignent Habitant et libèrent leurs places dans Pause.
|
||||
Explosions, pistons, fluides et autres dégâts environnementaux restent hors
|
||||
protection dans ce premier lot. **41 GameTests serveur ciblés** et parcours
|
||||
**Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.270](build/Sanctuary-beta.270.mrpack).
|
||||
|
||||
## beta.269 — Les réponses de Providence
|
||||
|
||||
[Six issues et leurs couleurs](docs/providence-verdicts-beta269.md) :
|
||||
Exaucée en vert, Différée en jaune, Refusée en rouge, Incomprise en gris,
|
||||
Alternative en violet ; Silence sans message visible. Couleurs conservées
|
||||
en SGA et en lecture normale FR/EN. Un verdict différé programme réellement
|
||||
le miracle plus tard ; une alternative accorde une autre aide admissible.
|
||||
Build, **191 vérifications indépendantes**, **22 GameTests serveur** et parcours
|
||||
**Vulkan FR/EN GUI 2/3/4** réussis. [Pack local beta.269](build/Sanctuary-beta.269.mrpack).
|
||||
|
||||
## beta.268 — Les quatorze prières
|
||||
|
||||
[Catalogue complet de Providence](docs/providence-fourteen-beta268.md) :
|
||||
récupération, retour, secours, soleil, pluie, guérison, nourriture, foudre,
|
||||
malédiction, vengeance, vol, désarmement, téléportation et réparation.
|
||||
Ciblage par `@Pseudo`, prières hostiles autorisées contre tous les joueurs
|
||||
connectés, transferts réels des objets et scellement des tombes consommées.
|
||||
L’attente de cinq secondes et les couleurs SGA sont conservées.
|
||||
Build, **146 vérifications indépendantes**, **20 GameTests serveur** et parcours
|
||||
client **Vulkan** réussis. [Pack local beta.268](build/Sanctuary-beta.268.mrpack).
|
||||
|
||||
## beta.267 — Providence prend le temps de répondre
|
||||
|
||||
[Attente de cinq secondes après `/pray`](docs/providence-delay-beta267.md),
|
||||
réglable entre 1 et 60 secondes. Une seule prière en attente par joueur,
|
||||
répétitions ignorées, conditions vérifiées à l'échéance et réponses colorées
|
||||
conservées. Build, 83 vérifications indépendantes, 15 GameTests serveur et
|
||||
parcours Vulkan avec réponse après cinq secondes réussis.
|
||||
[Pack local beta.267](build/Sanctuary-beta.267.mrpack).
|
||||
|
||||
## beta.266 — Couleurs de Providence
|
||||
|
||||
[Réponses SGA selon le verdict](docs/providence-colors-beta266.md) : vert pour
|
||||
un miracle accordé, jaune pour une attente ou une prière à clarifier, rouge
|
||||
pour un refus ou un échec. Build, 67 vérifications indépendantes, 15 GameTests
|
||||
serveur et parcours Vulkan FR/EN GUI 2/3/4 réussis.
|
||||
[Pack local beta.266](build/Sanctuary-beta.266.mrpack).
|
||||
|
||||
## beta.265 — Providence
|
||||
|
||||
[Prières et premiers miracles](docs/providence-beta265.md) : `/pray` interprète
|
||||
les concepts d'un dictionnaire JSON rechargeable, puis décide côté serveur
|
||||
d'éclaircir le temps ou de ramener le joueur près de sa dernière mort, si une
|
||||
arrivée sûre est disponible dans les chunks chargés. Probabilités et cooldowns
|
||||
persistants, journal borné, réponses SGA et lecture normale FR/EN avec
|
||||
`/providence readable true`. Restitution des objets et secours préparés,
|
||||
**désactivés** en attendant leurs contrats de sécurité.
|
||||
Build, **67 vérifications indépendantes**, **15 GameTests serveur** et parcours
|
||||
client **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.265](build/Sanctuary-beta.265.mrpack).
|
||||
|
||||
## beta.264 — Menu central et aperçus alignés
|
||||
|
||||
[Formulaire de la largeur de la hotbar native](docs/hello-layout-beta264.md) :
|
||||
couleur, biographie, choix et nom du familier regroupés au centre. Pseudo et
|
||||
nom au-dessus des aperçus 3D, sol commun et familiers bébé miniatures.
|
||||
Détails de l'espèce uniquement au survol, titre Hello World retiré.
|
||||
Retour séparé, Entrer dans Sanctuary dans le pied de page natif des menus.
|
||||
Build, 13 GameTests serveur et parcours Vulkan FR/EN GUI 2/3/4 réussis,
|
||||
avec 44 modèles bébé, admission, reconnexion et code d'accès vérifiés.
|
||||
[Pack local beta.264](build/Sanctuary-beta.264.mrpack).
|
||||
|
||||
## beta.263 — Bébés familiers dans Hello World
|
||||
|
||||
[Formes bébé natives dans l'aperçu 3D](docs/hello-baby-familiars-beta263.md) :
|
||||
modèles, textures et échelles d'âge adaptés au familier remis en jeu, avec
|
||||
suivi de la souris. Les 44 aperçus sont comparés aux proxies natifs et vérifiés
|
||||
à froid en Vulkan FR/EN GUI 2/3/4. Build, 13 GameTests serveur et export
|
||||
du [pack local beta.263](build/Sanctuary-beta.263.mrpack) vérifiés.
|
||||
|
||||
## beta.262 — Nouveau menu Hello World
|
||||
|
||||
[Personnage et familier dans un menu centré](docs/hello-world-menu-beta262.md) :
|
||||
aperçu du personnage à gauche, familiers à droite, biographie et nom au centre
|
||||
dans deux blocs symétriques. Texture native de hotbar pour neuf couleurs par
|
||||
page, flèches autour du pseudo, fond texturé uniforme et aperçus 3D qui suivent
|
||||
la souris. Saisies et sélection conservées, libellés FR/EN.
|
||||
Build, 13 GameTests serveur et parcours Vulkan FR/EN GUI 2/3/4 réussis,
|
||||
avec les 44 modèles de départ, création, reconnexion et code d’accès vérifiés.
|
||||
[Pack local beta.262](build/Sanctuary-beta.262.mrpack).
|
||||
|
||||
## beta.261 — Alt bref et Alt maintenu
|
||||
|
||||
[Un seul raccourci pour l’attaque et les ordres](docs/familiar-alt-beta261.md) :
|
||||
|
||||
+170
-4
@@ -1,5 +1,40 @@
|
||||
# Provenance et crédits
|
||||
|
||||
## Mods facultatifs beta.292
|
||||
|
||||
Les binaires officiels de [Simple Voice Chat 2.6.24+26.3](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy)
|
||||
(Max Henkel, All Rights Reserved), [Bobby 5.2.16+mc26.3](https://modrinth.com/mod/bobby/version/J7YnfCss)
|
||||
(johni0702, LGPL-3.0-or-later), [Lithium 0.26.2+mc26.3](https://modrinth.com/mod/lithium/version/xS0Q8LSi)
|
||||
(CaffeineMC et contributeurs, LGPL-3.0-only) et
|
||||
[Entity Culling 1.11.3 pour 26.3](https://modrinth.com/mod/entityculling/version/X1g3VhTx)
|
||||
(tr7zw et contributeurs, tr7zw Protective License) sont téléchargés séparément
|
||||
par packwiz, sans modification ni imbrication dans Sanctuary. Leurs notices,
|
||||
licences et bibliothèques imbriquées restent attachées aux JAR originaux.
|
||||
Leurs métadonnées et SHA-512 sont dans `packwiz/mods/` ;
|
||||
[choix de versions et vérifications](docs/optional-mods-beta292.md).
|
||||
|
||||
## Installateur packwiz Sanctuary beta.291
|
||||
|
||||
Le bandeau `launcher/packwiz/sanctuary.png` est le fichier `minecraft.png`
|
||||
fourni par le créateur le 26 octobre 2026, conservé sans modification.
|
||||
`InstallWindow.java` adapte la fenêtre de packwiz-installer v0.5.14 de
|
||||
comp500, sous MIT ; licence et source sont joints au JAR généré. Toutes les
|
||||
autres classes et les notices des dépendances de l'installateur officiel
|
||||
restent identiques. Sources, hashes et périmètre dans
|
||||
[le contrat du logo packwiz](docs/packwiz-logo-beta291.md).
|
||||
|
||||
## Tenues des zombies beta.287
|
||||
|
||||
Les neuf PNG `zombie.png`, `zombie2.png` à `zombie9.png` sont fournis par le
|
||||
créateur le 26 octobre 2026. Ils sont copiés sans modification sous
|
||||
`assets/sanctuary/textures/entity/zombies/zombie1.png` à `zombie9.png`.
|
||||
Le [reçu des sources et dérivés](docs/zombie-skins-beta287.json) conserve
|
||||
leurs SHA-256. `scripts/prepare_zombie_skins.py` produit aussi neuf adaptations
|
||||
UV pour le modèle bébé natif de Minecraft 26.3 par rééchantillonnage déterministe
|
||||
des faces ; aucun générateur d’image n’intervient. La géométrie et les couches
|
||||
d’armure restent issues de Minecraft. Les notices historiques des anciennes
|
||||
recolorations ci-dessous décrivent les livraisons précédentes.
|
||||
|
||||
Depuis beta.256, le pack client inclut [Sodium 0.9.2+mc26.3](https://modrinth.com/mod/sodium/version/bAZQdGpg),
|
||||
de JellySquid et des contributeurs CaffeineMC, sous Polyform-Shield-1.0.0.
|
||||
Le JAR officiel est une dépendance séparée téléchargée par packwiz ; il n’est
|
||||
@@ -106,15 +141,52 @@ L’icône d’objet correspondante provient de `../26.2/sanctuary`
|
||||
restent la propriété de leurs auteurs et ne sont pas placées sous la licence
|
||||
du code Sanctuary. Il ne s’agit pas d’une cape officielle accordée au compte.
|
||||
|
||||
Les familiers utilisent les modèles et textures résolus par Minecraft installé ;
|
||||
aucune copie des textures de mobs ni aucun ancien pouvoir n’est embarqué.
|
||||
Les familiers vanilla utilisent les modèles et textures résolus par Minecraft
|
||||
installé. L'exception du premier infecté est décrite ci-dessous.
|
||||
|
||||
## Infecté beta.280
|
||||
|
||||
Le modèle, les animations, l'armure et les sons sont ceux du zombie installé
|
||||
avec Minecraft 26.3. Aucun modèle ou son de Left 4 Dead n'est inclus ; cette
|
||||
série reprend seulement l'idée de rôles différents pour des zombies.
|
||||
|
||||
La texture `assets/sanctuary/textures/entity/infected.png` est une recoloration
|
||||
déterministe du zombie **Minecraft 26.3**, réalisée par
|
||||
`scripts/generate_infected_texture.py` à partir du JAR installé. Seules les
|
||||
couleurs du t-shirt et du pantalon changent ; coordonnées UV, peau, visage,
|
||||
chaussures et transparence restent identiques. Empreinte SHA-256 du PNG natif :
|
||||
`1b88eea9b63889c2c3399773f887af375defc3aad0e7a375adc84249da54cbbd`.
|
||||
Aucune texture produite par ImageGen n'est livrée.
|
||||
|
||||
L'icône `textures/item/infected_spawn_egg.png` est reprise intacte de Sanctuary
|
||||
26.2, commit `26f2c2a0061619df1f146f9081d6740c560a7497`, module `itsalive`.
|
||||
Elle provenait d'une recoloration de l'œuf Minecraft par
|
||||
`itsalive/tools/generate_infected_textures.sh`. Ces ressources dérivées ne sont
|
||||
pas couvertes par la licence GPL du code ; les droits sur les ressources
|
||||
Minecraft restent à Mojang/Microsoft.
|
||||
|
||||
## Chargeur beta.282
|
||||
|
||||
`assets/sanctuary/textures/entity/charger.png` utilise le même PNG natif du
|
||||
zombie Minecraft 26.3 et les mêmes masques UV que l'Infecté. La sélection
|
||||
`--variant charger` de `scripts/generate_infected_texture.py` produit un
|
||||
t-shirt vert et un pantalon anthracite, avec peau, visage, chaussures et alpha
|
||||
conservés pixel pour pixel. Aucun dessin génératif ni modèle de Left 4 Dead.
|
||||
SHA-256 : `cf2344d870753ed621b9fd7c3fd7dfe0b3dc35294620e798f113b71abd27fbda`.
|
||||
|
||||
L'icône `textures/item/charger_spawn_egg.png` est reprise intacte du module
|
||||
`itsalive` de Sanctuary 26.2, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, générée à partir de l'œuf Minecraft.
|
||||
Les droits sur les ressources Minecraft restent à Mojang/Microsoft.
|
||||
|
||||
## JEI beta.024
|
||||
|
||||
Just Enough Items, mezz, MIT : fork source de la branche 26.2, commit
|
||||
`aae2dfcfb82e6b5bce787ba72bb2eda7339b274f`, adapté à Minecraft 26.3-pre-2.
|
||||
Depuis beta.257, le fork Minecraft 26.3 `30.32.0-sanctuary.4` est un JAR client
|
||||
séparé et facultatif, avec la licence d’origine et [la provenance du portage](mods/jei/PROVENANCE.md).
|
||||
Depuis beta.280, le fork Minecraft 26.3 `30.32.0-sanctuary.4` est à nouveau
|
||||
embarqué dans le JAR Sanctuary, avec la licence d’origine et
|
||||
[la provenance du portage](mods/jei/PROVENANCE.md). Sa distribution séparée
|
||||
facultative concernait les beta.257–278.
|
||||
Les patches et la reconstruction sont fournis ; les téléchargements restent ignorés.
|
||||
Baked Substring Index 0.1.0 (MIT, James Mitchell) et Suffix Tree 1.1.0
|
||||
(Apache-2.0, Alessandro Bahgat Shehata) sont inclus avec leurs licences.
|
||||
@@ -212,3 +284,97 @@ leurs aperçus publics de haute qualité ; la cigale du soir utilise la
|
||||
conversion MP3 publique de Commons. Aucun enregistrement d’Animal Crossing
|
||||
n’est inclus. L’identification biologique du grillon reste non précisée ;
|
||||
la courtilière européenne sert d’analogue sonore.
|
||||
|
||||
## Spitter beta.283
|
||||
|
||||
`assets/sanctuary/textures/entity/spitter.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 via `scripts/generate_infected_texture.py --variant spitter`.
|
||||
Seuls le t-shirt orange et le pantalon bleu ardoise changent. Peau, visage,
|
||||
chaussures, alpha et UV restent identiques au PNG natif décrit ci-dessus.
|
||||
SHA-256 : `92f6a8f7ad09a817fc74bfc2728a5f75d34a838dcc91a2ddee336c31803c0592`.
|
||||
Aucun modèle, son ou dessin génératif de Left 4 Dead n'est inclus.
|
||||
|
||||
L'icône `textures/item/spitter_spawn_egg.png` est reprise intacte du module
|
||||
`itsalive` de Sanctuary 26.2, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, dérivée de l'œuf Minecraft.
|
||||
Les droits sur les ressources Minecraft restent à Mojang/Microsoft.
|
||||
|
||||
## Boomer beta.284
|
||||
|
||||
`assets/sanctuary/textures/entity/boomer.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 par `scripts/generate_infected_texture.py --variant boomer`.
|
||||
T-shirt violet et pantalon sable : 570 pixels de vêtements changent, avec
|
||||
visage, peau, chaussures, UV et transparence conservés. SHA-256 :
|
||||
`2cbc0a87435c8f12fbbd77bd3d7fc23c9843e9ae22493dbfb6ffe14ea492c230`.
|
||||
|
||||
Le même script recolore en violet la silhouette et les ombres de l'icône
|
||||
`spitter_spawn_egg.png` historique, décrite ci-dessus, pour produire
|
||||
`textures/item/boomer_spawn_egg.png`. SHA-256 :
|
||||
`dd11449f7072db2faf753e5d258acfe8e5d2c50116e076d9b799345ed5f62ffa`.
|
||||
Les droits sur ces ressources dérivées restent à Mojang/Microsoft. Aucun
|
||||
modèle, son ou bitmap de Left 4 Dead, ni texture générative, n'est livré.
|
||||
|
||||
## Smoker beta.285
|
||||
|
||||
`assets/sanctuary/textures/entity/smoker.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 par `scripts/generate_infected_texture.py --variant smoker`.
|
||||
T-shirt gris clair et pantalon vert olive : 570 pixels de vêtements modifiés ;
|
||||
visage, peau, chaussures, UV et alpha conservés. SHA-256 :
|
||||
`80b7b34c915466d2c022f2222bd5bedf5b5b6c39ab447a97dcae7edf66ba4d17`.
|
||||
|
||||
Le même script dérive `textures/item/smoker_spawn_egg.png` de la silhouette et
|
||||
des ombres de l'icône historique `spitter_spawn_egg.png`, avec la teinte du
|
||||
t-shirt. SHA-256 :
|
||||
`e73b7d1e08ef0f35bfc7bac3e81795b166b56d9a5267045bcef64ae311c256c6`.
|
||||
La tête du lasso utilise l'objet laisse natif et son câble le rendu natif des
|
||||
laisses tendues déjà employé par les tyroliennes. Aucun modèle, son ou bitmap
|
||||
de Left 4 Dead ni texture générative n'est inclus. Les droits sur les ressources
|
||||
Minecraft dérivées restent à Mojang/Microsoft.
|
||||
|
||||
## Hunter beta.286
|
||||
|
||||
`assets/sanctuary/textures/entity/hunter.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 par `scripts/generate_infected_texture.py --variant hunter`.
|
||||
T-shirt bleu et pantalon anthracite : 570 pixels de vêtements modifiés ; tête,
|
||||
peau, chaussures, UV et alpha conservés. SHA-256 :
|
||||
`1057efc7947105684a24fe165e83581b66fccc877687433cc00ed80231c2c970`.
|
||||
|
||||
Le même script dérive `textures/item/hunter_spawn_egg.png` de la silhouette et
|
||||
des ombres de l'icône historique `spitter_spawn_egg.png`, avec la teinte du
|
||||
t-shirt. SHA-256 :
|
||||
`294bbd145e09707f2006d51e9190b538b50148c44c13f3ecaf563e316f03384d`.
|
||||
Les sons et le modèle sont natifs à Minecraft. Aucun bitmap ou son de Left 4
|
||||
Dead ni texture générative n'est inclus. Les droits sur les ressources
|
||||
Minecraft dérivées restent à Mojang/Microsoft.
|
||||
|
||||
## Baleine fantôme volante beta.290
|
||||
|
||||
Le modèle, son animation, le rendu, l’IA et le spawner proviennent du module
|
||||
`itsalive` de Sanctuary 26.2, commit
|
||||
`94644a28f05b8010f06ec1181fe6c74a50ad80d6`, sous la licence du projet
|
||||
GPL-3.0-or-later. Cette source est elle-même le port de la baleine du JAR
|
||||
historique Real Ghost ; aucune archive 26.2 n’est redistribuée.
|
||||
`textures/entity/flying_whale/flying_whale.png` est repris octet pour octet.
|
||||
SHA-256 : `0828ba1b9443f31cdb0ce9c22393a8702a72b9e59c2a0c02f3dd6dedadd33fb8`.
|
||||
|
||||
L’icône `textures/item/flying_whale_spawn_egg.png` est une recoloration
|
||||
déterministe directe de `assets/minecraft/textures/item/egg.png` de Minecraft
|
||||
26.3, produite par `scripts/generate_whale_egg.py` ; silhouette, alpha et
|
||||
ombrage natifs conservés. Source SHA-256 :
|
||||
`3bfccf5fcfe66933d591e944f6b08456c8083f5c3898b924da52bfebc41e6322`.
|
||||
Résultat SHA-256 :
|
||||
`d476a3fedba3af3e463d6461b81cf6ca8702fe10ceb3934f426390d59da63f7e`.
|
||||
Les sons et particules sont natifs à Minecraft, référencés sans copie ; les
|
||||
droits sur les ressources Minecraft dérivées restent à Mojang/Microsoft.
|
||||
|
||||
|
||||
## Happy creepers
|
||||
|
||||
`assets/sanctuary/textures/entity/creeper/happy.png` est repris sans modification
|
||||
du module `itsalive` de Sanctuary 26.2, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, chemin
|
||||
`itsalive/src/main/resources/assets/itsalive/textures/entity/creeper/happy.png`.
|
||||
SHA-256 : `40e22bf0610cfc7ae8ca910b1e6bdce0c759ccbf8714df7566ebe5538cac24f0`.
|
||||
Il s'agit du visage souriant historique sur le modèle natif du creeper. Les
|
||||
droits sur les ressources Minecraft dérivées restent à Mojang/Microsoft.
|
||||
Aucune autre variante de texture ni ressource Mobopia n'est importée.
|
||||
|
||||
+39
-1
@@ -15,6 +15,37 @@ allprojects {
|
||||
tasks.named('build') { dependsOn(':sanctuary:build', ':jei:build', ':sanctuary-test:build') }
|
||||
tasks.named('check') { dependsOn(':sanctuary:check', ':jei:check', ':sanctuary-test:check', 'verifyPack') }
|
||||
|
||||
tasks.register('verifyServerUpdater', Exec) {
|
||||
group = 'verification'
|
||||
commandLine('python3', 'scripts/test_server_runtime.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyServerUpdater') }
|
||||
|
||||
tasks.register('verifyWebDistribution', Exec) {
|
||||
group = 'verification'
|
||||
commandLine('python3', 'scripts/test_publish_web_pack.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyWebDistribution') }
|
||||
|
||||
tasks.register('verifyOptionalMods', Exec) {
|
||||
group = 'verification'
|
||||
commandLine('python3', 'scripts/verify_optional_mods.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyOptionalMods') }
|
||||
|
||||
tasks.register('assemblePackwizInstaller', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Build and verify the Sanctuary-branded packwiz window.'
|
||||
commandLine('python3', 'scripts/packwiz_installer.py')
|
||||
}
|
||||
|
||||
tasks.register('verifyPackwizInstaller', Exec) {
|
||||
group = 'verification'
|
||||
dependsOn('assemblePackwizInstaller')
|
||||
commandLine('python3', 'scripts/test_packwiz_installer.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyPackwizInstaller') }
|
||||
|
||||
tasks.register('verifyPack', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Verify pinned pack dependencies, versions and index hashes.'
|
||||
@@ -24,10 +55,17 @@ tasks.register('verifyPack', Exec) {
|
||||
tasks.register('assemblePack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Assemble a local packwiz pack including the built Sanctuary mod.'
|
||||
dependsOn(':sanctuary:build', ':jei:build', 'verifyPack', 'assembleResourcePack', 'assembleResourceTemplate')
|
||||
dependsOn(':sanctuary:build', ':jei:build', 'verifyPack', 'verifyOptionalMods', 'assembleResourcePack', 'assembleResourceTemplate', 'assemblePackwizInstaller')
|
||||
commandLine('python3', 'scripts/pack.py', 'assemble')
|
||||
}
|
||||
|
||||
tasks.register('assembleServerPack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Build the Linux server kit and verify its packwiz profile.'
|
||||
dependsOn('assemblePack')
|
||||
commandLine('python3', 'scripts/server_pack.py')
|
||||
}
|
||||
|
||||
tasks.register('verifyResourcePack', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Verify the versioned Sanctuary texture sources and their release hashes.'
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## HELLO-264 — Menu central et sol commun — livré localement
|
||||
|
||||
Sur `codex/hello-world-menu`, socle beta.263. Colonne centrale de 182 pixels,
|
||||
choix regroupés par section, noms au-dessus des aperçus et familiers bébé
|
||||
miniatures alignés avec les pieds du personnage. Espèce, rareté et description
|
||||
au survol ; Retour séparé et Entrer dans Sanctuary dans le pied de page natif.
|
||||
Build, 13 GameTests serveur et parcours Vulkan FR/EN GUI 2/3/4 réussis ;
|
||||
44 modèles bébé, création, reconnexion et code d'accès vérifiés.
|
||||
Pack local beta.264 exporté et contrôlé.
|
||||
[Disposition et vérifications](hello-layout-beta264.md).
|
||||
|
||||
## HELLO-263 — Aperçus des bébés familiers — livré localement
|
||||
|
||||
Sur `codex/hello-world-menu`, socle beta.262. Modèles et textures bébé, échelles
|
||||
d'âge natives et cadrage adapté. Les 44 aperçus sont comparés aux proxies
|
||||
réels après `Mob.setBaby(true)` ; parcours Vulkan FR/EN GUI 2/3/4 réussi.
|
||||
Build, 13 GameTests serveur et pack local vérifiés.
|
||||
[Correction et vérifications](hello-baby-familiars-beta263.md).
|
||||
|
||||
## HELLO-262 — Personnage, couleurs et familier — livré localement
|
||||
|
||||
Branche `codex/hello-world-menu`, socle beta.261. Menu à trois colonnes, blocs
|
||||
centraux de même hauteur et colonnes latérales de même largeur. Palette serveur
|
||||
paginée par neuf avec la texture native de hotbar et les flèches du pseudo.
|
||||
Fond texturé uniforme ; personnage et familier en 3D suivent la souris.
|
||||
Biographie et nom au centre. Les règles et sauvegardes serveur sont conservées.
|
||||
Build, 13 GameTests serveur et parcours Vulkan FR/EN GUI 2/3/4 réussis ;
|
||||
44 modèles de départ, admission, reconnexion et code d’accès vérifiés.
|
||||
[Contrat et vérifications](hello-world-menu-beta262.md).
|
||||
|
||||
## SHADER-258 — Ombres des falaises et musique différée — livré localement
|
||||
|
||||
Branche `codex/zenith-shadows-beta258`, socle beta.257. Comparaison de profondeur
|
||||
@@ -1928,6 +1958,12 @@ et le client graphique avec connexion réelle passent. Voir
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Le cadrage du 26 octobre 2026 détaille les
|
||||
[tickets d'intégration du pack et des modules personnels](integrations-mods-next.md) :
|
||||
Simple Voice Chat, Bobby facultatif, Lithium, Entity Culling, Dozei Stream
|
||||
et Only Fun. Les quatre intégrations du pack sont préparées localement dans
|
||||
[beta.292](optional-mods-beta292.md) ; les deux modules personnels restent à cadrer.
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
| Thème | Contenus à découper plus tard |
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# DIST-WEB — téléchargement direct d'une instance Prism prête
|
||||
|
||||
Branche `codex/beta-download`, serveur dédié CT 350 sur `gpu`. Cette livraison
|
||||
conserve les binaires immuables beta.280 et ne modifie ni monde ni format de
|
||||
sauvegarde. L'Alpha reste hors cible.
|
||||
|
||||
## Résultat pour le joueur
|
||||
|
||||
Le bouton **Télécharger** de [Sanctuary Web](https://sanctuary-minecraft.net)
|
||||
donne directement le fichier **Sanctuary-Beta.zip** :
|
||||
|
||||
```text
|
||||
https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip
|
||||
```
|
||||
|
||||
Le joueur importe ce ZIP dans Prism (**Ajouter une instance → Importer depuis
|
||||
un ZIP**), puis lance **Sanctuary Beta**. Packwiz télécharge le pack complet au
|
||||
premier lancement et suit le même canal aux lancements suivants. La commande
|
||||
avant lancement et le bootstrap sont déjà inclus ; aucune configuration manuelle
|
||||
ou téléchargement séparé d'outil n'est demandé.
|
||||
|
||||
Le format ZIP d'instance Prism conserve ces réglages, contrairement à un MRpack
|
||||
standard. Le MRpack beta.280 reste accessible par son ancienne adresse pour
|
||||
compatibilité des liens, sans devenir un deuxième bouton du site. Le parcours
|
||||
manuel de configuration et son lien dans le pied de page sont retirés.
|
||||
|
||||
L'archive contient uniquement `instance.cfg`, `mmc-pack.json`, l'icône,
|
||||
`minecraft/packwiz-installer-bootstrap.jar` et, à partir de la préparation
|
||||
beta.291, `minecraft/packwiz-installer.jar` avec le
|
||||
[logo Sanctuary embarqué](packwiz-logo-beta291.md). Elle démarre avec zéro mod
|
||||
local. L'installateur est vérifié par SHA-256 et conservé par
|
||||
`--bootstrap-no-update` ; la mise à jour automatique des mods reste active.
|
||||
L'instance est nommée **Sanctuary Beta**, avec `ManagedPack=false`,
|
||||
`OverrideCommands=true`, la sélection automatique de Java et le canal stable :
|
||||
|
||||
```text
|
||||
https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Le nom du bouton et le téléchargement direct sont conservés. Le pied de page et
|
||||
le survol indiquent la version réellement publiée. Les réponses de téléchargement
|
||||
utilisent `Cache-Control: no-store` pour garder la même adresse à chaque livraison.
|
||||
|
||||
## Publication demandée via Codex
|
||||
|
||||
Aucune tâche périodique ne décide de publier une version. À la demande du créateur :
|
||||
|
||||
1. Vérifier et publier les nouveaux binaires immuables, puis le canal packwiz
|
||||
selon [le protocole de distribution](packwiz.md).
|
||||
2. Générer l'instance prête avec `python3 scripts/pack.py prism <url-du-canal>`.
|
||||
Joindre `build/Sanctuary-Prism-beta.xxx.zip` à la release, avec le MRpack et
|
||||
le kit serveur. L'export Prism est reproductible et conserve ses commandes.
|
||||
3. Mettre à jour la CT dédiée selon [le contrat serveur](server-pack.md), en
|
||||
conservant sauvegardes, réglages et secrets, puis vérifier son redémarrage.
|
||||
4. Publier le téléchargement Web après contrôle de cette CT et des deux archives :
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_web_pack.py \
|
||||
--web-ct 320 --server-ct 350 --version beta.280 \
|
||||
--mrpack-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack \
|
||||
--prism-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-Prism-beta.280.zip \
|
||||
--pack-url https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Cette dernière commande s'exécute sur `gpu`, où `pct` est disponible. `--dry-run`
|
||||
vérifie sans publier. Le script contrôle les index, les versions, les hashes des
|
||||
dépendances et le JAR Sanctuary de la CT, du canal et du MRpack. Il refuse une
|
||||
archive Prism sans commande active, visant un autre canal ou une autre plateforme,
|
||||
avec un bootstrap non vérifié ou des fichiers étrangers.
|
||||
|
||||
Les fichiers Web restent dans
|
||||
`/var/www/sanctuary/storage/app/distribution/releases/beta.xxx/prism/`.
|
||||
Chaque publication est immuable. Sous verrou, `current` bascule atomiquement après
|
||||
écriture complète. Les anciennes versions restent disponibles pour une reprise.
|
||||
Le script ne redémarre aucun serveur et ne change pas le canal lui-même.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Huit tests Python couvrent l'alignement CT/canal/MRpack, le refus d'un serveur
|
||||
arrêté ou divergent, le contrat d'import Prism prêt, les fichiers interdits et la
|
||||
publication atomique avec conservation de la précédente en cas de conflit.
|
||||
Ils font partie de `./gradlew check`.
|
||||
|
||||
Les quatre tests de téléchargement et les trois tests des métriques Laravel
|
||||
passent, ainsi que Pint : le bouton sert un ZIP anonyme directement, le nom est
|
||||
`Sanctuary-Beta.zip`, les publications incomplètes restent indisponibles, et le
|
||||
changement de version ne demande pas de reconstruire le cache de configuration.
|
||||
Le MRpack reste téléchargeable par son ancien lien.
|
||||
|
||||
Les tests natifs utilisent une extraction de l'archive prête dans un chemin
|
||||
contenant des espaces, avec les variables de sa propre commande avant lancement.
|
||||
Ils exécutent packwiz sans interface ; aucun Minecraft ni monde personnel n'est
|
||||
ouvert. Les fichiers témoins de réglages, favoris et sauvegardes sont comparés
|
||||
avant et après synchronisation. Les journaux sont conservés sous
|
||||
`build/prism-ready-validation*/`, hors de Git.
|
||||
|
||||
`./gradlew check build assemblePack -PsanctuaryFocusedTests=recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` réussit. L'archive prête mesure **92 417 octets**, SHA-256
|
||||
`30dd37f3a6e1d79f8b443f6312918258276c85d5b6a5e7fbdc9c3fea7562dbf2`.
|
||||
Une première tentative native reçoit des HTTP 502 de Gitea ; une nouvelle
|
||||
extraction entièrement vide réussit ensuite ses deux lancements de synchronisation.
|
||||
Le reçu est `build/prism-ready-validation-retry/validation.json`.
|
||||
|
||||
L'archive est ajoutée à la release beta.280 sans changer son tag ni ses artefacts
|
||||
existants. La CT Web publie ensuite le ZIP et le MRpack vérifiés dans la même
|
||||
bascule atomique. Le code Web est livré au commit
|
||||
`46d784ecbb8ba3d0f20199b85bcf486831ba3750` ; les changements préexistants sont
|
||||
conservés et les empreintes des comptes et permissions restent identiques.
|
||||
Sauvegarde privée :
|
||||
`/root/sanctuary-beta-maintenance/web-prism-ready-20261025-1792943502/`.
|
||||
|
||||
Le parcours navigateur **accueil → Télécharger → Sanctuary-Beta.zip reçu** est
|
||||
vérifié à 1280 × 900 et 390 × 844 : HTTP 200 direct, pas de changement de page,
|
||||
nom et SHA-256 identiques à l'archive testée, aucune erreur d'application ni
|
||||
débordement horizontal. Le lien de guide manuel est absent. Le MRpack historique
|
||||
répond toujours HTTP 200. Le navigateur utilisé est Playwright, le plugin
|
||||
Browser étant absent ; reçu et captures sous `/tmp/sanctuary-prism-ready-*`.
|
||||
Reçus opérationnels ignorés : `build/prism-ready-publication.json`,
|
||||
`build/prism-ready-web-publication.json`, `build/prism-ready-web-deployment.log`
|
||||
et `build/prism-ready-build.log`.
|
||||
@@ -0,0 +1,85 @@
|
||||
# ZOMBIES-284 — Boomer et bile
|
||||
|
||||
Branche `codex/boomer`, sur le socle local beta.283 conservé. État : implémenté et vérifié localement en beta.284.
|
||||
|
||||
## Contrat
|
||||
|
||||
Quatrième infecté : modèle adulte du zombie vanilla 26.3, t-shirt violet et
|
||||
pantalon sable recolorés pixel par pixel, visage/peau/UV/alpha conservés.
|
||||
Identifiants `sanctuary:boomer` et `sanctuary:boomer_spawn_egg`.
|
||||
Aucun bébé, y compris via œuf ou chargement ; pas de conversion en noyé.
|
||||
|
||||
- Sauvage : 10 PV, vitesse 0,20, mêlée 2, poursuite 32 blocs. À courte distance,
|
||||
prépare une gerbe pendant 20 ticks, puis vomit dans un cône de 5 blocs,
|
||||
orienté au début de la préparation. Recharge de 120 ticks. La bile révèle
|
||||
pendant 6 secondes et affaiblit pendant 3 secondes ; elle ne crée pas d'acide.
|
||||
- Une cible enduite attire jusqu'à six zombies déjà présents à 12 blocs, avec
|
||||
ligne de vue et chunks chargés, pendant six secondes au maximum. Pas de
|
||||
nouvelles créatures, de chargement distant ou d'horde générée. La diversion
|
||||
expire proprement et conserve la cible précédente si elle est encore valide.
|
||||
Retirer la révélation arrête aussi la diversion ; maximum 64 zombies détournés
|
||||
simultanément par serveur. Les projections touchent au plus 12 victimes.
|
||||
- À sa mort, éclaboussure unique de 3 blocs, 2 dégâts et bile, sans destruction
|
||||
de blocs ni feu. Une suppression administrative n'active pas l'éclaboussure.
|
||||
Les infectés de la famille et les joueurs créatifs/spectateurs sont protégés.
|
||||
Les murs et la différence de hauteur bloquent les projections.
|
||||
- Œuf créatif et apparition rare, poids 4 et groupe d'un dans les habitats
|
||||
de zombies de l'Overworld.
|
||||
- Familier rare, adulte miniature à taille variable persistante, 14 PV et
|
||||
vitesse 0,25. Gerbe automatique en cône et G d'éclaboussure à 3 blocs :
|
||||
dégâts, faiblesse et révélation passent par les règles communes des familiers,
|
||||
avec protection du propriétaire/alliés/duels. Le familier reste vivant et
|
||||
ne détourne pas de monstres contre des tiers.
|
||||
- Recharge du Boomer conservée au chargement ; préparation interrompue annulée.
|
||||
Les diversions sont transitoires ; aucun format existant ne change.
|
||||
|
||||
## Vérification
|
||||
|
||||
- Suite serveur ciblée : **68 tests réussis**, dont **14 nouveaux**. Fixtures
|
||||
plates explicites, graine de test `0`, aucun monde personnel ouvert.
|
||||
- Couverture Boomer : œuf/profil/spawn, règle adulte, préparation et vraie gerbe,
|
||||
esquive latérale, mur et étage, recharge après interruption et sur le tick
|
||||
exact du tir, sauvegarde/rechargement, mort native unique, suppression sans
|
||||
éclaboussure, famille protégée, six zombies maximum sans nouvelle entité,
|
||||
navigation/attaque native et restauration de la cible précédente après délai,
|
||||
absence de chargement distant, G et cône du familier avec protections communes,
|
||||
recharge partagée et tailles 120/500/1000/1500/3000/6000 conservant l'individu.
|
||||
- Parcours client natif **Vulkan / MoltenVK 1.4.2 / Apple M1**, Minecraft 26.3,
|
||||
Java 25, Loader 0.19.5, monde de test neuf de graine `284` : réussi avec arrêt
|
||||
normal du client. Appui réel sur G, dégâts/faiblesse/révélation, recharge et
|
||||
survie du familier ; gerbe sauvage, recrutement d'un zombie existant et
|
||||
éclaboussure après vrai dégât mortel. Trois tailles réelles 500/1000/1500,
|
||||
renderer adulte, œuf, infobulles et écran FR/EN aux échelles GUI 2/3/4 vérifiés.
|
||||
- La première vérification générale a rencontré le test préexistant
|
||||
`Companion019GameTests.creatureProfilesActuallyMove` (déplacement aérien).
|
||||
Les contrôles Boomer passaient ; la suite de 68 a ensuite repassé avec ce
|
||||
test inchangé. Journal conservé dans
|
||||
`build/boomer284-check-build-pack-attempt1.log`.
|
||||
|
||||
- `./gradlew check build assemblePack` réussi en **2 min 40 s**, avec
|
||||
`-PsanctuaryFocusedTests=boomer284,spitter283,charger282,infected280,familiar260,companions`
|
||||
et `-PsanctuaryAtlasOnly=true`. La suite complète ciblée repasse ses 68 tests.
|
||||
- PNG du Boomer comparé en RGBA à la recoloration attendue du zombie natif
|
||||
26.3 épinglé : **331 pixels de t-shirt et 239 de pantalon** modifiés,
|
||||
tête, peau, chaussures, alpha et coordonnées UV conservés. Les textures des
|
||||
trois infectés précédents restent identiques. L'œuf est une recoloration
|
||||
violette de la silhouette et des ombres de l'icône historique, sans changement
|
||||
d'alpha ; aucun outil de génération d'images utilisé.
|
||||
- JAR et export MRpack : ZIP intègres, version beta.284 et dépendances
|
||||
Minecraft 26.3 / Loader 0.19.5 contrôlées. Classes, œuf, texture, profil,
|
||||
loot, traductions et tags Boomer présents ; classes de tests exclues.
|
||||
Le JAR exporté est identique au JAR compilé. Pack beta.283 précédent conservé
|
||||
octet pour octet.
|
||||
|
||||
[Pack local beta.284](../build/Sanctuary-beta.284.mrpack), **16 650 758 octets**.
|
||||
SHA-256 : `4a27ce2b184c8797f0ed7891c6dc62d1db372e2fdabb4fd5de98389df16f25a3`.
|
||||
[Reçu et empreintes](../build/boomer284-validation.json).
|
||||
|
||||

|
||||
|
||||
La livraison reste locale : aucun tag/release, canal packwiz, serveur personnel
|
||||
ou instance Prism modifié. Les œufs restent créatifs ; leur acquisition en
|
||||
survie et les autres infectés demandent leurs propres tickets. Aucune génération
|
||||
de terrain ni sauvegarde personnelle modifiée.
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# ZOMBIES-282 — Chargeur
|
||||
|
||||
Branche `codex/charger`, sur le socle local beta.281 conservé. État : livré et vérifié localement.
|
||||
|
||||
## Contrat
|
||||
|
||||
- `sanctuary:charger`, œuf `sanctuary:charger_spawn_egg`, noms FR/EN. Modèle
|
||||
zombie adulte uniquement, tenue verte et pantalon anthracite ; texture native
|
||||
26.3 recolorée par script, visage, peau, chaussures et UV conservés.
|
||||
- Attributs de base : 10 PV, vitesse 0,27, mêlée 3, poursuite 36 blocs.
|
||||
Charge depuis 4 à 15 blocs avec ligne de vue et sol sous les pieds :
|
||||
préparation visible de 22 ticks, direction figée au départ, course de
|
||||
24 ticks au maximum, choc de 4 dégâts avec recul, récupération de 48 ticks,
|
||||
puis recharge de 100 ticks. Mêlée suspendue pendant ces trois phases.
|
||||
- La collision est celle du déplacement réel : pas de frappe à distance ou
|
||||
à travers un mur. Un joueur peut esquiver en sortant de la trajectoire.
|
||||
Obstacles, cible invalide ou bord d'une zone chargée terminent la charge.
|
||||
Aucune destruction de bloc, aucune téléportation ou génération forcée.
|
||||
- Spawn peu fréquent dans les habitats de zombies de l'Overworld, poids 4,
|
||||
un seul individu. Soleil, statut mort-vivant et absence de conversion en
|
||||
noyé comme l'Infecté. Les autres variantes restent à réaliser.
|
||||
- Familier adulte miniature, taille persistante variable, profil rare dédié :
|
||||
16 PV, vitesse 0,28, mêlée ; technique G de charge droite jusqu'à 8 blocs,
|
||||
préparation 22 ticks, récupération 24 ticks, recharge 12 secondes, dégâts 4.
|
||||
Variante apprise : ruée droite courte de 5 blocs. Droits, alliés, limites de
|
||||
dégâts et de contrôle restent ceux des familiers existants. Une charge
|
||||
déclenchée consomme sa recharge même si elle manque sa cible.
|
||||
- Œuf disponible en créatif ; pas de nouvelle acquisition en survie.
|
||||
|
||||
## Référence et sauvegardes
|
||||
|
||||
Référence en lecture seule : `../26.2/itsalive`, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, variante `CHARGER` et
|
||||
`tickCharger`. Les phases de l'ancien comportement servent de référence ; le
|
||||
contrôle des coups repose ici sur la trajectoire réellement parcourue.
|
||||
|
||||
Le nouveau type a sa propre persistance native. Pour la charge, seule la recharge est sauvée ;
|
||||
une phase interrompue reprend au repos avec une recharge à la relecture.
|
||||
Les identifiants, formats, individus, œufs et textures déjà livrés restent
|
||||
stables. Aucun monde personnel ouvert ; aucun chunk ou terrain réécrit.
|
||||
|
||||
## Validation
|
||||
|
||||
Les essais serveur ciblés ont validé les phases du mob avec déplacement natif,
|
||||
le choc et son recul, la récupération sans double mêlée, l'esquive latérale,
|
||||
un mur ajouté après le départ, la persistance d'une charge interrompue, le
|
||||
profil et l'œuf, les dégâts du familier, sa trajectoire et sa recharge sur
|
||||
échec. Deux contrôles couvrent aussi une téléportation interrompant la course
|
||||
et les commandes d'une monture pendant la préparation. La sonde du corridor
|
||||
complet vérifie les chunks avant toute collision ; elle ne les charge pas.
|
||||
La construction finale **`check build assemblePack` passe en 2 min 59 s**,
|
||||
avec les **45 cas ciblés**, dont onze nouveaux pour le Chargeur, et les contrôles
|
||||
purs du dépôt. Log : `build/charger282-check-build-pack.log`.
|
||||
|
||||
Le parcours client **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en **1 min 03 s** :
|
||||
vrai appui sur G, déplacement et choc du familier, recharge et récupération,
|
||||
phases préparation/course/choc d'un Chargeur sauvage, géométrie adulte,
|
||||
tailles 500/1 000/1 500 ‰, rareté, infobulles et menus **FR/EN GUI 2/3/4**.
|
||||
Douze captures sont conservées sous `build/charger282-screenshots/` ; vues de
|
||||
la miniature ordinaire, de la préparation et du menu FR GUI 2 inspectées.
|
||||
Log : `build/charger282-client-vulkan.log`, marqueur `CHARGER282_CLIENT_PASS`.
|
||||
|
||||
La comparaison des PNG confirme **570 pixels de vêtements recolorés**, avec
|
||||
558 pixels de peau/visage et tous les alpha intacts. La texture de l'Infecté
|
||||
rouge conserve exactement son empreinte beta.281. Génération reproductible :
|
||||
|
||||
```sh
|
||||
python3 scripts/generate_infected_texture.py /chemin/minecraft-26.3-client.jar --variant charger
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=charger282,infected280,familiar260,companions \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryCharger282ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Un contrôle historique de déplacement du dragon a échoué sur un passage
|
||||
intermédiaire, puis passé sans changement sur le suivant ; sa stabilité n'est
|
||||
pas démontrée par ce ticket. Un premier parcours client a passé ses étapes
|
||||
avant une erreur fastutil dans `ChunkMap` à la fermeture du serveur de test.
|
||||
La fixture nettoie désormais ses acteurs et attend leur retrait ; le parcours
|
||||
final se termine normalement.
|
||||
|
||||
## Essai local
|
||||
|
||||
```mcfunction
|
||||
/give @s sanctuary:charger_spawn_egg
|
||||
/summon sanctuary:charger
|
||||
```
|
||||
|
||||
L'œuf équipé invoque le familier miniature. G lance la charge ; les ordres et
|
||||
la sélection d'aptitudes suivent les contrôles habituels. Pendant une charge
|
||||
montée, préparation et récupération comprises, le pilote ne peut pas détourner
|
||||
la trajectoire avec les commandes de déplacement. Les autres profils utilisant
|
||||
`CHARGE` gardent leur comportement ; seul l'effet de profil `straight` utilise
|
||||
ce nouveau contrôleur.
|
||||
|
||||
La fréquence des rencontres naturelles en partie longue et le comportement
|
||||
sur toutes les formes de relief ne sont pas mesurés. Aucun monde personnel,
|
||||
serveur, canal packwiz ou instance Prism modifié.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- Mod : `mods/sanctuary/build/libs/sanctuary-beta.282.jar`.
|
||||
- Pack : `build/Sanctuary-beta.282.mrpack`.
|
||||
- Captures : `build/charger282-screenshots/`.
|
||||
- Reçu : `build/charger282-validation.json`.
|
||||
|
||||
JAR et MRpack vérifiés : version exacte, intégrité ZIP, Minecraft 26.3, Fabric
|
||||
Loader 0.19.5, un seul JAR Sanctuary identique au build, profils et contrôleurs
|
||||
de charge présents, deux textures identiques aux sources et fixtures exclues.
|
||||
@@ -0,0 +1,101 @@
|
||||
# CLAIM-03 — Chunks et protection temporaire, beta.272
|
||||
|
||||
Branche `codex/inhabitant-property`. Contrat du 24 octobre 2026, avant changement des données.
|
||||
|
||||
Un claim est un chunk entier de 16 × 16 blocs du monde principal, sur toute
|
||||
sa hauteur. L'admission Hello World donne accès au menu, sans terrain attribué.
|
||||
Les cinq premiers chunks sont gratuits ; le sixième coûte 100 points d'XP,
|
||||
le septième 200, puis +100 par chunk. Limite technique : 256 chunks par joueur,
|
||||
10 000 par monde. Les chunks peuvent être séparés. Aucun abandon ni transfert
|
||||
n'est proposé dans ce lot.
|
||||
|
||||
Chaque chunk peut recevoir sept jours de protection contre **64 saphirs**.
|
||||
Le propriétaire mine alors à ×2 et les autres à ×0,5. Sans protection : ×1.
|
||||
L'effet suit le bloc effectivement miné, même dans une sélection groupée.
|
||||
La pose, l'utilisation, les explosions, fluides et autres mécanismes gardent
|
||||
leurs règles normales. Le claim demeure après expiration. Le renouvellement
|
||||
est manuel et ajoute sept jours à la durée restante, sans prélèvement automatique.
|
||||
|
||||
Le temps compte en millisecondes monotones de serveur démarré, y compris sans
|
||||
propriétaire connecté. Il ne dépend ni du calendrier ni du nombre de ticks.
|
||||
L'arrêt normal sauvegarde l'horloge ; la relance reprend sa valeur sans compter
|
||||
l'arrêt. Sauvegarde toutes les 60 secondes et à chaque achat : un arrêt brutal
|
||||
peut restituer au plus le temps depuis le dernier point de sauvegarde (environ
|
||||
60 secondes, ou davantage si la boucle serveur est bloquée).
|
||||
|
||||
## Contrat de sauvegarde et migration
|
||||
|
||||
Nouveau registre séparé `<monde>/sanctuary/chunk-claims-v1.json`, schéma 1,
|
||||
lié à la graine. Il contient l'horloge serveur, une révision, les chunks, les
|
||||
transactions en attente et les UUID ayant volontairement quitté l'ancien modèle.
|
||||
Écritures atomiques vérifiées ; format invalide conservé et mutations suspendues.
|
||||
Un reçu persistant natif joueur `sanctuary:chunk_purchase_receipt_v1` relie le
|
||||
débit XP/saphirs ou le remboursement à la validation du registre. Une transaction
|
||||
réservée sans reçu sauvegardé est annulée ; un reçu durable la valide une seule fois.
|
||||
|
||||
L'ancien `properties-v1.json` reste dans son format exact. Ses volumes et leurs
|
||||
droits restent actifs tant que le propriétaire ne confirme pas la migration.
|
||||
La migration retire volontairement les droits de son volume et rembourse les
|
||||
extensions : 100, 300, 600 ou 1 000 XP selon le palier. Aucun chunk n'est créé
|
||||
à partir du volume : le joueur choisit ensuite ses chunks, dont cinq gratuits.
|
||||
Les achats anciens en attente sont résolus avant cette migration. Un nouveau
|
||||
chunk ne peut recouvrir l'emprise horizontale d'un volume ancien encore actif.
|
||||
Aucun bloc, terrain, génération, seed ou sauvegarde personnelle n'est modifié.
|
||||
|
||||
Un **clic droit directement sur la carte** ouvre un petit panneau sur le chunk :
|
||||
propriétaire, tarif, durée et bouton Revendiquer ou Protéger. Le prix apparaît
|
||||
avant l'action ; le clic sur ce bouton réalise l'achat sans quitter la carte.
|
||||
Échap ou un clic ailleurs ferme ce panneau. Aucun menu de grille séparé et aucun
|
||||
mode à activer pour le clic droit de l'Atlas ordinaire.
|
||||
|
||||
Habitant → Propriété et `/sanctuary property` ouvrent cette carte directement.
|
||||
Avant l'aptitude Atlas, elle montre uniquement le terrain déjà chargé et envoyé
|
||||
au joueur, à huit chunks au plus, sans conserver un historique supplémentaire.
|
||||
Les échantillons sont bornés à quatre tuiles par joueur et par tick, dans un
|
||||
budget global de 2 ms, sans charger de chunks. L'Atlas complet garde ses
|
||||
prérequis ; une fois débloqué, les tuiles découvertes peuvent aussi être choisies.
|
||||
Le serveur contrôle la portée, la frontière, la réserve de départ de 32 blocs,
|
||||
les fonds et la révision. La migration de l'ancien volume requiert une seconde
|
||||
confirmation explicite dans le panneau, avec le remboursement indiqué.
|
||||
|
||||
## Validation
|
||||
|
||||
Les **58 vérifications indépendantes** du registre passent : cinq claims gratuits,
|
||||
prix suivants, conflits, révision périmée, reprise avec ou sans reçu, horloge,
|
||||
trente jours d’arrêt simulés, renouvellement, expiration, migration et refus de
|
||||
JSON invalide ou d’une graine différente.
|
||||
|
||||
Le parcours client **Vulkan**, en **FR/EN GUI 2/3/4**, réussit en **54 s** :
|
||||
admission réelle, carte proche avant aptitude Atlas, recentrages rapides,
|
||||
clic sur un chunk voisin (9, 8 alors que le joueur est en 8, 8), premier claim,
|
||||
débit d’un stack de saphirs, puis clic droit dans l’Atlas ordinaire, panneau
|
||||
compact et Échap conservant la carte. Les huit captures finales sont dans
|
||||
`build/claims272-screenshots/` ; les formats FR GUI 2/4 et EN GUI 4 ont été
|
||||
inspectés. La fermeture ne reproduit plus l’exception de parcours des sessions
|
||||
Blocodex : le flush parcourt désormais un instantané stable.
|
||||
|
||||
Les tests natifs ciblent les paiements XP et saphirs, les reçus sauvegardés dans
|
||||
le joueur, les vrais `getDestroyProgress`, la frontière du chunk, la hauteur,
|
||||
les interactions libres, la migration remboursée et les refus terrain inconnu / spawn.
|
||||
Le lot couvre aussi les anciens volumes, le minage, les menus et l’overview.
|
||||
`check build assemblePack` réussit : **25 tests natifs ciblés**, en 2 min 44 s
|
||||
pour la commande complète. Le MRpack beta.272 est exporté et vérifié : ZIP,
|
||||
Minecraft 26.3, Loader 0.19.5 et octets des deux JAR locaux identiques aux builds.
|
||||
Les archives beta.270 et beta.271 gardent leurs hashes antérieurs.
|
||||
La validation finale et le MRpack sont consignés dans
|
||||
`build/claims272-validation.json`.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=claims272,property270,mining,overview271,menus \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryClaims272ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le multiplicateur s’applique à la progression native ; les blocs instantanés,
|
||||
le Créatif et la granularité des ticks conservent leur fonctionnement Minecraft.
|
||||
Abandon, transfert, partage de droits et protection contre les mécanismes
|
||||
indirects restent hors de ce lot. Aucun serveur ou instance personnelle déployé.
|
||||
@@ -0,0 +1,120 @@
|
||||
# CLAIM-278 — Duel Battle consenti pour une propriété
|
||||
|
||||
Branche `codex/claim-battle-duels`, demande du 25 octobre 2026.
|
||||
Le créateur confirme que cette étape vise les propriétés déjà revendiquées.
|
||||
|
||||
## Contrat du duel
|
||||
|
||||
Depuis le clic droit sur un claim, un autre habitant peut proposer de le disputer.
|
||||
Le challenger doit avoir atteint le seuil d’habitation Demeure sur ce chunk ;
|
||||
la priorité d’influence ne lui est pas nécessaire pour une proposition consentie.
|
||||
Le propriétaire garde ses droits acquis, même sans preuve Demeure rétroactive.
|
||||
Les deux joueurs doivent avoir Carte active et être disponibles pour Battle.
|
||||
|
||||
Le challenger valide une proposition explicite ; le propriétaire accepte ou
|
||||
refuse celle-ci. Le chunk, les deux participants, le temps de protection restant
|
||||
et les règles sont affichés. Une acceptation ne vaut que pour cette proposition.
|
||||
Une modification du claim avant acceptation invalide la proposition.
|
||||
|
||||
L’acceptation crée un Battle privé à deux joueurs : joueurs seuls, rayon de
|
||||
48 blocs autour du lieu annoncé par le challenger, cinq minutes, nourriture,
|
||||
potions et perles autorisées, mise d’entrée nulle. Les deux joueurs doivent
|
||||
être dans ce périmètre. Chacun valide ensuite Prêt dans Battle ; le challenger
|
||||
lance la préparation native de dix secondes. Aucun troisième participant ne
|
||||
peut rejoindre ce duel. Les morts, tombes, abandons et déconnexions pendant
|
||||
le combat suivent les règles Battle existantes. Elles sont rappelées avant
|
||||
l’accord. Pas de téléportation ni terrain créé automatiquement.
|
||||
|
||||
Le vainqueur devient propriétaire du seul chunk annoncé. Le défenseur gagnant
|
||||
conserve son claim. L’échéance absolue de protection est conservée : aucun temps
|
||||
n’est ajouté, retiré ou remboursé. Couleur, nom et multiplicateurs de minage
|
||||
suivent le nouveau propriétaire. Aucun coût XP ou saphir supplémentaire n’est
|
||||
prélevé pour transférer un claim déjà acheté.
|
||||
|
||||
Refus, annulation avant lancement, expiration d’invitation, égalité ou interruption
|
||||
du serveur sans résultat enregistré : aucune mutation de propriété. Un abandon
|
||||
ou une déconnexion après lancement vaut défaite selon Battle. Les autres claims
|
||||
et les duels ordinaires restent indépendants.
|
||||
|
||||
## Contrat de persistance et reprise
|
||||
|
||||
Aucune modification des schémas de Demeure, des claims, des mises Arena ou des
|
||||
reçus joueurs. Aucun monde personnel, chunk ou génération n’est modifié par les
|
||||
essais. Le transfert met à jour uniquement le propriétaire d’un claim existant,
|
||||
dans le schéma 1 de `sanctuary/chunk-claims-v1.json`, par écriture atomique.
|
||||
|
||||
Nouveau journal indépendant `sanctuary/claim-duels-v1.json`, schéma 1, lié à la
|
||||
graine. Il enregistre les deux accords sur le chunk et son état initial avant
|
||||
la création du Battle, puis le vainqueur avant toute mutation de propriétaire.
|
||||
Le résultat est appliqué par comparaison de l’état attendu, puis clôturé. Une
|
||||
reprise après interruption entre l’écriture du résultat et sa clôture applique
|
||||
ce transfert une seule fois. Les accords sans résultat durable sont annulés
|
||||
au redémarrage ; les mises Battle suivent leur propre journal de remboursement.
|
||||
|
||||
Les mutations du claim sont bloquées pendant un accord actif ou un résultat à
|
||||
régler. Les limites de propriété sont vérifiées avant accord ; les participants
|
||||
inscrits à Battle ne peuvent acheter d’autres claims pendant celui-ci. Les
|
||||
fichiers invalides ou modifiés extérieurement sont conservés et les nouveaux
|
||||
duels sont refusés. Une erreur avant écriture durable du vainqueur ne transfère
|
||||
pas la propriété ; une erreur après cette écriture garde le résultat à reprendre.
|
||||
Terminer les duels et solder le journal avant retour à une version antérieure.
|
||||
|
||||
## Vérification
|
||||
|
||||
`check build assemblePack` réussit en **2 min 55 s**, avec **40 tests serveur
|
||||
ciblés**. Les six nouveaux scénarios vérifient l’absence de transfert ou de
|
||||
combat sans le deuxième accord, les refus du challenger et des tiers tentant
|
||||
d’accepter à la place du propriétaire, le refus du propriétaire, la relecture
|
||||
de la proposition la plus récente, les validations Prêt natives, la mort réelle
|
||||
résolvant Battle et transférant le chunk exact, la conservation de son échéance,
|
||||
l’abandon après lancement, l’annulation avant lancement, l’égalité native au
|
||||
délai écoulé et l’invalidation d’un enjeu modifié ou expiré. Une arène publique
|
||||
ordinaire accepte toujours un troisième inscrit ; y gagner ne donne aucun claim.
|
||||
|
||||
Les **26 vérifications pures** du journal couvrent le consentement durable,
|
||||
les identités et chunks réservés, le refus d’un gagnant tiers, le redémarrage
|
||||
sans résultat, le résultat enregistré avant transfert, la reprise après mutation
|
||||
avant clôture, l’idempotence, la victoire du défenseur, les conflits de propriétaire
|
||||
ou de protection, la limite de 256 claims, la graine, les nombres JSON stricts
|
||||
et les fichiers corrompus ou modifiés extérieurement. Les **63 contrôles du
|
||||
registre de claims**, Demeure et les scénarios historiques ciblés passent aussi.
|
||||
|
||||
Le parcours **Vulkan FR/EN GUI 2/3/4** réussit en **53 s**, avec **18 captures**.
|
||||
Il ouvre le dossier depuis le clic droit de l’Atlas, lit les conséquences et
|
||||
les boutons d’acceptation/refus dans les deux langues aux trois tailles, refuse
|
||||
par le vrai paquet client, puis accepte une nouvelle proposition. L’enjeu est
|
||||
présent dans Battle avant Prêt. Une victoire native du propriétaire conserve
|
||||
son terrain. Après le délai de sortie de combat existant, le client devient
|
||||
challenger d’une autre propriété ; son propriétaire accepte indépendamment,
|
||||
les deux valident Prêt et le client lance réellement Battle. La mort native du
|
||||
défenseur transfère le claim et son échéance exacte. La carte déjà ouverte
|
||||
reçoit le nouveau propriétaire/couleur et raccorde le CTM au claim voisin sans
|
||||
réouverture. Captures : `build/claims278-screenshots/` ; inspections des écrans
|
||||
FR GUI 4 d’accord, FR GUI 2 de Battle et de la carte après transfert.
|
||||
|
||||
La réouverture d’un dossier n’est pas limitée par le délai anti-double-action :
|
||||
les lectures peuvent changer immédiatement de proposition, tandis que les
|
||||
mutations restent temporisées. Le panneau d’accord dispose d’un fond lisible
|
||||
et son compteur n’oblige pas à revenir en haut du défilement chaque seconde.
|
||||
Les combattants de ce parcours sont à l’air libre dans un monde plat ignoré.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=claims278,claims277,menu275,claims273,claims272,property270,mining,overview271,menus,map,demeure \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryClaims278ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
MRpack beta.278 exporté et vérifié : intégrité ZIP, Minecraft 26.3, Loader 0.19.5,
|
||||
versions/libellés, nouveaux contrôleurs présents et deux JAR identiques aux
|
||||
builds locaux. Les huit archives beta.270 à beta.277 gardent leurs empreintes.
|
||||
Reçu : `build/claims278-validation.json`. Aucun déploiement de serveur, monde
|
||||
personnel ou instance Prism, ni publication de canal.
|
||||
|
||||
Le lot porte sur un seul chunk déjà revendiqué et un Battle entre joueurs.
|
||||
Les familiers/modes mixtes, ensembles de claims, transferts sans combat ou
|
||||
attributions de terrains encore libres ne sont pas ajoutés. Les reprises sont
|
||||
éprouvées aux étapes du journal, sans simulation d’une panne physique du disque.
|
||||
@@ -0,0 +1,80 @@
|
||||
# CLAIM-276 — Contours CTM et protection à huit saphirs
|
||||
|
||||
Branche `codex/inhabitant-property`, demande du 25 octobre 2026.
|
||||
|
||||
## Résultat demandé
|
||||
|
||||
Toutes les propriétés doivent apparaître sur l’Atlas et son aperçu de Pause.
|
||||
Les claims d’un même propriétaire forment un contour continu, sans traits
|
||||
internes entre chunks adjacents. Une propriété sans protection garde uniquement
|
||||
son contour ; un chunk protégé ajoute un fond à la couleur du propriétaire.
|
||||
La protection coûte désormais **8 saphirs par chunk pour trois jours de serveur
|
||||
en marche**. Un arrêt du serveur suspend toujours le compteur ; une déconnexion
|
||||
ne le suspend pas. Les multiplicateurs de minage ×2 / ×0,5 restent identiques.
|
||||
|
||||
## Rendu et accès
|
||||
|
||||
- Contour d’un pixel de carte, directement dans les textures de terrain et de
|
||||
calque, avec les mêmes coordonnées et le même échantillonnage que la carte.
|
||||
- Raccords par propriétaire sur les quatre côtés et les angles rentrants ;
|
||||
la protection d’un voisin ne crée pas de frontière intérieure de propriété.
|
||||
- Fond uniquement sur les chunks encore protégés. À expiration, le contour
|
||||
reste visible et la propriété demeure attribuée.
|
||||
- Toute arrivée, suppression ou modification de claim recalcule aussi ses
|
||||
voisins, y compris les diagonales. Les achats actualisent la carte ouverte.
|
||||
- Couleur persistante du personnage, même hors ligne ; légende compacte pour
|
||||
tous les propriétaires visibles, survol et masquage conservés.
|
||||
- Aucun pixel de brouillard ou de vide n’est coloré. Seules les métadonnées des
|
||||
tuiles effectivement transmises sont acceptées. L’aptitude Carte achetée et
|
||||
active reste obligatoire pour ouvrir et gérer les propriétés.
|
||||
- Demeure et la disposition Habitant / Pause de beta.275 sont conservés.
|
||||
|
||||
## Contrat de conservation
|
||||
|
||||
Le schéma 1 de `sanctuary/chunk-claims-v1.json` et les reçus natifs restent
|
||||
identiques. Aucune réécriture automatique d’échéance, conversion de terrain,
|
||||
remise à zéro, attribution depuis Demeure ou régénération n’est effectuée.
|
||||
Les protections de sept jours déjà payées gardent leur échéance exacte.
|
||||
Une transaction de sept jours préparée par beta.272–275 reste récupérable :
|
||||
son reçu de paiement natif valide sa durée d’origine ; sans reçu, elle est
|
||||
annulée comme avant, sans retirer la couverture antérieure. Le lecteur accepte
|
||||
les deux périodes préparées, trois et sept jours ; les nouvelles réservations
|
||||
créées par beta.276 utilisent uniquement trois jours. Chaque renouvellement
|
||||
ajoute trois jours à l’échéance en cours si elle est encore active, sinon au
|
||||
temps serveur courant, contre huit saphirs. Aucun remboursement rétroactif
|
||||
n’est appliqué aux achats antérieurs.
|
||||
|
||||
## Vérification
|
||||
|
||||
`check build assemblePack` réussit en **2 min 38 s**, avec **30 tests serveur
|
||||
ciblés** et **63 vérifications du registre**. Les achats natifs refusent sept
|
||||
saphirs sans mutation, consomment exactement huit saphirs répartis sur plusieurs
|
||||
piles, couvrent un seul chunk pour trois jours et ajoutent exactement trois
|
||||
jours lors d’un renouvellement. La reprise avec/sans reçu d’une ancienne période
|
||||
de sept jours et le renouvellement de cette couverture après redémarrage sont
|
||||
vérifiés. Les protections et multiplicateurs restent applicables sans Carte.
|
||||
|
||||
Le parcours **Vulkan FR/EN GUI 2/3/4** réussit en **50 s**, avec **19 captures**.
|
||||
Il achète réellement Carte et une protection, contrôle les pixels du contour et
|
||||
du fond, le raccord en L et carré, les frontières entre propriétaires, le nom et
|
||||
la couleur du propriétaire hors ligne, l’arrivée d’un voisin sans réouverture,
|
||||
l’expiration dans le modèle, les paquets périmés et la borne du monde, le
|
||||
masquage/réaffichage et les pixels de l’aperçu de Pause. Les libellés montrent
|
||||
huit saphirs et trois jours. Captures dans `build/claims276-screenshots/` ;
|
||||
inspection des vues carte FR GUI 2, contexte FR GUI 4 / EN GUI 2 et Pause GUI 4.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=menu275,claims273,claims272,property270,mining,overview271,menus,map,demeure \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryClaims276ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
MRpack beta.276 exporté et vérifié : intégrité ZIP, Minecraft 26.3, Loader 0.19.5,
|
||||
versions et libellés FR/EN, deux JAR embarqués identiques aux builds locaux.
|
||||
Les six archives beta.270 à beta.275 gardent leurs empreintes. Reçu :
|
||||
`build/claims276-validation.json`. Aucun déploiement personnel ni publication
|
||||
de canal n’est effectué.
|
||||
@@ -0,0 +1,98 @@
|
||||
# CLAIM-277 — Habitation et influence avant revendication
|
||||
|
||||
Branche `codex/claims-demeure`, demande du 25 octobre 2026.
|
||||
Le créateur confirme le seuil existant de Demeure et précise qu’un autre joueur
|
||||
peut revendiquer le terrain s’il prend plus d’influence avant le premier claim.
|
||||
|
||||
## Règles
|
||||
|
||||
Une simple visite ne permet plus de revendiquer un chunk. Demeure doit y
|
||||
reconnaître un habitant : influence totale d’au moins 0,8, dont au moins 20 %
|
||||
pour un joueur, selon les seuils déjà utilisés par sa lecture du territoire.
|
||||
Aucun compteur de minutes, nuits ou délai supplémentaire n’est introduit.
|
||||
|
||||
Sur un chunk libre, l’habitant avec le plus d’influence effective actuelle peut
|
||||
revendiquer. À égalité (tolérance numérique 10⁻⁹ pour les arrondis), l’ordre
|
||||
persisté des premières contributions acceptées départage les habitants.
|
||||
Ainsi, le premier garde la priorité jusqu’à ce qu’un autre le dépasse. Les
|
||||
contributions peuvent précéder l’achat de Carte. Une identité hors ligne peut
|
||||
rester prioritaire ; les autres joueurs peuvent la dépasser par leur activité.
|
||||
La décroissance native de Demeure continue de s’appliquer aux terrains libres.
|
||||
|
||||
Le clic droit sur la carte explique la condition manquante ou nomme l’habitant
|
||||
prioritaire ; l’action est désactivée si la condition n’est pas remplie. Le
|
||||
panneau d’un terrain libre s’actualise. Commande et paquets d’action vérifient
|
||||
la même priorité côté serveur, au moment de l’achat, avant toute réservation
|
||||
ou dépense. Aucun duel ou transfert automatique n’est ajouté.
|
||||
|
||||
Une fois revendiqué, le claim reste attribué malgré les variations d’influence.
|
||||
Tous les claims antérieurs et leurs durées de protection sont conservés, sans
|
||||
exiger de preuve rétroactive. Leur protection reste renouvelable même si Demeure
|
||||
est indisponible. Cinq claims gratuits puis XP, huit saphirs pour trois jours de
|
||||
serveur, contours CTM et accès par Carte achetée et active restent inchangés.
|
||||
|
||||
## Contrat de conservation
|
||||
|
||||
Aucun nouveau fichier ni changement de schéma. `data/demeure/footprints-v1.json`
|
||||
et `sanctuary/chunk-claims-v1.json` restent identiques. L’ordre des premières
|
||||
traces est déjà conservé par la liste des influences et son rechargement ; on
|
||||
ne prétend pas reconstituer une date de franchissement du seuil absente des
|
||||
anciennes données. La requête de priorité est une lecture des empreintes,
|
||||
indépendante de leur tri visuel par influence et de l’UUID des joueurs.
|
||||
|
||||
Demeure conserve la responsabilité des traces, des budgets, des seuils, de la
|
||||
décroissance et de leur persistance. Sanctuary applique ses conditions de
|
||||
revendication. Les empreintes sont sauvegardées atomiquement avant de réserver
|
||||
ou débiter un nouveau claim. Une lecture ou écriture refusée empêche cet achat
|
||||
sans effacer le fichier ; les propriétés déjà acquises restent valables.
|
||||
|
||||
Un paquet de lecture `sanctuary:claim_residency_v1` fournit l’état et le nom/couleur
|
||||
prioritaire pour le seul chunk sélectionné et accessible. Les anciens codecs
|
||||
et identifiants restent inchangés. Aucun état d’habitation n’est transmis pour
|
||||
un terrain inaccessible ou avant Carte. Aucun monde personnel ni génération
|
||||
n’est modifié par les essais.
|
||||
|
||||
## Vérification
|
||||
|
||||
`check build assemblePack` réussit en **2 min 52 s**, avec **34 tests serveur
|
||||
ciblés**. Les quatre nouveaux tests couvrent les activités natives nécessaires,
|
||||
le refus d’un passage ou d’un achat sans habitation, cinq claims gratuits puis
|
||||
le coût du sixième, l’égalité et le dépassement avant revendication, l’acquisition
|
||||
par le nouvel habitant prioritaire, l’absence de transfert après achat et le
|
||||
renouvellement sans domination Demeure. Les propriétés anciennes sans preuve
|
||||
rétroactive restent protégées ; le paquet de lecture est testé, ainsi que le
|
||||
refus de divulguer les habitants d’un chunk inaccessible ou avant Carte.
|
||||
|
||||
Les tests purs Demeure ajoutent **14 scénarios d’habitation, priorité et point
|
||||
de sauvegarde**, dont l’ordre conservé après rechargement, indépendamment du tri
|
||||
visuel et des UUID, la décroissance, les dimensions et l’intégrité du fichier
|
||||
même quand le modèle en mémoire n’a pas changé. Les **63 vérifications du registre
|
||||
de claims** et les scénarios historiques ciblés passent aussi. Les montages de
|
||||
paiement et de migration préparent leurs traces d’habitation indépendamment des
|
||||
tentatives de claim : un clic refusé ne crée aucune influence.
|
||||
|
||||
Le parcours **Vulkan FR/EN GUI 2/3/4** final réussit en **51 s**, avec **28 captures**.
|
||||
Il passe par Hello World et l’achat de Carte, constate le bouton désactivé avant
|
||||
habitation, le refus d’un simple passage, le changement de priorité quand Nora
|
||||
prend plus d’influence et son retour au joueur lorsque celui-ci la dépasse.
|
||||
Un autre spot libre est réellement revendiqué par Nora après dépassement.
|
||||
Le nom/couleur prioritaire et le bouton désactivé sont vérifiés dans les deux
|
||||
langues aux trois tailles ; les contextes d’achat et de protection, les contours
|
||||
CTM, le masquage et l’aperçu de Pause restent valides. Captures dans
|
||||
`build/claims277-screenshots/`, avec inspection des panneaux FR GUI 4 et EN.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=claims277,menu275,claims273,claims272,property270,mining,overview271,menus,map,demeure \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryClaims277ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
MRpack beta.277 exporté et vérifié : ZIP, Minecraft 26.3, Loader 0.19.5,
|
||||
versions et libellés FR/EN, octets des deux JAR embarqués identiques aux builds
|
||||
locaux. Les archives beta.270 à beta.276 restent identiques. Reçu :
|
||||
`build/claims277-validation.json`. Aucune publication de canal ni synchronisation
|
||||
d’installation personnelle n’est effectuée.
|
||||
@@ -0,0 +1,71 @@
|
||||
# CLAIM-04 — Couleurs et état des claims sur la carte, beta.273
|
||||
|
||||
Branche `codex/inhabitant-property`, demande du 24 octobre 2026.
|
||||
|
||||
Chaque chunk revendiqué porte la couleur persistante de son propriétaire,
|
||||
choisie dans Hello World. Le nom et la couleur restent disponibles hors ligne.
|
||||
Un claim protégé a une teinte plus présente, une bordure pleine et un bouclier ;
|
||||
un claim sans protection a une teinte légère et une bordure en pointillés.
|
||||
Le survol donne le propriétaire, l’état et la durée restante ; une légende
|
||||
rappelle les deux styles. Le clic droit continue à ouvrir le panneau existant.
|
||||
|
||||
Un bouton **Propriétés : oui/non** masque ou réaffiche la teinte, les bordures,
|
||||
les boucliers, la légende et les infobulles de claims. Le choix est partagé
|
||||
avec l’aperçu de Pause et conservé tant que le client reste ouvert, y compris
|
||||
après fermeture de la carte. Le clic droit et les protections restent actifs ;
|
||||
les données continuent de se mettre à jour pour un réaffichage immédiat.
|
||||
Ce choix de session n’ajoute aucun fichier ni format de sauvegarde.
|
||||
|
||||
Les apparences sont envoyées automatiquement avec les tuiles de la carte,
|
||||
pour la vue proche comme pour l’Atlas. Aucun clic préalable n’est nécessaire.
|
||||
Seules les tuiles déjà transmises au joueur reçoivent leurs claims ; aucun
|
||||
terrain inconnu n’est révélé. Une acquisition ou un renouvellement actualise
|
||||
la tuile affichée. L’expiration change le style depuis la durée serveur restante,
|
||||
sans retirer la propriété. Les réponses d’une ancienne requête sont ignorées. Les achats diffusent aussi
|
||||
une mise à jour ponctuelle des cellules déjà observées, indépendante d’un
|
||||
rafraîchissement en cours ; aucune cellule inconnue n’est acceptée côté client.
|
||||
|
||||
Ce lot ajoute un paquet de présentation borné. Les identifiants existants,
|
||||
registres, reçus, règles, prix, durées et formats de sauvegarde restent identiques
|
||||
à beta.272. Aucune migration ni modification de terrain.
|
||||
|
||||
## Validation
|
||||
|
||||
Le parcours **Vulkan FR/EN, GUI 2/3/4**, passe en **62 s** sur un nouveau monde
|
||||
plat de graine 273. Il conserve l’admission et les achats réels de beta.272,
|
||||
puis compare quatre chunks contigus appartenant à deux propriétaires : chacun
|
||||
a un chunk protégé et un chunk sans protection. L’un des propriétaires n’est pas
|
||||
connecté. Les couleurs distinctes, le nom hors ligne, les deux styles, les
|
||||
mises à jour après achat, les requêtes rapides, le clic droit, Échap et l’aperçu
|
||||
de Pause sont vérifiés. Le masquage, sa conservation après fermeture, le clic
|
||||
droit sous masquage et le réaffichage sont contrôlés, avec captures de la carte
|
||||
et de l’aperçu de Pause sans propriétés. **19 captures** sont conservées dans
|
||||
`build/claims273-screenshots/`, avec inspection FR GUI 2/4, EN GUI 4 et Pause.
|
||||
|
||||
Le cache est également vérifié avec une horloge contrôlée : expiration sans
|
||||
perte de propriété, réponse périmée ignorée, mise à jour d’achat pendant un
|
||||
rafraîchissement, cellule inconnue refusée et remplacement par une tuile vide.
|
||||
Le test serveur ajouté couvre le nom et le RGB persistants d’un propriétaire
|
||||
hors ligne, la durée et le codec du paquet, avec refus hors des cellules
|
||||
transmises et au-delà de 64 cellules. Les registres conservent leurs 58
|
||||
vérifications indépendantes de beta.272.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=claims273,claims272,property270,mining,overview271,menus \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryClaims273ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
`check build assemblePack` réussit en **2 min 41 s**, avec **26 tests serveur
|
||||
ciblés** et les 58 vérifications indépendantes du registre. Le MRpack beta.273
|
||||
est exporté et vérifié : ZIP, Minecraft 26.3, Loader 0.19.5 et octets des deux
|
||||
JAR locaux identiques aux builds. Les archives beta.270 à beta.272 restent
|
||||
inchangées.
|
||||
|
||||
Le reçu final de build et de distribution est dans
|
||||
`build/claims273-validation.json`. Aucun serveur, canal packwiz, monde ou
|
||||
instance personnelle déployé. Aucun changement du comportement de protection.
|
||||
@@ -0,0 +1,75 @@
|
||||
# MAP-CTM — Calque Demeure continu, beta.274
|
||||
|
||||
Branche `codex/inhabitant-property`. Demande du 24 octobre 2026, après beta.273.
|
||||
La [refonte des propriétés](property-demeure-refonte.md) reste un cadrage en
|
||||
attente ; la fiche Habitant est conservée. Le créateur a confirmé que les claims
|
||||
actuels doivent rester accessibles.
|
||||
|
||||
Le calque Demeure garde sa texture de 1 024 × 1 024 pixels alignée sur celle du
|
||||
terrain, mais retire les cadres de chunks. Deux cellules adjacentes ayant la
|
||||
même couleur et la même influence se raccordent sans couture interne, y compris
|
||||
aux coins. Il ne s’agit pas d’ajouter des textures CTM de blocs dans le monde.
|
||||
|
||||
La teinte est uniforme dans chaque cellule connue. Sa présence suit la **part
|
||||
relative d’influence** déjà calculée par Demeure, avec un alpha de 16 + 96 × part / 100
|
||||
(0 si la part est nulle). Ce pourcentage n’est pas un compteur de jours habités
|
||||
ou une preuve d’ancienneté. Le terrain, le brouillard et le vide gardent leurs
|
||||
pixels ; seuls les chunks modifiés sont repeints. Un changement de pourcentage
|
||||
actualise maintenant la teinte même si la couleur du personnage reste identique.
|
||||
|
||||
## Protections, dans le même calque pixel
|
||||
|
||||
Une protection active prend une couleur uniforme (alpha 64/255) dans la même
|
||||
texture que Demeure : plus de rectangles GUI par chunk, de cadres, de pointillés
|
||||
ou de boucliers. Les cellules voisines de même propriétaire se rejoignent sans
|
||||
trait intérieur. Un claim sans protection reste déclaré et accessible au clic
|
||||
droit, sans aplat de protection. Si les deux affichages sont activés, la couleur
|
||||
de protection est prioritaire sur l’influence Demeure.
|
||||
|
||||
La légende associe les couleurs aux personnages ayant une protection visible,
|
||||
avec trois noms au maximum puis un compteur. Le bouton de masquage reste partagé
|
||||
avec Pause. Le calque est invalidé par les achats, les données de Demeure, les
|
||||
changements d’affichage et l’expiration ; les cellules expirées sont repeintes
|
||||
une fois. L’aperçu de Pause compose la même teinte dans ses propres pixels de
|
||||
terrain, en conservant le masque du vide et du brouillard. Aucun dessin de
|
||||
bordure ne dépend de l’adjacence : le raccord résulte des pixels uniformes.
|
||||
|
||||
L’infobulle du bouton décrit la présence et l’influence d’habitation, sans
|
||||
« aucun claim ni protection ». Les règles, scores et formats de Demeure,
|
||||
les droits et données de propriété, la génération et les mondes personnels
|
||||
ne sont pas modifiés par cette correction d’affichage.
|
||||
|
||||
## Vérification
|
||||
|
||||
`check build assemblePack` réussit en **2 min 50 s** : **29 tests serveur ciblés**
|
||||
(claims, ancien modèle, minage, overview, menus, Atlas et Demeure). Les contrôles
|
||||
indépendants du registre restent réussis. Les deux parcours **Vulkan FR/EN,
|
||||
GUI 2/3/4** passent : **58 s / 19 captures** pour les propriétés et **29 s /
|
||||
8 captures** pour Demeure.
|
||||
|
||||
Les tests de pixels vérifient les bords, coins et corps identiques sur des
|
||||
cellules adjacentes, la teinte seule des protections actives, les propriétaires
|
||||
hors ligne, le changement de pourcentage sans changement de couleur, le vide,
|
||||
le brouillard et le retrait d’une empreinte. Le masquage, le réaffichage,
|
||||
l’expiration, le clic droit, la composition dans Pause et l’accès Habitant sont
|
||||
couverts. Captures dans `build/map274-screenshots/` et
|
||||
`build/demeure274-screenshots/` ; inspections FR GUI 4, EN GUI 3, Pause et Habitant.
|
||||
|
||||
Le MRpack beta.274 est exporté et vérifié : ZIP, Minecraft 26.3, Loader 0.19.5
|
||||
et octets des deux JAR locaux identiques aux builds. Les archives beta.270 à
|
||||
beta.273 gardent leur empreinte. Reçu : `build/map274-validation.json`.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=claims273,claims272,property270,mining,overview271,menus,map,demeure \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryMap274ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
# Pour le second parcours, remplacer sanctuaryMap274ClientTests par sanctuaryDemeure274ClientTests.
|
||||
```
|
||||
|
||||
Export local uniquement. La proposition de claim issue de l’habitation, le
|
||||
premier occupant, les duels, l’accord de partage et un tarif moins élevé restent
|
||||
à définir dans le cadrage de reprise ; aucun n’est annoncé comme implémenté.
|
||||
@@ -0,0 +1,88 @@
|
||||
# LIGHT-288 — intégrer la correction des lumières portées
|
||||
|
||||
Branche `codex/dynamic-light-integration`, socle local beta.287 conservé.
|
||||
État : intégré, vérifié et construit en pack local beta.288.
|
||||
|
||||
## Résultat demandé
|
||||
|
||||
Intégrer au pack courant le correctif du commit
|
||||
`6f44214f2725b4ef2d1002606cb83e70885186ec`, branche `codex/dynamic-hand-light`.
|
||||
Sa version locale isolée était beta.283 ; la livraison combinée prend beta.288
|
||||
et conserve les zombies, leurs neuf tenues partagées, les bras natifs et le
|
||||
Hunter accroupi de beta.287.
|
||||
|
||||
Une torche ou une lanterne tenue réagissait dans les normales PBR sans éclairer
|
||||
le terrain avec Sodium. La collecte des sources était correcte, mais les copies
|
||||
`LevelSlice` utilisées par les workers Sodium ne recevaient pas l’instantané.
|
||||
|
||||
L’interface `DynamicLightContext` et deux mixins optionnels transmettent
|
||||
l’instantané immuable du contexte préparé côté rendu vers la copie du worker.
|
||||
Chaque nouveau travail remplace sa capture et `reset` la libère. Les lectures
|
||||
directes de lumière de bloc dans Sodium utilisent le même champ radial que
|
||||
le chemin natif, sans modifier le canal du ciel.
|
||||
|
||||
Les mixins ne sont sélectionnés que côté client avec Sodium présent. La
|
||||
version exacte reste `0.9.2+mc26.3`, facultative et active par défaut dans le
|
||||
pack ; aucune nouvelle dépendance. Les budgets, réglages et chemins natifs
|
||||
existants restent en vigueur. Aucun monde, identifiant, format de sauvegarde
|
||||
ou comportement serveur n’est modifié.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le test existant `HandLight283ClientChecks` est repris sans modification du
|
||||
commit source. Monde plat neuf, graine `283`, salle en pierre fermée et zone
|
||||
de pixels du sol sans interface ni objet en main visible. Deux parcours
|
||||
**Vulkan / MoltenVK 1.4.2, Apple M1** réussis :
|
||||
|
||||
| Rendu du pack combiné | Sol sans source | Sol avec torche |
|
||||
| --- | ---: | ---: |
|
||||
| Sodium `0.9.2+mc26.3` | 11,785 | 85,366 |
|
||||
| Natif, Sodium absent | 11,783 | 90,043 |
|
||||
|
||||
Ces moyennes de luminance sur 255 sont des mesures de pixels, pas des niveaux
|
||||
de lumière du serveur. Les deux mains, l’AO activée/désactivée, shaders/PBR
|
||||
coupés, profil Moyen sans/avec PBR, déplacement et torche jetée sont vérifiés.
|
||||
Après retrait, le sol retrouve sa luminosité initiale ; le moteur de lumière
|
||||
du monde reste à zéro. Avec Sodium, le worker retardé conserve son instantané,
|
||||
la réutilisation le remplace, le ciel reste inchangé et `reset` le libère.
|
||||
|
||||
Commandes des deux parcours, en sessions distinctes :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryHandLightClientTests=true -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientHeapGb=8 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le second parcours utilise `-PsanctuarySodiumClientTests=false`. Le parcours
|
||||
`Zombie287ClientChecks` réussit également avec Sodium activé, Vulkan, graine
|
||||
`287` : neuf tenues partagées pour les sept types, bébé classique, bras natifs,
|
||||
21 combinaisons de familiers et marche/bond du Hunter. Logs et captures sous
|
||||
`build/dynamic288/`.
|
||||
|
||||
`./gradlew check build assemblePack -PsanctuaryFocusedTests=lights,hunter286
|
||||
-PsanctuaryAtlasOnly=true` réussit en **2 min 42 s** : **19 GameTests requis**
|
||||
pour les lumières et le Hunter, test d’infrastructure compris, ainsi que tous
|
||||
les contrôles purs configurés par `check`.
|
||||
|
||||
L’export packwiz est vérifié : intégrité ZIP, version `beta.288`, Minecraft
|
||||
`26.3`, Fabric Loader `0.19.5`, un seul JAR Sanctuary identique au binaire
|
||||
construit. Par rapport au JAR beta.287, seules **cinq entrées** changent :
|
||||
la version Fabric, le plugin de sélection des mixins et les trois nouvelles
|
||||
classes du pont Sodium. Toutes les classes et ressources des zombies et
|
||||
familiers sont strictement identiques. Les tests et Sodium ne sont pas
|
||||
embarqués dans le JAR Sanctuary.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
- [Pack beta.288](../build/Sanctuary-beta.288.mrpack), 16 715 918 octets.
|
||||
SHA-256 : `51f70f5f00b0ccd293cf9be0dd34cf3e125c493d7929393428623668543c396b`.
|
||||
- JAR `mods/sanctuary/build/libs/sanctuary-beta.288.jar`.
|
||||
SHA-256 : `189883fad82476da221552fc56facb585f05f3167d0908a855f510625b932358`.
|
||||
- Reçu : `build/dynamic288/validation.json`, comparaison des archives et
|
||||
références des logs. Le MRpack beta.287 reste intact.
|
||||
|
||||
Aucune publication, synchronisation Prism ou ouverture de sauvegarde
|
||||
personnelle. La décroissance radiale et ses limites d’occultation restent
|
||||
celles de l’éclairage dynamique existant.
|
||||
@@ -0,0 +1,105 @@
|
||||
# WHALE-01 — baleine fantôme volante
|
||||
|
||||
Branche `codex/flying-ghost-whale`, préparation locale **beta.290** le
|
||||
26 octobre 2026. Demande : import direct de la baleine de Sanctuary 26.2,
|
||||
avec son IA et son œuf, deux baleines présentes au maximum sur le serveur.
|
||||
|
||||
## Import
|
||||
|
||||
Sources historiques : `26.2/itsalive`, commit
|
||||
`94644a28f05b8010f06ec1181fe6c74a50ad80d6`. Le modèle, ses animations,
|
||||
la texture nuageuse 512 × 256, les particules et les sons Minecraft sont
|
||||
repris. Les API Fabric/Minecraft sont adaptées à **Minecraft 26.3**, sans
|
||||
nouvelle dépendance. La texture de la baleine est copiée octet pour octet.
|
||||
|
||||
- `sanctuary:flying_whale` : baleine géante, paisible, volante, intangible,
|
||||
invulnérable, sans butin. Dimensions historiques 36 × 11, rendu ×2,7.
|
||||
- IA conservée : compagnon vivant non spectateur le plus proche à 520 blocs,
|
||||
cibles successives entre 68 et 140 blocs du joueur, altitude libre, virages
|
||||
progressifs, souffle et lente errance sans joueur.
|
||||
- Spawner historique : contrôle toutes les 400 ticks, une apparition réussie
|
||||
par tiers de journée (8 000 ticks), dans l’Overworld à 92–138 blocs d’un
|
||||
joueur. Il attend si le chunk candidat n’est pas déjà chargé. Aucun chunk
|
||||
n’est généré ou forcé par le spawner. Le calendrier appartient au serveur
|
||||
courant et repart au démarrage, comme le calendrier historique.
|
||||
- Dissipation au plus tard après 14 400 ticks (12 minutes à 20 TPS), possible
|
||||
dès 12 000 ticks après plus de 900 ticks sans joueur proche. Seules les
|
||||
baleines nommées sont sauvegardées, comme en 26.2 ; elles restent soumises
|
||||
à cette durée de vie après chargement.
|
||||
|
||||
Le quota historique de deux baleines chargées est partagé entre **toutes les
|
||||
dimensions du serveur**, et étendu aux œufs, invocations, distributeurs et
|
||||
rechargements. Une troisième entité chargée est retirée. L’utilisation
|
||||
manuelle d’un œuf est refusée avant consommation si les deux places sont
|
||||
prises, avec un message FR/EN. Retirer une baleine libère immédiatement sa
|
||||
place. Les baleines nommées dans des chunks déchargés ne réservent pas de
|
||||
place ; elles repassent par le quota au rechargement.
|
||||
|
||||
## Œuf Minecraft personnalisé
|
||||
|
||||
`sanctuary:flying_whale_spawn_egg` figure dans l’onglet Œufs d’apparition.
|
||||
Son icône est dérivée directement de **`assets/minecraft/textures/item/egg.png`
|
||||
de Minecraft 26.3**, sans reprendre le dessin historique de l’œuf.
|
||||
`scripts/generate_whale_egg.py` recolore les pixels en bleu nuage pâle avec
|
||||
huit pixels lavande. Dimensions 16 × 16, les 90 pixels de coquille,
|
||||
transparence, contour asymétrique et emplacements des ombres restent natifs.
|
||||
Le script refuse une version ou une empreinte source différente et propose
|
||||
`--check` pour comparer les pixels livrés sans réécriture.
|
||||
|
||||
```sh
|
||||
python3.14 scripts/generate_whale_egg.py --check /chemin/minecraft-client.jar
|
||||
/give @s sanctuary:flying_whale_spawn_egg
|
||||
/summon sanctuary:flying_whale ~ ~10 ~
|
||||
```
|
||||
|
||||
Libellés de l’entité, de l’œuf et du refus de quota en FR/EN. Aucun familier
|
||||
miniature, capture de baleine ou conversion d’ID 26.2 n’est ajouté. Pas de
|
||||
changement de générateur, de sauvegarde existante ou de publication distante.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `./gradlew check build assemblePack
|
||||
-PsanctuaryFocusedTests=whale290,recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 3 min 34 s**, avec **19 tests
|
||||
serveur requis réussis**, tous les contrôles purs de `check`, le JAR
|
||||
`sanctuary-beta.290.jar` et les profils packwiz client/serveur assemblés.
|
||||
Ce périmètre serveur cible la baleine, les recettes et la progression ;
|
||||
il ne rejoue pas les suites de génération historiques.
|
||||
- Archive JAR relue : version beta.290, classes et deux textures présentes,
|
||||
un seul JEI embarqué, aucune classe de test ; même JAR unique dans chacun
|
||||
des deux profils packwiz. Reçu SHA-256 sous `build/whale290/artifacts.json`.
|
||||
- Suite native ciblée `:sanctuary:runGameTest
|
||||
-PsanctuaryFocusedTests=whale290 -PsanctuaryAtlasOnly=true` : **7 tests
|
||||
requis réussis**, dont les six scénarios baleine et le contrôle
|
||||
d’infrastructure. Usage réel de l’œuf en survie, non-consommation à quota
|
||||
plein, quota Overworld/Nether, refus de la troisième invocation, libération
|
||||
d’une place, apparition naturelle à rayon/altitude historiques, déplacement
|
||||
natif, dissipation, collision sans modification du terrain et sauvegarde
|
||||
nommée vérifiés. Les tests attendent que les chunks de développement soient
|
||||
effectivement chargés avant de vérifier leur population.
|
||||
- Parcours graphique **Vulkan / MoltenVK 1.4.2 / Apple M1**, avec
|
||||
**Sodium 0.9.2+mc26.3**, réussi en **49 s** : deux baleines géantes
|
||||
translucides, nageoires animées, texture historique, véritable déplacement
|
||||
autour du joueur, œuf en main et hotbar, libellés FR/EN. Les captures
|
||||
`0000_whale290-native-egg-and-two-whales.png` et
|
||||
`0001_whale290-ghosts.png` sont inspectées visuellement.
|
||||
- `generate_whale_egg.py --check` : pixels livrés identiques au résultat
|
||||
déterministe, 90 pixels opaques et masque alpha strictement natif.
|
||||
PNG de la baleine identique octet pour octet à la référence 26.2 ;
|
||||
JSON des libellés FR/EN et modèles d’objet valides.
|
||||
|
||||
Commande du parcours graphique :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryWhale290ClientTests=true -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryFocusedTests=whale290 -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journaux et captures ignorés sous `build/whale290/`. Validation en monde
|
||||
plat de développement neuf, graine `290` pour le client ; aucun monde
|
||||
personnel ni serveur Beta n’est ouvert ou modifié. Le calendrier complet
|
||||
sur une journée réelle et un parcours multijoueur distant ne sont pas
|
||||
revendiqués ; les trois fenêtres sont exercées par les tests de calendrier.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Happy creepers — réintégration
|
||||
|
||||
Branche `codex/happy-creepers`, worktree isolé du chantier baleines.
|
||||
Version locale préparée : **beta.291**, sur le socle beta.289.
|
||||
|
||||
Cette fiche conserve le reçu de la branche dédiée. Son commit `0ffd371a` est
|
||||
repris dans la [livraison intégrée beta.293](release-beta293.md), avec les
|
||||
baleines, le logo packwiz et les quatre mods facultatifs. Consulter ce reçu
|
||||
pour l'état de publication et les vérifications de la combinaison finale.
|
||||
|
||||
## Contrat
|
||||
|
||||
Le happy creeper est un état du creeper Minecraft existant. **Maj + clic droit
|
||||
avec une fleur** équipe son unique emplacement cosmétique de tête ; **Maj +
|
||||
clic droit à main vide** rend cette même fleur. Le geste, les contrôles de
|
||||
distance/visibilité, la consommation en survie et la conservation des composants
|
||||
de l'objet sont ceux du système de chapeaux existant. Un emplacement occupé
|
||||
doit être vidé avant d'équiper une autre fleur.
|
||||
|
||||
Toute fleur du tag Minecraft `minecraft:flowers` rend le creeper heureux :
|
||||
visage souriant historique, fleur exacte visible sur la tête animée, aucun
|
||||
ciblage hostile ni explosion. L'équipement annule immédiatement une mèche
|
||||
déjà amorcée, y compris au briquet ; retirer la fleur ne reprend pas cette
|
||||
ancienne mèche. Tant que la fleur est portée, briquet et boule de feu ne
|
||||
l'allument pas et ne sont pas consommés ou usés. Le creeper continue d'utiliser
|
||||
ses déplacements natifs et peut toujours subir des dégâts.
|
||||
|
||||
Retirer la fleur ou la remplacer par un objet sans rapport rétablit le visage,
|
||||
le ciblage et l'explosion ordinaires. Un pot de fleurs, des feuilles ou des
|
||||
graines ne déclenchent pas cet état. La charge électrique est indépendante :
|
||||
un creeper chargé peut être heureux, et la foudre peut charger un happy
|
||||
creeper. Retirer sa fleur conserve sa charge.
|
||||
|
||||
## Sauvegarde et périmètre
|
||||
|
||||
L'état heureux se déduit uniquement de l'objet dans `sanctuary:mob_head`, dont
|
||||
le schéma 1 est réutilisé sans modification. Cette pile est déjà sauvegardée
|
||||
et synchronisée par les chapeaux de mobs. Aucun second drapeau, nouvelle
|
||||
entité, changement de format, migration de monde ou tirage de variante n'est
|
||||
ajouté. Un creeper portant déjà une fleur devient donc heureux avec cette
|
||||
version. L'identité, la santé, la charge et les autres données natives restent
|
||||
sur le même creeper. Le port d'un chapeau conserve sa protection habituelle
|
||||
contre la disparition à distance.
|
||||
|
||||
Les nouvelles apparences de creepers restent pour un prochain ticket. Le
|
||||
suivi du joueur, les apparitions naturelles de happy creepers et les anciennes
|
||||
entités `happy_creeper:*`/`mobopia:*` ne font pas partie de cet incrément.
|
||||
Aucune nouvelle interface n'est ajoutée ; les messages d'équipement et de
|
||||
retrait FR/EN existants sont réutilisés.
|
||||
|
||||
## Vérification
|
||||
|
||||
- Serveur `happycreeper,cosmetics,headblocks` : **28 tests requis réussis**,
|
||||
dont sept tests happy creeper et le test d'infrastructure Fabric. Annulation
|
||||
immédiate d'une vraie mèche à vingt ticks, refus répété du ciblage/allumage,
|
||||
représailles, tous les objets du tag fleurs, récupération de la pile nommée,
|
||||
allumage natif sans fleur et rechargement du même creeper chargé vérifiés.
|
||||
- Parcours client **Vulkan avec Sodium** : **réussi**. Vrais appuis sur Maj et
|
||||
clics souris pour équiper/récupérer une fleur en survie ; synchronisation de
|
||||
la pile et du visage ; AI active pendant 85 ticks près d'un vrai joueur ;
|
||||
sourire, fleur sur la tête animée et aura électrique native vérifiés.
|
||||
Monde plat de développement neuf, graine `290`, Minecraft 26.3 sur Apple M1.
|
||||
Capture : `build/happy-creepers-screenshots/four-states-vulkan.png` ; de gauche
|
||||
à droite : chargé, chargé heureux, heureux, ordinaire.
|
||||
- `check build assemblePack` : **réussi en 3 min 55 s**, contrôles purs et
|
||||
**28 tests serveur** inclus. Export MRpack réussi, versions et dépendances
|
||||
exactes contrôlées ; un seul JAR Sanctuary embarqué, identique au JAR
|
||||
construit ; texture historique identique octet par octet et classes de test
|
||||
absentes. Logs : `build/happy-creepers-final-check-build.log`,
|
||||
`build/happy-creepers-server.log`, `build/happy-creepers-vulkan.log`.
|
||||
Reçu : `build/happy-creepers-validation.json`.
|
||||
|
||||
Commandes reproductibles :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=happycreeper,cosmetics,headblocks \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryHappyCreeperClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryClientHeapGb=4 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le premier essai de `check build` associait par erreur le preset unifié et le
|
||||
profil rapide ; il a échoué au choix du générateur avant les tests. Le profil
|
||||
plat `sanctuaryAtlasOnly=true` évite ce conflit de configuration existant.
|
||||
Aucun monde personnel, serveur Beta, canal packwiz ou Prism n'est modifié.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.291](../build/Sanctuary-beta.291.mrpack), 16 724 142 octets.
|
||||
SHA-256 : `3c2d630fc85e88b08bac94cc2244e975d3552fcac4d508ad366adea730062468`.
|
||||
- JAR : `mods/sanctuary/build/libs/sanctuary-beta.291.jar`.
|
||||
SHA-256 : `e8e315db1e4831400244d17429723099fbfe3a352387d8a9cf0ecb01d8170e9e`.
|
||||
|
||||
Cette branche part de beta.289 et ne contient pas le travail parallèle des
|
||||
baleines préparé en beta.290. L'intégration des deux branches précède toute
|
||||
éventuelle publication commune. Aucun tag ni artefact distant n'est créé.
|
||||
@@ -0,0 +1,59 @@
|
||||
# HELLO-263 — Bébés familiers dans Hello World
|
||||
|
||||
Correction demandée le 16 octobre 2026 sur `codex/hello-world-menu`, après
|
||||
beta.262. Minecraft **26.3**, livraison locale **beta.263**.
|
||||
|
||||
La disposition est ensuite ajustée dans [HELLO-264](hello-layout-beta264.md) :
|
||||
colonne de la largeur de la hotbar et aperçus miniatures sur un sol commun.
|
||||
|
||||
Le familier est présenté dans sa forme bébé native, comme le proxy de rendu
|
||||
en jeu après `Mob.setBaby(true)`. Le choix concerne le modèle, les textures
|
||||
bébé et l'échelle d'âge ; le cadrage tient compte de la taille réduite.
|
||||
Les textures du chat et du loup sont explicitement remplacées par celles
|
||||
du chaton et du louveteau. Les espèces sans forme bébé native gardent leur
|
||||
apparence habituelle de familier miniature.
|
||||
Le chaton reprend la variante noire par défaut du proxy natif ; le poisson
|
||||
tropical reprend également son motif natif par défaut.
|
||||
|
||||
Le rendu reste utilisable avant la liaison des composants d'items et avant
|
||||
l'arrivée d'un monde client. Aucun changement des familiers remis, des règles
|
||||
serveur ou des sauvegardes. Hotbar de neuf couleurs, fond texturé, skin du
|
||||
personnage et suivi de la souris conservés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours Hello World existant est étendu : après le contrôle à froid des
|
||||
44 modèles/textures, l'âge, l'échelle d'âge et la texture de chaque aperçu
|
||||
sont comparés à un vrai proxy natif créé dans un monde de développement
|
||||
neuf, puis passé par `Mob.setBaby(true)`. Ces proxies ne sont jamais ajoutés
|
||||
au monde ou simulés. Le personnage, la pagination, les saisies, l'admission,
|
||||
la reconnexion et le code d'accès restent couverts par le même parcours.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryHello262ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=progression,starter,starter052 -PsanctuaryAtlasOnly=true
|
||||
```
|
||||
|
||||
Parcours client réussi en **1 min 26 s**, **Vulkan / MoltenVK 1.4.2 sur
|
||||
Apple M1**, avec 8 Gio maximum. Journal : `build/hello263-client-vulkan.log`,
|
||||
marqueurs `HELLO263_BABIES_PASS`, `HELLO262_MODELS_PASS` et
|
||||
`HELLO262_CLIENT_PASS`. Les 44 âges, échelles d'âge et textures correspondent
|
||||
aux proxies natifs ; la création, la reconnexion et le code d'accès passent.
|
||||
24 captures conservées dans `build/hello263-screenshots/` ; galerie à froid
|
||||
et poussin dans le menu inspectés.
|
||||
|
||||
`check build assemblePack` réussit en **3 min 4 s**, avec **13 GameTests
|
||||
serveur requis réussis**. Journal : `build/hello263-check-build-pack.log`.
|
||||
Le [pack local beta.263](../build/Sanctuary-beta.263.mrpack) est exporté et
|
||||
vérifié : intégrité ZIP, version, Minecraft 26.3, JAR identique au build et
|
||||
classes de test absentes. Face à beta.262, seule la classe de production
|
||||
`HelloFamiliarPreview` change. Reçu : `build/hello263-validation.json`.
|
||||
|
||||
Aucune publication ou installation personnelle effectuée. Les variantes
|
||||
visuelles restent celles par défaut ; Windows et serveur distant restent
|
||||
hors des essais.
|
||||
@@ -0,0 +1,74 @@
|
||||
# HELLO-264 — Colonne centrale et sol commun
|
||||
|
||||
Demande du 16 octobre 2026 sur `codex/hello-world-menu`, après beta.263.
|
||||
Minecraft **26.3**, livraison locale **beta.264**.
|
||||
|
||||
Le titre Hello World est retiré de l'affichage. Le menu central reprend les
|
||||
**182 pixels de la hotbar native**, avec neuf couleurs par page. La section
|
||||
Personnage contient le choix de couleur puis « Écris ta biographie ». Elle est
|
||||
suivie du choix parmi les trois œufs et du nom du familier. Les noms sont
|
||||
placés au-dessus des aperçus : pseudo coloré à gauche, nom saisi à droite.
|
||||
Les flèches restent dans la largeur de la colonne ; leur infobulle indique
|
||||
la page courante. Libellés français et anglais.
|
||||
|
||||
Les deux aperçus partagent la même hauteur de sol. Le familier garde sa forme
|
||||
bébé et une taille miniature ; son échelle ne cherche plus à remplir la zone.
|
||||
Le cadrage est limité à l'aperçu. Le personnage compense la rotation verticale
|
||||
pour garder ses pieds au sol. Les deux modèles suivent toujours la souris.
|
||||
L'espèce, la rareté et la description apparaissent uniquement au survol de
|
||||
l'œuf ou de l'aperçu, et ne prennent plus de place dans le formulaire.
|
||||
|
||||
Retour est placé en bas du contenu, séparé des sections. « Entrer dans
|
||||
Sanctuary » occupe un pied de page natif de 33 pixels, à la position du bouton
|
||||
Terminé des statistiques. Les deux boutons ont la largeur de la hotbar.
|
||||
Le fond légèrement texturé, les brouillons, la pagination, le verrouillage
|
||||
d'identité et le parcours de code d'accès sont conservés.
|
||||
|
||||
Les règles serveur, les sauvegardes et les familiers remis restent inchangés.
|
||||
Les variantes visuelles sont celles par défaut, comme dans beta.263.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours Vulkan vérifie FR/EN en GUI 2/3/4, la largeur et le centrage des
|
||||
champs, les boutons sans chevauchement et la place du pied de page natif.
|
||||
Il couvre aussi les neuf couleurs par page, la dernière page incomplète,
|
||||
les saisies après redimensionnement/refus serveur, l'identité verrouillée et
|
||||
le code d'accès. Les aides de saisie FR/EN sont raccourcies pour tenir dans
|
||||
la colonne, y compris lorsque les champs sont vides.
|
||||
|
||||
Les 44 modèles de départ sont rendus à froid et comparés aux vrais proxies
|
||||
natifs après `Mob.setBaby(true)`. Captures spécifiques du bébé zombie,
|
||||
du bébé chameau, du survol et des deux directions de souris. Admission avec
|
||||
nom, sauvegarde et reconnexion dans un monde de développement neuf, graine
|
||||
52 ; code d'accès avec réponse HTTP perdue, nouvel essai et admission unique
|
||||
dans un autre monde neuf, graine 166.
|
||||
|
||||
Parcours final réussi en **1 min 4 s**, sous **Vulkan / MoltenVK 1.4.2,
|
||||
Apple M1**, avec 8 Gio maximum. Les marqueurs `HELLO262_MODELS_PASS`,
|
||||
`HELLO263_BABIES_PASS`, `STARTER052_CLIENT_PASS`, `ACCESS166_CLIENT_PASS`
|
||||
et `HELLO262_CLIENT_PASS` figurent dans `build/hello264-client-vulkan.log`.
|
||||
**43 captures natives** conservées dans `build/hello264-screenshots/` ;
|
||||
menus FR/EN, zombie, chameau, suivi de souris, cadrage et infobulle inspectés.
|
||||
|
||||
`check build assemblePack` réussit en **3 min 9 s**, avec les **13 GameTests
|
||||
serveur requis réussis**. Journal : `build/hello264-check-build-pack.log`.
|
||||
Le [pack local beta.264](../build/Sanctuary-beta.264.mrpack) est exporté et
|
||||
vérifié : intégrité ZIP, version, Minecraft 26.3, JAR identique au build,
|
||||
classes de production identiques aux sorties compilées, traductions FR/EN
|
||||
identiques aux sources et classes de test absentes. Par rapport à beta.263,
|
||||
les seules classes de production modifiées sont `HelloWorldScreen` et ses
|
||||
classes internes, ainsi que `HelloFamiliarPreview`.
|
||||
Reçu : `build/hello264-validation.json`.
|
||||
|
||||
Aucune publication ou installation personnelle effectuée. Essais graphiques
|
||||
sur Apple M1 uniquement ; Windows et serveur distant hors des essais.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryHello262ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=progression,starter,starter052 -PsanctuaryAtlasOnly=true
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
# HELLO-262 — Menu de création du personnage et du familier
|
||||
|
||||
Demande du 16 octobre 2026, branche `codex/hello-world-menu`, socle beta.261,
|
||||
Minecraft **26.3**, livraison locale **beta.262**.
|
||||
|
||||
Les aperçus adultes de cette première version sont remplacés par les
|
||||
[formes bébé en beta.263](hello-baby-familiars-beta263.md).
|
||||
|
||||
## Menu
|
||||
|
||||
Trois colonnes centrées : personnage à gauche, formulaire au centre, familier
|
||||
à droite. Les colonnes latérales ont la même largeur ; le centre est partagé
|
||||
en deux blocs de même hauteur, personnage en haut et familier en bas.
|
||||
Aucun aplat de panneau : fond gris sombre légèrement texturé sur toute la page,
|
||||
avec la texture native de pierre teintée, proche du précédent `#171717`.
|
||||
|
||||
Le pseudo affiche la couleur choisie. Les flèches autour de lui parcourent
|
||||
**neuf couleurs serveur par page**. La barre reprend directement les sprites
|
||||
`minecraft:hud/hotbar` et `minecraft:hud/hotbar_selection`, sans texture dérivée.
|
||||
Aux petites tailles, elle se réduit proportionnellement tout en conservant ses
|
||||
neuf cases. La dernière page garde les emplacements vides inactifs. Noms des
|
||||
couleurs au survol et dans la narration ; libellés FR/EN.
|
||||
|
||||
Le personnage et le familier ont des **aperçus 3D qui suivent la souris**,
|
||||
sans devoir cliquer ou glisser. Le skin utilise le widget natif ; le familier
|
||||
utilise ses modèles et textures natifs. Les trois œufs restent les boutons de
|
||||
choix à droite. Le survol change l'aperçu ; sinon, le familier choisi est
|
||||
montré, ou le premier avant la sélection explicite.
|
||||
|
||||
Ces aperçus ne créent aucun `ItemStack`, entité ou monde de substitution.
|
||||
Les états graphiques sont construits directement avant l'admission. Les vaches,
|
||||
poules et cochons utilisent explicitement leurs variantes tempérées 26.3,
|
||||
sans lier ou remplacer les registres dynamiques du serveur.
|
||||
|
||||
La biographie et le nom du familier restent au centre. Le code d'accès apparaît
|
||||
lorsque le serveur l'exige. Pagination, redimensionnement et refus serveur
|
||||
conservent les saisies et le familier proposé ; une couleur devenue indisponible
|
||||
est retirée de la sélection. Une identité enregistrée reste verrouillée.
|
||||
Règles serveur, tirage, protocole et formats de sauvegarde inchangés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Parcours client **Vulkan / MoltenVK 1.4.2, Apple M1**, 8 Gio maximum : FR/EN,
|
||||
fenêtre 1280 × 960, GUI 2/3/4, soit 640 × 480, 427 × 320 et 320 × 240.
|
||||
Toutes les couleurs sont accessibles sans doublon, dernière page comprise.
|
||||
Contrôles sans chevauchement, boutons Annuler/Entrer lisibles sans défilement,
|
||||
focus des flèches conservé, brouillons et sélection conservés au redimensionnement.
|
||||
Offres sans familier, palette vide et identité verrouillée couvertes.
|
||||
|
||||
Les **44 modèles de départ** communs/peu communs sont soumis au rendu natif
|
||||
et leurs textures vérifiées sur un client à froid, **avant la liaison des
|
||||
composants d'items**. La galerie est rendue en Vulkan. Les déplacements du
|
||||
curseur vérifient les deux axes du familier et du personnage ; captures
|
||||
comparées pour contrôler le sens de rotation.
|
||||
|
||||
Admission avec nom et reconnexion dans un monde plat neuf, graine **52**.
|
||||
Admission par code dans un autre monde plat neuf, graine **166**, avec une API
|
||||
HTTP locale contrôlée : réponse perdue, nouvel essai et admission unique.
|
||||
Configuration temporaire restaurée. Les parcours Starter052 et Access166 sont
|
||||
réutilisés ; les assertions historiques de disposition de Starter052 et
|
||||
Progression003 sont adaptées.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryHello262ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=progression,starter,starter052 -PsanctuaryAtlasOnly=true
|
||||
```
|
||||
|
||||
Parcours client final réussi en **1 min 35 s**. Journal :
|
||||
`build/hello262-3d-client-vulkan-final.log`, marqueurs `HELLO262_MODELS_PASS`
|
||||
et `HELLO262_CLIENT_PASS`. **24 captures finales** conservées dans
|
||||
`build/hello262-final-screenshots/` ; vues FR/2, FR/4 avec accès, EN/4,
|
||||
galerie des 44 modèles et deux positions du curseur inspectées.
|
||||
|
||||
`check build assemblePack` réussit en **3 min 32 s**, avec **13 GameTests
|
||||
serveur requis réussis** et les contrôles purs du dépôt. Journal :
|
||||
`build/hello262-check-build-pack-final.log`. Mod et pack alignés sur beta.262.
|
||||
|
||||
[Pack local beta.262](../build/Sanctuary-beta.262.mrpack).
|
||||
Reçu et empreintes : `build/hello262-validation.json`.
|
||||
Le MRpack est vérifié après export : intégrité ZIP, Minecraft 26.3, version
|
||||
beta.262 et JAR Sanctuary identique au build ; classes de test absentes du mod.
|
||||
|
||||
## Limites
|
||||
|
||||
Aperçus avec les variantes visuelles par défaut, sans choix de variante.
|
||||
Aux petites tailles, la description latérale est abrégée par la place disponible ;
|
||||
l'infobulle garde le texte complet. La biographie conserve sa limite de
|
||||
256 caractères et son champ natif à défilement horizontal.
|
||||
Essais sur serveurs intégrés de développement ; Windows et serveur distant
|
||||
non testés. Aucune publication, modification du canal packwiz ou synchronisation
|
||||
d'une installation personnelle effectuée.
|
||||
@@ -0,0 +1,87 @@
|
||||
# ZOMBIES-286 — Hunter et bond offensif
|
||||
|
||||
Branche `codex/hunter`, sur le socle local beta.285 conservé. État : implémenté,
|
||||
vérifié côté serveur et sous Vulkan ; pack local beta.286 construit et vérifié.
|
||||
|
||||
## Contrat demandé
|
||||
|
||||
Sixième infecté : un Hunter qui prépare son bond, saute réellement vers sa
|
||||
cible et la blesse seulement si son corps la touche. Le bond peut rater : sa
|
||||
direction reste fixée après le départ et les blocs utilisent leurs collisions
|
||||
natives. Aucun plaquage ni dégât répété automatique n'est ajouté.
|
||||
|
||||
- Zombie adulte vanilla 26.3, t-shirt bleu soutenu et pantalon anthracite
|
||||
recolorés par script ; tête, peau, chaussures, UV et alpha conservés.
|
||||
Identifiants `sanctuary:hunter` et `sanctuary:hunter_spawn_egg`.
|
||||
Aucun bébé, y compris via œuf, chargement ou familier ; pas de noyé.
|
||||
- Sauvage : 10 PV, vitesse 0,30, mêlée 3, poursuite 36 blocs. Préparation
|
||||
de 16 ticks au sol, bond à 3–10 blocs avec ligne de vue, impulsion verticale
|
||||
0,62 et vitesse horizontale adaptée à la distance, plafonnée à 1,25.
|
||||
Une seule frappe de 4 dégâts au contact, récupération de 24 ticks,
|
||||
recharge de 120 ticks engagée au décollage, même si le bond rate.
|
||||
- Gravité, trajectoire et collisions natives ; corridor complet chargé et
|
||||
bordure vérifiés. Mur, plafond, atterrissage ou durée bornée terminent le bond.
|
||||
Une téléportation n'est pas interprétée comme un balayage d'attaque. Aucun
|
||||
bloc détruit, chunk généré ou monde existant réécrit.
|
||||
- Familier rare : adulte miniature à taille variable persistante, 12 PV,
|
||||
vitesse 0,30. Attaque de contact automatique et G de bond offensif à 8 blocs,
|
||||
avec trajectoire physique et récupération. Les protections communes des
|
||||
familiers gardent autorité sur les dégâts, destinataires, claims et duels.
|
||||
Le familier ne bondit pas lorsqu'il est monté ou transporté.
|
||||
- Œuf créatif, spawn rare dans les habitats de zombies de l'Overworld : poids 4,
|
||||
un individu. Recharge conservée au chargement ; préparation et saut interrompus
|
||||
ne reprennent pas après rechargement. Aucun identifiant ou format existant changé.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `:sanctuary:runGameTest -PsanctuaryFocusedTests=hunter286
|
||||
-PsanctuaryAtlasOnly=true` : **16 tests requis réussis**, dont le test
|
||||
d’infrastructure. Préparation accroupie, vraie élévation, contact unique,
|
||||
esquive, murs, plafond, atterrissage, recharge, sauvegarde interrompue,
|
||||
téléportation, corridor déchargé, G physique, protections communes et six
|
||||
tailles persistantes.
|
||||
- Régressions séparées : **77 tests** avec
|
||||
`ziplines242,boomer284,spitter283,charger282,infected280,familiar260,companions`,
|
||||
puis **19 tests** avec `smoker285`. Les joueurs simulés du Smoker imposent
|
||||
une session indépendante des tests d’IA asynchrones. Chaque total comprend
|
||||
son test d’infrastructure.
|
||||
- Deux arènes historiques corrigées sans changement de comportement : sol
|
||||
du Charger élargi pour garder l’esquive de quatre blocs sur la plateforme ;
|
||||
arènes du Smoker replacées dans les chunks suivis par GameTest pour que les
|
||||
recherches natives de projectiles voient immédiatement les joueurs.
|
||||
- `:sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryHunter286ClientTests=true -PsanctuaryQuickTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8
|
||||
-PsanctuaryClientNoVsync=true` : **réussi**, Vulkan / MoltenVK 1.4.2 /
|
||||
Apple M1. Monde de développement plat temporaire, graine `286` : appui G
|
||||
réel, familier en vol, contact et recharge ; bond sauvage sur le vrai joueur,
|
||||
dégâts, récupération et gravité ; esquive au clavier sans dégâts ; rendu
|
||||
adulte aux tailles 500/1000/1500 ; interface et infobulle FR/EN, GUI 2/3/4.
|
||||
Captures : `build/hunter286-screenshots/`.
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=hunter286
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 2 min 46 s**, y compris les 16
|
||||
tests Hunter rejoués et tous les contrôles purs configurés par `check`.
|
||||
- Export `packwiz modrinth export` et intégrité ZIP vérifiés : un seul JAR
|
||||
Sanctuary, identique au JAR construit ; version `beta.286`, Minecraft `26.3`,
|
||||
Fabric Loader `0.19.5`. Classes de test exclues ; classes Hunter, œuf,
|
||||
profil, butin, tags et traductions FR/EN présents. Pixels du PNG conformes
|
||||
au script, cinq textures antérieures et MRpack beta.285 inchangés.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.286](../build/Sanctuary-beta.286.mrpack), 16 682 455 octets.
|
||||
SHA-256 : `9363b9067bc9974cd5f850b671e0d97c1f0876eda6885a51e2acf32d6d3b1103`.
|
||||
- JAR `mods/sanctuary/build/libs/sanctuary-beta.286.jar`.
|
||||
SHA-256 : `02a62837d2385ec61cf76c915c2eb0e0cc5b339d5bd3ceab64a2f1086bc94abf`.
|
||||
- Reçu détaillé : `build/hunter286-validation.json`. Logs et captures ignorés
|
||||
sous `build/hunter286-*`. Les premiers essais échoués des arènes historiques
|
||||
sont conservés à côté des résultats finaux ; les réparations ci-dessus ne
|
||||
changent aucun comportement du Charger ou du Smoker.
|
||||
|
||||
## Limites
|
||||
|
||||
Le Hunter saute puis retombe : aucun plaquage ou maintien du joueur. La vitesse
|
||||
de lancement adapte la portée au sol ; un dénivelé, un obstacle ou le déplacement
|
||||
de la cible peut faire rater le contact. Seuls les chunks déjà chargés sont
|
||||
utilisés. Aucune validation graphique OpenGL, publication, synchronisation
|
||||
Prism, installation personnelle ou modification de sauvegarde existante.
|
||||
@@ -0,0 +1,76 @@
|
||||
# ZOMBIES-281 — Infectés adultes et familiers miniatures
|
||||
|
||||
Branche `codex/special-zombies`, suite au retour sur beta.280. État : livré et
|
||||
vérifié localement.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les infectés n'ont **aucune forme bébé**. Leur modèle et leur texture sont
|
||||
toujours ceux de l'adulte. Cette règle appartient à `AbstractInfectedEntity`,
|
||||
le socle commun des variantes : les futurs Hunter, Charger, Spitter et Boomer
|
||||
devront aussi en hériter. Ces variantes restent à implémenter.
|
||||
|
||||
L'Infecté actuel refuse le flag bébé des groupes naturels, des appels natifs,
|
||||
des œufs et du chargement des données d'entité. Il ne reçoit pas le bonus de
|
||||
vitesse bébé. Les bébés zombies Minecraft gardent leurs règles habituelles.
|
||||
|
||||
En familier, l'adulte est réduit avec le système `FamiliarSize` existant.
|
||||
Son trait de taille enregistré dans l'œuf reste identique : petites, ordinaires,
|
||||
grandes et colossales restent possibles. Le rendu en jeu, les aperçus des menus,
|
||||
les dimensions de monture et les sons utilisent tous la même forme adulte.
|
||||
La taille ne dépend donc plus d'un âge bébé.
|
||||
|
||||
Les identifiants, la texture vanilla recolorée, les règles de combat et les
|
||||
formats restent ceux de beta.280. Une ancienne donnée d'Infecté portant
|
||||
`IsBaby=true` est interprétée comme adulte à la lecture, sans changement de
|
||||
son UUID ou de sa santé. Aucun monde personnel n'est ouvert ou réécrit.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Refus du bébé et de son bonus de vitesse, avec bébé zombie vanilla conservé.
|
||||
- Lecture d'un ancien flag bébé sans perdre l'identité ni la santé.
|
||||
- Familier adulte à six tailles, de 120 à 6 000 ‰, sans changer le trait ni
|
||||
l'identité ; cohérence entre hauteur visible et hauteur de monture.
|
||||
- Parcours Vulkan sur un monde plat neuf, graine `281` : trois tailles réelles
|
||||
de familier, inspection de la géométrie adulte et menus FR/EN GUI 2/3/4.
|
||||
- `./gradlew check build assemblePack` et vérification du MRpack beta.281.
|
||||
|
||||
La suite de régression conserve son entrée `infected280` et son option client
|
||||
`sanctuaryInfected280ClientTests`, actualisées selon ce nouveau contrat.
|
||||
|
||||
Les **34 GameTests serveur** ciblés passent, dont neuf pour l'Infecté. Les
|
||||
contrôles de réapparition et des 88 familiers vanilla passent aussi. Le parcours
|
||||
client **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en **54 secondes**.
|
||||
Il vérifie le vrai rendu du familier, sa géométrie adulte et sa hauteur pour
|
||||
500, 1 000 et 1 500 ‰, ainsi que les noms, infobulles et menus FR/EN aux échelles
|
||||
2, 3 et 4. Dix captures sont conservées sous `build/infected281-screenshots/` ;
|
||||
les petites et grandes miniatures et le menu FR GUI 2 ont été inspectés.
|
||||
|
||||
Logs : `build/infected281-native.log`, `build/infected281-client-vulkan.log`
|
||||
(marqueur `INFECTED281_CLIENT_PASS`). La construction complète finale
|
||||
**`check build assemblePack` passe en 3 min 09 s**, avec les 34 tests serveur.
|
||||
Log : `build/infected281-check-build-pack.log`.
|
||||
|
||||
Le JAR et le MRpack beta.281 sont vérifiés : version exacte, intégrité ZIP,
|
||||
un seul JAR Sanctuary identique au build, socle adulte commun présent,
|
||||
texture corrigée conservée octet pour octet et fixtures de test exclues.
|
||||
Reçu : `build/infected281-validation.json`.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=infected280,familiar260,companions \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryInfected280ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
## Livraison locale
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.281.jar`
|
||||
- `build/Sanctuary-beta.281.mrpack`
|
||||
- `build/infected281-screenshots/0002_infected281-miniature-1000.png`
|
||||
|
||||
Aucun déploiement dans Prism ou un serveur personnel. Les artefacts beta.280
|
||||
sont conservés ; cette livraison utilise le numéro suivant.
|
||||
@@ -0,0 +1,135 @@
|
||||
# ZOMBIES-280 — Premier infecté
|
||||
|
||||
Le contrat bébé de cette livraison est remplacé par
|
||||
[beta.281 : adultes en miniature et taille variable](infected-adults-beta281.md).
|
||||
|
||||
Branche `codex/special-zombies`. Premier incrément demandé : l'Infecté simple,
|
||||
avant les Hunter, Charger, Spitter et Boomer. État : livré localement et vérifié.
|
||||
|
||||
## Contrat
|
||||
|
||||
- `sanctuary:infected` est un zombie vert au modèle vanilla, t-shirt rouge et
|
||||
pantalon brun sombre. Adulte et bébé utilisent les formes natives.
|
||||
- Attributs de base : 10 PV, vitesse 0,25 contre 0,23 pour le zombie ; mêlée
|
||||
native, dégâts de base 3, portée de poursuite 36 blocs. Il brûle au soleil
|
||||
et ne se transforme pas en noyé. Aucun pouvoir spécial dans ce premier lot.
|
||||
- Apparition au sol dans l'obscurité, selon les règles des monstres, uniquement
|
||||
dans les biomes de l'Overworld qui proposent déjà le zombie : poids 15,
|
||||
groupes de 1 à 3. Aucun spawner ni remplacement des zombies existants.
|
||||
- `sanctuary:infected_spawn_egg` apparaît dans l'onglet des œufs créatifs et
|
||||
invoque uniquement l'Infecté. Il peut être équipé comme familier bébé avec
|
||||
identité, suivi, ordres, santé et combat persistants du système existant.
|
||||
- Le familier a 10 PV, vitesse 0,30, morsure de mêlée et assaut court. Son
|
||||
profil FR/EN est distinct du zombie. Aucune nouvelle aptitude de travail.
|
||||
- Les autres infectés et le zombie des glaces restent à réaliser. Aucune
|
||||
nouvelle voie d'acquisition des œufs en survie n'est ajoutée dans ce lot.
|
||||
|
||||
## Référence et compatibilité
|
||||
|
||||
Référence en lecture seule : `../26.2/itsalive`, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, `InfectedVariant.INFECTED` et
|
||||
`InfectedEntity`. Le nouveau type est indépendant des cinq variantes sous
|
||||
`itsalive:infected` de l'ancien mod ; aucune importation de leur sauvegarde.
|
||||
Les identifiants, profils et données des familiers existants sont conservés.
|
||||
L'ajout de tables de spawn affecte les apparitions futures, sans changer le
|
||||
terrain, les graines, les chunks ou un format de sauvegarde.
|
||||
|
||||
La texture est une recoloration déterministe du zombie natif **26.3**, par
|
||||
masques UV et sélection des seuls pixels cyan du t-shirt et violets du pantalon.
|
||||
Le visage, la peau verte, les chaussures et l'alpha sont conservés pixel pour
|
||||
pixel. L'œuf reprend l'icône rouge historique. La première texture générée par
|
||||
ImageGen décalait l'atlas ; elle a été écartée après le retour du créateur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Les **33 GameTests serveur** ciblés passent, dont huit nouveaux : attributs,
|
||||
mêlée réelle et bébé ; utilisation de l'œuf en survie et consommation ;
|
||||
sauvegarde/relecture native avec UUID, âge et santé ; tables de spawn ;
|
||||
immersion sans conversion en noyé ; équipement et rééquipement du familier ;
|
||||
morsure réelle du familier ; changement d'œuf pendant un objectif en cours.
|
||||
Les suites de pouvoirs existants et de réapparition
|
||||
beta.260 passent avec le nouvel œuf installé.
|
||||
Log final : `build/infected280-check-build-pack.log`.
|
||||
Le serveur plat natif 26.3 utilise le désert ; le contrôle positif des tables
|
||||
porte sur cet habitat réellement générable, avec exclusions Champignons,
|
||||
Nether et End. Il ne mesure pas la fréquence des rencontres en partie longue.
|
||||
|
||||
Le parcours client final **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en 54 s :
|
||||
apparences adulte et bébé face au zombie classique, familier réellement équipé,
|
||||
renderer partagé avec les proxies, texture embarquée, rareté et infobulle
|
||||
native, noms et menus **FR/EN GUI 2/3/4**. Sept captures sont conservées dans
|
||||
`build/infected280-screenshots/`. Les vues comparatives, FR GUI 4 et EN GUI 2
|
||||
ont été inspectées après correction de la texture. La vue comparative finale
|
||||
confirme le visage et les bras vanilla, le t-shirt rouge et le pantalon brun
|
||||
sans décalage. Log : `build/infected280-client-vulkan.log`, marqueur
|
||||
`INFECTED280_CLIENT_PASS`. Aucun essai OpenGL.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=infected280,familiar260,companions \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryInfected280ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le mode plat explicite évite la fixture historique de génération, sans modifier
|
||||
le générateur de production. Tous les mondes utilisés sont jetables et ignorés.
|
||||
La construction complète finale **`check build assemblePack` passe en 2 min 59 s**,
|
||||
avec la texture corrigée et les 33 tests serveur. Les contrôles purs du dépôt
|
||||
passent également. Le MRpack est exporté et vérifié : intégrité ZIP,
|
||||
Minecraft 26.3, Fabric Loader 0.19.5, un seul JAR Sanctuary beta.280 identique
|
||||
au build et texture corrigée présente octet pour octet. Les fixtures de test
|
||||
sont exclues du JAR. Reçu : `build/infected280-validation.json`.
|
||||
|
||||
Le second parcours a également reproduit un plantage lors du remplacement
|
||||
d'un œuf : l'ancien objectif d'IA pouvait continuer un tick avant la création
|
||||
du nouvel individu. Le cerveau vérifie désormais que le familier est actif
|
||||
et que son œuf correspond encore à celui équipé avant d'agir. Un huitième
|
||||
GameTest couvre cet intervalle ; le parcours client final inclut le correctif.
|
||||
|
||||
## Essai local
|
||||
|
||||
Dans un monde de développement avec commandes autorisées :
|
||||
|
||||
```mcfunction
|
||||
/give @s sanctuary:infected_spawn_egg
|
||||
/summon sanctuary:infected
|
||||
```
|
||||
|
||||
L'œuf placé dans l'emplacement Familier invoque sa forme bébé. Les commandes
|
||||
Alt bref / Alt maintenu et la technique G restent celles des autres familiers.
|
||||
L'œuf est fourni en créatif ; aucune nouvelle acquisition en survie n'est
|
||||
annoncée. Aucun canal packwiz, serveur personnel ou instance Prism modifié.
|
||||
|
||||
## Ressources graphiques
|
||||
|
||||
- Texture livrée : `mods/sanctuary/src/main/resources/assets/sanctuary/textures/entity/infected.png`.
|
||||
- Œuf historique : `mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/infected_spawn_egg.png`.
|
||||
- Outil de texture : `scripts/generate_infected_texture.py`, Python standard et
|
||||
ImageMagick 7 pour lire/écrire le PNG. **Aucun dessin génératif ni changement
|
||||
de géométrie.** 331 pixels de t-shirt et 239 pixels de pantalon recolorés,
|
||||
nuances natives conservées ; visage et transparence vérifiés avant encodage,
|
||||
puis tous les pixels vérifiés après écriture.
|
||||
- SHA-256 du PNG source :
|
||||
`1b88eea9b63889c2c3399773f887af375defc3aad0e7a375adc84249da54cbbd`.
|
||||
- SHA-256 de la texture livrée :
|
||||
`398898aed83ec22ef525b60da120fc3e0b971b33ad3640c73104b952ad060e29`.
|
||||
|
||||
Reproduction sans modifier le JAR source :
|
||||
|
||||
```sh
|
||||
python3 scripts/generate_infected_texture.py /chemin/vers/minecraft-26.3-client.jar
|
||||
```
|
||||
|
||||
Voir aussi la provenance dans `THIRD_PARTY_NOTICES.md`.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- Mod : `mods/sanctuary/build/libs/sanctuary-beta.280.jar`.
|
||||
- Pack : `build/Sanctuary-beta.280.mrpack`.
|
||||
- Captures corrigées : `build/infected280-screenshots/`.
|
||||
|
||||
La livraison est locale. La publication et la mise à jour de l'instance Prism
|
||||
restent distinctes de ce travail de développement.
|
||||
@@ -0,0 +1,116 @@
|
||||
# MENU-271 — Habitant en deux colonnes et deux rangées
|
||||
|
||||
Demande du 24 octobre 2026, continuation de `codex/inhabitant-property`.
|
||||
Version locale `beta.271`, après le premier lot de propriété beta.270.
|
||||
|
||||
## Résultat demandé
|
||||
|
||||
- Pause retrouve **Combats / Battles** dans **Communauté**, sous Habitant.
|
||||
Story et Factions ne figurent pas dans ce menu.
|
||||
- Habitant présente une barre horizontale **Propriété — Factions — Familier**
|
||||
en haut, sur les mêmes boutons natifs que les catégories de Découvertes.
|
||||
- Le personnage et son nom occupent la cellule supérieure gauche, avec
|
||||
des marges autour de la silhouette. La biographie est en haut à droite.
|
||||
- Sous la biographie, un tableau de bord présente le temps joué, les blocs
|
||||
posés, les blocs cassés, les créatures tuées, les morts et les progrès obtenus.
|
||||
Son bouton Statistiques ouvre la page générale complète existante.
|
||||
- Sous le personnage, l’activité occupe tout le panneau inférieur gauche,
|
||||
jusqu’en bas du contenu. La liste affiche les dernières activités disponibles
|
||||
dans son propre défilement, et **Historique reste dans son pied de panneau**.
|
||||
Il ouvre les 24 dernières activités disponibles ; l’ancien journal des cycles
|
||||
et achats reste accessible dans cette page.
|
||||
- Aucun aperçu, nom ou réglage du familier ne figure dans Habitant. Son bouton
|
||||
ouvre la page dédiée avec l'aide alliée, le carnet, les choix en attente et
|
||||
les contrôles opérateur admissibles, en plus des commandes déjà présentes.
|
||||
- Propriété, Factions, Familier, Statistiques et Historique reviennent à
|
||||
Habitant ; Combats revient à Pause. Le pied de page natif est conservé.
|
||||
|
||||
Les textes FR/EN, nombres compacts et infobulles gardent les valeurs et titres
|
||||
complets accessibles dans les petits formats. Biographie, statistiques et
|
||||
activité défilent indépendamment, et conservent leur position à la mise à jour
|
||||
des données. Les hauteurs s’adaptent aux échelles GUI 2, 3 et 4 ; la silhouette
|
||||
garde de l’air autour d’elle dans la cellule supérieure gauche.
|
||||
|
||||
## Sources et contrat de données
|
||||
|
||||
L'aperçu est une **lecture personnelle côté serveur**, via l'acteur authentifié
|
||||
du protocole communautaire existant. Aucun UUID du joueur à consulter n'est
|
||||
accepté depuis le client. Les opérations de fichier ou JDBC restent dans le
|
||||
worker communautaire ; les données Minecraft sont copiées sur le thread serveur.
|
||||
|
||||
- Temps, minage, morts et créatures tuées : compteurs Minecraft existants.
|
||||
Le temps est celui des ticks de jeu Minecraft. Le minage suit les règles de
|
||||
comptage vanilla ; les destructions en Créatif ne sont pas inventées.
|
||||
- Pose : sommes des poses confirmées du Blocodex, y compris les identifiants
|
||||
sauvegardés d'un mod absent. Les usages d'objets ne sont pas assimilés à des
|
||||
poses. Une source endommagée est signalée indisponible, jamais remise à zéro.
|
||||
- Progrès : accomplissements natifs ayant un affichage public ; les recettes
|
||||
et critères incomplets ne sont pas présentés comme des achievements.
|
||||
La date est le moment où toutes les exigences deviennent satisfaites :
|
||||
un critère optionnel obtenu plus tard ne déplace pas cette date.
|
||||
- Activité : publications, annonces, réponses et bulletins personnels visibles,
|
||||
progrès achevés et achats/cycles/factions déjà datés dans les sauvegardes.
|
||||
Le contenu intégral des publications et leurs photos ne sont pas transmis.
|
||||
Les publications modérées et les réponses à un parent masqué sont exclues.
|
||||
- Tri décroissant par date avec identifiants stables pour les égalités,
|
||||
dédoublonnage et limite de 24. Les anciennes données sont lisibles sans
|
||||
journal supplémentaire. Une panne communautaire conserve l'aperçu natif
|
||||
et signale que les publications manquent temporairement.
|
||||
|
||||
Ce panneau n'enregistre pas le texte du chat, des messages privés ou toutes
|
||||
les actions du monde. Il réunit les événements déjà persistants et datés ci-dessus.
|
||||
Aucun format de sauvegarde, coût, droit, règle de jeu ou monde personnel ne
|
||||
change. Aucun familier n'est retiré de l'inventaire ni rappelé en ouvrant Habitant.
|
||||
Les artefacts beta.270 déjà exportés sont conservés.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`check build assemblePack` réussit en **2 min 37 s**, avec **15 GameTests
|
||||
serveur ciblés**. Le parcours Vulkan FR/EN aux échelles GUI 2, 3 et 4 réussit
|
||||
en **45 s** et utilise un nouveau monde
|
||||
plat de graine 271. Il vérifie la barre supérieure, les deux colonnes et deux
|
||||
rangées, les marges du personnage, le panneau d’activité jusqu’en bas et son
|
||||
bouton Historique, la biographie et les statistiques à droite, les défilements
|
||||
et la conservation de leur position après rafraîchissement.
|
||||
|
||||
Des compteurs serveur connus (une heure de ticks ajoutée, deux poses et
|
||||
25 blocs minés), un progrès natif et une annonce réellement publiée dans le
|
||||
monde de développement sont relus par le réseau. Le parcours conserve aussi
|
||||
les retours Propriété/Factions/Familier/Historique, la modification du consentement
|
||||
à l’aide alliée et l’achat d’une extension de propriété.
|
||||
|
||||
Les tests serveur ciblés couvrent les données personnelles, les dates des
|
||||
critères OR, les statistiques sans double comptage, les publications masquées,
|
||||
les sources persistantes lues sans modification, les pannes communautaires et
|
||||
le dédoublonnage. Le test de fichier communautaire couvre l’isolation des auteurs,
|
||||
les réponses sous un parent masqué, l’ordre et la limite de 24.
|
||||
La requête MariaDB reste paramétrée et bornée ; aucun serveur SQL réel n’a été
|
||||
exercé pour cette livraison.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=overview271,property270,progression,menus \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryInhabitant271ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Une exécution exploratoire de la suite supplémentaire `companions` a échoué
|
||||
sur `creatureProfilesActuallyMove` : « Native navigation produces an actual
|
||||
airborne displacement ». Les règles de déplacement ne sont pas modifiées ici ;
|
||||
ce comportement n’est pas validé ni corrigé par ce lot d’interface. Le contrôle
|
||||
Vulkan du bouton Familier et de son consentement est distinct de cet essai.
|
||||
Résumé conservé dans `build/inhabitant271-exploratory-navigation.txt`.
|
||||
|
||||
Journaux locaux ignorés : `build/inhabitant271-client-vulkan.log` et
|
||||
`build/inhabitant271-check-build-pack.log`. Les **32 captures** sont conservées
|
||||
dans `build/inhabitant271-screenshots/`. Les fiches Habitant FR/EN, les formats
|
||||
GUI 2/4 et la page d’activité ont été inspectés.
|
||||
|
||||
Export local vérifié : [Sanctuary-beta.271.mrpack](../build/Sanctuary-beta.271.mrpack),
|
||||
16 483 048 octets. Intégrité ZIP, versions Minecraft/Fabric, dépendances et JAR
|
||||
Sanctuary/JEI comparés aux builds. Le MRpack beta.270 reste identique par SHA-256.
|
||||
Reçu de validation : `build/inhabitant271-validation.json`.
|
||||
Aucune publication de canal ni synchronisation d’instance personnelle.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Prochaine passe — mods du pack et modules personnels
|
||||
|
||||
Cadrage du **26 octobre 2026**, à la demande de Koka. Initialement préparés
|
||||
pour après beta.291, les quatre tickets d'intégration sont désormais repris
|
||||
dans [la préparation locale beta.292](optional-mods-beta292.md), à sa demande.
|
||||
Dozei Stream et Only Fun restent des contrats futurs. Chaque implémentation
|
||||
prendra sa propre branche `codex/<sujet>` et son prochain compteur libre.
|
||||
Les versions du cadrage ci-dessous sont des candidats repérés pour **Minecraft 26.3,
|
||||
Fabric 0.19.5 et Java 25**, pas des intégrations validées. Tous les essais
|
||||
graphiques cibleront Vulkan, avec Sodium puis sans si le mod le permet.
|
||||
|
||||
L'API Modrinth a été interrogée avec les filtres exacts `game_versions=["26.3"]`
|
||||
et `loaders=["fabric"]`. Les JAR candidats ont été téléchargés dans le dossier
|
||||
ignoré `build/optional-mods291/`, vérifiés contre leur SHA-512 publié et leurs
|
||||
`fabric.mod.json` relus. Revalider versions, dépendances, licences et hashes au
|
||||
début de chaque ticket ; aucun binaire téléchargé n'entre dans Git.
|
||||
|
||||
| Ticket | Branche prévue | Candidat observé | Résultat à obtenir |
|
||||
| --- | --- | --- | --- |
|
||||
| PACK-VOICE-01 | `codex/simple-voice-chat` | [2.6.26+26.3, Fabric, bêta](https://modrinth.com/plugin/simple-voice-chat/version/cZm965sW) | Conversation de proximité client/serveur, compatible avec l'acoustique Sanctuary |
|
||||
| PACK-BOBBY-01 | `codex/bobby-optional` | [5.2.16+mc26.3](https://modrinth.com/mod/bobby/version/J7YnfCss) | Voir les chunks déjà reçus au-delà de la distance envoyée par le serveur, installation facultative |
|
||||
| PACK-LITHIUM-01 | `codex/lithium-integration` | [0.26.2+mc26.3, Fabric](https://modrinth.com/mod/lithium/version/xS0Q8LSi) | Réduire le coût de simulation en conservant les règles Sanctuary |
|
||||
| PACK-CULL-01 | `codex/entity-culling` | [1.11.3 pour 26.3, Fabric](https://modrinth.com/mod/entityculling/version/X1g3VhTx) | Éviter le rendu des entités et blocs entités masqués, sans disparitions incorrectes |
|
||||
| MODULE-DOZEI-01 | `codex/dozei-stream-contract` | Pas encore réalisé, aucun artefact fourni | Définir le premier résultat vérifiable de Dozei Stream |
|
||||
| MODULE-ONLYFUN-01 | `codex/only-fun-contract` | Mod personnel futur | Définir une première interaction potache autonome |
|
||||
|
||||
## PACK-VOICE-01 — Simple Voice Chat
|
||||
|
||||
Le JAR 2.6.26 déclare `minecraft=26.3.x`, Java ≥25, Loader ≥0.19.3 ;
|
||||
Fabric API doit être ≥0.160.5+26.3, ce qui correspond au socle actuel.
|
||||
**Conflit constaté : il déclare `breaks.sound_physics_remastered=<1.5.2`,
|
||||
alors que Sanctuary distribue 1.5.1+26.3.** Le candidat 2.6.25 a le même refus.
|
||||
Le JAR [2.6.24+26.3](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy)
|
||||
ne déclare pas ce conflit ; cela ne prouve pas sa compatibilité en partie.
|
||||
|
||||
Décider et tester soit une mise à jour délibérée de Sound Physics et de son
|
||||
adaptateur natif Sanctuary, soit une version vocale antérieure compatible.
|
||||
Ne pas contourner les dépendances de Fabric. Conserver un artefact autonome
|
||||
provenant de l'auteur et ses conditions de distribution.
|
||||
|
||||
Acceptation : connexion de deux clients de développement, voix de proximité,
|
||||
muet, push-to-talk, déconnexion/reconnexion, coexistence Sound Physics et
|
||||
préférences conservées. Vérifier les profils packwiz client/serveur, le refus
|
||||
des versions incompatibles et les interfaces FR/EN ajoutées par Sanctuary.
|
||||
L'activation réseau de la Beta sera une étape de livraison distincte : le
|
||||
[protocole amont](https://dev.modrepo.de/minecraft/voicechat/wiki/server_setup)
|
||||
utilise UDP et un port vocal propre, par défaut 24454. Tester depuis l'extérieur
|
||||
avec `/voicechat test`, puis un échange audible ; l'ouverture du port Minecraft
|
||||
TCP ne suffit pas. Aucun port ni serveur personnel n'est modifié par ce ticket
|
||||
documentaire.
|
||||
|
||||
## PACK-BOBBY-01 — cache client facultatif
|
||||
|
||||
Le candidat déclare un environnement client, Minecraft `~26.3`, Loader
|
||||
≥0.18.4 et refuse Sodium <0.6.0 ; Sodium 0.9.2 du pack satisfait ce seul
|
||||
contrat déclaré. Cloth Config et Mod Menu sont suggérés, pas requis dans le
|
||||
JAR inspecté. Le [principe amont](https://github.com/Johni0702/bobby) est de
|
||||
mettre en cache les chunks envoyés dans `.bobby`, puis d'afficher leur dernière
|
||||
copie connue à distance. Ce cache ne simule pas les chunks et ne remplace pas
|
||||
leur état serveur actuel.
|
||||
|
||||
Acceptation : choix packwiz **facultatif**, installation/désinstallation par
|
||||
le sélecteur et préférence conservée au second lancement ; parcours puis
|
||||
retour sur un serveur de développement avec distance d'envoi courte ; chunks
|
||||
lointains visibles sous Vulkan, actualisation lors de leur réception réelle,
|
||||
claims/Atlas et blocs Sanctuary cohérents au retour. Aucun monde personnel
|
||||
utilisé comme `bobby-fallback`. Définir une valeur initiale de distance et une
|
||||
politique de cache mesurées avant livraison ; pas de génération supplémentaire
|
||||
imposée au serveur.
|
||||
|
||||
## PACK-LITHIUM-01 — simulation
|
||||
|
||||
Le [candidat](https://github.com/CaffeineMC/lithium) peut fonctionner côté client
|
||||
et serveur. Ses dépendances déclarées sont Minecraft `~26.3`, Loader ≥0.19.5
|
||||
et MixinExtras ≥0.5.5. Vérifier la version réellement fournie par Loader ; ne
|
||||
pas ajouter une bibliothèque par supposition. Définir séparément les profils
|
||||
serveur et client, sans faire dépendre l'autorité serveur du choix d'un joueur.
|
||||
|
||||
Acceptation : démarrage et rechargement d'un monde de test, recettes et
|
||||
inventaires, IA des infectés/familiers/baleines, interactions multiblocs et
|
||||
budget de ticks comparés avant/après sur le même scénario. Les gains doivent
|
||||
être mesurés ; conserver les identifiants, sauvegardes et règles serveur.
|
||||
|
||||
## PACK-CULL-01 — masquage visuel
|
||||
|
||||
Le [candidat](https://github.com/tr7zw/EntityCulling) cible Minecraft `~26.3`
|
||||
et requiert Fabric API ; TRansition et TRender sont déjà imbriqués dans le
|
||||
JAR inspecté. Vérifier leurs hooks de rendu sur Vulkan avant toute inclusion,
|
||||
ainsi que les conditions de distribution du binaire original.
|
||||
|
||||
Acceptation : comparaison activé/désactivé sur une scène de développement,
|
||||
avec mur opaque, fenêtres, verre, sortie d'occlusion et changement rapide de
|
||||
caméra. Vérifier familiers portés, cosmétiques, baleines translucides, coffres
|
||||
et multiblocs ; aucune disparition visible incorrecte. Mesurer le gain et le
|
||||
coût CPU. Aucun essai OpenGL ne vaut validation de ce ticket.
|
||||
|
||||
## Modules personnels — contrats à écrire
|
||||
|
||||
**MODULE-DOZEI-01 :** conserver le nom donné « Dozei Stream » ; son rôle,
|
||||
ses sources, sa cible client/serveur, ses dépendances et son premier scénario
|
||||
restent à préciser avec le créateur. Ne déduire aucune fonctionnalité du nom.
|
||||
Acceptation du cadrage : une action observable et un périmètre testable avant
|
||||
de créer un faux artefact ou une entrée packwiz sans téléchargement.
|
||||
|
||||
**MODULE-ONLYFUN-01 :** reprendre la [vision Only Fun](vision.md#only-fun--interactions-sociales-et-absurdes),
|
||||
avec l'intention confirmée d'ajouter de l'humour « pipi caca ». C'est un mod
|
||||
personnel autonome, séparé du socle nécessaire pour jouer à Sanctuary.
|
||||
Choisir une première interaction, définir ses entrées, effets, données
|
||||
persistantes et branchements facultatifs au calendrier/aux événements.
|
||||
Acceptation du cadrage : petit scénario jouable défini, comportement en solo
|
||||
et serveur, installation indépendante et libellés FR/EN prévus. Le contenu
|
||||
futur de la vision ne devient pas une fonctionnalité livrée par ce document.
|
||||
|
||||
## Ordre proposé et fin d'un ticket
|
||||
|
||||
Commencer par Bobby facultatif, puis les optimisations mesurées ; traiter la
|
||||
voix avec sa décision Sound Physics et son parcours réseau. Les deux modules
|
||||
personnels commencent par leur contrat, sans dépendance implicite à ces mods.
|
||||
Cet ordre reste une proposition pour la prochaine séance.
|
||||
|
||||
Chaque intégration livrée passe `./gradlew check build assemblePack`, les
|
||||
essais propres au comportement ajouté, deux synchronisations packwiz isolées,
|
||||
la conservation des choix et le contrôle du profil serveur. Décrire les
|
||||
résultats réels et les limites restantes avant toute publication demandée.
|
||||
@@ -0,0 +1,189 @@
|
||||
# DIST-280 — JEI embarqué et packwiz serveur
|
||||
|
||||
Branche `codex/jei-packwiz-server`, base commit `697e4a2` (beta.278).
|
||||
Version réservée `beta.280` : beta.279 appartient au chantier parallèle
|
||||
Map Island Zoom, dont les modifications ne sont pas reprises ici.
|
||||
|
||||
## Contrat
|
||||
|
||||
- Un seul JAR Sanctuary à distribuer, avec le fork JEI exact
|
||||
`30.32.0-sanctuary.4` et ses notices/bibliothèques embarqués. Fabric API reste
|
||||
une dépendance commune ; Sodium et Sound Physics restent facultatifs côté client.
|
||||
- Même JAR sur client et serveur. JEI possède une initialisation commune
|
||||
(recettes et transfert), ses écrans restent dans son entrée client.
|
||||
- Un manifeste serveur `server/pack.toml` sur le même canal packwiz que le
|
||||
client, avec uniquement Sanctuary et Fabric API. Même version, URL et hash
|
||||
du JAR Sanctuary dans les deux profils. Publication atomique des manifestes
|
||||
après vérification de l'artefact immuable.
|
||||
- Un kit serveur local avec mise à jour packwiz sans interface et lancement
|
||||
Fabric. Les mondes, configurations, permissions, listes de joueurs et secrets
|
||||
restent hors de l'index. Une erreur de mise à jour empêche le démarrage.
|
||||
- Documenter les paramètres existants de l'API Sanctuary Web et de la MariaDB
|
||||
communautaire, sans inventer d'API ni migrer les données ou configurer un site
|
||||
public implicitement. Les secrets restent dans l'environnement du service.
|
||||
|
||||
## Vérifications locales
|
||||
|
||||
`./gradlew check build assemblePack assembleServerPack
|
||||
-PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true` réussit :
|
||||
13 GameTests requis, contrôles purs du dépôt et assemblages. Le fixture ciblé
|
||||
évite le harnais worldgen historique `unified`, sans modifier sa génération.
|
||||
Les sept tests de l'outil de mise à jour passent séparément, y compris le
|
||||
relais du signal d'arrêt et l'attente de Java avant libération du verrou.
|
||||
|
||||
Installation réelle avec packwiz, deux synchronisations et six fichiers
|
||||
personnels témoins conservés octet par octet. Le JAR de production démarre et
|
||||
s'arrête dans un monde plat de développement neuf avec le JEI imbriqué ;
|
||||
celui-ci correspond au fork construit. Minecraft normalise lui-même les fichiers
|
||||
`server.properties`, `ops.json` et `whitelist.json` au lancement : leurs valeurs
|
||||
sont vérifiées après cet arrêt, distinctement de la préservation par packwiz.
|
||||
|
||||
La transition du profil client retire réellement le JEI séparé suivi par
|
||||
packwiz et conserve les favoris. Les manifestes de publication client/serveur
|
||||
référencent le même artefact. Aucun nouveau parcours graphique n'est revendiqué ;
|
||||
les contrôles historiques Vulkan de l'interface ne sont pas rejoués ici.
|
||||
|
||||
Journaux et reçus ignorés : `build/jei-server-final.log`,
|
||||
`build/server280-updater-final.log`, `build/server280-validation-delivery/validation.json`,
|
||||
`build/client280-validation/validation.json`.
|
||||
|
||||
## Serveur dédié demandé le 25 octobre 2026
|
||||
|
||||
Le créateur demande une **nouvelle CT pour 20 joueurs** sur le nœud `gpu`.
|
||||
La CT 350 `sanctuary-beta-20` est créée à cette fin : Debian 13 amd64,
|
||||
6 vCPU, 8 Gio de RAM, disque 80 Gio, non privilégiée. Le budget Java retenu est
|
||||
6 Gio, avec 20 places, portée 6 et simulation 4 ; ce dimensionnement n'est pas
|
||||
une mesure de charge avec vingt joueurs connectés.
|
||||
|
||||
L'Alpha (CT 300), les CT 330 et 340 et les mondes existants restent hors cible.
|
||||
Le nouveau monde utilisera `sanctuary:sanctuary` (réglages initiaux
|
||||
`sanctuary:island231_medium`), graine `28020261025`, version beta.280.
|
||||
Aucune expansion n'est activée par le déploiement. Le site est dans la CT 320 ;
|
||||
son API et son schéma communautaire 3 sont présents.
|
||||
|
||||
## Livraison et mise en service
|
||||
|
||||
La [release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280)
|
||||
est publiée depuis le commit `23b295356b499404e0056affbb5d3b3811c1b01f`.
|
||||
Le canal packwiz est avancé au commit `338e8a62387048c7e703ef036fda759ef57b4130`,
|
||||
avec les profils client et serveur dans le même commit. Aucun JEI séparé n'est
|
||||
joint. Le MRpack contient le seul JAR Sanctuary en override, et trois dépendances
|
||||
téléchargées (Fabric API, Sodium et Sound Physics).
|
||||
|
||||
| Artefact | SHA-256 |
|
||||
| --- | --- |
|
||||
| `sanctuary-beta.280.jar` | `489dade38218d34d2a4502fa4ffe71f0bc13035948dc1c52b633b528e9855c7e` |
|
||||
| `Sanctuary-Server-beta.280.zip` | `54ed73e75b53305a9f0cc5505f3d64624d23598f906ac69d121030255ced0d5a` |
|
||||
| `Sanctuary-beta.280.mrpack` | `b23ba2d9cf72d27da2595e3f4a32a36656f71abf2d2cb4d6cefd7f9d29e4cd01` |
|
||||
|
||||
Le kit téléchargé dans la nouvelle CT correspond à cet artefact immuable.
|
||||
Java 25, Python 3.13 et le service `sanctuary-beta.service` sont installés ;
|
||||
le service est activé au démarrage. Le premier monde Sanctuary a été créé à
|
||||
la graine annoncée, avec le générateur 231, taille Medium, en Survie et
|
||||
`online-mode=true`. La réponse réseau Minecraft annonce 26.3, protocole 777,
|
||||
**20 places**, et le JEI embarqué est chargé côté serveur.
|
||||
|
||||
La synchronisation packwiz a été exécutée après arrêt propre du nouveau serveur :
|
||||
**61 fichiers de monde, de configuration et de secrets gardent leurs hashes**.
|
||||
Le même monde redémarre ensuite. L'outil maintient son verrou jusqu'à la fin
|
||||
de la sauvegarde Java, y compris lorsque systemd transmet SIGTERM.
|
||||
|
||||
L'API du site accepte l'authentification et refuse un corps de vérification
|
||||
vide avec HTTP 422 : aucun code d'habitant n'est consommé. L'accueil serveur
|
||||
confirme que les codes sont requis. Un compte MariaDB propre à Beta a été créé,
|
||||
restreint à son hôte et aux sept tables nécessaires, avec TLS obligatoire ;
|
||||
Connector/J utilise `sslMode=verify-full`. Le schéma existant 3 n'est pas migré.
|
||||
La ligne de statistiques du nouveau serveur est alimentée toutes les 30 secondes.
|
||||
|
||||
La console du site était non configurée. Seules ses trois variables RCON ont
|
||||
été renseignées pour Beta, après sauvegarde privée ; le cache Laravel est
|
||||
reconstruit sans redémarrer le site. La commande de lecture `list`, appelée
|
||||
par le vrai service PHP, répond « 0 of a max of 20 ». Le port RCON est filtré
|
||||
dans la CT Beta : accès du site et de loopback uniquement, refus depuis un autre
|
||||
client LAN. Aucune redirection publique RCON n'est créée.
|
||||
|
||||
Les secrets restent dans `/etc/sanctuary/beta.env`, le `server.properties`
|
||||
privé et l'environnement Web, jamais dans Git ou packwiz. Les unités
|
||||
`sanctuary-beta-rcon-firewall.service` et le complément systemd associé
|
||||
appartiennent à cette installation, hors de l'index de mods.
|
||||
|
||||
Le domaine demandé est `beta.sanctuary-minecraft.net`. Les deux enregistrements
|
||||
OVH sont publiés : A pour Beta et SRV `_minecraft._tcp.beta` vers le port 25567,
|
||||
TTL 300. Les quatorze enregistrements DNS préexistants sont conservés. Une
|
||||
réservation DHCP lie l'adresse de la CT à sa MAC ; elle est activée après test
|
||||
de syntaxe et redémarrage de dnsmasq, sans changer ses deux fichiers existants.
|
||||
Aucun serveur Minecraft ou proxy existant n'est redémarré par cette opération.
|
||||
|
||||
Une redirection UPnP Beta seule a servi d'amorçage. Le 25 octobre, le créateur
|
||||
réenregistre la règle Bbox permanente « Sanctuary beta 20 » : **port externe
|
||||
25567 → 192.168.1.122:25565**. L'API de la Bbox déclare encore `protocol=all`
|
||||
(TCP/UDP) ; l'accès Minecraft TCP est vérifié.
|
||||
|
||||
La redirection UPnP appartenant à Beta est ensuite supprimée. Son timer est
|
||||
désactivé, le service est arrêté, puis les trois fichiers du maintien sont
|
||||
retirés de la configuration active et archivés dans la CT sous
|
||||
`/root/sanctuary-beta-maintenance/upnp-retired-20261025-1792900244/`.
|
||||
Les sept autres règles NAT permanentes et les quatre autres redirections UPnP
|
||||
sont conservées. Le processus Minecraft reste le même : aucun redémarrage ni
|
||||
modification d'une sauvegarde n'est nécessaire à ce retrait.
|
||||
|
||||
Après suppression du secours, `beta.sanctuary-minecraft.net:25567` répond bien
|
||||
Minecraft **26.3**, protocole **777**, **0/20**, avec le MOTD du nouveau Beta.
|
||||
Le SRV public permet de saisir seulement `beta.sanctuary-minecraft.net`.
|
||||
La vérification utilise le chemin public depuis le LAN (hairpin) ; une validation
|
||||
depuis un réseau Internet extérieur reste à effectuer.
|
||||
|
||||
Le [MRpack client publié](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack)
|
||||
est retéléchargé et vérifié : 16 589 058 octets, hash identique au fichier local,
|
||||
ZIP intègre, version beta.280, Minecraft 26.3 et un seul override Sanctuary avec
|
||||
JEI intégré.
|
||||
|
||||
Aucune charge réelle avec vingt joueurs, nouvelle connexion OAuth Discord ou
|
||||
nouvelle qualification graphique n'est revendiquée.
|
||||
|
||||
Les sources sont isolées de Map Island Zoom. La prochaine livraison doit
|
||||
reprendre ce profil serveur et utiliser un compteur supérieur à beta.280.
|
||||
L'instance Prism personnelle n'a pas été synchronisée par cette mise en service.
|
||||
|
||||
Preuves locales supplémentaires : `build/server280-publication.log`,
|
||||
`build/server280-deployment.log`, `build/server280-gpu-update-validation.log`,
|
||||
`build/server280-rcon-setup.log`, `build/server280-dhcp-reservation.log`,
|
||||
`build/server280-upnp-retirement.log` et
|
||||
les inventaires sous `/root/beta-publication-20261025/` sur PVE. Ces fichiers
|
||||
restent hors de Git.
|
||||
|
||||
## Raccordement Sanctuary Web corrigé le 25 octobre 2026
|
||||
|
||||
Le statut et l'Intendance de [Sanctuary Web](https://sanctuary-minecraft.net)
|
||||
lisaient encore l'ancienne base distante par la connexion Laravel `game`, alors
|
||||
que Beta écrivait déjà dans la base `sanctuary` de la CT Web 320. La vérification
|
||||
initiale des écritures serveur ne couvrait donc pas toutes les lectures du site.
|
||||
Hermes confirme l'identité de cette instance Web ; les anciens sites de release
|
||||
ne sont pas remis en service.
|
||||
|
||||
La connexion `game` utilise maintenant la même base locale que Beta, en gardant
|
||||
`COMMUNITY_DB_CONNECTION=game` pour les services et les ressources
|
||||
d'administration. Le cache de configuration Laravel est reconstruit. Les sources
|
||||
Web déployées, qui contiennent des changements préexistants, restent inchangées.
|
||||
La connexion Discord fonctionne selon le créateur et n'est pas modifiée.
|
||||
|
||||
La nouvelle communauté ne contient aucune publication : Gazette, Tableau et
|
||||
Intendance affichent désormais cet état, sans suppression dans l'ancienne base.
|
||||
Les trois comptes, dont deux administrateurs, le groupe d'équipe, ses deux
|
||||
adhésions et les vingt-deux permissions sont conservés. Les empreintes des
|
||||
identités, des droits administrateurs et des tables d'équipe sont identiques
|
||||
avant et après l'opération.
|
||||
|
||||
Les services publics et les requêtes d'administration Gazette/Tableau lisent
|
||||
zéro publication. La page d'accueil et les deux listes publiques répondent HTTP
|
||||
200. Les métriques sont fraîches, le serveur est déclaré en fonctionnement, et
|
||||
son temps cumulé continue d'augmenter. Le compteur reste à zéro jour tant que
|
||||
Beta n'a pas cumulé vingt-quatre heures ; les anciens décès ne sont plus affichés.
|
||||
|
||||
La configuration précédente est sauvegardée dans la CT Web, avec accès root
|
||||
seul, sous
|
||||
`/root/sanctuary-beta-maintenance/web-game-connection-20261025-1792901058/`.
|
||||
Le reçu `build/sanctuary-web-beta-alignment.log` et la capture
|
||||
`build/sanctuary-web-beta-after.png` restent hors de Git. Cette correction de
|
||||
configuration et de documentation ne change aucun binaire beta.280 ni aucune
|
||||
sauvegarde Minecraft.
|
||||
@@ -0,0 +1,126 @@
|
||||
# MAP-279 — Carte des îles et grand dézoom
|
||||
|
||||
Branche `codex/map-islands-zoom`, base beta.278 ; livraison locale beta.279,
|
||||
Minecraft 26.3, demande du 25 octobre 2026.
|
||||
|
||||
## Résultat
|
||||
|
||||
La limite de zoom liée à une fenêtre de 1 024 blocs disparaît. La molette et
|
||||
les boutons permettent de passer du détail des blocs à une vue de plusieurs
|
||||
îles, puis à une emprise de 67 108 864 blocs, suffisante pour cadrer le monde.
|
||||
Le dézoom reste centré sous le pointeur ; les boutons gardent le centre de la
|
||||
vue. Les zones découvertes éloignées sont affichées ensemble ; les zones non
|
||||
relevées restent inconnues. Le centre de la carte peut rester fixe pendant que
|
||||
le joueur se déplace et reçoit de nouveaux chunks.
|
||||
|
||||
Le message « Carte pleine : nouveaux relevés suspendus » provenait du plafond
|
||||
de 16 384 chunks par couche et habitant. Il passe à **131 072**, sans éviction
|
||||
des découvertes. Une carte ayant atteint l'ancien plafond reprend ses relevés
|
||||
lorsque les chunks sont de nouveau reçus ou revisités. Les parcours non
|
||||
conservés avant ce correctif ne peuvent pas être reconstitués automatiquement.
|
||||
Cette capacité reste bornée ; ce lot ne promet pas une exploration illimitée.
|
||||
|
||||
## Rendu et serveur
|
||||
|
||||
La texture reste de 1 024 × 1 024 pixels. Seize niveaux de réduction regroupent
|
||||
les couleurs par carrés de 2, 4, 8… jusqu'à 65 536 blocs de côté par texel.
|
||||
Le niveau demandé dépend de la fenêtre et du zoom ; le serveur ne transmet
|
||||
que les tuiles de ce niveau dans le viewport, par lots de 64 maximum.
|
||||
Les caches réduits sont calculés à partir des seuls relevés personnels,
|
||||
à la demande, et invalidés par les changements de terrain. Les couleurs de
|
||||
terrain sont privilégiées sur le vide relevé pour garder les petits îlots.
|
||||
Ce choix accentue les très petites zones et n'est pas une mesure de leur aire.
|
||||
|
||||
Les six couches d'altitude restent distinctes. Les positions des joueurs et
|
||||
les marqueurs utilisent la même emprise étendue. La vue opérateur réduit
|
||||
uniquement les chunks déjà chargés, échantillonnés à son budget existant de
|
||||
quatre chunks par tick ; elle reste séparée des relevés personnels.
|
||||
Les permissions, jetons, dimensions et cadences de requête restent contrôlés.
|
||||
L'aperçu Pause conserve son échelle proche et bénéficie de la capacité accrue.
|
||||
|
||||
Les contours des propriétés, Demeure et le clic droit sur un chunk restent
|
||||
accessibles au niveau détaillé ; la vue d'ensemble indique en FR/EN qu'il faut
|
||||
zoomer pour les retrouver. La grille s'efface lorsque ses lignes seraient
|
||||
espacées de moins de huit pixels. L'échelle affiche les blocs par pixel au
|
||||
lieu d'un multiplicateur arrondi à zéro.
|
||||
|
||||
## Persistance et compatibilité
|
||||
|
||||
Les fichiers `data/sanctuary-atlas/<UUID>.json` et leurs six couches gardent
|
||||
le schéma 2, les mêmes coordonnées, couleurs, propriétaires et graines.
|
||||
Les lecteurs historiques de schéma 1 restent identiques. Aucun niveau réduit
|
||||
n'est écrit dans les sauvegardes. La limite de taille acceptée passe de 12 à
|
||||
64 Mio par fichier pour accueillir le plafond élargi ; les écritures atomiques
|
||||
et le refus d'écraser un fichier modifié extérieurement sont conservés.
|
||||
Aucun changement de génération, chunk, identifiant ou expansion.
|
||||
|
||||
Le protocole `sanctuary:atlas_request` ajoute le niveau de détail : mettre à
|
||||
jour client et serveur ensemble. Un retour vers beta.278 exige la copie
|
||||
antérieure des relevés dès qu'une couche a dépassé 16 384 chunks : l'ancien
|
||||
lecteur refuse un tel fichier. Aucune sauvegarde personnelle ni instance de
|
||||
jeu n'est ouverte ou modifiée par ce ticket.
|
||||
|
||||
## Vérification
|
||||
|
||||
Les contrôles purs couvrent les seize réductions, les coordonnées négatives,
|
||||
le bord du monde, la conservation d'un petit îlot, l'invalidation après une
|
||||
construction, les zones inconnues, les bornes des requêtes et les marqueurs
|
||||
lointains. Le schéma 2 est sauvegardé puis relu avec 17 000 chunks ; le plafond
|
||||
courant est aussi atteint sans éviction.
|
||||
|
||||
Commandes de validation :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=map244,map,claims273,claims277,waystones239 \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryMap279ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientHeapGb=8 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le parcours graphique utilise un monde plat de développement neuf, graine
|
||||
`279`, et cinq relevés de contrôle espacés de 8 192 blocs pour isoler les
|
||||
problèmes d'échelle. Il ne constitue pas une visite des cinq continents
|
||||
Sanctuary ni un benchmark multijoueur. Les déplacements lointains, envois
|
||||
natifs de chunks et changements de blocs utilisent le vrai serveur intégré.
|
||||
Le parcours **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en **1 min 07 s**,
|
||||
avec **11 captures**. Il vérifie les cinq relevés simultanés, une arrivée réelle
|
||||
à 9 008 blocs sans déplacer le centre de la carte ni changer son jeton,
|
||||
un bloc d'or natif nouvellement relevé, un dézoom au-delà de 256 blocs par pixel puis au
|
||||
niveau maximal, l'emprise du monde sur les deux axes, le retour à la couleur
|
||||
exacte du bloc, Pause, FR/EN GUI 2/3/4 et le retrait de l'aptitude Atlas.
|
||||
Une tuile chargée pour le seul contrôle opérateur, à 12 000 blocs, apparaît
|
||||
dans son aperçu réduit sans entrer dans les relevés personnels, puis disparaît
|
||||
quand les commandes sont retirées. Le chargement forcé appartient uniquement
|
||||
à la fixture du monde de test ; les requêtes Atlas n'en créent pas.
|
||||
|
||||
Les captures de vue d'ensemble FR GUI 4, de retour au détail, d'échelle du
|
||||
monde et d'opérateur ont été inspectées. Les cinq petits carrés colorés sont
|
||||
des relevés de contrôle, pas des captures du relief des continents.
|
||||
Captures : `build/map279-screenshots/` ; log :
|
||||
`build/map279-client-vulkan.log`, marqueur `MAP279_CLIENT_PASS`.
|
||||
|
||||
La construction complète et les **20 tests serveur ciblés** passent : relevé
|
||||
natif au-delà de l'ancien plafond, six couches, absence de découverte chez un
|
||||
autre habitant ou un spectateur, droits Atlas, claims, Demeure et Waystones.
|
||||
Le log est `build/map279-check-build-pack.log`. Les contrôles purs du dépôt
|
||||
passent également. La capacité maximale en multijoueur et le coût de sauvegarde
|
||||
d'une couche entièrement remplie ne sont pas mesurés par ce lot.
|
||||
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
La dernière passe `check build assemblePack` passe en **3 min 54 s**.
|
||||
Le MRpack est exporté et vérifié : intégrité ZIP, Minecraft 26.3, Loader 0.19.5,
|
||||
un seul JAR Sanctuary beta.279 identique au build local, nouvelle pyramide
|
||||
présente, fixtures de tests absentes et trois libellés FR/EN embarqués. Le JAR
|
||||
JEI est également identique au build local ; les trois dépendances distantes
|
||||
gardent leurs empreintes. Reçu : `build/map279-validation.json`.
|
||||
|
||||
- JAR : `mods/sanctuary/build/libs/sanctuary-beta.279.jar`, 16 239 330 octets.
|
||||
- MRpack : `build/Sanctuary-beta.279.mrpack`, 16 593 044 octets.
|
||||
- SHA-256 du MRpack : `1ebe43f559cec3727fea17ed8cf5593702109ceb6151f5e45125beda83cda6c8`.
|
||||
|
||||
Cette livraison est locale : aucun canal, tag distant, serveur personnel ou
|
||||
instance Prism n'est modifié.
|
||||
@@ -1,5 +1,15 @@
|
||||
# Menus Sanctuary — organisation
|
||||
|
||||
Évolution courante du 24 octobre 2026 : [Habitant et Pause beta.275](menu-atlas-beta275.md).
|
||||
La fiche ne contient plus que Factions/Familier en navigation, avec statistiques,
|
||||
personnage et biographie en trois colonnes. Une fois Carte débloquée, Pause garde
|
||||
Aventure à gauche, Communauté à droite et Partie sous la carte centrale ; Gazette
|
||||
et Tableau conservent les colonnes latérales. Les descriptions historiques
|
||||
ci-dessous et la [première fiche beta.271](inhabitant-menu-beta271.md) restent des références.
|
||||
|
||||
Évolution du 24 octobre 2026 : [fiche Habitant et Pause beta.271](inhabitant-menu-beta271.md).
|
||||
Le cadrage ci-dessous conserve l’organisation historique de beta.083.
|
||||
|
||||
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).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# MENU-275 — Habitant et Pause autour de la carte
|
||||
|
||||
Branche `codex/inhabitant-property`, demande du 24 octobre 2026.
|
||||
|
||||
- Retirer le conseil d’arrivée sur la première propriété gratuite.
|
||||
- Habitant : Factions et Familier en haut, statistiques transparentes à gauche,
|
||||
personnage au centre avec de l’air, biographie à droite. Retirer le panneau
|
||||
d’activité et son bouton Historique de cette fiche ; conserver les données.
|
||||
- Temps joué en jours, heures, minutes et secondes entières, à partir des ticks
|
||||
Minecraft. Aucune minute décimale. Retour à la ligne en petit format.
|
||||
- Les propriétés exigent l’aptitude Carte achetée et active. Aucun bouton dans
|
||||
Habitant ; accès depuis le clic droit sur l’Atlas ou la commande après déblocage.
|
||||
Le serveur applique cette condition aux commandes, actions et ancien paquet
|
||||
de carte locale. Les données et protections acquises restent conservées.
|
||||
- Avant Carte : conserver la disposition actuelle du menu Pause.
|
||||
- Après Carte : Gazette à gauche, Tableau à droite ; dans la colonne centrale,
|
||||
Découvertes, Progression, Habitant et Combats au-dessus de la carte, puis Options,
|
||||
Options du monde et Sauvegarder/quitter ou Déconnexion sous la carte.
|
||||
Les rubriques restent visibles : **Aventure** forme toujours la colonne de
|
||||
gauche (Découvertes puis Progression), **Communauté** celle de droite
|
||||
(Habitant puis Combats), même en grande vue. **Partie / Game** reste au-dessus
|
||||
des options. Le rectangle de carte remplit l’espace restant, réduit pour
|
||||
conserver ces titres. Reprendre reste dans le pied natif.
|
||||
|
||||
Aucun format de sauvegarde ni génération ne change. Les nouveaux prérequis
|
||||
n’attribuent ni ne retirent de propriété. Les protections existantes et leur
|
||||
prédiction de minage s’appliquent aussi aux joueurs sans Carte ; seul l’accès
|
||||
à leur gestion est verrouillé. La refonte habitation/duels/prix reste en attente.
|
||||
|
||||
## Vérification
|
||||
|
||||
`check build assemblePack` réussit en **2 min 45 s**, avec **30 tests serveur
|
||||
ciblés**. Les prérequis de Carte sont testés avant achat, après achat et après
|
||||
désactivation ; refus sans débit, conservation du claim et des protections,
|
||||
réactivation et multiplicateurs de minage pour les joueurs sans Carte.
|
||||
Le format de durée couvre dix limites de ticks et un compteur long.
|
||||
|
||||
Le parcours **Vulkan FR/EN GUI 2/3/4** réussit en **56 s**, avec **18 captures**.
|
||||
Il passe par Hello World réel, vérifie l’absence du conseil de propriété,
|
||||
la commande bloquée et le paquet de carte locale forgé, puis les trois colonnes
|
||||
d’Habitant, les unités de temps, la transparence et l’absence d’activité.
|
||||
Factions et Familier reviennent à la fiche. Les vues Pause avant/après Carte,
|
||||
les deux colonnes Aventure/Communauté même en grande vue, la rubrique Partie,
|
||||
les côtés Gazette/Tableau, la disposition des options, leur retour natif et la
|
||||
mise à jour après désactivation de Carte sont contrôlés. Les textes longs
|
||||
conservent les défilements et infobulles natifs dans les petits formats.
|
||||
|
||||
Captures dans `build/menu275-screenshots/`, avec inspections FR GUI 2/4 de
|
||||
Pause, FR GUI 4 d’Habitant. Le MRpack beta.275 est exporté et vérifié : ZIP,
|
||||
Minecraft 26.3, Loader 0.19.5 et octets des deux JAR locaux identiques aux builds.
|
||||
Les archives beta.270 à beta.274 restent identiques. Reçu :
|
||||
`build/menu275-validation.json`.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=menu275,claims273,claims272,property270,mining,overview271,menus,map,demeure \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryMenu275ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Aucune publication de canal ni synchronisation d’instance personnelle.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Sanctuary beta.289
|
||||
|
||||
Six nouveaux infectés rejoignent Sanctuary : Infecté, Charger, Spitter, Boomer,
|
||||
Smoker et Hunter, chacun avec son œuf et son comportement. Le Spitter laisse
|
||||
une flaque d’acide ; le Smoker doit toucher avec son lasso avant de tirer sa
|
||||
cible ; le Hunter prépare un bond qui peut être esquivé.
|
||||
|
||||
Neuf tenues aléatoires sont partagées avec le zombie classique. Les bras restent
|
||||
en avant, avec le Hunter accroupi au sol. Les infectés restent sans variante
|
||||
bébé ; leurs familiers reprennent le modèle adulte en miniature, à taille
|
||||
variable, avec leurs pouvoirs.
|
||||
|
||||
Les objets lumineux tenus en main éclairent de nouveau le terrain avec Sodium,
|
||||
y compris sans PBR. JEI est intégré au même JAR Sanctuary sur le client et le
|
||||
serveur. Le grand dézoom de carte et les relevés lointains sont conservés.
|
||||
|
||||
Minecraft 26.3 · Fabric 0.19.5. Construction complète, 31 tests serveur et parcours
|
||||
Vulkan des zombies/familiers vérifiés pour cette livraison. Les lumières utilisent
|
||||
le correctif de beta.288, dont les classes sont conservées à l’identique.
|
||||
|
||||
Le téléchargement du site reste l’instance Prism prête à suivre le canal stable.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Sanctuary beta.293
|
||||
|
||||
- Happy Creepers repris de la branche dédiée `codex/happy-creepers` : Maj +
|
||||
clic droit avec une fleur pour la porter, visage souriant et comportement
|
||||
passif ; retrait à main vide avec le même geste. Charge électrique conservée.
|
||||
- Baleines fantômes volantes et œuf natif de beta.290 inclus.
|
||||
- Logo Sanctuary affiché pendant les téléchargements et mises à jour packwiz,
|
||||
avec l'installateur intégré au ZIP Prism prêt à importer.
|
||||
- Simple Voice Chat 2.6.24+26.3, Bobby 5.2.16+mc26.3, Lithium 0.26.2+mc26.3 et
|
||||
Entity Culling 1.11.3 pour 26.3 disponibles comme choix facultatifs du client.
|
||||
Lithium et la voix font également partie du profil serveur.
|
||||
- Sound Physics 1.5.1, Sodium 0.9.2, JEI embarqué et Minecraft 26.3 / Fabric
|
||||
0.19.5 conservés. La version vocale retenue respecte ces dépendances.
|
||||
- Sauvegardes et réglages restent attachés à la même instance ; aucun changement
|
||||
de génération ou de format de monde. Dozei Stream et Only Fun restent futurs.
|
||||
|
||||
Le ZIP Prism suit le canal stable à chaque lancement. Les anciens utilisateurs
|
||||
gardent leur instance ; la première mise à jour de l'habillage doit aussi
|
||||
installer le nouvel outil packwiz et conserver `--bootstrap-no-update`.
|
||||
@@ -0,0 +1,164 @@
|
||||
# PACK-292 — voix, cache de chunks et optimisations
|
||||
|
||||
Préparation locale du **26 octobre 2026**, branche
|
||||
`codex/optional-mods-integration`, depuis le logo packwiz beta.291.
|
||||
Le même logo et le même canal stable sont conservés.
|
||||
|
||||
## Mods intégrés
|
||||
|
||||
| Mod | Binaire retenu pour Minecraft 26.3 / Fabric | Client | Serveur |
|
||||
| --- | --- | --- | --- |
|
||||
| [Simple Voice Chat](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy) | 2.6.24+26.3, bêta | Facultatif, coché initialement | Inclus, activé initialement |
|
||||
| [Bobby](https://modrinth.com/mod/bobby/version/J7YnfCss) | 5.2.16+mc26.3 | Facultatif, coché initialement | Exclu |
|
||||
| [Lithium](https://modrinth.com/mod/lithium/version/xS0Q8LSi) | 0.26.2+mc26.3 | Facultatif, coché initialement | Inclus, activé initialement |
|
||||
| [Entity Culling](https://modrinth.com/mod/entityculling/version/X1g3VhTx) | 1.11.3 pour mc26.3 | Facultatif, coché initialement | Exclu |
|
||||
|
||||
Chaque téléchargement utilise le binaire original, sa version Modrinth
|
||||
immuable et son SHA-512 dans `packwiz/mods/*.pw.toml`. Les JAR restent séparés,
|
||||
sans dépendance obligatoire ni copie imbriquée dans le JAR Sanctuary. Les
|
||||
descriptions du sélecteur packwiz sont en FR/EN. Les interfaces amont conservent
|
||||
leurs propres traductions, vérifiées pour le menu vocal français et anglais.
|
||||
|
||||
`scripts/data/optional-mods.json` fixe les identifiants, versions, côtés,
|
||||
dépendances et conflits attendus. `verify_optional_mods.py`, exécuté par
|
||||
`check` et `assemblePack`, vérifie les métadonnées Fabric des JAR, leur hash et
|
||||
les bibliothèques imbriquées. Les quatre fichiers réellement utilisés par
|
||||
Gradle sont aussi comparés aux binaires vérifiés pendant la validation.
|
||||
Minecraft reste **26.3**, Loader **0.19.5**, Java **25**, Fabric API
|
||||
**0.160.5+26.3** ; MixinExtras **0.5.5** est fourni par Loader et satisfait Lithium.
|
||||
|
||||
## Choix de version vocale
|
||||
|
||||
Les versions vocales 2.6.25 et 2.6.26 refusent explicitement Sound Physics
|
||||
Remastered <1.5.2. Le pack conserve son **1.5.1+26.3** et son interface native,
|
||||
donc la voix est épinglée en **2.6.24+26.3**, sans contournement des dépendances.
|
||||
Le plug-in Sound Physics est chargé par le serveur vocal et la connexion locale
|
||||
est authentifiée pendant le parcours client. Les mises à jour de ces deux mods
|
||||
doivent être traitées ensemble à la prochaine évolution de version.
|
||||
|
||||
Le menu vocal s'ouvre avec sa touche amont **V**, configurable ; la touche de
|
||||
push-to-talk se choisit dans les réglages vocaux. Aucun raccourci ni préférence
|
||||
personnelle n'est remplacé par le pack. La voix reste facultative pour le joueur.
|
||||
|
||||
Le profil serveur contient désormais **Fabric API, Sanctuary, Lithium et Simple
|
||||
Voice Chat**. Son choix est indépendant des options d'un client. Le kit Linux
|
||||
reste sans interface ; son installateur officiel est conservé. La vérification
|
||||
Web compare aussi les nouveaux JAR serveur au canal avant toute publication.
|
||||
|
||||
L'utilisation sur la Beta dédiée exigera le
|
||||
[port UDP vocal](https://dev.modrepo.de/minecraft/voicechat/wiki/server_setup),
|
||||
par défaut 24454, et un échange audible entre deux joueurs. Cette préparation
|
||||
ne modifie ni la CT Beta, ni ses règles réseau, ni l'instance personnelle. Le
|
||||
microphone est désactivé dans le test de développement ; aucune captation de la
|
||||
voix réelle de l'utilisateur n'est nécessaire à la validation réseau locale.
|
||||
|
||||
## Portée de Bobby et d'Entity Culling
|
||||
|
||||
[Bobby](https://github.com/Johni0702/bobby) conserve les chunks déjà envoyés
|
||||
par le serveur dans son cache client `.bobby`, puis peut afficher leur dernière
|
||||
copie connue au-delà de la distance d'envoi. Il ne génère pas de chunks et ne
|
||||
simule pas les zones lointaines. Le serveur reste autoritaire lorsqu'il renvoie
|
||||
l'état courant. Le pack ne distribue aucun monde `bobby-fallback`, cache, ni
|
||||
configuration personnelle ; la limite de distance et la conservation du cache
|
||||
restent celles du mod et des réglages du joueur.
|
||||
|
||||
[Entity Culling](https://github.com/tr7zw/EntityCulling) conserve son moteur
|
||||
amont et ses bibliothèques TRender/TRansition imbriquées. L'occlusion et la
|
||||
réapparition d'une entité sont exercées sur Vulkan. Aucun gain chiffré de FPS
|
||||
ou de ticks n'est revendiqué ; les validations vérifient ici le fonctionnement
|
||||
et la coexistence, sans benchmark représentatif de tous les mondes Sanctuary.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- `./gradlew check build assemblePack assembleServerPack
|
||||
-PsanctuaryFocusedTests=recipes,progression,smoker285,hunter286
|
||||
-PsanctuaryAtlasOnly=true -PsanctuaryOptional292ServerTests=true` :
|
||||
**réussi en 2 min 52 s**, **46 tests serveur requis réussis**, Lithium et voix
|
||||
effectivement chargés. Recettes, progression, lasso du Smoker et bond du Hunter
|
||||
sont exercés avec ces dépendances. Les contrôles de ressources, de packwiz et
|
||||
de l'habillage passent aussi. Le test de quota de baleines déjà en échec dans
|
||||
[beta.291](packwiz-logo-beta291.md) reste hors de ce périmètre, sans correction
|
||||
ni validation nouvelle de ce cas.
|
||||
- Parcours graphique **Vulkan / MoltenVK 1.4.2 / Apple M1 avec Sodium 0.9.2**,
|
||||
réussi en **38 s** : quatre mods présents, Sound Physics initialisé, entité
|
||||
masquée derrière le mur puis visible après retrait ; Bobby actif, stockage
|
||||
persistant et **35 chunks de cache** visibles après un déplacement de huit
|
||||
chunks avec une distance d'envoi de quatre. Authentification et connexion UDP
|
||||
vocales réelles, menus vocaux FR/EN ouverts en gardant le microphone muet.
|
||||
Les captures du mur, de l'entité réapparue et du menu FR sont inspectées.
|
||||
- Le même parcours **Vulkan sans Sodium** réussit en **43 s**, avec les mêmes
|
||||
contrôles, 35 chunks de cache et menus FR/EN. Un premier passage avait atteint
|
||||
tous les contrôles avant de se bloquer dans la synchronisation des threads du
|
||||
lanceur Fabric pendant la fermeture du monde ; il a été interrompu après
|
||||
capture des piles. La nouvelle exécution se ferme normalement. Cette limite
|
||||
du premier essai reste dans le journal, sans la présenter comme un succès.
|
||||
- Deux synchronisations client depuis le ZIP Prism réussies : **huit JAR**,
|
||||
un seul Sanctuary, mêmes hashes au second passage, logo de l'installateur
|
||||
conservé et trois fichiers témoins personnels inchangés. Puis deux passages
|
||||
supplémentaires avec un choix Bobby désactivé déjà persisté, simulé dans le
|
||||
cache de test : Bobby n'est pas réinstallé et le choix reste faux.
|
||||
- Deux synchronisations serveur réussies : exactement les quatre JAR du
|
||||
profil serveur ; aucun Bobby, Entity Culling, Sodium ou Sound Physics client.
|
||||
- Les **11 tests** du vérificateur Web passent, y compris la comparaison des
|
||||
dépendances partagées avec le relevé serveur et le refus d'un mod client dans
|
||||
son profil. Le MRpack exporté possède les sept dépendances distantes et leurs
|
||||
bons côtés et options ; les overrides sont limités à Sanctuary et son icône.
|
||||
- Contrôle complémentaire des baleines avec les quatre mods, Sodium et Vulkan :
|
||||
les assertions de rendu translucide, modèle animé, œuf FR/EN, deux entités
|
||||
chargées et déplacement réel atteignent `WHALE290_CLIENT_PASS`. La capture
|
||||
`0001_whale290-ghosts.png` est inspectée. Ce parcours se bloque ensuite à la
|
||||
fermeture dans la synchronisation du lanceur de tests Fabric et est arrêté
|
||||
après capture des piles ; il ne constitue pas une exécution complète réussie.
|
||||
- Après ajout des notices, le JAR et les archives sont réassemblés et les
|
||||
synchronisations sont répétées sur les binaires finaux. Les **2 658 classes
|
||||
de jeu** restent identiques à beta.291. Le vérificateur Web accepte le MRpack
|
||||
et le ZIP Prism finaux contre les métadonnées préparées et les hashes du
|
||||
serveur de test synchronisé, avec état de service simulé pour cette vérification
|
||||
hors ligne ; aucune CT de production n'est contactée.
|
||||
|
||||
Les essais utilisent uniquement des mondes et dossiers de développement sous
|
||||
`build/`. Les copies et hashes finaux sont consignés dans
|
||||
`build/optional-mods292/`, avec les journaux, captures et reçus natifs.
|
||||
Le passage vocal audible à deux joueurs, l'accès UDP depuis Internet et les
|
||||
rendus propres aux familiers et multiblocs avec culling restent des
|
||||
vérifications complémentaires à effectuer avant d'en revendiquer la couverture.
|
||||
|
||||
Commande du parcours client principal (ajouter `-PsanctuarySodiumClientTests=true`
|
||||
pour le passage avec Sodium) :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryOptional292ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryFocusedTests=recipes,progression
|
||||
```
|
||||
|
||||
L'option supplémentaire `-PsanctuaryOptional292WhaleOnly=true` sélectionne
|
||||
le diagnostic des baleines avec le même ensemble de mods ; son blocage de
|
||||
fermeture observé ci-dessus reste ouvert.
|
||||
|
||||
## Livraison et suite
|
||||
|
||||
Le compteur du pack et du mod est aligné sur **beta.292**. Les archives locales
|
||||
conservent l'habillage packwiz, les options et le canal de la Beta. Un ZIP Prism
|
||||
préparé ne fait pas avancer le canal public : avant publication, ce canal sert
|
||||
encore la version publiée. Aucune installation de jeu, sauvegarde personnelle,
|
||||
publication Web, release Gitea ou configuration réseau n'est modifiée ici.
|
||||
|
||||
Dozei Stream et Only Fun restent des modules personnels futurs, avec leurs
|
||||
[tickets de cadrage](integrations-mods-next.md). Aucun artefact fictif n'est
|
||||
ajouté au pack pour ces projets.
|
||||
|
||||
Artefacts locaux vérifiés, reçus dans `artifacts.json` :
|
||||
|
||||
| Fichier dans `build/` | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `Sanctuary-beta.292.mrpack` | 16 753 207 | `28980cec018fc81f700cb8ecb3981199537e04ff737bf58b220ce442b7e5dc56` |
|
||||
| `Sanctuary-Prism-beta.292.zip` | 3 928 525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.292.zip` | 4 036 530 | `0ac362bbfbfd2c03efdaf6b209d9319e648ad546bee074521038fffc86bb640a` |
|
||||
|
||||
JAR Sanctuary : `0163da08897de96559ab51f482363bd00c03cfb3309784cdb6e112acf25a4862`.
|
||||
Le ZIP Prism d'amorçage a les mêmes octets qu'en beta.291 : il contient l'outil,
|
||||
le logo et l'adresse stable, et les versions de Minecraft/Loader n'ont pas changé.
|
||||
Le MRpack et les métadonnées du canal préparé contiennent les nouveaux mods.
|
||||
@@ -0,0 +1,109 @@
|
||||
# PACK-LOGO — logo Sanctuary pendant packwiz
|
||||
|
||||
Branche `codex/packwiz-sanctuary-logo`, préparation locale **beta.291**, depuis
|
||||
le socle beta.290. Demande du 26 octobre 2026 : afficher le bandeau Sanctuary
|
||||
fourni dans la fenêtre de téléchargement et de mise à jour, avant Minecraft.
|
||||
|
||||
## Résultat
|
||||
|
||||
Le PNG `minecraft.png` fourni par le créateur est conservé octet pour octet
|
||||
dans `launcher/packwiz/sanctuary.png` (1024 × 256, RGBA). Son SHA-256 est
|
||||
`efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157`.
|
||||
La fenêtre l'affiche sur fond noir à 640 × 160, proportions conservées,
|
||||
au-dessus de la progression réelle. Ses boutons, le texte initial et le
|
||||
compte à rebours suivent la langue du système : français, anglais sinon.
|
||||
Les messages et dialogues du moteur amont conservent leurs traductions amont.
|
||||
|
||||
`scripts/packwiz_installer.py` télécharge le JAR officiel
|
||||
[packwiz-installer v0.5.14](https://github.com/packwiz/packwiz-installer/releases/tag/v0.5.14),
|
||||
vérifie son SHA-256, puis remplace uniquement `InstallWindow.class` par
|
||||
l'adaptation Java de sa fenêtre Swing. Toutes les autres classes et notices
|
||||
restent identiques : téléchargement, vérification des hashes, sélection des
|
||||
mods facultatifs, erreurs et annulation continuent de dépendre de packwiz.
|
||||
La licence MIT de comp500 et le source de l'adaptation sont joints au JAR.
|
||||
Le logo est embarqué : aucun téléchargement d'image n'est requis pour l'afficher.
|
||||
|
||||
Le build utilise Java 25 ; les classes de la fenêtre ciblent Java 8 comme
|
||||
l'amont. Les SHA-256 de l'amont, du logo et du résultat sont fixés dans
|
||||
`launcher/packwiz/installer.json`. Une compilation différente échoue avant
|
||||
d'écrire le JAR de distribution. Les dépendances et JAR générés restent dans
|
||||
`build/packwiz-branding/`, ignoré par Git.
|
||||
|
||||
## Archive Prism et mises à jour
|
||||
|
||||
La commande habituelle `python3.14 scripts/pack.py prism <url-du-canal>`
|
||||
embarque le bootstrap officiel vérifié et le nouvel installateur vérifié,
|
||||
avec la même configuration Minecraft 26.3 / Fabric 0.19.5 et le même canal
|
||||
stable. `--bootstrap-no-update` empêche le bootstrap de remplacer la fenêtre
|
||||
Sanctuary par le binaire officiel. Cette option concerne **l'installateur** ;
|
||||
la synchronisation des mods packwiz reste active à chaque lancement.
|
||||
Le nom beta.291 du ZIP local n'avance pas ce canal : avant publication, un
|
||||
lancement visant l'adresse stable récupère la version déjà publiée, beta.289
|
||||
au moment de cette préparation. Les essais beta.291 utilisent un serveur HTTP
|
||||
de développement isolé, jamais le monde ni le serveur personnel.
|
||||
|
||||
Les évolutions de l'installateur doivent donc être préparées et testées
|
||||
explicitement, avec mise à jour du verrou et du vérificateur Web. Une simple
|
||||
avance du canal de mods ne met pas à jour l'outil d'une instance existante.
|
||||
Lors d'une future synchronisation locale demandée, sauvegarder l'instance,
|
||||
copier le JAR vérifié et ajouter l'option à sa commande avant lancement en
|
||||
conservant les autres réglages ; conserver cette même instance, sans réimport.
|
||||
Le kit serveur conserve son installateur officiel et son fonctionnement sans
|
||||
interface. Le logo n'est pas ajouté à l'index des fichiers du monde ou du jeu.
|
||||
|
||||
`verify_prism` contrôle désormais les deux JAR par leurs hashes et la présence
|
||||
de l'option qui conserve l'habillage. Les sauvegardes et fichiers personnels
|
||||
restent refusés dans le ZIP. Le MRpack n'embarque pas les commandes Prism et
|
||||
ne suffit pas à installer cet habillage.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Logo fourni identique octet pour octet ; JAR résultant contrôlé par son
|
||||
SHA-256 `9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875`.
|
||||
- Les deux tests de distribution de `test_packwiz_installer.py` passent :
|
||||
toutes les entrées de l'amont sont relues, seule la classe de fenêtre change,
|
||||
et le véritable JAR vérifié est retrouvé dans l'export Prism.
|
||||
- Les dix tests de `test_publish_web_pack.py` passent : outils modifiés refusés,
|
||||
option de conservation obligatoire, contrôles de publication préservés.
|
||||
- Parcours Swing réel en FR et EN réussi avec le `GUIHandler` officiel :
|
||||
progression déterminée/indéterminée, options, Annuler, Continuer et compte à
|
||||
rebours. Les captures `preview-fr.png` et `preview-en.png` sont inspectées.
|
||||
Le test ne lance ni Minecraft ni un microphone.
|
||||
- Deux exécutions de `check build assemblePack` avec le profil
|
||||
`whale290,recipes,progression` échouent sur le même test serveur hérité :
|
||||
`eggQuotaNaturalSpawnAndFlightUseActualServerEntities`, quota après utilisation
|
||||
de l'œuf entre Overworld et Nether (18 réussites sur 19). Le profil isolé
|
||||
`whale290` échoue également sur ce cas (6 réussites sur 7). Les sources du
|
||||
comportement et de son test ne sont pas modifiées dans ce chantier. Cet échec
|
||||
reste ouvert ; aucune validation complète des baleines n'est revendiquée.
|
||||
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` réussit en **2 min 42 s**, avec **13 tests serveur
|
||||
requis réussis**, les contrôles purs de `check`, le JAR et les profils client
|
||||
et serveur assemblés. Ce périmètre ne valide pas le test de baleines en échec.
|
||||
- Comparaison des JAR beta.290/beta.291 : **2 658 classes de jeu identiques**,
|
||||
aucune classe ajoutée. Le compteur et les notices évoluent, sans changement
|
||||
du code de jeu.
|
||||
- Deux lancements du véritable bootstrap et de l'installateur adapté réussis
|
||||
depuis le ZIP extrait dans `native instance with spaces/`, avec les arguments
|
||||
de sa commande Prism et un canal HTTP local beta.291. Un seul JAR Sanctuary,
|
||||
fichiers gérés identiques au second passage, installateur avec logo conservé,
|
||||
trois fichiers témoins personnels inchangés. Aucun monde ouvert. Le mode
|
||||
sans interface vérifie la synchronisation ; les tests Swing ci-dessus
|
||||
vérifient la fenêtre. Reçu `native-validation.json`.
|
||||
|
||||
Archive locale `build/Sanctuary-Prism-beta.291.zip` : **3 928 525 octets**,
|
||||
SHA-256 `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9`.
|
||||
JAR Sanctuary beta.291 :
|
||||
`bd130418a05f820dffd86b00595c5b7ce96293ec82867926f5493a95736aee73`.
|
||||
Les interfaces Linux/Windows, le lancement Minecraft depuis Prism et une
|
||||
mise à jour de l'instance personnelle ne sont pas revendiqués par ces essais
|
||||
macOS. Le test de quota des baleines reste à résoudre avant une validation
|
||||
complète de cette base de jeu.
|
||||
|
||||
Les reçus et captures restent sous `build/packwiz-branding/`.
|
||||
|
||||
Les mods évoqués pendant ce chantier sont seulement
|
||||
[préparés en tickets pour la passe suivante](integrations-mods-next.md).
|
||||
Aucune publication du canal, du site, du serveur Beta ou de l'instance
|
||||
personnelle n'est demandée ou effectuée par cette préparation locale.
|
||||
+80
-39
@@ -1,5 +1,70 @@
|
||||
# Distribution packwiz et Prism
|
||||
|
||||
Le [téléchargement direct de Sanctuary Web](beta-download.md) donne une instance
|
||||
Prism déjà configurée. Le premier lancement télécharge les mods ; les suivants
|
||||
suivent les publications explicitement demandées via Codex.
|
||||
|
||||
La [procédure de reprise et de publication complète](updates.md) relie aussi
|
||||
le serveur dédié, le téléchargement du site et l'instance locale. Les reçus
|
||||
des anciennes versions ci-dessous restent historiques.
|
||||
|
||||
## beta.293 — livraison complète publiée
|
||||
|
||||
[Reçu de livraison](release-beta293.md) : Happy Creepers de la branche dédiée,
|
||||
baleines, logo et quatre mods facultatifs réunis. Release immuable, canal
|
||||
client/serveur, CT 350, téléchargement Web et même instance Prism personnelle
|
||||
mis à jour. Import neuf et instance existante vérifiés sur deux passages,
|
||||
sauvegardes et réglages conservés. Voix UDP 25565 annoncée sur le port public
|
||||
25567 via la règle Beta existante ; ping applicatif réussi depuis le LAN.
|
||||
L'audio à deux joueurs et l'accès depuis un réseau extérieur restent à essayer.
|
||||
|
||||
## beta.292 — quatre mods facultatifs (préparation locale)
|
||||
|
||||
[Voix, cache de chunks et optimisations](optional-mods-beta292.md) : Simple
|
||||
Voice Chat 2.6.24, Bobby 5.2.16, Lithium 0.26.2 et Entity Culling 1.11.3, tous
|
||||
épinglés pour Minecraft 26.3 / Fabric. Les quatre sont facultatifs côté client ;
|
||||
Lithium et la voix sont aussi présents dans le profil serveur. Le logo de
|
||||
beta.291 et les préférences de sélection sont conservés. Préparation locale,
|
||||
sans avance du canal public ni modification d'installation personnelle.
|
||||
|
||||
## beta.291 — logo dans l'installateur (préparation locale)
|
||||
|
||||
Le [bandeau Sanctuary fourni](packwiz-logo-beta291.md) est embarqué dans la
|
||||
fenêtre packwiz du ZIP Prism préparé. Le bootstrap reste officiel ; la fenêtre
|
||||
est adaptée et l'installateur épinglé avec `--bootstrap-no-update`. Les mods
|
||||
continuent de suivre le canal stable. Les instances déjà installées devront
|
||||
recevoir ce JAR et cette option lors d'une synchronisation explicitement demandée,
|
||||
en conservant la même instance. Cette préparation ne publie pas le canal.
|
||||
|
||||
## beta.280 — JEI embarqué et profil serveur
|
||||
|
||||
JEI rejoint le JAR Sanctuary ; `mods/sanctuary.pw.toml` reste le seul artefact
|
||||
Sanctuary à publier. `python3 scripts/pack.py release <sanctuary-https-url>`
|
||||
prépare désormais le client et `server/pack.toml`, qui partagent le même JAR,
|
||||
la même URL et le même hash. Le profil serveur contient seulement Fabric API
|
||||
et Sanctuary ; Sodium et Sound Physics restent facultatifs côté client.
|
||||
Les deux profils sont publiés dans le même commit du canal, après vérification
|
||||
du JAR immuable. L'ancienne entrée JEI est retirée de l'index client.
|
||||
|
||||
`./gradlew assembleServerPack` construit un kit Linux avec les outils épinglés
|
||||
et les commandes de synchronisation/démarrage. [Mode d'emploi](server-pack.md),
|
||||
[contrat et vérifications](jei-server-beta280.md). La configuration Sanctuary Web,
|
||||
MariaDB et les secrets restent hors de l'index. La préparation locale ne publie
|
||||
pas le canal et ne met pas à jour une installation personnelle.
|
||||
|
||||
Livraison du 25 octobre : [release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280)
|
||||
publiée, canal `338e8a62387048c7e703ef036fda759ef57b4130`, puis nouveau serveur
|
||||
CT 350 sur `gpu` installé avec le kit vérifié. Le monde neuf, les paramètres
|
||||
et les services Web sont vérifiés dans le [reçu](jei-server-beta280.md).
|
||||
Une synchronisation du serveur conserve 61 fichiers suivis. L'Alpha et
|
||||
l'instance Prism personnelle restent hors de cette mise en service.
|
||||
|
||||
La règle Bbox permanente est ensuite vérifiée et le secours UPnP Beta retiré.
|
||||
Le domaine Minecraft `beta.sanctuary-minecraft.net` répond sans ce secours,
|
||||
avec 20 places. [MRpack client beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack)
|
||||
vérifié et disponible ; le [reçu](jei-server-beta280.md) conserve les empreintes
|
||||
et la limite de validation réseau.
|
||||
|
||||
## beta.257 — Mods client séparés
|
||||
|
||||
JEI Sanctuary, Sound Physics Remastered et Sodium sont facultatifs et séparés
|
||||
@@ -2078,18 +2143,18 @@ La procédure complète, également utilisable manuellement :
|
||||
1. Incrémenter ensemble les versions de `gradle.properties` et `packwiz/pack.toml`
|
||||
selon la [convention `beta.xxx`](versioning.md).
|
||||
2. Exécuter `./gradlew check build assemblePack` et les vérifications du ticket.
|
||||
3. Préparer les métadonnées de la livraison, avec les deux URL exactes :
|
||||
3. Pour préparer les métadonnées manuellement depuis beta.280, utiliser la
|
||||
seule URL du JAR Sanctuary contenant JEI :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release \
|
||||
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.257/sanctuary-beta.257.jar \
|
||||
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.257/jei-sanctuary-26.3-30.32.0-sanctuary.4.jar
|
||||
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/sanctuary-beta.xxx.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
commit et joindre les JAR Sanctuary et JEI construits, sans remplacer les artefacts d'une release
|
||||
commit et joindre le JAR Sanctuary construit, sans remplacer les artefacts d'une release
|
||||
antérieure. Vérifier que le téléchargement public possède le SHA-256 indiqué
|
||||
dans `build/packwiz-release/mods/sanctuary.pw.toml` et `jei-sanctuary.pw.toml`.
|
||||
dans `build/packwiz-release/mods/sanctuary.pw.toml`, identique au profil serveur.
|
||||
5. Publier le contenu de `build/packwiz-release/` à la racine de la branche
|
||||
`packwiz` dans un seul commit, puis vérifier les fichiers et hashes depuis
|
||||
l'URL publique. Préserver l'historique de ce canal et ne jamais y copier le
|
||||
@@ -2111,45 +2176,21 @@ python3 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/bran
|
||||
|
||||
Il contient les versions exactes Minecraft/Fabric, la commande de lancement et
|
||||
`packwiz-installer-bootstrap` v0.0.3. Le script vérifie le SHA-256 du bootstrap
|
||||
officiel avant d'écrire `build/Sanctuary-Prism-auto-update.zip`. Ce ZIP est réservé
|
||||
officiel avant d'écrire `build/Sanctuary-Prism-beta.xxx.zip`. Ce ZIP est réservé
|
||||
aux nouvelles installations ; les instances déjà branchées sur packwiz suivent
|
||||
le canal sans être réimportées.
|
||||
|
||||
## Raccorder l'instance Prism existante
|
||||
## Importer l'instance Prism prête
|
||||
|
||||
Fermer Minecraft avant de modifier ses mods. Fermer également Prism avant de
|
||||
modifier directement `instance.cfg`, pour éviter que son état en mémoire écrase
|
||||
les changements. Dans l'instance Sanctuary Beta :
|
||||
Le bouton **Télécharger** de [Sanctuary Web](https://sanctuary-minecraft.net)
|
||||
télécharge directement [Sanctuary-Beta.zip](https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip).
|
||||
Dans Prism : **Ajouter une instance → Importer depuis un ZIP**, choisir ce fichier,
|
||||
puis lancer **Sanctuary Beta**. Packwiz télécharge automatiquement les mods.
|
||||
La commande avant lancement et l'outil sont déjà dans l'archive.
|
||||
|
||||
1. Sauvegarder `instance.cfg` et les anciens JAR Sanctuary dans un dossier de
|
||||
sauvegarde hors de `mods/`. Vérifier que l'instance utilise Minecraft
|
||||
`26.3-pre-2` et Fabric `0.19.5` ; ne pas convertir une instance 26.2.
|
||||
2. Mettre le bootstrap issu du ZIP dans son dossier `minecraft/` (ou `.minecraft/`
|
||||
selon l'installation).
|
||||
3. Dans **Modifier → Paramètres → Commandes personnalisées**, activer les
|
||||
commandes et renseigner la commande avant lancement ci-dessous.
|
||||
4. Pour une instance issue d'un ancien `.mrpack`, déplacer seulement l'ancien
|
||||
JAR Sanctuary non géré dans la sauvegarde préparée, puis faire la première
|
||||
synchronisation packwiz. Conserver les autres mods et données personnelles.
|
||||
Si la synchronisation échoue, remettre ce JAR et les réglages sauvegardés.
|
||||
5. Vérifier qu'il reste un seul mod Sanctuary actif et que `packwiz.json` est
|
||||
présent dans le dossier Minecraft. Les prochaines mises à jour sont suivies
|
||||
par ce manifeste local : ne pas le supprimer entre les versions.
|
||||
|
||||
Commande à coller dans l'interface Prism :
|
||||
|
||||
```text
|
||||
"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" --bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" --pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Une modification directe de `instance.cfg` doit échapper les guillemets au format
|
||||
Qt INI (`\"`) et activer `OverrideCommands=true`. Ces clés et
|
||||
`PreLaunchCommand` doivent être dans la section **`[General]`**, jamais ajoutées
|
||||
après la section `[UI]`. Le ZIP préparé le fait déjà. Rouvrir les commandes
|
||||
personnalisées dans Prism pour vérifier que la commande est effectivement lue.
|
||||
Une instance importée d'un `.mrpack` peut être marquée `ManagedPack=true` :
|
||||
dès que packwiz en assure les mises à jour, utiliser `ManagedPack=false` pour
|
||||
éviter deux mécanismes de gestion sur le même pack.
|
||||
Conserver cette même instance pour les versions suivantes : une publication
|
||||
explicitement demandée via Codex est suivie au lancement suivant. Une instance
|
||||
déjà branchée sur le canal stable n'a pas besoin d'être réimportée.
|
||||
|
||||
## Données locales et mondes
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Prochains tickets — carte immersive, progression et New Game+
|
||||
|
||||
**Ajout du 26 octobre 2026 :** [prochaine passe d'intégrations du pack](integrations-mods-next.md)
|
||||
pour Simple Voice Chat, Bobby facultatif, Lithium et Entity Culling, puis les
|
||||
contrats des futurs modules personnels Dozei Stream et Only Fun. Tickets
|
||||
initialement documentaires ; les quatre mods du pack sont désormais préparés
|
||||
dans [beta.292](optional-mods-beta292.md). Le cadrage historique ci-dessous est conservé.
|
||||
|
||||
Cadrage du 13 septembre 2026, après l'essai de beta.006.
|
||||
Branche documentaire : `codex/roadmap-progression-ngplus`.
|
||||
**Statut : MAP-03 implémenté en beta.007, PROG-02A en beta.008, PROG-02B en beta.009, PROG-02C en beta.010 ; CYCLE-01, PROG-03, FACTION-01 et NGP-01 sont réunis dans beta.011.**
|
||||
@@ -50,6 +56,22 @@ flowchart LR
|
||||
P --> N
|
||||
```
|
||||
|
||||
## CLAIM-278 — Battle consenti pour une propriété
|
||||
|
||||
[Le duel de propriété](claim-battle-beta278.md) vise un claim déjà revendiqué :
|
||||
proposition du challenger, accord explicite du propriétaire, puis préparation
|
||||
native Battle. Le vainqueur reçoit le chunk avec sa protection restante ; les
|
||||
résultats et transferts ont un journal de reprise. Le départage d’un terrain
|
||||
encore libre reste celui de Demeure.
|
||||
|
||||
## CLAIM-277 — Habitation avant revendication
|
||||
|
||||
[Le lot Demeure / claims](claim-demeure-beta277.md) applique le seuil existant
|
||||
et l’influence des habitants sur les chunks libres. À égalité, l’ordre des
|
||||
premières traces départage ; un habitant plus influent peut revendiquer avant
|
||||
le premier. Les propriétés acquises restent conservées. Les duels et transferts
|
||||
consentis ne sont pas ajoutés dans ce lot.
|
||||
|
||||
## CYCLE-01 — Définir ce qui termine et ce qui recommence
|
||||
|
||||
**Branche prévue :** `codex/progression-cycle-contract`.
|
||||
@@ -299,6 +321,12 @@ ou de prestige ne doit être encaissée deux fois pour un même événement.
|
||||
|
||||
## Après ce jalon
|
||||
|
||||
Le premier lot [CLAIM-02 / beta.270](property-beta270.md), demandé le
|
||||
24 octobre 2026, remplace pour l’implémentation actuelle le projet ci-dessous :
|
||||
propriété personnelle gratuite après Hello World, volume fini en trois
|
||||
dimensions et agrandissements en XP. Demeure et les factions ne donnent aucun
|
||||
droit automatique de propriété. Le paragraphe suivant conserve le cadrage historique.
|
||||
|
||||
**CLAIM-01** traitera la réservation et la protection des chunks à partir des
|
||||
factions et d'un contrat dédié : emprise, capacités, droits, conflits, abandon
|
||||
et incidence du prestige. La grille et Demeure restent des lectures du monde
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# CLAIM-02 — Première propriété d'habitant
|
||||
|
||||
Branche `codex/inhabitant-property`, version locale `beta.270`.
|
||||
La demande du 24 octobre 2026 remplace, pour ce lot, le projet CLAIM-01
|
||||
de titres temporaires par chunks et d'entretien en saphirs.
|
||||
|
||||
## Contrat du premier lot
|
||||
|
||||
Après Hello World, chaque habitant peut choisir gratuitement une propriété
|
||||
personnelle dans l'Overworld. Le centre est la position de ses pieds au moment
|
||||
de la confirmation. Le volume initial est un cube inclusif de 17 blocs de côté
|
||||
(rayon de 8 blocs), indépendant des chunks. Quatre extensions ajoutent chacune
|
||||
4 blocs de rayon dans les trois axes : côtés 25, 33, 41 et 49, pour 100, 200,
|
||||
300 puis 400 **points** d'XP Minecraft. Le centre ne change pas.
|
||||
Ce barème est une première hypothèse d'équilibrage, pas une décision définitive.
|
||||
|
||||
Une seule propriété par UUID, sans chevauchement en trois dimensions, y compris
|
||||
les extensions en attente de reprise. Le volume doit rester dans la hauteur
|
||||
constructible et la bordure du monde. Un cube de rayon 32 autour du spawn commun
|
||||
reste non revendicable. Aucun déplacement, abandon, transfert, invité, droit de
|
||||
faction ou remboursement n'est livré dans ce lot. Aucun objet spécial n'est
|
||||
nécessaire : Habitant → Propriété et `/sanctuary property` (alias `propriete` et
|
||||
`propriété`) ouvrent le menu ; `claim`, `expand` et `preview` sont aussi disponibles.
|
||||
L'aperçu affiche les coins exacts et des particules temporaires, sans poser de bloc.
|
||||
|
||||
Le titulaire peut modifier son volume. Les autres joueurs, y compris en Créatif,
|
||||
ne peuvent pas y miner, poser ou utiliser des blocs/objets. Les terrains libres
|
||||
gardent leurs règles ordinaires. Les contrôles portent aussi sur les cellules
|
||||
des actions groupées Sanctuary. La protection des dégâts environnementaux
|
||||
(explosions, pistons, fluides, feu, mobs), des entités et des commandes opérateur
|
||||
reste hors de ce premier lot et doit être annoncée dans l'interface.
|
||||
|
||||
Faction et Battle quittent le menu Pause et rejoignent Habitant. Le moteur de
|
||||
Battle reste disponible sans nouveau développement. Les deux anciennes places
|
||||
du menu Pause restent libres pour les futurs boutons.
|
||||
|
||||
## Sauvegarde et migration additive
|
||||
|
||||
Nouveau fichier `<monde>/sanctuary/properties-v1.json`, schéma 1, lié à la graine
|
||||
de l'Overworld : `schema`, `seed`, `claims` (UUID → centre et palier), `pending`
|
||||
(UUID → reçu et extension préparée). Aucun fichier existant ne change de format,
|
||||
aucun terrain n'est attribué au chargement, aucun chunk n'est régénéré. Les
|
||||
habitants existants ayant terminé Hello World ont le même droit gratuit à choisir
|
||||
un emplacement. Demeure reste une mesure d'habitation indépendante.
|
||||
|
||||
Écriture atomique avec contrôle de remplacement externe, lecture stricte et
|
||||
refus de schéma inconnu, conflit ou corruption. Une panne ferme les actions
|
||||
de propriété et la modification de blocs par les joueurs dans le monde concerné,
|
||||
sans réinitialiser le registre. Corriger/restaurer le fichier puis redémarrer.
|
||||
|
||||
Extension : préparer le journal, débiter l'XP, sauvegarder le joueur avec un
|
||||
reçu persistant `sanctuary:property_xp_receipt`, vérifier ce reçu sur disque,
|
||||
puis valider le nouveau volume. À la reconnexion, un reçu présent termine
|
||||
l'extension ; sinon la préparation est annulée et le volume initial conservé.
|
||||
Une reprise ne redébite jamais l'XP. Le journal réserve le volume agrandi jusqu'à
|
||||
la reprise du titulaire. Les requêtes portent le palier attendu pour refuser
|
||||
les répétitions et les écrans périmés.
|
||||
|
||||
## Validation
|
||||
|
||||
Le parcours client Vulkan / MoltenVK 1.4.2 sur Apple M1 passe en FR/EN,
|
||||
GUI 2/3/4, dans un nouveau monde de développement plat de graine **270** :
|
||||
accès depuis Habitant, absence des anciens boutons dans Pause, allers-retours
|
||||
Faction/Battle, confirmation de réservation, réservation par le réseau réel,
|
||||
puis achat d’une extension (150 → 50 points). Dix-neuf captures sont conservées
|
||||
dans `build/property270-screenshots/` ; les vues compactes FR ont été inspectées.
|
||||
|
||||
`check build assemblePack` réussit en **2 min 34 s**, avec les **41 GameTests serveur ciblés**
|
||||
(propriété, progression, minage, construction et cycles).
|
||||
Le banc de registre couvre 13 748 assertions, principalement les limites
|
||||
inclusives 3D, les volumes superposés, les conflits, le plafond, la reprise avec
|
||||
ou sans reçu, le refus des requêtes périmées et la conservation des fichiers
|
||||
invalides ou remplacés. Les GameTests utilisent deux joueurs serveur simulés
|
||||
pour les droits, le minage natif, la pose native, les outils, les interactions,
|
||||
les limites et le débit d’XP avec vérification du reçu natif sur disque.
|
||||
Aucun essai avec deux clients distants n’est revendiqué.
|
||||
|
||||
Commandes de validation :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=property270,progression,mining,building,cycle \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryProperty270ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journaux ignorés : `build/property270-check-build-pack-final.log` et
|
||||
`build/property270-client-vulkan.log`. Le pack local reste dans `build/packwiz`.
|
||||
Export importable vérifié : [Sanctuary-beta.270.mrpack](../build/Sanctuary-beta.270.mrpack)
|
||||
(intégrité ZIP, versions, dépendances et JAR Sanctuary/JEI identiques aux builds).
|
||||
Reçu local et empreinte du JAR : `build/property270-validation.json`.
|
||||
Aucune publication ni synchronisation d’instance personnelle n’a été effectuée.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Propriétés — refonte mise de côté, 24 octobre 2026
|
||||
|
||||
Retour du créateur après beta.273. Les propositions ci-dessous remplacent le
|
||||
cadrage futur ; elles ne sont pas annoncées comme des mécaniques implémentées.
|
||||
La fiche Habitant en deux colonnes et deux rangées est conservée.
|
||||
|
||||
## Retour et direction souhaitée
|
||||
|
||||
- Mettre de côté le chantier Propriété, jugé encore incertain.
|
||||
- Le rendu des claims sur la carte paraît superposé et insuffisamment net dans
|
||||
le style pixel art ; les zones adjacentes ne se raccordent pas comme souhaité.
|
||||
- S’appuyer sur **Demeure**, le système d’habitation existant. Avoir habité sur
|
||||
le terrain devrait être nécessaire pour pouvoir le revendiquer ; proposer
|
||||
la propriété à partir de cette habitation plutôt qu’un choix arbitraire de chunk.
|
||||
- Le premier habitant serait prioritaire. Lorsque deux habitants veulent le
|
||||
même endroit, envisager un départage par le système Battle existant ou par
|
||||
accord commun. La forme exacte du duel et du consentement reste à définir.
|
||||
- La propriété désigne un **lieu déclaré**. Les joueurs peuvent s’opposer et
|
||||
se faire la guerre de plusieurs façons ; ce n’est pas une interdiction générale
|
||||
d’agir sur le terrain.
|
||||
- Les **64 saphirs** de protection sont jugés trop chers. Aucun nouveau montant
|
||||
n’a été fixé : ne pas inventer un tarif de remplacement.
|
||||
- Le graphisme de protection prend trop de place. Direction souhaitée : une
|
||||
couleur unie, sans gros pictogrammes ni cadres par chunk, accompagnée d’une
|
||||
légende reliant la couleur au personnage qui a revendiqué le lieu.
|
||||
- Demeure doit lui aussi se lire comme un calque continu sans bordure interne,
|
||||
pour percevoir plus facilement les zones d’influence. Retirer « aucun claim
|
||||
ni protection » de son infobulle, devenu trompeur dans l’ensemble des systèmes.
|
||||
|
||||
## Questions de reprise
|
||||
|
||||
Le retour n’établit pas encore les seuils d’habitation, l’emprise d’un terrain,
|
||||
la preuve du premier occupant, les règles d’inactivité, le type de duel, ses
|
||||
conditions et son consentement, l’accord sans duel, le devenir des claims déjà
|
||||
achetés, ni le nouveau tarif. Tout changement de registre ou des droits acquis
|
||||
nécessitera son contrat de migration. Ne pas traiter une victoire au combat
|
||||
ou la présence Demeure comme un transfert automatique de propriété.
|
||||
|
||||
Le créateur a confirmé : **conserver les claims actuels accessibles**. La refonte
|
||||
des règles est mise en attente ; les claims, achats et protections du prototype
|
||||
restent disponibles. Aucune suppression des données existantes n’est autorisée.
|
||||
La passe beta.274 simplifie seulement leur affichage en calque pixel uniforme
|
||||
pour les protections, sans bordure ni bouclier, avec une légende couleur / propriétaire.
|
||||
|
||||
## Évolution d’accès — beta.275
|
||||
|
||||
Le créateur demande ensuite de réserver les propriétés aux joueurs ayant acheté
|
||||
et activé Carte. Le bouton Propriété quitte Habitant ; la commande et le clic
|
||||
droit de l’Atlas restent utilisables après ce déblocage. Les claims, leurs données
|
||||
et les protections acquises sont conservés ; le conseil de première arrivée est
|
||||
retiré. Cela ne met pas en œuvre l’habitation préalable, les duels ou un nouveau prix.
|
||||
|
||||
## Évolution d’affichage et de tarif — beta.276
|
||||
|
||||
Le créateur fixe maintenant le tarif : **8 saphirs par chunk pour trois jours de
|
||||
serveur en marche**. Il demande un contour CTM pour toutes les propriétés et
|
||||
un fond supplémentaire sur les chunks protégés. [Contrat de conservation des
|
||||
protections et reçus existants](claim-contours-beta276.md). L’habitation préalable
|
||||
et les duels restent en attente ; ce nouveau tarif remplace uniquement ce point
|
||||
jusqu’alors ouvert.
|
||||
|
||||
## Habitation et concurrence avant claim — beta.277
|
||||
|
||||
Le créateur reprend l’habitation préalable et confirme le seuil existant de
|
||||
Demeure. Il précise ensuite qu’un autre habitant doit pouvoir revendiquer s’il
|
||||
prend plus d’influence avant le claim du premier : la priorité reste donc
|
||||
contestable sur le terrain libre. [CLAIM-277](claim-demeure-beta277.md) retient
|
||||
l’influence effective et, à égalité, l’ordre des premières traces ; un claim
|
||||
acquis n’est pas transféré automatiquement. Les duels restent à définir.
|
||||
|
||||
## Duels de propriété consentis — beta.278
|
||||
|
||||
Le créateur demande un duel Battle avec l’accord des deux joueurs et confirme
|
||||
le périmètre : les propriétés déjà revendiquées. [CLAIM-278](claim-battle-beta278.md)
|
||||
relie un chunk précis au résultat d’un Battle privé à deux joueurs ; le gagnant
|
||||
reçoit ce claim et le temps de protection restant. Refus, annulation ou absence
|
||||
de vainqueur ne transfèrent rien. Les règles des terrains libres de beta.277
|
||||
restent indépendantes ; aucun duel n’attribue gratuitement un chunk encore libre.
|
||||
@@ -1,5 +1,25 @@
|
||||
# CLAIM-01 — Titres de propriété temporaires
|
||||
|
||||
[CLAIM-278](claim-battle-beta278.md) permet aux deux joueurs d’accepter un
|
||||
Battle dont le vainqueur devient propriétaire du claim annoncé.
|
||||
|
||||
[CLAIM-277](claim-demeure-beta277.md) relie les revendications au seuil
|
||||
d’habitation Demeure et à l’influence tant que le chunk reste libre.
|
||||
|
||||
[CLAIM-276](claim-contours-beta276.md) actualise le prototype : contours CTM pour
|
||||
toutes les propriétés, fond sur les claims protégés, **8 saphirs pour trois jours
|
||||
de serveur par chunk**. Les durées déjà payées sont conservées.
|
||||
|
||||
La [direction de refonte du 24 octobre](property-demeure-refonte.md) est mise en
|
||||
attente. Les claims existants restent accessibles ; habitation préalable, duels
|
||||
et nouveau tarif ne sont pas encore implémentés.
|
||||
|
||||
**Cadrage historique.** Le lot [CLAIM-03 / beta.272](chunk-claims-beta272.md)
|
||||
retient les claims par chunk depuis la carte, cinq gratuits puis un coût en XP,
|
||||
et une protection de sept jours de serveur pour 64 saphirs par chunk. Il remplace
|
||||
le premier modèle en volume [CLAIM-02 / beta.270](property-beta270.md). Les titres
|
||||
objets et autres intentions ci-dessous restent historiques.
|
||||
|
||||
**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
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# PROVIDENCE-265 — Prières et premiers miracles
|
||||
|
||||
Demande du 24 octobre 2026, branche `codex/providence-prayers`, livraison
|
||||
locale **beta.265**, Minecraft **26.3**, Fabric Loader **0.19.5** et
|
||||
Fabric API **0.160.5+26.3**. Aucune dépendance ajoutée.
|
||||
|
||||
## Intégration
|
||||
|
||||
Providence est un paquet autonome dans le mod Sanctuary existant. Son
|
||||
initialisation enregistre deux commandes et une session par serveur. Le
|
||||
vocabulaire, l'interpréteur, le moteur de décision et le journal utilisent
|
||||
du Java/Gson indépendant de Minecraft. Seuls la validation du monde,
|
||||
l'exécution et les réponses dépendent du jeu.
|
||||
|
||||
La tranche utilise les services actuels : contrat météo journalier,
|
||||
position de dernière mort sauvegardée nativement par Minecraft, écritures
|
||||
atomiques `AtomicJson`, commandes Fabric et ressources FR/EN. Aucun écouteur
|
||||
du chat général, LLM, appel HTTP ou service extérieur. Les tombes et les
|
||||
inventaires ne sont jamais interceptés par Providence.
|
||||
|
||||
## Jouer
|
||||
|
||||
```text
|
||||
/pray Ô seigneur, j'en ai marre de cet orage, faites revenir le soleil !
|
||||
/pray Pitié, ramenez-moi à l'endroit où je suis mort
|
||||
/providence readable true
|
||||
/providence readable false
|
||||
```
|
||||
|
||||
`/pray` est accessible sans opérateur, dans une partie solo comme sur un
|
||||
serveur dédié. La prière est limitée à 512 caractères. Elle reçoit une réponse
|
||||
privée courte, avec deux variantes par verdict. Le mode par défaut utilise
|
||||
la police `sanctuary:providence`, ressource du mod référençant la police SGA
|
||||
native `minecraft:alt`. Les textes SGA sont sans accents ; la lecture normale
|
||||
FR/EN conserve les accents et se choisit individuellement avec `readable`.
|
||||
Le choix persiste. Les variantes « entendue » sont préparées ; le parcours
|
||||
jouable répond directement par le verdict final.
|
||||
|
||||
Les prières espacées de moins de cinq secondes sont silencieuses, sans
|
||||
nouvelle recherche de destination ni nouvelle écriture. Une erreur de
|
||||
persistance ferme le moteur jusqu'à un rechargement valide ; un rechargement
|
||||
de configuration invalide conserve la session courante. Les données en erreur
|
||||
sont conservées.
|
||||
|
||||
## Compréhension symbolique
|
||||
|
||||
Au premier démarrage, le serveur copie deux fichiers embarqués vers :
|
||||
|
||||
- `config/sanctuary/providence/concepts.json` : concepts et synonymes, dont
|
||||
des expressions de plusieurs mots ; vocabulaire français et premiers
|
||||
synonymes anglais.
|
||||
- `config/sanctuary/providence/miracles.json` : motifs de reconnaissance,
|
||||
exclusions, conditions obligatoires, probabilités, cooldowns et rétention.
|
||||
|
||||
Après modification, un opérateur utilise `/providence reload`. Les deux
|
||||
fichiers sont validés avant le remplacement de la session courante. Un
|
||||
synonyme ajouté ne demande aucune recompilation. Les conditions de sécurité
|
||||
des exécuteurs ne peuvent pas être supprimées par les fichiers JSON.
|
||||
|
||||
La normalisation retire accents, casse et ponctuation ; la détection respecte
|
||||
les frontières des mots. Une demande doit commencer un segment, invoquer
|
||||
Providence ou comporter une expression directe comme « ramenez-moi ». Un
|
||||
constat tel que « le serveur arrête la pluie » reste sans effet. Chaque motif contient des groupes de concepts :
|
||||
tous les groupes sont requis, avec au moins un concept par groupe. Par
|
||||
exemple, demande + arrêt + pluie, ou demande + retour + soleil. Le score
|
||||
de couverture vaut le pourcentage de groupes satisfaits (0 à 100). Le seuil
|
||||
par défaut est 90 et un motif incomplet ne déclenche jamais d'effet. Ce score
|
||||
structurel n'est pas une probabilité de compréhension du langage.
|
||||
|
||||
Les indices ne sont pas assemblés entre phrases ou propositions séparées par
|
||||
une conjonction. Les négations, hypothèses et certaines formulations de récit
|
||||
bloquent la proposition concernée. Deux intentions complètes distinctes
|
||||
rendent la prière ambiguë et empêchent toute action. Un mot isolé comme
|
||||
« pluie », un récit de mort ou « J'aime cette pluie, ne l'arrêtez pas »
|
||||
n'exécute rien.
|
||||
|
||||
Ce moteur reconnaît un petit vocabulaire et des relations simples. Il ne
|
||||
résout pas la coréférence, le sarcasme, les citations imbriquées ou toutes
|
||||
les tournures françaises. Une prière correcte peut rester incomprise ; les
|
||||
formulations négatives et hypothétiques sont volontairement conservatrices.
|
||||
|
||||
## Miracles et décisions
|
||||
|
||||
| Miracle | Politique initiale | Conditions et effet |
|
||||
| --- | --- | --- |
|
||||
| `CLEAR_WEATHER` | 35 %, individuel 30 min, global 10 min | Joueur vivant, Overworld avec météo et temps non clair. Profil Sanctuary changé en `CLEAR` jusqu'au prochain jour civil ; prévision normale reprise le lendemain. En mode météo vanilla, éclaircie de 600 secondes configurable. |
|
||||
| `RETURN_TO_DEATH` | 50 %, individuel 15 min, aucun cooldown global | Joueur vivant, dernière mort connue dans la dimension courante, arrivée sûre dans les chunks déjà chargés. Téléportation native et distance de chute remise à zéro. |
|
||||
| `RECOVER_ITEMS` | Désactivé, activation JSON refusée | Intention reconnue, aucune lecture ou restitution des objets. Contrat anti-duplication encore à concevoir autour des tombes. |
|
||||
| `RESCUE_PLAYER` | Désactivé, activation JSON refusée | Intention reconnue, aucune extraction. Les critères de situation bloquée et de destination restent à valider. |
|
||||
|
||||
Aucun coût d'XP ou d'objet dans cette tranche. Les probabilités, délais et
|
||||
activation des deux pouvoirs validés sont configurables. Les délais utilisent
|
||||
l'heure UTC en millisecondes du serveur ; un recul de son horloge prolonge
|
||||
l'attente. Un refus probabiliste consomme les mêmes cooldowns qu'un accord,
|
||||
afin de ne pas relancer le tirage par répétition ou reconnexion. Un refus
|
||||
de condition, une intention inconnue ou un pouvoir désactivé ne consomme que
|
||||
la courte limite de fréquence des prières.
|
||||
|
||||
Le retour cherche le point sûr le plus proche dans une boîte de ±8 blocs
|
||||
horizontalement et ±6 verticalement autour de la dernière mort. Le sol doit
|
||||
être solide, le joueur debout doit tenir dans le volume libre, et les cases voisines
|
||||
doivent être chargées et dans la bordure. Fluides, lave, feu, cactus, magma,
|
||||
neige poudreuse, plantes blessantes et portails sont refusés. Aucun bloc
|
||||
n'est créé ou détruit, aucun chunk n'est chargé ou généré par cette recherche.
|
||||
Une mort dans le vide, dans une autre dimension ou trop loin des chunks
|
||||
chargés reste donc sans retour. Une arrivée sûre ne garantit pas l'absence
|
||||
d'ennemis ou de dangers futurs. Le joueur endormi, mort, spectateur ou passager
|
||||
est inéligible.
|
||||
|
||||
## Nouveau contrat de persistance isolé
|
||||
|
||||
`<monde>/data/sanctuary-providence.json`, schéma 1, contient uniquement les
|
||||
prières explicites admises par la limite de fréquence, leurs UUID, intentions,
|
||||
scores, ambiguïtés, concepts, conditions examinées, décisions et résultats,
|
||||
ainsi que les échéances et préférences de lecture. Il n'enregistre pas les
|
||||
autres messages du chat ou les messages privés entre joueurs.
|
||||
|
||||
Par défaut, les 500 dernières entrées au maximum sont conservées pendant
|
||||
14 jours. Le nettoyage intervient au chargement, à chaque décision et une
|
||||
fois par minute pendant que le serveur tourne. `historyEntries: 0` désactive
|
||||
la conservation du texte des prières tout en conservant les protections
|
||||
contre les répétitions. Les cooldowns non expirés restent indépendants de la
|
||||
rétention ; les échéances expirées sont éliminées. Les fichiers sont bornés
|
||||
(configurations 256 Kio chacune, état 8 Mio) et remplacés atomiquement.
|
||||
|
||||
Avant tout effet, la décision `PENDING` et ses cooldowns sont enregistrés
|
||||
(avec la conservation du texte désactivée, seules les réservations persistent).
|
||||
L'effet réussi devient `GRANTED` avec `executed: true`. Une interruption
|
||||
entre ces étapes laisse `PENDING` et `executed: false`, résultat incertain
|
||||
à examiner manuellement ; aucune exécution n'est rejouée au redémarrage.
|
||||
Il n'existe pas de transaction atomique entre les fichiers du monde Minecraft
|
||||
et le journal : une panne au milieu d'un effet peut donc laisser ce résultat
|
||||
incertain. Les refus et échecs ne sont jamais annoncés comme exécutés.
|
||||
|
||||
Contrat additif : aucune migration de fichier existant, changement de
|
||||
génération, expansion, réécriture de chunk ou manipulation de sauvegarde
|
||||
personnelle. La météo utilise son contrat existant sans en changer le schéma.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`check build assemblePack` réussit en **2 min 50 s**, sur Java 25.0.3,
|
||||
avec les **15 GameTests serveur requis réussis** (Providence, tombes et
|
||||
progression) et **67 vérifications Providence indépendantes de Minecraft**.
|
||||
Le parcours serveur couvre notamment les vraies commandes, le beau temps
|
||||
persistant, le cooldown global après rechargement, un rechargement JSON refusé
|
||||
sans perte de session, le choix d'accessibilité persistant, la position native
|
||||
après mort et sauvegarde/relecture, la téléportation réelle, les arrivées
|
||||
non chargées, la lave, le plafond, la pose rampante, le cactus et la restitution
|
||||
d'objets désactivée. Le journal de l'essai contient une erreur de rechargement
|
||||
JSON attendue, injectée pour vérifier la conservation de la session.
|
||||
|
||||
Les tests indépendants comprennent synonymes rechargés sans compilation,
|
||||
formulations familières FR/EN, récits sans demande, négations, hypothèses,
|
||||
ambiguïtés, refus probabiliste, conditions manquantes, pouvoirs désactivés,
|
||||
limite de fréquence, cooldowns individuels/globaux après redémarrage,
|
||||
interruption d'exécution, rétention par âge et par nombre, journal textuel
|
||||
désactivé, isolation des mondes, taille maximale, corruption et conflit
|
||||
avec une modification extérieure du fichier.
|
||||
|
||||
Le parcours client réussit en **1 min 1 s**, sous **Vulkan / MoltenVK 1.4.2,
|
||||
Apple M1**, avec 8 Gio maximum. Un `/pray` envoyé par la vraie connexion de jeu
|
||||
arrête l'orage et retourne une réponse SGA ; la commande d'accessibilité
|
||||
retourne ensuite une réponse en lecture normale. Les huit verdicts principaux
|
||||
sont rendus dans le chat natif en FR/EN, GUI 2/3/4, en SGA et en lecture normale.
|
||||
Les **12 captures natives** ont été inspectées : alphabet, ponctuation,
|
||||
accents et texte contenu dans le chat. Monde de développement neuf, graine 265.
|
||||
|
||||
Journaux et captures ignorés :
|
||||
`build/providence265-check-build-pack-final.log`,
|
||||
`build/providence265-client-vulkan.log`, `build/providence265-screenshots/`.
|
||||
Aucun essai OpenGL. Les essais concernent le serveur local et le client macOS ;
|
||||
aucune connexion distante multijoueur ni validation Windows n'est revendiquée.
|
||||
Aucune publication, avancée du canal packwiz ou installation personnelle.
|
||||
|
||||
Le [MRpack local beta.265](../build/Sanctuary-beta.265.mrpack) est exporté et
|
||||
vérifié : intégrité ZIP, version, Minecraft 26.3/Fabric exacts, un seul JAR
|
||||
Sanctuary identique au build, classes compilées et ressources Providence/FR/EN
|
||||
identiques aux sources, classes de test absentes. Par rapport à beta.264,
|
||||
les seules classes existantes modifiées sont `SanctuaryMod` et `WeatherService` ;
|
||||
le reste du nouveau code est dans `providence`. Empreintes et reçu :
|
||||
`build/providence265-validation.json`.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=providence265,graves,progression -PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryProvidence265ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# PROVIDENCE-266 — Couleurs des réponses SGA
|
||||
|
||||
Demande du 24 octobre 2026, branche `codex/providence-prayers`, livraison locale
|
||||
**beta.266**, Minecraft **26.3**.
|
||||
|
||||
Les réponses de Providence affichées en SGA utilisent les couleurs natives de
|
||||
Minecraft selon le verdict, pour les deux variantes et les traductions FR/EN :
|
||||
|
||||
| Couleur | Réponses |
|
||||
| --- | --- |
|
||||
| Vert | Miracle accordé ; confirmation de rechargement si affichée en SGA. |
|
||||
| Jaune | Prière entendue, cooldown, silence, prière incomprise ou ambiguë. |
|
||||
| Rouge | Refus, conditions non remplies, pouvoir désactivé, échec d'exécution ou erreur de configuration. |
|
||||
|
||||
Le jaune invite à attendre ou à reformuler. Il n'ajoute pas de file d'exécution
|
||||
automatique. Les messages d'aide et de changement de lecture restent neutres.
|
||||
Le mode de lecture normale conserve son affichage actuel ; la demande porte
|
||||
sur le SGA. La police et les textes des verdicts sont conservés.
|
||||
|
||||
La modification est limitée à `ProvidenceMessages` et aux métadonnées de
|
||||
livraison. Les règles de décision et la persistance restent celles de
|
||||
[Providence beta.265](providence-beta265.md).
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours existant Providence réussit sous **Vulkan / MoltenVK 1.4.2,
|
||||
Apple M1**, en **55 secondes**, avec 8 Gio maximum. Il couvre une vraie prière
|
||||
transmise au serveur, les traductions FR/EN et les échelles GUI 2/3/4, en SGA
|
||||
et en lecture normale. Douze captures sont conservées dans
|
||||
`build/providence266-screenshots/` ; les vues SGA FR GUI 2/4 et EN GUI 3 ont
|
||||
été inspectées, avec les trois couleurs et la police conservée.
|
||||
Journal : `build/providence266-client-vulkan.log`.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryProvidence265ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=providence265,graves,progression -PsanctuaryAtlasOnly=true
|
||||
```
|
||||
|
||||
`check build assemblePack` réussit en **2 min 38 s**, avec les **15 GameTests
|
||||
serveur requis** et les **67 vérifications indépendantes** de Providence.
|
||||
Journal : `build/providence266-check-build-pack.log`.
|
||||
|
||||
Le [pack local beta.266](../build/Sanctuary-beta.266.mrpack) est exporté et
|
||||
vérifié : intégrité ZIP, version, Minecraft 26.3, JAR embarqué identique au
|
||||
build et classes de test absentes. La seule classe modifiée par rapport au
|
||||
JAR beta.265 est `ProvidenceMessages`. Reçu et empreintes :
|
||||
`build/providence266-validation.json`. Aucune publication ni mise à jour
|
||||
d'une installation personnelle.
|
||||
@@ -0,0 +1,103 @@
|
||||
# PROVIDENCE-267 — Attendre la réponse de Providence
|
||||
|
||||
Demande du 24 octobre 2026, branche `codex/providence-prayers`, livraison locale
|
||||
**beta.267**, Minecraft **26.3**.
|
||||
|
||||
Après une prière admise par `/pray`, Providence attend **5 secondes** avant
|
||||
d'interpréter la demande, d'examiner le monde, de prendre sa décision et
|
||||
d'accomplir un éventuel miracle. Aucun verdict ou effet immédiat. Les réponses
|
||||
conservent les couleurs de beta.266 : vert accordé, jaune attente ou
|
||||
clarification, rouge refus ou échec.
|
||||
|
||||
Une seule prière peut attendre par joueur. Les répétitions pendant ce délai
|
||||
sont ignorées : elles ne remplacent pas les mots initiaux, ne raccourcissent
|
||||
pas l'attente et ne déclenchent pas un second tirage. Les autres joueurs
|
||||
attendent indépendamment. Les cooldowns persistants des miracles et la limite
|
||||
de fréquence des prières sont conservés ; cette dernière repart du traitement
|
||||
de la réponse. Le résultat de la commande `/pray` indique désormais l'admission
|
||||
dans la file, et non l'exécution immédiate d'un miracle.
|
||||
|
||||
## Configuration
|
||||
|
||||
Ajouter ou modifier dans `config/sanctuary/providence/miracles.json` :
|
||||
|
||||
```json
|
||||
"responseDelaySeconds": 5
|
||||
```
|
||||
|
||||
Valeur entière entre **1 et 60 secondes**, appliquée aux nouvelles prières
|
||||
après `/providence reload`. Les anciennes configurations beta.265/266 sans
|
||||
cette clé utilisent cinq secondes automatiquement ; leurs autres valeurs et
|
||||
leurs fichiers restent conservés. Le champ est fourni dans les configurations
|
||||
neuves, sans changer le schéma existant.
|
||||
|
||||
Une seconde correspond à 20 ticks du monde serveur. Le délai peut donc durer
|
||||
plus longtemps si le serveur ralentit ou si la partie solo est mise en pause.
|
||||
Le rechargement de la configuration conserve les prières déjà en attente et
|
||||
leur échéance initiale. Leur interprétation et les règles utilisées lors de la
|
||||
décision sont celles de la configuration alors active.
|
||||
|
||||
## Cycle de vie et sécurité
|
||||
|
||||
L'attente est traitée dans les ticks serveur, sans bloquer de thread. La file
|
||||
en mémoire est bornée à 1 024 prières et ne survit pas à l'arrêt du serveur.
|
||||
Une déconnexion ou une réapparition annule la demande en attente ; elle n'est
|
||||
pas remise à un nouvel objet joueur. Les joueurs déconnectés ou retirés sont
|
||||
aussi vérifiés avant tout traitement.
|
||||
|
||||
Les conditions sont évaluées à l'échéance, puis revérifiées juste avant l'effet
|
||||
par l'exécuteur existant. Une dernière mort ou une destination ayant changé
|
||||
pendant l'attente est donc examinée dans son état actuel. Les contrôles d'aide,
|
||||
de lecture normale et de rechargement restent immédiats.
|
||||
|
||||
Les prières sont journalisées quand elles sont traitées. Une attente annulée
|
||||
avant traitement ne crée pas de décision dans l'historique. La réservation
|
||||
`PENDING` avant les effets et les règles de non-rejeu de beta.265 sont
|
||||
conservées. Aucun changement du format des sauvegardes, de la génération ou
|
||||
des tombes ; aucune installation personnelle utilisée pour les essais.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le contrôle client Vulkan réussit en **47 secondes**, sous **MoltenVK 1.4.2 /
|
||||
Apple M1**, avec 8 Gio maximum, dans un monde de développement neuf graine 265.
|
||||
Une vraie commande réseau n'affiche aucune réponse immédiate ; le serveur
|
||||
conserve la pluie pendant l'attente. Un second `/pray bonjour` ne remplace pas
|
||||
la demande originale. Le miracle et une unique réponse verte arrivent après
|
||||
**101 ticks serveur** depuis l'envoi, soit environ cinq secondes.
|
||||
|
||||
Le parcours FR/EN GUI 2/3/4, en SGA et lecture normale, est conservé. Treize
|
||||
captures natives : `build/providence267-screenshots/`. La vue d'attente et les
|
||||
réponses SGA FR GUI 2 / EN GUI 4 ont été inspectées. Journal :
|
||||
`build/providence267-client-vulkan.log`, marqueur `PROVIDENCE267_DELAY_PASS`.
|
||||
|
||||
La suite serveur utilise un délai de test configurable d'une seconde. Elle
|
||||
vérifie l'absence d'effet et de décision anticipée, les répétitions ignorées,
|
||||
l'échéance conservée après rechargement, les cooldowns existants, les refus
|
||||
différés, la téléportation sûre, la dernière mort modifiée pendant l'attente
|
||||
et l'absence de traitement d'un joueur déconnecté. Le format des fichiers de
|
||||
configuration beta.265/266 est aussi relu sans réécriture dans les tests purs.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=providence265,graves,progression -PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryProvidence265ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Les essais couvrent le serveur local et le client macOS Vulkan. Aucun essai
|
||||
distant multijoueur ou Windows, aucune publication ou mise à jour personnelle.
|
||||
|
||||
`check build assemblePack` réussit en **2 min 43 s**, avec les **15 GameTests
|
||||
serveur requis** et **83 vérifications indépendantes** de Providence.
|
||||
Journal : `build/providence267-check-build-pack.log`.
|
||||
|
||||
Le [pack local beta.267](../build/Sanctuary-beta.267.mrpack) est exporté et
|
||||
vérifié : intégrité ZIP, version et Minecraft 26.3, JAR embarqué identique au
|
||||
build, configuration et classes Providence identiques aux sorties compilées,
|
||||
classes de test absentes. Les classes existantes modifiées depuis beta.266
|
||||
se limitent aux sources `MiracleRegistry` et `ProvidenceService`, classes
|
||||
internes comprises ; la file `PrayerQueue` est nouvelle. Reçu :
|
||||
`build/providence267-validation.json`.
|
||||
@@ -0,0 +1,190 @@
|
||||
# PROVIDENCE-268 — Les quatorze prières
|
||||
|
||||
Demande du 24 octobre 2026, branche `codex/providence-prayers`, livraison locale
|
||||
**beta.268**, Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
|
||||
**0.160.5+26.3** et Java 25.
|
||||
|
||||
Le catalogue compte désormais quatorze miracles actifs. `/pray <texte libre>`
|
||||
garde son interprétation symbolique locale, ses probabilités, ses conditions
|
||||
serveur et ses cooldowns persistants. La réponse et l’effet attendent toujours
|
||||
**cinq secondes**, réglables entre 1 et 60 secondes. Une seule demande attend
|
||||
par joueur. Le SGA reste **vert** si le miracle est accordé, **jaune** pour
|
||||
une attente ou une clarification, **rouge** pour un refus ou un échec.
|
||||
`/providence readable true` conserve la lecture normale FR/EN.
|
||||
|
||||
## Catalogue et réglages initiaux
|
||||
|
||||
Les durées ci-dessous sont les cooldowns individuels après un tirage admis,
|
||||
qu’il soit favorable ou refusé. Une condition impossible ne consomme pas ce
|
||||
tirage. Soleil et Pluie ont chacun un cooldown global supplémentaire de dix
|
||||
minutes. Les réglages restent modifiables dans
|
||||
`config/sanctuary/providence/miracles.json`, puis `/providence reload`.
|
||||
|
||||
| Nº | Prière | Exemple après `/pray` | Effet accordé | Chance / cooldown |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 01 | Récupération | Rendez-moi mon stuff, je suis mort | Transfère les objets restants de la dernière tombe admissible | 15 % / 60 min |
|
||||
| 02 | Retour | Ramenez-moi à ma tombe | Arrivée sûre près de la dernière mort, dans la même dimension | 50 % / 15 min |
|
||||
| 03 | Secours | Ramenez-moi au spawn | Arrivée sûre près du spawn global du serveur | 50 % / 15 min |
|
||||
| 04 | Soleil | Arrêtez la pluie | Arrête pluie et orage | 35 % / 30 min |
|
||||
| 05 | Pluie | Faites pleuvoir | Déclenche la pluie | 35 % / 30 min |
|
||||
| 06 | Guérison | Guéris-moi | Restaure les PV jusqu’au maximum acquis | 50 % / 5 min |
|
||||
| 07 | Nourriture | Nourris-moi | Restaure faim et saturation jusqu’à la capacité acquise | 50 % / 5 min |
|
||||
| 08 | Foudre | Foudroie @Pseudo | Éclair visuel et cinq points de dégâts natifs sur la cible | 25 % / 15 min |
|
||||
| 09 | Malédiction | Maudis @Pseudo | Effet négatif aléatoire pendant vingt secondes | 25 % / 15 min |
|
||||
| 10 | Vengeance | Punis @Pseudo | Punition aléatoire sur la cible | 25 % / 15 min |
|
||||
| 11 | Vol | Vole un objet à @Pseudo | Transfère une unité d’un objet aléatoire vers le demandeur | 25 % / 15 min |
|
||||
| 12 | Désarmement | Désarme @Pseudo | Fait tomber la pile réellement tenue en main principale | 35 % / 10 min |
|
||||
| 13 | Téléportation | Téléporte-moi près de @Pseudo | Arrivée sûre près du joueur, y compris dans une autre dimension | 50 % / 5 min |
|
||||
| 14 | Réparation | Répare mon outil | Répare l’objet endommagé tenu en main principale | 50 % / 5 min |
|
||||
|
||||
Les noms seuls des prières et les mots isolés ne sont pas des commandes
|
||||
impératives. Une négation, une hypothèse ou plusieurs intentions complètes
|
||||
empêchent un effet. Les nouveaux concepts et synonymes FR/EN sont déclaratifs.
|
||||
Guérison, Nourriture et Réparation visent le demandeur ; elles refusent si
|
||||
le besoin correspondant n’existe pas. Secours vise le spawn global, pas le lit.
|
||||
|
||||
## Joueurs visés et effets
|
||||
|
||||
Conformément au choix du créateur, les cinq prières hostiles peuvent viser
|
||||
**tous les joueurs connectés**, sans accord préalable ni exigence de PvP
|
||||
activé. La Téléportation utilise le même choix explicite de destinataire.
|
||||
`@Pseudo` sélectionne un nom complet sans distinction de casse. Un nom complet
|
||||
sans `@` est aussi reconnu ; `@` évite les collisions avec les mots usuels.
|
||||
Un nom inconnu, plusieurs cibles ou un alias partagé ne choisissent jamais
|
||||
un joueur au hasard. Les mentions sont masquées avant la reconnaissance
|
||||
pour qu’un pseudo comme « Pluie » ne change pas l’intention.
|
||||
|
||||
L’UUID est retenu dès la demande, puis la présence de ce même joueur et les
|
||||
conditions réelles sont revérifiées à l’échéance. Un autre compte arrivé sous
|
||||
le même nom ne reçoit pas le miracle. La cible doit encore être vivante et
|
||||
connectée. Une mention explicite sur une prière personnelle est refusée.
|
||||
|
||||
Foudre envoie aux clients proches l’éclair natif et applique les dégâts côté
|
||||
serveur à la seule cible. Aucun éclair n’est ajouté à la simulation serveur :
|
||||
le décor, le cuivre, les paratonnerres et les voisins restent hors de cet effet.
|
||||
Les protections et immunités natives aux dégâts restent applicables.
|
||||
Malédiction tire entre lenteur, faiblesse, fatigue de minage, nausée et faim,
|
||||
niveau I. Vengeance tire entre cet éclair, cette malédiction, ou quatre points
|
||||
de dégâts magiques accompagnés du retrait de quatre points de faim.
|
||||
|
||||
Vol sélectionne une pile admissible dans les rangées de stockage actuellement
|
||||
accessibles, hotbar comprise, puis transfère **une unité**, composants conservés.
|
||||
Équipement porté, main secondaire et stockage verrouillé ne sont pas parcourus.
|
||||
Une destination pleine ou un vol sur soi-même est refusé. Désarmement dépose
|
||||
la pile tenue, sans la recopier, avec le délai natif avant ramassage. Un objet
|
||||
endommagé conserve ses composants et son usure pendant ces transferts.
|
||||
|
||||
Les arrivées cherchent un sol, la place debout et l’absence de danger dans
|
||||
un rayon borné de huit blocs et six blocs verticaux. Elles utilisent uniquement
|
||||
les chunks déjà chargés, respectent la frontière et remettent la chute à zéro.
|
||||
Une destination non disponible entraîne un refus. Soleil et Pluie modifient
|
||||
le profil météo courant jusqu’au prochain minuit civil ; sans contrôleur météo
|
||||
actif, la durée native configurée vaut dix minutes par défaut.
|
||||
|
||||
## Récupération et contrat de sauvegarde additif
|
||||
|
||||
Récupération transfère **les objets encore présents**, sans restaurer une copie
|
||||
de l’inventaire de mort. Elle exige le propriétaire, la position et la dimension
|
||||
de la dernière mort natives, ainsi que le numéro de décès de la tombe.
|
||||
Elle recherche les tombes placées dans les neuf chunks déjà chargés autour de
|
||||
cette mort, les têtes flottantes à proximité (24 blocs horizontaux), et les
|
||||
propres têtes de tombe portées dans le stockage accessible du demandeur.
|
||||
Elle ne charge aucun chunk et n’ouvre pas l’inventaire d’un autre joueur.
|
||||
Les objets déjà retirés ne sont pas recréés. Il faut assez de place pour tout
|
||||
le contenu admissible ; sinon aucun transfert ni scellement n’a lieu.
|
||||
|
||||
Un nouveau registre distinct, `<monde>/data/sanctuary-providence-recoveries.json`,
|
||||
**schéma 1**, conserve la correspondance entre UUID de tombe consommée et UUID
|
||||
du bénéficiaire. Son absence signifie qu’aucune récupération Providence n’a
|
||||
encore été scellée. Il est créé au premier transfert, par remplacement atomique,
|
||||
avant la mutation des objets. Il est borné à 8 Mio et 100 000 tombes : à capacité
|
||||
atteinte, les nouvelles récupérations sont refusées. Les sceaux ne sont jamais
|
||||
purgés, rejoués ou retirés automatiquement.
|
||||
|
||||
Ce fichier doit être conservé avec les sauvegardes du monde. Le format des
|
||||
joueurs et des tombes reste inchangé ; aucune conversion ni réécriture générale
|
||||
d’un monde n’est nécessaire. Les interfaces et drops d’une tombe scellée
|
||||
masquent tout ancien contenu, y compris après rechargement d’un vieux chunk
|
||||
ou d’une ancienne copie de tête. Une corruption du registre bloque l’ouverture
|
||||
des tombes plutôt que d’effacer le registre.
|
||||
Les menus de la tombe placée partagent leur conteneur réel ; le propre menu
|
||||
de tombe du bénéficiaire est fermé avant son transfert.
|
||||
|
||||
Le transfert est **au plus une fois**, sans transaction atomique entre tous
|
||||
les fichiers Minecraft. Une interruption après le scellement mais avant la
|
||||
sauvegarde du bénéficiaire peut perdre la restitution : le système ne rejoue
|
||||
pas l’opération et conserve le sceau contre la duplication. Le journal des
|
||||
prières peut alors garder `PENDING`, avec `executed: false`, résultat incertain
|
||||
à examiner manuellement. Cette limite complète le contrat de beta.265.
|
||||
|
||||
## Configurations existantes
|
||||
|
||||
Le chargement ajoute en mémoire les concepts, les nouveaux synonymes des
|
||||
concepts REQUEST/RAIN/TRAVEL et les dix définitions absentes
|
||||
d’une ancienne configuration à quatre prières. Les deux anciens emplacements
|
||||
Récupération/Secours portant exactement `conditions: ["safety_unvalidated"]`
|
||||
reçoivent leurs nouvelles règles, probabilités et activation validées ; leurs
|
||||
cooldowns sont conservés. Une définition déjà valide explicitement désactivée
|
||||
reste désactivée. Les probabilités, cooldowns et dictionnaires existants sont
|
||||
conservés ; aucun fichier édité n’est réécrit par cette mise à niveau.
|
||||
|
||||
Les identifiants des quatre premiers miracles restent stables. Le journal
|
||||
`sanctuary-providence.json` conserve son schéma et sa rétention. Aucun changement
|
||||
de génération, de graine, d’expansion ou de monde personnel.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`check build assemblePack` réussit en **2 min 57 s**, avec les **20 GameTests
|
||||
serveur requis réussis** et les contrôles purs ci-dessous.
|
||||
|
||||
Le parcours client **Vulkan / MoltenVK 1.4.2 / Apple M1** réussit en **1 min 33 s**,
|
||||
avec 8 Gio maximum et un monde de développement neuf, graine 265. La vraie
|
||||
commande réseau météo attend **102 ticks serveur** avant le miracle et sa
|
||||
réponse unique. Une prière ciblée envoyée par la connexion de jeu produit aussi
|
||||
un éclair natif visible après cinq secondes, avec dégâts vérifiés sur le serveur
|
||||
et absence d’éclair dans sa simulation. Les réponses SGA et la lecture normale
|
||||
sont parcourues en FR/EN, GUI 2/3/4. Quatorze captures sont conservées ; les vues
|
||||
d’attente, SGA FR GUI 2, SGA/lecture normale EN GUI 4 et l’éclair ont été inspectés.
|
||||
|
||||
Les contrôles indépendants de Minecraft comptent **146 vérifications** :
|
||||
quatorze définitions, formulations FR/EN, négations, ambiguïtés, mots isolés,
|
||||
ciblage exact sans préfixe ni substitution, anciens dictionnaires/configurations,
|
||||
probabilités, cooldowns, journaux, corruption, écritures concurrentes, sceaux
|
||||
durables et non-rejeu. Les anciens fichiers personnalisés sont comparés avant
|
||||
et après chargement.
|
||||
|
||||
Les tests serveur couvrent les effets des quatorze miracles, les capacités
|
||||
acquises de PV/faim/stockage, le ciblage avec PvP désactivé, les transferts réels,
|
||||
le stockage verrouillé, les tombes placées/portées/flottantes, l’inventaire plein,
|
||||
les copies de tombe obsolètes après relecture des sceaux, les arrivées sûres,
|
||||
les commandes différées et l’absence de substitution d’UUID après départ.
|
||||
Le rechargement d’un JSON volontairement invalide écrit une erreur attendue
|
||||
pour vérifier que la session valide est conservée.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=providence265,providence268,graves,progression \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryProvidence265ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Les essais utilisent des joueurs simulés côté serveur et une connexion réelle
|
||||
locale côté client ; ils ne constituent pas un essai avec deux clients distants
|
||||
ni une validation Windows. Aucun déploiement personnel ou publication.
|
||||
Journaux et captures ignorés : `build/providence268-check-build-pack-final.log`,
|
||||
`build/providence268-client-vulkan.log`, `build/providence268-screenshots/`.
|
||||
|
||||
Le [pack local beta.268](../build/Sanctuary-beta.268.mrpack) est exporté et
|
||||
vérifié : ZIP intègre, version et dépendances exactes, un seul JAR Sanctuary
|
||||
embarqué identique au build, 33 classes Providence identiques aux sorties
|
||||
compilées, dictionnaires/règles/police/libellés identiques aux sources et
|
||||
absence de classes de test. Les seules classes existantes modifiées depuis
|
||||
beta.267 proviennent de `GraveService`, `WeatherService`, `MiracleExecutor`,
|
||||
`MiracleRegistry`, `MiracleValidator`, `PrayerInterpreter`, `ProvidenceEngine`
|
||||
et `ProvidenceService`, classes internes comprises. Le registre de sceaux,
|
||||
les transferts d’inventaire et le ciblage sont de nouveaux composants.
|
||||
Empreintes et reçu : `build/providence268-validation.json`.
|
||||
@@ -0,0 +1,175 @@
|
||||
# PROVIDENCE-269 — Six issues et leurs couleurs
|
||||
|
||||
Demande du 24 octobre 2026, branche `codex/providence-prayers`, livraison locale
|
||||
**beta.269**, Minecraft **26.3**, Java 25 et Fabric Loader **0.19.5**.
|
||||
|
||||
Providence dispose de six issues. Les couleurs sont appliquées au **SGA et à
|
||||
la lecture normale FR/EN**. Le silence ne produit aucun message, même un texte
|
||||
annonçant que Providence serait silencieuse.
|
||||
|
||||
| Verdict | Couleur | Comportement |
|
||||
| --- | --- | --- |
|
||||
| ✧ Exaucée | Vert `#55FF55` | Le miracle s’exécute juste avant la réponse favorable |
|
||||
| ◇ Différée | Jaune `#FFFF55` | Le serveur annonce l’attente et programme réellement le miracle plus tard |
|
||||
| ✕ Refusée | Rouge `#FF5555` | Aucun miracle n’est accordé |
|
||||
| ? Incomprise | Gris `#AAAAAA` | La demande n’a pas d’intention reconnue unique |
|
||||
| ○ Silence | Aucun affichage | Aucun message visible et aucun miracle ; la décision reste journalisée |
|
||||
| ⌁ Alternative | Violet `#FF55FF` | Une autre aide admissible est réellement accordée au demandeur |
|
||||
|
||||
L’attente initiale de **cinq secondes** de beta.267 est conservée. « Exaucée
|
||||
immédiatement » signifie donc au moment de la réponse, après cette attente.
|
||||
« Différée » ajoute par défaut **trente secondes après la réponse jaune**.
|
||||
Une réponse verte est ensuite envoyée si l’effet aboutit. Une condition devenue
|
||||
impossible produit un refus rouge à cette échéance. Les refus pour cooldown,
|
||||
pouvoir désactivé ou échec sont rouges ; ils ne programment aucun effet futur.
|
||||
Une ambiguïté de formulation rejoint le gris de la demande incomprise.
|
||||
|
||||
## Politique configurable
|
||||
|
||||
Le nouveau fichier `config/sanctuary/providence/responses.json` est créé au
|
||||
premier chargement, sans réécrire `miracles.json`, `concepts.json` ou un fichier
|
||||
`responses.json` existant. `/providence reload` recharge les trois fichiers.
|
||||
Un fichier invalide refuse le rechargement et conserve la session valide.
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": 1,
|
||||
"deferredChance": 0.20,
|
||||
"alternativeChance": 0.25,
|
||||
"silenceChance": 0.15,
|
||||
"deferredSeconds": 30,
|
||||
"alternatives": {
|
||||
"RECOVER_ITEMS": ["RETURN_TO_DEATH", "RESCUE_PLAYER"],
|
||||
"HEAL": ["FEED"],
|
||||
"FEED": ["HEAL"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cet extrait est valide ; le fichier distribué contient les quatorze intentions.
|
||||
Les probabilités sont comprises entre zéro et un, et la somme
|
||||
`alternativeChance + silenceChance` doit rester inférieure ou égale à un.
|
||||
`deferredSeconds` est un entier entre **1 et 3 600 secondes**. Chaque seconde
|
||||
correspond à vingt ticks serveur : pause et ralentissements allongent l’attente.
|
||||
Le réglage indépendant `responseDelaySeconds` de `miracles.json` reste entre
|
||||
une et soixante secondes.
|
||||
|
||||
La chance propre du miracle demandé est examinée en premier. Parmi les
|
||||
miracles accordés, 20 % sont différés par défaut, les autres sont exaucés au
|
||||
moment de la réponse. Pour une décision défavorable, un nouveau tirage alloue
|
||||
15 % au silence et 25 % à une tentative d’alternative. Une alternative n’est
|
||||
possible que pour une intention reconnue, active et hors cooldown ; une
|
||||
intention sans besoin actuel peut aussi recevoir une autre aide. Les demandes
|
||||
inconnues, ambiguës, désactivées ou en cooldown ne reçoivent pas d’alternative.
|
||||
Elles peuvent rester silencieuses selon le même réglage.
|
||||
|
||||
L’alternative choisit la première aide configurée qui est active, admissible
|
||||
et hors cooldown. Elle conserve **sa propre probabilité** : un tirage refusé
|
||||
ne déclenche pas de recherche successive d’un autre miracle. Les cooldowns
|
||||
du miracle demandé et de cette autre aide sont réservés avant son tirage,
|
||||
même si celui-ci refuse l’aide. Une autre formulation ou une autre intention
|
||||
ne permet donc pas de contourner le cooldown de cette aide.
|
||||
|
||||
Seules Guérison, Nourriture, Secours, Retour et Réparation peuvent servir
|
||||
d’alternatives ; elles s’adressent au demandeur. La configuration refuse une
|
||||
alternative identique à l’intention initiale, un doublon ou un effet ciblé
|
||||
hostile. Une cible absente, ambiguë ou remplacée n’est pas un prétexte pour
|
||||
accorder une autre aide. Les cinq prières hostiles demandées explicitement
|
||||
restent autorisées contre tous les joueurs connectés, comme en beta.268.
|
||||
|
||||
Le fichier distribué propose Retour puis Secours à la Récupération, Secours
|
||||
au Retour, Nourriture à la Guérison, Guérison à la Nourriture, et Guérison puis
|
||||
Nourriture aux autres intentions. Un joueur sans besoin admissible reçoit
|
||||
un refus ou du silence.
|
||||
|
||||
## Exécution différée et sauvegardes
|
||||
|
||||
Une prière en attente de réponse et un miracle différé occupent **la même
|
||||
place** dans la file : une seule opération par joueur, 1 024 au total.
|
||||
Les répétitions sont ignorées et ne remplacent pas la demande. Aucun thread
|
||||
ne dort. Le rechargement conserve l’échéance, l’intention et la cible déjà
|
||||
retenues ; le miracle n’est pas réinterprété ni tiré à nouveau à son échéance.
|
||||
|
||||
Le serveur revérifie alors l’état vivant du demandeur, la présence du même
|
||||
UUID ciblé, la destination, les objets réels et toutes les conditions du
|
||||
miracle. Il applique la définition active : désactiver ce miracle avant son
|
||||
échéance empêche l’effet. Un autre UUID portant le même pseudo ne
|
||||
remplace jamais la cible. Les arrivées et transferts conservent les protections
|
||||
de beta.268.
|
||||
|
||||
Les cooldowns positifs sont réservés au-delà du rendez-vous annoncé, puis
|
||||
prolongés si l’exécution réelle survient plus tard. Un cooldown global réglé
|
||||
à zéro reste désactivé : un joueur qui attend sa guérison ne bloque pas la
|
||||
guérison des autres joueurs. L’opération ne peut s’exécuter qu’une fois.
|
||||
|
||||
Une déconnexion, une réapparition ou l’arrêt normal du serveur annule le
|
||||
miracle différé, sans effet hors ligne. Les cooldowns réservés sont conservés. Une interruption brutale laisse une
|
||||
trace `DEFERRED` ou `PENDING`, avec `executed: false` ; les opérations en mémoire
|
||||
ne sont jamais reconstruites ou rejouées au redémarrage. Comme auparavant,
|
||||
une interruption pendant un effet peut laisser un résultat incertain.
|
||||
|
||||
Le journal `data/sanctuary-providence.json` garde **son schéma 1** et ses champs
|
||||
existants. Les nouveaux verdicts sont des valeurs du champ texte `decision`.
|
||||
La complétion ou l’annulation met à jour la même entrée, sans dupliquer la
|
||||
prière. Une alternative garde l’intention demandée et ajoute une condition
|
||||
`alternative_<ID>` pour identifier l’aide effectivement tentée.
|
||||
Le scellement permanent des tombes de beta.268 est inchangé. Aucun monde
|
||||
personnel, format de tombe, générateur ou identifiant `sanctuary:*` n’est modifié.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`check build assemblePack` réussit en **2 min 36 s**, avec **191 vérifications
|
||||
Providence indépendantes de Minecraft** (146 existantes et 45 nouvelles) et
|
||||
les **22 GameTests serveur requis**. Les scénarios serveur partagés gardent
|
||||
une politique de test explicite ; le parcours des six nouvelles issues commence
|
||||
après les scénarios existants pour éviter les rechargements concurrents de leur
|
||||
configuration commune.
|
||||
|
||||
Les nouvelles assertions couvrent les six issues, les cinq couleurs dans les
|
||||
deux modes, le silence sans message, la réparation/guérison différée, une autre
|
||||
aide effective, les cooldowns des deux aides y compris un tirage alternatif
|
||||
refusé, l’absence de blocage global quand le cooldown vaut zéro, les échéances
|
||||
conservées après rechargement, les cibles par UUID, les besoins modifiés,
|
||||
la désactivation avant échéance, l’annulation, le non-rejeu, le journal désactivé
|
||||
et la corruption des configurations. Le JSON invalide injecté dans l’ancien
|
||||
scénario de rechargement écrit une erreur attendue dans le journal serveur.
|
||||
|
||||
Le parcours **Vulkan / MoltenVK 1.4.2 / Apple M1** réussit en **1 min 22 s**,
|
||||
avec 8 Gio maximum, dans un monde de développement neuf de graine 265.
|
||||
Les cinq couleurs visibles sont rendues en FR/EN, GUI 2/3/4, SGA et lecture
|
||||
normale. Les commandes passent par la connexion réelle du jeu. La météo
|
||||
attend **102 ticks serveur** avant sa réponse ; un éclair ciblé reste visible.
|
||||
Une nouvelle prière de réparation conserve les cinq secondes d’attente initiale,
|
||||
puis montre une réponse jaune sans réparation. Avec un délai supplémentaire
|
||||
de **deux secondes réservé à cet essai**, l’objet reste endommagé pendant
|
||||
l’attente, puis est réparé et reçoit une réponse verte. Le réglage distribué
|
||||
reste trente secondes.
|
||||
|
||||
Seize captures natives : `build/providence269-screenshots/`. Les vues SGA
|
||||
FR GUI 2, lecture normale EN GUI 4, réparation différée et réparation terminée
|
||||
ont été inspectées : couleurs, police, texte contenu dans le chat et ordre des
|
||||
effets. Les essais concernent macOS local et des joueurs simulés côté serveur ;
|
||||
aucun essai avec deux clients distants ou sur Windows n’est revendiqué.
|
||||
|
||||
```sh
|
||||
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=providence265,providence268,providence269,graves,progression \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryProvidence265ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journaux ignorés : `build/providence269-check-build-pack-final.log` et
|
||||
`build/providence269-client-vulkan.log`.
|
||||
Le [pack local beta.269](../build/Sanctuary-beta.269.mrpack) est exporté et vérifié :
|
||||
ZIP intègre, version et dépendances exactes, un seul JAR Sanctuary identique au
|
||||
build, 36 classes Providence identiques aux sorties compilées, ressources
|
||||
identiques aux sources et absence de classes de test. Depuis beta.268, les
|
||||
classes existantes modifiées proviennent seulement de `PrayerQueue`,
|
||||
`ProvidenceEngine`, `ProvidenceMessages` et `ProvidenceService`, classes internes
|
||||
comprises. `ProvidenceResponses` et les types de réservation/exécution sont
|
||||
nouveaux. Reçu et empreintes : `build/providence269-validation.json`.
|
||||
Aucune publication, avancée du canal ou installation personnelle.
|
||||
@@ -0,0 +1,95 @@
|
||||
# RELEASE-289 — zombies, lumière portée et mise à jour complète
|
||||
|
||||
Branche `codex/release-beta289`. Publication demandée le 26 octobre 2026.
|
||||
État : **publiée et installée le 26 octobre 2026** sur la Beta dédiée,
|
||||
le site et l’instance Prism existante.
|
||||
|
||||
## Contenu
|
||||
|
||||
- Six infectés : Infecté, Charger, Spitter, Boomer, Smoker et Hunter, leurs œufs
|
||||
et leurs familiers adultes miniatures à taille variable, sans infecté bébé.
|
||||
- Neuf tenues fournies par le créateur, choisies par individu et conservées au
|
||||
rechargement ; zombie classique compris. Bras natifs en avant pour tous,
|
||||
Hunter accroupi au sol et bond offensif esquivable.
|
||||
- Correction des lumières portées avec Sodium, reprise du commit `6f44214`.
|
||||
- Réintégration du conditionnement JEI et de la chaîne client/serveur déjà
|
||||
publiée en beta.280, puis des outils de publication Web/Prism du runbook
|
||||
`fb20160e`. Aucun retour aux anciens réglages de carte de cette branche :
|
||||
le dézoom et les relevés de beta.279 restent conservés.
|
||||
|
||||
Le binaire local beta.288 reste immuable. La livraison beta.289
|
||||
contient aussi JEI, comme la Beta dédiée précédente. Minecraft 26.3,
|
||||
Fabric Loader 0.19.5, Fabric API 0.160.5+26.3, JEI 30.32.0-sanctuary.4,
|
||||
Sodium 0.9.2+mc26.3 et Sound Physics 1.5.1+26.3 restent épinglés.
|
||||
|
||||
## Cibles autorisées
|
||||
|
||||
Commit/push des sources, release Gitea immuable, canal packwiz stable client
|
||||
et serveur, CT Beta 350, téléchargement Web CT 320, puis synchronisation de
|
||||
la même instance personnelle Sanctuary Beta. Arrêt propre et sauvegarde de
|
||||
la Beta avant avancement du canal. Alpha CT 300, comptes, permissions,
|
||||
configuration privée, mondes et paramètres de génération conservés.
|
||||
Le site reçoit la publication de distribution ; aucun checkout live global
|
||||
n’est remplacé. Le bouton Télécharger reste le ZIP Prism prêt.
|
||||
|
||||
## Vérification et reçu
|
||||
|
||||
`./gradlew check build assemblePack assembleServerPack
|
||||
-PsanctuaryFocusedTests=recipes,progression,lights,hunter286
|
||||
-PsanctuaryAtlasOnly=true` réussit en **3 min 21 s**, avec **31 tests serveur**
|
||||
et les contrôles de la chaîne serveur/Web. Le parcours des zombies/familiers
|
||||
est rejoué sous Vulkan avec Sodium et beta.289 : réussi en 51 s.
|
||||
|
||||
Les 2 650 classes Sanctuary de beta.288 sont identiques ; le nouveau JAR
|
||||
ajoute le JEI embarqué et ses métadonnées/notices. Un seul JEI exact, côté
|
||||
commun ; aucune classe de test, aucun JEI séparé dans le MRpack. ZIP Prism prêt,
|
||||
kit serveur et archive MRpack contrôlés ; leurs contenus restent hors Git.
|
||||
Les hashes et les journaux se trouvent dans `build/release289/`.
|
||||
Les preuves propres à beta.288 restent dans `docs/dynamic-hand-light-beta288.md`.
|
||||
Elles ne sont pas présentées comme une validation du nouveau conditionnement.
|
||||
|
||||
## Publication et installation effectives
|
||||
|
||||
- Sources et tag `beta.289` : `6821f6ee12095583663f1aafb02c92fbb105683e`,
|
||||
poussés depuis `codex/release-beta289`.
|
||||
- [Release immuable](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.289)
|
||||
et trois archives publiques relues avec leurs SHA-256 exacts, HTTP 200.
|
||||
- Canal packwiz client et serveur :
|
||||
`56e3462a39d6b911c27ec926780ba5eb509e599c`, version `beta.289`.
|
||||
- CT 350 : même service `sanctuary-beta.service`, actif, JAR beta.289 et JEI
|
||||
embarqué confirmés dans le démarrage ; RCON répond, 0/20 joueurs au contrôle.
|
||||
Les 239 fichiers protégés et l’environnement privé sont identiques entre
|
||||
l’arrêt propre et la mise à jour, avant redémarrage.
|
||||
- CT 320 : bascule atomique de la distribution vers
|
||||
`storage/app/distribution/releases/beta.289/prism`. Le code live n’a pas été
|
||||
remplacé. Footer et attribut de survol du bouton public annoncent beta.289.
|
||||
Le ZIP anonyme répond HTTP 200, nom `Sanctuary-Beta.zip`, et le lien MRpack
|
||||
historique répond aussi avec le hash attendu.
|
||||
- Même communauté des deux côtés, métriques fraîches et serveur en marche.
|
||||
L’API d’admission répond 422 à un code volontairement invalide, sans appel
|
||||
de consommation d’un code.
|
||||
- Même instance personnelle **Sanctuary Beta**, dossier historique
|
||||
`Sanctuary-0.1.0-alpha.1` : passage de beta.144 à beta.289, un seul JAR
|
||||
Sanctuary, deux synchronisations identiques. Les 999 fichiers personnels
|
||||
contrôlés et les composants Minecraft/Fabric sont conservés. Aucun monde
|
||||
personnel n’a été ouvert ; l’installation isolée a aussi passé deux cycles.
|
||||
|
||||
Sauvegarde serveur complète avant avancement du canal :
|
||||
`/root/sanctuary-beta-backups/before-beta289-20261026T002155Z`,
|
||||
archive de 167 964 197 octets, SHA-256
|
||||
`724533e56bf4ab5319bbd519d123c6731fc882f684cfc0998d876c34ed09805d`.
|
||||
Sauvegarde locale complète de l’instance avant synchronisation :
|
||||
`sanctuary-backups/before-beta289-20261026T011713/instance.tar.gz`,
|
||||
230 400 000 octets. La publication Web beta.280 reste disponible pour reprise.
|
||||
|
||||
Le ping Minecraft via `beta.sanctuary-minecraft.net` et son SRV 25567 répond
|
||||
en 26.3, 0/20, depuis le Mac sur le LAN. **L’accès depuis un réseau extérieur
|
||||
n’a pas été testé.** Les vérifications graphiques de cette livraison ciblent
|
||||
Vulkan uniquement. Reçus détaillés ignorés sous `build/release289/`.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
- `Sanctuary-beta.289.mrpack` : 16,715,428 octets ; SHA-256 `7a8935225a97119ba278f8a7ccd51aae7efef588244be782661545c67270b8fc`.
|
||||
- `Sanctuary-Prism-beta.289.zip` : 92,417 octets ; SHA-256 `30dd37f3a6e1d79f8b443f6312918258276c85d5b6a5e7fbdc9c3fea7562dbf2`.
|
||||
- `Sanctuary-Server-beta.289.zip` : 4,036,416 octets ; SHA-256 `5b86b16e9d0fd944363016b79df397391eda88f24dc80b9cf0e2bd2f71fe75d9`.
|
||||
- JAR Sanctuary SHA-256 : `feec13d0b0e708473179ac3234060fb58858d94d19bc9ddf9b7137512875dc41`.
|
||||
@@ -0,0 +1,136 @@
|
||||
# beta.293 — livraison intégrée
|
||||
|
||||
Livraison demandée le **26 octobre 2026** pour essayer le parcours complet :
|
||||
site → ZIP Prism → packwiz avec logo → jeu et serveur Beta.
|
||||
Branche `codex/release-beta293`.
|
||||
|
||||
## Contenu et provenance
|
||||
|
||||
La base conserve les baleines de `a93e2611`, l'habillage packwiz et les quatre
|
||||
mods facultatifs de `97c601cd`. Les Happy Creepers sont repris du commit
|
||||
`0ffd371a` de `codex/happy-creepers`, sans réimplémentation : les règles et la
|
||||
texture restent celles de cette branche dédiée. Les conflits d'intégration
|
||||
portaient sur les versions, les listes de tests et les documents communs.
|
||||
La livraison utilise le prochain compteur libre **beta.293** ; les préparations
|
||||
locales beta.291 et beta.292 ne sont pas publiées séparément.
|
||||
|
||||
[Notes de version](notes-beta293.md), [Happy Creepers](happy-creepers.md),
|
||||
[logo packwiz](packwiz-logo-beta291.md), [mods facultatifs](optional-mods-beta292.md).
|
||||
|
||||
Le test du quota interdimensionnel des baleines attend désormais la fin du
|
||||
chargement des entités du chunk Nether via `waitForEntities`. Ses deux fixtures
|
||||
restent immobiles : l'une au centre du chunk forcé, l'autre en hauteur pour que
|
||||
sa boîte de 36 blocs ne croise pas le nettoyage des tests voisins. Le test
|
||||
d'origine pouvait perdre une fixture avant de vérifier le quota. Le code de
|
||||
jeu reste identique ; **79 tests serveur** passent sur la base réunie, avec
|
||||
Lithium et la voix chargés. Le déplacement de la baleine naturelle reste testé
|
||||
avec son IA active.
|
||||
|
||||
## Cibles de livraison
|
||||
|
||||
- Artefacts immuables et canal client/serveur Gitea ;
|
||||
- CT **350** et service `sanctuary-beta.service`, après arrêt et sauvegarde ;
|
||||
- téléchargement direct de Sanctuary Web, CT **320**, sans modification du code live ;
|
||||
- même instance Prism **Sanctuary Beta**, avec sauvegarde, installateur habillé
|
||||
vérifié et conservation des données personnelles.
|
||||
|
||||
La règle NAT Beta existante `25567 → 192.168.1.122:25565` couvre TCP et UDP.
|
||||
La voix utilise **UDP 25565 dans la CT**, annoncé comme
|
||||
`beta.sanctuary-minecraft.net:25567`. Aucun changement de Bbox, UPnP, Alpha,
|
||||
RCON ou règles des autres serveurs n'est nécessaire.
|
||||
|
||||
## Validation de la livraison
|
||||
|
||||
`check build assemblePack assembleServerPack` : **réussi en 2 min 46 s**,
|
||||
avec les contrôles purs et **79 tests serveur requis**. Profil natif :
|
||||
`whale290,happycreeper,cosmetics,headblocks,recipes,progression,smoker285,hunter286`,
|
||||
`sanctuaryAtlasOnly=true`, `sanctuaryOptional292ServerTests=true`.
|
||||
Journal : `build/release293/check-build-final.log`.
|
||||
|
||||
Parcours Happy Creepers **Vulkan avec Sodium et les quatre mods chargés** :
|
||||
toutes les assertions en jeu réussissent sur deux essais, capture des quatre
|
||||
états inspectée (`build/release293/happy-four-states-vulkan.png`). Équipement et
|
||||
retrait au clavier/souris, fleur exacte synchronisée, visage restauré, IA
|
||||
passive près du joueur et charge indépendante vérifiés. La fermeture du monde
|
||||
reste bloquée dans la synchronisation `Phaser` de Fabric Client GameTest ; ce
|
||||
contrôle graphique n'est donc pas un succès de tâche Gradle. Le premier dump
|
||||
montre le thread de test dans `TestSingleplayerContextImpl.close`, le serveur
|
||||
dans `ThreadingImpl.enterPhase` et le rendu dans `IntegratedServer.halt`.
|
||||
Ce blocage de l'infrastructure de test est consigné séparément des assertions
|
||||
réussies et du build complet réussi. Les parcours des quatre mods avec et sans
|
||||
Sodium ont leur reçu dans [beta.292](optional-mods-beta292.md).
|
||||
|
||||
Les archives ont été contrôlées avant publication : sept dépendances externes
|
||||
et un seul JAR Sanctuary dans le MRpack ; huit mods du client et quatre du
|
||||
serveur ; JEI embarqué, aucun test ni dépendance facultative imposée dans le
|
||||
JAR Sanctuary. Texture happy identique à la branche dédiée, classes creeper
|
||||
et baleine présentes. ZIP Prism sans mods ni sauvegardes personnelles.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.293.jar` | 18171689 | `4a0a3941f651ba3566ec41940c2f4c820d917121312e3aead23b39f99633ebe3` |
|
||||
| `Sanctuary-beta.293.mrpack` | 16761300 | `80e1d9a54165f279738a6486c8c3cd7e5e90ad7ad4ca81d32a4d4a04b15f0e90` |
|
||||
| `Sanctuary-Prism-beta.293.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.293.zip` | 4036536 | `5f3135f7f5b5448023f9873c5242e3c2ab9444c197e1b551d356c8b1120bf01d` |
|
||||
|
||||
## État opérationnel
|
||||
|
||||
**Livraison complète effectuée le 26 octobre 2026.**
|
||||
|
||||
| Cible | État vérifié |
|
||||
| --- | --- |
|
||||
| Source et tag `beta.293` | `678d77fb91ef5e3ffee96cac5c88ebafa0a77976` |
|
||||
| Canal client/serveur | `687d07e57ec008e1865a0081f317c4f3e3159e14` |
|
||||
| Release Gitea | [beta.293](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.293), quatre artefacts publics vérifiés |
|
||||
| CT 350 | Service actif, Sanctuary beta.293, Fabric API, Lithium et Voice Chat chargés ; démarrage sans erreur |
|
||||
| CT 320 | Publication atomique `releases/beta.293/prism`, ancienne beta.289 conservée |
|
||||
| Prism personnel | Même instance **Sanctuary Beta**, huit mods et un seul JAR Sanctuary beta.293 ; deux synchronisations identiques |
|
||||
|
||||
Le serveur était vide (0/20) avant arrêt. Sauvegarde complète privée :
|
||||
`/root/sanctuary-beta-backups/before-beta293-20261026T032339Z/instance.tar.gz`,
|
||||
SHA-256 `b9bd9fa7b10d7e54b3595d52061529a3da8a5c9193f1854966a60fda975e9f11`.
|
||||
Les **240 fichiers protégés sont identiques après synchronisation, avant
|
||||
redémarrage**, de même que l'environnement privé. Après redémarrage, Minecraft
|
||||
réécrit le commentaire horodaté de `server.properties` ; toutes les clés et
|
||||
valeurs restent identiques. Seul le nouveau fichier vocal a été configuré :
|
||||
`port=25565`, `bind_address=` vide,
|
||||
`voice_host=beta.sanctuary-minecraft.net:25567`, `force_voice_chat=false`.
|
||||
Aucun changement de monde, de génération ou de format n'est effectué par
|
||||
l'outil de mise à jour ; le serveur reprend son monde existant.
|
||||
|
||||
La voix écoute en UDP 25565. Un ping applicatif vocal valide répond via le
|
||||
nom public et le port 25567 depuis le Mac (68 ms). Le statut Minecraft via
|
||||
DNS/SRV répond également : 26.3, 0/20. Ces contrôles partent du LAN via
|
||||
l'adresse publique : **l'accès depuis un autre réseau et l'audio audible à
|
||||
deux joueurs restent à essayer**. L'authentification vocale dans les mondes de
|
||||
test est réussie ; elle ne remplace pas cet essai vocal distant.
|
||||
L'API d'admission authentifiée refuse le code volontairement invalide
|
||||
(`HTTP 200`, `valid=false`, `reason=invalid`), sans appel à `redeem`.
|
||||
Les métriques Web sont fraîches, en marche, avec le même UUID communautaire
|
||||
`cc6f4f5a-e58d-409a-879f-81daecf59461` et la même base de jeu.
|
||||
|
||||
Le [ZIP direct du site](https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip)
|
||||
répond HTTP 200 sous le nom **Sanctuary-Beta.zip**, avec le hash du tableau.
|
||||
Le lien MRpack historique répond aussi HTTP 200 avec son hash exact.
|
||||
Le bouton, son survol et le footer affichent beta.293. Le code Web live est
|
||||
resté sur `eadc30e8c7b6dda50bc810584af057235e7fdba8` ; les **73 entrées Git
|
||||
préexistantes sont identiques**, sans pull, reset ni reconstruction de cache.
|
||||
Une importation isolée du ZIP a récupéré les huit mods depuis le vrai canal
|
||||
HTTPS, puis une deuxième synchronisation a conservé exactement leurs hashes.
|
||||
|
||||
Sauvegarde Prism complète sous le dossier historique de l'instance :
|
||||
`sanctuary-backups/before-beta293-20261026T030401Z/instance.tar.gz`, SHA-256
|
||||
`9bfc9b963551885d3e124c865aabe0dc9750ecb6cfed89b7d936a3e14abe51bb`.
|
||||
Les **999 fichiers personnels contrôlés sont inchangés**, et aucun monde
|
||||
personnel n'a été ouvert. Seules les deux commandes `PreLaunchCommand` de
|
||||
`instance.cfg` et le JAR de l'installateur ont reçu l'habillage ; les autres
|
||||
lignes de configuration sont identiques. L'outil reste épinglé par
|
||||
`--bootstrap-no-update`, et le pack continue de suivre le canal stable.
|
||||
Sodium et Sound Physics ont gardé leurs sélections ; les quatre nouveaux
|
||||
choix sont activés par défaut et restent facultatifs côté client.
|
||||
Installateur SHA-256 :
|
||||
`9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875`.
|
||||
|
||||
Les journaux et reçus détaillés sont sous `build/release293/`, hors de Git.
|
||||
Le worktree original `codex/happy-creepers` est resté propre au commit
|
||||
`0ffd371adac0c4f950237448625673b83a94bee5`.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Sanctuary — serveur Linux avec packwiz
|
||||
|
||||
Prérequis : Java 25, Python 3.11 ou plus récent, Linux et accès HTTPS sortant.
|
||||
Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
|
||||
**0.160.5+26.3**. Le même JAR Sanctuary contient le fork JEI
|
||||
**30.32.0-sanctuary.4**, son réseau commun et son interface client.
|
||||
|
||||
## Préparer le kit
|
||||
|
||||
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
|
||||
Le kit de la livraison courante est `build/Sanctuary-Server-beta.293.zip` ; les manifestes locaux sont
|
||||
dans `build/packwiz-server/`. Le ZIP est un amorçage : il télécharge les mods
|
||||
depuis le canal au premier démarrage, il n'est pas une copie hors ligne du pack.
|
||||
Il n'est utilisable sur le canal public qu'après publication de cette version.
|
||||
|
||||
Décompresser le kit **une seule fois**, dans un dossier serveur neuf. Pour
|
||||
reprendre une installation existante, arrêter le service, sauvegarder le dossier
|
||||
entier, vérifier Minecraft/Loader et poser uniquement les fichiers du kit en
|
||||
conservant le monde et la configuration. Ne pas écraser un fichier déjà présent
|
||||
sans comparaison. Un ancien JEI externe géré par packwiz est retiré par le nouvel
|
||||
index ; un JAR ajouté manuellement doit être retiré après sauvegarde pour éviter
|
||||
de charger une autre version du fork. Conserver `config/jei/`.
|
||||
|
||||
Canal prévu, publié avec le client dans le même commit de distribution :
|
||||
|
||||
```text
|
||||
https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/server/pack.toml
|
||||
```
|
||||
|
||||
`./server.sh update` synchronise les mods sans démarrer Minecraft.
|
||||
`./server.sh start` synchronise puis lance Fabric en console. En cas d'échec,
|
||||
le serveur ne démarre pas. Accepter l'EULA Minecraft et régler le serveur
|
||||
explicitement avant ouverture aux joueurs ; le kit ne les remplit pas.
|
||||
Le lanceur Fabric télécharge les fichiers Mojang/Loader nécessaires au lancement.
|
||||
|
||||
Les outils de démarrage sont épinglés par hash dans `.tools/platform.json`.
|
||||
Un changement de Minecraft ou Loader est refusé avant mise à jour et exige un
|
||||
nouveau kit et un contrat de migration. Le verrou empêche une mise à jour en
|
||||
parallèle d'un serveur lancé par ce kit. Arrêter aussi tout service historique
|
||||
qui utiliserait un autre lanceur.
|
||||
|
||||
Variables facultatives : `SANCTUARY_JAVA` (chemin du binaire Java),
|
||||
`SANCTUARY_MEMORY` (`4G` par défaut), `SANCTUARY_PACK_URL` (autre canal ou
|
||||
serveur HTTP de laboratoire). Les variables sont héritées de l'environnement ;
|
||||
le script ne charge pas automatiquement un `.env`.
|
||||
|
||||
Un exemple systemd est fourni sous `examples/sanctuary-beta.service`.
|
||||
Installer le kit dans `/srv/minecraft/sanctuary-beta`, créer l'utilisateur
|
||||
`minecraft`, donner ce dossier à cet utilisateur et placer l'environnement
|
||||
privé dans `/etc/sanctuary/beta.env` (root, mode 600). Copier ensuite l'unité
|
||||
dans `/etc/systemd/system/`, recharger systemd et activer le service. Le mode
|
||||
`KillMode=mixed` transmet l'arrêt au lanceur, qui attend la sauvegarde Java
|
||||
avant de rendre le verrou de mise à jour.
|
||||
|
||||
## Relier Sanctuary Web
|
||||
|
||||
L'API d'accueil et la communauté sont deux services existants distincts.
|
||||
Le pack ne contient ni secret ni configuration de production.
|
||||
|
||||
Pour l'accès/candidature, fournir dans l'environnement du service Minecraft :
|
||||
|
||||
```text
|
||||
SANCTUARY_APP_URL=https://sanctuary-minecraft.net
|
||||
SANCTUARY_API_TOKEN=<jeton API du site>
|
||||
```
|
||||
|
||||
Le site doit fournir les POST authentifiés `/api/v1/access-codes/verify` et
|
||||
`/redeem`, avec les reçus d'admission de beta.166. Aucun appel d'essai ne doit
|
||||
consommer un vrai code de joueur. Sans ces deux paramètres, le mod autonome
|
||||
laisse l'accueil sans code ; une configuration partielle ferme l'accueil.
|
||||
|
||||
Pour Gazette/Tableau et les statistiques, reprendre le **serverId existant**
|
||||
dans `config/sanctuary-community.json`, configurer `mode: database`, le JDBC
|
||||
MariaDB, l'utilisateur et `passwordEnv: SANCTUARY_COMMUNITY_DB_PASSWORD`.
|
||||
Le mot de passe est uniquement dans l'environnement. Utiliser le même UUID dans
|
||||
`SANCTUARY_COMMUNITY_SERVER_ID` côté Laravel. L'exemple sous `examples/` doit être
|
||||
complété, puis copié volontairement ; il ne remplace aucune configuration.
|
||||
Garder `online-mode=true` pour l'identité et la liaison des comptes.
|
||||
|
||||
Les migrations SQL sont celles du site et doivent être vérifiées sur la base
|
||||
cible avant activation. Basculer du mode fichier vers MariaDB n'importe pas les
|
||||
anciennes publications : cela demande un contrat de migration séparé.
|
||||
Le kit n'effectue aucune migration SQL ni modification de sauvegarde.
|
||||
|
||||
## Mises à jour et retour arrière
|
||||
|
||||
Arrêter le service, sauvegarder, lancer `./server.sh update`, vérifier les logs,
|
||||
puis redémarrer le même dossier serveur. `packwiz.json` doit rester présent.
|
||||
Depuis la préparation beta.292, le pack serveur possède Fabric API, Sanctuary,
|
||||
Lithium et Simple Voice Chat. Bobby et Entity Culling restent uniquement
|
||||
clients. [Versions, validation et port UDP vocal](optional-mods-beta292.md).
|
||||
Il ne gère ni monde,
|
||||
`server.properties`, `eula.txt`, `config/`, listes de joueurs, permissions,
|
||||
captures ou secrets. Les réglages des opérateurs restent en place.
|
||||
|
||||
Sauvegarder aussi l'identifiant d'admission et ses reçus avec le monde, ainsi
|
||||
que les configurations et l'UUID communautaire. Un retour arrière reprend le
|
||||
dernier artefact immuable et la sauvegarde correspondant à son contrat de
|
||||
données ; ne pas supprimer de journaux pour forcer le chargement.
|
||||
|
||||
Référence : [installation packwiz sur serveur](https://packwiz.infra.link/tutorials/installing/packwiz-installer/#using-a-modpack-with-a-server).
|
||||
Dans le dépôt, consulter aussi `docs/inscription-web-beta166.md` et
|
||||
`docs/community-contract-v1.md` pour les contrats et droits SQL complets.
|
||||
@@ -0,0 +1,92 @@
|
||||
# ZOMBIES-285 — Smoker, lasso et traction en tyrolienne
|
||||
|
||||
Branche `codex/smoker`, sur le socle local beta.284 conservé. État : implémenté et vérifié localement en beta.285.
|
||||
|
||||
## Contrat demandé
|
||||
|
||||
Le Smoker doit toucher réellement sa cible avec un lasso avant de la ramener
|
||||
vers lui par un trajet de tyrolienne. Le projectile garde sa direction de tir :
|
||||
une esquive pendant le vol, un obstacle ou un tir trop court peut le faire rater.
|
||||
|
||||
- Modèle adulte du zombie vanilla 26.3, t-shirt gris clair et pantalon vert
|
||||
olive recolorés par script, tête/peau/chaussures/UV/alpha conservés.
|
||||
Identifiants `sanctuary:smoker` et `sanctuary:smoker_spawn_egg`. Aucun bébé,
|
||||
y compris via œuf, données chargées ou proxy du familier ; pas de noyé.
|
||||
- Sauvage : 10 PV, vitesse 0,23, mêlée 3, poursuite 36 blocs. Tir à 4–14 blocs
|
||||
avec ligne de vue, préparation de 18 ticks, lasso à 1,05 bloc/tick, recharge
|
||||
de 160 ticks engagée au lancer, même si le lasso rate.
|
||||
- Un câble visible relie le Smoker au lasso en vol, puis à la cible touchée.
|
||||
Un seul câble par source et une seule traction par victime. La source reste
|
||||
immobile. Traction à **0,58 bloc/tick**, même vitesse que les tyroliennes,
|
||||
jusqu'à environ 1,8 bloc de la source, sans pendule et sans dégâts périodiques.
|
||||
- Le volume complet de la victime est vérifié tous les 0,20 bloc : mur, bordure,
|
||||
hauteur invalide ou chunk non chargé arrêtent la traction. Aucun passage à
|
||||
travers les blocs, aucune génération de terrain ou installation de poulie.
|
||||
La gravité reprend ensuite normalement ; les chutes restent possibles.
|
||||
- Blesser la source, frapper le crochet, tuer la source, quitter la dimension,
|
||||
téléporter l'un des acteurs ou dépasser la durée coupe le câble. Le trajet
|
||||
hostile impose le déplacement après une touche ; s'accroupir ne le désactive
|
||||
pas. Les montures/passagers, créatifs/spectateurs et boss ne sont pas capturés.
|
||||
- Familier rare, adulte miniature à taille variable persistante : 12 PV,
|
||||
vitesse 0,26, lasso automatique et G à 14 blocs. Le tir doit aussi toucher ;
|
||||
dégâts d'impact et contrôle passent par les protections communes des familiers
|
||||
(propriétaire, alliés, claims, duels et résistance aux contrôles répétés).
|
||||
- Œuf créatif, apparition rare dans les habitats de zombies de l'Overworld,
|
||||
poids 4, un individu. Recharge sauvegardée ; câble et trajet transitoires non
|
||||
sauvegardés. Aucun identifiant ou format existant remplacé.
|
||||
|
||||
## Vérification
|
||||
|
||||
- **19 tests ciblés Smoker** réussis : 18 nouveaux scénarios et le contrôle
|
||||
du framework. Œuf/profil/spawn, adulte, préparation et recharge au lancer,
|
||||
contact réel préalable au déplacement, vitesse 0,58, esquive en vol, mur,
|
||||
obstacle bas sur le corps, blessure de la source même après guérison, crochet
|
||||
coupé, source retirée/déplacée, téléportation et créatif, exclusivité des
|
||||
trajets, sauvegarde sans corde/bébé, chunk non chargé, G avec dégâts/contrôle
|
||||
communs, raté sans remboursement, annulation par ordre, propriétaire/animaux/
|
||||
boss protégés, arrêt avant une cellule interdite et tailles 120 à 6000.
|
||||
- **77 régressions** réussies dans une autre session neuve : tyroliennes fixes
|
||||
et portatives, Boomer, Spitter, Charger, Infecté et socle des familiers. Les
|
||||
deux suites incluent chacune le contrôle du framework. Le mélange des fixtures
|
||||
de joueurs simulés du Smoker avec les scénarios d'IA asynchrones a provoqué
|
||||
des interférences dans un monde partagé ; les suites sont donc isolées,
|
||||
sans modifier les tests des infectés précédents. Journaux :
|
||||
`build/smoker285-check-build-pack.log` et `build/smoker285-regressions.log`.
|
||||
- `./gradlew check build assemblePack` réussi en **2 min 49 s**, avec
|
||||
`-PsanctuaryFocusedTests=smoker285 -PsanctuaryAtlasOnly=true`. Les régressions
|
||||
utilisent `ziplines242,boomer284,spitter283,charger282,infected280,familiar260,companions`
|
||||
avec le même monde plat explicite de graine `0`, dans une session distincte.
|
||||
- Parcours client natif **Vulkan / MoltenVK 1.4.2 / Apple M1**, Java 25,
|
||||
Minecraft 26.3, Loader 0.19.5, monde neuf de graine `285` : réussi avec arrêt
|
||||
normal du client. Appui réel sur G, vol puis touche et traction de l'ennemi,
|
||||
câble natif segmenté, recharge, trois tailles adultes 500/1000/1500 et
|
||||
infobulles/écran FR/EN GUI 2/3/4 vérifiés.
|
||||
- Le même parcours joue le rôle de la victime : un déplacement latéral réel
|
||||
au clavier fait rater le premier lasso. Le second touche réellement, puis
|
||||
transporte le joueur malgré Reculer/Accroupir, jusqu'au Smoker. Position du
|
||||
joueur vérifiée côté serveur et dans le client rendu, câble supprimé à
|
||||
l'arrivée, gravité normale conservée. Le familier reprend son positionnement
|
||||
de combat habituel après avoir achevé sa traction ; l'arrivée est contrôlée
|
||||
par rapport au point d'ancrage effectif du tir.
|
||||
- Texture 64×64 comparée en RGBA au zombie vanilla épinglé : **331 pixels de
|
||||
t-shirt et 239 de pantalon** recolorés, coordonnées/alpha/tête inchangés.
|
||||
Les quatre anciennes textures sont identiques. Œuf 16×16 comparé à sa
|
||||
recoloration attendue, silhouette intacte. Aucun outil de génération d'images.
|
||||
- JAR et export MRpack : ZIP intègres, version beta.285, dépendances exactes,
|
||||
classes, œuf, modèle, texture, profil, butin, tags et traductions présents.
|
||||
Classes de tests exclues ; JAR du pack identique au JAR compilé.
|
||||
Le pack beta.284 précédent est conservé octet pour octet.
|
||||
|
||||
[Pack local beta.285](../build/Sanctuary-beta.285.mrpack), **16 666 919 octets**.
|
||||
SHA-256 : `49dbe9798ecd567f6f53b0482515682b25f4a0dc2c09c24126a3d1579cacf5d0`.
|
||||
[Reçu de validation et empreintes](../build/smoker285-validation.json).
|
||||
|
||||

|
||||

|
||||
|
||||
Livraison locale uniquement : aucun tag/release, canal packwiz, serveur personnel
|
||||
ou instance Prism modifié. Les œufs restent créatifs ; leur acquisition en
|
||||
survie relève d'un autre ticket. Essais de transport sur serveur natif et
|
||||
serveur intégré ; latence de clients distants non couverte. Aucun monde
|
||||
personnel ni format de sauvegarde existant modifié.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# ZOMBIES-283 — Spitter et zone d'acide
|
||||
|
||||
Branche `codex/spitter`, sur le socle local beta.282 conservé. État : implémenté et vérifié localement en beta.283.
|
||||
|
||||
## Contrat demandé
|
||||
|
||||
Le Spitter utilise toujours le modèle adulte du zombie, un t-shirt orange et
|
||||
un pantalon bleu ardoise. Son œuf est `sanctuary:spitter_spawn_egg` et son
|
||||
familier reprend l'adulte en miniature, avec le trait de taille existant.
|
||||
Les textures sont recolorées par script à partir du PNG vanilla 26.3, avec
|
||||
peau, visage, chaussures, transparence et UV conservés.
|
||||
|
||||
**Le comportement diffère de 26.2**, selon le retour du créateur : le crachat
|
||||
ne blesse ni n'empoisonne directement. Il dépose à l'impact une **zone verte
|
||||
persistante**, utilisant les particules et le rayon d'un nuage de potion.
|
||||
Une attente de 10 ticks laisse réagir, puis la zone inflige 1 dégât par seconde
|
||||
aux entités admissibles qui restent dedans. En sortir arrête immédiatement les
|
||||
dégâts ; aucun poison ou ralentissement ne prolonge l'effet après la sortie.
|
||||
|
||||
- Spitter : 10 PV de base, vitesse 0,24, poursuite 36 blocs, mêlée 3 si la cible
|
||||
approche à moins de 3 blocs. À 3–15 blocs avec ligne de vue : préparation de
|
||||
14 ticks, crachat visible, recharge de 100 ticks.
|
||||
- Zone sauvage : rayon initial de 2,5 blocs qui diminue, durée de 100 ticks
|
||||
après l'attente, au sol ; les murs et étages séparent les victimes. Un seul
|
||||
nuage actif par source, le suivant remplace le précédent. Source et autres
|
||||
infectés de la famille immunisés contre la zone sauvage.
|
||||
- Projectile : collision native, durée et distance bornées, arrêt avant les
|
||||
chunks non chargés. Une surface solide proche de l'impact est nécessaire.
|
||||
Aucun bloc changé, aucun terrain ou chunk généré par la mécanique.
|
||||
- Œuf créatif et spawn rare dans les habitats de zombies de l'Overworld :
|
||||
poids 4, un seul individu. Aucun bébé, pas de transformation en noyé.
|
||||
- Familier rare : 12 PV, vitesse 0,28, positionnement à distance ; initiative
|
||||
de petite zone et technique G de zone persistante de 2,5 blocs / 5 secondes.
|
||||
Dégâts périodiques et destinataires passent par les règles des familiers,
|
||||
avec protection du propriétaire, des alliés et des duels. Les tirs automatiques
|
||||
attendent la fin du projectile puis de la zone active, afin de préserver G.
|
||||
- Projectiles et zones sont transitoires et ne sont pas sauvegardés. La recharge
|
||||
du mob est conservée ; aucun format existant ni identifiant livré modifié.
|
||||
|
||||
Les autres infectés et l'acquisition des œufs en survie restent hors périmètre.
|
||||
Aucun monde personnel ouvert ; publication ou mise à jour Prism non demandée.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `./gradlew check build assemblePack` réussi, avec
|
||||
`-PsanctuaryFocusedTests=spitter283,charger282,infected280,familiar260,companions`
|
||||
et `-PsanctuaryAtlasOnly=true` : **54 tests serveur**, dont **9 nouveaux**.
|
||||
Fixtures plates explicites, sans ouvrir de sauvegarde personnelle.
|
||||
- Couverture Spitter : œuf/profil/spawn, adulte et tailles persistantes, délai,
|
||||
pulses, sortie sans statut, rétrécissement/expiration, murs/étages, famille,
|
||||
remplacement et nettoyage, non-sauvegarde, surface absente, chunks non chargés,
|
||||
vraie préparation/tir sauvage, interruption/recharge conservée, vrai projectile
|
||||
de familier, propriétaire et foule de 13 animaux protégés. Les entités immunes
|
||||
sont écartées avant le budget de 12 victimes admissibles par pulse.
|
||||
- Parcours client natif **Vulkan / MoltenVK 1.4.2 / Apple M1**, graine `283`,
|
||||
Minecraft **26.3**, Fabric Loader **0.19.5**, Java **25** : réussi avec arrêt
|
||||
normal du client. Appui réel sur G, projectile/zone/dégâts, sortie sans nouveaux
|
||||
dégâts, recharge et préparation/zone sauvage vérifiés. Modèle adulte aux tailles
|
||||
500/1000/1500 et écran/infobulles FR/EN aux échelles GUI 2/3/4 contrôlés.
|
||||
- Le parcours intégré a révélé une collision prédite côté client qui retirait
|
||||
l'entrée serveur du projectile avant l'impact réel. Le suivi et la résolution
|
||||
des projectiles gérés restent désormais côté serveur ; le test G couvre ce cas.
|
||||
- Texture 64×64 : **331 pixels de t-shirt et 239 de pantalon** recolorés.
|
||||
Résultat RGBA comparé au script et au PNG natif épinglé ; visage, peau,
|
||||
chaussures, alpha et UV conservés. Empreintes des textures Infecté/Chargeur
|
||||
précédentes inchangées. Icône d'œuf historique comparée octet par octet.
|
||||
- JAR et export MRpack : ZIP intègres, version exacte beta.283, versions
|
||||
Minecraft/Loader vérifiées, classes et données Spitter présentes, tests exclus.
|
||||
Le JAR contenu dans le pack correspond exactement au JAR compilé.
|
||||
|
||||
[Pack local beta.283](../build/Sanctuary-beta.283.mrpack), **16 634 641 octets**.
|
||||
SHA-256 : `399077b7dba950550fcbfd2492859e26acc0939d45e8206673a44baa88a9a77f`.
|
||||
[Reçu de validation et empreintes](../build/spitter283-validation.json).
|
||||
|
||||

|
||||
|
||||
Les essais utilisent des mondes neufs de développement. La distribution reste
|
||||
locale : aucun tag/release, canal packwiz, serveur personnel ou instance Prism
|
||||
modifié. L'acquisition des œufs en survie et les autres infectés restent à traiter.
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# UPDATE-OPS — développer et publier Sanctuary
|
||||
|
||||
Procédure de reprise demandée par Koka le **26 octobre 2026**, branche
|
||||
`codex/update-workflow`. Elle relie les sources du jeu, packwiz, le serveur Beta,
|
||||
Sanctuary Web et l'instance Prism locale. Les anciennes livraisons restent dans
|
||||
[packwiz.md](packwiz.md) ; le parcours de téléchargement actuel est décrit dans
|
||||
[beta-download.md](beta-download.md).
|
||||
|
||||
## Reprendre un chantier
|
||||
|
||||
Lire `AGENTS.md`, `README.md`, le ticket et l'état Git des dépôts concernés.
|
||||
Choisir la branche source qui contient les dernières livraisons voulues, puis
|
||||
travailler sur `codex/<sujet>`. Les branches des collègues peuvent avancer : ne
|
||||
pas prendre automatiquement `main`, le plus grand compteur ou un autre worktree
|
||||
comme base. Comparer les commits et les changements avant toute intégration.
|
||||
|
||||
| Élément | Référence à retrouver |
|
||||
| --- | --- |
|
||||
| Sources du jeu | `https://git.botsu.net/koka/sanctuary-beta.git` |
|
||||
| Sources du site | `https://git.botsu.net/chris/sanctuary-web.git` |
|
||||
| Canal client | `https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml` |
|
||||
| Canal serveur | Même adresse, avec `packwiz/server/pack.toml` |
|
||||
| Site actif | `https://sanctuary-minecraft.net` — Sanctuary Web, pas un ancien site de release |
|
||||
| Entrée Minecraft | `beta.sanctuary-minecraft.net` — SRV vers le port public 25567 |
|
||||
| Téléchargement unique du site | `/downloads/sanctuary-prism.zip`, nom reçu `Sanctuary-Beta.zip` |
|
||||
|
||||
Développer/tester localement ne publie pas. Un simple `commit et push` pousse
|
||||
les sources ; il ne redémarre pas les services. Quand Koka demande une livraison
|
||||
complète ou « mets tout à jour », dérouler les cibles demandées jusqu'à leur
|
||||
vérification sans redemander les choix déjà établis. Cette note mémorise la
|
||||
procédure ; elle ne lance aucune publication périodique.
|
||||
|
||||
## Préparer une livraison du jeu
|
||||
|
||||
Pour chaque nouveau binaire Sanctuary, prendre le prochain numéro **libre**
|
||||
`beta.xxx` après examen des branches/releases. Aligner `mod_version` et
|
||||
`pack_version` dans `gradle.properties`, puis `version` dans `packwiz/pack.toml`.
|
||||
Une modification documentaire ou du site seule ne change pas ce compteur.
|
||||
|
||||
Utiliser **Java 25** et **Python 3.11+**. Sur le Mac actuel, `python3` peut encore
|
||||
être Python 3.9 : utiliser `python3.14` pour les outils opérationnels. Aucun
|
||||
changement de Minecraft/Fabric ou de format de sauvegarde n'est implicite.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack assembleServerPack
|
||||
python3.14 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Ajouter les vérifications utiles au ticket ; les essais graphiques du jeu
|
||||
ciblent **Vulkan uniquement**. Le profil ciblé employé pour beta.280 était
|
||||
`-PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true` : ce reçu
|
||||
historique ne remplace pas les tests d'un futur changement touchant un autre
|
||||
comportement. Consigner les limites, ne pas présenter un contrôle exclu comme
|
||||
réussi.
|
||||
|
||||
Les sorties attendues pour la version choisie sont :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.xxx.jar` : un JAR Sanctuary contenant JEI ;
|
||||
- `build/Sanctuary-beta.xxx.mrpack` : format conservé pour les liens existants ;
|
||||
- `build/Sanctuary-Prism-beta.xxx.zip` : instance prête pour le bouton du site ;
|
||||
- `build/Sanctuary-Server-beta.xxx.zip` : kit Linux ;
|
||||
- les métadonnées client/serveur de `build/packwiz-release/` après publication.
|
||||
|
||||
Vérifier les versions, SHA-256, index et contenus avant publication. Ne pas
|
||||
versionner les binaires, dépendances, mondes ni secrets. Le ZIP Prism contient
|
||||
seulement sa configuration, son icône, le bootstrap et, depuis beta.291,
|
||||
l'installateur habillé avec le logo Sanctuary ; le premier lancement télécharge
|
||||
les mods. [Contrat et maintien de l'outil](packwiz-logo-beta291.md). L'option
|
||||
`--bootstrap-no-update` conserve cet installateur vérifié ; elle ne désactive
|
||||
pas les mises à jour du pack. Ne pas ajouter de réglages ou sauvegardes personnels.
|
||||
|
||||
## Publier les artefacts et le canal
|
||||
|
||||
Committer et pousser la branche source vérifiée, puis exécuter depuis cette
|
||||
même branche propre, en remplaçant `beta.xxx` et le chemin des notes :
|
||||
|
||||
```sh
|
||||
python3.14 scripts/publish_pack.py \
|
||||
--notes-file docs/notes-du-ticket.md \
|
||||
--asset build/Sanctuary-beta.xxx.mrpack \
|
||||
--asset build/Sanctuary-Prism-beta.xxx.zip \
|
||||
--asset build/Sanctuary-Server-beta.xxx.zip
|
||||
```
|
||||
|
||||
Cette commande crée/vérifie la release, joint les artefacts, publie ensemble
|
||||
les profils client et serveur et vérifie leurs téléchargements publics.
|
||||
Elle utilise les identifiants du gestionnaire Git en mémoire. Le tag est
|
||||
exactement `beta.xxx`, sans `v`. Une release déjà publiée reste immuable : ne
|
||||
pas déplacer le tag ou remplacer une pièce jointe différente. Une reprise se
|
||||
fait depuis le commit source exact de la release, pas depuis un commit
|
||||
documentaire ultérieur portant le même compteur.
|
||||
|
||||
`publish_pack.py` avance déjà le canal : ne pas faire un deuxième push manuel
|
||||
du pack. Il ne met à jour ni le serveur, ni Prism, ni le téléchargement Web.
|
||||
Tester deux synchronisations avec le véritable installateur dans un dossier
|
||||
isolé ; vérifier l'absence de doublon Sanctuary et la stabilité du second
|
||||
passage. Conserver le reçu et les hashes dans `build/`, hors de Git.
|
||||
|
||||
## Mettre à jour le serveur Beta dédié
|
||||
|
||||
Depuis la préparation beta.292, le profil serveur comprend aussi Lithium et
|
||||
Simple Voice Chat ; Bobby et Entity Culling restent clients. Le vérificateur
|
||||
Web compare les hashes de ces dépendances partagées avec ceux de la CT.
|
||||
Avant la mise en service vocale demandée, vérifier séparément le port UDP
|
||||
annoncé et un échange audible à deux joueurs ; consulter
|
||||
[le contrat beta.292](optional-mods-beta292.md). Ajouter le JAR vocal au pack
|
||||
ne configure pas les redirections ou le pare-feu du serveur.
|
||||
|
||||
La cible est la **CT 350 sur le nœud `gpu`**, dossier
|
||||
`/srv/minecraft/sanctuary-beta`, service `sanctuary-beta.service`, utilisateur
|
||||
`minecraft`. La CT 300 Alpha et les autres serveurs sont hors cible.
|
||||
L'accès et les chemins propres au poste sont dans la compétence locale
|
||||
`sanctuary-updates`, référence `references/local-environment.md`.
|
||||
|
||||
Quand la livraison comprend le serveur, après les builds et avant d'avancer
|
||||
le canal, arrêter proprement la Beta. Attendre la fin de Java et de la
|
||||
sauvegarde, puis sauvegarder son dossier entier et son environnement privé
|
||||
dans un emplacement root privé hors du dossier géré par packwiz. Noter le
|
||||
commit du canal et les hashes précédents. Cela évite que des clients à jour
|
||||
rejoignent brièvement l'ancien serveur.
|
||||
|
||||
Après publication du canal, ces commandes s'exécutent **dans la CT 350** :
|
||||
|
||||
```sh
|
||||
systemctl stop sanctuary-beta.service
|
||||
runuser -u minecraft -- /srv/minecraft/sanctuary-beta/server.sh update
|
||||
systemctl start sanctuary-beta.service
|
||||
systemctl is-active sanctuary-beta.service
|
||||
journalctl -u sanctuary-beta.service -n 100 --no-pager
|
||||
```
|
||||
|
||||
La sauvegarde précède `update` ; le premier `stop` ci-dessus reste nécessaire
|
||||
si cette étape est exécutée séparément. Ne redémarrer qu'après une mise à jour
|
||||
réussie. L'outil refuse un changement implicite de Minecraft/Fabric et une
|
||||
mise à jour concurrente au lancement. Il ne charge pas automatiquement
|
||||
`/etc/sanctuary/beta.env` : reprendre seulement les variables non secrètes
|
||||
nécessaires si le Java ou le canal diffèrent de ceux du kit. Le service charge
|
||||
l'environnement privé au démarrage.
|
||||
|
||||
Conserver le monde, `packwiz.json`, `config/`, `server.properties`,
|
||||
`eula.txt`, les listes de joueurs, opérateurs, permissions et reçus d'admission.
|
||||
Garder l'UUID communautaire existant. Ne pas recréer une CT, réextraire un kit
|
||||
sur tout le serveur ou réinitialiser Gazette/Tableau/Intendance pour une mise
|
||||
à jour courante. [Contrat complet du serveur](server-pack.md).
|
||||
|
||||
Contrôler le JAR effectivement chargé, Fabric API, l'admission sans consommer
|
||||
un vrai code, les logs, le ping Minecraft et la fraîcheur des métriques Web.
|
||||
Un ping depuis le LAN via l'IP publique ne prouve pas un accès depuis un réseau
|
||||
extérieur. Ne pas remettre l'ancien secours UPnP : la règle Bbox permanente
|
||||
existe déjà.
|
||||
|
||||
## Actualiser le téléchargement et la version du site
|
||||
|
||||
Après vérification de la CT Beta, transférer le script versionné
|
||||
`scripts/publish_web_pack.py` sur **`gpu`** dans un dossier temporaire privé et
|
||||
vérifier que sa copie a le même SHA-256. Lancer sur `gpu`, où `pct` existe :
|
||||
|
||||
```sh
|
||||
python3 /chemin/temporaire/publish_web_pack.py \
|
||||
--web-ct 320 --server-ct 350 --version beta.xxx \
|
||||
--mrpack-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/Sanctuary-beta.xxx.mrpack \
|
||||
--prism-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/Sanctuary-Prism-beta.xxx.zip \
|
||||
--pack-url https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Si le contrôle passe, reprendre la même commande sans `--dry-run`. Ce script
|
||||
vérifie le serveur en marche, les deux profils, les archives et leurs hashes,
|
||||
puis bascule atomiquement `storage/app/distribution/current` dans la CT Web.
|
||||
Il ne modifie pas le code du site et ne redémarre pas Minecraft.
|
||||
|
||||
Le footer et le survol du bouton lisent cette publication via
|
||||
`App\Services\PackDistribution`. Ne pas remplacer leurs versions en dur ni
|
||||
reconfigurer le bouton à chaque livraison. Le bouton **Télécharger** donne
|
||||
directement le ZIP prêt ; le joueur l'importe une fois dans Prism puis lance
|
||||
pour télécharger le pack. Aucun guide manuel ni téléchargement séparé ne
|
||||
remplace ce parcours. L'ancien `/downloads/sanctuary-beta.mrpack` reste valide.
|
||||
|
||||
Vérifier le ZIP reçu anonymement (HTTP 200, nom `Sanctuary-Beta.zip`, SHA-256),
|
||||
le footer et le survol, puis le lien MRpack historique. Une bascule de version
|
||||
ne nécessite pas de reconstruire le cache Laravel.
|
||||
|
||||
## Livrer une modification du site
|
||||
|
||||
Travailler dans les sources **Sanctuary Web** et lire leur `AGENTS.md` ainsi
|
||||
que les règles/compétences applicables. Tester les comportements modifiés,
|
||||
formater le PHP avec `vendor/bin/pint --dirty --format agent`, construire les
|
||||
assets concernés et vérifier le rendu navigateur. Les contrôles existants
|
||||
pour cette chaîne sont `tests/Feature/DownloadTest.php` et
|
||||
`tests/Feature/ServerMetricsTest.php`.
|
||||
|
||||
**La CT 320 est un checkout de production avec des changements préexistants.**
|
||||
Ne pas y faire `git pull`, `reset --hard`, `clean`, ou une copie avec suppression
|
||||
depuis `origin/main`. Inspecter les fichiers concernés et leurs hashes, préparer
|
||||
un patch limité conservant les autres différences, sauvegarder les fichiers
|
||||
et caches affectés dans `/root/sanctuary-beta-maintenance/`, puis appliquer
|
||||
avec refus si les fichiers ont changé depuis l'inspection. Conserver
|
||||
propriétaires/modes et écrire les assets avant les vues qui les référencent.
|
||||
|
||||
Ne vider/reconstruire que les caches concernés : `view:clear` pour Blade ;
|
||||
`config:cache` si la configuration change ; `route:cache` si des routes changent
|
||||
et étaient déjà mises en cache. Exécuter Artisan comme `www-data` depuis
|
||||
`/var/www/sanctuary`. Aucun changement de base, réinitialisation de données ou
|
||||
migration ne découle d'une simple livraison de présentation.
|
||||
|
||||
Contrôler le site public, les métriques et la distribution. Les comptes admins,
|
||||
groupes staff et permissions restent identiques ; comparer leurs empreintes si
|
||||
l'intervention touche leur contexte. Les scripts ponctuels dans `build/`
|
||||
restent des reçus historiques, à ne pas rejouer avec leurs anciennes hypothèses.
|
||||
La procédure complémentaire du dépôt Web est `docs/updates.md`.
|
||||
|
||||
## Synchroniser l'instance locale existante
|
||||
|
||||
Retrouver l'instance **Sanctuary Beta** et lire sa configuration. Son dossier
|
||||
historique peut encore s'appeler `Sanctuary-0.1.0-alpha.1` ; ce nom n'indique
|
||||
pas sa version courante. Ne pas créer une nouvelle instance à chaque livraison,
|
||||
ne pas réimporter le ZIP et ne pas toucher aux anciennes instances 26.2.
|
||||
|
||||
Si la demande inclut la mise à jour locale : attendre la fermeture du jeu,
|
||||
sauvegarder les fichiers gérés et les métadonnées de l'instance hors de `mods/`,
|
||||
relever les hashes des données personnelles, puis exécuter sa commande packwiz
|
||||
avant lancement avec ses vrais `INST_JAVA`, `INST_MC_DIR`, `INST_DIR`.
|
||||
Pour une synchronisation sans lancer Minecraft, ajouter `-g` à l'installateur.
|
||||
Lors de la première livraison de l'habillage beta.291, sauvegarder aussi la
|
||||
commande existante et le JAR de l'installateur. Copier l'installateur vérifié du
|
||||
ZIP dans cette même instance et ajouter `--bootstrap-no-update` à sa commande
|
||||
avant lancement ; le canal de mods seul ne remplace pas cet outil. Les prochains
|
||||
changements de l'installateur suivent la même mise à jour explicite et vérifiée.
|
||||
Ne jamais lancer `instance.cfg` comme du shell ni évaluer une chaîne de commande
|
||||
non vérifiée. Les chemins avec espaces doivent rester des arguments séparés.
|
||||
|
||||
Faire deux passages et comparer : un seul JAR Sanctuary attendu, mêmes fichiers
|
||||
gérés au second passage, mêmes données personnelles avant/après. Conserver
|
||||
`saves/`, captures, favoris, réglages, packs graphiques et configurations tierces.
|
||||
Ne pas ouvrir un monde personnel pour prouver une simple synchronisation.
|
||||
|
||||
## Reprise et reçu de livraison
|
||||
|
||||
Pour un échec, conserver l'état/les logs, revenir au dernier artefact immuable
|
||||
et à la sauvegarde compatible. Un retour du canal doit former un nouveau commit
|
||||
de métadonnées sans réécrire l'historique. Une reprise du téléchargement Web
|
||||
rebascule son lien `current` vers une publication complète conservée ; elle ne
|
||||
change pas à elle seule le serveur ou le canal. Ne pas démarrer un ancien JAR
|
||||
sur des données migrées sans vérifier le contrat de sauvegarde.
|
||||
|
||||
Consigner pour chaque livraison : commit source, version/tag, commit du canal,
|
||||
hashes et URL des artefacts, CT/service et version chargée, publication Web,
|
||||
version locale effectivement synchronisée, sauvegardes, vérifications et limites.
|
||||
Mettre à jour la référence d'environnement de la compétence lorsque les cibles
|
||||
ou chemins changent. La mémorisation de cette procédure ne prétend pas que
|
||||
toutes les installations sont déjà synchronisées.
|
||||
+18
-1
@@ -522,7 +522,24 @@ Un sneak + clic droit permet de porter un mob sur sa tête, avec une limite d'un
|
||||
|
||||
### Hostiles et fantômes
|
||||
|
||||
Une famille de zombies inspirée de Left 4 Dead comprend le **Hunter**, le **Charger**, le **Spitter**, le **Boomer** et l'**Infecté**. Le **Tank** et le **zombie géant** sont des boss futurs.
|
||||
Une famille de zombies inspirée de Left 4 Dead comprend le **Hunter**, le **Charger**, le **Spitter**, le **Boomer**, le **Smoker** et l'**Infecté**. Le **Tank** et le **zombie géant** sont des boss futurs.
|
||||
|
||||
Les infectés n'ont pas de variante bébé. Leurs familiers utilisent le modèle
|
||||
adulte en miniature, avec une taille variable propre à chaque individu.
|
||||
Le Spitter dépose une zone d’acide persistante au sol, comme une potion
|
||||
persistante : sortir de la zone arrête les dégâts, sans poison résiduel.
|
||||
Le Boomer projette une bile à courte portée qui attire les zombies présents
|
||||
vers la cible enduite, et éclabousse en mourant sans abîmer le terrain. Son
|
||||
familier affaiblit les ennemis par sa bile et reste vivant après son pouvoir.
|
||||
Le Smoker doit toucher sa cible avec un lasso physique qui peut rater, puis
|
||||
la ramène vers lui par une traction de tyrolienne. Les obstacles ou les
|
||||
blessures de la source coupent ce câble temporaire.
|
||||
Le Hunter prépare puis bondit physiquement vers sa cible. Seul le contact
|
||||
inflige des dégâts ; la direction reste fixe après le départ, permettant
|
||||
l’esquive. Son familier miniature possède le même bond offensif.
|
||||
Depuis beta.287, le zombie classique et ces six infectés partagent neuf tenues
|
||||
aléatoires stables par individu. Tous gardent les bras classiques en avant. Le Hunter
|
||||
conserve sa posture accroupie au sol, familiers compris.
|
||||
|
||||
Le système d'insomnie et les phantoms vanilla disparaissent lorsque le monde passe au temps réel. Ils laissent place à d'autres fantômes, dont une **baleine volante, fantomatique et apaisante**, qui passe autour des joueurs et peut s'inviter dans le paysage comme un photobomb.
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# ZOMBIES-287 — tenues aléatoires et posture du Hunter
|
||||
|
||||
Branche `codex/zombie-appearances`, sur le socle local beta.286 conservé.
|
||||
État : implémenté et vérifié ; pack local beta.287 construit et contrôlé.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les neuf PNG fournis par le créateur remplacent les tenues fixes du zombie
|
||||
classique et des six infectés Sanctuary. Même collection aléatoire pour tous,
|
||||
choix stable par UUID natif ; les familiers utilisent leur identité persistante.
|
||||
Les fichiers adultes sont copiés à l’identique, sans génération d’image.
|
||||
Les bébés du zombie classique gardent leur modèle natif avec une adaptation
|
||||
UV déterministe ; les infectés restent exclusivement adultes, leurs familiers
|
||||
restent des adultes miniatures à taille variable.
|
||||
|
||||
Tous gardent les deux bras en avant avec l’animation native du zombie. Le
|
||||
Hunter conserve sa posture accroupie au sol, avec la marche native des jambes ;
|
||||
il se redresse pendant son bond. La même présentation s’applique aux familiers.
|
||||
La posture ne change ni les collisions, ni la vitesse, ni les dégâts, ni les
|
||||
règles de sauvegarde. Aucun nouvel état réseau ou format persistant n’est ajouté.
|
||||
Aucun changement de génération ou d’identifiant.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `python3 scripts/prepare_zombie_skins.py --check` : neuf fichiers adultes
|
||||
identiques aux PNG fournis et neuf adaptations bébé conformes aux UV natifs
|
||||
de `BabyZombieModel` 26.3. Reçu : `docs/zombie-skins-beta287.json`.
|
||||
- Parcours `:sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryZombie287ClientTests=true -PsanctuaryQuickTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8
|
||||
-PsanctuaryClientNoVsync=true` : **réussi**. Vulkan / MoltenVK 1.4.2 /
|
||||
Apple M1, monde plat de développement temporaire, graine `287`.
|
||||
Neuf tenues accessibles pour chacun des sept types ; texture stable après
|
||||
mouvement et reconstruction avec la même identité ; bébé classique avec son
|
||||
modèle natif ; les deux bras natifs en avant pour tous ; 21 combinaisons de
|
||||
familiers (sept types × tailles 500/1000/1500), proportions adultes des
|
||||
infectés, rééquipement sans changement de tenue ; Hunter réellement en marche
|
||||
accroupie, puis redressé et en l’air pendant son bond.
|
||||
Captures finales : `build/zombie287-screenshots/`.
|
||||
- Régressions `ziplines242,boomer284,spitter283,charger282,infected280,familiar260,companions` :
|
||||
**77 tests requis réussis**, en session distincte des joueurs simulés du Smoker
|
||||
et des arènes du Hunter.
|
||||
- Régression `smoker285` dans une autre session : **19 tests requis réussis**.
|
||||
Les comptes incluent chacun leur test d’infrastructure.
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=hunter286
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 2 min 40 s**, y compris les
|
||||
**16 tests Hunter** et tous les contrôles purs configurés par `check`.
|
||||
- Export packwiz, intégrité ZIP et métadonnées vérifiés : `beta.287`,
|
||||
Minecraft `26.3`, Fabric Loader `0.19.5`, un seul JAR Sanctuary identique
|
||||
au fichier construit. Les dix-huit PNG correspondent au reçu ; les neuf
|
||||
adultes correspondent aussi aux fichiers fournis. Classes de test exclues,
|
||||
rendu natif des bras confirmé, archive beta.286 conservée à l’identique.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.287](../build/Sanctuary-beta.287.mrpack), 16 713 076 octets.
|
||||
SHA-256 : `58c9375b8de43f77d55114bc8683a524416a4cb13eca7c6e18cb6fd93c3fd6f0`.
|
||||
- JAR `mods/sanctuary/build/libs/sanctuary-beta.287.jar`.
|
||||
SHA-256 : `26ba7a1f488896dd7e099e24113a1049ec96ea95f3dbcf0559456bd9cb688324`.
|
||||
- Reçu détaillé : `build/zombie287-validation.json`. Logs et captures finales
|
||||
ignorés sous `build/zombie287-*` ; aperçu recadré de la collection :
|
||||
`build/zombie287-wardrobe-preview.png`.
|
||||
|
||||
## Limites
|
||||
|
||||
La collection concerne le zombie classique et les six infectés ajoutés ici.
|
||||
Les noyés, husks et zombies villageois gardent leurs modèles et tenues propres.
|
||||
Les neuf fichiers adultes sont exploités tels quels ; seuls les dérivés destinés
|
||||
au bébé classique sont rééchantillonnés. Aucun infecté bébé n’est introduit.
|
||||
Les sauvegardes et réglages d’une installation personnelle ne sont pas touchés.
|
||||
@@ -0,0 +1,65 @@
|
||||
[
|
||||
{
|
||||
"source_file": "zombie.png",
|
||||
"asset": "zombie1.png",
|
||||
"sha256": "37cb6e101f6832172bd597341a6b1e37b215268e7ece5256079b4df30a98b4b4",
|
||||
"baby_asset": "zombie1_baby.png",
|
||||
"baby_sha256": "46e85f677761df2c54bed402fcc7f080342ad004e68375ad60494c229828be4d"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie2.png",
|
||||
"asset": "zombie2.png",
|
||||
"sha256": "3c84f78cd6c3beac5cd27b9ac4d2da4b8d0a679326dcdfc991271359aababbe6",
|
||||
"baby_asset": "zombie2_baby.png",
|
||||
"baby_sha256": "dbaf18b2c026b01c26dc9b3f156ca8757d43acca64a85a335e980d53046c6a58"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie3.png",
|
||||
"asset": "zombie3.png",
|
||||
"sha256": "af3e6a9f529276b853609e8389a6dea1aa92d3f2cdae6fdecb906424b959794a",
|
||||
"baby_asset": "zombie3_baby.png",
|
||||
"baby_sha256": "18249a78f397937ffdb335cf9eb3558c0a24a7bf4f488056c37227b94f880ba6"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie4.png",
|
||||
"asset": "zombie4.png",
|
||||
"sha256": "f7b4b9024efc810aef3c768a7a7024dbf90d2fabffd06031551d3f4bad8f4cd7",
|
||||
"baby_asset": "zombie4_baby.png",
|
||||
"baby_sha256": "55df3c6b44096503ca38ee5ad5322bfc957f993ebd1269b59038c8a95d7035e4"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie5.png",
|
||||
"asset": "zombie5.png",
|
||||
"sha256": "dc6b84f3f9b679c2de8c7cf29eaa0feb6b2762f0c9854d86a26f45123e697feb",
|
||||
"baby_asset": "zombie5_baby.png",
|
||||
"baby_sha256": "8fd1e604493ff472657d9162abb4b1144674cb4c8a4a333d32d7335bff9da018"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie6.png",
|
||||
"asset": "zombie6.png",
|
||||
"sha256": "948f1e9823a15ace01bbc3fdeae8f3a8448e4fc4f90c2d79f35fe95dac3f9e37",
|
||||
"baby_asset": "zombie6_baby.png",
|
||||
"baby_sha256": "08a008c5b0618242784381a74955e20ddcfcc4752b739b07eba318e1c9553daa"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie7.png",
|
||||
"asset": "zombie7.png",
|
||||
"sha256": "3494c1ecff8bd5dd39d483f8de3747c784dbef161a96aa731e505eea25fdd4bd",
|
||||
"baby_asset": "zombie7_baby.png",
|
||||
"baby_sha256": "661c6ac403226c38d15d7af40318ef4d1ccf71e9e7d4d34f69b5b182cb4e45bf"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie8.png",
|
||||
"asset": "zombie8.png",
|
||||
"sha256": "d9798a366c0c91629ceb7bb4414b8deaf85516e733ee6d1ff11efa568125356f",
|
||||
"baby_asset": "zombie8_baby.png",
|
||||
"baby_sha256": "5ea53fbe31de2ec5286906fc4589a6adf547d0b3c0c991dd81c7226a00dbcffb"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie9.png",
|
||||
"asset": "zombie9.png",
|
||||
"sha256": "99e918bebd7f449e9c36c123b0b4b4ad86cc0091d276785674180bd07361886b",
|
||||
"baby_asset": "zombie9_baby.png",
|
||||
"baby_sha256": "3391e90916558dfe31c08a9e298f058f917b0d4c64027f9deff5355912c9458d"
|
||||
}
|
||||
]
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.261
|
||||
pack_version=beta.261
|
||||
mod_version=beta.293
|
||||
pack_version=beta.293
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.4
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Adapted from packwiz-installer v0.5.14 InstallWindow.kt.
|
||||
* Copyright (c) 2021 comp500. MIT license, see LICENSE.
|
||||
* Sanctuary branding: KOKA99CAB, 2026.
|
||||
*/
|
||||
package link.infra.packwiz.installer.ui.gui;
|
||||
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Image;
|
||||
import java.util.Locale;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
/** Presentation only: GUIHandler retains all packwiz installation decisions. */
|
||||
public final class InstallWindow extends JFrame {
|
||||
private final boolean french = "fr".equals(Locale.getDefault().getLanguage());
|
||||
private final JLabel progressLabel;
|
||||
private final JProgressBar progressBar;
|
||||
private final JButton options;
|
||||
private final JButton cancel;
|
||||
private final JButton ok;
|
||||
private final JPanel buttons;
|
||||
|
||||
public InstallWindow(GUIHandler handler) {
|
||||
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||
setResizable(false);
|
||||
|
||||
ImageIcon logo = new ImageIcon(InstallWindow.class.getResource("/sanctuary/packwiz/logo.png"));
|
||||
JPanel header = new JPanel(new BorderLayout());
|
||||
header.setBackground(Color.BLACK);
|
||||
header.setBorder(new EmptyBorder(14, 24, 4, 24));
|
||||
JLabel wordmark = new JLabel(new ImageIcon(
|
||||
logo.getImage().getScaledInstance(640, 160, Image.SCALE_AREA_AVERAGING)));
|
||||
wordmark.getAccessibleContext().setAccessibleName("Sanctuary");
|
||||
header.add(wordmark, BorderLayout.CENTER);
|
||||
add(header, BorderLayout.NORTH);
|
||||
|
||||
JPanel progress = new JPanel(new BorderLayout(0, 10));
|
||||
progress.setBorder(new EmptyBorder(20, 24, 12, 24));
|
||||
progressBar = new JProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.getAccessibleContext().setAccessibleName(text("Progression", "Progress"));
|
||||
progressLabel = new JLabel(text("Chargement du pack…", "Loading the pack…"));
|
||||
// Keep long filenames from widening the window beyond the screen.
|
||||
progressLabel.setPreferredSize(new Dimension(640, 24));
|
||||
progress.add(progressBar, BorderLayout.NORTH);
|
||||
progress.add(progressLabel, BorderLayout.SOUTH);
|
||||
add(progress, BorderLayout.CENTER);
|
||||
|
||||
buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
|
||||
buttons.setBorder(new EmptyBorder(0, 16, 18, 16));
|
||||
options = new JButton(text("Mods optionnels…", "Optional mods…"));
|
||||
options.addActionListener(event -> {
|
||||
options.setText(text("Chargement…", "Loading…"));
|
||||
options.setEnabled(false);
|
||||
handler.setOptionsButtonPressed(true);
|
||||
});
|
||||
cancel = new JButton(text("Annuler", "Cancel"));
|
||||
cancel.addActionListener(event -> {
|
||||
cancel.setEnabled(false);
|
||||
handler.setCancelButtonPressed(true);
|
||||
});
|
||||
ok = new JButton(text("Continuer", "Continue"));
|
||||
ok.addActionListener(event -> handler.setOkButtonPressed(true));
|
||||
buttons.add(options);
|
||||
buttons.add(cancel);
|
||||
add(buttons, BorderLayout.SOUTH);
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
private String text(String fr, String en) {
|
||||
return french ? fr : en;
|
||||
}
|
||||
|
||||
public final void displayProgress(InstallProgress progress) {
|
||||
progressBar.setIndeterminate(!progress.getHasProgress());
|
||||
progressBar.setStringPainted(progress.getHasProgress());
|
||||
if (progress.getHasProgress()) {
|
||||
progressBar.setMaximum(progress.getProgressTotal());
|
||||
progressBar.setValue(progress.getProgress());
|
||||
progressBar.setString(progress.getProgress() + " / " + progress.getProgressTotal());
|
||||
} else {
|
||||
progressBar.setValue(0);
|
||||
}
|
||||
progressLabel.setText(progress.getMessage());
|
||||
progressLabel.setToolTipText(progress.getMessage());
|
||||
}
|
||||
|
||||
public final void disableOptionsButton(boolean hasOptions) {
|
||||
options.setText(hasOptions ? text("Mods optionnels…", "Optional mods…")
|
||||
: text("Aucun mod optionnel", "No optional mods"));
|
||||
options.setEnabled(false);
|
||||
}
|
||||
|
||||
public final void showOk(boolean hideCancel) {
|
||||
if (hideCancel) {
|
||||
buttons.remove(cancel);
|
||||
}
|
||||
if (ok.getParent() != buttons) {
|
||||
buttons.add(ok);
|
||||
}
|
||||
buttons.revalidate();
|
||||
buttons.repaint();
|
||||
}
|
||||
|
||||
public final void hideOk() {
|
||||
buttons.remove(ok);
|
||||
if (cancel.getParent() != buttons) {
|
||||
buttons.add(cancel);
|
||||
}
|
||||
buttons.revalidate();
|
||||
buttons.repaint();
|
||||
}
|
||||
|
||||
public final void timeoutOk(long remaining) {
|
||||
// Upstream GUIHandler invokes this callback from its countdown timer.
|
||||
if (!SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(() -> timeoutOk(remaining));
|
||||
return;
|
||||
}
|
||||
ok.setText(text("Continuer", "Continue") + " (" + remaining + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Swing integration check for the adapted window and the upstream GUIHandler. */
|
||||
import java.awt.Component;
|
||||
import java.awt.Container;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Window;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingUtilities;
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress;
|
||||
import link.infra.packwiz.installer.ui.gui.GUIHandler;
|
||||
import link.infra.packwiz.installer.ui.gui.InstallWindow;
|
||||
|
||||
public final class InstallWindowCheck {
|
||||
private static <T extends Component> T find(Container root, Class<T> type, String text) {
|
||||
for (Component child : root.getComponents()) {
|
||||
if (type.isInstance(child) && (text == null ||
|
||||
(child instanceof JButton && ((JButton) child).getText().equals(text)))) {
|
||||
return type.cast(child);
|
||||
}
|
||||
if (child instanceof Container) {
|
||||
T nested = find((Container) child, type, text);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
boolean fr = "fr".equals(Locale.getDefault().getLanguage());
|
||||
GUIHandler handler = new GUIHandler();
|
||||
try {
|
||||
handler.setTitle("Sanctuary · packwiz");
|
||||
handler.show();
|
||||
SwingUtilities.invokeAndWait(() -> {});
|
||||
InstallWindow window = null;
|
||||
for (Window candidate : Window.getWindows()) {
|
||||
if (candidate instanceof InstallWindow) window = (InstallWindow) candidate;
|
||||
}
|
||||
final InstallWindow frame = window;
|
||||
require(frame != null && frame.isVisible(), "Upstream handler did not show branded window");
|
||||
handler.submitProgress(new InstallProgress(fr ? "Téléchargement des mods…" : "Downloading mods…", 3, 10));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
JProgressBar bar = find(frame, JProgressBar.class, null);
|
||||
require(bar != null && !bar.isIndeterminate() && bar.getValue() == 3
|
||||
&& bar.getMaximum() == 10, "Upstream progress was not preserved");
|
||||
frame.showOk(false);
|
||||
});
|
||||
// Exercise the timer callback from outside the Swing thread.
|
||||
frame.timeoutOk(5);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
JButton next = find(frame, JButton.class, fr ? "Continuer (5)" : "Continue (5)");
|
||||
require(next != null, "Countdown or locale broken");
|
||||
next.doClick();
|
||||
require(handler.getOkButtonPressed(), "Continue no longer reaches upstream handler");
|
||||
frame.hideOk();
|
||||
// Capture actual Swing layout, including the bundled logo, in both locales.
|
||||
BufferedImage image = new BufferedImage(frame.getWidth(), frame.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
frame.paint(graphics);
|
||||
graphics.dispose();
|
||||
try { ImageIO.write(image, "png", new File(args[0])); }
|
||||
catch (Exception error) { throw new RuntimeException(error); }
|
||||
frame.showOk(true);
|
||||
require(find(frame, JButton.class, fr ? "Annuler" : "Cancel") == null,
|
||||
"Cancel should be hidden during the optional-mod countdown");
|
||||
frame.hideOk();
|
||||
JButton options = find(frame, JButton.class, fr ? "Mods optionnels…" : "Optional mods…");
|
||||
options.doClick();
|
||||
require(handler.getOptionsButtonPressed() && !options.isEnabled(), "Options callback broken");
|
||||
frame.disableOptionsButton(false);
|
||||
require(find(frame, JButton.class, fr ? "Aucun mod optionnel" : "No optional mods") != null,
|
||||
"Empty options label broken");
|
||||
find(frame, JButton.class, fr ? "Annuler" : "Cancel").doClick();
|
||||
require(handler.getCancelButtonPressed(), "Cancellation callback broken");
|
||||
frame.displayProgress(new InstallProgress("Loading…"));
|
||||
require(find(frame, JProgressBar.class, null).isIndeterminate(), "Indeterminate progress broken");
|
||||
});
|
||||
System.out.println("PASS: " + Locale.getDefault() + " logo/progress/options/continue/countdown/cancel");
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 comp500
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Fenêtre packwiz Sanctuary
|
||||
|
||||
`sanctuary.png` est le bandeau fourni par le créateur, conservé sans modification.
|
||||
`InstallWindow.java` adapte la seule fenêtre de progression de packwiz-installer
|
||||
v0.5.14, sous MIT. Le moteur officiel et ses autres interfaces restent intacts.
|
||||
Le contrat de distribution est dans [le ticket beta.291](../../docs/packwiz-logo-beta291.md).
|
||||
|
||||
Construire avec le JDK 25 utilisé par le dépôt :
|
||||
|
||||
```sh
|
||||
python3.14 scripts/packwiz_installer.py
|
||||
python3.14 scripts/test_packwiz_installer.py
|
||||
```
|
||||
|
||||
Le premier appel télécharge l'amont vérifié dans `build/packwiz-branding/`.
|
||||
Le JAR Sanctuary généré doit correspondre à `installer.json` ; ce verrou et
|
||||
`INSTALLER_SHA256` de `scripts/publish_web_pack.py` évoluent ensemble après revue
|
||||
et vérification de la nouvelle fenêtre. Ne pas remplacer le moteur ni désactiver
|
||||
le contrôle de hash pour faire passer une compilation différente.
|
||||
|
||||
Le parcours Swing réel FR/EN se vérifie sur un poste graphique, sans ouvrir
|
||||
Minecraft :
|
||||
|
||||
```sh
|
||||
javac -encoding UTF-8 -classpath build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
-d build/packwiz-branding/check launcher/packwiz/InstallWindowCheck.java
|
||||
java -Duser.language=fr -classpath build/packwiz-branding/check:build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
InstallWindowCheck build/packwiz-branding/preview-fr.png
|
||||
java -Duser.language=en -classpath build/packwiz-branding/check:build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
InstallWindowCheck build/packwiz-branding/preview-en.png
|
||||
```
|
||||
|
||||
Le séparateur de classpath est `;` sous Windows. Le parcours contrôle les
|
||||
callbacks du vrai `GUIHandler`, la progression et le compte à rebours ; il
|
||||
capture la fenêtre et la ferme. Les captures ne font pas partie du JAR livré.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"upstream_version": "v0.5.14",
|
||||
"upstream_url": "https://github.com/packwiz/packwiz-installer/releases/download/v0.5.14/packwiz-installer.jar",
|
||||
"upstream_sha256": "c9f646908d340d84773948a9a7d98bc1dae250d35e1016dc6e2b8459760b5598",
|
||||
"logo_sha256": "efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157",
|
||||
"installer_sha256": "9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -27,8 +27,10 @@ le cache ignoré manque, extrait le commit fixé et applique `patches/26.3.patch
|
||||
La première reconstruction nécessite Internet, Git, patch et Python 3.
|
||||
`./gradlew :jei:sourcesJar` produit aussi les sources Java effectivement compilées.
|
||||
|
||||
Depuis beta.257, JEI est un JAR client séparé et facultatif. Sanctuary conserve
|
||||
son pont de catalogue ; le retrait de JEI ne supprime aucune connaissance serveur.
|
||||
Depuis beta.280, JEI est de nouveau embarqué dans Sanctuary, avec ses métadonnées
|
||||
Fabric, ses entrées communes/cliente et son pont de catalogue. Il n'est plus
|
||||
distribué séparément dans packwiz. La séparation facultative beta.257–278
|
||||
n'a modifié aucune connaissance serveur ; ces données restent conservées.
|
||||
JEI conserve ses deux bibliothèques internes nécessaires à la recherche :
|
||||
|
||||
- `net.mezzdev:baked-substring-index:0.1.0`, MIT, James Mitchell.
|
||||
|
||||
+133
-1
@@ -64,6 +64,7 @@ if (clientRenderTests) {
|
||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
||||
}
|
||||
systemProperty('sanctuary.test.external257Integration', providers.gradleProperty('sanctuaryExternal257Integration').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.optional292WhaleOnly', providers.gradleProperty('sanctuaryOptional292WhaleOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.ssr', providers.gradleProperty('sanctuarySsrClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.shaderPerformance254', providers.gradleProperty('sanctuaryShaderPerformance254').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.shaderMovingSun254', providers.gradleProperty('sanctuaryShaderMovingSun254').getOrElse('false'))
|
||||
@@ -151,6 +152,12 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def happyCreeper = providers.gradleProperty('sanctuaryHappyCreeperClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryHappyCreeperClientTests', happyCreeper)
|
||||
if (happyCreeper) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.HappyCreeperClientChecks') }
|
||||
def optional292 = providers.gradleProperty('sanctuaryOptional292ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryOptional292ClientTests', optional292)
|
||||
if (optional292) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Optional292ClientChecks') }
|
||||
def external257 = providers.gradleProperty('sanctuaryExternal257ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryExternal257ClientTests', external257)
|
||||
if (external257) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.External257ClientChecks') }
|
||||
@@ -208,6 +215,10 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('sanctuaryColored139ClientTests', colored139Client)
|
||||
if (colored139Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.ColoredLights139ClientChecks') }
|
||||
|
||||
def handLightClient = providers.gradleProperty('sanctuaryHandLightClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryHandLightClientTests', handLightClient)
|
||||
if (handLightClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.HandLight283ClientChecks') }
|
||||
|
||||
def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPbr134ClientTests', pbr134Client)
|
||||
if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') }
|
||||
@@ -278,7 +289,7 @@ tasks.named('processGametestResources') {
|
||||
|
||||
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
|
||||
def focusClasses = [familiar260: 'Familiar260GameTests', census: 'CensusGameTestsTerrain', insects245: 'Insect245GameTests', map244: 'Atlas244GameTests', ziplines242: 'Zipline242GameTests', waystones239: 'Waystone239GameTests', waystonesLarge239: 'WaystoneLarge239GameTests', forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
def focusClasses = [happycreeper: 'HappyCreeperGameTests', whale290: 'FlyingWhale290GameTests', hunter286: 'Hunter286GameTests', smoker285: 'Smoker285GameTests', boomer284: 'Boomer284GameTests', spitter283: 'Spitter283GameTests', charger282: 'Charger282GameTests', infected280: 'Infected280GameTests', claims278: 'ClaimBattle278GameTests', claims277: 'ClaimDemeure277GameTests', menu275: 'Menu275GameTests', claims273: 'ClaimMap273GameTests', claims272: 'ChunkClaims272GameTests', overview271: 'Inhabitant271GameTests', property270: 'Property270GameTests', providence269: 'Providence269GameTests', providence268: 'Providence268GameTests', providence265: 'Providence265GameTests', familiar260: 'Familiar260GameTests', census: 'CensusGameTestsTerrain', insects245: 'Insect245GameTests', map244: 'Atlas244GameTests', ziplines242: 'Zipline242GameTests', waystones239: 'Waystone239GameTests', waystonesLarge239: 'WaystoneLarge239GameTests', forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
|
||||
inputs.property('focusedNative', focused)
|
||||
def yellowRock222Client = providers.gradleProperty('sanctuaryYellowRock222ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -400,6 +411,33 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('insects245Client', insects245Client)
|
||||
if (insects245Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Insect245ClientChecks') }
|
||||
|
||||
def zombie287Client = providers.gradleProperty('sanctuaryZombie287ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('zombie287Client', zombie287Client)
|
||||
if (zombie287Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Zombie287ClientChecks') }
|
||||
def whale290Client = providers.gradleProperty('sanctuaryWhale290ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('whale290Client', whale290Client)
|
||||
if (whale290Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.FlyingWhale290ClientChecks') }
|
||||
def hunter286Client = providers.gradleProperty('sanctuaryHunter286ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('hunter286Client', hunter286Client)
|
||||
if (hunter286Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Hunter286ClientChecks') }
|
||||
def smoker285Client = providers.gradleProperty('sanctuarySmoker285ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('smoker285Client', smoker285Client)
|
||||
if (smoker285Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Smoker285ClientChecks') }
|
||||
def boomer284Client = providers.gradleProperty('sanctuaryBoomer284ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('boomer284Client', boomer284Client)
|
||||
if (boomer284Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Boomer284ClientChecks') }
|
||||
def spitter283Client = providers.gradleProperty('sanctuarySpitter283ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('spitter283Client', spitter283Client)
|
||||
if (spitter283Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Spitter283ClientChecks') }
|
||||
def charger282Client = providers.gradleProperty('sanctuaryCharger282ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('charger282Client', charger282Client)
|
||||
if (charger282Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Charger282ClientChecks') }
|
||||
def infected280Client = providers.gradleProperty('sanctuaryInfected280ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('infected280Client', infected280Client)
|
||||
if (infected280Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Infected280ClientChecks') }
|
||||
def map279Client = providers.gradleProperty('sanctuaryMap279ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('map279Client', map279Client)
|
||||
if (map279Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Map279ClientChecks') }
|
||||
def map244Client = providers.gradleProperty('sanctuaryMap244ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('map244Client', map244Client)
|
||||
if (map244Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Map244ClientChecks') }
|
||||
@@ -486,6 +524,41 @@ tasks.named('processGametestResources') {
|
||||
def starterNameClient = providers.gradleProperty('sanctuaryStarterNameClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('starterNameClient', starterNameClient)
|
||||
if (starterNameClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Starter052ClientChecks') }
|
||||
def providence265Client = providers.gradleProperty('sanctuaryProvidence265ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryProvidence265ClientTests', providence265Client)
|
||||
if (providence265Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Providence265ClientChecks') }
|
||||
def claims278Client = providers.gradleProperty('sanctuaryClaims278ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryClaims278ClientTests', claims278Client)
|
||||
if (claims278Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ClaimBattle278ClientChecks') }
|
||||
def claims277Client = providers.gradleProperty('sanctuaryClaims277ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryClaims277ClientTests', claims277Client)
|
||||
if (claims277Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ClaimDemeure277ClientChecks') }
|
||||
def claims276Client = providers.gradleProperty('sanctuaryClaims276ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryClaims276ClientTests', claims276Client)
|
||||
if (claims276Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ClaimMap276ClientChecks') }
|
||||
def menu275Client = providers.gradleProperty('sanctuaryMenu275ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMenu275ClientTests', menu275Client)
|
||||
if (menu275Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Menu275ClientChecks') }
|
||||
def map274Client = providers.gradleProperty('sanctuaryMap274ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryMap274ClientTests', map274Client)
|
||||
if (map274Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Map274ClientChecks') }
|
||||
def demeure274Client = providers.gradleProperty('sanctuaryDemeure274ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryDemeure274ClientTests', demeure274Client)
|
||||
if (demeure274Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Demeure274ClientChecks') }
|
||||
def claims273Client = providers.gradleProperty('sanctuaryClaims273ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryClaims273ClientTests', claims273Client)
|
||||
if (claims273Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ClaimMap273ClientChecks') }
|
||||
def claims272Client = providers.gradleProperty('sanctuaryClaims272ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryClaims272ClientTests', claims272Client)
|
||||
if (claims272Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ChunkClaims272ClientChecks') }
|
||||
def inhabitant271Client = providers.gradleProperty('sanctuaryInhabitant271ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryInhabitant271ClientTests', inhabitant271Client)
|
||||
def property270Client = providers.gradleProperty('sanctuaryProperty270ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryProperty270ClientTests', property270Client)
|
||||
if (property270Client || inhabitant271Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Property270ClientChecks') }
|
||||
def hello262Client = providers.gradleProperty('sanctuaryHello262ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryHello262ClientTests', hello262Client)
|
||||
if (hello262Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Hello262ClientChecks') }
|
||||
def head050Client = providers.gradleProperty('sanctuaryHead050ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('head050Client', head050Client)
|
||||
if (head050Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Head050ClientChecks') }
|
||||
@@ -635,6 +708,20 @@ tasks.named('processGametestResources') {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Extra mods are independent pack dependencies, never nested in Sanctuary.
|
||||
def extras = new groovy.json.JsonSlurper().parse(rootProject.file('scripts/data/optional-mods.json'))
|
||||
def clientExtras = providers.gradleProperty('sanctuaryOptional292ClientTests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryOptionalModsRuntime').map { it.toBoolean() }.getOrElse(false)
|
||||
def serverExtras = providers.gradleProperty('sanctuaryOptional292ServerTests').map { it.toBoolean() }.getOrElse(false)
|
||||
if (clientExtras || serverExtras) {
|
||||
extras.each { slug, pin ->
|
||||
if (clientExtras || pin.side == 'both') {
|
||||
runtimeOnly("maven.modrinth:${pin.project}:${pin.artifact}") { transitive = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientRenderTests) extras.each { slug, pin ->
|
||||
compileOnly("maven.modrinth:${pin.project}:${pin.artifact}") { transitive = false }
|
||||
}
|
||||
// Optional adapter is compiled against the exact pack version, never embedded.
|
||||
compileOnly 'maven.modrinth:AANobbMI:bAZQdGpg'
|
||||
if (providers.gradleProperty('sanctuarySodiumClientTests').map { it.toBoolean() }.getOrElse(false)) {
|
||||
@@ -651,6 +738,8 @@ dependencies {
|
||||
if (providers.gradleProperty('sanctuaryJeiRuntime').map { it.toBoolean() }.getOrElse(true)) {
|
||||
runtimeOnly project(':jei')
|
||||
}
|
||||
// One distributed Sanctuary JAR; keep the exact JEI fork and its own metadata.
|
||||
include project(':jei')
|
||||
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}"
|
||||
@@ -1473,3 +1562,46 @@ tasks.register('exportStandaloneLaunch') {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Providence's interpreter, decisions and persistence run independently of Minecraft.
|
||||
tasks.register('providence265Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.providence.Providence265Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('providence265Smoke') }
|
||||
|
||||
tasks.register('providence269Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.providence.Providence269Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('providence269Smoke') }
|
||||
|
||||
tasks.register('property270Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.property.Property270Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('property270Smoke') }
|
||||
|
||||
tasks.register('chunkClaims272Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.property.ChunkClaims272Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('chunkClaims272Smoke') }
|
||||
|
||||
tasks.register('playTime275Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.inhabitant.PlayTime275Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('playTime275Smoke') }
|
||||
|
||||
tasks.register('claimDuel278Smoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.property.ClaimDuel278Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('claimDuel278Smoke') }
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public final class Access166ClientChecks implements FabricClientGameTest {
|
||||
if(!running.get()||!(m.gui.screen() instanceof HelloWorldScreen screen))return;
|
||||
if(!filled.getAndSet(true)){
|
||||
welcome.set(screen);
|
||||
check(screen.children().stream().anyMatch(child->child instanceof Button b && b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals("sanctuary.progression.get_code")),"Registration link is offered by configured server");
|
||||
check(screen.children().stream().anyMatch(child->child instanceof Button b && b.getMessage().getContents() instanceof TranslatableContents t && (t.getKey().equals("sanctuary.progression.get_code")||t.getKey().equals("sanctuary.progression.get_code_short"))),"Registration link is offered by configured server");
|
||||
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());
|
||||
for(var child:screen.children())if(child instanceof EditBox box && box.getMessage().getContents() instanceof TranslatableContents text){
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.BoomerEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Boomer284ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<BoomerEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var preparing=new AtomicBoolean();var coated=new AtomicBoolean();var lured=new AtomicBoolean();
|
||||
var cow=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.animal.cow.Cow>();
|
||||
var recruit=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var mob=wild.get();if(mob!=null&&!mob.isRemoved()){
|
||||
preparing.set(mob.windingUp());if(cow.get()!=null)coated.set(cow.get().hasEffect(net.minecraft.world.effect.MobEffects.GLOWING));
|
||||
if(recruit.get()!=null)lured.set(recruit.get().getTarget()==cow.get());
|
||||
}});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Boomer284-"+UUID.randomUUID());s.setSeed("284");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
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());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.BOOMER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.BOOMER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:boomer"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.BOOMER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/boomer.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.BOOMER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
var pet=FamiliarService.active(p);pet.teleportTo(.5,1,2.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,4.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);c.waitTicks(24);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(enemy.get().getHealth()<100&&enemy.get().hasEffect(net.minecraft.world.effect.MobEffects.WEAKNESS)&&enemy.get().hasEffect(net.minecraft.world.effect.MobEffects.GLOWING),"Real G splashes, weakens and reveals the enemy");
|
||||
check(pet.isAlive()&&pet.getHealth()==14&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"G keeps the miniature alive and commits cooldown");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("boomer284-bile-burst");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(20);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(FamiliarBattle.state(p).cast==null,"Signature and first automatic preparation finish normally");
|
||||
enemy.get().setPermanentlyInvulnerable(true);enemy.get().removeAllEffects();enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("boomer284-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.BOOMER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.BOOMER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Gerbe de bile":"Bile spray";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("boomer284-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
var target=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(.5,1,8.5);p.level().addFreshEntity(target);cow.set(target);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setPos(-3.5,1,10.5);p.level().addFreshEntity(zombie);recruit.set(zombie);
|
||||
var mob=wild.get();mob.setPermanentlyInvulnerable(false);mob.setTarget(target);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.waitFor(m->preparing.get(),200);c.waitTicks(2);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("boomer284-windup");
|
||||
c.waitFor(m->coated.get()&&lured.get(),150);c.waitTicks(3);c.takeScreenshot("boomer284-wild-bile");
|
||||
server.runOnServer(s->{check(cow.get().hasEffect(net.minecraft.world.effect.MobEffects.WEAKNESS)&&recruit.get().getTarget()==cow.get(),"Wild bile marks its victim and redirects an existing native zombie");
|
||||
var mob=wild.get();mob.setNoAi(true);recruit.get().setNoAi(true);recruit.get().teleportTo(-5.5,1,9.5);
|
||||
cow.get().teleportTo(mob.getX(),1,mob.getZ()+1.5);cow.get().setHealth(100);cow.get().setInvulnerableTime(0);cow.get().removeAllEffects();
|
||||
mob.hurtServer((net.minecraft.server.level.ServerLevel)mob.level(),mob.damageSources().generic(),100);
|
||||
check(!mob.isAlive()&&cow.get().getHealth()==98&&cow.get().hasEffect(net.minecraft.world.effect.MobEffects.GLOWING),"Native lethal damage triggers the Boomer's harmless-to-terrain death splash");
|
||||
});c.waitTicks(2);c.takeScreenshot("boomer284-death-splash");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
wild.get().discard();wild.set(null);cow.get().discard();enemy.get().discard();recruit.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(20)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("BOOMER284_CLIENT_PASS: Vulkan, adult skin, three miniature sizes, real G bile/effects/cooldown without sacrifice, wild windup/bile/recruitment/death splash, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.ChargerEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Charger282ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<ChargerEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var phase=new java.util.concurrent.atomic.AtomicReference<>(ChargerEntity.Phase.IDLE);
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var mob=wild.get();if(mob!=null&&!mob.isRemoved())phase.set(mob.phase());});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Charger282-"+UUID.randomUUID());s.setSeed("282");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
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());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.CHARGER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.CHARGER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:charger"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.CHARGER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/charger.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.CHARGER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,6.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);c.waitTicks(90);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(enemy.get().getHealth()<100,"Native G input triggers a real straight familiar charge and impact: ready="+FamiliarBattle.bond(p).readyAt()+" pet="+FamiliarService.active(p).position()+" cast="+FamiliarBattle.state(p).cast+" message="+(FamiliarClient.view==null?null:FamiliarClient.view.message()));
|
||||
check(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"G commits cooldown and finishes recovery");
|
||||
enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});c.waitTicks(10);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("charger282-adult-zombie");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("charger282-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.CHARGER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.CHARGER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Coup d’épaule":"Shoulder strike";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("charger282-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
var cow=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.animal.cow.Cow>();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
var target=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(.5,1,13.5);p.level().addFreshEntity(target);cow.set(target);
|
||||
var mob=wild.get();mob.setPermanentlyInvulnerable(false);mob.setTarget(target);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.waitFor(m->phase.get()==ChargerEntity.Phase.WINDUP,200);c.waitTicks(3);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("charger282-windup");
|
||||
c.waitFor(m->phase.get()==ChargerEntity.Phase.CHARGING,100);c.waitTicks(2);c.takeScreenshot("charger282-charging");
|
||||
c.waitFor(m->phase.get()==ChargerEntity.Phase.RECOVERING,100);
|
||||
server.runOnServer(s->check(cow.get().getHealth()==96,"The wild charger reaches actual body contact and deals four damage"));
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
wild.get().discard();wild.set(null);cow.get().discard();enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(20)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("CHARGER282_CLIENT_PASS: Vulkan, adult skin, three miniature sizes, native G impact and cooldown, wild windup/charge/impact, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class ChunkClaims272ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;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);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Claims272 "+UUID.randomUUID());s.setSeed("272");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);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);});
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.isComplete()&&map.status().equals("ready"),400);
|
||||
c.runOnClient(m->{require(!ProgressionClient.progress.atlas(),"Property map does not grant the Atlas aptitude");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.eligible()&&d.count()==0&&d.cost()==0,"Hello World admits free chunk selection");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims272-map-free");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.claim_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).count()==1,200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),64));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(5);c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).sapphires()==64,200);
|
||||
c.waitTicks(3);c.takeScreenshot("claims272-map-sapphires");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.protect_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).message().equals("protected"),200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(ChunkPropertyService.sapphires(p)==0,"Map action debits full stack");p.setExperienceLevels(100);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase Atlas normally");ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);c.waitTicks(30);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.runOnClient(m->rightClick(m.gui.screen()));
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.x()==9&&d.z()==8,"Ordinary Atlas right-click selects actual chunk without a mode toggle");widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible).forEach(b->require(b.getX()>=0&&b.getRight()<=m.gui.screen().width&&b.getBottom()<=m.gui.screen().height,"Visible controls fit"));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims272-map-protected-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));require(m.gui.screen() instanceof SanctuaryAtlasScreen&&view(m.gui.screen())==null,"Escape closes the context panel and keeps the map");});
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private static ChunkPropertyPayloads.View view(Screen screen){return screen instanceof SanctuaryAtlasScreen map?map.propertyView():null;}
|
||||
private static void rightClick(Screen screen){var atlas=(SanctuaryAtlasScreen)screen;require(atlas.mouseClicked(new MouseButtonEvent(atlas.width/2.+48,(atlas.contentTop+atlas.contentBottom)/2.+16,new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false),"Right click is handled on the terrain");}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.arena.*;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.gems.Gems;
|
||||
import fr.koka.demeure.DemeureService;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.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.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.*;
|
||||
|
||||
/** Real claim proposals through the network, explicit owner consent and native Battle deaths. */
|
||||
public final class ClaimBattle278ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
private final List<ServerPlayer> guests=new ArrayList<>();
|
||||
private final AtomicReference<ClaimDuels.Offer> offer=new AtomicReference<>();
|
||||
private final AtomicReference<ServerPlayer> opponent=new AtomicReference<>();
|
||||
private long paid;private final AtomicLong nextBattleAt=new AtomicLong();
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;var intro=new AtomicBoolean(true);var key=new ChunkClaims.Key(9,8);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{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);button(s,"sanctuary.progression.enter").onPress(ENTER);intro.set(false);}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ClaimBattle278 "+UUID.randomUUID());s.setSeed("278");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=host(s);p.teleportTo(128,4,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);p.setExperienceLevels(20);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase actual Atlas");ProgressionService.send(p,"");claim(s,p,key);var guest=guest(p,"Nora");opponent.set(guest);inhabit(p,key,1);inhabit(guest,key,1);require(ClaimDuels.propose(guest,key,ChunkPropertyService.store(s).revision()).equals("sent"),"Remote challenger explicitly proposes the claim stake");offer.set(ClaimDuels.at(s,key));});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);
|
||||
// Owner reaches the review using the existing map context.
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(9,8)!=null,400);
|
||||
c.runOnClient(m->rightClick((SanctuaryAtlasScreen)m.gui.screen(),152,136));c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).propertyView()!=null,200);
|
||||
c.waitTicks(3);c.runOnClient(m->button(m.gui.screen(),"sanctuary.claim_duel.map_button").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof ClaimDuelScreen&&dataOffer("OFFER"),200);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new ClaimDuelScreen(null,key));});
|
||||
c.waitTicks(3);c.takeScreenshot("claim278-owner-terms-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{require(button(m.gui.screen(),"sanctuary.claim_duel.accept").active,"Owner has an explicit property acceptance button");require(widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w->key(w).equals("sanctuary.claim_duel.consequences")),"Property transfer and real death consequences are visible before agreement");bottom(m.gui.screen());});
|
||||
c.waitTicks(3);c.takeScreenshot("claim278-owner-consent-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claim_duel.decline").onPress(ENTER));
|
||||
c.waitFor(m->ClaimDuelClient.data!=null&&ClaimDuelClient.data.has("offer")&&ClaimDuelClient.data.getAsJsonObject("offer").get("result").getAsString().equals("declined"),200);
|
||||
server.runOnServer(s->{require(Arenas.match(host(s))==null&&ChunkPropertyService.store(s).get(key).owner().equals(host(s).getUUID()),"Real decline packet starts no fight and changes no claim");var guest=guest(host(s),"Camille");opponent.set(guest);inhabit(guest,key,1);require(ClaimDuels.propose(guest,key,ChunkPropertyService.store(s).revision()).equals("sent"),"Fresh challenger proposes a new immutable offer");offer.set(ClaimDuels.at(s,key));});
|
||||
c.runOnClient(m->ClaimDuelClient.open(null,key));c.waitFor(m->dataOffer("OFFER")&&ClaimDuelClient.data.getAsJsonObject("offer").get("id").getAsString().equals(offer.get().id.toString()),200);
|
||||
c.waitTicks(3);c.runOnClient(m->{bottom(m.gui.screen());button(m.gui.screen(),"sanctuary.claim_duel.accept").onPress(ENTER);});
|
||||
c.waitFor(m->dataOffer("OPEN"),200);c.waitTicks(3);c.takeScreenshot("claim278-mutual-agreement");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claim_duel.battle").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof ArenaScreen&&ArenaClient.data.has("match"),200);
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->{require(ArenaClient.data.getAsJsonObject("match").has("claim"),"Battle prominently carries the exact property stake");bottom(m.gui.screen());button(m.gui.screen(),"sanctuary.arena.ready").onPress(ENTER);});
|
||||
c.waitFor(m->ArenaClient.data.has("match")&&ArenaClient.data.getAsJsonObject("match").get("ready").getAsBoolean(),200);
|
||||
server.runOnServer(s->{var m=offer.get().match;Arenas.ready(m,opponent.get());require(Arenas.start(m,opponent.get()).equals("started"),"Both players ready before the challenger launches native Battle");m.until=FamiliarBattle.now();Arenas.tick(s);opponent.get().setInvulnerableTime(0);require(opponent.get().hurtServer(opponent.get().level(),opponent.get().damageSources().playerAttack(host(s)),100),"Owner wins through native player damage");require(m.phase==Arenas.Phase.FINISHED&&m.winner.equals(host(s).getUUID())&&ChunkPropertyService.store(s).get(key).owner().equals(host(s).getUUID()),"Defender victory preserves existing ownership");nextBattleAt.set(FamiliarBattle.now()+FamiliarBattle.COMBAT_MILLIS+100);});
|
||||
// Respect Battle's existing post-combat timer before a new dispute.
|
||||
c.waitFor(m->System.currentTimeMillis()>=nextBattleAt.get(),600);
|
||||
// Now the real client challenges someone else's protected chunk.
|
||||
var second=new ChunkClaims.Key(10,8);
|
||||
server.runOnServer(s->{require(!FamiliarBattle.fighting(host(s)),"Native post-combat cooldown elapsed normally");var guest=guest(host(s),"Benoit");opponent.set(guest);claim(s,guest,second);paid=ChunkPropertyService.store(s).get(second).until();inhabit(host(s),second,1);inhabit(guest,second,1);var p=host(s);require(ClaimDuels.canPropose(p,second).isEmpty(),"Second duel setup: reason="+ClaimDuels.canPropose(p,second)+" alive="+p.isAlive()+" removed="+p.isRemoved()+" creative="+p.isCreative()+" spectator="+p.isSpectator()+" arena="+Arenas.enrolled(p)+" familiarDuel="+(FamiliarDuels.match(p)!=null)+" fighting="+FamiliarBattle.fighting(p)+" pendingSelf="+(ChunkPropertyService.store(s).pending(p.getUUID())!=null)+" pendingOwner="+(ChunkPropertyService.store(s).pending(guest.getUUID())!=null)+" selectable="+ChunkPropertyService.selectable(p,second));});
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",160,4,136))));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,8)!=null,400);
|
||||
c.runOnClient(m->rightClick((SanctuaryAtlasScreen)m.gui.screen(),168,136));c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).propertyView()!=null,200);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claim_duel.map_button").onPress(ENTER));c.waitFor(m->m.gui.screen() instanceof ClaimDuelScreen&&ClaimDuelClient.data!=null&&!ClaimDuelClient.data.has("offer"),200);
|
||||
c.waitTicks(3);c.runOnClient(m->{bottom(m.gui.screen());require(button(m.gui.screen(),"sanctuary.claim_duel.propose").active,"Challenger proposal must be available: "+ClaimDuelClient.data);button(m.gui.screen(),"sanctuary.claim_duel.propose").onPress(ENTER);});c.waitFor(m->dataOffer("OFFER")||!ClaimDuelClient.data.get("message").getAsString().isEmpty(),200);c.runOnClient(m->require(dataOffer("OFFER"),"Proposal response: "+ClaimDuelClient.data));
|
||||
c.waitTicks(3);c.takeScreenshot("claim278-challenger-waiting-owner");
|
||||
server.runOnServer(s->{offer.set(ClaimDuels.at(s,second));require(Arenas.match(host(s))==null,"Client proposal alone cannot force its opponent into Battle");require(ClaimDuels.accept(opponent.get(),offer.get().id).equals("accepted"),"Other property owner independently accepts the exact proposal");});
|
||||
c.waitFor(m->dataOffer("OPEN"),200);c.runOnClient(m->button(m.gui.screen(),"sanctuary.claim_duel.battle").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof ArenaScreen&&ArenaClient.data.has("match"),200);
|
||||
c.waitTicks(3);c.runOnClient(m->{bottom(m.gui.screen());button(m.gui.screen(),"sanctuary.arena.ready").onPress(ENTER);});
|
||||
c.waitFor(m->ArenaClient.data.has("match")&&ArenaClient.data.getAsJsonObject("match").get("ready").getAsBoolean(),200);
|
||||
server.runOnServer(s->Arenas.ready(offer.get().match,opponent.get()));
|
||||
c.waitFor(m->ArenaClient.data.getAsJsonObject("match").getAsJsonArray("entrants").asList().stream().allMatch(v->v.getAsJsonObject().get("ready").getAsBoolean()),200);
|
||||
for(String locale:List.of("fr_fr","en_us")){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new ArenaScreen(null));});
|
||||
c.waitTicks(3);c.takeScreenshot("claim278-battle-stake-"+locale);
|
||||
}
|
||||
c.waitTicks(3);c.runOnClient(m->{bottom(m.gui.screen());button(m.gui.screen(),"sanctuary.arena.start").onPress(ENTER);});
|
||||
c.waitFor(m->ArenaClient.data.getAsJsonObject("match").get("phase").getAsString().equals("COUNTDOWN"),200);
|
||||
server.runOnServer(s->{offer.get().match.until=FamiliarBattle.now();Arenas.tick(s);});
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",160,4,136))));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,8)!=null&&map.claimOverlay.get(10,8).claim().owner().equals(opponent.get().getUUID()),400);
|
||||
c.waitTicks(3);c.takeScreenshot("claim278-map-before-result");
|
||||
server.runOnServer(s->{var p=host(s);var loser=opponent.get();loser.setInvulnerableTime(0);require(loser.hurtServer(loser.level(),loser.damageSources().playerAttack(p),100),"Native challenger victory");var claim=ChunkPropertyService.store(s).get(second);require(claim.owner().equals(p.getUUID())&&claim.until()==paid,"Battle transfers ownership and preserves the exact paid expiry");require(ChunkPropertyService.store(s).get(key).owner().equals(p.getUUID()),"Other property is unchanged");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,8).claim().owner().equals(m.player.getUUID()),200);
|
||||
c.waitTicks(3);c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.get(9,8).claim().color()==map.claimOverlay.get(10,8).claim().color(),"Transferred claim uses winner's persistent color");require(map.claimOverlay.ink(9,8).pixel(15,8)==map.claimOverlay.tint(9,8),"CTM removes the former ownership boundary");});
|
||||
c.takeScreenshot("claim278-map-live-transfer");
|
||||
server.runOnServer(s->{for(var p:guests){var m=Arenas.match(p);if(m!=null)Arenas.finish(m,null,"interrupted");s.getPlayerList().getPlayersByUUID().remove(p.getUUID());p.discard();}});
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;intro.set(false);guests.clear();}
|
||||
}
|
||||
private ServerPlayer guest(ServerPlayer host,String name){
|
||||
var s=host.level().getServer();var profile=new com.mojang.authlib.GameProfile(UUID.randomUUID(),name);var p=new ServerPlayer(s,host.level(),profile,ClientInformation.createDefault());
|
||||
var connection=new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND);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.setGameMode(net.minecraft.world.level.GameType.SURVIVAL);p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.setHealth(6);p.setNoGravity(true);p.setPos(host.getX()+4,host.getY(),host.getZ()+2);p.getAbilities().invulnerable=false;p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
|
||||
var identities=ProgressionService.session(s).inhabitants;try{require(identities.create(p.getUUID(),name,"",identities.available().getFirst()),"Create guest identity");identities.arrive(p.getUUID());}catch(Exception e){throw new RuntimeException(e);}
|
||||
p.setExperienceLevels(20);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Guest has real Atlas");s.getPlayerList().getPlayersByUUID().put(p.getUUID(),p);guests.add(p);return p;
|
||||
}
|
||||
private static void claim(MinecraftServer s,ServerPlayer p,ChunkClaims.Key key){try{p.level().getChunk(key.x(),key.z());var c=ChunkPropertyService.store(s);var tx=c.prepare(p.getUUID(),"claim",key,c.revision());c.recover(p.getUUID(),tx.receipt().toString());p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(Gems.SAPPHIRE.gem(),8));require(ChunkPropertyService.act(p,"protect",key,c.revision()).equals("protected"),"Pay eight sapphires for actual protection");}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void inhabit(ServerPlayer p,ChunkClaims.Key key,double amount){DemeureService.record(p,new BlockPos(key.x()*16,-40,key.z()*16),amount);}
|
||||
private static ServerPlayer host(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static boolean dataOffer(String phase){return ClaimDuelClient.data!=null&&ClaimDuelClient.data.has("offer")&&ClaimDuelClient.data.getAsJsonObject("offer").get("phase").getAsString().equals(phase);}
|
||||
private static void rightClick(SanctuaryAtlasScreen map,int x,int z){map.mouseClicked(new MouseButtonEvent(map.width/2.+(x-map.centerX())*map.zoom(),(map.contentTop+map.contentBottom)/2.+(z-map.centerZ())*map.zoom(),new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false);}
|
||||
private static void bottom(Screen s){widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).forEach(w->w.setScrollAmount(10000));}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean yes,String why){if(!yes)throw new AssertionError(why);}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class ClaimDemeure277ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;var onboarding=new AtomicBoolean(true);var neighbour=new java.util.concurrent.atomic.AtomicReference<net.minecraft.server.level.ServerPlayer>();
|
||||
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);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ClaimDemeure277 "+UUID.randomUUID());s.setSeed("277");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);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(20);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase actual Atlas before accessing properties");ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);
|
||||
checkCache();
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.isComplete()&&map.status().equals("ready"),400);
|
||||
c.runOnClient(m->{require(ProgressionClient.progress.atlas(),"Property management requires the purchased Atlas");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.eligible()&&d.count()==0&&d.cost()==0,"Hello World admits free chunk selection");});
|
||||
c.waitFor(m->residency(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{require(residency(m.gui.screen()).reason().equals("habitation_required")&&!button(m.gui.screen(),"sanctuary.claims.claim_at_cost").active,"Empty land cannot be claimed before habitation, even for free");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-habitation-required");
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(152,-40,136),.01);});
|
||||
c.waitTicks(45);c.runOnClient(m->require(!button(m.gui.screen(),"sanctuary.claims.claim_at_cost").active,"A passing trace never enables the claim"));
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(152,-40,136),.5);});
|
||||
c.waitFor(m->residency(m.gui.screen())!=null&&residency(m.gui.screen()).reason().equals("habitation_ready")&&button(m.gui.screen(),"sanctuary.claims.claim_at_cost").active,200);
|
||||
world.getServer().runOnServer(server->{var guest=guest(server);neighbour.set(guest);fr.koka.demeure.DemeureService.record(guest,new net.minecraft.core.BlockPos(152,-40,136),.6);});
|
||||
c.waitFor(m->residency(m.gui.screen())!=null&&residency(m.gui.screen()).reason().equals("habitation_priority"),200);
|
||||
c.runOnClient(m->{require(residency(m.gui.screen()).name().equals("Nora")&&!button(m.gui.screen(),"sanctuary.claims.claim_at_cost").active,"A later stronger inhabitant is named and prevents the first resident from claiming");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-stronger-neighbour");
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(152,-40,136),.2);});
|
||||
c.waitFor(m->residency(m.gui.screen())!=null&&residency(m.gui.screen()).reason().equals("habitation_ready")&&button(m.gui.screen(),"sanctuary.claims.claim_at_cost").active,200);
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-priority-regained");
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-map-free");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.claim_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).count()==1,200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.get(9,8)!=null&&map.claimOverlay.remaining(map.claimOverlay.get(9,8))==0,"New ownership appears immediately without reopening the map");});
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),8));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(5);c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).sapphires()==8,200);
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-map-sapphires");
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int border=0xFF000000|map.claimOverlay.get(9,8).claim().color();require(pixel(map,144,132)==border&&pixel(map,152,136)==0,"Unprotected claim is visible as an outline with a clear interior");require(button(map,"sanctuary.claims.protect_at_cost").getMessage().getString().contains("8"),"Context action displays the new price");});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.protect_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).message().equals("protected"),200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.remaining(map.claimOverlay.get(9,8))>ChunkClaims.PROTECTION_MILLIS-10_000,"Protection updates the currently open map immediately");require(widgets(map).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->key(w).equals("sanctuary.claims.protected")).allMatch(w->w.getMessage().getString().contains("three")||w.getMessage().getString().contains("trois")),"Purchase confirmation uses the new duration");});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.level().getChunk(10,8);var store=ChunkPropertyService.store(server);fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(160,-40,128),1);require(ChunkPropertyService.act(p,"claim",new ChunkClaims.Key(10,8),store.revision()).equals("claimed"),"Create an adjacent unprotected claim while the map stays open");});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).claimOverlay.get(10,8)!=null,200);c.waitTicks(3);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int fill=0x40000000|map.claimOverlay.get(9,8).claim().color();int border=0xFF000000|map.claimOverlay.get(9,8).claim().color();require(pixel(map,159,132)==fill&&pixel(map,160,132)==0&&pixel(map,175,132)==border,"Live neighbour arrival removes both shared contours across protection states");});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),8));var store=ChunkPropertyService.store(server);require(ChunkPropertyService.act(p,"protect",new ChunkClaims.Key(10,8),store.revision()).equals("protected"),"Protect the second chunk for eight sapphires");});
|
||||
c.waitFor(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();return map.claimOverlay.remaining(map.claimOverlay.get(10,8))>0;},200);c.waitTicks(3);
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();var guest=neighbour.get();guest.level().getChunk(12,8);fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(192,-40,128),.5);fr.koka.demeure.DemeureService.record(guest,new net.minecraft.core.BlockPos(192,-40,128),.6);var store=ChunkPropertyService.store(server);require(ChunkPropertyService.act(guest,"claim",new ChunkClaims.Key(12,8),store.revision()).equals("claimed"),"Later stronger inhabitant really claims an unclaimed spot");require(store.get(new ChunkClaims.Key(9,8)).owner().equals(p.getUUID()),"Increased neighbour influence never transfers the existing purchased claim");fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(136,-40,152),.5);fr.koka.demeure.DemeureService.record(guest,new net.minecraft.core.BlockPos(136,-40,152),.6);});
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var identities=ProgressionService.session(server).inhabitants;
|
||||
UUID offline=UUID.randomUUID();int color=identities.available().getFirst();
|
||||
try{require(identities.create(offline,"Camille","",color),"Offline owner identity created");identities.arrive(offline);var store=ChunkPropertyService.store(server);
|
||||
fixture(store,p.getUUID(),11,8,false);fixture(store,p.getUUID(),11,9,false);fixture(store,offline,9,9,true);fixture(store,offline,10,9,true);
|
||||
}catch(Exception e){throw new RuntimeException(e);}
|
||||
});
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(11,8)!=null,400);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();var own=map.claimOverlay.get(9,8);var other=map.claimOverlay.get(10,9);
|
||||
require(own!=null&&own.claim().color()!=other.claim().color(),"Two owners have their distinct persistent colors before any context click");
|
||||
require(map.claimOverlay.remaining(own)>0&&map.claimOverlay.remaining(map.claimOverlay.get(11,8))==0,"Same owner has both protected and unprotected chunks");
|
||||
require(other.claim().name().equals("Camille")&&map.claimOverlay.remaining(other)>0,"Offline owner name, color and active protection arrive with map tiles");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-local-two-owners");
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();require(ChunkPropertyService.sapphires(p)==0,"Both protections debit exactly eight sapphires");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);c.waitTicks(30);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",152,-40,160)));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(10,8)!=null,400);
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int own=0x40000000|map.claimOverlay.get(9,8).claim().color();int other=0x40000000|map.claimOverlay.get(10,9).claim().color();
|
||||
int ownBorder=0xFF000000|map.claimOverlay.get(9,8).claim().color(),otherBorder=0xFF000000|map.claimOverlay.get(10,9).claim().color();
|
||||
require(pixel(map,144,128)==ownBorder&&pixel(map,159,132)==own&&pixel(map,160,132)==own,"Protected neighbours retain one outer contour and continuous fill");
|
||||
require(pixel(map,144,144)==otherBorder&&pixel(map,159,148)==other&&pixel(map,160,148)==other,"Offline owner's adjacent chunks connect without an internal contour");
|
||||
require(pixel(map,183,136)==0&&pixel(map,191,136)==ownBorder&&pixel(map,176,132)==0,"Unprotected claims show their outer contour without a fill or shared-edge stroke");
|
||||
require(pixel(map,168,143)==ownBorder&&pixel(map,168,144)==otherBorder,"Different owners retain their distinct boundaries");
|
||||
});
|
||||
c.takeScreenshot("claims277-colors-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();var guest=neighbour.get();map.acceptResidency(new ChunkPropertyPayloads.Residency(1000,1000,"habitation_priority","Wrong tile",0xFF0000));require(map.propertyView()==null,"A residency reply for another tile cannot open a panel");});
|
||||
c.runOnClient(m->rightClick(m.gui.screen()));
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.x()==9&&d.z()==8,"Ordinary Atlas right-click selects actual chunk without a mode toggle");widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible).forEach(b->require(b.getX()>=0&&b.getRight()<=m.gui.screen().width&&b.getBottom()<=m.gui.screen().height,"Visible controls fit"));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-map-protected-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));require(m.gui.screen() instanceof SanctuaryAtlasScreen&&view(m.gui.screen())==null,"Escape closes the context panel and keeps the map");});
|
||||
c.runOnClient(m->rightClickAt(m.gui.screen(),136,152));
|
||||
c.waitFor(m->residency(m.gui.screen())!=null&&residency(m.gui.screen()).reason().equals("habitation_priority"),200);
|
||||
c.runOnClient(m->{require(residency(m.gui.screen()).name().equals("Nora")&&!button(m.gui.screen(),"sanctuary.claims.claim_at_cost").active,"Priority details and disabled action render in each locale and size");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-priority-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0)));
|
||||
}
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_visible").onPress(ENTER);require(!ChunkPropertyClient.propertiesVisible(),"Properties can be hidden without changing server protection");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{require(view(m.gui.screen()).x()==9,"Right-click remains available when claims are hidden");m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-hidden-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->require(!ChunkPropertyClient.propertiesVisible(),"Hidden state is shared with Pause and retained when the map closes"));
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-hidden-pause");
|
||||
c.runOnClient(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(11,8)!=null,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_hidden").onPress(ENTER);require(ChunkPropertyClient.propertiesVisible(),"Properties can be shown again with their cached current state");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-restored-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();require(map.claimOverlay.get(10,9).claim().name().equals("Camille"),"Pause preview receives offline ownership with its terrain tiles");var pixels=((net.minecraft.client.renderer.texture.DynamicTexture)field(map,"texture")).getPixels();int ox=(int)field(map,"originX"),oz=(int)field(map,"originZ");int border=0xFF000000|map.claimOverlay.get(11,8).claim().color();require(pixels.getPixel(191-ox,136-oz)==border,"Pause uses the same pixel contour for an unprotected claim");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims277-pause-preview");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private static net.minecraft.server.level.ServerPlayer guest(net.minecraft.server.MinecraftServer server){
|
||||
var profile=new com.mojang.authlib.GameProfile(UUID.randomUUID(),"Nora");
|
||||
var player=new net.minecraft.server.level.ServerPlayer(server,server.overworld(),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);
|
||||
player.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(server,connection,player,net.minecraft.server.network.CommonListenerCookie.createInitial(profile,false));
|
||||
player.setGameMode(net.minecraft.world.level.GameType.SURVIVAL);player.setPos(192,-40,128);
|
||||
var identities=ProgressionService.session(server).inhabitants;
|
||||
try{require(identities.create(player.getUUID(),"Nora","",identities.available().getFirst()),"Create neighbour identity");identities.arrive(player.getUUID());}catch(Exception e){throw new RuntimeException(e);}
|
||||
player.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(player,ProgressRecord.INITIAL);player.setExperienceLevels(20);
|
||||
require(ProgressionService.purchase(player,"atlas",ProgressEffects.get(player).revision()),"Neighbour has a purchased Atlas");return player;
|
||||
}
|
||||
private static ChunkPropertyPayloads.Residency residency(Screen s){return s instanceof SanctuaryAtlasScreen map?(ChunkPropertyPayloads.Residency)field(map,"propertyResidency"):null;}
|
||||
private static void fixture(ChunkClaims store,UUID owner,int x,int z,boolean protect)throws Exception{
|
||||
var key=new ChunkClaims.Key(x,z);var tx=store.prepare(owner,"claim",key,store.revision());require(tx!=null,"Fixture chunk is free");store.recover(owner,tx.receipt().toString());
|
||||
if(protect){tx=store.prepare(owner,"protect",key,store.revision());store.recover(owner,tx.receipt().toString());}
|
||||
}
|
||||
private static void checkCache(){
|
||||
var time=new java.util.concurrent.atomic.AtomicLong();var cache=new ClaimMapOverlay(time::get);UUID id=UUID.randomUUID();var key=new ChunkClaims.Key(-1,2);
|
||||
var paint=new ChunkPropertyPayloads.Paint(-1,2,id,"Test",0x123456,1000);cache.begin(7);cache.observe(-1,2);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of(paint)));
|
||||
require(cache.get(-1,2).claim().color()==0x123456&&cache.remaining(cache.get(-1,2))==1000,"Exact RGB is retained");
|
||||
time.set(1_001_000_000L);require(cache.remaining(cache.get(-1,2))==0&&cache.get(-1,2)!=null,"Expiration changes style while keeping ownership");require(cache.ink(-1,2).pixel(0,8)==0xFF123456&&cache.ink(-1,2).pixel(8,8)==0&&cache.expiredTiles().contains(key)&&cache.expiredTiles().isEmpty(),"Expiry removes the fill, retains the outline and invalidates once");
|
||||
cache.begin(8);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of()));require(cache.get(-1,2)!=null,"Stale viewport cannot erase current claims");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(key),List.of(paint)));require(cache.remaining(cache.get(-1,2))==1000,"Live purchase update survives a pending viewport refresh");
|
||||
var hidden=new ChunkClaims.Key(30,30);cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(hidden),List.of(new ChunkPropertyPayloads.Paint(30,30,id,"Hidden",0xFFFFFF,1000))));require(cache.get(30,30)==null,"Live updates cannot reveal an unobserved chunk");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(8,List.of(key),List.of()));require(cache.get(-1,2)==null,"An empty current tile removes stale paint");cache.clear();
|
||||
var a=new ChunkClaims.Key(0,0);var east=new ChunkClaims.Key(1,0);var south=new ChunkClaims.Key(0,1);var diagonal=new ChunkClaims.Key(1,1);
|
||||
cache.begin(9);for(var k:List.of(a,east,south,diagonal))cache.observe(k.x(),k.z());
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(9,List.of(a,east,south),List.of(new ChunkPropertyPayloads.Paint(0,0,id,"Test",0x123456,0),new ChunkPropertyPayloads.Paint(1,0,id,"Test",0x123456,1000),new ChunkPropertyPayloads.Paint(0,1,id,"Test",0x123456,0))));
|
||||
require(cache.ink(0,0).pixel(15,8)==0&&cache.ink(0,0).pixel(8,15)==0&&cache.ink(0,0).pixel(15,15)==0xFF123456,"L-shaped property removes internal edges and closes its concave corner");
|
||||
require(cache.ink(1,0).pixel(0,8)==0x40123456,"Protected fill joins unprotected ownership without a border");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(diagonal),List.of(new ChunkPropertyPayloads.Paint(1,1,id,"Test",0x123456,0))));
|
||||
require(cache.ink(0,0).pixel(15,15)==0,"Completing a square removes the former concave corner");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(diagonal),List.of()));
|
||||
require(cache.ink(0,0).pixel(15,15)==0xFF123456,"Removal restores the adjoining corner");
|
||||
require(ClaimMapOverlay.affected(List.of(diagonal)).contains(a),"Diagonal invalidation includes the affected corner tile");
|
||||
require(cache.ink(1_875_000,1_875_000).pixel(0,0)==0&&cache.get(1_875_001,0)==null,"World-edge adjacency is bounded");
|
||||
}
|
||||
private static int pixel(SanctuaryAtlasScreen map,int x,int z){return ((net.minecraft.client.renderer.texture.DynamicTexture)field(map,"footprintTexture")).getPixels().getPixel(x-(int)field(map,"originX"),z-(int)field(map,"originZ"));}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static ChunkPropertyPayloads.View view(Screen screen){return screen instanceof SanctuaryAtlasScreen map?map.propertyView():null;}
|
||||
private static void rightClick(Screen screen){rightClickAt(screen,152,136);}
|
||||
private static void rightClickAt(Screen screen,int x,int z){var atlas=(SanctuaryAtlasScreen)screen;require(atlas.mouseClicked(new MouseButtonEvent(atlas.width/2.+(x-atlas.centerX())*atlas.zoom(),(atlas.contentTop+atlas.contentBottom)/2.+(z-atlas.centerZ())*atlas.zoom(),new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false),"Right click is handled on the terrain");}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class ClaimMap273ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;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);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ClaimMap273 "+UUID.randomUUID());s.setSeed("273");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);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);});
|
||||
checkCache();
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.isComplete()&&map.status().equals("ready"),400);
|
||||
c.runOnClient(m->{require(!ProgressionClient.progress.atlas(),"Property map does not grant the Atlas aptitude");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.eligible()&&d.count()==0&&d.cost()==0,"Hello World admits free chunk selection");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-map-free");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.claim_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).count()==1,200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.get(9,8)!=null&&map.claimOverlay.remaining(map.claimOverlay.get(9,8))==0,"New ownership appears immediately without reopening the map");});
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),64));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(5);c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).sapphires()==64,200);
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-map-sapphires");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.protect_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).message().equals("protected"),200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.remaining(map.claimOverlay.get(9,8))>ChunkClaims.PROTECTION_MILLIS-10_000,"Protection updates the currently open map immediately");});
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var identities=ProgressionService.session(server).inhabitants;
|
||||
UUID offline=UUID.randomUUID();int color=identities.available().getFirst();
|
||||
try{require(identities.create(offline,"Camille","",color),"Offline owner identity created");identities.arrive(offline);var store=ChunkPropertyService.store(server);
|
||||
fixture(store,p.getUUID(),10,8,false);fixture(store,offline,9,9,false);fixture(store,offline,10,9,true);
|
||||
}catch(Exception e){throw new RuntimeException(e);}
|
||||
});
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();var own=map.claimOverlay.get(9,8);var other=map.claimOverlay.get(10,9);
|
||||
require(own!=null&&own.claim().color()!=other.claim().color(),"Two owners have their distinct persistent colors before any context click");
|
||||
require(map.claimOverlay.remaining(own)>0&&map.claimOverlay.remaining(map.claimOverlay.get(10,8))==0,"Same owner has both protected and unprotected chunks");
|
||||
require(other.claim().name().equals("Camille")&&map.claimOverlay.remaining(other)>0,"Offline owner name, color and active protection arrive with map tiles");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-local-two-owners");
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(ChunkPropertyService.sapphires(p)==0,"Map action debits full stack");p.setExperienceLevels(100);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase Atlas normally");ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);c.waitTicks(30);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",152,-40,160)));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(10,8)!=null,400);
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-colors-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->rightClick(m.gui.screen()));
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.x()==9&&d.z()==8,"Ordinary Atlas right-click selects actual chunk without a mode toggle");widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible).forEach(b->require(b.getX()>=0&&b.getRight()<=m.gui.screen().width&&b.getBottom()<=m.gui.screen().height,"Visible controls fit"));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-map-protected-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));require(m.gui.screen() instanceof SanctuaryAtlasScreen&&view(m.gui.screen())==null,"Escape closes the context panel and keeps the map");});
|
||||
}
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_visible").onPress(ENTER);require(!ChunkPropertyClient.propertiesVisible(),"Properties can be hidden without changing server protection");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{require(view(m.gui.screen()).x()==9,"Right-click remains available when claims are hidden");m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-hidden-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->require(!ChunkPropertyClient.propertiesVisible(),"Hidden state is shared with Pause and retained when the map closes"));
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-hidden-pause");
|
||||
c.runOnClient(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_hidden").onPress(ENTER);require(ChunkPropertyClient.propertiesVisible(),"Properties can be shown again with their cached current state");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-restored-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();require(map.claimOverlay.get(10,9).claim().name().equals("Camille"),"Pause preview receives offline ownership with its terrain tiles");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims273-pause-preview");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private static void fixture(ChunkClaims store,UUID owner,int x,int z,boolean protect)throws Exception{
|
||||
var key=new ChunkClaims.Key(x,z);var tx=store.prepare(owner,"claim",key,store.revision());require(tx!=null,"Fixture chunk is free");store.recover(owner,tx.receipt().toString());
|
||||
if(protect){tx=store.prepare(owner,"protect",key,store.revision());store.recover(owner,tx.receipt().toString());}
|
||||
}
|
||||
private static void checkCache(){
|
||||
var time=new java.util.concurrent.atomic.AtomicLong();var cache=new ClaimMapOverlay(time::get);UUID id=UUID.randomUUID();var key=new ChunkClaims.Key(-1,2);
|
||||
var paint=new ChunkPropertyPayloads.Paint(-1,2,id,"Test",0x123456,1000);cache.begin(7);cache.observe(-1,2);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of(paint)));
|
||||
require(cache.get(-1,2).claim().color()==0x123456&&cache.remaining(cache.get(-1,2))==1000,"Exact RGB is retained");
|
||||
time.set(1_001_000_000L);require(cache.remaining(cache.get(-1,2))==0&&cache.get(-1,2)!=null,"Expiration changes style while keeping ownership");
|
||||
cache.begin(8);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of()));require(cache.get(-1,2)!=null,"Stale viewport cannot erase current claims");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(key),List.of(paint)));require(cache.remaining(cache.get(-1,2))==1000,"Live purchase update survives a pending viewport refresh");
|
||||
var hidden=new ChunkClaims.Key(30,30);cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(hidden),List.of(new ChunkPropertyPayloads.Paint(30,30,id,"Hidden",0xFFFFFF,1000))));require(cache.get(30,30)==null,"Live updates cannot reveal an unobserved chunk");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(8,List.of(key),List.of()));require(cache.get(-1,2)==null,"An empty current tile removes stale paint");cache.clear();
|
||||
}
|
||||
private static ChunkPropertyPayloads.View view(Screen screen){return screen instanceof SanctuaryAtlasScreen map?map.propertyView():null;}
|
||||
private static void rightClick(Screen screen){var atlas=(SanctuaryAtlasScreen)screen;require(atlas.mouseClicked(new MouseButtonEvent(atlas.width/2.+(152-atlas.centerX())*atlas.zoom(),(atlas.contentTop+atlas.contentBottom)/2.+(136-atlas.centerZ())*atlas.zoom(),new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false),"Right click is handled on the terrain");}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class ClaimMap276ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;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);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ClaimMap276 "+UUID.randomUUID());s.setSeed("276");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);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(20);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase actual Atlas before accessing properties");ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);
|
||||
checkCache();
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.isComplete()&&map.status().equals("ready"),400);
|
||||
c.runOnClient(m->{require(ProgressionClient.progress.atlas(),"Property management requires the purchased Atlas");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.eligible()&&d.count()==0&&d.cost()==0,"Hello World admits free chunk selection");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-map-free");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.claim_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).count()==1,200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.get(9,8)!=null&&map.claimOverlay.remaining(map.claimOverlay.get(9,8))==0,"New ownership appears immediately without reopening the map");});
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),8));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(5);c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).sapphires()==8,200);
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-map-sapphires");
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int border=0xFF000000|map.claimOverlay.get(9,8).claim().color();require(pixel(map,144,132)==border&&pixel(map,152,136)==0,"Unprotected claim is visible as an outline with a clear interior");require(button(map,"sanctuary.claims.protect_at_cost").getMessage().getString().contains("8"),"Context action displays the new price");});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.protect_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).message().equals("protected"),200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.remaining(map.claimOverlay.get(9,8))>ChunkClaims.PROTECTION_MILLIS-10_000,"Protection updates the currently open map immediately");require(widgets(map).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->key(w).equals("sanctuary.claims.protected")).allMatch(w->w.getMessage().getString().contains("three")||w.getMessage().getString().contains("trois")),"Purchase confirmation uses the new duration");});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.level().getChunk(10,8);var store=ChunkPropertyService.store(server);require(ChunkPropertyService.act(p,"claim",new ChunkClaims.Key(10,8),store.revision()).equals("claimed"),"Create an adjacent unprotected claim while the map stays open");});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).claimOverlay.get(10,8)!=null,200);c.waitTicks(3);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int fill=0x40000000|map.claimOverlay.get(9,8).claim().color();int border=0xFF000000|map.claimOverlay.get(9,8).claim().color();require(pixel(map,159,132)==fill&&pixel(map,160,132)==0&&pixel(map,175,132)==border,"Live neighbour arrival removes both shared contours across protection states");});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),8));var store=ChunkPropertyService.store(server);require(ChunkPropertyService.act(p,"protect",new ChunkClaims.Key(10,8),store.revision()).equals("protected"),"Protect the second chunk for eight sapphires");});
|
||||
c.waitFor(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();return map.claimOverlay.remaining(map.claimOverlay.get(10,8))>0;},200);c.waitTicks(3);
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var identities=ProgressionService.session(server).inhabitants;
|
||||
UUID offline=UUID.randomUUID();int color=identities.available().getFirst();
|
||||
try{require(identities.create(offline,"Camille","",color),"Offline owner identity created");identities.arrive(offline);var store=ChunkPropertyService.store(server);
|
||||
fixture(store,p.getUUID(),11,8,false);fixture(store,p.getUUID(),11,9,false);fixture(store,offline,9,9,true);fixture(store,offline,10,9,true);
|
||||
}catch(Exception e){throw new RuntimeException(e);}
|
||||
});
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(11,8)!=null,400);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();var own=map.claimOverlay.get(9,8);var other=map.claimOverlay.get(10,9);
|
||||
require(own!=null&&own.claim().color()!=other.claim().color(),"Two owners have their distinct persistent colors before any context click");
|
||||
require(map.claimOverlay.remaining(own)>0&&map.claimOverlay.remaining(map.claimOverlay.get(11,8))==0,"Same owner has both protected and unprotected chunks");
|
||||
require(other.claim().name().equals("Camille")&&map.claimOverlay.remaining(other)>0,"Offline owner name, color and active protection arrive with map tiles");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-local-two-owners");
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();require(ChunkPropertyService.sapphires(p)==0,"Both protections debit exactly eight sapphires");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);c.waitTicks(30);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",152,-40,160)));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(10,8)!=null,400);
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int own=0x40000000|map.claimOverlay.get(9,8).claim().color();int other=0x40000000|map.claimOverlay.get(10,9).claim().color();
|
||||
int ownBorder=0xFF000000|map.claimOverlay.get(9,8).claim().color(),otherBorder=0xFF000000|map.claimOverlay.get(10,9).claim().color();
|
||||
require(pixel(map,144,128)==ownBorder&&pixel(map,159,132)==own&&pixel(map,160,132)==own,"Protected neighbours retain one outer contour and continuous fill");
|
||||
require(pixel(map,144,144)==otherBorder&&pixel(map,159,148)==other&&pixel(map,160,148)==other,"Offline owner's adjacent chunks connect without an internal contour");
|
||||
require(pixel(map,183,136)==0&&pixel(map,191,136)==ownBorder&&pixel(map,176,132)==0,"Unprotected claims show their outer contour without a fill or shared-edge stroke");
|
||||
require(pixel(map,168,143)==ownBorder&&pixel(map,168,144)==otherBorder,"Different owners retain their distinct boundaries");
|
||||
});
|
||||
c.takeScreenshot("claims276-colors-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->rightClick(m.gui.screen()));
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.x()==9&&d.z()==8,"Ordinary Atlas right-click selects actual chunk without a mode toggle");widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible).forEach(b->require(b.getX()>=0&&b.getRight()<=m.gui.screen().width&&b.getBottom()<=m.gui.screen().height,"Visible controls fit"));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-map-protected-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));require(m.gui.screen() instanceof SanctuaryAtlasScreen&&view(m.gui.screen())==null,"Escape closes the context panel and keeps the map");});
|
||||
}
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_visible").onPress(ENTER);require(!ChunkPropertyClient.propertiesVisible(),"Properties can be hidden without changing server protection");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{require(view(m.gui.screen()).x()==9,"Right-click remains available when claims are hidden");m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));});
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-hidden-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->require(!ChunkPropertyClient.propertiesVisible(),"Hidden state is shared with Pause and retained when the map closes"));
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-hidden-pause");
|
||||
c.runOnClient(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(11,8)!=null,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_hidden").onPress(ENTER);require(ChunkPropertyClient.propertiesVisible(),"Properties can be shown again with their cached current state");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-restored-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();require(map.claimOverlay.get(10,9).claim().name().equals("Camille"),"Pause preview receives offline ownership with its terrain tiles");var pixels=((net.minecraft.client.renderer.texture.DynamicTexture)field(map,"texture")).getPixels();int ox=(int)field(map,"originX"),oz=(int)field(map,"originZ");int border=0xFF000000|map.claimOverlay.get(11,8).claim().color();require(pixels.getPixel(191-ox,136-oz)==border,"Pause uses the same pixel contour for an unprotected claim");});
|
||||
c.waitTicks(3);c.takeScreenshot("claims276-pause-preview");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private static void fixture(ChunkClaims store,UUID owner,int x,int z,boolean protect)throws Exception{
|
||||
var key=new ChunkClaims.Key(x,z);var tx=store.prepare(owner,"claim",key,store.revision());require(tx!=null,"Fixture chunk is free");store.recover(owner,tx.receipt().toString());
|
||||
if(protect){tx=store.prepare(owner,"protect",key,store.revision());store.recover(owner,tx.receipt().toString());}
|
||||
}
|
||||
private static void checkCache(){
|
||||
var time=new java.util.concurrent.atomic.AtomicLong();var cache=new ClaimMapOverlay(time::get);UUID id=UUID.randomUUID();var key=new ChunkClaims.Key(-1,2);
|
||||
var paint=new ChunkPropertyPayloads.Paint(-1,2,id,"Test",0x123456,1000);cache.begin(7);cache.observe(-1,2);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of(paint)));
|
||||
require(cache.get(-1,2).claim().color()==0x123456&&cache.remaining(cache.get(-1,2))==1000,"Exact RGB is retained");
|
||||
time.set(1_001_000_000L);require(cache.remaining(cache.get(-1,2))==0&&cache.get(-1,2)!=null,"Expiration changes style while keeping ownership");require(cache.ink(-1,2).pixel(0,8)==0xFF123456&&cache.ink(-1,2).pixel(8,8)==0&&cache.expiredTiles().contains(key)&&cache.expiredTiles().isEmpty(),"Expiry removes the fill, retains the outline and invalidates once");
|
||||
cache.begin(8);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of()));require(cache.get(-1,2)!=null,"Stale viewport cannot erase current claims");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(key),List.of(paint)));require(cache.remaining(cache.get(-1,2))==1000,"Live purchase update survives a pending viewport refresh");
|
||||
var hidden=new ChunkClaims.Key(30,30);cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(hidden),List.of(new ChunkPropertyPayloads.Paint(30,30,id,"Hidden",0xFFFFFF,1000))));require(cache.get(30,30)==null,"Live updates cannot reveal an unobserved chunk");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(8,List.of(key),List.of()));require(cache.get(-1,2)==null,"An empty current tile removes stale paint");cache.clear();
|
||||
var a=new ChunkClaims.Key(0,0);var east=new ChunkClaims.Key(1,0);var south=new ChunkClaims.Key(0,1);var diagonal=new ChunkClaims.Key(1,1);
|
||||
cache.begin(9);for(var k:List.of(a,east,south,diagonal))cache.observe(k.x(),k.z());
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(9,List.of(a,east,south),List.of(new ChunkPropertyPayloads.Paint(0,0,id,"Test",0x123456,0),new ChunkPropertyPayloads.Paint(1,0,id,"Test",0x123456,1000),new ChunkPropertyPayloads.Paint(0,1,id,"Test",0x123456,0))));
|
||||
require(cache.ink(0,0).pixel(15,8)==0&&cache.ink(0,0).pixel(8,15)==0&&cache.ink(0,0).pixel(15,15)==0xFF123456,"L-shaped property removes internal edges and closes its concave corner");
|
||||
require(cache.ink(1,0).pixel(0,8)==0x40123456,"Protected fill joins unprotected ownership without a border");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(diagonal),List.of(new ChunkPropertyPayloads.Paint(1,1,id,"Test",0x123456,0))));
|
||||
require(cache.ink(0,0).pixel(15,15)==0,"Completing a square removes the former concave corner");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(diagonal),List.of()));
|
||||
require(cache.ink(0,0).pixel(15,15)==0xFF123456,"Removal restores the adjoining corner");
|
||||
require(ClaimMapOverlay.affected(List.of(diagonal)).contains(a),"Diagonal invalidation includes the affected corner tile");
|
||||
require(cache.ink(1_875_000,1_875_000).pixel(0,0)==0&&cache.get(1_875_001,0)==null,"World-edge adjacency is bounded");
|
||||
}
|
||||
private static int pixel(SanctuaryAtlasScreen map,int x,int z){return ((net.minecraft.client.renderer.texture.DynamicTexture)field(map,"footprintTexture")).getPixels().getPixel(x-(int)field(map,"originX"),z-(int)field(map,"originZ"));}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static ChunkPropertyPayloads.View view(Screen screen){return screen instanceof SanctuaryAtlasScreen map?map.propertyView():null;}
|
||||
private static void rightClick(Screen screen){var atlas=(SanctuaryAtlasScreen)screen;require(atlas.mouseClicked(new MouseButtonEvent(atlas.width/2.+(152-atlas.centerX())*atlas.zoom(),(atlas.contentTop+atlas.contentBottom)/2.+(136-atlas.centerZ())*atlas.zoom(),new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false),"Right click is handled on the terrain");}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
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.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class Demeure274ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
Starter039TestSupport.selectFirst(s);widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(ENTER);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);armed.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Demeure274 "+UUID.randomUUID());s.setSeed("274");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);
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);server.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);p.setExperienceLevels(20);
|
||||
require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Real Atlas purchase");
|
||||
for(int z=8;z<=9;z++)for(int x=9;x<=10;x++){
|
||||
byte[] colors=new byte[256];Arrays.fill(colors,net.minecraft.world.level.material.MapColor.GOLD.getPackedId(net.minecraft.world.level.material.MapColor.Brightness.NORMAL));colors[17]=0;colors[18]=1;
|
||||
AtlasService.observations(p).observe(new AtlasStore.Tile(x,z,colors));fr.koka.demeure.DemeureService.record(p,new net.minecraft.core.BlockPos(x*16,-40,z*16),8);
|
||||
}ProgressionService.send(p,"");
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",152,-40,160)));
|
||||
if(ChunkPropertyClient.propertiesVisible())button(m.gui.screen(),"sanctuary.claims.map_visible").onPress(ENTER);
|
||||
button(m.gui.screen(),"sanctuary.atlas.demeure_off").onPress(ENTER);
|
||||
});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.footprintCount()>=4&&pixel(map,144,128)!=0,400);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int tint=pixel(map,144,128);
|
||||
require(tint==pixel(map,159,132)&&tint==pixel(map,160,132),"Adjacent same-influence chunks have no internal border");
|
||||
require(tint==pixel(map,159,143)&&tint==pixel(map,160,144),"Corners connect with the same tint");
|
||||
require(tint==pixel(map,147,131),"Edges, corners and body share one intensity");
|
||||
String hint=net.minecraft.network.chat.Component.translatable("sanctuary.atlas.demeure_hint").getString().toLowerCase(Locale.ROOT);
|
||||
require(!hint.contains("claim")&&!hint.contains("protection"),"Demeure help describes habitation");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("demeure274-connected-"+locale+"-gui"+scale);
|
||||
}
|
||||
var fixture=c.computeOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();var key=new AtlasStore.Key(10,9);
|
||||
var homes=(Map<AtlasStore.Key,AtlasDemeurePayload.Footprint>)field(map,"homes");var surveys=(Map<AtlasStore.Key,byte[]>)field(map,"surveyed");
|
||||
return new Fixture((int)field(map,"token"),key,surveys.get(key).clone(),homes.get(key));
|
||||
});
|
||||
byte[] maskedColors=fixture.colors.clone();maskedColors[17]=0;maskedColors[18]=1;
|
||||
var masked=new Fixture(fixture.token,fixture.key,maskedColors,fixture.home);send(world.getServer(),masked,List.of(fixture.home));
|
||||
c.waitFor(m->pixel((SanctuaryAtlasScreen)m.gui.screen(),161,145)==0&&pixel((SanctuaryAtlasScreen)m.gui.screen(),162,145)==0,200);
|
||||
var weaker=new AtlasDemeurePayload.Footprint(10,9,fixture.home.state(),fixture.home.kind(),fixture.home.label(),25,fixture.home.color());
|
||||
send(world.getServer(),masked,List.of(weaker));
|
||||
c.waitFor(m->pixel((SanctuaryAtlasScreen)m.gui.screen(),160,144)==(0x28000000|weaker.color()),200);
|
||||
c.runOnClient(m->require(pixel((SanctuaryAtlasScreen)m.gui.screen(),160,144)!=pixel((SanctuaryAtlasScreen)m.gui.screen(),144,128),"Percentage change alone repaints intensity"));
|
||||
c.waitTicks(3);c.takeScreenshot("demeure274-influence-strength");
|
||||
send(world.getServer(),masked,List.of());c.waitFor(m->pixel((SanctuaryAtlasScreen)m.gui.screen(),160,144)==0,200);
|
||||
c.runOnClient(m->{ProgressionClient.open(m.gui.screen(),"identity");require(button(m.gui.screen(),"sanctuary.property.title")!=null,"Habitant property entry remains in place");});
|
||||
c.waitTicks(3);c.takeScreenshot("demeure274-habitant-preserved");c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private record Fixture(int token,AtlasStore.Key key,byte[] colors,AtlasDemeurePayload.Footprint home){}
|
||||
private static void send(net.fabricmc.fabric.api.client.gametest.v1.context.TestServerContext server,Fixture f,List<AtlasDemeurePayload.Footprint> homes){server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(p,new AtlasPayloads.Response(f.token,false,true,false,false,"ready",List.of(new AtlasStore.Tile(f.key.x(),f.key.z(),f.colors))));net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.send(p,new AtlasDemeurePayload(f.token,homes));});}
|
||||
private static int pixel(SanctuaryAtlasScreen map,int x,int z){return ((net.minecraft.client.renderer.texture.DynamicTexture)field(map,"footprintTexture")).getPixels().getPixel(x-(int)field(map,"originX"),z-(int)field(map,"originZ"));}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.whale.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real pack renderer, animated ghost and native egg under Vulkan. Fresh development world only. */
|
||||
public final class FlyingWhale290ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
|
||||
var whaleId = new AtomicInteger();
|
||||
c.runOnClient(m -> {
|
||||
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
|
||||
check(device.backendName().equalsIgnoreCase("vulkan"), "Whale validation requires Vulkan: " + device.backendName());
|
||||
System.out.println("WHALE290_BACKEND: " + device.backendName() + " / " + device.name());
|
||||
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("FlyingWhale290-" + UUID.randomUUID()); s.setSeed("290"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.getInventory().clearContent(); p.teleportTo(.5, 32, .5); p.setYRot(0); p.setXRot(-8);
|
||||
p.getAbilities().flying = true; p.onUpdateAbilities();
|
||||
p.getInventory().setItem(0, new ItemStack(FlyingWhales.SPAWN_EGG));
|
||||
p.getInventory().setItem(1, new ItemStack(Items.EGG)); p.inventoryMenu.broadcastChanges();
|
||||
p.level().setBlockAndUpdate(new BlockPos(0, 30, 0), Blocks.GLASS.defaultBlockState());
|
||||
for (int i = 0; i < 2; i++) {
|
||||
var whale = FlyingWhales.TYPE.create(p.level(), EntitySpawnReason.COMMAND);
|
||||
whale.setNoAi(true); whale.setPos(i == 0 ? -4 : 30, i == 0 ? 35 : 47, i == 0 ? 43 : 92);
|
||||
whale.setYRot(i == 0 ? 105 : 250); whale.setYHeadRot(whale.getYRot()); whale.setYBodyRot(whale.getYRot());
|
||||
p.level().addFreshEntity(whale); if (i == 0) whaleId.set(whale.getId());
|
||||
}
|
||||
});
|
||||
c.waitFor(m -> m.level.getEntity(whaleId.get()) instanceof FlyingWhaleEntity, 400);
|
||||
c.waitTicks(20);
|
||||
server.runOnServer(s -> check(FlyingWhales.countLoaded(s) == 2, "Two real whales are loaded after the player chunks arrive"));
|
||||
c.runOnClient(m -> {
|
||||
m.player.setYRot(0); m.player.setXRot(-8); m.getWindow().setWindowed(1280, 960);
|
||||
var whale = (FlyingWhaleEntity)m.level.getEntity(whaleId.get());
|
||||
check(m.getEntityRenderDispatcher().getRenderer(whale) instanceof FlyingWhaleRenderer, "Historical renderer is installed");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/flying_whale/flying_whale.png")).isPresent(), "Historical cloud texture is packaged");
|
||||
var renderer = (FlyingWhaleRenderer)m.getEntityRenderDispatcher().getRenderer(whale);
|
||||
var state = renderer.createRenderState(whale, 1);
|
||||
check((renderer.getModelTint(state) >>> 24) < 255, "The whale keeps its translucent ghost tint");
|
||||
var model = new FlyingWhaleModel(m.getEntityModels().bakeLayer(FlyingWhaleModel.LAYER_LOCATION));
|
||||
state.ageInTicks = 0; model.setupAnim(state);
|
||||
float fin = model.root().getChild("whale").getChild("left_fin").zRot;
|
||||
state.ageInTicks = 60; model.setupAnim(state);
|
||||
check(Math.abs(fin - model.root().getChild("whale").getChild("left_fin").zRot) > .01, "Fins animate on the actual model");
|
||||
});
|
||||
c.waitTicks(35); c.takeScreenshot("whale290-native-egg-and-two-whales");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1); c.waitTicks(3);
|
||||
c.takeScreenshot("whale290-ghosts");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
String name = new ItemStack(FlyingWhales.SPAWN_EGG).getHoverName().getString();
|
||||
check(!name.contains("item.sanctuary") && name.contains(locale.equals("fr_fr") ? "baleine" : "Whale"), "Native egg label is translated");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("whale290-egg-" + locale);
|
||||
}
|
||||
var start = new AtomicReference<Vec3>();
|
||||
server.runOnServer(s -> {
|
||||
var whale = (FlyingWhaleEntity)s.overworld().getEntity(whaleId.get());
|
||||
start.set(whale.position()); whale.setNoAi(false);
|
||||
});
|
||||
c.waitTicks(60);
|
||||
server.runOnServer(s -> {
|
||||
var whale = (FlyingWhaleEntity)s.overworld().getEntity(whaleId.get());
|
||||
check(whale.position().distanceToSqr(start.get()) > .01, "Historical AI flies around the real connected player");
|
||||
});
|
||||
c.takeScreenshot("whale290-flight");
|
||||
System.out.println("WHALE290_CLIENT_PASS: Vulkan, two giant translucent animated whales, native egg, FR/EN, actual historical flight");
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.creeper.HappyCreepers;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
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.renderer.entity.CreeperRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real sneak-clicks, synced flower/face, charged combination and real server AI under Vulkan. */
|
||||
public final class HappyCreeperClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
|
||||
c.runOnClient(m -> {
|
||||
m.options.renderDistance().set(5); m.options.simulationDistance().set(5); m.options.pauseOnLostFocus = false;
|
||||
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("voicechat")) {
|
||||
var voice = de.maxhenkel.voicechat.VoicechatClient.CLIENT_CONFIG;
|
||||
voice.muted.set(true); voice.disabled.set(true); voice.onboardingFinished.set(true);
|
||||
voice.macosCheckMicrophonePermission.set(false);
|
||||
}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> { if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("HappyCreeper-" + UUID.randomUUID()); s.setSeed("290"); 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 -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
int id = server.computeOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); p.getAbilities().invulnerable = true;
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL, true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.getInventory().clearContent(); p.teleportTo(.5, 1, .5); p.setYRot(0); p.setXRot(0);
|
||||
fr.koka.sanctuary.inventory.AccessoryAccess.of(p).clearContent(); FamiliarService.tick(p);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.POPPY)); p.inventoryMenu.broadcastChanges();
|
||||
for (int x = -9; x <= 9; x++) for (int z = -3; z <= 12; z++)
|
||||
p.level().setBlockAndUpdate(new BlockPos(x, 0, z), Blocks.STONE.defaultBlockState());
|
||||
var mob = EntityTypes.CREEPER.create(p.level(), EntitySpawnReason.COMMAND); mob.setNoAi(true);
|
||||
mob.setPos(.5, 1, 2.5); mob.setYRot(180); mob.yHeadRot = mob.yBodyRot = 180; p.level().addFreshEntity(mob); return mob.getId();
|
||||
});
|
||||
c.runOnClient(m -> m.getWindow().setWindowed(1280, 960));
|
||||
c.waitFor(m -> m.level.getEntity(id) != null && m.player.getMainHandItem().is(Items.POPPY), 200);
|
||||
c.getInput().holdKey(o -> o.keyShift); c.waitTicks(5); click(c, id); c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> m.level.getEntity(id) instanceof Creeper mob && HappyCreepers.isHappy(mob) && m.player.getMainHandItem().isEmpty(), 200);
|
||||
c.runOnClient(m -> {
|
||||
var mob = (Creeper) m.level.getEntity(id); var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
var state = renderer.createRenderState(mob, 1); var hat = ((MobHeadState.Access) state).sanctuary$mobHat();
|
||||
check(hat.active && !hat.head.block.isEmpty(), "Exact equipped flower reaches the native head layer");
|
||||
check(renderer.getTextureLocation(state).equals(SanctuaryMod.id("textures/entity/creeper/happy.png")), "Synced flower selects the restored smile");
|
||||
check(m.getResourceManager().getResource(renderer.getTextureLocation(state)).isPresent(), "Historical happy texture is packaged");
|
||||
});
|
||||
server.runOnServer(s -> { var mob = (Creeper) s.getPlayerList().getPlayers().getFirst().level().getEntity(id); mob.setNoAi(false); });
|
||||
c.waitTicks(85);
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); var mob = (Creeper) p.level().getEntity(id);
|
||||
check(mob != null && mob.isAlive() && mob.getTarget() == null && !mob.isIgnited(), "Real happy AI survives next to a survival player without attacking");
|
||||
mob.setNoAi(true); mob.teleportTo(.5, 1, 2.5);
|
||||
}); c.waitTicks(5);
|
||||
c.getInput().holdKey(o -> o.keyShift); c.waitTicks(5); click(c, id); c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> m.level.getEntity(id) instanceof Creeper mob && !HappyCreepers.isHappy(mob), 200);
|
||||
c.runOnClient(m -> {
|
||||
var mob = (Creeper) m.level.getEntity(id); var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
check(!((MobHeadState.Access) renderer.createRenderState(mob, 1)).sanctuary$mobHat().active, "Removing the flower clears its render state");
|
||||
check(!renderer.getTextureLocation(renderer.createRenderState(mob, 1)).getNamespace().equals("sanctuary"), "Removal restores the native face");
|
||||
});
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); check(p.getInventory().countItem(Items.POPPY) == 1, "Real empty-hand gesture returns the one flower");
|
||||
p.level().getEntity(id).discard(); p.teleportTo(.5, 1, -2.5);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
var mob = EntityTypes.CREEPER.create(p.level(), EntitySpawnReason.COMMAND); mob.setNoAi(true);
|
||||
mob.setPos((i - 1.5) * 2 + .5, 1, 4.5); mob.setYRot(180); mob.yHeadRot = mob.yBodyRot = 180;
|
||||
if (i == 1 || i == 2) HeadEquipment.set(mob, new ItemStack(i == 1 ? Items.POPPY : Items.PINK_TULIP));
|
||||
if (i >= 2) mob.thunderHit(p.level(), EntityTypes.LIGHTNING_BOLT.create(p.level(), EntitySpawnReason.COMMAND));
|
||||
mob.clearFire(); p.level().addFreshEntity(mob);
|
||||
}
|
||||
}); c.waitTicks(20);
|
||||
c.runOnClient(m -> { m.player.setYRot(0); m.player.setXRot(4);
|
||||
int happy = 0, chargedHappy = 0;
|
||||
for (var mob : m.level.getEntitiesOfClass(Creeper.class, m.player.getBoundingBox().inflate(20))) {
|
||||
var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob); var state = renderer.createRenderState(mob, 1);
|
||||
if (((MobHeadState.Access) state).sanctuary$mobHat().happyCreeper) { happy++; if (state.isPowered) chargedHappy++; }
|
||||
}
|
||||
check(happy == 2 && chargedHappy == 1, "Normal, happy, charged-happy and charged states coexist on the real client");
|
||||
});
|
||||
c.getInput().pressKey(InputConstants.KEY_F1); c.waitTicks(3); c.takeScreenshot("happy-creepers-four-states"); c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
System.out.println("HAPPY_CREEPER_CLIENT_PASS: Vulkan, real sneak-click equipment/removal, exact synced flower, restored face, passive server AI, independent charge");
|
||||
} finally { intro.set(false); c.getInput().releaseKey(o -> o.keyShift); }
|
||||
}
|
||||
|
||||
private static void click(ClientGameTestContext c, int id) {
|
||||
c.runOnClient(m -> {
|
||||
var e = m.level.getEntity(id); var delta = e.getBoundingBox().getCenter().subtract(m.player.getEyePosition());
|
||||
m.player.setYRot((float) Math.toDegrees(Math.atan2(-delta.x, delta.z)));
|
||||
m.player.setXRot((float) -Math.toDegrees(Math.atan2(delta.y, Math.sqrt(delta.x * delta.x + delta.z * delta.z))));
|
||||
}); c.waitTicks(3);
|
||||
c.runOnClient(m -> check(m.hitResult instanceof net.minecraft.world.phys.EntityHitResult hit && hit.getEntity().getId() == id, "Mouse actually aims at the creeper"));
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_RIGHT); c.waitTicks(7);
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.companion.CompanionType;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.gametest.Starter052ClientChecks;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
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.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Cold configuration UI, all palette pages and server refreshes, then a real named admission. */
|
||||
public final class Hello262ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent PRESS=new KeyEvent(InputConstants.KEY_RETURN,0,0);
|
||||
private static final String BIO="Une habitante qui explore les îles.",NAME="Étoile de lune";
|
||||
private static final List<Integer> PALETTE=java.util.stream.IntStream.range(0,20).map(i->InhabitantStore.color(42,i)).boxed().toList();
|
||||
private static final List<StarterCompanions.Choice> CHOICES=List.of("cat","chicken","pig").stream().map(id->new StarterCompanions.Choice(id,CompanionType.Rarity.COMMON)).toList();
|
||||
private static StarterPayloads.Welcome welcome(List<Integer> colors,String error,boolean locked){return new StarterPayloads.Welcome(new ProgressionPayloads.Hello("Poupoutain",colors,error),locked?BIO:"",locked,CHOICES);}
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;
|
||||
try{
|
||||
c.runOnClient(m->{require(m.player==null&&m.level==null,"Welcome renders on a cold client");m.getWindow().setWindowed(1280,960);});
|
||||
c.waitTicks(5);
|
||||
Hello262PreviewChecks.run(c);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new HelloWorldScreen(welcome(PALETTE,"",false),()->{}));
|
||||
var s=(HelloWorldScreen)m.gui.screen();bounds(s);
|
||||
require(!button(s,"sanctuary.progression.enter").active,"An explicit colour and familiar are required");
|
||||
edit(s,"sanctuary.progression.bio").setValue(BIO);edit(s,"sanctuary.starter.name").setValue(NAME);
|
||||
slots(s).get(1).onPress(PRESS);
|
||||
var seen=new ArrayList<Integer>();int count=colors(s).size();
|
||||
require(count==9,"Nine colours per page at every GUI scale");
|
||||
require(!page(s,false).active,"Previous page disabled at start");
|
||||
do{
|
||||
seen.addAll(colors(s).stream().map(b->number(b,"rgb")).toList());
|
||||
bounds(s);
|
||||
if(!page(s,true).active)break;
|
||||
var next=page(s,true);s.setFocused(next);next.onPress(PRESS);
|
||||
require(s.getFocused()==next,"Keyboard focus remains on the palette arrow while paging");
|
||||
}while(true);
|
||||
require(seen.equals(PALETTE),"Every server colour appears once, in order, including the partial final page");
|
||||
colors(s).getLast().onPress(PRESS);int selected=number(s,"color");
|
||||
page(s,false).onPress(PRESS);
|
||||
require(number(s,"color")==selected&&edit(s,"sanctuary.progression.bio").getValue().equals(BIO)&&edit(s,"sanctuary.starter.name").getValue().equals(NAME),"Paging preserves selection and both drafts");
|
||||
require(button(s,"sanctuary.progression.enter").active,"Selection on another palette page remains valid");
|
||||
m.resizeGui();s=(HelloWorldScreen)m.gui.screen();
|
||||
require(number(s,"color")==selected&&edit(s,"sanctuary.starter.name").getValue().equals(NAME),"Resize preserves choices and drafts");
|
||||
var remaining=PALETTE.subList(0,7);
|
||||
s.accept(welcome(remaining,"color_taken",false));
|
||||
require(number(s,"color")==-1&&!button(s,"sanctuary.progression.enter").active,"A colour taken on the server clears only the rejected colour");
|
||||
require(edit(s,"sanctuary.progression.bio").getValue().equals(BIO)&&edit(s,"sanctuary.starter.name").getValue().equals(NAME)&&value(s,"starter").equals("chicken"),"Server rejection keeps biography, familiar and name");
|
||||
colors(s).getFirst().onPress(PRESS);bounds(s);
|
||||
s.accept(welcome(PALETTE,"",false));colors(s).getFirst().onPress(PRESS);
|
||||
edit(s,"sanctuary.progression.bio").setCursorPosition(0);edit(s,"sanctuary.progression.bio").setHighlightPos(0);bounds(s);
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("hello262-"+locale+"-gui"+scale);
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
cursor(c,false);c.waitTicks(3);c.takeScreenshot("hello262-player-cursor-left");
|
||||
var before=c.computeOnClient(m->portraitAngles((HelloWorldScreen)m.gui.screen()));
|
||||
cursor(c,true);c.waitTicks(3);c.takeScreenshot("hello262-player-cursor-right");
|
||||
c.runOnClient(m->{var after=portraitAngles((HelloWorldScreen)m.gui.screen());require(before[0]<0&&after[0]>0&&after[1]<before[1]-10,"Native 3D player faces the cursor on both axes without dragging: "+Arrays.toString(before)+" -> "+Arrays.toString(after));});
|
||||
cursor(c,false);c.waitTicks(3);
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
var s=(HelloWorldScreen)m.gui.screen();s.setAccessSite("https://example.com/register");s.requireAccess(false);
|
||||
edit(s,"sanctuary.progression.code").setValue("bad");
|
||||
require(!button(s,"sanctuary.progression.enter").active,"Unrecognized access code blocks entry");bounds(s);
|
||||
s.prepareAccess(true);s.accept(welcome(PALETTE,"",false));
|
||||
require(button(s,"sanctuary.progression.enter").active,"Already recognized admission can resume");bounds(s);
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("hello262-access-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{
|
||||
var s=new HelloWorldScreen(new StarterPayloads.Welcome(new ProgressionPayloads.Hello("Poupoutain",PALETTE,""),"",false,
|
||||
List.of("zombie","camel","chicken").stream().map(id->new StarterCompanions.Choice(id,CompanionType.Rarity.COMMON)).toList()),()->{});
|
||||
m.gui.setScreen(s);colors(s).getFirst().onPress(PRESS);slots(s).getFirst().onPress(PRESS);edit(s,"sanctuary.starter.name").setValue("Petit Zombie");bounds(s);
|
||||
});
|
||||
cursor(c,false);c.waitTicks(3);c.takeScreenshot("hello264-zombie-"+locale+"-gui"+scale);
|
||||
cursor(c,true);c.waitTicks(3);c.takeScreenshot("hello264-zombie-cursor-right-"+locale+"-gui"+scale);cursor(c,false);
|
||||
c.runOnClient(m->{var s=(HelloWorldScreen)m.gui.screen();slots(s).get(1).onPress(PRESS);edit(s,"sanctuary.starter.name").setValue("Petit chameau");});
|
||||
c.waitTicks(3);c.takeScreenshot("hello264-camel-"+locale+"-gui"+scale);
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
c.runOnClient(m->{var s=(HelloWorldScreen)m.gui.screen();var egg=slots(s).getFirst();s.setFocused(null);
|
||||
var w=m.getWindow();double x=egg.getX()+11,y=egg.getY()+11,sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();
|
||||
m.mouseHandler.onMove(w.handle(),x*sx,y*sy,0,0);m.mouseHandler.handleAccumulatedMovement();});
|
||||
c.waitTicks(10);c.takeScreenshot("hello264-zombie-hover");cursor(c,false);
|
||||
}
|
||||
c.runOnClient(m->{var s=(HelloWorldScreen)m.gui.screen();s.prepareAccess(true);s.accept(welcome(PALETTE,"",false));edit(s,"sanctuary.starter.name").setValue(NAME);slots(s).get(1).onPress(PRESS);});
|
||||
}
|
||||
c.runOnClient(m->{
|
||||
var s=(HelloWorldScreen)m.gui.screen();
|
||||
int chosen=number(s,"color");
|
||||
m.options.guiScale().set(2);m.resizeGui();s=(HelloWorldScreen)m.gui.screen();
|
||||
require(colors(s).size()==9&&number(s,"color")==chosen,"Wider menu keeps nine colours and the selected RGB");
|
||||
m.options.guiScale().set(4);m.resizeGui();s=(HelloWorldScreen)m.gui.screen();
|
||||
require(colors(s).size()==9&&number(s,"color")==chosen&&edit(s,"sanctuary.starter.name").getValue().equals(NAME),"Narrow menu keeps nine colours, the draft and selected RGB");
|
||||
s.accept(welcome(List.of(PALETTE.getLast()),"",true));bounds(s);
|
||||
require(!colors(s).getFirst().active&&!page(s,true).active&&!page(s,false).active,"Saved identity and singleton palette stay locked");
|
||||
require(button(s,"sanctuary.progression.enter").active,"Saved identity can finish its familiar choice");
|
||||
m.gui.setScreen(new HelloWorldScreen(new ProgressionPayloads.Hello("Poupoutain",List.of(PALETTE.getFirst()),""),()->{}));s=(HelloWorldScreen)m.gui.screen();
|
||||
colors(s).getFirst().onPress(PRESS);bounds(s);
|
||||
require(button(s,"sanctuary.progression.enter").active,"Identity-only offers require no familiar");
|
||||
m.gui.setScreen(new HelloWorldScreen(new ProgressionPayloads.Hello("Poupoutain",List.of(),"storage_invalid"),()->{}));bounds((HelloWorldScreen)m.gui.screen());
|
||||
require(!button((HelloWorldScreen)m.gui.screen(),"sanctuary.progression.enter").active,"Empty offers stay visible and cannot be submitted");
|
||||
m.gui.setScreen(new TitleScreen(false));
|
||||
});
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
var babies=Hello262PreviewChecks.verifyRealBabies();
|
||||
new Starter052ClientChecks().runTest(c);
|
||||
require(babies.get(),"Welcome previews match the real native baby forms after admission");
|
||||
new Access166ClientChecks().runTest(c);
|
||||
System.out.println("HELLO262_CLIENT_PASS: Vulkan, cold welcome, FR/EN GUI 2/3/4, full palette, access and locked identity, rejection/drafts, real named and code admissions, saved reconnect");
|
||||
}
|
||||
private static List<Button> colors(HelloWorldScreen s){return buttons(s,"ColorButton");}
|
||||
private static List<Button> slots(HelloWorldScreen s){return buttons(s,"StarterSlot");}
|
||||
private static List<Button> buttons(HelloWorldScreen s,String type){return s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible&&b.getClass().getSimpleName().equals(type)).toList();}
|
||||
private static Button page(HelloWorldScreen s,boolean next){return buttons(s,"ColorPageButton").stream().filter(b->b.getMessage().getString().equals(next?">":"<")).findFirst().orElseThrow();}
|
||||
private static AbstractWidget widget(HelloWorldScreen s,String key){return s.children().stream().filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static Button button(HelloWorldScreen s,String key){return (Button)widget(s,key);}
|
||||
private static EditBox edit(HelloWorldScreen s,String key){return (EditBox)widget(s,key);}
|
||||
private static int number(Object target,String field){return (Integer)value(target,field);}
|
||||
private static float[] portraitAngles(HelloWorldScreen s){var portrait=s.children().stream().filter(w->w.getClass().getSimpleName().equals("PlayerPortrait")).findFirst().orElseThrow();return new float[]{(Float)value(portrait,"yaw"),(Float)value(portrait,"pitch")};}
|
||||
private static void cursor(ClientGameTestContext c,boolean bottomRight){c.runOnClient(m->{var w=m.getWindow();double x=bottomRight?w.getGuiScaledWidth()-1:0,y=bottomRight?w.getGuiScaledHeight()-1:0,sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});}
|
||||
@SuppressWarnings("unchecked") private static <T> T value(Object target,String field){try{var f=target.getClass().getDeclaredField(field);f.setAccessible(true);return (T)f.get(target);}catch(Exception error){throw new AssertionError(error);}}
|
||||
private static void bounds(HelloWorldScreen s){
|
||||
var widgets=s.children().stream().filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.visible).toList();
|
||||
for(var w:widgets)require(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Control fits "+s.width+"x"+s.height+": "+w.getMessage().getString());
|
||||
for(String key:List.of("sanctuary.progression.enter","gui.back")){
|
||||
var b=button(s,key);require(net.minecraft.client.Minecraft.getInstance().font.width(b.getMessage())<=b.getWidth()-8,"Action label fits without scrolling: "+key);
|
||||
}
|
||||
var biography=edit(s,"sanctuary.progression.bio");
|
||||
for(String hint:List.of("sanctuary.progression.bio_hint","sanctuary.starter.name_hint"))
|
||||
require(net.minecraft.client.Minecraft.getInstance().font.width(net.minecraft.network.chat.Component.translatable(hint))<=biography.getWidth()-8,"Placeholder fits the hotbar column: "+hint);
|
||||
var enter=button(s,"sanctuary.progression.enter");var back=button(s,"gui.back");
|
||||
require(biography.getWidth()==182&&Math.abs(biography.getX()*2+biography.getWidth()-s.width)<=1,"Form uses the centred native hotbar width");
|
||||
require(enter.getWidth()==biography.getWidth()&&back.getWidth()==biography.getWidth(),"Both actions use the same width as the form");
|
||||
require(enter.getY()==s.height-26&&back.getBottom()<s.height-33,"Enter occupies the native Done footer; Back stays in content");
|
||||
if(!slots(s).isEmpty()){
|
||||
var name=edit(s,"sanctuary.starter.name");
|
||||
require(name.getX()==biography.getX()&&name.getWidth()==biography.getWidth(),"Name and biography share the hotbar column");
|
||||
require(slots(s).getFirst().getY()>biography.getBottom()&&slots(s).getLast().getBottom()<name.getY(),"Familiar choices follow biography and precede the name");
|
||||
for(var egg:slots(s))require(egg.getX()>=biography.getX()&&egg.getRight()<=biography.getRight(),"Egg choices are inside the central column");
|
||||
}
|
||||
var portrait=widgets.stream().filter(w->w.getClass().getSimpleName().equals("PlayerPortrait")).findFirst().orElseThrow();
|
||||
require(portrait.getY()==number(s,"previewY")&&portrait.getHeight()==number(s,"previewHeight"),"Portraits share the same bounds and floor");
|
||||
for(int i=0;i<widgets.size();i++)for(int j=i+1;j<widgets.size();j++){
|
||||
var a=widgets.get(i);var b=widgets.get(j);
|
||||
require(a.getRight()<=b.getX()||b.getRight()<=a.getX()||a.getBottom()<=b.getY()||b.getBottom()<=a.getY(),"Controls do not overlap: "+a.getMessage().getString()+" / "+b.getMessage().getString());
|
||||
}
|
||||
}
|
||||
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka.sanctuary.companion.CompanionType;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.state.*;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Every eligible starter submits a textured native model before item components are bound. */
|
||||
final class Hello262PreviewChecks {
|
||||
static final List<CompanionType> TYPES=Arrays.stream(CompanionType.values()).filter(t->t.rarity.ordinal()<2).toList();
|
||||
@SuppressWarnings({"rawtypes","unchecked"}) static void run(ClientGameTestContext c){
|
||||
c.runOnClient(m->{
|
||||
require(m.level==null&&m.player==null,"3D previews precede world admission");
|
||||
boolean unbound=false;
|
||||
try{new ItemStack(Items.COW_SPAWN_EGG);}catch(NullPointerException expected){unbound="Components not bound yet".equals(expected.getMessage());}
|
||||
require(unbound,"Preview is tested in the cold multiplayer component state");
|
||||
for(var type:TYPES){
|
||||
var state=HelloFamiliarPreview.state(type.id());
|
||||
var livingState=(LivingEntityRenderState)state;
|
||||
if(List.of("cat","chicken","cow","pig","wolf","zombie","turtle").contains(type.id()))require(livingState.isBaby,"Native baby appearance is selected for "+type.id());
|
||||
if(List.of("spider","skeleton","frog","cod").contains(type.id()))require(!livingState.isBaby,"Species without native baby forms retain their real familiar appearance: "+type.id());
|
||||
EntityRenderer renderer=m.getEntityRenderDispatcher().getRenderer(state);
|
||||
var calls=new AtomicInteger();
|
||||
var collector=(SubmitNodeCollector)Proxy.newProxyInstance(Hello262PreviewChecks.class.getClassLoader(),new Class<?>[]{SubmitNodeCollector.class},(proxy,method,args)->{
|
||||
if(method.getName().equals("order"))return proxy;
|
||||
if(method.getName().equals("submitModel"))calls.incrementAndGet();
|
||||
if(method.isDefault())return InvocationHandler.invokeDefault(proxy,method,args);
|
||||
return null;
|
||||
});
|
||||
var camera=new CameraRenderState();camera.pos=Vec3.ZERO;
|
||||
renderer.submit(state,new PoseStack(),collector,camera);
|
||||
require(calls.get()>0,"Native model is submitted for "+type.id());
|
||||
require(state.boundingBoxHeight>0&&state.boundingBoxWidth>0,"Preview has finite native dimensions: "+type.id());
|
||||
if(renderer instanceof LivingEntityRenderer living){
|
||||
var texture=living.getTextureLocation((LivingEntityRenderState)state);
|
||||
require(m.getResourceManager().getResource(texture).isPresent(),"Preview texture exists for "+type.id()+": "+texture);
|
||||
}
|
||||
}
|
||||
m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new Gallery());
|
||||
});
|
||||
c.getInput().setCursorPos(0,0);c.waitTicks(4);c.takeScreenshot("hello262-cold-models-left");
|
||||
float[] left=c.computeOnClient(m->{var state=(LivingEntityRenderState)((Gallery)m.gui.screen()).states.get("cat");return new float[]{state.yRot,state.xRot};});
|
||||
c.getInput().setCursorPos(640,480);c.waitTicks(4);c.takeScreenshot("hello262-cold-models-right");
|
||||
c.runOnClient(m->{
|
||||
var state=(LivingEntityRenderState)((Gallery)m.gui.screen()).states.get("cat");
|
||||
require(state.yRot<left[0]&&state.xRot>left[1],"Native familiar head follows the cursor horizontally and vertically");
|
||||
require(m.level==null&&m.player==null,"Model rendering never creates a world or player");
|
||||
});
|
||||
c.getInput().setCursorPos(0,0);
|
||||
System.out.println("HELLO262_MODELS_PASS: "+TYPES.size()+" native starter models with baby forms and textures, unbound item components, 3D mouse tracking");
|
||||
}
|
||||
@SuppressWarnings({"rawtypes","unchecked"}) static java.util.concurrent.atomic.AtomicBoolean verifyRealBabies(){
|
||||
var verified=new java.util.concurrent.atomic.AtomicBoolean();
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(verified.get()||m.player==null||m.level==null)return;
|
||||
var mismatches=new ArrayList<String>();
|
||||
for(var type:TYPES){
|
||||
var preview=(LivingEntityRenderState)HelloFamiliarPreview.state(type.id());
|
||||
var entity=preview.entityType.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
require(entity instanceof Mob,"Familiar is a native mob: "+type.id());
|
||||
var mob=(Mob)entity;mob.setId(m.player.getId()+1000+type.ordinal());mob.setPos(m.player.position());mob.setBaby(true);
|
||||
EntityRenderer renderer=m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
var actual=(LivingEntityRenderState)renderer.createRenderState(mob,1);
|
||||
if(preview.isBaby!=actual.isBaby)mismatches.add(type.id()+" age "+preview.isBaby+" / "+actual.isBaby);
|
||||
if(preview.ageScale!=actual.ageScale)mismatches.add(type.id()+" age scale "+preview.ageScale+" / "+actual.ageScale);
|
||||
if(renderer instanceof LivingEntityRenderer living){
|
||||
var texture=living.getTextureLocation(actual);var previewTexture=living.getTextureLocation(preview);
|
||||
if(!texture.equals(previewTexture))mismatches.add(type.id()+" texture "+previewTexture+" / "+texture);
|
||||
}
|
||||
}
|
||||
require(mismatches.isEmpty(),"All previews match native Mob.setBaby(true): "+String.join("; ",mismatches));
|
||||
verified.set(true);System.out.println("HELLO263_BABIES_PASS: all 44 previews match native Mob.setBaby(true) ages, age scales and textures");
|
||||
});
|
||||
return verified;
|
||||
}
|
||||
private static final class Gallery extends Screen {
|
||||
final Map<String,EntityRenderState> states=new HashMap<>();
|
||||
Gallery(){super(Component.literal("Hello World · 3D previews"));}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float delta){extractMenuBackground(g);}
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g,int mx,int my,float delta){
|
||||
super.extractRenderState(g,mx,my,delta);g.centeredText(font,title,width/2,4,0xFFFFFFFF);
|
||||
int cellWidth=width/8,cellHeight=(height-20)/6;
|
||||
for(int i=0;i<TYPES.size();i++){
|
||||
var type=TYPES.get(i);int x=i%8*cellWidth,y=20+i/8*cellHeight;
|
||||
states.put(type.id(),HelloFamiliarPreview.extract(g,type.id(),x+4,y+2,cellWidth-8,cellHeight-16,mx,my));
|
||||
g.centeredText(font,type.label(),x+cellWidth/2,y+cellHeight-11,0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void require(boolean condition,String message){if(!condition)throw new AssertionError(message);}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.HunterEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Hunter286ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<HunterEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var phase=new java.util.concurrent.atomic.AtomicReference<>(HunterEntity.Phase.IDLE);
|
||||
var familyAir=new AtomicBoolean();var familyPeak=new java.util.concurrent.atomic.AtomicReference<Double>(1D);var wildPeak=new java.util.concurrent.atomic.AtomicReference<Double>(1D);
|
||||
var signature=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.familiar.FamiliarTechniques.Cast>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{
|
||||
var mob=wild.get();if(mob!=null&&!mob.isRemoved()){phase.set(mob.phase());wildPeak.set(Math.max(wildPeak.get(),mob.getY()));}
|
||||
if(s.getPlayerList().getPlayers().isEmpty())return;var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
var cast=FamiliarBattle.state(p).cast;if(cast!=null&&!cast.automatic&&cast.technique.effect().equals("pounce"))signature.compareAndSet(null,cast);
|
||||
if(pet!=null&&signature.get()!=null){familyPeak.set(Math.max(familyPeak.get(),pet.getY()));if(pet.getY()>1.4&&!pet.onGround())familyAir.set(true);}
|
||||
});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Hunter286-"+UUID.randomUUID());s.setSeed("286");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
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());
|
||||
for(int x=-2;x<=2;x++)for(int z:new int[]{3,7,11})p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.HUNTER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.HUNTER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:hunter"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.HUNTER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/hunter.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.HUNTER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
wild.get().teleportTo(3.5,1,4.5);var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,8.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);
|
||||
c.waitFor(m->familyAir.get(),250);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(1);c.takeScreenshot("hunter286-familiar-airborne");
|
||||
c.waitTicks(60);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(signature.get()!=null&&signature.get().hit.size()==1&&enemy.get().getHealth()<100&&familyPeak.get()>2,"Real G uses an airborne native body and one signature hit");
|
||||
check(pet.isAlive()&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"The miniature survives and retains its durable G cooldown");
|
||||
FamiliarBattle.command(p,"follow");wild.get().teleportTo(.5,1,4.5);enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("hunter286-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.HUNTER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.HUNTER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Griffure":"Claw strike";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("hunter286-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
p.getAbilities().invulnerable=false;p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);p.setHealth(6);
|
||||
var mob=wild.get();mob.teleportTo(.5,1,8.5);mob.setPermanentlyInvulnerable(false);mob.setOnGround(true);mob.setTarget(p);mob.setNoAi(false);phase.set(HunterEntity.Phase.IDLE);wildPeak.set(1D);
|
||||
});c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.WINDUP,200);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("hunter286-windup");
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.LEAPING,150);c.waitTicks(2);c.takeScreenshot("hunter286-wild-airborne");
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.RECOVERING,150);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(p.getHealth()<6&&p.getHealth()>0&&wildPeak.get()>2,"The wild Hunter physically jumps onto the real player and attacks");
|
||||
check(wild.get().pounceCooldown()>0&&!wild.get().doHurtTarget(p.level(),p),"Recovery prevents a second melee strike after impact");p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
});c.takeScreenshot("hunter286-contact");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(28);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(wild.get().onGround()&&!wild.get().isNoGravity(),"The Hunter lands with normal gravity");
|
||||
wild.get().discard();var mob=Infected.HUNTER.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,8.5);mob.setOnGround(true);wild.set(mob);p.level().addFreshEntity(mob);
|
||||
p.setGameMode(net.minecraft.world.level.GameType.SURVIVAL);p.setHealth(6);p.teleportTo(.5,1,.5);p.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);p.setYRot(0);p.setXRot(0);mob.setTarget(p);phase.set(HunterEntity.Phase.IDLE);wildPeak.set(1D);
|
||||
});c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.LEAPING,250);c.getInput().holdKey(o->o.keyRight);c.waitTicks(12);c.getInput().releaseKey(o->o.keyRight);c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(p.getHealth()==6&&Math.abs(p.getX()-.5)>.7&&wildPeak.get()>2&&wild.get().pounceCooldown()>0,"Real sideways keyboard input dodges the fixed airborne trajectory");p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("hunter286-dodged");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();wild.get().discard();wild.set(null);enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(24)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("HUNTER286_CLIENT_PASS: Vulkan, adult skin, native airborne G/contact/cooldown, real wild jump/player attack/recovery/landing, real keyboard dodge, three adult miniature sizes, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Infected280ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Infected281-"+UUID.randomUUID());s.setSeed("281");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
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());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.SPAWN_EGG);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:infected"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.TYPE.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(InfectedRenderer.TEXTURE).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.SPAWN_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.UNCOMMON,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});c.waitTicks(60);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("infected281-adult-zombie");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("infected281-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.SPAWN_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.SPAWN_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Morsure":"Bite";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("infected281-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
System.out.println("INFECTED281_CLIENT_PASS: Vulkan, adult-only infected, three real miniature sizes, translated egg and FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class Map274ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;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);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Map274 "+UUID.randomUUID());s.setSeed("274");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);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);});
|
||||
checkCache();
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.isComplete()&&map.status().equals("ready"),400);
|
||||
c.runOnClient(m->{require(!ProgressionClient.progress.atlas(),"Property map does not grant the Atlas aptitude");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.eligible()&&d.count()==0&&d.cost()==0,"Hello World admits free chunk selection");});
|
||||
c.waitTicks(3);c.takeScreenshot("map274-map-free");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.claim_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).count()==1,200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.get(9,8)!=null&&map.claimOverlay.remaining(map.claimOverlay.get(9,8))==0,"New ownership appears immediately without reopening the map");});
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().setItem(9,new net.minecraft.world.item.ItemStack(fr.koka.sanctuary.gems.Gems.SAPPHIRE.gem(),64));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(5);c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).sapphires()==64,200);
|
||||
c.waitTicks(3);c.takeScreenshot("map274-map-sapphires");
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.claims.protect_at_cost").onPress(ENTER));
|
||||
c.waitFor(m->view(m.gui.screen())!=null&&view(m.gui.screen()).message().equals("protected"),200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.claimOverlay.remaining(map.claimOverlay.get(9,8))>ChunkClaims.PROTECTION_MILLIS-10_000,"Protection updates the currently open map immediately");});
|
||||
world.getServer().runOnServer(server->{
|
||||
var p=server.getPlayerList().getPlayers().getFirst();var identities=ProgressionService.session(server).inhabitants;
|
||||
UUID offline=UUID.randomUUID();int color=identities.available().getFirst();
|
||||
try{require(identities.create(offline,"Camille","",color),"Offline owner identity created");identities.arrive(offline);var store=ChunkPropertyService.store(server);
|
||||
fixture(store,p.getUUID(),10,8,true);fixture(store,p.getUUID(),11,8,false);fixture(store,offline,9,9,true);fixture(store,offline,10,9,true);
|
||||
}catch(Exception e){throw new RuntimeException(e);}
|
||||
});
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));button(m.gui.screen(),"sanctuary.atlas.recenter").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(11,8)!=null,400);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();var own=map.claimOverlay.get(9,8);var other=map.claimOverlay.get(10,9);
|
||||
require(own!=null&&own.claim().color()!=other.claim().color(),"Two owners have their distinct persistent colors before any context click");
|
||||
require(map.claimOverlay.remaining(own)>0&&map.claimOverlay.remaining(map.claimOverlay.get(11,8))==0,"Same owner has both protected and unprotected chunks");
|
||||
require(other.claim().name().equals("Camille")&&map.claimOverlay.remaining(other)>0,"Offline owner name, color and active protection arrive with map tiles");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("map274-local-two-owners");
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(ChunkPropertyService.sapphires(p)==0,"Map action debits full stack");p.setExperienceLevels(100);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase Atlas normally");ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);c.waitTicks(30);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null,new fr.koka.sanctuary.community.CommunityTask.Location("minecraft:overworld",152,-40,160)));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.renderedPixelAt(152,136)!=0,400);
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(10,8)!=null,400);
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();int own=0x40000000|map.claimOverlay.get(9,8).claim().color();int other=0x40000000|map.claimOverlay.get(10,9).claim().color();
|
||||
require(pixel(map,144,128)==own&&pixel(map,159,132)==own&&pixel(map,160,132)==own,"Protected neighbours use one pixel tint without chunk borders");
|
||||
require(pixel(map,144,144)==other&&pixel(map,159,148)==other&&pixel(map,160,148)==other,"Offline owner regions connect in the same texture");
|
||||
require(pixel(map,176,128)==0,"Unprotected declared property has no protection tint");
|
||||
});
|
||||
c.takeScreenshot("map274-colors-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->rightClick(m.gui.screen()));
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{var d=view(m.gui.screen());require(d.x()==9&&d.z()==8,"Ordinary Atlas right-click selects actual chunk without a mode toggle");widgets(m.gui.screen()).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible).forEach(b->require(b.getX()>=0&&b.getRight()<=m.gui.screen().width&&b.getBottom()<=m.gui.screen().height,"Visible controls fit"));});
|
||||
c.waitTicks(3);c.takeScreenshot("map274-map-protected-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));require(m.gui.screen() instanceof SanctuaryAtlasScreen&&view(m.gui.screen())==null,"Escape closes the context panel and keeps the map");});
|
||||
}
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_visible").onPress(ENTER);require(!ChunkPropertyClient.propertiesVisible(),"Properties can be hidden without changing server protection");rightClick(m.gui.screen());});
|
||||
c.waitFor(m->view(m.gui.screen())!=null,200);
|
||||
c.runOnClient(m->{require(view(m.gui.screen()).x()==9,"Right-click remains available when claims are hidden");m.gui.screen().keyPressed(new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_ESCAPE,0,0));});
|
||||
c.waitTicks(3);c.takeScreenshot("map274-hidden-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->require(!ChunkPropertyClient.propertiesVisible(),"Hidden state is shared with Pause and retained when the map closes"));
|
||||
c.waitTicks(3);c.takeScreenshot("map274-hidden-pause");
|
||||
c.runOnClient(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.claimOverlay.get(10,9)!=null&&map.claimOverlay.get(11,8)!=null,400);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.claims.map_hidden").onPress(ENTER);require(ChunkPropertyClient.propertiesVisible(),"Properties can be shown again with their cached current state");});
|
||||
c.waitTicks(3);c.takeScreenshot("map274-restored-map");
|
||||
c.runOnClient(m->m.gui.setScreen(new net.minecraft.client.gui.screens.PauseScreen(true)));
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess pause&&pause.sanctuary$pauseMap()!=null&&pause.sanctuary$pauseMap().claimOverlay.get(10,9)!=null,400);
|
||||
c.runOnClient(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();require(map.claimOverlay.get(10,9).claim().name().equals("Camille"),"Pause preview receives offline ownership with its terrain tiles");});
|
||||
c.waitTicks(3);c.takeScreenshot("map274-pause-preview");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private static void fixture(ChunkClaims store,UUID owner,int x,int z,boolean protect)throws Exception{
|
||||
var key=new ChunkClaims.Key(x,z);var tx=store.prepare(owner,"claim",key,store.revision());require(tx!=null,"Fixture chunk is free");store.recover(owner,tx.receipt().toString());
|
||||
if(protect){tx=store.prepare(owner,"protect",key,store.revision());store.recover(owner,tx.receipt().toString());}
|
||||
}
|
||||
private static void checkCache(){
|
||||
var time=new java.util.concurrent.atomic.AtomicLong();var cache=new ClaimMapOverlay(time::get);UUID id=UUID.randomUUID();var key=new ChunkClaims.Key(-1,2);
|
||||
var paint=new ChunkPropertyPayloads.Paint(-1,2,id,"Test",0x123456,1000);cache.begin(7);cache.observe(-1,2);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of(paint)));
|
||||
require(cache.get(-1,2).claim().color()==0x123456&&cache.remaining(cache.get(-1,2))==1000,"Exact RGB is retained");
|
||||
time.set(1_001_000_000L);require(cache.remaining(cache.get(-1,2))==0&&cache.get(-1,2)!=null,"Expiration changes style while keeping ownership");require(cache.tint(-1,2)==0&&cache.expiredTiles().contains(key)&&cache.expiredTiles().isEmpty(),"Expiry invalidates its pixel tile once");
|
||||
cache.begin(8);cache.accept(new ChunkPropertyPayloads.Overlay(7,List.of(key),List.of()));require(cache.get(-1,2)!=null,"Stale viewport cannot erase current claims");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(key),List.of(paint)));require(cache.remaining(cache.get(-1,2))==1000,"Live purchase update survives a pending viewport refresh");
|
||||
var hidden=new ChunkClaims.Key(30,30);cache.accept(new ChunkPropertyPayloads.Overlay(0,List.of(hidden),List.of(new ChunkPropertyPayloads.Paint(30,30,id,"Hidden",0xFFFFFF,1000))));require(cache.get(30,30)==null,"Live updates cannot reveal an unobserved chunk");
|
||||
cache.accept(new ChunkPropertyPayloads.Overlay(8,List.of(key),List.of()));require(cache.get(-1,2)==null,"An empty current tile removes stale paint");cache.clear();
|
||||
}
|
||||
private static int pixel(SanctuaryAtlasScreen map,int x,int z){return ((net.minecraft.client.renderer.texture.DynamicTexture)field(map,"footprintTexture")).getPixels().getPixel(x-(int)field(map,"originX"),z-(int)field(map,"originZ"));}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static ChunkPropertyPayloads.View view(Screen screen){return screen instanceof SanctuaryAtlasScreen map?map.propertyView():null;}
|
||||
private static void rightClick(Screen screen){var atlas=(SanctuaryAtlasScreen)screen;require(atlas.mouseClicked(new MouseButtonEvent(atlas.width/2.+(152-atlas.centerX())*atlas.zoom(),(atlas.contentTop+atlas.contentBottom)/2.+(136-atlas.centerZ())*atlas.zoom(),new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false),"Right click is handled on the terrain");}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.PauseScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Five separated survey fixtures, real remote chunk delivery and native Vulkan rendering. */
|
||||
public final class Map279ClientChecks implements FabricClientGameTest {
|
||||
private static final int[][] ISLANDS={{0,0},{8192,0},{-8192,0},{0,8192},{0,-8192}};
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get() && m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Map279-"+UUID.randomUUID());s.setSeed("279");s.setAllowCommands(false);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null
|
||||
&& e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null && ProgressionClient.active() && m.gui.screen()==null && !IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
p.getAbilities().invulnerable=true;p.getAbilities().flying=true;p.onUpdateAbilities();p.setNoGravity(true);
|
||||
p.teleportTo(p.level(),.5,80,.5,Set.of(),0,0,true);
|
||||
var r=ProgressRecord.INITIAL.buy("atlas",1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");
|
||||
var store=AtlasService.observations(p);byte[] history=new byte[256];Arrays.fill(history,(byte)42);
|
||||
for(int i=0;i<16384;i++)store.observe(new AtlasStore.Tile(i,3000,history));
|
||||
for(int i=0;i<ISLANDS.length;i++) {
|
||||
byte[] colors=new byte[256];Arrays.fill(colors,(byte)((MapColor.GRASS.id+i*2)*4+2));
|
||||
for(int z=-8;z<=8;z++)for(int x=-8;x<=8;x++)store.observe(new AtlasStore.Tile(ISLANDS[i][0]/16+x,ISLANDS[i][1]/16+z,colors));
|
||||
}
|
||||
check(store.size()>16384 && !store.full(),"Old full atlas resumes surveying");
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a && a.isComplete() && a.observedTiles()>0,400);
|
||||
c.runOnClient(m->zoom((SanctuaryAtlasScreen)m.gui.screen(),-23));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a && a.detail()>0 && a.isComplete() && allIslands(a),500);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(a.zoom()<1./32,"Zoom passes one screen pixel per chunk");check(a.renderedPixelAt(9008,0)==0,"Remote destination is initially unknown");});
|
||||
c.takeScreenshot("map279-five-islands-overview");
|
||||
int token=c.computeOnClient(m->field(m.gui.screen(),"token",Integer.class));
|
||||
// The camera remains at the origin while native chunks arrive beyond the remote survey fixture.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(p.level(),9008.5,80,.5,Set.of(),0,0,true);});
|
||||
var gold=new BlockPos(9008,100,0);
|
||||
server.runOnServer(s->s.overworld().getChunk(563,0));
|
||||
c.waitTicks(30);server.runOnServer(s->s.overworld().setBlockAndUpdate(gold,Blocks.GOLD_BLOCK.defaultBlockState()));
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.renderedPixelAt(9008,0)!=0;},200);
|
||||
for(int attempt=0;attempt<30;attempt++) {
|
||||
boolean surveyed=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var tile=AtlasService.observations(p).get(563,0);return tile!=null && Byte.toUnsignedInt(tile.colors()[0])/4==MapColor.GOLD.id;});
|
||||
if(surveyed)break;c.waitTicks(2);
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var tile=AtlasService.observations(p).get(563,0);
|
||||
check(tile!=null && Byte.toUnsignedInt(tile.colors()[0])/4==MapColor.GOLD.id,"Native remote construction is surveyed after old capacity");});
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(field(a,"token",Integer.class)==token && Math.abs(a.centerX())<1,"Remote travel updates the same fixed overview stream");});
|
||||
for(String lang:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});
|
||||
c.waitTicks(15);c.takeScreenshot("map279-overview-"+lang+"-gui"+scale);
|
||||
c.runOnClient(m->check(allIslands((SanctuaryAtlasScreen)m.gui.screen()),"Five islands remain painted after GUI resize"));
|
||||
}
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();zoom(a,-12);});
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.zoom()<1./256 && a.isComplete();},400);
|
||||
c.takeScreenshot("map279-beyond-256-blocks-per-pixel");
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();zoom(a,-100);});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).detail()==16 && ((SanctuaryAtlasScreen)m.gui.screen()).isComplete(),400);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(Math.min(a.width,a.height)/a.zoom()>60000000,"The whole world fits both window axes");});
|
||||
c.takeScreenshot("map279-world-scale");
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();zoom(a,100);a.recenter();});
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.detail()==0 && a.isComplete() && a.renderedPixelAt(9008,0)==MapColor.GOLD.calculateARGBColor(MapColor.Brightness.NORMAL);},400);
|
||||
c.takeScreenshot("map279-return-to-blocks");
|
||||
c.runOnClient(m->m.gui.setScreen(new PauseScreen(true)));
|
||||
c.waitFor(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();return map!=null && map.knownPixels()>0;},300);
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryAtlasScreen(null)));
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()>0,300);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.overworld().setChunkForced(750,0,true);s.overworld().getChunk(750,0);s.overworld().setBlockAndUpdate(new BlockPos(12000,100,0),Blocks.DIAMOND_BLOCK.defaultBlockState());
|
||||
check(!AtlasService.observations(p).contains(750,0),"Loaded operator fixture is outside personal discoveries");
|
||||
((net.minecraft.client.server.IntegratedServer)s).setWorldAllowCommands(true);
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.operator && field(m.gui.screen(),"operator",Boolean.class) && ((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()>0,400);
|
||||
c.runOnClient(m->zoom((SanctuaryAtlasScreen)m.gui.screen(),-25));
|
||||
try{c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.detail()>0 && a.isComplete() && a.renderedPixelAt(12000,0)!=0;},600);}
|
||||
catch(AssertionError e){c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();System.out.println("MAP279_OPERATOR_DEBUG status="+a.status()+" detail="+a.detail()+" complete="+a.isComplete()+" tiles="+a.observedTiles()+" center="+a.centerX()+","+a.centerZ()+" operator="+field(a,"operator",Boolean.class));});server.runOnServer(s->System.out.println("MAP279_OPERATOR_LOADED="+(s.overworld().getChunkSource().getChunkNow(750,0)!=null)));throw e;}
|
||||
c.takeScreenshot("map279-operator-overview");
|
||||
server.runOnServer(s->{check(!AtlasService.observations(s.getPlayerList().getPlayers().getFirst()).contains(750,0),"Operator reduction never adds personal discoveries");((net.minecraft.client.server.IntegratedServer)s).setWorldAllowCommands(false);});
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return !ProgressionClient.operator && !field(a,"operator",Boolean.class) && a.isComplete() && a.observedTiles()>0 && a.renderedPixelAt(12000,0)==0;},400);
|
||||
server.runOnServer(s->{s.overworld().setChunkForced(750,0,false);var p=s.getPlayerList().getPlayers().getFirst();p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()==0,200);
|
||||
System.out.println("MAP279_CLIENT_PASS: Vulkan, expanded exploration, five separated surveys, fixed distant overview, world scale, return to exact blocks, Pause, FR/EN GUI 2/3/4, operator overview isolation and revocation");
|
||||
}finally{intro.set(false);c.runOnClient(m->m.gui.setScreen(null));}
|
||||
}
|
||||
private static boolean allIslands(SanctuaryAtlasScreen a){for(var island:ISLANDS)if(a.renderedPixelAt(island[0],island[1])==0)return false;return true;}
|
||||
private static void zoom(SanctuaryAtlasScreen a,double scroll){
|
||||
double y=field(a,"focusY",Integer.class)+field(a,"focusSize",Integer.class)/2.;
|
||||
check(a.mouseScrolled(a.width/2.,y,0,scroll),"Native map wheel accepted");
|
||||
}
|
||||
private static <T> T field(Object target,String name,Class<T> type){try{var f=target.getClass().getDeclaredField(name);f.setAccessible(true);return type.cast(f.get(target));}catch(Exception e){throw new RuntimeException(e);}}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.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.*;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
public final class Menu275ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;var armed=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(armed.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
widgets(s).filter(EditBox.class::isInstance).map(EditBox.class::cast).filter(e->key(e).equals("sanctuary.progression.bio")).findFirst().orElseThrow().setValue("Je parcours Sanctuary et je construis mon histoire avec les habitants. Je garde les souvenirs des lieux explorés, des rencontres et des projets du village. ".repeat(3));
|
||||
Starter039TestSupport.selectFirst(s);widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(ENTER);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);armed.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Menu275 "+UUID.randomUUID());s.setSeed("275");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);
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);server.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);
|
||||
p.getStats().setValue(p,net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.PLAY_TIME),1875680);
|
||||
fr.koka.sanctuary.knowledge.BlockKnowledgeService.recordPlaced(p,net.minecraft.world.level.block.Blocks.STONE);
|
||||
});
|
||||
c.runOnClient(m->{require(chat(m).stream().noneMatch(t->t.contains("first property is free")||t.contains("première propriété est gratuite")),"No arrival property advertisement");m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->chat(m).stream().anyMatch(t->t.contains("Atlas to access properties")||t.contains("Carte pour accéder aux propriétés")),200);
|
||||
c.runOnClient(m->{require(m.gui.screen()==null,"Locked property command does not open a map");m.gui.setScreen(new SanctuaryAtlasScreen(null,true));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.status().equals("locked"),200);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();require(map.knownPixels()==0,"Retired local-map constructor cannot reveal terrain");int token=(int)field(map,"token");net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking.send(new ChunkPropertyPayloads.MapRequest(token,8,8));});
|
||||
c.waitTicks(5);c.runOnClient(m->{require(((SanctuaryAtlasScreen)m.gui.screen()).knownPixels()==0,"Legacy map packet is locked on the server");m.gui.setScreen(null);});
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),false);});
|
||||
c.waitTicks(3);c.takeScreenshot("menu275-locked-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.progression.identity").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof ProgressionScreen&&InhabitantOverviewClient.data()!=null,300);
|
||||
c.runOnClient(m->identityLayout(m.gui.screen()));c.waitTicks(3);c.takeScreenshot("menu275-inhabitant-"+locale+"-gui"+scale);
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.cycle.faction").onPress(ENTER));c.waitFor(m->m.gui.screen() instanceof CycleFactionScreen,200);
|
||||
c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof ProgressionScreen,"Faction returns to Habitant");button(m.gui.screen(),"sanctuary.familiar.title").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof FamiliarScreen,200);c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof ProgressionScreen,"Familiar returns to Habitant");});
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(new PauseScreen(true)));
|
||||
}
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(20);require(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase actual Atlas");ProgressionService.send(p,"");});
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess p&&p.sanctuary$pauseMap()!=null&&p.sanctuary$pauseMap().knownPixels()>0,400);
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess p&&p.sanctuary$pauseMap()!=null&&p.sanctuary$pauseMap().knownPixels()>0,400);
|
||||
c.runOnClient(m->pauseLayout(m.gui.screen(),true));c.waitTicks(3);c.takeScreenshot("menu275-atlas-"+locale+"-gui"+scale);
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
c.runOnClient(m->button(m.gui.screen(),"menu.options").onPress(ENTER));c.waitFor(m->m.gui.screen() instanceof net.minecraft.client.gui.screens.options.OptionsScreen,200);
|
||||
c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof PauseScreen,"Native options return to Pause");});
|
||||
}
|
||||
}
|
||||
c.runOnClient(m->m.getConnection().sendCommand("sanctuary property"));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.knownPixels()>0,400);
|
||||
c.runOnClient(m->{var map=(SanctuaryAtlasScreen)m.gui.screen();double x=map.width/2.+(152-map.centerX())*map.zoom(),y=(map.contentTop+map.contentBottom)/2.+(136-map.centerZ())*map.zoom();map.mouseClicked(new MouseButtonEvent(x,y,new MouseButtonInfo(com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_RIGHT,0)),false);});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen map&&map.propertyView()!=null,200);
|
||||
c.runOnClient(m->{require(((SanctuaryAtlasScreen)m.gui.screen()).propertyView().eligible(),"Properties remain available through the unlocked Atlas");m.gui.setScreen(new PauseScreen(true));});
|
||||
world.getServer().runOnServer(server->{var p=server.getPlayerList().getPlayers().getFirst();require(AptitudeService.set(p,new AptitudeService.Set("atlas",false,AptitudeService.preferences(p).disabledMask())),"Disable Atlas");AptitudeService.send(p);});
|
||||
c.waitFor(m->m.gui.screen() instanceof PauseAtlasAccess p&&p.sanctuary$pauseMap()==null,200);
|
||||
c.runOnClient(m->{pauseLayout(m.gui.screen(),false);m.gui.setScreen(null);});
|
||||
}finally{Starter039TestSupport.manual=false;}
|
||||
}
|
||||
private static void identityLayout(Screen s){
|
||||
require(!has(s,"sanctuary.property.title")&&!has(s,"sanctuary.cycle.log"),"Habitant has neither Property nor the activity history");
|
||||
var faction=button(s,"sanctuary.cycle.faction");var familiar=button(s,"sanctuary.familiar.title");require(faction.getY()==familiar.getY()&&faction.getRight()<familiar.getX(),"Only Factions and Familiar share the top row");
|
||||
var skin=widgets(s).filter(PlayerSkinWidget.class::isInstance).map(PlayerSkinWidget.class::cast).findFirst().orElseThrow();
|
||||
var stats=button(s,"sanctuary.inhabitant.overview.stats");var bio=widget(s,"sanctuary.inhabitant.bio");
|
||||
require(stats.getRight()<skin.getX()&&bio.getX()>skin.getRight(),"Statistics, character and biography occupy three distinct columns");
|
||||
require(Math.abs(skin.getX()+skin.getWidth()/2-s.width/2)<=2,"Character is centered with room around it");
|
||||
require(widgets(s).noneMatch(w->w.getClass().getSimpleName().equals("Activity")||w.getClass().getSimpleName().equals("Frame")),"Activity and opaque statistics frames are absent");
|
||||
String time=widget(s,"sanctuary.inhabitant.overview.play_time").getMessage().getString();
|
||||
require((time.contains("1 d")||time.contains("1 j"))&&time.contains("2 h")&&time.contains("min")&&time.contains(" s")&&!time.matches(".*\\d+[.,]\\d+.*"),"Played time uses whole days, hours, minutes and seconds: "+time);
|
||||
}
|
||||
private static void pauseLayout(Screen s,boolean atlas){
|
||||
var map=((PauseAtlasAccess)s).sanctuary$pauseMap();require((map!=null)==atlas,"Pause map matches purchased active Atlas");
|
||||
String[] keys={"sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.progression.identity","sanctuary.arena.title"};
|
||||
var article=widget(s,"sanctuary.community.article");var notice=widget(s,"sanctuary.community.notice");
|
||||
var adventure=widget(s,"sanctuary.community.pause_group_adventure");var community=widget(s,"sanctuary.community.pause_group_community");var game=widget(s,"sanctuary.community.pause_group_game");
|
||||
require(adventure.getX()<community.getX(),"Adventure and Community remain distinct left/right groups");
|
||||
if(atlas){
|
||||
require(button(s,keys[0]).getX()==button(s,keys[1]).getX()&&button(s,keys[0]).getY()<button(s,keys[1]).getY(),"Discovery and Progression form the Adventure column even in wide view");
|
||||
require(button(s,keys[2]).getX()==button(s,keys[3]).getX()&&button(s,keys[2]).getY()<button(s,keys[3]).getY(),"Inhabitant and Battles form the Community column");
|
||||
require(game.getY()>=map.getBottom()&&game.getBottom()<=button(s,"menu.options").getY(),"Game heading is retained above its native actions");
|
||||
for(String key:keys)require(button(s,key).getBottom()<=map.getY(),"Navigation stays above the map: "+key);
|
||||
require(button(s,"menu.options").getY()>=map.getBottom()&&button(s,"options.worldOptions.button").getY()>=map.getBottom(),"Native options stay below the map");
|
||||
var quit=has(s,"menu.returnToMenu")?button(s,"menu.returnToMenu"):button(s,"menu.disconnect");require(quit.getY()>button(s,"menu.options").getY(),"Quit occupies the final central row");
|
||||
require(article.getRight()<map.getX()&¬ice.getX()>map.getRight(),"Gazette is left and Notice board is right of the map");
|
||||
require(map.getWidth()>0&&map.getHeight()>30,"Map retains a usable central viewport");
|
||||
}else require(article.getX()<button(s,keys[0]).getX()&¬ice.getX()>button(s,keys[2]).getRight(),"Locked layout keeps the original side feeds and central menu");
|
||||
widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.visible).forEach(b->require(b.getX()>=0&&b.getRight()<=s.width&&b.getY()>=0&&b.getBottom()<=s.height,"Native control fits: "+b.getMessage().getString()));
|
||||
}
|
||||
private static List<String> chat(net.minecraft.client.Minecraft m){return ((List<net.minecraft.client.multiplayer.chat.GuiMessage>)field(m.gui.hud.getChat(),"allMessages")).stream().map(t->t.content().getString()).toList();}
|
||||
private static Object field(Object o,String name){try{var f=o.getClass().getDeclaredField(name);f.setAccessible(true);return f.get(o);}catch(Exception e){throw new AssertionError(e);}}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
private static AbstractWidget widget(Screen s,String key){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(b->key(b).equals(key)).findFirst().orElseThrow(()->new AssertionError("Missing widget "+key));}
|
||||
private static Button button(Screen s,String key){return (Button)widget(s,key);}
|
||||
private static boolean has(Screen s,String key){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(b->key(b).equals(key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import de.johni0702.minecraft.bobby.Bobby;
|
||||
import de.johni0702.minecraft.bobby.ext.ClientChunkCacheExt;
|
||||
import de.maxhenkel.voicechat.VoicechatClient;
|
||||
import de.maxhenkel.voicechat.gui.VoiceChatScreen;
|
||||
import de.maxhenkel.voicechat.voice.client.ClientManager;
|
||||
import dev.tr7zw.entityculling.EntityCullingModBase;
|
||||
import dev.tr7zw.entityculling.access.Cullable;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Full optional pack on Vulkan, with cache and culling exercised in a fresh lab world. */
|
||||
public final class Optional292ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro = new AtomicBoolean(true);
|
||||
var hidden = new AtomicInteger();
|
||||
c.runOnClient(m -> {
|
||||
check(com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName()
|
||||
.equalsIgnoreCase("vulkan"), "Only Vulkan is validated");
|
||||
for (String id : new String[]{"bobby", "lithium", "entityculling", "voicechat", "sound_physics_remastered"})
|
||||
check(FabricLoader.getInstance().isModLoaded(id), "Missing pack mod " + id);
|
||||
check(VoicechatClient.CLIENT_CONFIG != null, "Voice Chat initialized with Sound Physics");
|
||||
// Test configuration only: no capture of the user's microphone.
|
||||
VoicechatClient.CLIENT_CONFIG.muted.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.disabled.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.onboardingFinished.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.macosCheckMicrophonePermission.set(false);
|
||||
m.options.renderDistance().set(10); m.options.simulationDistance().set(8);
|
||||
m.options.pauseOnLostFocus = false;
|
||||
// Fabric's client runner resets configuration before initialization.
|
||||
// Exercise Bobby's live reload in the development directory instead.
|
||||
try {
|
||||
var path = FabricLoader.getInstance().getConfigDir().resolve("bobby.conf");
|
||||
Files.writeString(path, Files.readString(path)
|
||||
.replaceAll("(?m)^view-distance-overwrite=.*$", "view-distance-overwrite=4")
|
||||
.replaceAll("(?m)^unload-delay-secs=.*$", "unload-delay-secs=0"));
|
||||
} catch (java.io.IOException error) { throw new AssertionError(error); }
|
||||
});
|
||||
c.waitFor(m -> Bobby.getInstance().getConfig().getViewDistanceOverwrite() == 4, 200);
|
||||
if (Boolean.getBoolean("sanctuary.test.optional292WhaleOnly")) {
|
||||
new FlyingWhale290ClientChecks().runTest(c);
|
||||
System.out.println("OPTIONAL292_WHALE_PASS: four optional mods with the translucent Sanctuary renderer");
|
||||
return;
|
||||
}
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Optional292-" + UUID.randomUUID()); s.setSeed("292"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null
|
||||
&& !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.teleportTo(.5, 32, .5); p.setYRot(0); p.setXRot(0);
|
||||
p.getAbilities().flying = true; p.onUpdateAbilities();
|
||||
for (int x = -5; x <= 5; x++) for (int y = 29; y <= 39; y++)
|
||||
p.level().setBlockAndUpdate(new BlockPos(x, y, 6), Blocks.STONE.defaultBlockState());
|
||||
var pig = EntityTypes.PIG.create(p.level(), EntitySpawnReason.COMMAND);
|
||||
pig.setNoAi(true); pig.setNoGravity(true); pig.setPos(.5, 32, 10.5);
|
||||
p.level().addFreshEntity(pig); hidden.set(pig.getId());
|
||||
});
|
||||
c.waitFor(m -> m.level.getEntity(hidden.get()) != null, 400);
|
||||
c.runOnClient(m -> {
|
||||
check(Bobby.getInstance().isEnabled(), "Bobby is active in the limited-view lab");
|
||||
check(((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager() != null,
|
||||
"Bobby attached its cache to the actual client chunk manager");
|
||||
m.player.setYRot(0); m.player.setXRot(0); m.getWindow().setWindowed(1280, 960);
|
||||
check(EntityCullingModBase.instance.cullTask != null, "Culling worker initialized on Vulkan");
|
||||
});
|
||||
c.waitFor(m -> ((Cullable)m.level.getEntity(hidden.get())).isCulled(), 400);
|
||||
c.takeScreenshot("optional292-occluded");
|
||||
server.runOnServer(s -> {
|
||||
for (int x = -5; x <= 5; x++) for (int y = 29; y <= 39; y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x, y, 6), Blocks.AIR.defaultBlockState());
|
||||
});
|
||||
c.waitFor(m -> !((Cullable)m.level.getEntity(hidden.get())).isCulled(), 400);
|
||||
c.takeScreenshot("optional292-visible-again");
|
||||
// Traverse beyond the short server view, then verify persisted chunk-cache data.
|
||||
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().teleportTo(128.5, 32, .5));
|
||||
c.waitTicks(160);
|
||||
c.waitFor(m -> !((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager()
|
||||
.getFakeChunks().isEmpty(), 600);
|
||||
c.runOnClient(m -> {
|
||||
var manager = ((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager();
|
||||
check(manager != null && manager.getStorage() != null, "Bobby has persistent storage after traversal");
|
||||
System.out.println("OPTIONAL292_BOBBY " + manager.getDebugString());
|
||||
});
|
||||
c.waitFor(m -> ClientManager.getClient() != null && ClientManager.getClient().getConnection() != null
|
||||
&& ClientManager.getClient().getConnection().isConnected(), 600);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.gui.setScreen(new VoiceChatScreen());
|
||||
check(m.gui.screen() instanceof VoiceChatScreen, "Voice Chat screen opens with Sound Physics");
|
||||
check(ClientManager.getPlayerStateManager().isMuted(), "Voice chat stays muted during testing");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("optional292-voice-" + locale);
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
}
|
||||
System.out.println("OPTIONAL292_CLIENT_PASS: Vulkan, four mods, Sound Physics, culling/uncover, Bobby cache, UDP connection and FR/EN voice UI");
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
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.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.components.events.*;
|
||||
import net.minecraft.client.gui.screens.PauseScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
|
||||
/** Real admission and property packets, plus both locales at the supported GUI scales on Vulkan. */
|
||||
public final class Property270ClientChecks implements FabricClientGameTest {
|
||||
private static final KeyEvent ENTER=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
Starter039TestSupport.manual=true;var onboarding=new AtomicBoolean(true);
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(onboarding.get()&&m.gui.screen() instanceof HelloWorldScreen s){
|
||||
s.children().stream().filter(EditBox.class::isInstance).map(EditBox.class::cast)
|
||||
.filter(e->key(e).equals("sanctuary.progression.bio")).findFirst().orElseThrow()
|
||||
.setValue("Je parcours les îles de Sanctuary, je bâtis une maison près des arbres et je garde les souvenirs de mes voyages. Chaque nouvelle rencontre nourrit mon histoire et mes projets pour le village.");
|
||||
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);
|
||||
button(s,"sanctuary.progression.enter").onPress(ENTER);onboarding.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Inhabitant271 "+UUID.randomUUID());s.setSeed("271");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);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(128,-40,128);p.setNoGravity(true);s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);
|
||||
p.awardStat(net.minecraft.stats.Stats.PLAY_TIME,72000);p.awardStat(net.minecraft.stats.Stats.BLOCK_MINED.get(net.minecraft.world.level.block.Blocks.STONE),25);
|
||||
fr.koka.sanctuary.knowledge.BlockKnowledgeService.recordPlaced(p,net.minecraft.world.level.block.Blocks.STONE);
|
||||
fr.koka.sanctuary.knowledge.BlockKnowledgeService.recordPlaced(p,net.minecraft.world.level.block.Blocks.COBBLESTONE);
|
||||
var advancement=s.getAdvancements().get(net.minecraft.resources.Identifier.withDefaultNamespace("story/mine_stone"));
|
||||
for(var criterion:advancement.value().criteria().keySet())p.getAdvancements().award(advancement,criterion);
|
||||
});
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.getConnection().sendCommand("sanctuary property");});
|
||||
c.waitFor(m->m.gui.screen() instanceof PropertyScreen s&&s.data()!=null,200);
|
||||
c.runOnClient(m->{var s=(PropertyScreen)m.gui.screen();require(s.data().eligible()&&s.data().claim()==null,"Admitted inhabitant has a free property");});
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitFor(m->!CommunityClient.pending(),200);
|
||||
String publication=UUID.randomUUID().toString();var posted=new AtomicBoolean(false);
|
||||
c.runOnClient(m->require(CommunityClient.request(null,"create",publication,"","notice","info","Mon atelier près du village","Une annonce pour vérifier mon activité personnelle.",0,0,r->{require(!r.has("error"),"Local development publication commits");posted.set(true);}),"Publication request accepted"));
|
||||
c.waitFor(m->posted.get(),200);
|
||||
c.runOnClient(m->m.gui.toastManager().clear());
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)){
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
m.gui.setScreen(new PauseScreen(true));var pause=m.gui.screen();
|
||||
require(widgets(pause).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).equals("sanctuary.cycle.faction")||key(b).equals("sanctuary.story.title")),"Faction and Story are absent from Pause");
|
||||
require(button(pause,"sanctuary.arena.title").active,"Battle is restored in Pause");
|
||||
require(button(pause,"sanctuary.arena.title").getX()==button(pause,"sanctuary.progression.identity").getX()
|
||||
&&button(pause,"sanctuary.arena.title").getY()>button(pause,"sanctuary.progression.identity").getY(),"Battle belongs to Community below Inhabitant");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-pause-"+locale+"-gui"+scale);
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.arena.title").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof ArenaScreen&&ArenaClient.view!=null,200);
|
||||
c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof PauseScreen,"Battle returns to Pause");});
|
||||
}
|
||||
c.runOnClient(m->{var pause=m.gui.screen();
|
||||
ProgressionClient.open(pause,"identity");var identity=m.gui.screen();
|
||||
identityLayout(identity);
|
||||
});
|
||||
c.waitFor(m->InhabitantOverviewClient.data()!=null&&InhabitantOverviewClient.data().events().stream().anyMatch(e->e.id().equals("post:"+publication)),300);
|
||||
c.runOnClient(m->{var d=InhabitantOverviewClient.data();require(d.playTicks()>=72000&&d.placed()==2&&d.mined()==25&&d.advancements()>0,"Network overview contains real personal counters");identityLayout(m.gui.screen());});
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-identity-"+locale+"-gui"+scale);
|
||||
if(scale==4)c.runOnClient(m->{
|
||||
var screen=(ProgressionScreen)m.gui.screen();var stats=area(screen,"sanctuary.inhabitant.overview.deaths");
|
||||
stats.setScrollAmount(10000);double offset=stats.scrollAmount();require(offset>0,"Compact statistics have a native independent scroll");
|
||||
screen.refreshIdentity();require(area(screen,"sanctuary.inhabitant.overview.deaths").scrollAmount()==offset,"Live refresh preserves the statistics reading position");
|
||||
area(screen,"sanctuary.inhabitant.overview.deaths").setScrollAmount(0);
|
||||
});
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.cycle.faction").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof CycleFactionScreen&&CycleClient.view!=null,200);
|
||||
c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof ProgressionScreen,"Faction returns to Habitant");button(m.gui.screen(),"sanctuary.cycle.log").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof InhabitantActivityScreen,200);
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-activity-history");
|
||||
c.runOnClient(m->{require(widgets(m.gui.screen()).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(w->w.getMessage().getString().contains("Mon atelier")),"Own publication appears in the complete history");button(m.gui.screen(),"sanctuary.inhabitant.overview.cycles").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof CycleFactionScreen&&CycleClient.view!=null,200);
|
||||
c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof InhabitantActivityScreen,"Cycle archive returns to activity history");m.gui.screen().onClose();require(m.gui.screen() instanceof ProgressionScreen,"Activity history returns to Habitant");
|
||||
button(m.gui.screen(),"sanctuary.inhabitant.overview.stats").onPress(ENTER);require(m.gui.screen() instanceof DiscoveryStatsScreen,"Complete statistics are accessible");m.gui.screen().onClose();});
|
||||
}
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.familiar.title").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof FamiliarScreen&&CompanionClient.record!=null,200);
|
||||
c.runOnClient(m->{var screen=m.gui.screen();String consent="sanctuary.companion."+(CompanionClient.record.acceptAllies()?"consent_on":"consent_off");require(button(screen,consent).active,"Familiar consent setting moved into its dedicated page");
|
||||
require(widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).noneMatch(b->key(b).equals("sanctuary.familiar.title")),"No recursive Familiar button inside its own page");
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-familiar-"+locale+"-gui"+scale);
|
||||
if(locale.equals("fr_fr")&&scale==2){
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.companion.consent_on").onPress(ENTER));
|
||||
c.waitFor(m->CompanionClient.record!=null&&!CompanionClient.record.acceptAllies()&&hasButton(m.gui.screen(),"sanctuary.companion.consent_off"),200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(!fr.koka.sanctuary.companion.CompanionService.record(p).acceptAllies(),"Moved consent control changes the actual server state");});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.companion.consent_off").onPress(ENTER));
|
||||
c.waitFor(m->CompanionClient.record!=null&&CompanionClient.record.acceptAllies()&&hasButton(m.gui.screen(),"sanctuary.companion.consent_on"),200);
|
||||
}
|
||||
c.runOnClient(m->{m.gui.screen().onClose();require(m.gui.screen() instanceof ProgressionScreen,"Familiar returns to Habitant");identityLayout(m.gui.screen());});
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.property.title").onPress(ENTER));
|
||||
c.waitFor(m->m.gui.screen() instanceof PropertyScreen s&&s.data()!=null,200);
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-property-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->{var s=(PropertyScreen)m.gui.screen();button(s,"sanctuary.property.claim").onPress(ENTER);require(button(s,"sanctuary.property.confirm").active,"Explicit claim confirmation");
|
||||
widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).forEach(a->a.setScrollAmount(10000));
|
||||
});
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-confirm-"+locale+"-gui"+scale);
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.property.cancel").onPress(ENTER));
|
||||
}
|
||||
c.runOnClient(m->{var s=m.gui.screen();button(s,"sanctuary.property.claim").onPress(ENTER);button(s,"sanctuary.property.confirm").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof PropertyScreen s&&s.data()!=null&&s.data().claim()!=null,200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(PropertyService.store(s).get(p.getUUID()).tier()==0,"Real network claim persists");fr.koka.sanctuary.familiar.FamiliarExperience.setPlayerXp(p,150);});
|
||||
c.runOnClient(m->{m.gui.setScreen(null);m.getConnection().sendCommand("sanctuary propriété");});
|
||||
c.waitFor(m->m.gui.screen() instanceof PropertyScreen s&&s.data()!=null&&s.data().claim()!=null,200);
|
||||
c.runOnClient(m->{button(m.gui.screen(),"sanctuary.property.expand").onPress(ENTER);button(m.gui.screen(),"sanctuary.property.confirm").onPress(ENTER);});
|
||||
c.waitFor(m->m.gui.screen() instanceof PropertyScreen s&&s.data()!=null&&s.data().claim().tier()==1,200);
|
||||
world.getServer().runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();require(fr.koka.sanctuary.familiar.FamiliarExperience.playerXp(p)==50,"Real packet purchase debits exactly 100 points");});
|
||||
c.waitTicks(3);c.takeScreenshot("inhabitant271-expanded");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{onboarding.set(false);Starter039TestSupport.manual=false;}
|
||||
System.out.println("INHABITANT271_CLIENT_PASS: Vulkan, FR/EN GUI 2/3/4, top navigation, two columns and two rows, airy character on the left, full-height activity below, biography and statistics on the right, History at activity footer, familiar hidden, consent round trip, Battle in Community, Story hidden, property regression");
|
||||
}
|
||||
private static void identityLayout(Screen s){
|
||||
var skin=widgets(s).filter(PlayerSkinWidget.class::isInstance).map(PlayerSkinWidget.class::cast).findFirst().orElseThrow();
|
||||
int contentWidth=Math.min(720,s.width-24),contentX=(s.width-contentWidth)/2,columnWidth=(contentWidth-12)/2;
|
||||
require(Math.abs(skin.getX()+skin.getWidth()/2-(contentX+columnWidth/2))<=1,"Player is centred in the left column");
|
||||
require(skin.getWidth()<=columnWidth-24,"Character retains breathing room on both sides");
|
||||
require(widgets(s).noneMatch(InhabitantCompanionWidget.class::isInstance),"No familiar portrait in Habitant");
|
||||
require(!hasButton(s,"sanctuary.arena.title")&&!hasButton(s,"sanctuary.companion.consent_on")&&!hasButton(s,"sanctuary.companion.consent_off"),"Only personal navigation remains in Habitant");
|
||||
int x=-1,navY=button(s,"sanctuary.property.title").getY();
|
||||
for(String key:List.of("sanctuary.property.title","sanctuary.cycle.faction","sanctuary.familiar.title")){
|
||||
var b=button(s,key);require(b.active&&b.getY()==navY&&b.getBottom()<skin.getY()&&b.getX()>x,"Three ordered links share the top bar: "+key);
|
||||
require(b.getX()>=0&&b.getRight()<=s.width,"Identity navigation fits horizontally");x=b.getRight();
|
||||
}
|
||||
var history=button(s,"sanctuary.cycle.log");require(history.getX()<skin.getX()&&history.getY()>skin.getBottom()&&history.getBottom()<s.height-33,"History is below the left activity feed and above the footer");
|
||||
require(history.getBottom()==s.height-41,"Activity footer extends to the bottom content boundary");
|
||||
var stats=button(s,"sanctuary.inhabitant.overview.stats");require(stats.getX()>skin.getRight()&&stats.getY()>skin.getBottom(),"Statistics occupy the lower right cell");
|
||||
widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getClass().getSimpleName().equals("Activity"))
|
||||
.forEach(w->require(w.getY()>skin.getBottom()&&w.getRight()<stats.getX(),"Activity occupies the lower left cell"));
|
||||
var biography=widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->key(w).equals("sanctuary.inhabitant.bio")).findFirst().orElseThrow();
|
||||
require(biography.getX()>skin.getRight(),"Biography is on the right of the character");
|
||||
}
|
||||
private static boolean hasButton(Screen s,String key){return s!=null&&widgets(s).filter(Button.class::isInstance).map(Button.class::cast).anyMatch(b->key(b).equals(key));}
|
||||
private static AbstractScrollArea area(Screen s,String key){return widgets(s).filter(w->w instanceof AbstractScrollArea&&w instanceof ContainerEventHandler)
|
||||
.filter(w->widgets((ContainerEventHandler)w).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).anyMatch(c->key(c).equals(key))).map(AbstractScrollArea.class::cast).findFirst().orElseThrow();}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static java.util.stream.Stream<GuiEventListener> widgets(ContainerEventHandler c){return c.children().stream().flatMap(w->w instanceof ContainerEventHandler nested?java.util.stream.Stream.concat(java.util.stream.Stream.of(w),widgets(nested)):java.util.stream.Stream.of(w));}
|
||||
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("Missing button "+key));}
|
||||
private static void require(boolean value,String message){if(!value)throw new AssertionError(message);}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.SmokerEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Smoker285ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<SmokerEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var familyFlying=new AtomicBoolean();var familyCaught=new AtomicBoolean();var wildFlying=new AtomicBoolean();var wildCaught=new AtomicBoolean();
|
||||
var familyWire=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.infected.SmokerLassoEntity>();
|
||||
var familyAnchor=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.phys.Vec3>();
|
||||
var wildWire=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.infected.SmokerLassoEntity>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{
|
||||
if(s.getPlayerList().getPlayers().isEmpty())return;var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
var f=pet==null?null:fr.koka.sanctuary.infected.SmokerLassoEntity.active(pet);familyWire.set(f);
|
||||
if(f!=null&&familyAnchor.get()==null)familyAnchor.set(pet.position());
|
||||
familyFlying.set(f!=null&&!f.isReeling());if(f!=null&&f.isReeling())familyCaught.set(true);
|
||||
var mob=wild.get();var w=mob==null||mob.isRemoved()?null:fr.koka.sanctuary.infected.SmokerLassoEntity.active(mob);wildWire.set(w);
|
||||
wildFlying.set(w!=null&&!w.isReeling());if(w!=null&&w.isReeling()&&w.caughtEntity()==p)wildCaught.set(true);
|
||||
});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Smoker285-"+UUID.randomUUID());s.setSeed("285");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=22;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)for(int z:new int[]{3,7,11,15})p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.SMOKER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.SMOKER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:smoker"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.SMOKER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/smoker.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.SMOKER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
wild.get().teleportTo(3.5,1,4.5);var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,10.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);
|
||||
c.waitFor(m->familyFlying.get(),250);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(enemy.get().getHealth()==100,"Releasing G does not hit before physical contact");check(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"G commits cooldown when the lasso is launched");});
|
||||
c.runOnClient(m->{var hook=m.level.getEntitiesOfClass(fr.koka.sanctuary.infected.SmokerLassoEntity.class,m.player.getBoundingBox().inflate(24)).stream().findFirst().orElseThrow();
|
||||
var renderer=(SmokerLassoRenderer)m.getEntityRenderDispatcher().getRenderer(hook);var state=renderer.createRenderState(hook,1);
|
||||
check(state.leashStates!=null&&!state.leashStates.isEmpty(),"The real client draws the native taut zipline cable");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("smoker285-familiar-lasso-flight");
|
||||
c.waitFor(m->familyCaught.get(),150);c.waitTicks(1);c.takeScreenshot("smoker285-familiar-pull");c.waitTicks(22);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(enemy.get().getHealth()<100&&enemy.get().position().distanceTo(familyAnchor.get())<2.3,"A real G impact drags the actual enemy near the miniature: hp="+enemy.get().getHealth()+" target="+enemy.get().position()+" pet="+pet.position());
|
||||
check(pet.isAlive()&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"The familiar survives and retains spent cooldown");
|
||||
FamiliarBattle.command(p,"follow");wild.get().teleportTo(.5,1,4.5);enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("smoker285-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.SMOKER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.SMOKER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Petit lasso":"Short lasso";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("smoker285-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
p.getAbilities().invulnerable=false;p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
var mob=wild.get();mob.teleportTo(.5,1,14.5);mob.setPermanentlyInvulnerable(false);mob.setTarget(p);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->wildFlying.get(),250);
|
||||
server.runOnServer(s->wild.get().setNoAi(true));
|
||||
c.getInput().holdKey(o->o.keyRight);c.waitTicks(12);c.getInput().releaseKey(o->o.keyRight);c.waitTicks(20);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(!wildCaught.get()&&Math.abs(p.getX()-.5)>.65&&p.getZ()<2&&fr.koka.sanctuary.infected.SmokerLassoEntity.active(wild.get())==null,"A real sideways keyboard input dodges the flying lasso without a guaranteed grab");
|
||||
check(wild.get().lassoCooldown()>0,"A missed wild shot still pays its cooldown");
|
||||
wild.get().discard();var mob=Infected.SMOKER.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,14.5);mob.setOnGround(true);wild.set(mob);p.level().addFreshEntity(mob);
|
||||
p.teleportTo(.5,1,.5);p.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);p.setYRot(0);p.setXRot(0);mob.setTarget(p);wildCaught.set(false);wildFlying.set(false);
|
||||
});c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->wildFlying.get(),250);server.runOnServer(s->wild.get().setNoAi(true));
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("smoker285-wild-lasso-flight");
|
||||
c.waitFor(m->wildCaught.get(),150);c.waitTicks(1);c.takeScreenshot("smoker285-player-captured");
|
||||
double capturedZ=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getZ());
|
||||
c.getInput().holdKey(o->o.keyDown);c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getZ()>capturedZ+.5,"A successful hostile hit reels the player even against backward/sneak input");});
|
||||
c.getInput().releaseKey(o->o.keyDown);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitFor(m->wildWire.get()==null,200);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.distanceTo(wild.get())<2.2&&!fr.koka.sanctuary.infected.SmokerLassoEntity.pulled(p)&&!p.isNoGravity(),"The real player arrives near the Smoker, releases the cable and keeps normal gravity");});
|
||||
c.runOnClient(m->check(m.player.getZ()>11,"The rendered player travels with the authoritative zipline"));
|
||||
c.takeScreenshot("smoker285-player-arrival");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
wild.get().discard();wild.set(null);enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.SmokerLassoEntity.class,p.getBoundingBox().inflate(32)).forEach(Entity::discard);
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(32)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("SMOKER285_CLIENT_PASS: Vulkan, native cable renderer, real G physical hit and pull, real keyboard dodge, hostile player zipline against input and arrival, three adult miniature sizes, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.SpitterEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
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.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Spitter283ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<SpitterEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var preparing=new AtomicBoolean();
|
||||
var cloud=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.infected.AcidZoneEntity>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var mob=wild.get();if(mob!=null&&!mob.isRemoved()){
|
||||
preparing.set(mob.windingUp());cloud.set(fr.koka.sanctuary.infected.AcidZoneEntity.active(mob));
|
||||
|
||||
}});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Spitter283-"+UUID.randomUUID());s.setSeed("283");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
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());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.SPITTER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.SPITTER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:spitter"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.SPITTER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/spitter.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.SPITTER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,6.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);c.waitTicks(55);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var zone=fr.koka.sanctuary.infected.AcidZoneEntity.active(FamiliarService.active(p));
|
||||
check(zone!=null&&!zone.isRemoved()&&zone instanceof AreaEffectCloud,"G creates a real native potion-style cloud");
|
||||
check(enemy.get().getHealth()<100&&enemy.get().getActiveEffects().isEmpty(),"Only the grounded zone damages without poison or slowness");
|
||||
check(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"G commits cooldown when the projectile is released");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("spitter283-acid-zone");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
var healthAfterExit=new java.util.concurrent.atomic.AtomicReference<Float>();
|
||||
server.runOnServer(s->{healthAfterExit.set(enemy.get().getHealth());enemy.get().teleportTo(-3.5,1,6.5);});c.waitTicks(25);
|
||||
server.runOnServer(s->{check(enemy.get().getHealth()==healthAfterExit.get(),"Leaving the field stops subsequent damage immediately");
|
||||
enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("spitter283-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.SPITTER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.SPITTER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Petite flaque acide":"Small acid pool";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("spitter283-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
var cow=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.animal.cow.Cow>();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
var target=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(.5,1,10.5);p.level().addFreshEntity(target);cow.set(target);
|
||||
var mob=wild.get();mob.setPermanentlyInvulnerable(false);mob.setTarget(target);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.waitFor(m->preparing.get(),200);c.waitTicks(2);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("spitter283-windup");
|
||||
c.waitFor(m->cloud.get()!=null&&!cloud.get().isRemoved(),100);c.waitTicks(25);
|
||||
server.runOnServer(s->check(cow.get().getHealth()<100&&cow.get().getActiveEffects().isEmpty(),"Wild projectile creates a damaging zone without status effects"));
|
||||
c.takeScreenshot("spitter283-wild-zone");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
var zone=fr.koka.sanctuary.infected.AcidZoneEntity.active(wild.get());if(zone!=null)zone.discard();
|
||||
wild.get().discard();wild.set(null);cow.get().discard();enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.AcidSpitEntity.class,p.getBoundingBox().inflate(24)).forEach(Entity::discard);
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(20)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("SPITTER283_CLIENT_PASS: Vulkan, adult skin, three miniature sizes, real G projectile/cloud/damage/exit/cooldown, wild windup/cloud, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real Vulkan models, stable wardrobes, native baby UVs, native forward arms and Hunter locomotion. */
|
||||
public final class Zombie287ClientChecks implements FabricClientGameTest {
|
||||
private static final List<EntityType<? extends Zombie>> TYPES=List.of(EntityTypes.ZOMBIE,Infected.TYPE,Infected.HUNTER,Infected.BOOMER,Infected.CHARGER,Infected.SPITTER,Infected.SMOKER);
|
||||
private static final List<Item> EGGS=List.of(Items.ZOMBIE_SPAWN_EGG,Infected.SPAWN_EGG,Infected.HUNTER_EGG,Infected.BOOMER_EGG,Infected.CHARGER_EGG,Infected.SPITTER_EGG,Infected.SMOKER_EGG);
|
||||
private static final List<String> NAMES=List.of("Zombie","Infecté","Hunter","Boomer","Charger","Spitter","Smoker");
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
private static ItemStack sized(ItemStack egg,int size){
|
||||
var result=egg.copy();var tag=new net.minecraft.nbt.CompoundTag();tag.putInt("schema",1);tag.putInt("per_mille",size);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,result,t->t.put(FamiliarSize.KEY,tag));return result;
|
||||
}
|
||||
private static InfectedRenderer.State state(net.minecraft.client.Minecraft m,Zombie mob){
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Ordinary zombies and every infected share the new renderer");
|
||||
return ((InfectedRenderer)m.getEntityRenderDispatcher().getRenderer(mob)).createRenderState(mob,1);
|
||||
}
|
||||
private static void posture(InfectedRenderer.State s,boolean hunter) {
|
||||
var model=new net.minecraft.client.model.monster.zombie.ZombieModel<InfectedRenderer.State>(net.minecraft.client.Minecraft.getInstance().getEntityModels().bakeLayer(net.minecraft.client.model.geom.ModelLayers.ZOMBIE));
|
||||
s.walkAnimationSpeed=.6F;s.walkAnimationPos=2;s.swingAnimation=0;model.setupAnim(s);
|
||||
check(model.rightArm.xRot< -.5&&model.leftArm.xRot< -.5,"Every zombie keeps both native arms forward");
|
||||
if(hunter)check(s.isCrouching&&model.body.xRot>.4,"Hunter keeps its crouched walking skeleton");
|
||||
else check(!s.isCrouching&&Math.abs(model.body.xRot)<.01,"Other roles retain their native standing posture");
|
||||
float leg=model.rightLeg.xRot;s.walkAnimationPos=5;model.setupAnim(s);
|
||||
check(Math.abs(model.rightLeg.xRot-leg)>.1,"Role silhouettes retain native leg locomotion");
|
||||
}
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var staged=new ArrayList<Zombie>();var intro=new AtomicBoolean(true);var hunterId=new AtomicInteger(-1);
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Zombies287-"+UUID.randomUUID());s.setSeed("287");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->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
for(String command:List.of("sanctuary time vanilla","time set midnight","sanctuary weather vanilla","weather clear"))s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),command);
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);p.getAbilities().invulnerable=true;p.teleportTo(.5,1,-2.5);p.getInventory().clearContent();
|
||||
for(int x=-26;x<=26;x++)for(int z=-16;z<=26;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-14;x<=14;x++)for(int z:new int[]{6,10,14})p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
for(int n=0;n<9;n++){var mob=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);mob.setUUID(new UUID(0,n));mob.setNoAi(true);mob.setPermanentlyInvulnerable(true);mob.setPos((n-4)*2+.5,1,8.5);mob.setYRot(180);mob.yHeadRot=180;mob.yBodyRot=180;p.level().addFreshEntity(mob);staged.add(mob);}
|
||||
});c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.player.setYRot(0);m.player.setXRot(0);});c.waitTicks(20);
|
||||
c.runOnClient(m->{var found=new HashSet<Integer>();
|
||||
for(var type:TYPES)for(int i=0;i<9;i++){
|
||||
var mob=type.create(m.level,EntitySpawnReason.COMMAND);mob.setId(100000+i);mob.setUUID(new UUID(0,i));mob.setOnGround(true);var a=state(m,mob);
|
||||
check(a.skin==i,"Every role can wear all nine supplied skins");found.add(a.skin);var renderer=(InfectedRenderer)m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
check(renderer.getTextureLocation(a).equals(ZombieSkins.texture(i,false))&&m.getResourceManager().getResource(renderer.getTextureLocation(a)).isPresent(),"The selected adult texture is available");
|
||||
mob.tickCount+=200;mob.setPos(5,2,8);check(state(m,mob).skin==i,"Time/movement do not reroll clothing");
|
||||
var clone=type.create(m.level,EntitySpawnReason.LOAD);clone.setId(100100+i);clone.setUUID(mob.getUUID());check(state(m,clone).skin==i,"Reloaded identity retains its clothing");
|
||||
}
|
||||
check(found.size()==9,"All nine variants are reachable");
|
||||
});c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("zombie287-nine-skins");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();staged.forEach(Entity::discard);staged.clear();
|
||||
for(int i=0;i<TYPES.size();i++){var mob=TYPES.get(i).create(p.level(),EntitySpawnReason.COMMAND);mob.setUUID(new UUID(10,i));mob.setNoAi(true);mob.setPermanentlyInvulnerable(true);mob.setPos((i-3)*2.4+.5,1,8.5);mob.setYRot(180);mob.yHeadRot=180;mob.yBodyRot=180;mob.setCustomName(Component.literal(NAMES.get(i)));mob.setCustomNameVisible(true);p.level().addFreshEntity(mob);staged.add(mob);}
|
||||
var baby=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);baby.setBaby(true);baby.setNoAi(true);baby.setPos(9.5,1,8.5);baby.setYRot(180);baby.yHeadRot=180;p.level().addFreshEntity(baby);staged.add(baby);
|
||||
});c.waitTicks(18);
|
||||
c.runOnClient(m->{for(int i=0;i<TYPES.size();i++){final int at=i;var mob=m.level.getEntitiesOfClass(Zombie.class,m.player.getBoundingBox().inflate(30)).stream().filter(e->e.getUUID().equals(new UUID(10,at))).findFirst().orElseThrow();var a=state(m,mob);posture(a,i==2);}
|
||||
var baby=m.level.getEntitiesOfClass(Zombie.class,m.player.getBoundingBox().inflate(30)).stream().filter(Zombie::isBaby).findFirst().orElseThrow();var a=state(m,baby);check(a.isBaby&&m.getResourceManager().getResource(ZombieSkins.texture(a.skin,true)).isPresent(),"Ordinary baby has its native model and adapted UV texture");
|
||||
});c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("zombie287-role-postures");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{staged.forEach(Entity::discard);staged.clear();s.getPlayerList().getPlayers().getFirst().teleportTo(.5,1,-6.5);});
|
||||
// No nearby monsters during wardrobe checks: changing species must not enter combat.
|
||||
for(int i=0;i<EGGS.size();i++)for(int size:new int[]{500,1000,1500}){
|
||||
final int at=i;server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var current=FamiliarBattle.egg(p);var egg=!current.isEmpty()&¤t.is(EGGS.get(at))?current:new ItemStack(EGGS.get(at));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,sized(egg,size));FamiliarService.tick(p);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().anyMatch(e->e.egg().is(EGGS.get(at))&&FamiliarSize.value(e.egg())==size),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);check(pet!=null,"Each egg equips a familiar");pet.setNoAi(true);pet.teleportTo(.5,1,-2.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;});c.waitTicks(4);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().filter(e->e.egg().is(EGGS.get(at))).findFirst().orElseThrow();var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);var render=renderer.createRenderState(pet,1);var a=(InfectedRenderer.State)render.appearance;
|
||||
check(a.isBaby==(at==0),"Infected familiars keep adult geometry at every scale");check(a.skin==ZombieSkins.index(FamiliarIdentity.get(pet.egg())),"Familiar clothing uses its persistent bond identity");if(at>0)posture(a,at==2);
|
||||
});
|
||||
if(size==1000){c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(1);c.takeScreenshot("zombie287-familiar-"+NAMES.get(at));c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);}
|
||||
}
|
||||
var remembered=new AtomicReference<UUID>();var savedEgg=new AtomicReference<ItemStack>();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();savedEgg.set(FamiliarBattle.egg(p).copy());remembered.set(FamiliarIdentity.get(savedEgg.get()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);});c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,savedEgg.get());FamiliarService.tick(p);});c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().anyMatch(e->remembered.get().equals(FamiliarIdentity.get(e.egg()))),200);
|
||||
server.runOnServer(s->FamiliarService.active(s.getPlayerList().getPlayers().getFirst()).setNoAi(true));c.waitTicks(5);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().findFirst().orElseThrow();var a=(InfectedRenderer.State)((FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet)).createRenderState(pet,1).appearance;check(a.skin==ZombieSkins.index(remembered.get()),"Re-equipping the same individual retains clothing");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.teleportTo(.5,1,-6.5);p.getAbilities().invulnerable=false;
|
||||
var mob=Infected.HUNTER.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,10.5);mob.setOnGround(true);p.level().addFreshEntity(mob);mob.setTarget(p);hunterId.set(mob.getId());staged.add(mob);
|
||||
});c.waitFor(m->{var e=m.level.getEntity(hunterId.get());return e instanceof HunterEntity h&&state(m,h).isCrouching&&state(m,h).walkAnimationSpeed>.05;},250);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("zombie287-hunter-walking-sneak");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
c.waitFor(m->m.level.getEntity(hunterId.get()) instanceof HunterEntity h&&h.getY()>1.5&&!state(m,h).isCrouching,300);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("zombie287-hunter-jumping");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{staged.forEach(Entity::discard);staged.clear();});c.waitTicks(20);
|
||||
System.out.println("ZOMBIE287_CLIENT_PASS: Vulkan, nine shared stable skins, normal zombie/native baby UV, native forward arms for all seven types, all familiar roles at three sizes, persistent re-equip wardrobe, real walking Hunter crouch and airborne standing jump");
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
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.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
|
||||
/** GPU brightness on real terrain, independent of the PBR response or held-item pixels. */
|
||||
public final class HandLight283ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{
|
||||
check(com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName().equalsIgnoreCase("vulkan"),"Vulkan required");
|
||||
m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;
|
||||
m.options.bobView().set(false);m.options.setCameraType(CameraType.FIRST_PERSON);
|
||||
m.getWindow().setWindowed(1280,800);VanillaLight.setEnabled(false);
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
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("HandLight283-"+UUID.randomUUID());s.setSeed("283");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
command(s,"sanctuary time vanilla");command(s,"sanctuary weather vanilla");command(s,"time set midnight");command(s,"weather clear");
|
||||
s.getGlobalGameRules().set(GameRules.ADVANCE_TIME,false,s);s.getGlobalGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();
|
||||
for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)for(int y=99;y<=107;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(y==99||y==107||Math.abs(x)==10||Math.abs(z)==10?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(.5,100,.5);
|
||||
});
|
||||
c.waitFor(m->m.player.getY()>99,200);look(c,server,0,35);c.waitTicks(60);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);settle(c);
|
||||
var dark=capture(c,"hand283-dark");double darkMean=mean(dark);
|
||||
equip(c,server,EquipmentSlot.MAINHAND,Items.TORCH);var torch=capture(c,"hand283-main-torch");
|
||||
double torchMean=mean(torch);System.out.println("HAND283_BRIGHTNESS dark="+darkMean+" torch="+torchMean);
|
||||
check(torchMean>darkMean+12,"Held torch must brighten terrain with PBR and shader OFF: "+darkMean+" -> "+torchMean);
|
||||
Runnable pinned=FabricLoader.getInstance().isModLoaded("sodium")?SodiumHandLight283Checks.capture(c):()->{};
|
||||
for(boolean ao:new boolean[]{false,true}) {
|
||||
c.runOnClient(m->{m.options.ambientOcclusion().set(ao);m.levelExtractor.allChanged();});settle(c);
|
||||
check(mean(capture(c,"hand283-ao-"+ao))>darkMean+12,"Both flat and smooth terrain receive hand light");
|
||||
}
|
||||
equip(c,server,EquipmentSlot.MAINHAND,null);check(Math.abs(mean(capture(c,null))-darkMean)<2,"Removing torch restores darkness");
|
||||
pinned.run();
|
||||
equip(c,server,EquipmentSlot.OFFHAND,Items.LANTERN);check(mean(capture(c,"hand283-offhand-lantern"))>darkMean+12,"Offhand lantern brightens terrain");
|
||||
c.runOnClient(m->{VanillaLight.setProfile(ShaderProfile.MEDIUM);VanillaLight.setEnabled(true);VanillaLight.setPbr(false);});settle(c);
|
||||
check(mean(capture(c,"hand283-medium-pbr-off"))>darkMean+12,"Medium shader keeps actual light without PBR");
|
||||
c.runOnClient(m->VanillaLight.setPbr(true));settle(c);check(mean(capture(c,"hand283-medium-pbr-on"))>darkMean+12,"PBR keeps terrain illumination");
|
||||
c.runOnClient(m->VanillaLight.setEnabled(false));equip(c,server,EquipmentSlot.OFFHAND,null);
|
||||
check(Math.abs(mean(capture(c,"hand283-removed"))-darkMean)<2,"No stale light remains");
|
||||
equip(c,server,EquipmentSlot.MAINHAND,Items.TORCH);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(8.5,100,.5));c.waitTicks(12);settle(c);
|
||||
c.runOnClient(m->{var lights=DynamicLightClient.snapshot(m.level);
|
||||
check((lights.apply(0,8.5,101.5,.5)&255)>200,"Source follows the moving holder");
|
||||
check((lights.apply(0,.5,101.5,.5)&255)<120,"Old position no longer has full brightness");});
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(.5,100,.5));
|
||||
equip(c,server,EquipmentSlot.MAINHAND,null);
|
||||
server.runOnServer(s->{var drop=new net.minecraft.world.entity.item.ItemEntity(s.overworld(),.5,100,2.5,new ItemStack(Items.TORCH));
|
||||
drop.setNoGravity(true);drop.setPickUpDelay(32767);s.overworld().addFreshEntity(drop);});c.waitTicks(12);settle(c);
|
||||
check(mean(capture(c,"hand283-dropped"))>darkMean+12,"Dropped sources use the same terrain bridge");
|
||||
server.runOnServer(s->{var drops=new java.util.ArrayList<net.minecraft.world.entity.Entity>();s.overworld().getAllEntities().forEach(e->{if(e instanceof net.minecraft.world.entity.item.ItemEntity)drops.add(e);});drops.forEach(net.minecraft.world.entity.Entity::discard);});
|
||||
c.waitTicks(12);settle(c);check(Math.abs(mean(capture(c,null))-darkMean)<2,"Removing dropped source restores darkness");
|
||||
c.runOnClient(m->{check(m.level.getBrightness(LightLayer.BLOCK,new BlockPos(0,101,2))==0,"World light engine remains untouched");
|
||||
check(DynamicLightClient.snapshot(m.level).size()==0,"Sources are cleared");});
|
||||
System.out.println("HAND283_PASS sodium="+FabricLoader.getInstance().isModLoaded("sodium")+" dark="+darkMean+" torch="+torchMean+" Vulkan hands, AO, shader, PBR, movement, drop, cleanup");
|
||||
} finally {intro.set(false);}
|
||||
c.runOnClient(m->check(DynamicLightClient.pendingSections()==0,"Disconnect clears pending sections"));
|
||||
}
|
||||
private static void equip(ClientGameTestContext c,TestServerContext server,EquipmentSlot slot,Item item) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(slot,item==null?ItemStack.EMPTY:new ItemStack(item));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(12);settle(c);
|
||||
}
|
||||
private static net.minecraft.client.input.KeyEvent enter() {return new net.minecraft.client.input.KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void settle(ClientGameTestContext c) {
|
||||
c.waitFor(m->DynamicLightClient.pendingSections()==0&&m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(25);
|
||||
}
|
||||
private static double mean(Frame frame) {
|
||||
double sum=0;int count=0;
|
||||
// Centre-left floor, away from GUI, arm, item and room edges.
|
||||
for(int y=frame.height()/3;y<frame.height()*2/3;y++)for(int x=frame.width()/4;x<frame.width()/2;x++) {
|
||||
int pixel=frame.pixels()[y*frame.width()+x];sum+=.2126*(pixel>>16&255)+.7152*(pixel>>8&255)+.0722*(pixel&255);count++;
|
||||
}
|
||||
return sum/count;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.check;
|
||||
import fr.koka.sanctuary.client.DynamicLightRegion;
|
||||
import fr.koka.sanctuary.lighting.DynamicLightField;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.caffeinemc.mods.sodium.client.world.LevelSlice;
|
||||
import net.caffeinemc.mods.sodium.client.world.cloned.ClonedChunkSectionCache;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
|
||||
/** Loaded only with the optional exact pack dependency. Exercise late/reused worker jobs. */
|
||||
final class SodiumHandLight283Checks {
|
||||
private static final BlockPos PROBE=new BlockPos(0,101,2);
|
||||
static Runnable capture(ClientGameTestContext c) {
|
||||
var context=c.computeOnClient(m->LevelSlice.prepare(m.level,SectionPos.of(PROBE),new ClonedChunkSectionCache(m.level)));
|
||||
var snapshot=((DynamicLightRegion)context).sanctuary$lightSnapshot();
|
||||
check(snapshot.size()==1,"Sodium job captures the hand source");
|
||||
var slice=c.computeOnClient(m->new LevelSlice(m.level));
|
||||
return ()->{
|
||||
var fresh=c.computeOnClient(m->LevelSlice.prepare(m.level,SectionPos.of(PROBE),new ClonedChunkSectionCache(m.level)));
|
||||
var future=CompletableFuture.runAsync(()->{
|
||||
slice.copyData(context);
|
||||
check(((DynamicLightRegion)(Object)slice).sanctuary$lightSnapshot()==snapshot,"Late worker keeps its submitted immutable snapshot");
|
||||
check(slice.getBrightness(LightLayer.BLOCK,PROBE)>10,"Delayed job stays lit after live source removal");
|
||||
check(slice.getBrightness(LightLayer.SKY,PROBE)==0,"Sodium sky light remains unchanged");
|
||||
slice.copyData(fresh);
|
||||
check(slice.getBrightness(LightLayer.BLOCK,PROBE)==0,"Reused worker replaces old source data");
|
||||
slice.reset();
|
||||
check(((DynamicLightRegion)(Object)slice).sanctuary$lightSnapshot()==DynamicLightField.Snapshot.EMPTY,"Reset releases the snapshot");
|
||||
});
|
||||
c.waitFor(m->future.isDone(),200);future.join();
|
||||
System.out.println("HAND283_SODIUM_SNAPSHOT_PASS late worker, replacement, sky, reset");
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -48,9 +48,9 @@ public final class Progression003ClientChecks implements FabricClientGameTest {
|
||||
context.runOnClient(client -> {
|
||||
var colorButtons=client.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).toList();
|
||||
require(colorButtons.size()==20 && colorButtons.stream().allMatch(b -> b.getMessage().getContents()
|
||||
require(colorButtons.size()==9 && colorButtons.stream().allMatch(b -> b.getMessage().getContents()
|
||||
instanceof net.minecraft.network.chat.contents.TranslatableContents t && t.getKey().startsWith("sanctuary.color.")),
|
||||
"All twenty native colour buttons display translated hue names");
|
||||
"A hotbar page of nine native colour swatches exposes translated hue names");
|
||||
});
|
||||
context.takeScreenshot("sanctuary-beta005-couleurs-fr");
|
||||
Blocodex004ClientChecks.language(context,initialLanguage);
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.client.HelloWorldScreen;
|
||||
import fr.koka.sanctuary.providence.*;
|
||||
import fr.koka.sanctuary.client.ProgressionClient;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
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.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.ChatScreen;
|
||||
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;
|
||||
|
||||
/** Native Vulkan chat rendering plus a prayer sent over the real play connection. */
|
||||
public final class Providence265ClientChecks 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);
|
||||
var enter=new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);
|
||||
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 config=new Providence265TestConfig();
|
||||
var world=c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Providence265 "+UUID.randomUUID());s.setSeed("265");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);
|
||||
c.runOnClient(m -> {m.getWindow().setWindowed(1280,960);m.options.guiScale().set(2);m.resizeGui();m.gui.hud.getChat().clearMessages(true);});
|
||||
var submittedAt=new AtomicLong();
|
||||
world.getServer().runOnServer(s -> {
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.PEACEFUL,true);
|
||||
if(fr.koka.sanctuary.weather.WeatherService.active(s.overworld()))
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather rain");
|
||||
else {var weather=s.overworld().getWeatherData();weather.setRainTime(10000);weather.setRaining(true);}
|
||||
submittedAt.set(s.overworld().getGameTime());
|
||||
});
|
||||
c.runOnClient(m -> m.getConnection().sendCommand("pray Ô seigneur, j'en ai marre de cet orage, faites revenir le soleil !"));
|
||||
c.waitTicks(10);
|
||||
c.runOnClient(m -> {
|
||||
require(messages(m).stream().noneMatch(message -> message.getContents() instanceof TranslatableContents t && t.getKey().startsWith("sanctuary.providence.")),"No immediate Providence reply");
|
||||
m.getConnection().sendCommand("pray bonjour");
|
||||
m.gui.setScreen(new ChatScreen("/pray ",false));
|
||||
});
|
||||
world.getServer().runOnServer(s -> require(s.overworld().getWeatherData().isRaining(),"No miracle executes during the response wait"));
|
||||
c.takeScreenshot("providence267-waiting");
|
||||
c.waitFor(m -> messages(m).stream().anyMatch(message -> message.getContents() instanceof TranslatableContents t && t.getKey().contains("sga.granted")),300);
|
||||
world.getServer().runOnServer(s -> {
|
||||
long elapsed=s.overworld().getGameTime()-submittedAt.get();
|
||||
require(elapsed>=100 && !s.overworld().getWeatherData().isRaining() && !s.overworld().getWeatherData().isThundering(),"Real network prayer changes weather only after five seconds");
|
||||
System.out.println("PROVIDENCE267_DELAY_PASS elapsed_server_ticks="+elapsed);
|
||||
});
|
||||
c.runOnClient(m -> {
|
||||
require(messages(m).stream().filter(message -> message.getContents() instanceof TranslatableContents t && t.getKey().contains("sga.granted")).count()==1,"Repeated prayer never replaces or duplicates the delayed reply");
|
||||
m.gui.setScreen(null);
|
||||
});
|
||||
c.runOnClient(m -> m.getConnection().sendCommand("providence readable true"));
|
||||
c.waitFor(m -> messages(m).stream().anyMatch(message -> message.getContents() instanceof TranslatableContents t && t.getKey().contains("readable.accessibility")),200);
|
||||
for (String locale:List.of("fr_fr","en_us")) for (int scale:List.of(2,3,4)) for (boolean readable:List.of(false,true)) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.options.guiScale().set(scale);m.resizeGui();m.gui.hud.getChat().clearMessages(true);
|
||||
for (String key:List.of("granted","deferred","refused","incomprehensible","alternative")) {
|
||||
var message=ProvidenceMessages.message(key,readable,0);
|
||||
require(!message.getString().startsWith("sanctuary."),"Translated Providence message");
|
||||
var expected=switch(key){case "granted"->net.minecraft.ChatFormatting.GREEN;case "deferred"->net.minecraft.ChatFormatting.YELLOW;case "refused"->net.minecraft.ChatFormatting.RED;case "incomprehensible"->net.minecraft.ChatFormatting.GRAY;default->net.minecraft.ChatFormatting.LIGHT_PURPLE;};
|
||||
require(message.getStyle().getColor()!=null&&message.getStyle().getColor().getValue()==net.minecraft.network.chat.TextColor.fromLegacyFormat(expected).getValue(),"Five distinct verdict colours in SGA and readable mode");
|
||||
require(!ProvidenceMessages.visible("silent"),"Silent outcome is never added to visible chat");
|
||||
if (!readable) require(message.getStyle().getFont().toString().contains("providence"),"SGA font retained");
|
||||
m.gui.hud.getChat().addClientSystemMessage(message);
|
||||
}
|
||||
m.gui.setScreen(new ChatScreen("/pray ",false));
|
||||
});
|
||||
c.waitTicks(4);c.takeScreenshot("providence265-"+locale+"-gui"+scale+(readable?"-readable":"-sga"));
|
||||
}
|
||||
c.runOnClient(m -> {m.gui.setScreen(null);m.getConnection().sendCommand("providence readable false");});
|
||||
c.waitFor(m -> messages(m).stream().anyMatch(message -> message.getContents() instanceof TranslatableContents t && t.getKey().contains("sga.accessibility")),200);
|
||||
c.waitTicks(110);
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.setHealth(p.getMaxHealth());p.setInvulnerableTime(0);p.damageCooldownTime=0;
|
||||
});
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected("fr_fr");m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(2);m.resizeGui();
|
||||
m.gui.hud.getChat().clearMessages(true);m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);
|
||||
m.getConnection().sendCommand("pray foudroie @"+m.player.getGameProfile().name());
|
||||
});
|
||||
c.waitTicks(10);
|
||||
world.getServer().runOnServer(s -> {var p=s.getPlayerList().getPlayers().getFirst();require(p.getHealth()==p.getMaxHealth(),"Targeted lightning waits for the same five-second response deadline");});
|
||||
c.waitFor(m -> !m.level.getEntitiesOfClass(net.minecraft.world.entity.LightningBolt.class,m.player.getBoundingBox().inflate(4)).isEmpty(),300);
|
||||
c.takeScreenshot("providence268-lightning");
|
||||
world.getServer().runOnServer(s -> {
|
||||
var p=s.getPlayerList().getPlayers().getFirst();
|
||||
require(p.getHealth()<p.getMaxHealth(),"Real named prayer damages its target on the server");
|
||||
require(p.level().getEntitiesOfClass(net.minecraft.world.entity.LightningBolt.class,p.getBoundingBox().inflate(4)).isEmpty(),"Native lightning visual has no server world mutations");
|
||||
});
|
||||
c.waitFor(m -> messages(m).stream().anyMatch(message -> message.getContents() instanceof TranslatableContents t && t.getKey().contains("sga.granted")),100);
|
||||
c.runOnClient(m -> {m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON);m.gui.setScreen(null);});
|
||||
System.out.println("PROVIDENCE268_LIGHTNING_CLIENT_PASS: actual named network prayer, five-second delay, native visible bolt and server-only target damage");
|
||||
c.waitTicks(110);
|
||||
world.getServer().runOnServer(s -> {
|
||||
try{Providence269GameTests.configure(s,1,1,0,0,5,2);}catch(Exception e){throw new RuntimeException(e);}
|
||||
var tool=new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.DIAMOND_PICKAXE);tool.setDamageValue(77);
|
||||
s.getPlayerList().getPlayers().getFirst().setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,tool);
|
||||
});
|
||||
c.runOnClient(m -> {m.gui.hud.getChat().clearMessages(true);m.getConnection().sendCommand("pray repare mon outil");});
|
||||
c.waitFor(m -> messages(m).stream().anyMatch(message -> message.getContents() instanceof TranslatableContents t&&t.getKey().contains("sga.deferred")),300);
|
||||
world.getServer().runOnServer(s -> require(s.getPlayerList().getPlayers().getFirst().getMainHandItem().getDamageValue()==77,"Yellow deferred response precedes the real repair effect"));
|
||||
c.runOnClient(m -> m.gui.setScreen(new ChatScreen("",false)));c.takeScreenshot("providence269-deferred");c.waitTicks(10);
|
||||
world.getServer().runOnServer(s -> require(s.getPlayerList().getPlayers().getFirst().getMainHandItem().getDamageValue()==77,"Deferred repair still waits after its first visible response"));
|
||||
c.waitFor(m -> messages(m).stream().anyMatch(message -> message.getContents() instanceof TranslatableContents t&&t.getKey().contains("sga.granted")),160);
|
||||
world.getServer().runOnServer(s -> require(s.getPlayerList().getPlayers().getFirst().getMainHandItem().getDamageValue()==0,"Deferred repair actually runs at the later deadline"));
|
||||
c.takeScreenshot("providence269-granted");c.runOnClient(m -> m.gui.setScreen(null));
|
||||
System.out.println("PROVIDENCE269_DEFERRED_CLIENT_PASS: real network prayer, five-second response wait, yellow deferral, separate two-second effect wait, real repair and green completion");
|
||||
System.out.println("PROVIDENCE265_CLIENT_PASS: real network command, server weather, accessibility option, native chat FR/EN GUI 2/3/4 SGA/readable");
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
finally { onboarding.set(false); }
|
||||
}
|
||||
@SuppressWarnings("unchecked") private static List<Component> messages(net.minecraft.client.Minecraft m) {
|
||||
try {
|
||||
var state=m.gui.hud.getChat().storeState();var field=state.getClass().getDeclaredField("messages");field.setAccessible(true);
|
||||
return ((List<net.minecraft.client.multiplayer.chat.GuiMessage>)field.get(state)).stream().map(net.minecraft.client.multiplayer.chat.GuiMessage::content).toList();
|
||||
} catch(Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
private static void require(boolean value,String message) { if(!value) throw new AssertionError(message); }
|
||||
}
|
||||
+2
-2
@@ -35,8 +35,8 @@ public final class Starter052ClientChecks implements FabricClientGameTest {
|
||||
require(name(screen).getValue().equals(NAME)&&screen.starterChoices().equals(choices),"Rejected colour preserves name and original eggs");
|
||||
screen.accept(welcome);
|
||||
var slots=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("StarterSlot")).toList();
|
||||
require(slots.getFirst().getX()==name(screen).getX()&&slots.getFirst().getX()==color(screen).getX(),"Egg choices align with the name field and colour controls on the left");
|
||||
require(name(screen).getY()>slots.getFirst().getBottom()&&button(screen,"sanctuary.progression.enter").getY()>name(screen).getBottom(),"Name beneath eggs and above actions");
|
||||
require(slots.getFirst().getX()>=name(screen).getX()&&slots.getLast().getRight()<=name(screen).getRight(),"Egg choices occupy the central familiar section");
|
||||
require(name(screen).getY()>color(screen).getBottom()&&button(screen,"sanctuary.progression.enter").getY()>Math.max(name(screen).getBottom(),slots.getFirst().getBottom()),"Character above familiar name, actions below both panels");
|
||||
for(var child:screen.children())if(child instanceof AbstractWidget w)require(w.getX()>=0&&w.getRight()<=screen.width&&w.getY()>=0&&w.getBottom()<=screen.height,"Widget fits "+locale+" "+screen.width+"x"+screen.height);
|
||||
require(!name(screen).getMessage().getString().startsWith("sanctuary."),"Localized name field");
|
||||
System.out.println("STARTER052_LAYOUT "+locale+" gui="+screen.width+"x"+screen.height);
|
||||
|
||||
@@ -52,6 +52,18 @@ public final class Atlas244GameTests {
|
||||
&& h.getLevel().getChunkSource().getChunkNow(far,far)==null,"No chunk ticket or terrain generation for the atlas");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void deliveredTerrainContinuesBeyondOldCapacity(GameTestHelper h) {
|
||||
var player=inhabitant(h,GameType.CREATIVE);var store=AtlasService.observations(player);
|
||||
byte[] history=new byte[256];Arrays.fill(history,(byte)42);
|
||||
for(int i=0;i<16384;i++)store.observe(new AtlasStore.Tile(i,1000,history));
|
||||
var origin=h.absolutePos(BlockPos.ZERO);
|
||||
var chunk=h.getLevel().getChunkSource().getChunkNow(origin.getX()>>4,origin.getZ()>>4);
|
||||
h.assertTrue(!store.full(),"Old full atlas can record new islands");
|
||||
AtlasService.observeSentChunk(player,chunk);
|
||||
h.assertTrue(store.size()>16384 && store.contains(chunk.getPos().x(),chunk.getPos().z()),"Native sent chunk is recorded after the old ceiling");
|
||||
for(int altitude:AtlasAltitude.LEVELS)h.assertTrue(AtlasService.observations(player,altitude).contains(chunk.getPos().x(),chunk.getPos().z()),"All six layers still survey delivered terrain");
|
||||
h.succeed();
|
||||
}
|
||||
private static ServerPlayer inhabitant(GameTestHelper h,GameType mode) {
|
||||
var p=(ServerPlayer)h.makeMockServerPlayer(mode);
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);return p;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.animal.cow.Cow;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native cone, death splash and temporary diversion; familiar permission and miniature paths. */
|
||||
public final class Boomer284GameTests {
|
||||
private static final class StillZombie extends Zombie {
|
||||
StillZombie(net.minecraft.world.level.Level level){super(EntityTypes.ZOMBIE,level);}
|
||||
@Override protected void registerGoals() { }
|
||||
}
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
var at=h.absolutePos(new BlockPos(0,174+slot*11,0));
|
||||
for(int x=-8;x<=8;x++)for(int z=-6;z<=15;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,7,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static BoomerEntity boomer(GameTestHelper h,BlockPos at) {
|
||||
var source=Infected.BOOMER.create(h.getLevel(),EntitySpawnReason.COMMAND);source.setNoAi(true);source.setOnGround(true);
|
||||
source.setPos(Vec3.atBottomCenterOf(at));h.getLevel().addFreshEntity(source);return source;
|
||||
}
|
||||
private static Cow cow(GameTestHelper h,Vec3 at) {
|
||||
var target=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(at);h.getLevel().addFreshEntity(target);return target;
|
||||
}
|
||||
private static Zombie enemy(GameTestHelper h,Vec3 at) {
|
||||
var target=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(at);h.getLevel().addFreshEntity(target);return target;
|
||||
}
|
||||
@GameTest public void eggProfileSpawnAndAdultRule(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.BOOMER_EGG);var mob=Infected.BOOMER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.BOOMER&&SpawnEggItem.byId(Infected.BOOMER).orElseThrow().value()==Infected.BOOMER_EGG,"Native egg identifies the new Boomer in both directions");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.20,"Boomer stays adult with its slow role attributes");
|
||||
var profile=FamiliarCatalog.bundled().forEgg(egg);
|
||||
h.assertTrue(profile.id().equals("sanctuary:boomer")&&profile.health()==14&&profile.signature().effect().equals("bile"),"Bundled familiar supplies its own bile profile");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.BOOMER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Boomer receives one rare singleton spawn entry");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=60) public void nativeWindupReleasesBileAndPaysCooldown(GameTestHelper h) {
|
||||
var at=arena(h,1);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
h.runAfterDelay(10,()->h.assertTrue(source.windingUp()&&!target.hasEffect(MobEffects.GLOWING),"Visible preparation precedes coating"));
|
||||
h.runAfterDelay(27,()->{
|
||||
h.assertTrue(target.hasEffect(MobEffects.GLOWING)&&target.hasEffect(MobEffects.WEAKNESS)&&target.getHealth()==10,"The real AI vomits bile without direct acid damage");
|
||||
h.assertTrue(source.bileCooldown()>0&&!source.windingUp(),"The completed cone enters cooldown");source.discard();target.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest(maxTicks=60) public void sidestepEvadesTheLockedCone(GameTestHelper h) {
|
||||
var at=arena(h,2);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
h.runAfterDelay(10,()->target.setPos(Vec3.atBottomCenterOf(at.offset(4,0,3))));
|
||||
h.runAfterDelay(27,()->{h.assertTrue(!target.hasEffect(MobEffects.GLOWING)&&source.bileCooldown()>0,"A side step avoids the original cone direction");source.discard();target.discard();h.succeed();});
|
||||
}
|
||||
@GameTest(maxTicks=45) public void interruptedWindupReloadKeepsOnlyCooldown(GameTestHelper h) {
|
||||
var at=arena(h,3);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
h.runAfterDelay(10,()->{
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());source.save(output);
|
||||
var tag=output.buildResult();tag.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),tag),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof BoomerEntity b&&!b.isBaby()&&!b.windingUp()&&b.bileCooldown()==120,"A saved preparation reloads as an adult with cooldown, without a replayed cone");
|
||||
for(int x=-3;x<=3;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,2),Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
h.runAfterDelay(30,()->{h.assertTrue(!source.windingUp()&&!target.hasEffect(MobEffects.GLOWING),"A wall interrupts preparation before release");source.discard();target.discard();h.succeed();});
|
||||
}
|
||||
@GameTest public void lethalNativeDamageSplashesExactlyOnceWithoutBreakingTerrain(GameTestHelper h) {
|
||||
var at=arena(h,4);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,2)));
|
||||
source.hurtServer(h.getLevel(),source.damageSources().generic(),100);
|
||||
h.assertTrue(!source.isAlive()&&target.getHealth()==8&&target.hasEffect(MobEffects.GLOWING)&&target.hasEffect(MobEffects.WEAKNESS),"Native lethal damage produces one two-damage bile splash");
|
||||
target.setInvulnerableTime(0);source.die(source.damageSources().generic());
|
||||
h.assertTrue(target.getHealth()==8,"Repeated native death notification cannot splash twice");
|
||||
h.assertTrue(h.getLevel().getBlockState(at.below()).is(Blocks.STONE)&&h.getLevel().getBlockState(at).isAir(),"Death never explodes terrain or lights fire");
|
||||
source.discard();target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void administrativeRemovalDoesNotBurst(GameTestHelper h) {
|
||||
var at=arena(h,5);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,2)));source.discard();
|
||||
h.assertTrue(target.getHealth()==10&&!target.hasEffect(MobEffects.GLOWING),"Discarding or unloading a living Boomer is not a lethal explosion");target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void wallsHeightBackAndInfectedFamilyBlockCoating(GameTestHelper h) {
|
||||
var at=arena(h,6);var source=boomer(h,at);var front=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,3)));
|
||||
var back=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,-3)));var hidden=cow(h,Vec3.atBottomCenterOf(at.offset(2,0,3)));
|
||||
var upstairs=cow(h,Vec3.atBottomCenterOf(at.offset(0,2,2)));var family=Infected.CHARGER.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
family.setNoAi(true);family.setPos(Vec3.atBottomCenterOf(at.offset(0,0,2)));h.getLevel().addFreshEntity(family);
|
||||
for(int z=1;z<=4;z++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(1,y,z),Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(BoomerBile.wild(source,new Vec3(0,0,1),false)==1&&front.hasEffect(MobEffects.GLOWING),"Only the visible creature inside the forward cone is coated");
|
||||
h.assertTrue(!back.hasEffect(MobEffects.GLOWING)&&!hidden.hasEffect(MobEffects.GLOWING)&&!upstairs.hasEffect(MobEffects.GLOWING)&&!family.hasEffect(MobEffects.GLOWING),"Bile cannot cross walls/floors, reach behind the cone or coat infected relatives");
|
||||
for(var entity:java.util.List.of(source,front,back,hidden,upstairs,family))entity.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void attractionIsBoundedToSixAlreadyPresentZombies(GameTestHelper h) {
|
||||
var at=arena(h,7);var source=boomer(h,at);var victim=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));var zombies=new java.util.ArrayList<Zombie>();
|
||||
for(int i=0;i<8;i++) { var zombie=new StillZombie(h.getLevel());zombie.setPos(Vec3.atBottomCenterOf(at.offset(i%3-1,0,7+i/3)));h.getLevel().addFreshEntity(zombie);zombies.add(zombie); }
|
||||
int before=h.getLevel().getEntitiesOfClass(Zombie.class,source.getBoundingBox().inflate(13)).size();BoomerBile.wild(source,new Vec3(0,0,1),false);
|
||||
h.assertTrue(zombies.stream().filter(z->z.getTarget()==victim).count()==6,"Bile recruits at most six existing zombies");
|
||||
h.assertTrue(h.getLevel().getEntitiesOfClass(Zombie.class,source.getBoundingBox().inflate(13)).size()==before,"The horde effect never spawns extra entities");
|
||||
zombies.forEach(Entity::discard);source.discard();victim.discard();h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=160) public void recruitedZombieReallyPursuesAndThenRestoresPreviousTarget(GameTestHelper h) {
|
||||
var at=arena(h,8);var source=boomer(h,at);var victim=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));victim.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);victim.setHealth(100);
|
||||
var previous=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,14)));var zombie=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
zombie.setPos(Vec3.atBottomCenterOf(at.offset(0,0,10)));zombie.setOnGround(true);h.getLevel().addFreshEntity(zombie);zombie.setTarget(previous);
|
||||
BoomerBile.wild(source,new Vec3(0,0,1),false);
|
||||
h.assertTrue(zombie.getTarget()==victim,"Coating temporarily diverts the native zombie");
|
||||
h.runAfterDelay(70,()->h.assertTrue(zombie.getZ()<at.getZ()+9&&victim.getHealth()<100,"The real zombie navigates toward and attacks the marked creature"));
|
||||
h.runAfterDelay(140,()->{
|
||||
h.assertTrue(zombie.getTarget()==previous&&!victim.hasEffect(MobEffects.GLOWING),"Diversion expires and restores the original valid target");
|
||||
source.discard();victim.discard();previous.discard();zombie.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void familiarGDealsBileDamageAndProtectsOwnerAndPassiveAnimals(GameTestHelper h) {
|
||||
var p=pet(h,9);var pet=FamiliarService.active(p);var front=enemy(h,pet.position().add(0,0,2));var back=enemy(h,pet.position().add(0,0,-2));var animal=cow(h,pet.position().add(1,0,0));
|
||||
float health=p.getHealth();FamiliarBattle.engage(p,front,false);
|
||||
h.assertTrue(FamiliarBattle.activate(p).equals("activated"),"Shared manual entry point accepts the Boomer's signature");
|
||||
var cast=FamiliarBattle.state(p).cast;for(int i=0;i<20;i++)FamiliarTechniques.advance(pet);
|
||||
h.assertTrue(front.getHealth()<20&&back.getHealth()<20&&front.hasEffect(MobEffects.WEAKNESS)&&front.hasEffect(MobEffects.GLOWING)&&cast.hit.size()==2,"G splashes enemies around the familiar using shared damage and effects");
|
||||
h.assertTrue(pet.isAlive()&&pet.getHealth()==14&&p.getHealth()==health&&animal.getHealth()==10&&!animal.hasEffect(MobEffects.GLOWING),"The familiar never sacrifices itself or coats its owner/passive animals");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&CompanionService.record(p).readyAt()==FamiliarBattle.bond(p).readyAt(),"Released G commits the durable shared cooldown");
|
||||
front.discard();back.discard();animal.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarConeHasNoBackHitAndNeverRedirectsHostiles(GameTestHelper h) {
|
||||
var p=pet(h,10);var pet=FamiliarService.active(p);var front=enemy(h,pet.position().add(0,0,2));var back=enemy(h,pet.position().add(0,0,-2));
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).initiative(),true,front,front.position());FamiliarActions.execute(cast);
|
||||
h.assertTrue(front.getHealth()<20&&front.hasEffect(MobEffects.WEAKNESS)&&back.getHealth()==20&&!back.hasEffect(MobEffects.GLOWING),"Automatic bile affects only the forward cone");
|
||||
h.assertTrue(front.getTarget()==null&&back.getTarget()==null,"Familiar bile does not take control of monsters against third parties");front.discard();back.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void everyMiniatureSizeRetainsAdultGeometryAndBond(GameTestHelper h) {
|
||||
var p=pet(h,11);var id=FamiliarBattle.bond(p).id();
|
||||
for(int value:new int[]{120,500,1000,1500,3000,6000}) {
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);var pet=FamiliarService.active(p);
|
||||
h.assertTrue(pet!=null&&!pet.babyAppearance()&&FamiliarSize.value(pet.egg())==value&&Math.abs(pet.visualHeight()-.85*value/1000)<.001&&FamiliarBattle.bond(p).id().equals(id),"Every supported size stays an adult miniature with the same identity: "+value);
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void remoteProjectionDoesNotLoadChunks(GameTestHelper h) {
|
||||
var remote=h.absolutePos(new BlockPos(10000,200,10000));var source=Infected.BOOMER.create(h.getLevel(),EntitySpawnReason.COMMAND);source.setPos(Vec3.atCenterOf(remote));
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(remote)&&BoomerBile.wild(source,new Vec3(0,0,1),false)==0&&!h.getLevel().hasChunkAt(remote),"A remote projection neither affects unloaded mobs nor requests their chunks");h.succeed();
|
||||
}
|
||||
@GameTest public void savingOnTheReleaseTickAlreadyPreservesCooldown(GameTestHelper h) {
|
||||
var at=arena(h,0);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
for(int i=0;i<40&&!target.hasEffect(MobEffects.GLOWING);i++)h.getLevel().tickNonPassenger(source);
|
||||
h.assertTrue(target.hasEffect(MobEffects.GLOWING)&&!source.windingUp()&&source.bileCooldown()==120,"Cooldown is committed on release, before the goal's next stop callback");
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());source.save(output);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof BoomerEntity b&&b.bileCooldown()==120&&!b.windingUp(),"An immediate save/reload cannot reset the released vomit's cooldown");
|
||||
source.discard();target.discard();h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);p.setPos(Vec3.atBottomCenterOf(arena(h,slot)));var egg=new ItemStack(Infected.BOOMER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);var pet=FamiliarService.active(p);pet.setPos(p.position());pet.setOnGround(true);pet.setNoAi(true);return p;
|
||||
}
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.EntityTypeTags;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real native movement, contact, wall collision and the familiar's straight-charge path. */
|
||||
public final class Charger282GameTests {
|
||||
private static final class StillCow extends net.minecraft.world.entity.animal.cow.Cow {
|
||||
StillCow(net.minecraft.world.level.Level level){super(EntityTypes.COW,level);}
|
||||
@Override protected void registerGoals() { }
|
||||
}
|
||||
private record Wild(ChargerEntity mob,net.minecraft.world.entity.animal.cow.Cow target,BlockPos origin) {
|
||||
void close(){mob.discard();target.discard();}
|
||||
}
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
var at=h.absolutePos(new BlockPos(0,70+slot*8,0));
|
||||
// Keep the four-block sidestep on the arena floor instead of testing fall damage.
|
||||
for(int x=-5;x<=5;x++)for(int z=-2;z<=34;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,4,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static Wild wild(GameTestHelper h,int slot) {
|
||||
var at=arena(h,slot);var mob=Infected.CHARGER.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
mob.setPos(Vec3.atBottomCenterOf(at));mob.setOnGround(true);
|
||||
var target=new StillCow(h.getLevel());
|
||||
target.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);
|
||||
target.setPos(Vec3.atBottomCenterOf(at.offset(0,0,7)));
|
||||
h.getLevel().addFreshEntity(target);h.getLevel().addFreshEntity(mob);mob.setTarget(target);
|
||||
return new Wild(mob,target,at);
|
||||
}
|
||||
@GameTest public void eggProfileTagsAndAdultRuleAreRegistered(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.CHARGER_EGG);var mob=Infected.CHARGER.create(h.getLevel(),EntitySpawnReason.MOB_SUMMONED);
|
||||
mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.CHARGER&&SpawnEggItem.byId(Infected.CHARGER).orElseThrow().value()==Infected.CHARGER_EGG,"Native charger egg mapping is bidirectional");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.27,"Charger is adult-only without the baby speed bonus");
|
||||
h.assertTrue(Infected.CHARGER.builtInRegistryHolder().is(EntityTypeTags.UNDEAD)&&Infected.CHARGER.builtInRegistryHolder().is(EntityTypeTags.BURN_IN_DAYLIGHT),"Charger remains an undead daylight-sensitive zombie");
|
||||
h.assertTrue(FamiliarCatalog.bundled().forEgg(egg).id().equals("sanctuary:charger"),"Charger has a dedicated bundled profile");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.CHARGER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Zombie habitats receive one rare singleton charger entry");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=150) public void windupImpactRecoveryAndCooldownUseRealPhysics(GameTestHelper h) {
|
||||
var w=wild(h,1);var start=w.mob.position();
|
||||
h.runAfterDelay(10,()->h.assertTrue(w.mob.phase()==ChargerEntity.Phase.WINDUP&&w.mob.position().subtract(start).horizontalDistance()<.1,"The charger holds its position while visibly preparing"));
|
||||
h.runAfterDelay(45,()->{
|
||||
h.assertTrue(w.target.getHealth()==96&&w.target.getZ()>w.origin.getZ()+7.5,"One real charge impact deals four damage and pushes the target: health="+w.target.getHealth()+" z="+w.target.getZ()+" origin="+w.origin+" phase="+w.mob.phase());
|
||||
h.assertTrue(w.mob.phase()==ChargerEntity.Phase.RECOVERING,"Impact enters recovery");
|
||||
h.assertTrue(!w.mob.doHurtTarget(h.getLevel(),w.target),"Native melee cannot double-hit during recovery");
|
||||
});
|
||||
h.runAfterDelay(110,()->{h.assertTrue(w.mob.phase()==ChargerEntity.Phase.IDLE&&w.mob.chargeCooldown()>0,"Recovery ends before another charge is permitted");w.close();h.succeed();});
|
||||
}
|
||||
@GameTest(maxTicks=120) public void movingAsideDodgesInsteadOfBeingHomedOn(GameTestHelper h) {
|
||||
var w=wild(h,2);var started=new java.util.concurrent.atomic.AtomicBoolean();double x=w.mob.getX();
|
||||
h.onEachTick(()->{if(w.mob.phase()==ChargerEntity.Phase.CHARGING&&started.compareAndSet(false,true)){
|
||||
w.target.setPos(w.target.position().add(4,0,0));
|
||||
h.runAfterDelay(30,()->{
|
||||
h.assertTrue(Math.abs(w.mob.getX()-x)<.01&&w.mob.getZ()>w.origin.getZ()+4,"A released charge keeps its original straight direction");
|
||||
h.assertTrue(w.target.getHealth()==100&&w.mob.phase()==ChargerEntity.Phase.RECOVERING,"A sidestep avoids contact and a miss still requires recovery: health="+w.target.getHealth()+" target="+w.target.position()+" phase="+w.mob.phase());
|
||||
w.close();h.succeed();
|
||||
});
|
||||
}});
|
||||
}
|
||||
@GameTest(maxTicks=120) public void newWallStopsTheBodyWithoutDamageThroughIt(GameTestHelper h) {
|
||||
var w=wild(h,3);var started=new java.util.concurrent.atomic.AtomicBoolean();
|
||||
h.onEachTick(()->{if(w.mob.phase()==ChargerEntity.Phase.CHARGING&&started.compareAndSet(false,true)){
|
||||
for(int x=-3;x<=3;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(w.origin.offset(x,y,4),Blocks.STONE.defaultBlockState());
|
||||
h.runAfterDelay(25,()->{
|
||||
h.assertTrue(w.mob.getZ()<=w.origin.getZ()+3.71&&w.target.getHealth()==100,"Native collision stops the actual body and swept contact cannot cross the wall");
|
||||
h.assertTrue(w.mob.phase()==ChargerEntity.Phase.RECOVERING&&h.getLevel().getBlockState(w.origin.offset(0,1,4)).is(Blocks.STONE),"Wall collision causes recovery without breaking blocks");
|
||||
w.close();h.succeed();
|
||||
});
|
||||
}});
|
||||
}
|
||||
@GameTest(maxTicks=100) public void reloadingAnInterruptedChargePreservesOnlyCooldown(GameTestHelper h) {
|
||||
var w=wild(h,4);w.mob.setHealth(7);
|
||||
h.runAfterDelay(10,()->{
|
||||
w.mob.setDeltaMovement(1.18,0,0);
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());w.mob.save(output);
|
||||
var saved=output.buildResult();saved.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof ChargerEntity c&&c.getUUID().equals(w.mob.getUUID())&&c.getHealth()==7&&!c.isBaby()
|
||||
&&c.phase()==ChargerEntity.Phase.IDLE&&c.chargeCooldown()==100&&c.chargeDirection().equals(Vec3.ZERO)&&c.getDeltaMovement().horizontalDistance()==0,"Reload retains UUID and health but cannot resume or reset a partly prepared rush");
|
||||
w.close();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void familiarActuallyRushesAndCommitsSharedCooldown(GameTestHelper h) {
|
||||
var p=pet(h,6);var pet=FamiliarService.active(p);var enemy=enemy(p);var profile=FamiliarBattle.profile(p);
|
||||
h.assertTrue(!pet.babyAppearance()&&profile.id().equals("sanctuary:charger")&&pet.getMaxHealth()==16,"Equipped charger is a miniature adult using its own profile");
|
||||
FamiliarBattle.engage(p,enemy,false);h.assertTrue(FamiliarBattle.activate(p).equals("activated"),"The normal manual technique entry point starts the rush");
|
||||
var cast=FamiliarBattle.state(p).cast;run(p,80);
|
||||
h.assertTrue(enemy.getHealth()<20&&enemy.getHealth()>=16&&cast.hit.size()==1,"The real moved body hits once through shared damage and armor rules");
|
||||
h.assertTrue(enemy.getDeltaMovement().z>0&&FamiliarBattle.state(p).cast==null,"Impact pushes the enemy and recovery completes");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&CompanionService.record(p).readyAt()==FamiliarBattle.bond(p).readyAt(),"The durable shared cooldown is committed at release");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarMissKeepsStraightTrajectoryAndPaysCooldown(GameTestHelper h) {
|
||||
var p=pet(h,7);var pet=FamiliarService.active(p);var enemy=enemy(p);double x=pet.getX();
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());FamiliarBattle.state(p).cast=cast;
|
||||
run(p,22);enemy.setPos(enemy.position().add(4,0,0));run(p,70);
|
||||
h.assertTrue(Math.abs(pet.getX()-x)<.001&&pet.getZ()>p.getZ()+4&&enemy.getHealth()==20,"The familiar does not home in on a sidestepping target");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"A missed released charge still consumes cooldown and recovery");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarStopsBeforeWallsAndCannotChargeItsOwner(GameTestHelper h) {
|
||||
var p=pet(h,8);var pet=FamiliarService.active(p);var enemy=enemy(p);double wall=pet.getZ()+2;
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());FamiliarBattle.state(p).cast=cast;run(p,22);
|
||||
var pos=BlockPos.containing(pet.getX(),pet.getY(),wall);
|
||||
for(int x=-2;x<=2;x++)for(int y=0;y<3;y++)p.level().setBlockAndUpdate(pos.offset(x,y,0),Blocks.STONE.defaultBlockState());
|
||||
run(p,70);h.assertTrue(enemy.getHealth()==20&&pet.getZ()<wall&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"An obstacle stops the familiar without a through-wall hit or a cooldown refund");
|
||||
float health=p.getHealth();var denied=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,p,p.position());FamiliarBattle.state(p).cast=denied;run(p,30);
|
||||
h.assertTrue(p.getHealth()==health&&denied.hit.isEmpty(),"Even a server-created cast cannot bypass the owner protection rules");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void ownerTeleportCannotBecomeASweptRemoteHit(GameTestHelper h) {
|
||||
var p=pet(h,9);var pet=FamiliarService.active(p);var enemy=enemy(p);
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());FamiliarBattle.state(p).cast=cast;
|
||||
run(p,22);pet.teleportTo(pet.getX(),pet.getY(),pet.getZ()+10);run(p,40);
|
||||
h.assertTrue(enemy.getHealth()==20&&cast.hit.isEmpty()&&FamiliarBattle.state(p).cast==null,"A discontinuous relocation across the target cancels contact rather than hitting from a distance");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"An interrupted released charge retains its cooldown");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void mountedWindupBlocksSteeringAndThenRestoresIt(GameTestHelper h) {
|
||||
var p=pet(h,10);var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();
|
||||
size.putInt("schema",1);size.putInt("per_mille",2500);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
var floor=p.blockPosition();
|
||||
for(int x=-3;x<=3;x++)for(int z=-2;z<=34;z++) {
|
||||
p.level().setBlockAndUpdate(floor.offset(x,4,z),Blocks.AIR.defaultBlockState());
|
||||
p.level().setBlockAndUpdate(floor.offset(x,8,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);var pet=FamiliarService.active(p);pet.setPos(Vec3.atBottomCenterOf(floor));pet.setOnGround(true);
|
||||
h.assertTrue(pet.mount(p),"The existing colossal familiar can be mounted");
|
||||
var enemy=enemy(p);FamiliarBattle.state(p).cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());
|
||||
p.setYRot(90);p.setLastClientInput(new net.minecraft.world.entity.player.Input(true,false,false,true,true,false,false));
|
||||
pet.setDeltaMovement(Vec3.ZERO);var start=pet.position();pet.travel(Vec3.ZERO);
|
||||
h.assertTrue(pet.position().subtract(start).horizontalDistance()<.001&&pet.getDeltaMovement().horizontalDistance()<.001,"A rider cannot steer or jump out of the committed charge windup");
|
||||
FamiliarBattle.cancel(p);pet.setOnGround(true);pet.travel(Vec3.ZERO);
|
||||
h.assertTrue(pet.getDeltaMovement().horizontalDistance()>0,"Normal rider movement resumes when the cast ends");
|
||||
p.stopRiding();enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void unloadedCorridorIsRejectedWithoutLoadingIt(GameTestHelper h) {
|
||||
var at=h.absolutePos(new BlockPos(10000,90,10000));
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(at),"The remote corridor starts unloaded");
|
||||
h.assertTrue(!ChargeContact.loaded(h.getLevel(),new net.minecraft.world.phys.AABB(at).inflate(.3)),"A full body corridor must already be loaded");
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(at),"The charge probe does not request or generate the remote chunk");h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);var at=arena(h,slot);
|
||||
p.setPos(Vec3.atBottomCenterOf(at));var egg=new ItemStack(Infected.CHARGER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);pet.setPos(p.position());pet.setOnGround(true);return p;
|
||||
}
|
||||
private static net.minecraft.world.entity.monster.zombie.Zombie enemy(ServerPlayer p) {
|
||||
var enemy=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);enemy.setNoAi(true);enemy.setPos(p.position().add(0,0,5));p.level().addFreshEntity(enemy);return enemy;
|
||||
}
|
||||
private static void run(ServerPlayer p,int ticks) {
|
||||
var pet=FamiliarService.active(p);
|
||||
for(int i=0;i<ticks&&FamiliarBattle.state(p).cast!=null;i++) {
|
||||
pet.setOnGround(true);FamiliarTechniques.advance(pet);pet.move(MoverType.SELF,pet.getDeltaMovement().multiply(1,0,1));pet.setDeltaMovement(Vec3.ZERO);
|
||||
}
|
||||
}
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.property.*;
|
||||
import fr.koka.sanctuary.progression.ProgressionService;
|
||||
import fr.koka.sanctuary.familiar.FamiliarExperience;
|
||||
import fr.koka.sanctuary.gems.Gems;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
public final class ChunkClaims272GameTests {
|
||||
private static ServerPlayer admitted(GameTestHelper h,int x,int z)throws Exception{
|
||||
var p=Progression003GameTests.player(h);var s=ProgressionService.session(h.getLevel().getServer());
|
||||
s.inhabitants.create(p.getUUID(),"ClaimsTest","",s.inhabitants.available().getFirst());s.inhabitants.arrive(p.getUUID());
|
||||
p.setAttached(ProgressionService.RECORD,fr.koka.sanctuary.progression.ProgressRecord.INITIAL.json());
|
||||
fr.koka.sanctuary.progression.ProgressEffects.apply(p,fr.koka.sanctuary.progression.ProgressRecord.INITIAL);
|
||||
p.setExperienceLevels(20);
|
||||
if(!ProgressionService.purchase(p,"atlas",fr.koka.sanctuary.progression.ProgressEffects.get(p).revision()))throw new AssertionError("Claim fixtures require the purchased Atlas");
|
||||
FamiliarExperience.setPlayerXp(p,0);
|
||||
p.setPos(x*16+.5,80,z*16+.5);p.level().getChunk(x,z);
|
||||
fr.koka.demeure.DemeureService.record(p,p.blockPosition(),1);return p;
|
||||
}
|
||||
private static String act(ServerPlayer p,String action,int x,int z){
|
||||
return ChunkPropertyService.act(p,action,new ChunkClaims.Key(x,z),ChunkPropertyService.store(p.level().getServer()).revision());
|
||||
}
|
||||
@GameTest public void nativeXpSapphirePurchasesAndReceipts(GameTestHelper h)throws Exception{
|
||||
var p=admitted(h,300,300);var s=ChunkPropertyService.store(p.level().getServer());
|
||||
var guest=Progression003GameTests.player(h);h.assertTrue(!ChunkPropertyService.eligible(guest),"Admission is required");
|
||||
for(int i=0;i<5;i++){p.level().getChunk(300+i,300);fr.koka.demeure.DemeureService.record(p,new BlockPos((300+i)*16,80,300*16),1);h.assertTrue(act(p,"claim",300+i,300).equals("claimed"),"Five free inhabited chunks need no XP");}
|
||||
long rev=s.revision();p.level().getChunk(305,300);
|
||||
fr.koka.demeure.DemeureService.record(p,new BlockPos(305*16,80,300*16),1);
|
||||
h.assertTrue(act(p,"claim",305,300).equals("xp")&&s.revision()==rev&&s.count(p.getUUID())==5,"Sixth claim requires 100 points without a reservation on refusal");
|
||||
FamiliarExperience.setPlayerXp(p,500);h.assertTrue(act(p,"claim",305,300).equals("claimed")&&FamiliarExperience.playerXp(p)==400,"Sixth chunk debits 100 native XP");
|
||||
p.level().getChunk(306,300);fr.koka.demeure.DemeureService.record(p,new BlockPos(306*16,80,300*16),1);h.assertTrue(act(p,"claim",306,300).equals("claimed")&&FamiliarExperience.playerXp(p)==200,"Seventh debits 200 points");
|
||||
h.assertTrue(ChunkPropertyService.act(p,"claim",new ChunkClaims.Key(307,300),rev).equals("changed")&&FamiliarExperience.playerXp(p)==200,"Stale request cannot charge again");
|
||||
h.assertTrue(act(p,"protect",300,300).equals("sapphires")&&s.get(new ChunkClaims.Key(300,300)).until()==0,"No coverage without sapphires");
|
||||
p.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),4));p.getInventory().setItem(10,new ItemStack(Gems.SAPPHIRE.gem(),3));
|
||||
h.assertTrue(act(p,"protect",300,300).equals("sapphires")&&ChunkPropertyService.sapphires(p)==7,"Seven sapphires remain untouched");
|
||||
p.getInventory().setItem(11,new ItemStack(Gems.SAPPHIRE.gem(),2));
|
||||
h.assertTrue(act(p,"protect",300,300).equals("protected")&&ChunkPropertyService.sapphires(p)==1,"Exactly eight sapphires consumed across stacks");
|
||||
var key=new ChunkClaims.Key(300,300);long until=s.get(key).until();
|
||||
h.assertTrue(until-s.now()>3L*24*60*60*1000-10_000&&until-s.now()<=3L*24*60*60*1000&&s.get(new ChunkClaims.Key(301,300)).until()==0,"Three days are per chunk");
|
||||
var file=p.level().getServer().getWorldPath(LevelResource.PLAYER_DATA_DIR).resolve(p.getStringUUID()+".dat");
|
||||
var nbt=net.minecraft.nbt.NbtIo.readCompressed(file,net.minecraft.nbt.NbtAccounter.create(32L*1024*1024));
|
||||
h.assertTrue(nbt.toString().contains(p.getAttachedOrElse(ChunkPropertyService.RECEIPT,"missing")),"Native player receipt is durable before registry commit");
|
||||
ChunkPropertyService.recover(p);h.assertTrue(s.get(key).until()==until&&ChunkPropertyService.sapphires(p)==1,"Repeated recovery cannot renew or charge again");
|
||||
p.getInventory().setItem(12,new ItemStack(Gems.SAPPHIRE.gem(),8));
|
||||
h.assertTrue(act(p,"protect",300,300).equals("protected")&&ChunkPropertyService.sapphires(p)==1&&s.get(key).until()==until+3L*24*60*60*1000,"Another eight sapphires append exactly three days without losing existing coverage");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void nativeDurationUsesActualChunkAndKeepsInteractionsFree(GameTestHelper h)throws Exception{
|
||||
var owner=admitted(h,340,340);var other=admitted(h,340,340);var s=ChunkPropertyService.store(owner.level().getServer());
|
||||
h.assertTrue(act(owner,"claim",340,340).equals("claimed"),"Reserve chunk");
|
||||
var pos=new BlockPos(340*16+15,80,340*16+2);var state=Blocks.STONE.defaultBlockState();owner.level().setBlockAndUpdate(pos,state);
|
||||
float ownBase=state.getDestroyProgress(owner,owner.level(),pos),otherBase=state.getDestroyProgress(other,other.level(),pos);
|
||||
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),64));h.assertTrue(act(owner,"protect",340,340).equals("protected"),"Activate protection");
|
||||
h.assertTrue(state.getDestroyProgress(owner,owner.level(),pos)==ownBase*2&&state.getDestroyProgress(other,other.level(),pos)==otherBase*.5f,"Real native mining duration hook multiplies once");
|
||||
h.assertTrue(state.getDestroyProgress(other,other.level(),pos.east())==otherBase,"Adjacent chunk has no slowdown");
|
||||
h.assertTrue(ChunkPropertyService.multiplier(owner,owner.level(),pos.above(100))==2&&ChunkPropertyService.multiplier(other,other.level(),pos.below(100))==.5f,"Chunk column covers all build heights");
|
||||
h.assertTrue(owner.level().mayInteract(other,pos)&&other.mayUseItemAt(pos,net.minecraft.core.Direction.UP,new ItemStack(Items.COBBLESTONE)),"Claim does not deny interactions and placement");
|
||||
h.assertTrue(other.gameMode.destroyBlock(pos),"Other player can still finish breaking a protected block");
|
||||
h.assertTrue(act(other,"protect",340,340).equals("not_owner"),"Other player cannot buy protection on someone else's claim");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void voluntaryLegacyMigrationRefundsAndPreservesOriginalFile(GameTestHelper h)throws Exception{
|
||||
var owner=admitted(h,380,380);var other=admitted(h,380,380);var legacy=PropertyService.store(owner.level().getServer());var at=owner.blockPosition();
|
||||
h.assertTrue(PropertyService.act(owner,"claim",-1).equals("claimed"),"Legacy fixture reserves volume");FamiliarExperience.setPlayerXp(owner,500);
|
||||
h.assertTrue(PropertyService.act(owner,"expand",0).equals("expanded")&&FamiliarExperience.playerXp(owner)==400,"Legacy fixture has a paid extension");
|
||||
var path=owner.level().getServer().getWorldPath(LevelResource.ROOT).resolve("sanctuary/properties-v1.json");byte[] before=Files.readAllBytes(path);
|
||||
h.assertTrue(!PropertyService.mayModify(owner.level(),other,at)&&act(other,"claim",380,380).equals("legacy_overlap"),"Legacy rights stay active before migration");
|
||||
h.assertTrue(act(owner,"claim",381,380).equals("legacy"),"Owner must explicitly migrate");
|
||||
h.assertTrue(act(owner,"migrate",380,380).equals("migrated")&&FamiliarExperience.playerXp(owner)==500,"Confirmed migration refunds exact tier XP");
|
||||
h.assertTrue(Arrays.equals(before,Files.readAllBytes(path))&&legacy.get(owner.getUUID()).tier()==1,"Legacy schema/file is preserved");
|
||||
h.assertTrue(PropertyService.mayModify(owner.level(),other,at)&&ChunkPropertyService.store(owner.level().getServer()).count(owner.getUUID())==0,"Retirement removes hard protection without assigning chunks");
|
||||
h.assertTrue(act(owner,"migrate",380,380).equals("changed")&&FamiliarExperience.playerXp(owner)==500,"No duplicate refund");
|
||||
h.assertTrue(act(owner,"claim",380,380).equals("claimed"),"Owner chooses first free chunk after migration");h.succeed();
|
||||
}
|
||||
@GameTest public void serverRejectsRemoteUndiscoveredAndSpawnClaims(GameTestHelper h)throws Exception{
|
||||
var p=admitted(h,420,420);var s=ChunkPropertyService.store(p.level().getServer());
|
||||
h.assertTrue(act(p,"claim",1600,1600).equals("undiscovered")&&s.count(p.getUUID())==0,"Forged remote chunk is refused without loading it");
|
||||
var spawn=p.level().getServer().getRespawnData().pos();int x=spawn.getX()>>4,z=spawn.getZ()>>4;p.setPos(spawn.getX(),80,spawn.getZ());p.level().getChunk(x,z);
|
||||
h.assertTrue(act(p,"claim",x,z).equals("spawn")&&s.count(p.getUUID())==0,"Spawn reservation cannot become private");h.succeed();
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.demeure.DemeureService;
|
||||
import fr.koka.sanctuary.arena.*;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.gems.Gems;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class ClaimBattle278GameTests {
|
||||
private static final class Fixture implements AutoCloseable{
|
||||
final GameTestHelper h;final ChunkClaims.Key key;final ChunkClaims claims;final ServerPlayer owner,challenger;
|
||||
final List<ServerPlayer> players=new ArrayList<>();final long paid;
|
||||
Fixture(GameTestHelper h,int x)throws Exception{
|
||||
this.h=h;key=new ChunkClaims.Key(x,700);owner=player();challenger=player();claims=ChunkPropertyService.store(h.getLevel().getServer());
|
||||
var tx=claims.prepare(owner.getUUID(),"claim",key,claims.revision());claims.recover(owner.getUUID(),tx.receipt().toString());
|
||||
DemeureService.record(owner,owner.blockPosition(),1);DemeureService.record(challenger,challenger.blockPosition(),1);
|
||||
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),8));h.assertTrue(ChunkPropertyService.act(owner,"protect",key,claims.revision()).equals("protected"),"Fixture protection is paid through the real service");paid=claims.get(key).until();
|
||||
}
|
||||
ServerPlayer player()throws Exception{
|
||||
var p=Progression003GameTests.player(h);var server=h.getLevel().getServer();var identities=ProgressionService.session(server).inhabitants;
|
||||
identities.create(p.getUUID(),"Battle"+p.getStringUUID().substring(0,8),"",identities.available().getFirst());identities.arrive(p.getUUID());
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);p.setExperienceLevels(20);
|
||||
h.assertTrue(ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()),"Purchase actual Atlas");FamiliarExperience.setPlayerXp(p,500);
|
||||
p.setPos(key.x()*16+2+players.size()*2,80,key.z()*16+2);p.setNoGravity(true);p.level().getChunk(key.x(),key.z());p.getAbilities().invulnerable=false;
|
||||
p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
|
||||
server.getPlayerList().getPlayers().add(p);server.getPlayerList().getPlayersByUUID().put(p.getUUID(),p);players.add(p);return p;
|
||||
}
|
||||
ClaimDuels.Offer propose(){h.assertTrue(ClaimDuels.propose(challenger,key,claims.revision()).equals("sent"),"Challenger explicitly proposes the immutable property stake");return ClaimDuels.at(owner.level().getServer(),key);}
|
||||
Arenas.Match agree(){var o=propose();h.assertTrue(ClaimDuels.accept(owner,o.id).equals("accepted"),"Only the owner explicitly accepts the same stake");return o.match;}
|
||||
void start(Arenas.Match m){for(var p:List.of(owner,challenger))Arenas.ready(m,p);h.assertTrue(Arenas.start(m,challenger).equals("started"),"Both Ready confirmations are required by native Battle");}
|
||||
void fight(Arenas.Match m){start(m);m.until=FamiliarBattle.now();Arenas.tick(owner.level().getServer());h.assertTrue(m.phase==Arenas.Phase.FIGHT,"Native countdown transitions to combat");}
|
||||
public void close(){for(var p:players){var m=Arenas.match(p);if(m!=null)Arenas.finish(m,null,"interrupted");var o=ClaimDuels.at(p.level().getServer(),key);if(o!=null&&o.pending())ClaimDuels.cancel(p,o.id);p.level().getServer().getPlayerList().getPlayers().remove(p);p.level().getServer().getPlayerList().getPlayersByUUID().remove(p.getUUID());}}
|
||||
}
|
||||
@GameTest public void invitationAndConsentCannotBeForged(GameTestHelper h)throws Exception{
|
||||
try(var f=new Fixture(h,700)){
|
||||
var third=f.player();var o=f.propose();
|
||||
h.assertTrue(Arenas.match(f.challenger)==null&&!ClaimDuels.locked(f.owner.level().getServer(),f.key)&&f.claims.get(f.key).owner().equals(f.owner.getUUID()),"A proposal alone neither starts combat nor moves ownership");
|
||||
h.assertTrue(ClaimDuels.accept(f.challenger,o.id).equals("unavailable")&&ClaimDuels.accept(third,o.id).equals("unavailable"),"Neither challenger nor third party can forge the owner's agreement");
|
||||
h.assertTrue(ClaimDuels.cancel(third,o.id).equals("unavailable"),"A third party cannot decide the proposal");
|
||||
h.assertTrue(!ChunkPropertyService.transferDuel(f.owner.level().getServer(),f.key,f.owner.getUUID(),f.challenger.getUUID(),f.paid),"An ordinary result without durable consent cannot authorize transfer");
|
||||
h.assertTrue(ClaimDuels.cancel(f.owner,o.id).equals("declined")&&f.claims.get(f.key).owner().equals(f.owner.getUUID()),"Owner refusal leaves claim and paid coverage intact");
|
||||
DemeureService.record(third,third.blockPosition(),1);h.assertTrue(ClaimDuels.propose(third,f.key,f.claims.revision()).equals("sent"),"A refusal does not reserve the property against another resident");
|
||||
var latest=ClaimDuels.at(f.owner.level().getServer(),f.key);ClaimDuels.cancel(f.owner,latest.id);
|
||||
h.assertTrue(!latest.id.equals(o.id)&&ClaimDuels.at(f.owner.level().getServer(),f.key).id.equals(latest.id),"Reopening shows the most recent completed proposal");
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest public void realPlayerBattleWinnerReceivesOnlyTheAgreedClaim(GameTestHelper h)throws Exception{
|
||||
try(var f=new Fixture(h,702)){
|
||||
var third=f.player();var m=f.agree();
|
||||
h.assertTrue(m.rules.mode()==Arenas.Mode.PLAYERS&&m.rules.duel()&&m.entry==0&&m.entrants.size()==2,"Real private player duel without extra entry charge");
|
||||
h.assertTrue(Arenas.join(m,third).equals("unavailable")&&Arenas.start(m,f.challenger).equals("not_ready"),"No third entrant and no start before both Ready");
|
||||
h.assertTrue(FamiliarExperience.playerXp(f.challenger)==500&&FamiliarExperience.playerXp(f.owner)==500,"Consent takes no XP or new sapphires");
|
||||
f.fight(m);f.owner.setInvulnerableTime(0);
|
||||
h.assertTrue(f.owner.hurtServer(f.owner.level(),f.owner.damageSources().playerAttack(f.challenger),100),"Native PvP damage reaches the agreed rival");
|
||||
h.assertTrue(!f.owner.isAlive()&&m.phase==Arenas.Phase.FINISHED&&m.winner.equals(f.challenger.getUUID()),"Native death resolves Battle");
|
||||
h.assertTrue(f.claims.get(f.key).owner().equals(f.challenger.getUUID())&&f.claims.get(f.key).until()==f.paid,"Winner receives the exact claim with the original paid expiry");
|
||||
h.assertTrue(f.claims.count(f.owner.getUUID())==0&&f.claims.count(f.challenger.getUUID())==1&&!ClaimDuels.locked(f.owner.level().getServer(),f.key),"Settlement updates both counts and releases the stake");
|
||||
long revision=f.claims.revision();Arenas.finish(m,f.challenger.getUUID(),"death");h.assertTrue(f.claims.revision()==revision,"Repeated finish cannot repeat the transfer");
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest public void defenderKeepsProtectionWhenChallengerForfeitsAfterLaunch(GameTestHelper h)throws Exception{
|
||||
try(var f=new Fixture(h,704)){
|
||||
var m=f.agree();f.start(m);Arenas.withdraw(f.challenger);
|
||||
h.assertTrue(m.phase==Arenas.Phase.FINISHED&&m.winner.equals(f.owner.getUUID()),"Battle forfeit after launch awards defender victory");
|
||||
h.assertTrue(f.claims.get(f.key).owner().equals(f.owner.getUUID())&&f.claims.get(f.key).until()==f.paid,"Defender victory never resets or consumes paid protection");
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest public void cancellationAndDrawLeaveOwnershipUnchanged(GameTestHelper h)throws Exception{
|
||||
try(var f=new Fixture(h,706)){
|
||||
var m=f.agree();h.assertTrue(ClaimDuels.cancel(f.owner,m.id).equals("cancelled")&&m.phase==Arenas.Phase.FINISHED,"Either participant can cancel before launch");
|
||||
h.assertTrue(f.claims.get(f.key).owner().equals(f.owner.getUUID())&&!ClaimDuels.locked(f.owner.level().getServer(),f.key),"Cancelled mutual agreement grants no claim");
|
||||
}
|
||||
try(var f=new Fixture(h,707)){
|
||||
var m=f.agree();f.fight(m);m.until=FamiliarBattle.now();Arenas.tick(f.owner.level().getServer());
|
||||
h.assertTrue(m.phase==Arenas.Phase.FINISHED&&m.winner==null&&m.reason.equals("draw"),"Native time limit without elimination is a draw");
|
||||
h.assertTrue(f.claims.get(f.key).owner().equals(f.owner.getUUID())&&f.claims.get(f.key).until()==f.paid,"A draw transfers no property");
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest public void changedOrExpiredProposalRequiresNewConsent(GameTestHelper h)throws Exception{
|
||||
try(var f=new Fixture(h,708)){
|
||||
var o=f.propose();f.owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),8));
|
||||
h.assertTrue(ChunkPropertyService.act(f.owner,"protect",f.key,f.claims.revision()).equals("protected"),"An unaccepted invitation does not freeze the owner's rights");
|
||||
h.assertTrue(ClaimDuels.accept(f.owner,o.id).equals("changed")&&Arenas.match(f.owner)==null,"Changed protection terms cannot reuse previous challenger consent");
|
||||
}
|
||||
try(var f=new Fixture(h,709)){
|
||||
var o=f.propose();var deadline=ClaimDuels.Offer.class.getDeclaredField("expires");deadline.setAccessible(true);deadline.setLong(o,FamiliarBattle.now()-1);
|
||||
h.assertTrue(ClaimDuels.accept(f.owner,o.id).equals("unavailable")&&f.claims.get(f.key).owner().equals(f.owner.getUUID()),"An expired invitation cannot arm a transfer");
|
||||
}h.succeed();
|
||||
}
|
||||
@GameTest public void ordinaryBattleRetainsPublicRosterAndNeverTransfersProperty(GameTestHelper h)throws Exception{
|
||||
try(var f=new Fixture(h,710)){
|
||||
var third=f.player();h.assertTrue(Arenas.create(f.owner,new Arenas.Rules(Arenas.Mode.PLAYERS,false,true,48,180),-1,0).equals("created"),"Ordinary public Battle still opens");
|
||||
var m=Arenas.match(f.owner);h.assertTrue(Arenas.join(m,f.challenger).equals("joined")&&Arenas.join(m,third).equals("joined"),"Public arena still accepts a third participant");
|
||||
for(var e:m.entrants.values())Arenas.ready(m,e.player);h.assertTrue(Arenas.start(m,f.owner).equals("started"),"Ordinary unanimous start stays available");
|
||||
Arenas.eliminate(f.owner,"forfeit");Arenas.eliminate(third,"forfeit");
|
||||
h.assertTrue(m.winner.equals(f.challenger.getUUID())&&f.claims.get(f.key).owner().equals(f.owner.getUUID()),"Winning another Battle never creates a property entitlement");
|
||||
}h.succeed();
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.demeure.DemeureService;
|
||||
import fr.koka.sanctuary.property.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.familiar.FamiliarExperience;
|
||||
import fr.koka.sanctuary.gems.Gems;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
public final class ClaimDemeure277GameTests {
|
||||
private static ServerPlayer resident(GameTestHelper h,int x,int z)throws Exception{
|
||||
var p=Progression003GameTests.player(h);var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;
|
||||
identities.create(p.getUUID(),"Resident"+p.getStringUUID().substring(0,8),"",identities.available().getFirst());identities.arrive(p.getUUID());
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);
|
||||
p.setExperienceLevels(20);if(!ProgressionService.purchase(p,"atlas",ProgressEffects.get(p).revision()))throw new AssertionError("Purchase Atlas");
|
||||
FamiliarExperience.setPlayerXp(p,500);p.setPos(x*16+.5,80,z*16+.5);p.level().getChunk(x,z);return p;
|
||||
}
|
||||
private static String act(ServerPlayer p,String action,ChunkClaims.Key key){return ChunkPropertyService.act(p,action,key,ChunkPropertyService.store(p.level().getServer()).revision());}
|
||||
private static void inhabit(ServerPlayer p,ChunkClaims.Key key,double amount){DemeureService.record(p,new BlockPos(key.x()*16,80,key.z()*16),amount);}
|
||||
@GameTest public void successfulActivityRequiredBeforeFreeAndPaidClaims(GameTestHelper h)throws Exception{
|
||||
var p=resident(h,600,600);var key=new ChunkClaims.Key(600,600);var store=ChunkPropertyService.store(p.level().getServer());long revision=store.revision();
|
||||
h.assertTrue(act(p,"claim",key).equals("habitation_required")&&store.revision()==revision&&FamiliarExperience.playerXp(p)==500,"No activity refuses even the first free claim without mutation");
|
||||
inhabit(p,key,.01);h.assertTrue(act(p,"claim",key).equals("habitation_required"),"A mere passage is insufficient");
|
||||
for(int i=0;i<10;i++){var pos=new BlockPos(600*16+i,80,600*16+2);p.level().setBlockAndUpdate(pos,Blocks.STONE.defaultBlockState());h.assertTrue(p.gameMode.destroyBlock(pos),"Successful native mining contributes to Demeure");}
|
||||
h.assertTrue(ChunkPropertyService.residency(p,key).reason().equals("habitation_ready")&&act(p,"claim",key).equals("claimed"),"Actual successful activities reach native habitation threshold");
|
||||
for(int i=1;i<5;i++){var next=new ChunkClaims.Key(600+i,600);p.level().getChunk(next.x(),next.z());inhabit(p,next,1);h.assertTrue(act(p,"claim",next).equals("claimed"),"Five inhabited claims are still free");}
|
||||
var paid=new ChunkClaims.Key(605,600);p.level().getChunk(paid.x(),paid.z());revision=store.revision();
|
||||
h.assertTrue(act(p,"claim",paid).equals("habitation_required")&&store.revision()==revision&&FamiliarExperience.playerXp(p)==500,"Missing habitation cannot debit the sixth claim");
|
||||
inhabit(p,paid,1);h.assertTrue(act(p,"claim",paid).equals("claimed")&&FamiliarExperience.playerXp(p)==400,"Qualified sixth claim costs exactly one hundred XP");h.succeed();
|
||||
}
|
||||
@GameTest public void strongerLaterResidentCanClaimButInfluenceNeverTransfersOwnedLand(GameTestHelper h)throws Exception{
|
||||
var a=resident(h,620,620);var b=resident(h,620,620);var key=new ChunkClaims.Key(620,620);var store=ChunkPropertyService.store(a.level().getServer());
|
||||
inhabit(a,key,1);inhabit(b,key,1);
|
||||
h.assertTrue(ChunkPropertyService.residency(a,key).reason().equals("habitation_ready")&&act(b,"claim",key).equals("habitation_priority"),"Equal influence favors the first inhabitant");
|
||||
inhabit(b,key,1);long revision=store.revision();
|
||||
h.assertTrue(act(a,"claim",key).equals("habitation_priority")&&store.revision()==revision&&FamiliarExperience.playerXp(a)==500,"A stronger later inhabitant removes first resident's claim priority without debit");
|
||||
h.assertTrue(act(b,"claim",key).equals("claimed")&&store.get(key).owner().equals(b.getUUID()),"Later stronger inhabitant actually claims the free spot");
|
||||
inhabit(a,key,4);h.assertTrue(act(a,"claim",key).equals("taken")&&store.get(key).owner().equals(b.getUUID()),"Increasing influence cannot seize a claim that was already purchased");
|
||||
b.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),8));h.assertTrue(act(b,"protect",key).equals("protected")&&ChunkPropertyService.sapphires(b)==0,"Current owner renews protection even after losing influence leadership");
|
||||
h.assertTrue(ChunkPropertyService.multiplier(b,b.level(),b.blockPosition())==2&&ChunkPropertyService.multiplier(a,a.level(),a.blockPosition())==.5f,"Paid mining effects still follow legal ownership");h.succeed();
|
||||
}
|
||||
@GameTest public void previousClaimsWithoutHabitationKeepTheirRights(GameTestHelper h)throws Exception{
|
||||
var p=resident(h,640,640);var key=new ChunkClaims.Key(640,640);var store=ChunkPropertyService.store(p.level().getServer());
|
||||
var tx=store.prepare(p.getUUID(),"claim",key,store.revision());store.recover(p.getUUID(),tx.receipt().toString());
|
||||
h.assertTrue(ChunkPropertyService.residency(p,key).reason().equals("habitation_required"),"Legacy fixture has no retroactive Demeure evidence");
|
||||
p.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),8));h.assertTrue(act(p,"protect",key).equals("protected")&&store.get(key).owner().equals(p.getUUID()),"Previously acquired claims stay owned and renewable");h.succeed();
|
||||
}
|
||||
@GameTest public void inaccessibleChunksRevealNoResidentAndResidencyCodecIsBounded(GameTestHelper h)throws Exception{
|
||||
var a=resident(h,660,660);var b=resident(h,760,760);var key=new ChunkClaims.Key(660,660);inhabit(a,key,1);
|
||||
var hidden=ChunkPropertyService.residency(b,key);h.assertTrue(hidden.reason().equals("undiscovered")&&hidden.name().isEmpty(),"An arbitrary refresh cannot disclose unknown habitation");
|
||||
var visible=ChunkPropertyService.residency(a,key);var buffer=new net.minecraft.network.RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(),h.getLevel().registryAccess());
|
||||
try{ChunkPropertyPayloads.Residency.CODEC.encode(buffer,visible);h.assertTrue(ChunkPropertyPayloads.Residency.CODEC.decode(buffer).equals(visible)&&buffer.readableBytes()==0,"Separate residency codec round trips actual priority and identity");}finally{buffer.release();}
|
||||
boolean refused=false;try{new ChunkPropertyPayloads.Residency(0,0,"invented","",0);}catch(IllegalArgumentException e){refused=true;}h.assertTrue(refused,"Unknown status cannot enter the presentation contract");
|
||||
AptitudeService.set(a,new AptitudeService.Set("atlas",false,AptitudeService.preferences(a).disabledMask()));
|
||||
h.assertTrue(ChunkPropertyService.residency(a,key).name().isEmpty()&&act(a,"claim",key).equals("atlas_required"),"No residency or claim access before an active purchased Atlas");h.succeed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.property.*;
|
||||
import fr.koka.sanctuary.progression.ProgressionService;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
|
||||
public final class ClaimMap273GameTests {
|
||||
@GameTest public void offlineIdentityColorAndBoundedOverlayCodec(GameTestHelper h)throws Exception{
|
||||
var viewer=Progression003GameTests.player(h);var session=ProgressionService.session(h.getLevel().getServer());
|
||||
UUID offline=UUID.randomUUID();int color=session.inhabitants.available().getFirst();
|
||||
h.assertTrue(session.inhabitants.create(offline,"OfflineOwner","",color),"Offline identity fixture uses persistent Hello World palette");session.inhabitants.arrive(offline);
|
||||
var key=new ChunkClaims.Key(-20,40);var claim=new ChunkClaims.Claim(key,offline,6000);
|
||||
var paint=ChunkPropertyService.appearance(viewer,claim,1000);
|
||||
h.assertTrue(paint.color()==color&&paint.name().equals("OfflineOwner")&&paint.remaining()==5000,"Offline owner retains their real name, color and protection time");
|
||||
h.assertTrue(ChunkPropertyService.appearance(viewer,claim,7000).remaining()==0,"Expired appearance retains identity with no protection");
|
||||
var payload=new ChunkPropertyPayloads.Overlay(42,List.of(key),List.of(paint));
|
||||
var buffer=new RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(),h.getLevel().registryAccess());
|
||||
try{ChunkPropertyPayloads.Overlay.CODEC.encode(buffer,payload);h.assertTrue(ChunkPropertyPayloads.Overlay.CODEC.decode(buffer).equals(payload),"Bounded packet preserves exact owner RGB and chunk coordinates");}finally{buffer.release();}
|
||||
boolean refused=false;try{new ChunkPropertyPayloads.Overlay(42,List.of(new ChunkClaims.Key(0,0)),List.of(paint));}catch(IllegalArgumentException expected){refused=true;}
|
||||
h.assertTrue(refused,"No claim may be sent outside its transmitted map tiles");
|
||||
refused=false;try{new ChunkPropertyPayloads.Overlay(42,java.util.stream.IntStream.range(0,65).mapToObj(i->new ChunkClaims.Key(i,0)).toList(),List.of());}catch(IllegalArgumentException expected){refused=true;}
|
||||
h.assertTrue(refused,"Overlay batches are limited to 64 tiles");h.succeed();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -24,7 +24,9 @@ public final class Companion019GameTests {
|
||||
@GameTest public void catalogMatchesEveryNativeEgg(GameTestHelper h){
|
||||
var found=new HashSet<CompanionType>();
|
||||
for(var item:BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem){
|
||||
var type=CompanionType.find(BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(new ItemStack(item))).toString());
|
||||
var id=BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(new ItemStack(item)));
|
||||
if(!id.getNamespace().equals("minecraft"))continue;
|
||||
var type=CompanionType.find(id.toString());
|
||||
h.assertTrue(type!=null,"Each native egg has a configured power definition");found.add(type);
|
||||
}
|
||||
h.assertTrue(found.size()==88&&found.size()==CompanionType.values().length,"Exactly 88 native familiar species");h.succeed();
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.whale.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class FlyingWhale290GameTests {
|
||||
@GameTest public void cloudKeepsHistoricalAttributesAndGhostRules(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(whale.getMaxHealth() == 500 && whale.getAttributeValue(Attributes.FOLLOW_RANGE) == 520,
|
||||
"Historical internal health and player range are preserved");
|
||||
h.assertTrue(whale.getBbWidth() == 36 && whale.getBbHeight() == 11 && whale.isNoGravity()
|
||||
&& whale.isInvulnerable() && whale.noPhysics && whale.fireImmune(), "Giant flying cloud is invulnerable and intangible");
|
||||
h.assertTrue(!whale.blocksBuilding && !whale.isAttackable() && !whale.isPickable()
|
||||
&& !whale.isPushable() && !whale.canBeHitByProjectile() && !whale.isPushedByFluid(),
|
||||
"Whale cannot obstruct, attack or intercept projectiles");
|
||||
float health = whale.getHealth();
|
||||
h.assertTrue(!whale.hurtServer(h.getLevel(), h.getLevel().damageSources().genericKill(), 1000)
|
||||
&& whale.getHealth() == health, "Even direct server damage is refused");
|
||||
whale.push(2, 3, 4);
|
||||
h.assertTrue(whale.getDeltaMovement().equals(Vec3.ZERO) && !whale.removeWhenFarAway(1_000_000),
|
||||
"No knockback or vanilla distance despawn");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void nativeCollisionDoesNotBreakTerrain(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
var pos = h.absolutePos(new BlockPos(2, 2, 2));
|
||||
h.getLevel().setBlockAndUpdate(pos, Blocks.STONE.defaultBlockState());
|
||||
whale.setPos(Vec3.atCenterOf(pos).add(-2, 0, 0));
|
||||
var start = whale.position();
|
||||
whale.move(MoverType.SELF, new Vec3(4, 0, 0));
|
||||
h.assertTrue(whale.position().distanceTo(start.add(4, 0, 0)) < .00001
|
||||
&& h.getLevel().getBlockState(pos).is(Blocks.STONE), "Native movement passes through stone without editing it");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void onlyNamedWhalesPersistAndNativeReloadRestoresGhostRules(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(!whale.shouldBeSaved(), "Unnamed visitors are transient, as in 26.2");
|
||||
whale.setCustomName(Component.literal("Nuage")); whale.seedOrbit(2.25);
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
|
||||
h.assertTrue(whale.shouldBeSaved() && whale.save(output), "Named whale can be saved natively");
|
||||
var saved = output.buildResult();
|
||||
h.assertTrue(saved.getDoubleOr("OrbitAngle", 0) == 2.25, "Historical orbit angle survives native save");
|
||||
saved.putBoolean("NoGravity", false); saved.putBoolean("Invulnerable", false);
|
||||
var input = TagValueInput.create(ProblemReporter.DISCARDING, h.getLevel().registryAccess(), saved);
|
||||
var loaded = EntityType.loadEntityRecursive(input, h.getLevel(), EntitySpawnReason.LOAD, e -> e);
|
||||
h.assertTrue(loaded instanceof FlyingWhaleEntity ghost && ghost.getUUID().equals(whale.getUUID())
|
||||
&& ghost.getName().getString().equals("Nuage") && ghost.isNoGravity() && ghost.isInvulnerable()
|
||||
&& ghost.noPhysics, "Identity, name and ghost physics survive a native save/reload");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void scheduleHasThreeWindowsAndNoCrossServerStaticCounter(GameTestHelper h) {
|
||||
var schedule = new FlyingWhaleSpawner.Schedule();
|
||||
for (int i = 0; i < 399; i++) h.assertTrue(!schedule.due(), "No spawn check before 400 ticks");
|
||||
h.assertTrue(schedule.due() && !schedule.due(), "Check at 400 ticks then reset");
|
||||
h.assertTrue(schedule.window(0) == 0 && schedule.available(0), "First window starts available");
|
||||
schedule.spawned(0);
|
||||
h.assertTrue(!schedule.available(0) && schedule.window(7999) == 0 && !schedule.available(0), "One success per daily window");
|
||||
h.assertTrue(schedule.window(8000) == 1 && schedule.available(1), "Second window starts at 8000");
|
||||
schedule.spawned(1);
|
||||
h.assertTrue(schedule.window(16000) == 2 && schedule.available(2), "Third window starts at 16000");
|
||||
schedule.spawned(2);
|
||||
h.assertTrue(schedule.window(24000) == 0 && schedule.available(0), "New day resets windows");
|
||||
h.assertTrue(new FlyingWhaleSpawner.Schedule().available(0), "A new server has an independent schedule");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void naturalPlacementKeepsOverworldAndHeightBounds(GameTestHelper h) {
|
||||
var level = h.getLevel(); var random = RandomSource.create(290);
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMinY() + 18, 0), random), "Lower altitude margin is excluded");
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMaxY() - 18, 0), random), "Upper altitude margin is excluded");
|
||||
h.assertTrue(FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMinY() + 30, 0), random), "Free Overworld altitude is accepted");
|
||||
var nether = level.getServer().getLevel(Level.NETHER);
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, nether, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, 64, 0), random), "Natural visitors stay in the Overworld");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100) public void eggQuotaNaturalSpawnAndFlightUseActualServerEntities(GameTestHelper h) {
|
||||
var level = h.getLevel(); var server = level.getServer(); var p = Progression003GameTests.player(h);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(FlyingWhales.SPAWN_EGG, 3));
|
||||
var at = p.blockPosition().below();
|
||||
level.setBlockAndUpdate(at, Blocks.STONE.defaultBlockState());
|
||||
var context = new UseOnContext(p, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(Vec3.atCenterOf(at).add(0, .5, 0), Direction.UP, at, false));
|
||||
h.assertTrue(SpawnEggItem.getType(p.getMainHandItem()) == FlyingWhales.TYPE, "Native egg mapping resolves the whale");
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 1 && p.getMainHandItem().getCount() == 2,
|
||||
"Real survival egg use spawns one whale and consumes one egg");
|
||||
var first = level.getEntities(FlyingWhales.TYPE, Entity::isAlive).getFirst();
|
||||
first.setNoAi(true);
|
||||
// Its 36-block hitbox must not overlap neighbouring tests' cleanup areas.
|
||||
first.setPos(p.position().add(0, 80, 0));
|
||||
var nether = server.getLevel(Level.NETHER); nether.setChunkForced(0, 0, true); nether.getChunk(0, 0);
|
||||
nether.waitForEntities(new net.minecraft.world.level.ChunkPos(0, 0), 0);
|
||||
var other = FlyingWhales.TYPE.create(nether, EntitySpawnReason.COMMAND);
|
||||
// Keep the quota fixture inside its single forced chunk during the delay.
|
||||
other.setNoAi(true); other.setPos(8, 64, 8); nether.addFreshEntity(other);
|
||||
h.runAfterDelay(5, () -> {
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2,
|
||||
"Both quota fixtures remain loaded before egg use; firstRemoved=" + first.isRemoved()
|
||||
+ ", netherRemoved=" + other.isRemoved());
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2 && p.getMainHandItem().getCount() == 2,
|
||||
"Two across dimensions block the third egg without consuming it; count=" + FlyingWhales.countLoaded(server)
|
||||
+ ", eggs=" + p.getMainHandItem().getCount() + ", netherRemoved=" + other.isRemoved());
|
||||
var third = FlyingWhales.TYPE.create(level, EntitySpawnReason.COMMAND);
|
||||
third.setPos(p.position()); level.addFreshEntity(third);
|
||||
h.assertTrue(third.isRemoved() && FlyingWhales.countLoaded(server) == 2, "Commands cannot exceed the shared limit");
|
||||
other.discard();
|
||||
nether.setChunkForced(0, 0, false);
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2 && p.getMainHandItem().getCount() == 1,
|
||||
"Removing a whale releases its place immediately");
|
||||
level.getEntities(FlyingWhales.TYPE, Entity::isAlive).forEach(Entity::discard);
|
||||
// Load only development chunks for this deterministic radius test.
|
||||
for (int x = -10; x <= 10; x++) for (int z = -10; z <= 10; z++)
|
||||
level.getChunk(p.blockPosition().getX() / 16 + x, p.blockPosition().getZ() / 16 + z);
|
||||
h.assertTrue(FlyingWhaleSpawner.spawnFor(level, p, RandomSource.create(290)), "Actual natural spawner creates a visitor");
|
||||
var whale = level.getEntities(FlyingWhales.TYPE, Entity::isAlive).getFirst();
|
||||
double radius = whale.position().subtract(p.position()).horizontalDistance();
|
||||
h.assertTrue(radius >= 92 && radius < 138 && whale.getY() >= level.getMinY() + 22
|
||||
&& whale.getY() <= level.getMaxY() - 22, "Native spawn retains historical radius and altitude");
|
||||
whale.setPos(p.position().add(0, 30, 0));
|
||||
var start = whale.position();
|
||||
h.runAfterDelay(30, () -> {
|
||||
h.assertTrue(whale.position().distanceToSqr(start) > .001 && whale.isNoGravity() && whale.noPhysics,
|
||||
"Historical AI actually moves through the air on native server ticks");
|
||||
whale.tickCount = 14400;
|
||||
});
|
||||
h.runAfterDelay(33, () -> {
|
||||
h.assertTrue(whale.isRemoved() && FlyingWhales.countLoaded(server) == 0,
|
||||
"Historical twelve minute lifetime fades and releases quota");
|
||||
h.succeed();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.creeper.HappyCreepers;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
|
||||
public final class HappyCreeperGameTests {
|
||||
@GameTest public void ordinaryHeadGestureConsumesAndReturnsTheSameFlower(GameTestHelper h) {
|
||||
var p = player(h); var mob = creeper(h); mob.setPos(p.position().add(0, 0, 1));
|
||||
var flower = new ItemStack(Items.POPPY, 3);
|
||||
flower.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Rose"));
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, flower); p.setShiftKeyDown(true);
|
||||
h.assertTrue(MobHeadService.equip(p, mob), "Existing head gesture equips the flower");
|
||||
h.assertTrue(flower.getCount() == 2 && HappyCreepers.isHappy(mob), "One real flower is consumed and calms the creeper");
|
||||
h.assertTrue(ItemStack.matches(HeadCosmeticService.appearance(mob), flower.copyWithCount(1)), "Exact flower and components are synchronized");
|
||||
h.assertTrue(mob.requiresCustomPersistence(), "Equipped creeper does not despawn at distance");
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
h.assertTrue(MobHeadService.equip(p, mob) && !HappyCreepers.isHappy(mob), "Empty-hand head gesture removes happiness");
|
||||
h.assertTrue(p.getInventory().countItem(Items.POPPY) == 1 && p.getInventory().getNonEquipmentItems().stream()
|
||||
.anyMatch(s -> s.is(Items.POPPY) && ItemStack.matches(s, flower.copyWithCount(1))), "The same named flower returns once");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void everyTaggedFlowerWorksButOtherHeadObjectsDoNot(GameTestHelper h) {
|
||||
var mob = creeper(h); int flowers = 0;
|
||||
for (var item : BuiltInRegistries.ITEM) {
|
||||
var stack = new ItemStack(item);
|
||||
if (!HappyCreepers.isFlower(stack)) continue;
|
||||
HeadEquipment.set(mob, stack);
|
||||
h.assertTrue(HappyCreepers.isHappy(mob), "Tagged flower is accepted: " + item); flowers++;
|
||||
}
|
||||
h.assertTrue(flowers >= 15, "The exact Minecraft version resolves its flower tag");
|
||||
for (var item : java.util.List.of(Items.FLOWER_POT, Items.OAK_LEAVES, Items.WHEAT_SEEDS, Items.STONE)) {
|
||||
HeadEquipment.set(mob, new ItemStack(item));
|
||||
h.assertTrue(!HappyCreepers.isHappy(mob), "A different head object keeps the ordinary temperament");
|
||||
}
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void flowerImmediatelyCancelsAnAlreadyIgnitedFuse(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h);
|
||||
mob.setTarget(p); mob.ignite();
|
||||
for (int i = 0; i < 20; i++) mob.tick();
|
||||
h.assertTrue(mob.isIgnited() && mob.getSwelling(1) > 0, "Real native fuse is already progressing");
|
||||
HeadEquipment.set(mob, new ItemStack(Items.DANDELION));
|
||||
h.assertTrue(!mob.isIgnited() && mob.getTarget() == null && mob.getSwelling(1) == 0, "Equipment immediately clears target, ignition and accumulated fuse");
|
||||
for (int i = 0; i < 60; i++) { mob.setTarget(p); mob.setSwellDir(1); mob.ignite(); mob.tick(); }
|
||||
h.assertTrue(mob.isAlive() && mob.getTarget() == null && !mob.isIgnited() && mob.getSwellDir() < 0 && mob.getSwelling(1) == 0,
|
||||
"Repeated targeting and ignition cannot make a happy creeper explode");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.tick();
|
||||
h.assertTrue(mob.isAlive() && !mob.isIgnited() && mob.getSwelling(1) == 0, "Removing the flower cannot resume an old fuse");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void happyCreeperRejectsRetaliationAndKeepsItsOrdinaryMovement(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h); mob.setNoAi(false);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.CORNFLOWER));
|
||||
mob.hurtServer(h.getLevel(), mob.damageSources().playerAttack(p), 1);
|
||||
for (int i = 0; i < 40; i++) mob.tick();
|
||||
h.assertTrue(mob.getTarget() == null && !mob.isAggressive() && mob.isAlive() && !mob.isNoAi(), "Damage does not restore hostility or disable the native AI");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.setTarget(p);
|
||||
h.assertTrue(mob.getTarget() == p, "Removing the flower restores normal targeting");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void flintAndFireChargeCannotIgniteOrSpendItemsWhileHappy(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.BLUE_ORCHID));
|
||||
for (var hand : InteractionHand.values()) for (var item : java.util.List.of(Items.FLINT_AND_STEEL, Items.FIRE_CHARGE)) {
|
||||
var stack = new ItemStack(item, item == Items.FIRE_CHARGE ? 3 : 1);
|
||||
var before = stack.copy(); p.setItemInHand(hand, stack);
|
||||
mob.interact(p, hand, net.minecraft.world.phys.Vec3.ZERO);
|
||||
h.assertTrue(!mob.isIgnited() && ItemStack.matches(stack, before), "Igniter remains intact in either hand");
|
||||
}
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void chargedHappyCreeperKeepsIdentityFlowerAndChargeAfterNativeReload(GameTestHelper h) {
|
||||
var mob = creeper(h);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.PINK_TULIP));
|
||||
mob.thunderHit(h.getLevel(), EntityTypes.LIGHTNING_BOLT.create(h.getLevel(), EntitySpawnReason.COMMAND));
|
||||
h.assertTrue(HappyCreepers.isHappy(mob) && mob.isPowered(), "Lightning charges a happy creeper without changing its temperament");
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
|
||||
h.assertTrue(mob.save(output), "Vanilla entity saves normally");
|
||||
var loaded = (Creeper) EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,
|
||||
h.getLevel().registryAccess(), output.buildResult()), h.getLevel(), EntitySpawnReason.LOAD, e -> e);
|
||||
h.assertTrue(loaded.getType() == EntityTypes.CREEPER && loaded.getUUID().equals(mob.getUUID()) && loaded.isPowered()
|
||||
&& HappyCreepers.isHappy(loaded) && HeadEquipment.get(loaded).is(Items.PINK_TULIP), "Existing head save preserves the same creeper, flower and independent electric charge");
|
||||
loaded.ignite(); loaded.tick(); h.assertTrue(!loaded.isIgnited() && loaded.isAlive(), "Reloaded flower still prevents explosions");
|
||||
HeadEquipment.set(loaded, ItemStack.EMPTY);
|
||||
h.assertTrue(loaded.isPowered() && !HappyCreepers.isHappy(loaded), "Taking the flower leaves the electric charge intact");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); loaded.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void ordinaryAndUnequippedCreepersStillExplodeNormally(GameTestHelper h) {
|
||||
for (boolean previouslyHappy : new boolean[]{false, true}) {
|
||||
var mob = creeper(h);
|
||||
if (previouslyHappy) { HeadEquipment.set(mob, new ItemStack(Items.POPPY)); HeadEquipment.set(mob, ItemStack.EMPTY); }
|
||||
mob.ignite(); h.assertTrue(mob.isIgnited(), "Normal ignition is available without a flower");
|
||||
for (int i = 0; i < 31 && !mob.isRemoved(); i++) mob.tick();
|
||||
h.assertTrue(mob.isRemoved(), "Native fuse and explosion still finish without a flower");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
private static Creeper creeper(GameTestHelper h) {
|
||||
var mob = EntityTypes.CREEPER.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
mob.setNoAi(true); mob.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(2, 2, 2))); return mob;
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h) {
|
||||
var p = Progression003GameTests.player(h); p.setUUID(java.util.UUID.randomUUID());
|
||||
p.setAttached(ProgressionService.RECORD, ProgressRecord.INITIAL.json()); ProgressEffects.apply(p, ProgressRecord.INITIAL); return p;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user