Integrate Happy Creepers and prepare the complete beta.293 update
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-10-26 04:23:06 +01:00
parent 97c601cd7f
commit 678d77fb91
21 changed files with 615 additions and 9 deletions
+8
View File
@@ -1,3 +1,11 @@
# 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.
+7
View File
@@ -7,6 +7,13 @@ 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 — livraison intégrée en cours
[Préparation de la livraison complète](docs/release-beta293.md) : Happy Creepers
de la branche dédiée, baleines fantômes, logo packwiz et quatre mods facultatifs.
Le serveur Beta, le téléchargement Web et la même instance Prism sont les cibles
demandées ; les étapes réellement effectuées sont consignées dans le reçu.
## 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).
+12
View File
@@ -366,3 +366,15 @@ 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.
+99
View File
@@ -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éé.
+19
View File
@@ -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`.
+80
View File
@@ -0,0 +1,80 @@
# 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 utilisera **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
Validation et livraison en cours. Les hashes, sauvegardes et contrôles réels
seront consignés ici après exécution ; cet état ne vaut pas une publication.
Les journaux de cette livraison sont sous `build/release293/`.
+1 -1
View File
@@ -8,7 +8,7 @@ Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
## Préparer le kit
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
Le kit de la préparation courante est `build/Sanctuary-Server-beta.292.zip` ; les manifestes locaux sont
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.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.292
pack_version=beta.292
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
+5 -2
View File
@@ -152,6 +152,9 @@ 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') }
@@ -286,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 = [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']
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)
@@ -707,7 +710,7 @@ 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)
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 ->
@@ -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);
}
}
@@ -107,10 +107,19 @@ public final class FlyingWhale290GameTests {
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);
other.setPos(0, 64, 0); nether.addFreshEntity(other);
// 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)
@@ -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;
}
}
@@ -8,7 +8,7 @@
"entrypoints": {
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
"fabric-gametest": ["fr.koka.sanctuary.gametest.FlyingWhale290GameTests", "fr.koka.sanctuary.gametest.Hunter286GameTests", "fr.koka.sanctuary.gametest.Smoker285GameTests", "fr.koka.sanctuary.gametest.Boomer284GameTests", "fr.koka.sanctuary.gametest.Spitter283GameTests", "fr.koka.sanctuary.gametest.Charger282GameTests", "fr.koka.sanctuary.gametest.Infected280GameTests", "fr.koka.sanctuary.gametest.Menu275GameTests", "fr.koka.sanctuary.gametest.ClaimMap273GameTests", "fr.koka.sanctuary.gametest.ChunkClaims272GameTests", "fr.koka.sanctuary.gametest.Inhabitant271GameTests","fr.koka.sanctuary.gametest.Property270GameTests","fr.koka.sanctuary.gametest.Providence269GameTests","fr.koka.sanctuary.gametest.Providence268GameTests","fr.koka.sanctuary.gametest.Providence265GameTests","fr.koka.sanctuary.gametest.Insect245GameTests","fr.koka.sanctuary.gametest.Forgerie238GameTests","fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests", "fr.koka.sanctuary.gametest.Waystone239GameTests"],
"fabric-gametest": ["fr.koka.sanctuary.gametest.HappyCreeperGameTests", "fr.koka.sanctuary.gametest.FlyingWhale290GameTests", "fr.koka.sanctuary.gametest.Hunter286GameTests", "fr.koka.sanctuary.gametest.Smoker285GameTests", "fr.koka.sanctuary.gametest.Boomer284GameTests", "fr.koka.sanctuary.gametest.Spitter283GameTests", "fr.koka.sanctuary.gametest.Charger282GameTests", "fr.koka.sanctuary.gametest.Infected280GameTests", "fr.koka.sanctuary.gametest.Menu275GameTests", "fr.koka.sanctuary.gametest.ClaimMap273GameTests", "fr.koka.sanctuary.gametest.ChunkClaims272GameTests", "fr.koka.sanctuary.gametest.Inhabitant271GameTests","fr.koka.sanctuary.gametest.Property270GameTests","fr.koka.sanctuary.gametest.Providence269GameTests","fr.koka.sanctuary.gametest.Providence268GameTests","fr.koka.sanctuary.gametest.Providence265GameTests","fr.koka.sanctuary.gametest.Insect245GameTests","fr.koka.sanctuary.gametest.Forgerie238GameTests","fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests", "fr.koka.sanctuary.gametest.Waystone239GameTests"],
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
},
"mixins": ["sanctuary-gametest.mixins.json"],
@@ -5,4 +5,5 @@ public final class MobHeadState {
public HeadCosmeticState head;
public float yaw,pitch,scale=1;
public boolean active;
public boolean happyCreeper;
}
@@ -14,7 +14,7 @@ public final class HeadEquipment {
public static ItemStack get(LivingEntity e){return e instanceof Player p?AccessoryAccess.of(p).getItem(AccessoryContainer.HEAD):e instanceof Mob m?mob(m).stack:ItemStack.EMPTY;}
public static void set(LivingEntity e,ItemStack stack){
if(e instanceof Player p)AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,stack);
else if(e instanceof Mob m){HeadBlockService.release(m);mob(m).stack=stack;mob(m).saved=null;MobHeadService.track(m);MobHeadService.publish(m);}
else if(e instanceof Mob m){HeadBlockService.release(m);mob(m).stack=stack;mob(m).saved=null;fr.koka.sanctuary.creeper.HappyCreepers.headChanged(m);MobHeadService.track(m);MobHeadService.publish(m);}
}
public static void replace(LivingEntity e,ItemStack expected,ItemStack replacement){
if(e instanceof Player p)AccessoryAccess.of(p).replaceHeadState(expected,replacement);
@@ -0,0 +1,29 @@
package fr.koka.sanctuary.creeper;
import fr.koka.sanctuary.cosmetics.HeadCosmeticService;
import fr.koka.sanctuary.cosmetics.HeadEquipment;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.monster.Creeper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Item;
/** Happiness is derived from the existing authoritative head slot, never saved twice. */
public final class HappyCreepers {
public static final TagKey<Item> FLOWERS = TagKey.create(Registries.ITEM, Identifier.withDefaultNamespace("flowers"));
public interface Access { void sanctuary$calm(); }
private HappyCreepers() {}
public static boolean isFlower(ItemStack stack) { return stack.is(FLOWERS); }
public static boolean isHappy(Creeper creeper) {
return isFlower(creeper.level().isClientSide()
? HeadCosmeticService.appearance(creeper) : HeadEquipment.get(creeper));
}
public static void headChanged(Mob mob) {
if (mob instanceof Creeper creeper && isHappy(creeper)) ((Access) creeper).sanctuary$calm();
}
}
@@ -0,0 +1,61 @@
package fr.koka.sanctuary.mixin;
import fr.koka.sanctuary.creeper.HappyCreepers;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.monster.Creeper;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.*;
@Mixin(Creeper.class)
public abstract class HappyCreeperMixin implements HappyCreepers.Access {
@Shadow @Final private static EntityDataAccessor<Boolean> DATA_IS_IGNITED;
@Shadow private int swell;
@Shadow private int oldSwell;
public void sanctuary$calm() {
var creeper = (Creeper) (Object) this;
if (creeper.getTarget() != null || creeper.isAggressive()) creeper.getNavigation().stop();
creeper.setTarget(null);
creeper.setAggressive(false);
creeper.setSwellDir(-1);
creeper.getEntityData().set(DATA_IS_IGNITED, false);
swell = oldSwell = 0;
}
@Inject(method = "tick", at = @At("HEAD"))
private void sanctuary$stayCalm(CallbackInfo ci) {
if (HappyCreepers.isHappy((Creeper) (Object) this)) sanctuary$calm();
}
@Inject(method = "setTarget", at = @At("HEAD"), cancellable = true)
private void sanctuary$noHostileTarget(LivingEntity target, CallbackInfo ci) {
if (target != null && HappyCreepers.isHappy((Creeper) (Object) this)) ci.cancel();
}
@Inject(method = "setSwellDir", at = @At("HEAD"), cancellable = true)
private void sanctuary$noFuse(int direction, CallbackInfo ci) {
if (direction > 0 && HappyCreepers.isHappy((Creeper) (Object) this)) ci.cancel();
}
@Inject(method = "ignite", at = @At("HEAD"), cancellable = true)
private void sanctuary$noIgnition(CallbackInfo ci) {
if (HappyCreepers.isHappy((Creeper) (Object) this)) ci.cancel();
}
@Inject(method = "explodeCreeper", at = @At("HEAD"), cancellable = true)
private void sanctuary$noExplosion(CallbackInfo ci) {
if (HappyCreepers.isHappy((Creeper) (Object) this)) { sanctuary$calm(); ci.cancel(); }
}
@Inject(method = "mobInteract", at = @At("HEAD"), cancellable = true)
private void sanctuary$keepIgniter(Player player, InteractionHand hand, CallbackInfoReturnable<InteractionResult> cir) {
if (HappyCreepers.isHappy((Creeper) (Object) this) && player.getItemInHand(hand).is(ItemTags.CREEPER_IGNITERS))
cir.setReturnValue(InteractionResult.SUCCESS);
}
}
@@ -0,0 +1,27 @@
package fr.koka.sanctuary.mixin.client;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.client.MobHeadState;
import fr.koka.sanctuary.creeper.HappyCreepers;
import net.minecraft.client.renderer.entity.CreeperRenderer;
import net.minecraft.client.renderer.entity.state.CreeperRenderState;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.monster.Creeper;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.*;
@Mixin(CreeperRenderer.class)
public abstract class HappyCreeperRendererMixin {
@Unique private static final Identifier SANCTUARY_HAPPY = SanctuaryMod.id("textures/entity/creeper/happy.png");
@Inject(method = "extractRenderState(Lnet/minecraft/world/entity/monster/Creeper;Lnet/minecraft/client/renderer/entity/state/CreeperRenderState;F)V", at = @At("TAIL"))
private void sanctuary$happyFace(Creeper creeper, CreeperRenderState state, float partial, CallbackInfo ci) {
((MobHeadState.Access) state).sanctuary$mobHat().happyCreeper = HappyCreepers.isHappy(creeper);
}
@Inject(method = "getTextureLocation(Lnet/minecraft/client/renderer/entity/state/CreeperRenderState;)Lnet/minecraft/resources/Identifier;", at = @At("HEAD"), cancellable = true)
private void sanctuary$texture(CreeperRenderState state, CallbackInfoReturnable<Identifier> cir) {
if (((MobHeadState.Access) state).sanctuary$mobHat().happyCreeper) cir.setReturnValue(SANCTUARY_HAPPY);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -4,6 +4,7 @@
"plugin": "fr.koka.sanctuary.mixin.OptionalModMixins",
"compatibilityLevel": "JAVA_25",
"mixins": [
"HappyCreeperMixin",
"InhabitantAdvancementAccess",
"ChunkMiningSpeedMixin",
"PropertyLevelMixin",
@@ -200,6 +201,7 @@
"NaturalSnowEditMixin"
],
"client": [
"client.HappyCreeperRendererMixin",
"client.ReflectionSceneMixin",
"client.PbrAnimationClockMixin",
"client.PbrAnimationSpriteMixin",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.292"
version = "beta.293"
pack-format = "packwiz:1.1.0"
[index]