Compare commits

...
8 Commits
Author SHA1 Message Date
koka 34cdcfd00e docs: synchronize README through beta.295
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 06:42:57 +01:00
koka c0b6b14e89 docs: record completed beta.295 delivery
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 06:30:17 +01:00
koka 2734c2383a Add the default Sanctuary server and pack favicon (beta.295)
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 06:24:03 +01:00
koka 74dc1265a8 docs: record completed beta.293 delivery
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 04:31:27 +01:00
koka 678d77fb91 Integrate Happy Creepers and prepare the complete beta.293 update
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 04:23:06 +01:00
koka 97c601cd7f Add Sanctuary packwiz branding and optional pack mods (beta.292) 2026-10-26 03:59:53 +01:00
koka a93e261120 feat: import flying ghost whale and native egg (beta.290) 2026-10-26 02:45:21 +01:00
koka 72f1cf50ae docs: record completed beta.289 delivery
Build Sanctuary / build (push) Canceled after 0s
2026-10-26 01:35:04 +01:00
82 changed files with 3802 additions and 52 deletions
+16
View File
@@ -1,3 +1,19 @@
# 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.
+72 -9
View File
@@ -1,16 +1,77 @@
# Sanctuary
[Dernière release : beta.295](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.295) ·
[Reçu de livraison](docs/release-beta295.md) ·
[Installation et mises à jour](docs/packwiz.md).
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
Nord glacial, Sud des canyons, Ouest océanique et Est volcanique.
Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.295 — Serveur Sanctuary dans la liste multijoueur
[Serveur préenregistré et logo du pack](docs/default-server-beta295.md) :
ajout initial de **Sanctuary Beta** sans écraser les favoris personnels,
avec icône 64 × 64 également distribuée au serveur. Intègre la simplification
de `/sanctuary property`. Build, 18 tests serveur et parcours Vulkan FR/EN
réussis. [Livraison complète effectuée](docs/release-beta295.md) : serveur,
téléchargement Web et instance Prism historique en beta.295.
## beta.294 — Une seule commande de propriété (intégrée à beta.295)
[La commande de propriété garde uniquement son nom anglais](docs/property-command-beta294.md) :
`/sanctuary property`, avec `claim`, `protect` et `duel`. Les alias `propriete`
et `propriété` sont retirés. Build, **18 tests serveur ciblés** et assemblage
du pack réussis lors de la préparation locale. Cette version n’a pas été
publiée séparément ; son contenu est livré avec beta.295.
## 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.
@@ -1225,9 +1286,8 @@ explicites, résultat commun, sans récompense économique dans ce prototype.
Le laboratoire utilise un nouveau monde ; aucune mise à jour de Prism ni du
canal public n'est effectuée par ce chantier.
[Dernière release : beta.166](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.166) ·
[Historique des publications beta.144–166](docs/publications-beta144-166.md).
Les releases sont distinctes du canal packwiz, qui reste inchangé lors de ce rattrapage.
Ce rattrapage historique des releases n’avait pas modifié le canal packwiz.
La [direction de travail du 25 septembre](docs/ecosysteme-communaute-economie.md)
pose les liens entre tableau, Gazette, statistiques en BDD, État, navets,
@@ -3691,7 +3751,7 @@ Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembr
| Fabric API | 0.160.5+26.3 |
| Fabric Loom | 1.17.20 |
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
| Sanctuary / pack | beta.166 |
| Sanctuary / pack | beta.295 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -3720,7 +3780,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-beta.166.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-beta.295.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
@@ -3770,11 +3830,14 @@ sauvegarde 26.2 : aucun outil de migration de ses chunks n'est livré ici.
It's Alive !, Only Fun et Master Key restent des modules autonomes prévus par la
vision. Leurs sources 26.2 ne sont pas copiées dans ce socle. Fabric API est
requise ; Demeure appartient au code interne de Sanctuary. Depuis beta.257,
le port source de JEI pour 26.3 et Sound Physics sont des mods client séparés
et facultatifs, comme Sodium. Leurs sources ou procédures de reconstruction
et leurs attributions figurent dans `mods/demeure/`, `mods/jei/` et
`mods/sound-physics-remastered/`.
requise ; Demeure appartient au code interne de Sanctuary. Depuis beta.280,
le port source de JEI pour 26.3 est embarqué dans le JAR Sanctuary pour le
client et le serveur. Sound Physics et Sodium restent facultatifs côté client.
Depuis beta.293, Simple Voice Chat, Bobby, Lithium et Entity Culling sont
aussi facultatifs côté client ; la voix et Lithium font également partie du
profil serveur. Voir [les dépendances du pack](docs/optional-mods-beta292.md).
Les sources ou procédures de reconstruction et les attributions figurent
dans `mods/demeure/`, `mods/jei/` et `mods/sound-physics-remastered/`.
## Travailler par tickets
+55
View File
@@ -1,5 +1,28 @@
# 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
@@ -323,3 +346,35 @@ t-shirt. SHA-256 :
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.
+20 -1
View File
@@ -27,6 +27,25 @@ tasks.register('verifyWebDistribution', Exec) {
}
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.'
@@ -36,7 +55,7 @@ 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')
}
+6
View File
@@ -1958,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 |
+6 -2
View File
@@ -24,8 +24,12 @@ 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 légère contient uniquement `instance.cfg`, `mmc-pack.json`, l'icône et
`minecraft/packwiz-installer-bootstrap.jar`. Elle démarre avec zéro mod local.
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 :
+53
View File
@@ -0,0 +1,53 @@
# PACK-03 — Serveur Sanctuary préenregistré et icône
Ticket du **26 octobre 2026**, branche `codex/default-server`, version `beta.295`.
Le premier démarrage du client ajoute **Sanctuary Beta**, adresse publique
`beta.sanctuary-minecraft.net`, dans la liste multijoueur vanilla. Le mécanisme
est livré dans le JAR : il fonctionne après import du ZIP Prism du site,
installation MRpack et mise à jour d'une instance packwiz existante.
`servers.dat` reste un fichier personnel, jamais géré par packwiz. L'ajout
conserve l'ordre, les noms, réglages et champs inconnus des autres favoris.
Un favori Sanctuary existant est conservé sans doublon, y compris avec le nom
DNS en majuscules, un point final ou les ports 25565/25567. Une entrée cachée
issue de connexion directe devient visible sans être renommée. Avant la
première écriture, une copie exacte est conservée dans
`servers.dat.before-sanctuary-beta`. Un fichier illisible reste intact.
Le marqueur local `.sanctuary/default-server-beta` indique que l'ajout initial
est terminé : le joueur peut ensuite renommer, déplacer ou supprimer le favori.
Il n'est pas recréé à chaque lancement. Aucun téléchargement ni connexion de
jeu automatique n'est ajouté.
Le logo 160 × 160 du pack est redimensionné en PNG 64 × 64, sans nouvelle
illustration. Cette même image sert d'icône initiale du favori et de
`server-icon.png` dans le profil serveur packwiz. Le serveur annonce ainsi
l'icône aux clients lors du ping de la liste multijoueur. Le profil client ne
possède jamais `servers.dat` ; le profil serveur possède explicitement cette
icône. Les mondes et réglages de jeu ne changent pas.
La livraison commune intègre le ticket terminé
[CLAIM-03](property-command-beta294.md) : seule la commande anglaise
`/sanctuary property` et ses sous-commandes sont conservées. Les deux fichiers
de production, les deux fichiers de GameTests et le contrat sont repris
exactement depuis sa préparation locale beta.294, après ses 18 tests serveur
réussis, sans modifier son checkout de travail.
## Vérifications
- `defaultServerListSmoke` : 15 contrôles, dont lecture par `ServerData`
vanilla, icône 64 × 64, favoris existants, sauvegarde exacte, suppression,
absence de doublon et fichier corrompu suivi d'une réparation.
- Parcours client **Vulkan FR/EN**, GUI 3 en 1280 × 800 : réussi en 38 s,
hook réel de démarrage, liste vanilla, nom et icône, ping public 0/20.
Deux captures inspectées ; aucun monde ouvert. Journal et captures sous
`build/release295/` et `mods/sanctuary/build/run/clientGameTest/screenshots/`.
- `./gradlew check build assemblePack assembleServerPack` : réussi en
**3 min 22 s**, avec les contrôles purs et **18 tests serveur ciblés**
`property270,claims272,claims277,claims278`, `sanctuaryAtlasOnly=true`.
Les tests vérifient à nouveau la commande unifiée sur la base commune.
Journal : `build/release295/check-build.log`.
- **Publication effectuée** : Gitea, canal client/serveur, CT 350, téléchargement
Web et instance Prism historique en beta.295. [Reçu complet](release-beta295.md),
sauvegardes et limite de l’instance importée encore ouverte.
+105
View File
@@ -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.
+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éé.
+124
View File
@@ -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.
+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`.
+17
View File
@@ -0,0 +1,17 @@
# Sanctuary beta.295
Le serveur **Sanctuary Beta** est enregistré automatiquement au premier
lancement du client, avec le logo du modpack. Il utilise l'adresse publique
`beta.sanctuary-minecraft.net`. Les autres favoris restent conservés ; vous
pouvez ensuite renommer, déplacer ou supprimer cette entrée.
Le serveur annonce aussi le logo Sanctuary dans la liste multijoueur.
Le même parcours s'applique au ZIP Prism du site, au MRpack et aux instances
existantes qui suivent le canal packwiz.
La commande de propriété est unifiée : **`/sanctuary property`**, avec
`claim`, `protect` et `duel`. Les alias `propriete` et `propriété` sont retirés.
Les règles, prix et protections des claims restent identiques.
Les sauvegardes et paramètres de génération sont conservés ; aucun nouveau
monde n'est nécessaire.
+164
View File
@@ -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.
+109
View File
@@ -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.
+33
View File
@@ -8,6 +8,34 @@ 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
@@ -2166,6 +2194,11 @@ déjà branchée sur le canal stable n'a pas besoin d'être réimportée.
## Données locales et mondes
Depuis beta.295, le premier lancement du client ajoute le serveur Sanctuary
dans les favoris sans remplacer leur liste. Un marqueur local respecte ensuite
les suppressions volontaires ; le profil serveur distribue le logo 64 × 64.
Voir [le contrat](default-server-beta295.md).
Le pack livré possède uniquement les fichiers déclarés dans son index. Il ne
déclare ni `saves/`, ni `options.txt`, `servers.dat`, `screenshots/`, ni les packs
graphiques personnels. L'installation de packwiz et une mise à jour des mods
+6
View File
@@ -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.**
+60
View File
@@ -0,0 +1,60 @@
# CLAIM-03 — Une seule commande de propriété
Ticket local du **26 octobre 2026**, branche `codex/property-command`,
préparation locale `beta.294`.
## Demande et résultat attendu
Les trois variantes de propriété encombrent les commandes proposées au joueur.
Conserver uniquement le nom anglais `/sanctuary property`, cohérent avec les
autres commandes. Retirer les alias `propriete` et `propriété`, y compris pour
les duels, afin qu'aucun enregistrement secondaire ne les réintroduise.
Commandes conservées :
- `/sanctuary property` : ouvrir la carte de propriété ;
- `/sanctuary property claim` : revendiquer le chunk courant ;
- `/sanctuary property protect` : acheter sa protection ;
- `/sanctuary property duel <id>` : consulter le duel proposé.
Ce ticket remplace uniquement les alias de commande introduits en beta.270.
Les règles de revendication, prix, protections, interfaces FR/EN et sauvegardes
restent identiques. Aucun contrat de migration n'est nécessaire.
## Critères d'acceptation
- La complétion de `/sanctuary prop` propose uniquement `property`.
- Les deux alias retirés sont absents de l'arbre serveur et sont refusés,
seuls comme avec `claim`, `protect` ou `duel`.
- La branche anglaise et ses trois sous-commandes restent accessibles sans
permission opérateur. Revendication et protection fonctionnent via le
répartiteur réel des commandes.
- Les tests de propriétés et de duels passent ; le mod et le pack portent
ensemble `beta.294`.
## Vérifications
`check build assemblePack` réussit en **4 min 4 s**, sur Minecraft 26.3,
Fabric 0.19.5 et Java 25. Les **18 GameTests serveur ciblés** passent :
propriétés historiques, claims par chunks, priorité Demeure et duels Battle.
Le contrôle de commande vérifie la complétion unique, les anciens alias refusés
et les sous-commandes conservées. Le scénario natif de minage revendique et
protège désormais son terrain via les vraies commandes `property claim` et
`property protect`, en contrôlant le propriétaire et les huit saphirs débités.
Les **63 contrôles du registre de claims**, les **26 contrôles du journal de
duels** et les autres vérifications de `check` passent également.
```sh
./gradlew check build assemblePack \
-PsanctuaryFocusedTests=property270,claims272,claims277,claims278 \
-PsanctuaryAtlasOnly=true
```
Journal ignoré : `build/property294-check-build-pack.log`.
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.294.jar`.
Les packs client et serveur sont assemblés et vérifiés dans `build/packwiz`
et `build/packwiz-server`. La validation porte sur l'arbre serveur et les
commandes exécutées ; aucun essai graphique n'est revendiqué pour ce ticket.
Préparation locale uniquement ; aucune publication ni synchronisation d'une
installation de jeu ou du serveur Beta demandée pour ce ticket.
+43 -3
View File
@@ -1,7 +1,8 @@
# RELEASE-289 — zombies, lumière portée et mise à jour complète
Branche `codex/release-beta289`. Publication demandée le 26 octobre 2026.
État : artefacts construits et vérifiés, publication demandée en cours.
État : **publiée et installée le 26 octobre 2026** sur la Beta dédiée,
le site et l’instance Prism existante.
## Contenu
@@ -16,8 +17,8 @@ Branche `codex/release-beta289`. Publication demandée le 26 octobre 2026.
`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. Le prochain binaire est beta.289
car il contient aussi JEI, comme la Beta dédiée actuelle. Minecraft 26.3,
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.
@@ -47,6 +48,45 @@ 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`.
+136
View File
@@ -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`.
+80
View File
@@ -0,0 +1,80 @@
# beta.295 — livraison du serveur préenregistré
Livraison complète du **26 octobre 2026**, branche `codex/default-server`.
[Contrat PACK-03](default-server-beta295.md), [notes de version](notes-beta295.md).
Le ticket CLAIM-03 terminé est intégré depuis ses fichiers validés, sans
modifier son checkout. beta.294 reste une préparation locale non publiée.
## Versions réellement livrées
| Cible | État vérifié |
| --- | --- |
| Source et tag `beta.295` | `2734c2383a8ce8562506a9d6bd1da036084143a5` |
| Canal client/serveur | `9a3dd1bea975efc94cbdd5e9f7ef7288458063b7` |
| Release | [beta.295](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.295), quatre artefacts publics vérifiés |
| CT 350 | Service actif, quatre mods dont Sanctuary beta.295, démarrage sans erreur |
| Icône annoncée | Logo PNG 64 × 64, hash du ping public identique à la ressource du JAR |
| CT 320 | `releases/beta.295/prism`, beta.293 conservée, téléchargement direct et footer actualisés |
| Prism historique | Même instance Sanctuary Beta, huit mods et un seul JAR Sanctuary beta.295 |
Le ZIP du site reste une amorce du canal stable. Ses octets sont identiques
à ceux de beta.293 car la plateforme et les outils sont inchangés ; le premier
lancement récupère désormais le JAR beta.295 qui ajoute le serveur.
## Vérifications
- `check build assemblePack assembleServerPack` réussi en **3 min 22 s** :
contrôles purs et **18 tests serveur** `property270,claims272,claims277,claims278`.
- **15 contrôles des favoris** : conservation, sauvegarde, absence de doublon,
suppression volontaire, lecture vanilla de l’icône et corruption réparable.
- **Vulkan FR/EN**, GUI 3 en 1280 × 800 : réussi en 38 s, captures inspectées.
Hook réel de démarrage et liste multijoueur vanilla ; aucun monde ouvert.
- Deux synchronisations packwiz identiques dans une importation isolée, puis
deux dans l’instance historique. Serveur ajouté dans celle-ci avec le même
code d’initialisation, deux appels identiques, sans lancer le jeu.
- Ping public via DNS/SRV : Minecraft 26.3, 0/20, icône vérifiée depuis le LAN.
Aucun essai depuis un réseau extérieur ajouté.
- Admission : HTTP 200, code volontairement invalide refusé, aucun redeem.
Métriques Web fraîches et en marche ; même base et UUID communautaire.
## Artefacts
| Artefact | Octets | SHA-256 |
| --- | ---: | --- |
| `sanctuary-beta.295.jar` | 18179585 | `45a4af94c56bdd25f1eeeaa5e642588e0f9b1ab3f77e934f396fee1a73898831` |
| `Sanctuary-beta.295.mrpack` | 16768587 | `ab1b71de5a70b0f097a2b89a5ed7d4ca2ca3b3453b19d404b5ebf85ff205cfd7` |
| `Sanctuary-Prism-beta.295.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
| `Sanctuary-Server-beta.295.zip` | 4036670 | `40841bf0f0c8a16d56ef04441f0f71729348b14e6eba98ebf0865aacea843b67` |
Icône serveur SHA-256 :
`2b8966e8f2a0cfe064a1dfac4205874216373f8bb4c59e8eb712d5e7d204fce7`.
## Conservation et limites
Serveur vide avant arrêt. Sauvegarde complète privée :
`/root/sanctuary-beta-backups/before-beta295-20261026T052413Z/instance.tar.gz`,
SHA-256 `b46c036a439944f79d5f89a188064e7507bc5804ffaf8e3a6f42458319340f66`.
Les **248 fichiers protégés** et l’environnement privé sont identiques après
synchronisation, avant redémarrage. Ensuite toutes les clés/valeurs de
`server.properties` restent identiques ; Minecraft réécrit son commentaire
horodaté. Voix, réseau et monde existant gardent leurs réglages. Aucune
migration de monde ou de génération.
Prism : sauvegarde complète sous le dossier de l’instance historique,
`sanctuary-backups/before-beta295-20261026T052117Z/instance.tar.gz`,
SHA-256 `a128a1463cf598707674c84834c9b456c36d781fbf558daa4ce195713ceca8ca`.
Les **999 fichiers personnels préexistants** et `instance.cfg` sont identiques.
Seuls les fichiers gérés sont actualisés, et les nouveaux `servers.dat` et
`.sanctuary/default-server-beta` initialisent le favori. Aucun monde personnel ouvert.
Une autre instance importée, dossier `Sanctuary-Beta`, était en cours de jeu.
Elle n’a pas été modifiée pendant cette session et suivra le canal beta.295
au prochain lancement. Aucune nouvelle instance personnelle créée ici.
Web : HEAD `eadc30e8c7b6dda50bc810584af057235e7fdba8` et **73 changements préexistants** identiques.
Aucun remplacement du code live ni reconstruction de cache. ZIP et MRpack
publics HTTP 200 avec hashes exacts, ZIP reçu sous le nom `Sanctuary-Beta.zip`.
Reçus, journaux, hashes et scripts opérationnels : `build/release295/`, hors Git.
Les limites vocales antérieures restent celles de [beta.293](release-beta293.md) ;
cette livraison ne revendique pas de nouvel échange audible à deux joueurs.
+9 -2
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 est `build/Sanctuary-Server-beta.289.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.
@@ -56,6 +56,10 @@ avant de rendre le verrou de mise à jour.
L'API d'accueil et la communauté sont deux services existants distincts.
Le pack ne contient ni secret ni configuration de production.
Depuis beta.295, le profil serveur gère aussi `server-icon.png`, le logo du
pack en 64 × 64. La synchronisation le pose à la racine ; Minecraft l'annonce
aux clients au démarrage suivant. Le favori client est ajouté séparément au
premier lancement, sans indexer le fichier personnel `servers.dat`.
Pour l'accès/candidature, fournir dans l'environnement du service Minecraft :
@@ -86,7 +90,10 @@ Le kit n'effectue aucune migration SQL ni modification de sauvegarde.
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.
Le pack ne possède que les mods Fabric API et Sanctuary. Il ne gère ni monde,
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.
+24 -2
View File
@@ -63,8 +63,17 @@ Les sorties attendues pour la version choisie sont :
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 et le bootstrap ; le premier lancement
télécharge les mods. Ne pas lui ajouter des réglages ou sauvegardes personnels.
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.
Depuis beta.295, le JAR client initialise une seule fois le favori Sanctuary,
pour les nouvelles installations comme pour les instances déjà reliées au
canal. `servers.dat` reste personnel et hors index. Le profil serveur distribue
en revanche `server-icon.png` (logo 64 × 64) ; vérifier son hash et l'icône
annoncée par le ping après redémarrage. [Contrat](default-server-beta295.md).
## Publier les artefacts et le canal
@@ -95,6 +104,14 @@ 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.
@@ -210,6 +227,11 @@ sauvegarder les fichiers gérés et les métadonnées de l'instance hors de `mod
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.
+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.289
pack_version=beta.289
mod_version=beta.295
pack_version=beta.295
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
+134
View File
@@ -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 + ")");
}
}
+91
View File
@@ -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();
}
}
}
+21
View File
@@ -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.
+35
View File
@@ -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é.
+7
View File
@@ -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

+35 -1
View File
@@ -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,15 @@ tasks.named('runGameTest') {
}
tasks.named('processGametestResources') {
def defaultServer295 = providers.gradleProperty('sanctuaryDefaultServer295ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryDefaultServer295ClientTests', defaultServer295)
if (defaultServer295) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.DefaultServer295ClientChecks') }
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') }
@@ -282,7 +292,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 = [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)
@@ -407,6 +417,9 @@ tasks.named('processGametestResources') {
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') }
@@ -698,6 +711,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)) {
@@ -1581,3 +1608,10 @@ tasks.register('claimDuel278Smoke', JavaExec) {
mainClass = 'fr.koka.sanctuary.property.ClaimDuel278Smoke'
}
tasks.named('check') { dependsOn('claimDuel278Smoke') }
tasks.register('defaultServerListSmoke', JavaExec) {
dependsOn('testClasses')
classpath = sourceSets.test.runtimeClasspath
mainClass = 'fr.koka.sanctuary.client.DefaultServerListSmoke'
}
tasks.named('check') { dependsOn('defaultServerListSmoke') }
@@ -0,0 +1,42 @@
package fr.koka.sanctuary.client;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
/** Exercises the real startup hook and vanilla multiplayer list without opening a world. */
public final class DefaultServer295ClientChecks implements FabricClientGameTest {
@Override public void runTest(ClientGameTestContext c) {
c.runOnClient(m -> {
if (!com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName().equalsIgnoreCase("vulkan"))
throw new AssertionError("Vulkan required");
if (!java.nio.file.Files.exists(m.gameDirectory.toPath().resolve(".sanctuary/default-server-beta")))
throw new AssertionError("Client startup hook did not initialize the default");
});
for (String locale : new String[]{"fr_fr", "en_us"}) {
c.runOnClient(m -> {
m.getWindow().setWindowed(1280, 800);
m.options.guiScale().set(3); m.resizeGui();
m.getLanguageManager().setSelected(locale);
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
m.gui.setScreen(new JoinMultiplayerScreen(new TitleScreen()));
var list = ((JoinMultiplayerScreen) m.gui.screen()).getServers();
int count = 0;
for (int i = 0; i < list.size(); i++) {
var server = list.get(i);
if (server.ip.equals(DefaultServerList.ADDRESS)) {
count++;
if (!server.name.equals("Sanctuary Beta") || server.getIconBytes() == null)
throw new AssertionError("Missing default name or cached icon");
}
}
if (count != 1) throw new AssertionError("Expected one Sanctuary default, got " + count);
});
c.waitTicks(30);
c.takeScreenshot("default-server295-" + locale);
c.runOnClient(m -> m.gui.setScreen(new TitleScreen()));
}
System.out.println("DEFAULT_SERVER295_VULKAN_PASS: startup, vanilla list and logo FR/EN; no world opened");
}
}
@@ -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");
}
}
}
@@ -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);
}
}
@@ -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");
}
}
@@ -56,10 +56,11 @@ public final class ChunkClaims272GameTests {
}
@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 dispatcher=owner.level().getServer().getCommands().getDispatcher();var source=owner.createCommandSourceStack();
h.assertTrue(dispatcher.execute("sanctuary property claim",source)==1&&s.get(new ChunkClaims.Key(340,340)).owner().equals(owner.getUUID()),"English command reserves the current 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");
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),64));h.assertTrue(dispatcher.execute("sanctuary property protect",source)==1&&ChunkPropertyService.sapphires(owner)==56,"English command activates protection for eight sapphires");
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");
@@ -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();
});
});
}
}
@@ -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;
}
}
@@ -66,7 +66,19 @@ public final class Property270GameTests {
PropertyService.recover(p);h.assertTrue(FamiliarExperience.playerXp(p)==250,"Repeated recovery does not debit again");
var reloaded=new PropertyStore(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("sanctuary/properties-v1.json"),server.overworld().getSeed());
h.assertTrue(reloaded.get(p.getUUID()).equals(store.get(p.getUUID())),"Committed property survives disk reload");
for(String alias:java.util.List.of("property","propriete","propriété"))h.assertTrue(server.getCommands().getDispatcher().getRoot().getChild("sanctuary").getChild(alias).canUse(p.createCommandSourceStack()),"Property commands need no operator");
var dispatcher=server.getCommands().getDispatcher();var source=p.createCommandSourceStack();
var root=dispatcher.getRoot().getChild("sanctuary");var property=root.getChild("property");
h.assertTrue(property!=null&&property.canUse(source)&&property.getCommand()!=null,"English property command opens the menu without operator permissions");
for(String action:java.util.List.of("claim","protect","duel"))h.assertTrue(property.getChild(action)!=null&&property.getChild(action).canUse(source),"English property command retains "+action);
var suggestions=dispatcher.getCompletionSuggestions(dispatcher.parse("sanctuary prop",source)).join().getList().stream().map(s->s.getText()).toList();
h.assertTrue(suggestions.equals(java.util.List.of("property")),"Property completion offers only the English command");
for(String alias:java.util.List.of("propriete","propriété")){
h.assertTrue(root.getChild(alias)==null,"Removed property alias is absent: "+alias);
for(String suffix:java.util.List.of(""," claim"," protect"," duel 00000000-0000-0000-0000-000000000000")){
boolean rejected=false;try{dispatcher.execute("sanctuary "+alias+suffix,source);}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){rejected=true;}
h.assertTrue(rejected,"Removed property alias cannot execute: "+alias+suffix);
}
}
h.succeed();
}
@GameTest public void spawnDimensionAndHeightCannotBeClaimed(GameTestHelper h)throws Exception{
@@ -8,7 +8,7 @@
"entrypoints": {
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
"fabric-gametest": ["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"],
@@ -46,6 +46,7 @@ public final class SanctuaryMod implements ModInitializer {
fr.koka.sanctuary.property.PropertyService.register();
fr.koka.sanctuary.insects.Insects.register();
fr.koka.sanctuary.infected.Infected.register();
fr.koka.sanctuary.whale.FlyingWhales.register();
fr.koka.sanctuary.waystone.Waystones.register();
fr.koka.sanctuary.zipline.Ziplines.register();
fr.koka.sanctuary.palace.OriginBlock.register();
@@ -0,0 +1,78 @@
package fr.koka.sanctuary.client;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Base64;
import java.util.Locale;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
/** One-time client default; servers.dat remains personal and outside the pack index. */
public final class DefaultServerList {
public static final String ADDRESS = "beta.sanctuary-minecraft.net";
public static final String ICON = "/assets/sanctuary/server-icon.png";
private DefaultServerList() {}
public static void install(Path gameDirectory) throws IOException {
Path marker = gameDirectory.resolve(".sanctuary/default-server-beta");
if (Files.exists(marker)) return;
Path file = gameDirectory.resolve("servers.dat");
CompoundTag root = NbtIo.read(file);
if (root == null) root = new CompoundTag();
if (root.contains("servers") && !(root.get("servers") instanceof ListTag)) {
throw new IOException("Unexpected server list format; preserving servers.dat");
}
ListTag servers = root.getListOrEmpty("servers");
CompoundTag existing = null;
for (var tag : servers) {
if (!(tag instanceof CompoundTag server)) {
throw new IOException("Unexpected server entry; preserving servers.dat");
}
if (isSanctuary(server.getStringOr("ip", ""))) existing = server;
}
boolean changed = existing == null || existing.getBooleanOr("hidden", false);
if (existing == null) {
CompoundTag server = new CompoundTag();
server.putString("name", "Sanctuary Beta");
server.putString("ip", ADDRESS);
try (var icon = DefaultServerList.class.getResourceAsStream(ICON)) {
if (icon == null) throw new IOException("Missing Sanctuary server icon");
server.putString("icon", Base64.getEncoder().encodeToString(icon.readAllBytes()));
}
servers.add(server);
} else if (changed) {
existing.putBoolean("hidden", false);
}
if (changed) {
root.put("servers", servers);
Path backup = gameDirectory.resolve("servers.dat.before-sanctuary-beta");
if (Files.exists(file) && !Files.exists(backup)) Files.copy(file, backup);
Path temporary = Files.createTempFile(gameDirectory, "sanctuary-servers-", ".dat");
try {
NbtIo.write(root, temporary);
try {
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException ignored) {
Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temporary);
}
}
Files.createDirectories(marker.getParent());
Files.writeString(marker, ADDRESS + "\n");
}
private static boolean isSanctuary(String address) {
String normalized = address.strip().toLowerCase(Locale.ROOT);
int separator = normalized.indexOf(':');
String host = separator < 0 ? normalized : normalized.substring(0, separator);
String port = separator < 0 ? "" : normalized.substring(separator);
if (host.endsWith(".")) host = host.substring(0, host.length() - 1);
return host.equals(ADDRESS) && (port.isEmpty() || port.equals(":25565") || port.equals(":25567"));
}
}
@@ -0,0 +1,88 @@
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
package fr.koka.sanctuary.client;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.client.FlyingWhaleRenderState;
import net.minecraft.client.model.EntityModel;
import net.minecraft.client.model.geom.ModelLayerLocation;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeDeformation;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.LayerDefinition;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.model.geom.builders.PartDefinition;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.util.Mth;
public final class FlyingWhaleModel
extends EntityModel<FlyingWhaleRenderState> {
public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(SanctuaryMod.id("flying_whale"), "main");
private static final float DEG_TO_RAD = (float)Math.PI / 180;
private final ModelPart whale;
private final ModelPart body;
private final ModelPart head;
private final ModelPart jaw;
private final ModelPart leftFin;
private final ModelPart rightFin;
private final ModelPart dorsalFin;
private final ModelPart tailBase;
private final ModelPart tailMid;
private final ModelPart tailFluke;
public FlyingWhaleModel(ModelPart root) {
super(root, RenderTypes::entityTranslucent);
this.whale = root.getChild("whale");
this.body = this.whale.getChild("body");
this.head = this.whale.getChild("head");
this.jaw = this.head.getChild("jaw");
this.leftFin = this.whale.getChild("left_fin");
this.rightFin = this.whale.getChild("right_fin");
this.dorsalFin = this.whale.getChild("dorsal_fin");
this.tailBase = this.whale.getChild("tail_base");
this.tailMid = this.tailBase.getChild("tail_mid");
this.tailFluke = this.tailMid.getChild("tail_fluke");
}
public static LayerDefinition createBodyLayer() {
MeshDefinition mesh = new MeshDefinition();
PartDefinition root = mesh.getRoot();
PartDefinition whale = root.addOrReplaceChild("whale", CubeListBuilder.create(), PartPose.offset(0.0f, 5.0f, 0.0f));
whale.addOrReplaceChild("body", CubeListBuilder.create().texOffs(0, 0).addBox(-22.0f, -18.0f, -46.0f, 44.0f, 32.0f, 92.0f, new CubeDeformation(0.35f)).texOffs(0, 126).addBox(-17.0f, 4.0f, -40.0f, 34.0f, 13.0f, 78.0f, new CubeDeformation(0.12f)), PartPose.ZERO);
PartDefinition head = whale.addOrReplaceChild("head", CubeListBuilder.create().texOffs(206, 0).addBox(-20.0f, -16.0f, -28.0f, 40.0f, 28.0f, 32.0f, new CubeDeformation(0.2f)).texOffs(206, 64).addBox(-14.0f, -8.0f, -42.0f, 28.0f, 18.0f, 18.0f, new CubeDeformation(0.1f)).texOffs(306, 64).addBox(-4.0f, -19.0f, -17.0f, 8.0f, 5.0f, 9.0f), PartPose.offset(0.0f, 0.0f, -45.0f));
head.addOrReplaceChild("jaw", CubeListBuilder.create().texOffs(206, 106).addBox(-12.0f, 0.0f, -21.0f, 24.0f, 8.0f, 24.0f), PartPose.offset(0.0f, 8.0f, -20.0f));
whale.addOrReplaceChild("left_fin", CubeListBuilder.create().texOffs(0, 220).addBox(0.0f, -2.0f, -8.0f, 52.0f, 4.0f, 18.0f, new CubeDeformation(0.04f)), PartPose.offsetAndRotation(18.5f, 3.5f, -20.0f, -0.06981317f, -0.17453292f, 0.41887903f));
whale.addOrReplaceChild("right_fin", CubeListBuilder.create().texOffs(0, 220).addBox(-52.0f, -2.0f, -8.0f, 52.0f, 4.0f, 18.0f, new CubeDeformation(0.04f)), PartPose.offsetAndRotation(-18.5f, 3.5f, -20.0f, -0.06981317f, 0.17453292f, -0.41887903f));
whale.addOrReplaceChild("dorsal_fin", CubeListBuilder.create().texOffs(150, 220).addBox(-4.0f, -24.0f, -5.0f, 8.0f, 24.0f, 14.0f, new CubeDeformation(0.03f)), PartPose.offsetAndRotation(0.0f, -15.0f, 2.0f, -0.12217305f, 0.0f, 0.0f));
PartDefinition tailBase = whale.addOrReplaceChild("tail_base", CubeListBuilder.create().texOffs(356, 0).addBox(-12.0f, -10.0f, 0.0f, 24.0f, 20.0f, 38.0f, new CubeDeformation(0.08f)), PartPose.offset(0.0f, -1.0f, 41.0f));
PartDefinition tailMid = tailBase.addOrReplaceChild("tail_mid", CubeListBuilder.create().texOffs(356, 64).addBox(-7.0f, -7.0f, 0.0f, 14.0f, 14.0f, 31.0f), PartPose.offset(0.0f, 0.0f, 34.0f));
PartDefinition tailFluke = tailMid.addOrReplaceChild("tail_fluke", CubeListBuilder.create(), PartPose.offset(0.0f, 0.0f, 29.0f));
tailFluke.addOrReplaceChild("left_lobe", CubeListBuilder.create().texOffs(256, 178).addBox(0.0f, -2.0f, -9.0f, 38.0f, 4.0f, 22.0f, new CubeDeformation(0.05f)), PartPose.offsetAndRotation(0.0f, 0.0f, 0.0f, 0.0f, -0.12217305f, -0.12217305f));
tailFluke.addOrReplaceChild("right_lobe", CubeListBuilder.create().texOffs(256, 178).addBox(-38.0f, -2.0f, -9.0f, 38.0f, 4.0f, 22.0f, new CubeDeformation(0.05f)), PartPose.offsetAndRotation(0.0f, 0.0f, 0.0f, 0.0f, 0.12217305f, 0.12217305f));
return LayerDefinition.create(mesh, 512, 256);
}
public void setupAnim(FlyingWhaleRenderState state) {
super.setupAnim(state);
float age = state.ageInTicks;
float slowWave = Mth.sin((age * 0.035f));
float tailWave = Mth.sin((age * 0.058f));
float bodyRoll = Mth.sin((age * 0.017f)) * 0.035f;
float finDrift = Mth.sin((age * 0.041f + 1.7f)) * 0.075f;
this.whale.y = 5.0f + slowWave * 1.25f;
this.whale.zRot = bodyRoll;
this.whale.xRot = Mth.sin((age * 0.013f)) * 0.018f;
this.body.zRot = bodyRoll * 0.35f;
this.head.xRot = -0.02617994f + Mth.sin((age * 0.025f)) * 0.018f;
this.jaw.xRot = (float)Math.PI / 90 + Mth.sin((age * 0.031f)) * 0.022f;
this.leftFin.zRot = 0.41887903f + finDrift;
this.rightFin.zRot = -0.41887903f - finDrift;
this.leftFin.xRot = -0.08726646f + slowWave * 0.025f;
this.rightFin.xRot = -0.08726646f - slowWave * 0.025f;
this.dorsalFin.zRot = -bodyRoll * 0.65f;
this.tailBase.yRot = tailWave * 0.13f;
this.tailMid.yRot = tailWave * 0.22f;
this.tailFluke.yRot = tailWave * 0.31f;
this.tailFluke.zRot = Mth.sin((age * 0.029f)) * 0.045f;
}
}
@@ -0,0 +1,8 @@
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
package fr.koka.sanctuary.client;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
public final class FlyingWhaleRenderState
extends LivingEntityRenderState {
}
@@ -0,0 +1,41 @@
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.vertex.PoseStack;
import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.client.FlyingWhaleModel;
import fr.koka.sanctuary.client.FlyingWhaleRenderState;
import fr.koka.sanctuary.whale.FlyingWhaleEntity;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.MobRenderer;
import net.minecraft.resources.Identifier;
public final class FlyingWhaleRenderer
extends MobRenderer<FlyingWhaleEntity, FlyingWhaleRenderState, FlyingWhaleModel> {
private static final Identifier TEXTURE = SanctuaryMod.id("textures/entity/flying_whale/flying_whale.png");
private static final float MODEL_SCALE = 2.7f;
public FlyingWhaleRenderer(EntityRendererProvider.Context context) {
super(context, new FlyingWhaleModel(context.bakeLayer(FlyingWhaleModel.LAYER_LOCATION)), 4.25f);
}
public Identifier getTextureLocation(FlyingWhaleRenderState state) {
return TEXTURE;
}
public FlyingWhaleRenderState createRenderState() {
return new FlyingWhaleRenderState();
}
protected void scale(FlyingWhaleRenderState state, PoseStack poseStack) {
poseStack.scale(2.7f, 2.7f, 2.7f);
}
protected float getShadowRadius(FlyingWhaleRenderState state) {
return 9.5f;
}
protected int getModelTint(FlyingWhaleRenderState state) {
return -1191182337;
}
}
@@ -5,4 +5,5 @@ public final class MobHeadState {
public HeadCosmeticState head;
public float yaw,pitch,scale=1;
public boolean active;
public boolean happyCreeper;
}
@@ -17,7 +17,16 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
}
public static void registerClient() {
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
try {
DefaultServerList.install(client.gameDirectory.toPath());
} catch (java.io.IOException | RuntimeException error) {
org.slf4j.LoggerFactory.getLogger("Sanctuary").warn("Could not add the default server; existing favorites preserved", error);
}
});
InsectRenderer.register();
net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry.registerModelLayer(FlyingWhaleModel.LAYER_LOCATION, FlyingWhaleModel::createBodyLayer);
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(fr.koka.sanctuary.whale.FlyingWhales.TYPE, FlyingWhaleRenderer::new);
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
net.minecraft.world.entity.EntityTypes.ZOMBIE,InfectedRenderer::new);
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
@@ -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);
}
}
@@ -68,12 +68,11 @@ public final class ChunkPropertyService {
view(p,key,result,false);
}));
CommandRegistrationCallback.EVENT.register((dispatcher,access,env)->{
var root=Commands.literal("sanctuary");for(String alias:List.of("property","propriete","propriété")){
var branch=Commands.literal(alias).executes(c->{open(c.getSource().getPlayerOrException());return 1;});
for(String action:List.of("claim","protect"))branch.then(Commands.literal(action).executes(c->{
var p=c.getSource().getPlayerOrException();var s=store(p.level().getServer());var result=act(p,action,at(p),s==null?-1:s.revision());p.sendSystemMessage(label(result));view(p,at(p),result,false);return result.equals("claimed")||result.equals("protected")?1:0;
}));root.then(branch);
}dispatcher.register(root);
var branch=Commands.literal("property").executes(c->{open(c.getSource().getPlayerOrException());return 1;});
for(String action:List.of("claim","protect"))branch.then(Commands.literal(action).executes(c->{
var p=c.getSource().getPlayerOrException();var s=store(p.level().getServer());var result=act(p,action,at(p),s==null?-1:s.revision());p.sendSystemMessage(label(result));view(p,at(p),result,false);return result.equals("claimed")||result.equals("protected")?1:0;
}));
dispatcher.register(Commands.literal("sanctuary").then(branch));
});
ClaimDuels.register();
}
@@ -33,7 +33,7 @@ public final class ClaimDuelNetwork {
ServerPlayConnectionEvents.DISCONNECT.register((h,s)->{WATCHERS.remove(h.player);REQUESTS.remove(h.player);});
CommandRegistrationCallback.EVENT.register((dispatcher,access,env)->{
var root=Commands.literal("sanctuary");
for(String alias:List.of("property","propriete","propriété"))root.then(Commands.literal(alias).then(Commands.literal("duel")
root.then(Commands.literal("property").then(Commands.literal("duel")
.then(Commands.argument("id",com.mojang.brigadier.arguments.StringArgumentType.word()).executes(c->{
var p=c.getSource().getPlayerOrException();UUID id;try{id=UUID.fromString(com.mojang.brigadier.arguments.StringArgumentType.getString(c,"id"));}catch(IllegalArgumentException e){return 0;}
var o=ClaimDuels.get(p.level().getServer(),id);if(o==null||!o.participant(p.getUUID()))return 0;
@@ -0,0 +1,396 @@
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
package fr.koka.sanctuary.whale;
import fr.koka.sanctuary.SanctuaryMod;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.SpawnGroupData;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.ServerLevelAccessor;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.world.phys.Vec3;
public final class FlyingWhaleEntity
extends Mob {
private static final String DATA_VERSION_KEY = "SanctuaryWhaleDataVersion";
private static final String ORBIT_ANGLE_KEY = "OrbitAngle";
private static final String LOST_PLAYER_TICKS_KEY = "LostPlayerTicks";
private static final double COMPANION_RANGE = 520.0;
private static final double MIN_WANDER_RADIUS = 68.0;
private static final double WANDER_RADIUS_VARIANCE = 72.0;
private static final double DESPAWN_RANGE = 620.0;
private static final double WORLD_Y_MARGIN = 18.0;
private static final double MIN_LOOK_DISTANCE_SQR = 1.0E-4;
private static final double WANDER_TARGET_REACHED_DISTANCE_SQR = 784.0;
private static final int MIN_WANDER_TICKS = 12000;
private static final int MAX_WANDER_TICKS = 14400;
private static final int NO_PLAYER_DESPAWN_TICKS = 900;
private static final float MAX_TURN_DEGREES = 2.8f;
private static final float MAX_PITCH_DEGREES = 28.0f;
private Vec3 wanderTarget;
private double orbitAngle;
private int lostPlayerTicks;
private int nextBreathTicks;
private int wanderTargetTicks;
public FlyingWhaleEntity(EntityType<? extends FlyingWhaleEntity> entityType, Level level) {
super(entityType, level);
this.setNoGravity(true);
this.setPermanentlyInvulnerable(true);
this.noPhysics = true;
this.blocksBuilding = false;
this.xpReward = 0;
this.nextBreathTicks = 80 + this.random.nextInt(120);
}
public static AttributeSupplier.Builder createAttributes() {
return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 500.0).add(Attributes.MOVEMENT_SPEED, 0.04).add(Attributes.FLYING_SPEED, 0.18).add(Attributes.FOLLOW_RANGE, 520.0).add(Attributes.KNOCKBACK_RESISTANCE, 1.0).add(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE, 1.0);
}
public static boolean canSpawn(EntityType<FlyingWhaleEntity> entityType, ServerLevelAccessor level, EntitySpawnReason spawnReason, BlockPos blockPos, RandomSource random) {
ServerLevel world = level.getLevel();
return world.dimension() == Level.OVERWORLD && blockPos.getY() > world.getMinY() + 18.0 && blockPos.getY() < world.getMaxY() - 18.0;
}
public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, SpawnGroupData spawnGroupData) {
SpawnGroupData finalizedSpawn = super.finalizeSpawn(level, difficulty, spawnReason, spawnGroupData);
this.orbitAngle = this.random.nextDouble() * Math.PI * 2.0;
this.nextBreathTicks = 60 + this.random.nextInt(180);
this.lostPlayerTicks = 0;
this.setNoGravity(true);
this.setPermanentlyInvulnerable(true);
this.noPhysics = true;
return finalizedSpawn;
}
protected void addAdditionalSaveData(ValueOutput output) {
super.addAdditionalSaveData(output);
output.putInt(DATA_VERSION_KEY, 1);
output.putDouble(ORBIT_ANGLE_KEY, this.orbitAngle);
output.putInt(LOST_PLAYER_TICKS_KEY, this.lostPlayerTicks);
}
protected void readAdditionalSaveData(ValueInput input) {
super.readAdditionalSaveData(input);
this.orbitAngle = input.getDoubleOr(ORBIT_ANGLE_KEY, this.random.nextDouble() * Math.PI * 2.0);
this.lostPlayerTicks = input.getIntOr(LOST_PLAYER_TICKS_KEY, 0);
this.setNoGravity(true);
this.setPermanentlyInvulnerable(true);
this.noPhysics = true;
}
protected void customServerAiStep(ServerLevel serverLevel) {
super.customServerAiStep(serverLevel);
this.keepCloudRules();
ServerPlayer companion = this.findCompanion(serverLevel);
if (companion != null) {
this.lostPlayerTicks = 0;
this.tickAroundPlayer(serverLevel, companion);
} else {
++this.lostPlayerTicks;
this.tickSlowWander(serverLevel);
}
if (this.shouldFadeNow()) {
this.fadeAway(serverLevel);
return;
}
this.breatheIfNeeded(serverLevel);
}
public void tick() {
super.tick();
this.keepCloudRules();
this.updateVisualRotation();
if (this.level().isClientSide()) {
this.spawnClientCloudWake();
}
}
public boolean hurtServer(ServerLevel serverLevel, DamageSource damageSource, float amount) {
if (this.tickCount % 10 == 0) {
serverLevel.sendParticles(ParticleTypes.CLOUD, this.getX(), this.getY() + 2.6, this.getZ(), 8, 2.2, 1.0, 2.2, 0.02);
serverLevel.playSound(null, this.getX(), this.getY() + 2.0, this.getZ(), SoundEvents.BREEZE_IDLE_AIR, SoundSource.NEUTRAL, 0.25f, 0.62f);
}
return false;
}
public boolean isAttackable() {
return false;
}
public boolean skipAttackInteraction(Entity entity) {
return true;
}
public boolean canBeHitByProjectile() {
return false;
}
public boolean isPickable() {
return false;
}
public boolean isPushable() {
return false;
}
public boolean canBeCollidedWith(Entity entity) {
return false;
}
public boolean canCollideWith(Entity entity) {
return false;
}
public void push(Entity entity) {
}
public void push(double x, double y, double z) {
}
public boolean isPushedByFluid() {
return false;
}
public boolean removeWhenFarAway(double distanceToClosestPlayer) {
return false;
}
public void checkDespawn() {
}
public boolean shouldBeSaved() {
return this.hasCustomName();
}
public boolean fireImmune() {
return true;
}
protected double getDefaultGravity() {
return 0.0;
}
protected void checkFallDamage(double fallDistance, boolean landed, BlockState state, BlockPos pos) {
}
protected boolean isAffectedByBlocks() {
return false;
}
protected SoundEvent getAmbientSound() {
return SoundEvents.ELDER_GUARDIAN_AMBIENT;
}
protected SoundEvent getHurtSound(DamageSource damageSource) {
return SoundEvents.DOLPHIN_AMBIENT;
}
protected SoundEvent getDeathSound() {
return SoundEvents.ELDER_GUARDIAN_DEATH;
}
protected float getSoundVolume() {
return 0.72f;
}
public float getVoicePitch() {
return 0.34f + this.random.nextFloat() * 0.08f;
}
public int getAmbientSoundInterval() {
return 480;
}
public boolean shouldRenderAtSqrDistance(double distance) {
double maxDistance = 520.0;
return distance < maxDistance * maxDistance;
}
public void seedOrbit(double orbitAngle) {
this.orbitAngle = orbitAngle;
}
private void keepCloudRules() {
this.setNoGravity(true);
this.setPermanentlyInvulnerable(true);
this.noPhysics = true;
this.blocksBuilding = false;
this.resetFallDistance();
this.clearFire();
this.setAirSupply(this.getMaxAirSupply());
}
private boolean shouldFadeNow() {
return this.tickCount >= 14400 || this.tickCount >= 12000 && this.lostPlayerTicks > 900;
}
private void tickAroundPlayer(ServerLevel serverLevel, Player player) {
double distanceSqr;
--this.wanderTargetTicks;
if (this.needsPlayerWanderTarget(player)) {
this.wanderTarget = this.choosePlayerWanderTarget(serverLevel, player);
this.wanderTargetTicks = 20 * (35 + this.random.nextInt(56));
}
double maxSpeed = (distanceSqr = this.position().distanceToSqr(this.wanderTarget)) > 32400.0 ? 0.3 : 0.14;
double turnSpeed = distanceSqr > 14400.0 ? 0.022 : 0.01;
this.moveToward(this.wanderTarget, turnSpeed, maxSpeed);
}
private void tickSlowWander(ServerLevel serverLevel) {
if (this.wanderTarget == null || this.position().distanceToSqr(this.wanderTarget) < 9.0 || this.random.nextInt(140) == 0) {
double targetX = this.getX() + (this.random.nextDouble() - 0.5) * 90.0;
double targetZ = this.getZ() + (this.random.nextDouble() - 0.5) * 90.0;
double targetY = this.randomWorldY(serverLevel);
this.wanderTarget = new Vec3(targetX, targetY, targetZ);
}
this.moveToward(this.wanderTarget, 0.012, 0.12);
}
private boolean needsPlayerWanderTarget(Player player) {
return this.wanderTarget == null || this.wanderTargetTicks <= 0 || this.position().distanceToSqr(this.wanderTarget) < 784.0 || this.wanderTarget.distanceToSqr(player.position()) > 384400.0;
}
private Vec3 choosePlayerWanderTarget(ServerLevel serverLevel, Player player) {
this.orbitAngle += 0.55 + this.random.nextDouble() * 1.45;
double radius = 68.0 + this.random.nextDouble() * 72.0;
double targetX = player.getX() + Mth.cos(((float)this.orbitAngle)) * radius;
double targetZ = player.getZ() + Mth.sin(((float)this.orbitAngle)) * radius;
double targetY = this.randomWorldY(serverLevel);
return new Vec3(targetX, targetY, targetZ);
}
private double randomWorldY(ServerLevel serverLevel) {
double minY = serverLevel.getMinY() + 18.0;
double maxY = serverLevel.getMaxY() - 18.0;
if (maxY <= minY) {
return (minY + maxY) * 0.5;
}
return minY + this.random.nextDouble() * (maxY - minY);
}
private void updateVisualRotation() {
Vec3 movement = this.getDeltaMovement();
if (movement.lengthSqr() <= 1.0E-4) {
if (this.wanderTarget != null && this.position().distanceToSqr(this.wanderTarget) > 1.0E-4) {
this.facePosition(this.wanderTarget);
}
return;
}
this.facePosition(this.position().add(movement.scale(18.0)));
}
private void facePosition(Vec3 targetPos) {
Vec3 from = this.position();
double deltaX = targetPos.x - from.x;
double deltaY = targetPos.y - from.y;
double deltaZ = targetPos.z - from.z;
double horizontalDistance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
if (horizontalDistance * horizontalDistance + deltaY * deltaY <= 1.0E-4) {
return;
}
float targetYaw = (float)(Mth.atan2(deltaZ, deltaX) * 57.2957763671875) - 90.0f;
float yaw = Mth.approachDegrees((float)this.getYRot(), (float)targetYaw, 2.8f);
this.setYRot(yaw);
this.setYHeadRot(yaw);
this.setYBodyRot(yaw);
float targetPitch = (float)(-(Mth.atan2(deltaY, horizontalDistance) * 57.2957763671875));
this.setXRot(Mth.approachDegrees((float)this.getXRot(), (float)Mth.clamp((float)targetPitch, -28.0f, 28.0f), 0.6f));
}
private void moveToward(Vec3 targetPos, double turnSpeed, double maxSpeed) {
Vec3 toTarget = targetPos.subtract(this.position());
double distance = toTarget.length();
if (distance < 0.001) {
this.setDeltaMovement(this.getDeltaMovement().scale(0.88));
return;
}
Vec3 desired = toTarget.normalize().scale(maxSpeed);
this.setDeltaMovement(this.getDeltaMovement().lerp(desired, turnSpeed));
}
private ServerPlayer findCompanion(ServerLevel serverLevel) {
ServerPlayer closest = null;
double closestDistance = 270400.0;
for (ServerPlayer player : serverLevel.players()) {
double distance;
if (!this.isValidCompanion(player) || !((distance = this.distanceToSqr(player)) < closestDistance)) continue;
closest = player;
closestDistance = distance;
}
return closest;
}
private boolean isValidCompanion(Player player) {
return player.isAlive() && !player.isSpectator() && this.distanceToSqr(player) <= 270400.0;
}
private void breatheIfNeeded(ServerLevel serverLevel) {
--this.nextBreathTicks;
if (this.nextBreathTicks > 0) {
return;
}
this.nextBreathTicks = 180 + this.random.nextInt(260);
Vec3 movement = this.getDeltaMovement();
Vec3 forward = movement.horizontalDistanceSqr() > 1.0E-4 ? new Vec3(movement.x, 0.0, movement.z).normalize() : Vec3.directionFromRotation(0.0f, (float)this.getYRot());
Vec3 blowhole = this.position().add(forward.scale(6.2)).add(0.0, 4.4, 0.0);
serverLevel.sendParticles(ParticleTypes.CLOUD, blowhole.x, blowhole.y, blowhole.z, 28, 0.85, 0.45, 0.85, 0.035);
serverLevel.sendParticles(ParticleTypes.SPORE_BLOSSOM_AIR, blowhole.x, blowhole.y, blowhole.z, 12, 0.65, 0.3, 0.65, 0.012);
serverLevel.playSound(null, blowhole.x, blowhole.y, blowhole.z, SoundEvents.PUFFER_FISH_BLOW_OUT, SoundSource.NEUTRAL, 0.9f, 0.38f + this.random.nextFloat() * 0.08f);
}
private void spawnClientCloudWake() {
if (this.tickCount % 3 != 0 || this.random.nextFloat() > 0.68f) {
return;
}
Vec3 movement = this.getDeltaMovement();
if (movement.lengthSqr() <= 0.0025) {
return;
}
Vec3 direction = movement.normalize();
double side = (this.random.nextDouble() - 0.5) * 8.5;
double back = 5.5 + this.random.nextDouble() * 5.5;
double x = this.getX() - direction.x * back - direction.z * side;
double y = this.getY() + 2.2 + (this.random.nextDouble() - 0.5) * 2.4;
double z = this.getZ() - direction.z * back + direction.x * side;
this.level().addParticle(ParticleTypes.CLOUD, x, y, z, -direction.x * 0.015, 0.006 + this.random.nextDouble() * 0.012, -direction.z * 0.015);
}
private void fadeAway(ServerLevel serverLevel) {
if (this.isRemoved()) {
return;
}
serverLevel.sendParticles(ParticleTypes.CLOUD, this.getX(), this.getY() + 2.6, this.getZ(), 60, 5.5, 2.3, 5.5, 0.04);
serverLevel.sendParticles(ParticleTypes.POOF, this.getX(), this.getY() + 2.4, this.getZ(), 22, 3.5, 1.5, 3.5, 0.02);
serverLevel.playSound(null, this.getX(), this.getY() + 2.0, this.getZ(), SoundEvents.BREEZE_WHIRL, SoundSource.NEUTRAL, 0.7f, 0.48f);
this.discard();
}
public boolean isTooFarFromPlayers(ServerLevel serverLevel) {
double despawnDistanceSqr = 384400.0;
for (ServerPlayer player : serverLevel.players()) {
if (player.isSpectator() || !player.isAlive() || !(this.distanceToSqr(player) <= despawnDistanceSqr)) continue;
return false;
}
return true;
}
}
@@ -0,0 +1,31 @@
package fr.koka.sanctuary.whale;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.SpawnEggItem;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
/** Keep native egg placement and consumption, but refuse a full server before spending an egg. */
public final class FlyingWhaleSpawnEgg extends SpawnEggItem {
public FlyingWhaleSpawnEgg(Properties properties) { super(properties); }
@Override public InteractionResult useOn(UseOnContext context) {
if (full(context.getLevel(), context.getPlayer())) return InteractionResult.FAIL;
return super.useOn(context);
}
@Override public InteractionResult use(Level level, Player player, InteractionHand hand) {
if (full(level, player)) return InteractionResult.FAIL;
return super.use(level, player, hand);
}
private static boolean full(Level level, Player player) {
if (!(level instanceof ServerLevel serverLevel) || FlyingWhales.hasRoom(serverLevel.getServer())) return false;
if (player != null) player.sendOverlayMessage(Component.translatable("sanctuary.whale.limit"));
return true;
}
}
@@ -0,0 +1,78 @@
package fr.koka.sanctuary.whale;
import java.util.WeakHashMap;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.level.Level;
/** Port of the 26.2 spawner: 400 ticks, three daily windows, 92..138 blocks from a player. */
public final class FlyingWhaleSpawner {
private static final WeakHashMap<MinecraftServer, Schedule> SCHEDULES = new WeakHashMap<>();
public static final class Schedule {
private int ticksUntilCheck = 400;
private long trackedDay = Long.MIN_VALUE;
private int spawnedWindowsMask;
public boolean due() { if (--ticksUntilCheck > 0) return false; ticksUntilCheck = 400; return true; }
public int window(long clockTime) {
long day = Math.floorDiv(clockTime, 24000);
if (trackedDay != day) { trackedDay = day; spawnedWindowsMask = 0; }
return Math.floorMod(clockTime, 24000) / 8000;
}
public boolean available(int window) { return (spawnedWindowsMask & (1 << window)) == 0; }
public void spawned(int window) { spawnedWindowsMask |= 1 << window; }
}
public static void register() {
ServerTickEvents.END_SERVER_TICK.register(FlyingWhaleSpawner::tick);
ServerLifecycleEvents.SERVER_STOPPED.register(SCHEDULES::remove);
}
private static void tick(MinecraftServer server) {
var schedule = SCHEDULES.computeIfAbsent(server, s -> new Schedule());
if (!schedule.due()) return;
ServerLevel level = server.overworld();
int window = schedule.window(level.getDefaultClockTime());
if (!schedule.available(window) || !FlyingWhales.hasRoom(server)) return;
var candidates = level.players().stream().filter(p -> p.isAlive() && !p.isSpectator()).toList();
if (candidates.isEmpty()) return;
RandomSource random = level.getRandom();
if (spawnFor(level, candidates.get(random.nextInt(candidates.size())), random)) schedule.spawned(window);
}
public static boolean spawnFor(ServerLevel level, ServerPlayer player, RandomSource random) {
if (level.dimension() != Level.OVERWORLD || player.level() != level || !player.isAlive()
|| player.isSpectator() || !FlyingWhales.hasRoom(level.getServer())) return false;
double angle = random.nextDouble() * Math.PI * 2;
double radius = 92 + random.nextDouble() * 46;
double x = player.getX() + Math.cos(angle) * radius;
double z = player.getZ() + Math.sin(angle) * radius;
double minY = level.getMinY() + 22;
double maxY = level.getMaxY() - 22;
double y = maxY <= minY ? (minY + maxY) * .5 : minY + random.nextDouble() * (maxY - minY);
var pos = BlockPos.containing(x, y, z);
// No chunk generation or force-loading for an ambient visitor.
if (!level.hasChunkAt(pos) || !FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL, pos, random)) return false;
var whale = FlyingWhales.TYPE.create(level, EntitySpawnReason.NATURAL);
if (whale == null) return false;
whale.snapTo(x, y, z, (float)Math.toDegrees(angle) - 90, 0);
whale.finalizeSpawn(level, level.getCurrentDifficultyAt(pos), EntitySpawnReason.NATURAL, null);
whale.seedOrbit(angle + Math.PI);
if (!level.addFreshEntity(whale) || whale.isRemoved()) return false;
level.sendParticles(ParticleTypes.CLOUD, x, y + 2.5, z, 44, 5, 1.8, 5, .03);
level.playSound(null, x, y + 2.5, z, SoundEvents.ELDER_GUARDIAN_AMBIENT, SoundSource.NEUTRAL, .65F, .34F);
return true;
}
private FlyingWhaleSpawner() { }
}
@@ -0,0 +1,48 @@
package fr.koka.sanctuary.whale;
import fr.koka.sanctuary.SanctuaryMod;
import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents;
import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.levelgen.Heightmap;
/** The historical ambient whale, with one shared quota for the whole server. */
public final class FlyingWhales {
public static final int MAX_LOADED = 2;
public static final EntityType<FlyingWhaleEntity> TYPE = Registry.register(BuiltInRegistries.ENTITY_TYPE,
SanctuaryMod.id("flying_whale"), EntityType.Builder.of(FlyingWhaleEntity::new, MobCategory.AMBIENT)
.sized(36, 11).clientTrackingRange(32).updateInterval(3).fireImmune().noLootTable()
.build(ResourceKey.create(Registries.ENTITY_TYPE, SanctuaryMod.id("flying_whale"))));
public static final Item SPAWN_EGG = Registry.register(BuiltInRegistries.ITEM,
SanctuaryMod.id("flying_whale_spawn_egg"), new FlyingWhaleSpawnEgg(new Item.Properties().spawnEgg(TYPE)
.setId(ResourceKey.create(Registries.ITEM, SanctuaryMod.id("flying_whale_spawn_egg")))));
public static void register() {
FabricDefaultAttributeRegistry.register(TYPE, FlyingWhaleEntity.createAttributes());
SpawnPlacements.register(TYPE, SpawnPlacementTypes.NO_RESTRICTIONS,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, FlyingWhaleEntity::canSpawn);
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.SPAWN_EGGS).register(out -> out.accept(SPAWN_EGG));
// Covers commands, dispensers, eggs and named whales reloaded from chunks as well.
ServerEntityEvents.ENTITY_LOAD.register((entity, level) -> {
if (entity instanceof FlyingWhaleEntity && countLoaded(level.getServer()) > MAX_LOADED) entity.discard();
});
FlyingWhaleSpawner.register();
}
public static int countLoaded(MinecraftServer server) {
int count = 0;
for (var level : server.getAllLevels()) count += level.getEntities(TYPE, Entity::isAlive).size();
return count;
}
public static boolean hasRoom(MinecraftServer server) { return countLoaded(server) < MAX_LOADED; }
private FlyingWhales() { }
}
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:item/flying_whale_spawn_egg"
}
}
@@ -1,4 +1,8 @@
{
"entity.sanctuary.flying_whale": "Flying Ghost Whale",
"item.sanctuary.flying_whale_spawn_egg": "Flying Ghost Whale Spawn Egg",
"sanctuary.whale.limit": "Two whales are already present on the server.",
"sanctuary.recipes.jei_missing": "JEI is disabled or missing. Enable the Sanctuary JEI mod and restart the game to browse the catalogue. Manual crafting remains available.",
"sanctuary.story.title": "Story",
"sanctuary.story.search": "Search stories…",
@@ -1,4 +1,8 @@
{
"entity.sanctuary.flying_whale": "Baleine fantôme volante",
"item.sanctuary.flying_whale_spawn_egg": "Œuf d’apparition de baleine fantôme volante",
"sanctuary.whale.limit": "Deux baleines sont déjà présentes sur le serveur.",
"sanctuary.recipes.jei_missing": "JEI est désactivé ou absent. Réactivez le mod JEI Sanctuary puis redémarrez le jeu pour consulter le catalogue. La fabrication manuelle reste disponible.",
"sanctuary.story.title": "Story",
"sanctuary.story.search": "Rechercher une histoire…",
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "sanctuary:item/flying_whale_spawn_egg"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

@@ -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",
@@ -0,0 +1,86 @@
package fr.koka.sanctuary.client;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo;
public final class DefaultServerListSmoke {
private static int checks;
private static void check(boolean condition, String message) {
checks++;
if (!condition) throw new AssertionError(message);
}
public static void main(String[] args) throws Exception {
net.minecraft.SharedConstants.tryDetectVersion();
Path root = Files.createTempDirectory("sanctuary-server-default-");
try {
Path fresh = Files.createDirectory(root.resolve("fresh"));
DefaultServerList.install(fresh);
CompoundTag server = NbtIo.read(fresh.resolve("servers.dat")).getListOrEmpty("servers").getCompoundOrEmpty(0);
check(server.getStringOr("name", "").equals("Sanctuary Beta"), "Default display name");
check(server.getStringOr("ip", "").equals(DefaultServerList.ADDRESS), "Public SRV address");
byte[] icon = Base64.getDecoder().decode(server.getStringOr("icon", ""));
var image = javax.imageio.ImageIO.read(new java.io.ByteArrayInputStream(icon));
check(image.getWidth() == 64 && image.getHeight() == 64, "Minecraft favicon dimensions");
check(Arrays.equals(net.minecraft.client.multiplayer.ServerData.read(server).getIconBytes(), icon), "Vanilla reads the cached icon");
byte[] first = Files.readAllBytes(fresh.resolve("servers.dat"));
DefaultServerList.install(fresh);
check(Arrays.equals(first, Files.readAllBytes(fresh.resolve("servers.dat"))), "Second launch leaves favorites byte-identical");
CompoundTag empty = new CompoundTag(); empty.put("servers", new ListTag());
NbtIo.write(empty, fresh.resolve("servers.dat"));
DefaultServerList.install(fresh);
check(NbtIo.read(fresh.resolve("servers.dat")).equals(empty), "User may remove the default permanently");
Path personal = Files.createDirectory(root.resolve("personal"));
CompoundTag other = new CompoundTag(); other.putString("name", "Mes amis"); other.putString("ip", "friends.example:25568");
other.putBoolean("acceptTextures", true); other.putString("custom", "preserve me");
CompoundTag original = new CompoundTag(); original.putString("extra", "preserve root fields");
ListTag entries = new ListTag(); entries.add(other); original.put("servers", entries);
NbtIo.write(original, personal.resolve("servers.dat"));
byte[] originalBytes = Files.readAllBytes(personal.resolve("servers.dat"));
DefaultServerList.install(personal);
CompoundTag merged = NbtIo.read(personal.resolve("servers.dat"));
check(merged.getListOrEmpty("servers").size() == 2, "Default appends to personal list");
check(merged.getListOrEmpty("servers").getCompoundOrEmpty(0).equals(other)
&& merged.getStringOr("extra", "").equals("preserve root fields"), "Preserve order, settings and unknown fields");
check(Arrays.equals(originalBytes, Files.readAllBytes(personal.resolve("servers.dat.before-sanctuary-beta"))), "Original favorites backed up exactly");
for (String address : new String[]{DefaultServerList.ADDRESS, "BETA.SANCTUARY-MINECRAFT.NET.:25565", DefaultServerList.ADDRESS + ":25567"}) {
Path existing = Files.createTempDirectory(root, "existing-");
CompoundTag favorite = new CompoundTag(); favorite.putString("name", "Mon Sanctuary"); favorite.putString("ip", address);
ListTag list = new ListTag(); list.add(favorite); CompoundTag nbt = new CompoundTag(); nbt.put("servers", list);
NbtIo.write(nbt, existing.resolve("servers.dat"));
byte[] before = Files.readAllBytes(existing.resolve("servers.dat"));
DefaultServerList.install(existing);
check(Arrays.equals(before, Files.readAllBytes(existing.resolve("servers.dat"))), "Existing favorite preserved without duplicate: " + address);
}
Path hidden = Files.createDirectory(root.resolve("hidden"));
CompoundTag favorite = server.copy(); favorite.putBoolean("hidden", true); favorite.putString("name", "My Beta");
ListTag list = new ListTag(); list.add(favorite); CompoundTag nbt = new CompoundTag(); nbt.put("servers", list);
NbtIo.write(nbt, hidden.resolve("servers.dat"));
DefaultServerList.install(hidden);
ListTag shown = NbtIo.read(hidden.resolve("servers.dat")).getListOrEmpty("servers");
check(shown.size() == 1 && !shown.getCompoundOrEmpty(0).getBooleanOr("hidden", false)
&& shown.getCompoundOrEmpty(0).getStringOr("name", "").equals("My Beta"), "Promote direct-connect history without duplicate or rename");
Path corrupt = Files.createDirectory(root.resolve("corrupt"));
byte[] broken = new byte[]{10, 0, 0, 9}; Files.write(corrupt.resolve("servers.dat"), broken);
try { DefaultServerList.install(corrupt); throw new AssertionError("Corrupt file accepted"); }
catch (java.io.IOException | RuntimeException expected) { }
check(Arrays.equals(broken, Files.readAllBytes(corrupt.resolve("servers.dat")))
&& !Files.exists(corrupt.resolve(".sanctuary/default-server-beta")), "Corrupt favorites preserved; retry allowed");
NbtIo.write(empty, corrupt.resolve("servers.dat"));
DefaultServerList.install(corrupt);
check(NbtIo.read(corrupt.resolve("servers.dat")).getListOrEmpty("servers").size() == 1, "Retry after user repairs file");
System.out.println("DefaultServerList: " + checks + " checks passed");
} finally {
try (var files = Files.walk(root)) {
for (Path path : files.sorted(java.util.Comparator.reverseOrder()).toList()) Files.delete(path);
}
}
}
}
+10 -1
View File
@@ -3,6 +3,10 @@
Ce dossier contient le manifeste packwiz source et Fabric API, avec une version
exacte de Minecraft et du chargeur Fabric. Le JAR Sanctuary embarque Demeure
et le port expérimental de JEI ; les autres intégrations sont vérifiées par ticket.
Depuis la préparation beta.292, Simple Voice Chat, Bobby, Lithium et Entity
Culling rejoignent les options du pack. Bobby et Entity Culling restent
clients ; Lithium et la voix sont également dans
le profil serveur. [Versions et vérifications](../docs/optional-mods-beta292.md).
Depuis la racine :
@@ -33,10 +37,15 @@ Pour préparer une instance importable dans Prism :
python3 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
```
Importer `build/Sanctuary-Prism-auto-update.zip` dans Prism, via **Ajouter une
Importer `build/Sanctuary-Prism-beta.xxx.zip` dans Prism, via **Ajouter une
instance → Importer**. Conserver ensuite cette même instance : packwiz vérifie
les mises à jour avant chaque lancement. Java 25 doit être disponible dans Prism.
La génération du ZIP ne publie pas le pack et ne modifie aucune instance locale.
Depuis la préparation beta.291, ce ZIP contient aussi l'installateur avec
le [logo Sanctuary pendant packwiz](../docs/packwiz-logo-beta291.md).
Le bootstrap conserve sa fenêtre avec `--bootstrap-no-update`, sans désactiver
la synchronisation du pack. Une instance existante garde son dossier et reçoit
le nouvel outil seulement lors d'une mise à jour locale demandée.
## Pack local et export ponctuel
+20
View File
@@ -4,11 +4,31 @@ hash-format = "sha256"
file = "icon.png"
hash = "33adb8f689d55b7bf8d0824b02942ea3823f128b27fab458713ab9582061a1a0"
[[files]]
file = "mods/bobby.pw.toml"
hash = "72ce751a7c14567629bac0b127f9d6635638f66cd0614b513eacc3179b227f97"
metafile = true
[[files]]
file = "mods/entityculling.pw.toml"
hash = "37112e88c0e1685c09a5dbb6e4e7f37c75b11e8f495a25a6444ea7a022680886"
metafile = true
[[files]]
file = "mods/fabric-api.pw.toml"
hash = "e3a8b560330802ec556f34a7de7a73bc712202359a657a36b39e6f85598bf8b8"
metafile = true
[[files]]
file = "mods/lithium.pw.toml"
hash = "022e5963937052ed68092b8c812615acd737714e7f161c86b3f2d8be770260b6"
metafile = true
[[files]]
file = "mods/simple-voice-chat.pw.toml"
hash = "af6ef838579aa5bc22e4934f5b3ecc43f5c3c1967560dc3707c2b53451451a2f"
metafile = true
[[files]]
file = "mods/sodium.pw.toml"
hash = "e4c2d1e88f5a15e919e1b13a6959b1484035b4c02c42e20442635569179c237d"
+17
View File
@@ -0,0 +1,17 @@
name = "Bobby"
filename = "bobby-5.2.16+mc26.3.jar"
side = "client"
[download]
url = "https://cdn.modrinth.com/data/M08ruV16/versions/J7YnfCss/bobby-5.2.16%2Bmc26.3.jar"
hash-format = "sha512"
hash = "f9ed86c48eb095b0e35f6f16abd376ad8dd3d2a5cd336fe8907d0562c9ecd1e0c7af35fc5afed121e1385fc14e41cc5f915ca986c3e19434f5c278cc0664b444"
[option]
optional = true
default = true
description = "Cache client facultatif des chunks déjà reçus pour voir plus loin. / Optional client cache of previously received chunks for a longer view."
[update.modrinth]
mod-id = "M08ruV16"
version = "J7YnfCss"
+17
View File
@@ -0,0 +1,17 @@
name = "EntityCulling"
filename = "entityculling-fabric-1.11.3-mc26.3.jar"
side = "client"
[download]
url = "https://cdn.modrinth.com/data/NNAgCjsB/versions/X1g3VhTx/entityculling-fabric-1.11.3-mc26.3.jar"
hash-format = "sha512"
hash = "b6209bb62d72376d3fd284da137ec93a6dd38ebe5fd38ab9190deeade68b5b7f4bf915f4abcb4a807afe18cb9f0b59e11be789d46df6f0e6f64388ed2edc284d"
[option]
optional = true
default = true
description = "Masquage facultatif des entités cachées derrière les blocs. / Optional culling of entities hidden behind blocks."
[update.modrinth]
mod-id = "NNAgCjsB"
version = "X1g3VhTx"
+17
View File
@@ -0,0 +1,17 @@
name = "Lithium"
filename = "lithium-fabric-0.26.2+mc26.3.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/gvQqBUqZ/versions/xS0Q8LSi/lithium-fabric-0.26.2%2Bmc26.3.jar"
hash-format = "sha512"
hash = "4d7fee66132eedc71feab9390b92c95d7058edbdad0fecfac1d836a2950b97a7ca463afbede61c7ef361ce65e1f927ab9e89dde5bf2e0e0ce486afb5c5dbee40"
[option]
optional = true
default = true
description = "Optimisations facultatives de la simulation. / Optional game simulation optimizations."
[update.modrinth]
mod-id = "gvQqBUqZ"
version = "xS0Q8LSi"
+17
View File
@@ -0,0 +1,17 @@
name = "Simple Voice Chat"
filename = "voicechat-fabric-2.6.24+26.3.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/9eGKb6K1/versions/OLnMVWXy/voicechat-fabric-2.6.24%2B26.3.jar"
hash-format = "sha512"
hash = "414eb51967305fe7740d34016bc8e3e4e2fa1590e1278ad06bcdfa0687f65007b825d345dc09f79a992704a51ec93baf01fe727e0bbc32fdf506828b5a65d85a"
[option]
optional = true
default = true
description = "Voix de proximité facultative, nécessite le serveur vocal. / Optional proximity voice chat; needs a voice-enabled server."
[update.modrinth]
mod-id = "9eGKb6K1"
version = "OLnMVWXy"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.289"
version = "beta.295"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "f2e7a69c0d7a62cd3cfa4895940ed5de4dcccc74e5f76a822529adcc8bf966c1"
hash = "21e12b4b57d86ef2a312769da3319081ab7dbe86b0120d377de431e421444057"
[versions]
fabric = "0.19.5"
+64
View File
@@ -0,0 +1,64 @@
{
"simple-voice-chat": {
"id": "voicechat",
"version": "2.6.24+26.3",
"project": "9eGKb6K1",
"artifact": "OLnMVWXy",
"filename": "voicechat-fabric-2.6.24+26.3.jar",
"side": "both",
"depends": {
"fabricloader": ">=0.19.3",
"minecraft": "26.3.x",
"java": ">=25"
},
"breaks": {
"fabric-api": "<0.160.5+26.3"
}
},
"bobby": {
"id": "bobby",
"version": "5.2.16+mc26.3",
"project": "M08ruV16",
"artifact": "J7YnfCss",
"filename": "bobby-5.2.16+mc26.3.jar",
"side": "client",
"depends": {
"fabricloader": ">=0.18.4",
"minecraft": "~26.3"
},
"breaks": {
"sodium": "<0.6.0"
}
},
"lithium": {
"id": "lithium",
"version": "0.26.2+mc26.3",
"project": "gvQqBUqZ",
"artifact": "xS0Q8LSi",
"filename": "lithium-fabric-0.26.2+mc26.3.jar",
"side": "both",
"depends": {
"fabricloader": ">=0.19.5",
"minecraft": "~26.3",
"mixinextras": ">=0.5.5"
},
"breaks": {
"optifabric": "*"
}
},
"entityculling": {
"id": "entityculling",
"version": "1.11.3",
"project": "NNAgCjsB",
"artifact": "X1g3VhTx",
"filename": "entityculling-fabric-1.11.3-mc26.3.jar",
"side": "client",
"depends": {
"minecraft": "~26.3",
"fabric-api": "*"
},
"breaks": {
"tlskincape": "*"
}
}
}
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Customize Minecraft 26.3 egg.png pixel for pixel, preserving its exact alpha mask.
Requires ImageMagick 7. No resampling, generated outline or ellipse is used.
"""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[1]
ASSET = "assets/minecraft/textures/item/egg.png"
SOURCE_SHA256 = "3bfccf5fcfe66933d591e944f6b08456c8083f5c3898b924da52bfebc41e6322"
OUTPUT = ROOT / "mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/flying_whale_spawn_egg.png"
# Small cloud blue and lavender flecks, entirely inside the native shell.
FLECKS = {(6, 5), (7, 5), (6, 6), (10, 8), (10, 9), (9, 9), (5, 10), (6, 11)}
def recolor(original):
if len(original) != 16 * 16 * 4:
raise ValueError("Expected the native 16 x 16 RGBA egg")
result = bytearray(original)
for y in range(16):
for x in range(16):
i = (y * 16 + x) * 4
red, green, blue, alpha = original[i:i + 4]
if not alpha:
continue
light = max(red, green, blue) / 255
tint = (.57, .68, .92) if (x, y) in FLECKS else (.88, .96, 1.0)
result[i:i + 3] = bytes(round(light * channel * 255) for channel in tint)
if result[3::4] != original[3::4]:
raise AssertionError("The Minecraft silhouette changed")
return bytes(result)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("minecraft_jar", type=Path)
parser.add_argument("--check", action="store_true", help="Compare the committed texture without rewriting it")
args = parser.parse_args()
with zipfile.ZipFile(args.minecraft_jar) as archive:
if json.loads(archive.read("version.json"))["id"] != "26.3":
raise ValueError("Only the pinned Minecraft 26.3 texture is supported")
source = archive.read(ASSET)
if hashlib.sha256(source).hexdigest() != SOURCE_SHA256:
raise ValueError("Unexpected native egg texture")
original = subprocess.run(["magick", "png:-", "-depth", "8", "rgba:-"], input=source, capture_output=True, check=True).stdout
result = recolor(original)
if not args.check:
subprocess.run(["magick", "-size", "16x16", "-depth", "8", "rgba:-", "-strip", f"PNG32:{OUTPUT}"], input=result, check=True)
actual = subprocess.run(["magick", str(OUTPUT), "-depth", "8", "rgba:-"], capture_output=True, check=True).stdout
if actual != result:
raise AssertionError("The texture differs from the deterministic Minecraft egg customization")
print("Minecraft 26.3 egg: 16 x 16, exact silhouette, alpha and native shadow positions preserved")
print(f"Source SHA-256: {SOURCE_SHA256}")
print(f"Whale egg SHA-256: {hashlib.sha256(OUTPUT.read_bytes()).hexdigest()}")
if __name__ == "__main__":
main()
+22 -5
View File
@@ -12,6 +12,8 @@ from urllib.parse import urlsplit
from urllib.request import urlopen
import zipfile
import packwiz_installer
try:
import tomllib
except ImportError:
@@ -120,14 +122,15 @@ def assemble(jar_url=None):
require(not any('demeure-' in j['file'] for j in metadata.get('jars', [])), 'Duplicate Demeure mod')
require('fr/koka/sanctuarytest/WorldgenLab.class' in archive.namelist(), 'Sanctuary terrain runtime missing')
require('fr/koka/sanctuarytest/QuickTestMod.class' not in archive.namelist(), 'Lab entrypoint leaked into production')
require(not {'jei', 'sound_physics_remastered', 'sodium'} & metadata['depends'].keys(),
external = {'jei', 'sound_physics_remastered', 'sodium', 'bobby', 'lithium', 'entityculling', 'voicechat'}
require(not external & metadata['depends'].keys(),
'External client mods must not be required by Sanctuary')
embedded_jei = []
for nested_mod in metadata.get('jars', []):
with zipfile.ZipFile(io.BytesIO(archive.read(nested_mod['file']))) as candidate:
if 'fabric.mod.json' in candidate.namelist():
info = json.loads(candidate.read('fabric.mod.json'))
require(info['id'] not in ('sound_physics_remastered', 'sodium'),
require(info['id'] not in external - {'jei'},
'External client mod is still nested in Sanctuary: ' + info['id'])
if info['id'] == 'jei':
embedded_jei.append(archive.read(nested_mod['file']))
@@ -188,12 +191,16 @@ def assemble(jar_url=None):
server.mkdir(parents=True)
server_entries = []
for entry in staged_entries:
if entry['file'] not in ('mods/fabric-api.pw.toml', 'mods/sanctuary.pw.toml', 'mods/' + jar.name):
if entry['file'] not in ('mods/fabric-api.pw.toml', 'mods/sanctuary.pw.toml', 'mods/' + jar.name,
'mods/lithium.pw.toml', 'mods/simple-voice-chat.pw.toml'):
continue
target = server / entry['file']
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(destination / entry['file'], target)
server_entries.append(entry)
icon = ROOT / 'mods/sanctuary/src/main/resources/assets/sanctuary/server-icon.png'
shutil.copy2(icon, server / 'server-icon.png')
server_entries.append({'file': 'server-icon.png', 'hash': sha256(icon)})
write_index(server, server_entries, name='Sanctuary Server')
verify_server(server)
print('Client pack staged at ' + str(destination))
@@ -225,7 +232,14 @@ def verify_server(base):
require(manifest['versions'] == read_toml(SOURCE / 'pack.toml')['versions'], 'Server platform drift')
files = {entry['file'] for entry in entries}
sanctuary = 'mods/sanctuary.pw.toml' if 'mods/sanctuary.pw.toml' in files else 'mods/sanctuary-' + values['mod_version'] + '.jar'
require(files == {'mods/fabric-api.pw.toml', sanctuary}, 'Server pack must contain only Fabric API and Sanctuary')
require(files == {'mods/fabric-api.pw.toml', sanctuary, 'mods/lithium.pw.toml',
'mods/simple-voice-chat.pw.toml', 'server-icon.png'},
'Server pack must contain the shared mods and the Sanctuary server icon')
icon = (base / 'server-icon.png').read_bytes()
require(icon == (ROOT / 'mods/sanctuary/src/main/resources/assets/sanctuary/server-icon.png').read_bytes()
and icon[:8] == b'\x89PNG\r\n\x1a\n'
and int.from_bytes(icon[16:20], 'big') == 64
and int.from_bytes(icon[20:24], 'big') == 64, 'Server icon must be the verified 64x64 PNG')
for entry in entries:
if entry.get('metafile'):
require(read_toml(base / entry['file'])['side'] == 'both', 'Client mod in server pack')
@@ -236,12 +250,14 @@ def prism(pack_url):
"""Create an importable launcher template; never modify an installed instance."""
https_url(pack_url)
values, _ = check()
installer = packwiz_installer.build()
with urlopen(BOOTSTRAP_URL, timeout=60) as response:
bootstrap = response.read()
require(hashlib.sha256(bootstrap).hexdigest() == BOOTSTRAP_SHA256,
'Unexpected packwiz bootstrap SHA-256')
command = ('"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" '
'--bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" '
'--bootstrap-no-update '
'--pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" '
+ pack_url)
# Prism instance.cfg uses Qt INI escaping for quotation marks.
@@ -266,7 +282,8 @@ def prism(pack_url):
with zipfile.ZipFile(destination, 'w', zipfile.ZIP_DEFLATED) as archive:
files = {'instance.cfg': config.encode(), 'sanctuary-beta.png': (SOURCE / 'icon.png').read_bytes(),
'mmc-pack.json': (json.dumps(components, indent=2) + '\n').encode(),
'minecraft/packwiz-installer-bootstrap.jar': bootstrap}
'minecraft/packwiz-installer-bootstrap.jar': bootstrap,
'minecraft/packwiz-installer.jar': installer}
for name, data in files.items():
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.compress_type = zipfile.ZIP_DEFLATED
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Build the branded packwiz window over an immutable upstream installer.
Only InstallWindow.class is replaced. Downloads, hashes, optional mods, error
handling and cancellation remain those of the official v0.5.14 installer.
Generated JARs and the downloaded dependency stay in ignored build/.
"""
import hashlib
import io
import json
from pathlib import Path
import shutil
import subprocess
import zipfile
from urllib.request import urlopen
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'launcher/packwiz'
OUTPUT = ROOT / 'build/packwiz-branding/packwiz-installer-sanctuary.jar'
WINDOW = 'link/infra/packwiz/installer/ui/gui/InstallWindow.class'
LOGO = 'sanctuary/packwiz/logo.png'
def digest(data):
return hashlib.sha256(data).hexdigest()
def require(condition, message):
if not condition:
raise SystemExit(message)
def build():
lock = json.loads((SOURCE / 'installer.json').read_text())
logo = (SOURCE / 'sanctuary.png').read_bytes()
require(digest(logo) == lock['logo_sha256'], 'Packwiz logo hash mismatch')
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
upstream = OUTPUT.parent / 'packwiz-installer-upstream.jar'
if not upstream.is_file():
with urlopen(lock['upstream_url'], timeout=45) as response:
data = response.read()
require(digest(data) == lock['upstream_sha256'], 'Upstream packwiz hash mismatch')
upstream.write_bytes(data)
require(digest(upstream.read_bytes()) == lock['upstream_sha256'],
'Cached upstream packwiz hash mismatch')
classes = OUTPUT.parent / 'classes'
if classes.exists():
shutil.rmtree(classes)
classes.mkdir()
subprocess.run(['javac', '--release', '8', '-Xlint:-options', '-encoding', 'UTF-8', '-g:none',
'-classpath', str(upstream), '-d', str(classes),
str(SOURCE / 'InstallWindow.java')], check=True)
with zipfile.ZipFile(upstream) as archive:
entries = {name: archive.read(name) for name in archive.namelist()
if not name.endswith('/')}
require(WINDOW in entries, 'Upstream InstallWindow moved; review the adapter')
entries[WINDOW] = (classes / WINDOW).read_bytes()
entries[LOGO] = logo
for name in ('InstallWindow.java', 'LICENSE'):
entries['META-INF/sanctuary-packwiz/' + name] = (SOURCE / name).read_bytes()
output = io.BytesIO()
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as archive:
# The bootstrap uses JarInputStream; keep the manifest first.
names = ['META-INF/MANIFEST.MF'] + sorted(set(entries) - {'META-INF/MANIFEST.MF'})
for name in names:
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, entries[name])
data = output.getvalue()
require(digest(data) == lock['installer_sha256'],
'Branded installer differs from lock: ' + digest(data))
OUTPUT.write_bytes(data)
print('Sanctuary packwiz installer verified: ' + str(OUTPUT))
return data
if __name__ == '__main__':
build()
+3 -3
View File
@@ -116,9 +116,9 @@ def remote_tag_commit(tag):
def channel_file(relative):
"""The indexed pack icon is the only non-TOML file allowed on the channel."""
"""Only generated metadata and the two verified pack/server icons are allowed."""
path = Path(relative)
return path.suffix == '.toml' or path.as_posix() == 'icon.png'
return path.suffix == '.toml' or path.as_posix() in ('icon.png', 'server/server-icon.png')
def publish_channel(origin, version, source_head):
@@ -130,7 +130,7 @@ def publish_channel(origin, version, source_head):
'Client and server must publish the same Sanctuary artifact')
files = [path for path in staged.rglob('*') if path.is_file()]
pack.require(all(channel_file(path.relative_to(staged)) for path in files),
'Distribution branch must contain only generated TOML metadata and the pack icon')
'Distribution branch must contain only generated TOML metadata and verified icons')
if (staged / 'icon.png').exists():
pack.require((staged / 'icon.png').read_bytes() == (ROOT / 'packwiz/icon.png').read_bytes(),
'Distribution icon differs from the verified source icon')
+15 -3
View File
@@ -20,6 +20,7 @@ from urllib.request import urlopen
import zipfile
BOOTSTRAP_SHA256 = 'a8fbb24dc604278e97f4688e82d3d91a318b98efc08d5dbfcbcbcab6443d116c'
INSTALLER_SHA256 = '9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875'
def require(condition, message):
@@ -66,9 +67,12 @@ def verify_prism(data, client, pack_url):
with zipfile.ZipFile(io.BytesIO(data)) as archive:
require(archive.testzip() is None, 'Corrupt Prism archive')
require(set(archive.namelist()) == {'instance.cfg', 'mmc-pack.json', 'sanctuary-beta.png',
'minecraft/packwiz-installer-bootstrap.jar'}, 'Unexpected Prism archive contents')
'minecraft/packwiz-installer-bootstrap.jar', 'minecraft/packwiz-installer.jar'},
'Unexpected Prism archive contents')
require(digest(archive.read('minecraft/packwiz-installer-bootstrap.jar')) == BOOTSTRAP_SHA256,
'Prism bootstrap hash mismatch')
require(digest(archive.read('minecraft/packwiz-installer.jar')) == INSTALLER_SHA256,
'Prism Sanctuary installer hash mismatch')
require(digest(archive.read('sanctuary-beta.png')) == client['icon_sha256'], 'Prism icon drift')
config = configparser.ConfigParser(interpolation=None)
config.read_string(archive.read('instance.cfg').decode())
@@ -78,6 +82,7 @@ def verify_prism(data, client, pack_url):
'Prism instance is not configured for automatic updates')
command = ('"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" '
'--bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" '
'--bootstrap-no-update '
'--pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" ' + pack_url)
require(general['PreLaunchCommand'].replace('\\"', '"') == command, 'Prism pre-launch command drift')
components = json.loads(archive.read('mmc-pack.json'))['components']
@@ -95,7 +100,13 @@ def verify(mrpack, client, server, installed, version):
require(profile['manifest']['version'] == version, 'Channel version differs from dedicated CT')
require(client['manifest']['versions'] == server['manifest']['versions'], 'Client/server platform drift')
filename = 'sanctuary-' + version + '.jar'
require(set(server['mods']) == {filename, installed['fabric_api_filename']}, 'Unexpected server mods')
expected_server = {name for name, mod in client['mods'].items() if mod.get('side', 'both') != 'client'}
require(set(server['mods']) == expected_server, 'Unexpected server mods')
for name in expected_server - {filename, installed['fabric_api_filename']}:
require(server['mods'][name] == client['mods'][name], 'Client/server dependency drift: ' + name)
dependency = server['mods'][name]['download']
require(installed.get('mod_hashes', {}).get(name, {}).get(dependency['hash-format']) == dependency['hash'],
'Server dependency does not match the dedicated CT: ' + name)
require(client['mods'][filename] == server['mods'][filename], 'Client/server Sanctuary artifact drift')
download = server['mods'][filename]['download']
require(download['hash-format'] == 'sha256' and download['hash'] == installed['sanctuary_sha256'],
@@ -180,7 +191,8 @@ p=Path(sys.argv[1]);jars=list((p/'mods').glob('sanctuary-*.jar'));apis=list((p/'
assert len(jars)==len(apis)==1,'Ambiguous installed mods'
print(json.dumps({'running':subprocess.run(['systemctl','is-active','--quiet','sanctuary-beta.service']).returncode==0,
'sanctuary_sha256':hashlib.sha256(jars[0].read_bytes()).hexdigest(),
'fabric_api_filename':apis[0].name,'fabric_api_hashes':{a:hashlib.new(a,apis[0].read_bytes()).hexdigest() for a in ('sha256','sha512')}}))
'fabric_api_filename':apis[0].name,'fabric_api_hashes':{a:hashlib.new(a,apis[0].read_bytes()).hexdigest() for a in ('sha256','sha512')},
'mod_hashes':{f.name:{a:hashlib.new(a,f.read_bytes()).hexdigest() for a in ('sha256','sha512')} for f in (p/'mods').glob('*.jar')}}))
'''
installed = json.loads(ct(args.server_ct, 'python3', '-c', inventory, args.server_root))
client = channel(args.pack_url)
+56
View File
@@ -0,0 +1,56 @@
"""Verify that branding changes only the upstream window and survives Prism export."""
import configparser
import io
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest.mock import patch
import pack
import packwiz_installer as installer
import publish_web_pack as web
class InstallerDistributionTests(unittest.TestCase):
def test_upstream_engine_and_notices_are_preserved_byte_for_byte(self):
with zipfile.ZipFile(installer.OUTPUT.parent / 'packwiz-installer-upstream.jar') as upstream, \
zipfile.ZipFile(installer.OUTPUT) as branded:
self.assertIsNone(branded.testzip())
self.assertEqual(branded.namelist()[0], 'META-INF/MANIFEST.MF')
changed = {name for name in upstream.namelist() if not name.endswith('/')
and upstream.read(name) != branded.read(name)}
self.assertEqual(changed, {installer.WINDOW})
self.assertEqual(set(branded.namelist()) - set(upstream.namelist()), {
installer.LOGO, 'META-INF/sanctuary-packwiz/LICENSE',
'META-INF/sanctuary-packwiz/InstallWindow.java'})
self.assertEqual(branded.read(installer.LOGO), (installer.SOURCE / 'sanctuary.png').read_bytes())
def test_export_embeds_verified_installer_and_preserves_stable_channel(self):
channel = 'https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml'
# Use a fixture for the network fetch; the installer itself is the real built JAR.
fake_bootstrap = b'verified-bootstrap-fixture'
source = pack.ROOT
values, _ = pack.check()
with tempfile.TemporaryDirectory() as directory, \
patch.object(pack, 'ROOT', Path(directory)), \
patch.object(pack, 'check', return_value=(values, [])), \
patch.object(pack, 'urlopen', return_value=io.BytesIO(fake_bootstrap)), \
patch.object(pack, 'BOOTSTRAP_SHA256', installer.digest(fake_bootstrap)), \
patch.object(web, 'BOOTSTRAP_SHA256', installer.digest(fake_bootstrap)), \
patch.object(installer, 'build', return_value=installer.OUTPUT.read_bytes()):
pack.prism(channel)
exported = Path(directory) / 'build' / ('Sanctuary-Prism-' + values['pack_version'] + '.zip')
data = exported.read_bytes()
client = {'icon_sha256': pack.sha256(source / 'packwiz/icon.png'),
'manifest': pack.read_toml(source / 'packwiz/pack.toml')}
web.verify_prism(data, client, channel)
with zipfile.ZipFile(io.BytesIO(data)) as archive:
config = configparser.ConfigParser(interpolation=None)
config.read_string(archive.read('instance.cfg').decode())
self.assertIn('--bootstrap-no-update', config['General']['PreLaunchCommand'])
self.assertEqual(archive.read('minecraft/packwiz-installer.jar'), installer.OUTPUT.read_bytes())
if __name__ == '__main__':
unittest.main()
+42 -4
View File
@@ -31,7 +31,10 @@ class WebDistributionTests(unittest.TestCase):
def mrpack(self, jar=None, version='beta.280', extra=None):
index = {'formatVersion': 1, 'game': 'minecraft', 'versionId': version,
'dependencies': {'minecraft': '26.3', 'fabric-loader': '0.19.5'},
'files': [{'path': 'mods/fabric-api.jar', 'hashes': {'sha512': self.api['download']['hash']}, 'downloads': [self.api['download']['url']]}]}
'files': [{'path': 'mods/' + name,
'hashes': {mod['download']['hash-format']: mod['download']['hash']},
'downloads': [mod['download']['url']]}
for name, mod in self.profile['mods'].items() if name != self.filename]}
out = io.BytesIO()
with zipfile.ZipFile(out, 'w') as z:
z.writestr('modrinth.index.json', json.dumps(index))
@@ -67,9 +70,29 @@ class WebDistributionTests(unittest.TestCase):
with self.assertRaisesRegex(SystemExit, 'Unexpected MRpack overrides'):
web.verify(self.mrpack(extra='overrides/server.properties'), self.profile, self.profile, self.installed, self.version)
def prism(self, override='true', url='https://pack.example/pack.toml', extra=None):
def test_shared_dependencies_match_runtime_and_client_mods_stay_off_server(self):
lithium = {'filename': 'lithium.jar', 'side': 'both',
'download': {'url': 'https://example.test/lithium.jar', 'hash-format': 'sha512',
'hash': hashlib.sha512(b'lithium').hexdigest()}}
bobby = {'filename': 'bobby.jar', 'side': 'client',
'download': {'url': 'https://example.test/bobby.jar', 'hash-format': 'sha512',
'hash': hashlib.sha512(b'bobby').hexdigest()}}
self.profile['mods'].update({'lithium.jar': lithium, 'bobby.jar': bobby})
server = copy.deepcopy(self.profile)
del server['mods']['bobby.jar']
self.installed['mod_hashes'] = {'lithium.jar': {'sha512': lithium['download']['hash']}}
web.verify(self.mrpack(), self.profile, server, self.installed, self.version)
with self.assertRaisesRegex(SystemExit, 'Unexpected server mods'):
web.verify(self.mrpack(), self.profile, self.profile, self.installed, self.version)
self.installed['mod_hashes']['lithium.jar']['sha512'] = '0' * 128
with self.assertRaisesRegex(SystemExit, 'Server dependency does not match'):
web.verify(self.mrpack(), self.profile, server, self.installed, self.version)
def prism(self, override='true', url='https://pack.example/pack.toml', extra=None,
installer=b'installer', no_update=True):
command = ('"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" '
'--bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" '
+ ('--bootstrap-no-update ' if no_update else '') +
'--pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" ' + url)
config = '[General]\nname=Sanctuary Beta\nInstanceType=OneSix\nManagedPack=false\nOverrideCommands='+override+'\nPreLaunchCommand='+command.replace('"', '\\"')+'\n'
out = io.BytesIO()
@@ -77,22 +100,37 @@ class WebDistributionTests(unittest.TestCase):
z.writestr('instance.cfg', config)
z.writestr('sanctuary-beta.png', b'icon')
z.writestr('minecraft/packwiz-installer-bootstrap.jar', b'bootstrap')
z.writestr('minecraft/packwiz-installer.jar', installer)
z.writestr('mmc-pack.json', json.dumps({'components': [{'uid':'net.minecraft','version':'26.3'}, {'uid':'net.fabricmc.fabric-loader','version':'0.19.5'}]}))
if extra:
z.writestr(extra, 'unwanted')
return out.getvalue()
def test_prism_is_ready_to_import_with_automatic_updates(self):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')), \
patch.object(web, 'INSTALLER_SHA256', web.digest(b'installer')):
result = web.verify_prism(self.prism(), self.profile | {'icon_sha256': web.digest(b'icon')}, 'https://pack.example/pack.toml')
self.assertEqual(result['prism_sha256'], web.digest(self.prism()))
def test_prism_without_prelaunch_or_with_another_channel_is_rejected(self):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')), \
patch.object(web, 'INSTALLER_SHA256', web.digest(b'installer')):
for data in (self.prism(override='false'), self.prism(url='https://other.example/pack.toml')):
with self.subTest(), self.assertRaises(SystemExit):
web.verify_prism(data, self.profile | {'icon_sha256': web.digest(b'icon')}, 'https://pack.example/pack.toml')
def test_branding_cannot_be_replaced_or_overwritten_by_bootstrap(self):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')), \
patch.object(web, 'INSTALLER_SHA256', web.digest(b'installer')):
with self.assertRaisesRegex(SystemExit, 'Sanctuary installer hash mismatch'):
web.verify_prism(self.prism(installer=b'changed'), self.profile, 'https://pack.example/pack.toml')
with self.assertRaisesRegex(SystemExit, 'pre-launch command drift'):
web.verify_prism(self.prism(no_update=False), self.profile | {'icon_sha256': web.digest(b'icon')}, 'https://pack.example/pack.toml')
def test_installer_lock_matches_publication_verifier(self):
lock = json.loads((pack.ROOT / 'launcher/packwiz/installer.json').read_text())
self.assertEqual(lock['installer_sha256'], web.INSTALLER_SHA256)
def test_prism_cannot_include_worlds_or_unverified_tools(self):
with self.assertRaisesRegex(SystemExit, 'bootstrap hash mismatch'):
web.verify_prism(self.prism(), self.profile, 'https://pack.example/pack.toml')
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Verify pinned optional Fabric mods before staging or running the pack."""
import hashlib
import io
import json
from urllib.request import urlopen
import zipfile
import pack
def verify():
values = pack.properties()
pack.require(values['minecraft_version'] == '26.3' and values['loader_version'] == '0.19.5',
'Revalidate optional mods for the new Minecraft/Loader platform')
lock = json.loads((pack.ROOT / 'scripts/data/optional-mods.json').read_text())
cache = pack.ROOT / 'build/external-mods'
cache.mkdir(parents=True, exist_ok=True)
for slug, expected in lock.items():
mod = pack.read_toml(pack.SOURCE / 'mods' / (slug + '.pw.toml'))
pack.require(mod['filename'] == expected['filename'] and mod['side'] == expected['side']
and mod['update']['modrinth'] == {'mod-id': expected['project'], 'version': expected['artifact']},
'Optional mod pin drift: ' + slug)
pack.require(mod['option']['optional'] and mod['option']['default'],
'Pack extras must stay optional and enabled by default: ' + slug)
download = mod['download']
pack.require(download['hash-format'] == 'sha512', 'Expected SHA-512: ' + slug)
target = cache / mod['filename']
if not target.is_file():
with urlopen(pack.https_url(download['url']), timeout=45) as response:
data = response.read()
pack.require(hashlib.sha512(data).hexdigest() == download['hash'], 'Download hash mismatch: ' + slug)
target.write_bytes(data)
data = target.read_bytes()
pack.require(hashlib.sha512(data).hexdigest() == download['hash'], 'Cached mod hash mismatch: ' + slug)
with zipfile.ZipFile(io.BytesIO(data)) as archive:
metadata = json.loads(archive.read('fabric.mod.json'))
pack.require(metadata['id'] == expected['id'] and metadata['version'] == expected['version']
and metadata.get('depends', {}) == expected['depends']
and metadata.get('breaks', {}) == expected['breaks'],
'Fabric compatibility contract changed: ' + slug)
for nested in metadata.get('jars', []):
pack.require(archive.read(nested['file']).startswith(b'PK'), 'Missing nested dependency: ' + slug)
print('Four optional mods verified: exact versions, sides, hashes, dependencies and conflicts.')
if __name__ == '__main__':
verify()