Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34cdcfd00e | ||
|
|
c0b6b14e89 | ||
|
|
2734c2383a | ||
|
|
74dc1265a8 | ||
|
|
678d77fb91 | ||
|
|
97c601cd7f | ||
|
|
a93e261120 | ||
|
|
72f1cf50ae |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 :
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,105 @@
|
||||
# WHALE-01 — baleine fantôme volante
|
||||
|
||||
Branche `codex/flying-ghost-whale`, préparation locale **beta.290** le
|
||||
26 octobre 2026. Demande : import direct de la baleine de Sanctuary 26.2,
|
||||
avec son IA et son œuf, deux baleines présentes au maximum sur le serveur.
|
||||
|
||||
## Import
|
||||
|
||||
Sources historiques : `26.2/itsalive`, commit
|
||||
`94644a28f05b8010f06ec1181fe6c74a50ad80d6`. Le modèle, ses animations,
|
||||
la texture nuageuse 512 × 256, les particules et les sons Minecraft sont
|
||||
repris. Les API Fabric/Minecraft sont adaptées à **Minecraft 26.3**, sans
|
||||
nouvelle dépendance. La texture de la baleine est copiée octet pour octet.
|
||||
|
||||
- `sanctuary:flying_whale` : baleine géante, paisible, volante, intangible,
|
||||
invulnérable, sans butin. Dimensions historiques 36 × 11, rendu ×2,7.
|
||||
- IA conservée : compagnon vivant non spectateur le plus proche à 520 blocs,
|
||||
cibles successives entre 68 et 140 blocs du joueur, altitude libre, virages
|
||||
progressifs, souffle et lente errance sans joueur.
|
||||
- Spawner historique : contrôle toutes les 400 ticks, une apparition réussie
|
||||
par tiers de journée (8 000 ticks), dans l’Overworld à 92–138 blocs d’un
|
||||
joueur. Il attend si le chunk candidat n’est pas déjà chargé. Aucun chunk
|
||||
n’est généré ou forcé par le spawner. Le calendrier appartient au serveur
|
||||
courant et repart au démarrage, comme le calendrier historique.
|
||||
- Dissipation au plus tard après 14 400 ticks (12 minutes à 20 TPS), possible
|
||||
dès 12 000 ticks après plus de 900 ticks sans joueur proche. Seules les
|
||||
baleines nommées sont sauvegardées, comme en 26.2 ; elles restent soumises
|
||||
à cette durée de vie après chargement.
|
||||
|
||||
Le quota historique de deux baleines chargées est partagé entre **toutes les
|
||||
dimensions du serveur**, et étendu aux œufs, invocations, distributeurs et
|
||||
rechargements. Une troisième entité chargée est retirée. L’utilisation
|
||||
manuelle d’un œuf est refusée avant consommation si les deux places sont
|
||||
prises, avec un message FR/EN. Retirer une baleine libère immédiatement sa
|
||||
place. Les baleines nommées dans des chunks déchargés ne réservent pas de
|
||||
place ; elles repassent par le quota au rechargement.
|
||||
|
||||
## Œuf Minecraft personnalisé
|
||||
|
||||
`sanctuary:flying_whale_spawn_egg` figure dans l’onglet Œufs d’apparition.
|
||||
Son icône est dérivée directement de **`assets/minecraft/textures/item/egg.png`
|
||||
de Minecraft 26.3**, sans reprendre le dessin historique de l’œuf.
|
||||
`scripts/generate_whale_egg.py` recolore les pixels en bleu nuage pâle avec
|
||||
huit pixels lavande. Dimensions 16 × 16, les 90 pixels de coquille,
|
||||
transparence, contour asymétrique et emplacements des ombres restent natifs.
|
||||
Le script refuse une version ou une empreinte source différente et propose
|
||||
`--check` pour comparer les pixels livrés sans réécriture.
|
||||
|
||||
```sh
|
||||
python3.14 scripts/generate_whale_egg.py --check /chemin/minecraft-client.jar
|
||||
/give @s sanctuary:flying_whale_spawn_egg
|
||||
/summon sanctuary:flying_whale ~ ~10 ~
|
||||
```
|
||||
|
||||
Libellés de l’entité, de l’œuf et du refus de quota en FR/EN. Aucun familier
|
||||
miniature, capture de baleine ou conversion d’ID 26.2 n’est ajouté. Pas de
|
||||
changement de générateur, de sauvegarde existante ou de publication distante.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `./gradlew check build assemblePack
|
||||
-PsanctuaryFocusedTests=whale290,recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 3 min 34 s**, avec **19 tests
|
||||
serveur requis réussis**, tous les contrôles purs de `check`, le JAR
|
||||
`sanctuary-beta.290.jar` et les profils packwiz client/serveur assemblés.
|
||||
Ce périmètre serveur cible la baleine, les recettes et la progression ;
|
||||
il ne rejoue pas les suites de génération historiques.
|
||||
- Archive JAR relue : version beta.290, classes et deux textures présentes,
|
||||
un seul JEI embarqué, aucune classe de test ; même JAR unique dans chacun
|
||||
des deux profils packwiz. Reçu SHA-256 sous `build/whale290/artifacts.json`.
|
||||
- Suite native ciblée `:sanctuary:runGameTest
|
||||
-PsanctuaryFocusedTests=whale290 -PsanctuaryAtlasOnly=true` : **7 tests
|
||||
requis réussis**, dont les six scénarios baleine et le contrôle
|
||||
d’infrastructure. Usage réel de l’œuf en survie, non-consommation à quota
|
||||
plein, quota Overworld/Nether, refus de la troisième invocation, libération
|
||||
d’une place, apparition naturelle à rayon/altitude historiques, déplacement
|
||||
natif, dissipation, collision sans modification du terrain et sauvegarde
|
||||
nommée vérifiés. Les tests attendent que les chunks de développement soient
|
||||
effectivement chargés avant de vérifier leur population.
|
||||
- Parcours graphique **Vulkan / MoltenVK 1.4.2 / Apple M1**, avec
|
||||
**Sodium 0.9.2+mc26.3**, réussi en **49 s** : deux baleines géantes
|
||||
translucides, nageoires animées, texture historique, véritable déplacement
|
||||
autour du joueur, œuf en main et hotbar, libellés FR/EN. Les captures
|
||||
`0000_whale290-native-egg-and-two-whales.png` et
|
||||
`0001_whale290-ghosts.png` sont inspectées visuellement.
|
||||
- `generate_whale_egg.py --check` : pixels livrés identiques au résultat
|
||||
déterministe, 90 pixels opaques et masque alpha strictement natif.
|
||||
PNG de la baleine identique octet pour octet à la référence 26.2 ;
|
||||
JSON des libellés FR/EN et modèles d’objet valides.
|
||||
|
||||
Commande du parcours graphique :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryWhale290ClientTests=true -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryFocusedTests=whale290 -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journaux et captures ignorés sous `build/whale290/`. Validation en monde
|
||||
plat de développement neuf, graine `290` pour le client ; aucun monde
|
||||
personnel ni serveur Beta n’est ouvert ou modifié. Le calendrier complet
|
||||
sur une journée réelle et un parcours multijoueur distant ne sont pas
|
||||
revendiqués ; les trois fenêtres sont exercées par les tests de calendrier.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Happy creepers — réintégration
|
||||
|
||||
Branche `codex/happy-creepers`, worktree isolé du chantier baleines.
|
||||
Version locale préparée : **beta.291**, sur le socle beta.289.
|
||||
|
||||
Cette fiche conserve le reçu de la branche dédiée. Son commit `0ffd371a` est
|
||||
repris dans la [livraison intégrée beta.293](release-beta293.md), avec les
|
||||
baleines, le logo packwiz et les quatre mods facultatifs. Consulter ce reçu
|
||||
pour l'état de publication et les vérifications de la combinaison finale.
|
||||
|
||||
## Contrat
|
||||
|
||||
Le happy creeper est un état du creeper Minecraft existant. **Maj + clic droit
|
||||
avec une fleur** équipe son unique emplacement cosmétique de tête ; **Maj +
|
||||
clic droit à main vide** rend cette même fleur. Le geste, les contrôles de
|
||||
distance/visibilité, la consommation en survie et la conservation des composants
|
||||
de l'objet sont ceux du système de chapeaux existant. Un emplacement occupé
|
||||
doit être vidé avant d'équiper une autre fleur.
|
||||
|
||||
Toute fleur du tag Minecraft `minecraft:flowers` rend le creeper heureux :
|
||||
visage souriant historique, fleur exacte visible sur la tête animée, aucun
|
||||
ciblage hostile ni explosion. L'équipement annule immédiatement une mèche
|
||||
déjà amorcée, y compris au briquet ; retirer la fleur ne reprend pas cette
|
||||
ancienne mèche. Tant que la fleur est portée, briquet et boule de feu ne
|
||||
l'allument pas et ne sont pas consommés ou usés. Le creeper continue d'utiliser
|
||||
ses déplacements natifs et peut toujours subir des dégâts.
|
||||
|
||||
Retirer la fleur ou la remplacer par un objet sans rapport rétablit le visage,
|
||||
le ciblage et l'explosion ordinaires. Un pot de fleurs, des feuilles ou des
|
||||
graines ne déclenchent pas cet état. La charge électrique est indépendante :
|
||||
un creeper chargé peut être heureux, et la foudre peut charger un happy
|
||||
creeper. Retirer sa fleur conserve sa charge.
|
||||
|
||||
## Sauvegarde et périmètre
|
||||
|
||||
L'état heureux se déduit uniquement de l'objet dans `sanctuary:mob_head`, dont
|
||||
le schéma 1 est réutilisé sans modification. Cette pile est déjà sauvegardée
|
||||
et synchronisée par les chapeaux de mobs. Aucun second drapeau, nouvelle
|
||||
entité, changement de format, migration de monde ou tirage de variante n'est
|
||||
ajouté. Un creeper portant déjà une fleur devient donc heureux avec cette
|
||||
version. L'identité, la santé, la charge et les autres données natives restent
|
||||
sur le même creeper. Le port d'un chapeau conserve sa protection habituelle
|
||||
contre la disparition à distance.
|
||||
|
||||
Les nouvelles apparences de creepers restent pour un prochain ticket. Le
|
||||
suivi du joueur, les apparitions naturelles de happy creepers et les anciennes
|
||||
entités `happy_creeper:*`/`mobopia:*` ne font pas partie de cet incrément.
|
||||
Aucune nouvelle interface n'est ajoutée ; les messages d'équipement et de
|
||||
retrait FR/EN existants sont réutilisés.
|
||||
|
||||
## Vérification
|
||||
|
||||
- Serveur `happycreeper,cosmetics,headblocks` : **28 tests requis réussis**,
|
||||
dont sept tests happy creeper et le test d'infrastructure Fabric. Annulation
|
||||
immédiate d'une vraie mèche à vingt ticks, refus répété du ciblage/allumage,
|
||||
représailles, tous les objets du tag fleurs, récupération de la pile nommée,
|
||||
allumage natif sans fleur et rechargement du même creeper chargé vérifiés.
|
||||
- Parcours client **Vulkan avec Sodium** : **réussi**. Vrais appuis sur Maj et
|
||||
clics souris pour équiper/récupérer une fleur en survie ; synchronisation de
|
||||
la pile et du visage ; AI active pendant 85 ticks près d'un vrai joueur ;
|
||||
sourire, fleur sur la tête animée et aura électrique native vérifiés.
|
||||
Monde plat de développement neuf, graine `290`, Minecraft 26.3 sur Apple M1.
|
||||
Capture : `build/happy-creepers-screenshots/four-states-vulkan.png` ; de gauche
|
||||
à droite : chargé, chargé heureux, heureux, ordinaire.
|
||||
- `check build assemblePack` : **réussi en 3 min 55 s**, contrôles purs et
|
||||
**28 tests serveur** inclus. Export MRpack réussi, versions et dépendances
|
||||
exactes contrôlées ; un seul JAR Sanctuary embarqué, identique au JAR
|
||||
construit ; texture historique identique octet par octet et classes de test
|
||||
absentes. Logs : `build/happy-creepers-final-check-build.log`,
|
||||
`build/happy-creepers-server.log`, `build/happy-creepers-vulkan.log`.
|
||||
Reçu : `build/happy-creepers-validation.json`.
|
||||
|
||||
Commandes reproductibles :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=happycreeper,cosmetics,headblocks \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryHappyCreeperClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryClientHeapGb=4 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le premier essai de `check build` associait par erreur le preset unifié et le
|
||||
profil rapide ; il a échoué au choix du générateur avant les tests. Le profil
|
||||
plat `sanctuaryAtlasOnly=true` évite ce conflit de configuration existant.
|
||||
Aucun monde personnel, serveur Beta, canal packwiz ou Prism n'est modifié.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.291](../build/Sanctuary-beta.291.mrpack), 16 724 142 octets.
|
||||
SHA-256 : `3c2d630fc85e88b08bac94cc2244e975d3552fcac4d508ad366adea730062468`.
|
||||
- JAR : `mods/sanctuary/build/libs/sanctuary-beta.291.jar`.
|
||||
SHA-256 : `e8e315db1e4831400244d17429723099fbfe3a352387d8a9cf0ecb01d8170e9e`.
|
||||
|
||||
Cette branche part de beta.289 et ne contient pas le travail parallèle des
|
||||
baleines préparé en beta.290. L'intégration des deux branches précède toute
|
||||
éventuelle publication commune. Aucun tag ni artefact distant n'est créé.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Prochaine passe — mods du pack et modules personnels
|
||||
|
||||
Cadrage du **26 octobre 2026**, à la demande de Koka. Initialement préparés
|
||||
pour après beta.291, les quatre tickets d'intégration sont désormais repris
|
||||
dans [la préparation locale beta.292](optional-mods-beta292.md), à sa demande.
|
||||
Dozei Stream et Only Fun restent des contrats futurs. Chaque implémentation
|
||||
prendra sa propre branche `codex/<sujet>` et son prochain compteur libre.
|
||||
Les versions du cadrage ci-dessous sont des candidats repérés pour **Minecraft 26.3,
|
||||
Fabric 0.19.5 et Java 25**, pas des intégrations validées. Tous les essais
|
||||
graphiques cibleront Vulkan, avec Sodium puis sans si le mod le permet.
|
||||
|
||||
L'API Modrinth a été interrogée avec les filtres exacts `game_versions=["26.3"]`
|
||||
et `loaders=["fabric"]`. Les JAR candidats ont été téléchargés dans le dossier
|
||||
ignoré `build/optional-mods291/`, vérifiés contre leur SHA-512 publié et leurs
|
||||
`fabric.mod.json` relus. Revalider versions, dépendances, licences et hashes au
|
||||
début de chaque ticket ; aucun binaire téléchargé n'entre dans Git.
|
||||
|
||||
| Ticket | Branche prévue | Candidat observé | Résultat à obtenir |
|
||||
| --- | --- | --- | --- |
|
||||
| PACK-VOICE-01 | `codex/simple-voice-chat` | [2.6.26+26.3, Fabric, bêta](https://modrinth.com/plugin/simple-voice-chat/version/cZm965sW) | Conversation de proximité client/serveur, compatible avec l'acoustique Sanctuary |
|
||||
| PACK-BOBBY-01 | `codex/bobby-optional` | [5.2.16+mc26.3](https://modrinth.com/mod/bobby/version/J7YnfCss) | Voir les chunks déjà reçus au-delà de la distance envoyée par le serveur, installation facultative |
|
||||
| PACK-LITHIUM-01 | `codex/lithium-integration` | [0.26.2+mc26.3, Fabric](https://modrinth.com/mod/lithium/version/xS0Q8LSi) | Réduire le coût de simulation en conservant les règles Sanctuary |
|
||||
| PACK-CULL-01 | `codex/entity-culling` | [1.11.3 pour 26.3, Fabric](https://modrinth.com/mod/entityculling/version/X1g3VhTx) | Éviter le rendu des entités et blocs entités masqués, sans disparitions incorrectes |
|
||||
| MODULE-DOZEI-01 | `codex/dozei-stream-contract` | Pas encore réalisé, aucun artefact fourni | Définir le premier résultat vérifiable de Dozei Stream |
|
||||
| MODULE-ONLYFUN-01 | `codex/only-fun-contract` | Mod personnel futur | Définir une première interaction potache autonome |
|
||||
|
||||
## PACK-VOICE-01 — Simple Voice Chat
|
||||
|
||||
Le JAR 2.6.26 déclare `minecraft=26.3.x`, Java ≥25, Loader ≥0.19.3 ;
|
||||
Fabric API doit être ≥0.160.5+26.3, ce qui correspond au socle actuel.
|
||||
**Conflit constaté : il déclare `breaks.sound_physics_remastered=<1.5.2`,
|
||||
alors que Sanctuary distribue 1.5.1+26.3.** Le candidat 2.6.25 a le même refus.
|
||||
Le JAR [2.6.24+26.3](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy)
|
||||
ne déclare pas ce conflit ; cela ne prouve pas sa compatibilité en partie.
|
||||
|
||||
Décider et tester soit une mise à jour délibérée de Sound Physics et de son
|
||||
adaptateur natif Sanctuary, soit une version vocale antérieure compatible.
|
||||
Ne pas contourner les dépendances de Fabric. Conserver un artefact autonome
|
||||
provenant de l'auteur et ses conditions de distribution.
|
||||
|
||||
Acceptation : connexion de deux clients de développement, voix de proximité,
|
||||
muet, push-to-talk, déconnexion/reconnexion, coexistence Sound Physics et
|
||||
préférences conservées. Vérifier les profils packwiz client/serveur, le refus
|
||||
des versions incompatibles et les interfaces FR/EN ajoutées par Sanctuary.
|
||||
L'activation réseau de la Beta sera une étape de livraison distincte : le
|
||||
[protocole amont](https://dev.modrepo.de/minecraft/voicechat/wiki/server_setup)
|
||||
utilise UDP et un port vocal propre, par défaut 24454. Tester depuis l'extérieur
|
||||
avec `/voicechat test`, puis un échange audible ; l'ouverture du port Minecraft
|
||||
TCP ne suffit pas. Aucun port ni serveur personnel n'est modifié par ce ticket
|
||||
documentaire.
|
||||
|
||||
## PACK-BOBBY-01 — cache client facultatif
|
||||
|
||||
Le candidat déclare un environnement client, Minecraft `~26.3`, Loader
|
||||
≥0.18.4 et refuse Sodium <0.6.0 ; Sodium 0.9.2 du pack satisfait ce seul
|
||||
contrat déclaré. Cloth Config et Mod Menu sont suggérés, pas requis dans le
|
||||
JAR inspecté. Le [principe amont](https://github.com/Johni0702/bobby) est de
|
||||
mettre en cache les chunks envoyés dans `.bobby`, puis d'afficher leur dernière
|
||||
copie connue à distance. Ce cache ne simule pas les chunks et ne remplace pas
|
||||
leur état serveur actuel.
|
||||
|
||||
Acceptation : choix packwiz **facultatif**, installation/désinstallation par
|
||||
le sélecteur et préférence conservée au second lancement ; parcours puis
|
||||
retour sur un serveur de développement avec distance d'envoi courte ; chunks
|
||||
lointains visibles sous Vulkan, actualisation lors de leur réception réelle,
|
||||
claims/Atlas et blocs Sanctuary cohérents au retour. Aucun monde personnel
|
||||
utilisé comme `bobby-fallback`. Définir une valeur initiale de distance et une
|
||||
politique de cache mesurées avant livraison ; pas de génération supplémentaire
|
||||
imposée au serveur.
|
||||
|
||||
## PACK-LITHIUM-01 — simulation
|
||||
|
||||
Le [candidat](https://github.com/CaffeineMC/lithium) peut fonctionner côté client
|
||||
et serveur. Ses dépendances déclarées sont Minecraft `~26.3`, Loader ≥0.19.5
|
||||
et MixinExtras ≥0.5.5. Vérifier la version réellement fournie par Loader ; ne
|
||||
pas ajouter une bibliothèque par supposition. Définir séparément les profils
|
||||
serveur et client, sans faire dépendre l'autorité serveur du choix d'un joueur.
|
||||
|
||||
Acceptation : démarrage et rechargement d'un monde de test, recettes et
|
||||
inventaires, IA des infectés/familiers/baleines, interactions multiblocs et
|
||||
budget de ticks comparés avant/après sur le même scénario. Les gains doivent
|
||||
être mesurés ; conserver les identifiants, sauvegardes et règles serveur.
|
||||
|
||||
## PACK-CULL-01 — masquage visuel
|
||||
|
||||
Le [candidat](https://github.com/tr7zw/EntityCulling) cible Minecraft `~26.3`
|
||||
et requiert Fabric API ; TRansition et TRender sont déjà imbriqués dans le
|
||||
JAR inspecté. Vérifier leurs hooks de rendu sur Vulkan avant toute inclusion,
|
||||
ainsi que les conditions de distribution du binaire original.
|
||||
|
||||
Acceptation : comparaison activé/désactivé sur une scène de développement,
|
||||
avec mur opaque, fenêtres, verre, sortie d'occlusion et changement rapide de
|
||||
caméra. Vérifier familiers portés, cosmétiques, baleines translucides, coffres
|
||||
et multiblocs ; aucune disparition visible incorrecte. Mesurer le gain et le
|
||||
coût CPU. Aucun essai OpenGL ne vaut validation de ce ticket.
|
||||
|
||||
## Modules personnels — contrats à écrire
|
||||
|
||||
**MODULE-DOZEI-01 :** conserver le nom donné « Dozei Stream » ; son rôle,
|
||||
ses sources, sa cible client/serveur, ses dépendances et son premier scénario
|
||||
restent à préciser avec le créateur. Ne déduire aucune fonctionnalité du nom.
|
||||
Acceptation du cadrage : une action observable et un périmètre testable avant
|
||||
de créer un faux artefact ou une entrée packwiz sans téléchargement.
|
||||
|
||||
**MODULE-ONLYFUN-01 :** reprendre la [vision Only Fun](vision.md#only-fun--interactions-sociales-et-absurdes),
|
||||
avec l'intention confirmée d'ajouter de l'humour « pipi caca ». C'est un mod
|
||||
personnel autonome, séparé du socle nécessaire pour jouer à Sanctuary.
|
||||
Choisir une première interaction, définir ses entrées, effets, données
|
||||
persistantes et branchements facultatifs au calendrier/aux événements.
|
||||
Acceptation du cadrage : petit scénario jouable défini, comportement en solo
|
||||
et serveur, installation indépendante et libellés FR/EN prévus. Le contenu
|
||||
futur de la vision ne devient pas une fonctionnalité livrée par ce document.
|
||||
|
||||
## Ordre proposé et fin d'un ticket
|
||||
|
||||
Commencer par Bobby facultatif, puis les optimisations mesurées ; traiter la
|
||||
voix avec sa décision Sound Physics et son parcours réseau. Les deux modules
|
||||
personnels commencent par leur contrat, sans dépendance implicite à ces mods.
|
||||
Cet ordre reste une proposition pour la prochaine séance.
|
||||
|
||||
Chaque intégration livrée passe `./gradlew check build assemblePack`, les
|
||||
essais propres au comportement ajouté, deux synchronisations packwiz isolées,
|
||||
la conservation des choix et le contrôle du profil serveur. Décrire les
|
||||
résultats réels et les limites restantes avant toute publication demandée.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Sanctuary beta.293
|
||||
|
||||
- Happy Creepers repris de la branche dédiée `codex/happy-creepers` : Maj +
|
||||
clic droit avec une fleur pour la porter, visage souriant et comportement
|
||||
passif ; retrait à main vide avec le même geste. Charge électrique conservée.
|
||||
- Baleines fantômes volantes et œuf natif de beta.290 inclus.
|
||||
- Logo Sanctuary affiché pendant les téléchargements et mises à jour packwiz,
|
||||
avec l'installateur intégré au ZIP Prism prêt à importer.
|
||||
- Simple Voice Chat 2.6.24+26.3, Bobby 5.2.16+mc26.3, Lithium 0.26.2+mc26.3 et
|
||||
Entity Culling 1.11.3 pour 26.3 disponibles comme choix facultatifs du client.
|
||||
Lithium et la voix font également partie du profil serveur.
|
||||
- Sound Physics 1.5.1, Sodium 0.9.2, JEI embarqué et Minecraft 26.3 / Fabric
|
||||
0.19.5 conservés. La version vocale retenue respecte ces dépendances.
|
||||
- Sauvegardes et réglages restent attachés à la même instance ; aucun changement
|
||||
de génération ou de format de monde. Dozei Stream et Only Fun restent futurs.
|
||||
|
||||
Le ZIP Prism suit le canal stable à chaque lancement. Les anciens utilisateurs
|
||||
gardent leur instance ; la première mise à jour de l'habillage doit aussi
|
||||
installer le nouvel outil packwiz et conserver `--bootstrap-no-update`.
|
||||
@@ -0,0 +1,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.
|
||||
@@ -0,0 +1,164 @@
|
||||
# PACK-292 — voix, cache de chunks et optimisations
|
||||
|
||||
Préparation locale du **26 octobre 2026**, branche
|
||||
`codex/optional-mods-integration`, depuis le logo packwiz beta.291.
|
||||
Le même logo et le même canal stable sont conservés.
|
||||
|
||||
## Mods intégrés
|
||||
|
||||
| Mod | Binaire retenu pour Minecraft 26.3 / Fabric | Client | Serveur |
|
||||
| --- | --- | --- | --- |
|
||||
| [Simple Voice Chat](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy) | 2.6.24+26.3, bêta | Facultatif, coché initialement | Inclus, activé initialement |
|
||||
| [Bobby](https://modrinth.com/mod/bobby/version/J7YnfCss) | 5.2.16+mc26.3 | Facultatif, coché initialement | Exclu |
|
||||
| [Lithium](https://modrinth.com/mod/lithium/version/xS0Q8LSi) | 0.26.2+mc26.3 | Facultatif, coché initialement | Inclus, activé initialement |
|
||||
| [Entity Culling](https://modrinth.com/mod/entityculling/version/X1g3VhTx) | 1.11.3 pour mc26.3 | Facultatif, coché initialement | Exclu |
|
||||
|
||||
Chaque téléchargement utilise le binaire original, sa version Modrinth
|
||||
immuable et son SHA-512 dans `packwiz/mods/*.pw.toml`. Les JAR restent séparés,
|
||||
sans dépendance obligatoire ni copie imbriquée dans le JAR Sanctuary. Les
|
||||
descriptions du sélecteur packwiz sont en FR/EN. Les interfaces amont conservent
|
||||
leurs propres traductions, vérifiées pour le menu vocal français et anglais.
|
||||
|
||||
`scripts/data/optional-mods.json` fixe les identifiants, versions, côtés,
|
||||
dépendances et conflits attendus. `verify_optional_mods.py`, exécuté par
|
||||
`check` et `assemblePack`, vérifie les métadonnées Fabric des JAR, leur hash et
|
||||
les bibliothèques imbriquées. Les quatre fichiers réellement utilisés par
|
||||
Gradle sont aussi comparés aux binaires vérifiés pendant la validation.
|
||||
Minecraft reste **26.3**, Loader **0.19.5**, Java **25**, Fabric API
|
||||
**0.160.5+26.3** ; MixinExtras **0.5.5** est fourni par Loader et satisfait Lithium.
|
||||
|
||||
## Choix de version vocale
|
||||
|
||||
Les versions vocales 2.6.25 et 2.6.26 refusent explicitement Sound Physics
|
||||
Remastered <1.5.2. Le pack conserve son **1.5.1+26.3** et son interface native,
|
||||
donc la voix est épinglée en **2.6.24+26.3**, sans contournement des dépendances.
|
||||
Le plug-in Sound Physics est chargé par le serveur vocal et la connexion locale
|
||||
est authentifiée pendant le parcours client. Les mises à jour de ces deux mods
|
||||
doivent être traitées ensemble à la prochaine évolution de version.
|
||||
|
||||
Le menu vocal s'ouvre avec sa touche amont **V**, configurable ; la touche de
|
||||
push-to-talk se choisit dans les réglages vocaux. Aucun raccourci ni préférence
|
||||
personnelle n'est remplacé par le pack. La voix reste facultative pour le joueur.
|
||||
|
||||
Le profil serveur contient désormais **Fabric API, Sanctuary, Lithium et Simple
|
||||
Voice Chat**. Son choix est indépendant des options d'un client. Le kit Linux
|
||||
reste sans interface ; son installateur officiel est conservé. La vérification
|
||||
Web compare aussi les nouveaux JAR serveur au canal avant toute publication.
|
||||
|
||||
L'utilisation sur la Beta dédiée exigera le
|
||||
[port UDP vocal](https://dev.modrepo.de/minecraft/voicechat/wiki/server_setup),
|
||||
par défaut 24454, et un échange audible entre deux joueurs. Cette préparation
|
||||
ne modifie ni la CT Beta, ni ses règles réseau, ni l'instance personnelle. Le
|
||||
microphone est désactivé dans le test de développement ; aucune captation de la
|
||||
voix réelle de l'utilisateur n'est nécessaire à la validation réseau locale.
|
||||
|
||||
## Portée de Bobby et d'Entity Culling
|
||||
|
||||
[Bobby](https://github.com/Johni0702/bobby) conserve les chunks déjà envoyés
|
||||
par le serveur dans son cache client `.bobby`, puis peut afficher leur dernière
|
||||
copie connue au-delà de la distance d'envoi. Il ne génère pas de chunks et ne
|
||||
simule pas les zones lointaines. Le serveur reste autoritaire lorsqu'il renvoie
|
||||
l'état courant. Le pack ne distribue aucun monde `bobby-fallback`, cache, ni
|
||||
configuration personnelle ; la limite de distance et la conservation du cache
|
||||
restent celles du mod et des réglages du joueur.
|
||||
|
||||
[Entity Culling](https://github.com/tr7zw/EntityCulling) conserve son moteur
|
||||
amont et ses bibliothèques TRender/TRansition imbriquées. L'occlusion et la
|
||||
réapparition d'une entité sont exercées sur Vulkan. Aucun gain chiffré de FPS
|
||||
ou de ticks n'est revendiqué ; les validations vérifient ici le fonctionnement
|
||||
et la coexistence, sans benchmark représentatif de tous les mondes Sanctuary.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- `./gradlew check build assemblePack assembleServerPack
|
||||
-PsanctuaryFocusedTests=recipes,progression,smoker285,hunter286
|
||||
-PsanctuaryAtlasOnly=true -PsanctuaryOptional292ServerTests=true` :
|
||||
**réussi en 2 min 52 s**, **46 tests serveur requis réussis**, Lithium et voix
|
||||
effectivement chargés. Recettes, progression, lasso du Smoker et bond du Hunter
|
||||
sont exercés avec ces dépendances. Les contrôles de ressources, de packwiz et
|
||||
de l'habillage passent aussi. Le test de quota de baleines déjà en échec dans
|
||||
[beta.291](packwiz-logo-beta291.md) reste hors de ce périmètre, sans correction
|
||||
ni validation nouvelle de ce cas.
|
||||
- Parcours graphique **Vulkan / MoltenVK 1.4.2 / Apple M1 avec Sodium 0.9.2**,
|
||||
réussi en **38 s** : quatre mods présents, Sound Physics initialisé, entité
|
||||
masquée derrière le mur puis visible après retrait ; Bobby actif, stockage
|
||||
persistant et **35 chunks de cache** visibles après un déplacement de huit
|
||||
chunks avec une distance d'envoi de quatre. Authentification et connexion UDP
|
||||
vocales réelles, menus vocaux FR/EN ouverts en gardant le microphone muet.
|
||||
Les captures du mur, de l'entité réapparue et du menu FR sont inspectées.
|
||||
- Le même parcours **Vulkan sans Sodium** réussit en **43 s**, avec les mêmes
|
||||
contrôles, 35 chunks de cache et menus FR/EN. Un premier passage avait atteint
|
||||
tous les contrôles avant de se bloquer dans la synchronisation des threads du
|
||||
lanceur Fabric pendant la fermeture du monde ; il a été interrompu après
|
||||
capture des piles. La nouvelle exécution se ferme normalement. Cette limite
|
||||
du premier essai reste dans le journal, sans la présenter comme un succès.
|
||||
- Deux synchronisations client depuis le ZIP Prism réussies : **huit JAR**,
|
||||
un seul Sanctuary, mêmes hashes au second passage, logo de l'installateur
|
||||
conservé et trois fichiers témoins personnels inchangés. Puis deux passages
|
||||
supplémentaires avec un choix Bobby désactivé déjà persisté, simulé dans le
|
||||
cache de test : Bobby n'est pas réinstallé et le choix reste faux.
|
||||
- Deux synchronisations serveur réussies : exactement les quatre JAR du
|
||||
profil serveur ; aucun Bobby, Entity Culling, Sodium ou Sound Physics client.
|
||||
- Les **11 tests** du vérificateur Web passent, y compris la comparaison des
|
||||
dépendances partagées avec le relevé serveur et le refus d'un mod client dans
|
||||
son profil. Le MRpack exporté possède les sept dépendances distantes et leurs
|
||||
bons côtés et options ; les overrides sont limités à Sanctuary et son icône.
|
||||
- Contrôle complémentaire des baleines avec les quatre mods, Sodium et Vulkan :
|
||||
les assertions de rendu translucide, modèle animé, œuf FR/EN, deux entités
|
||||
chargées et déplacement réel atteignent `WHALE290_CLIENT_PASS`. La capture
|
||||
`0001_whale290-ghosts.png` est inspectée. Ce parcours se bloque ensuite à la
|
||||
fermeture dans la synchronisation du lanceur de tests Fabric et est arrêté
|
||||
après capture des piles ; il ne constitue pas une exécution complète réussie.
|
||||
- Après ajout des notices, le JAR et les archives sont réassemblés et les
|
||||
synchronisations sont répétées sur les binaires finaux. Les **2 658 classes
|
||||
de jeu** restent identiques à beta.291. Le vérificateur Web accepte le MRpack
|
||||
et le ZIP Prism finaux contre les métadonnées préparées et les hashes du
|
||||
serveur de test synchronisé, avec état de service simulé pour cette vérification
|
||||
hors ligne ; aucune CT de production n'est contactée.
|
||||
|
||||
Les essais utilisent uniquement des mondes et dossiers de développement sous
|
||||
`build/`. Les copies et hashes finaux sont consignés dans
|
||||
`build/optional-mods292/`, avec les journaux, captures et reçus natifs.
|
||||
Le passage vocal audible à deux joueurs, l'accès UDP depuis Internet et les
|
||||
rendus propres aux familiers et multiblocs avec culling restent des
|
||||
vérifications complémentaires à effectuer avant d'en revendiquer la couverture.
|
||||
|
||||
Commande du parcours client principal (ajouter `-PsanctuarySodiumClientTests=true`
|
||||
pour le passage avec Sodium) :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryOptional292ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryFocusedTests=recipes,progression
|
||||
```
|
||||
|
||||
L'option supplémentaire `-PsanctuaryOptional292WhaleOnly=true` sélectionne
|
||||
le diagnostic des baleines avec le même ensemble de mods ; son blocage de
|
||||
fermeture observé ci-dessus reste ouvert.
|
||||
|
||||
## Livraison et suite
|
||||
|
||||
Le compteur du pack et du mod est aligné sur **beta.292**. Les archives locales
|
||||
conservent l'habillage packwiz, les options et le canal de la Beta. Un ZIP Prism
|
||||
préparé ne fait pas avancer le canal public : avant publication, ce canal sert
|
||||
encore la version publiée. Aucune installation de jeu, sauvegarde personnelle,
|
||||
publication Web, release Gitea ou configuration réseau n'est modifiée ici.
|
||||
|
||||
Dozei Stream et Only Fun restent des modules personnels futurs, avec leurs
|
||||
[tickets de cadrage](integrations-mods-next.md). Aucun artefact fictif n'est
|
||||
ajouté au pack pour ces projets.
|
||||
|
||||
Artefacts locaux vérifiés, reçus dans `artifacts.json` :
|
||||
|
||||
| Fichier dans `build/` | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `Sanctuary-beta.292.mrpack` | 16 753 207 | `28980cec018fc81f700cb8ecb3981199537e04ff737bf58b220ce442b7e5dc56` |
|
||||
| `Sanctuary-Prism-beta.292.zip` | 3 928 525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.292.zip` | 4 036 530 | `0ac362bbfbfd2c03efdaf6b209d9319e648ad546bee074521038fffc86bb640a` |
|
||||
|
||||
JAR Sanctuary : `0163da08897de96559ab51f482363bd00c03cfb3309784cdb6e112acf25a4862`.
|
||||
Le ZIP Prism d'amorçage a les mêmes octets qu'en beta.291 : il contient l'outil,
|
||||
le logo et l'adresse stable, et les versions de Minecraft/Loader n'ont pas changé.
|
||||
Le MRpack et les métadonnées du canal préparé contiennent les nouveaux mods.
|
||||
@@ -0,0 +1,109 @@
|
||||
# PACK-LOGO — logo Sanctuary pendant packwiz
|
||||
|
||||
Branche `codex/packwiz-sanctuary-logo`, préparation locale **beta.291**, depuis
|
||||
le socle beta.290. Demande du 26 octobre 2026 : afficher le bandeau Sanctuary
|
||||
fourni dans la fenêtre de téléchargement et de mise à jour, avant Minecraft.
|
||||
|
||||
## Résultat
|
||||
|
||||
Le PNG `minecraft.png` fourni par le créateur est conservé octet pour octet
|
||||
dans `launcher/packwiz/sanctuary.png` (1024 × 256, RGBA). Son SHA-256 est
|
||||
`efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157`.
|
||||
La fenêtre l'affiche sur fond noir à 640 × 160, proportions conservées,
|
||||
au-dessus de la progression réelle. Ses boutons, le texte initial et le
|
||||
compte à rebours suivent la langue du système : français, anglais sinon.
|
||||
Les messages et dialogues du moteur amont conservent leurs traductions amont.
|
||||
|
||||
`scripts/packwiz_installer.py` télécharge le JAR officiel
|
||||
[packwiz-installer v0.5.14](https://github.com/packwiz/packwiz-installer/releases/tag/v0.5.14),
|
||||
vérifie son SHA-256, puis remplace uniquement `InstallWindow.class` par
|
||||
l'adaptation Java de sa fenêtre Swing. Toutes les autres classes et notices
|
||||
restent identiques : téléchargement, vérification des hashes, sélection des
|
||||
mods facultatifs, erreurs et annulation continuent de dépendre de packwiz.
|
||||
La licence MIT de comp500 et le source de l'adaptation sont joints au JAR.
|
||||
Le logo est embarqué : aucun téléchargement d'image n'est requis pour l'afficher.
|
||||
|
||||
Le build utilise Java 25 ; les classes de la fenêtre ciblent Java 8 comme
|
||||
l'amont. Les SHA-256 de l'amont, du logo et du résultat sont fixés dans
|
||||
`launcher/packwiz/installer.json`. Une compilation différente échoue avant
|
||||
d'écrire le JAR de distribution. Les dépendances et JAR générés restent dans
|
||||
`build/packwiz-branding/`, ignoré par Git.
|
||||
|
||||
## Archive Prism et mises à jour
|
||||
|
||||
La commande habituelle `python3.14 scripts/pack.py prism <url-du-canal>`
|
||||
embarque le bootstrap officiel vérifié et le nouvel installateur vérifié,
|
||||
avec la même configuration Minecraft 26.3 / Fabric 0.19.5 et le même canal
|
||||
stable. `--bootstrap-no-update` empêche le bootstrap de remplacer la fenêtre
|
||||
Sanctuary par le binaire officiel. Cette option concerne **l'installateur** ;
|
||||
la synchronisation des mods packwiz reste active à chaque lancement.
|
||||
Le nom beta.291 du ZIP local n'avance pas ce canal : avant publication, un
|
||||
lancement visant l'adresse stable récupère la version déjà publiée, beta.289
|
||||
au moment de cette préparation. Les essais beta.291 utilisent un serveur HTTP
|
||||
de développement isolé, jamais le monde ni le serveur personnel.
|
||||
|
||||
Les évolutions de l'installateur doivent donc être préparées et testées
|
||||
explicitement, avec mise à jour du verrou et du vérificateur Web. Une simple
|
||||
avance du canal de mods ne met pas à jour l'outil d'une instance existante.
|
||||
Lors d'une future synchronisation locale demandée, sauvegarder l'instance,
|
||||
copier le JAR vérifié et ajouter l'option à sa commande avant lancement en
|
||||
conservant les autres réglages ; conserver cette même instance, sans réimport.
|
||||
Le kit serveur conserve son installateur officiel et son fonctionnement sans
|
||||
interface. Le logo n'est pas ajouté à l'index des fichiers du monde ou du jeu.
|
||||
|
||||
`verify_prism` contrôle désormais les deux JAR par leurs hashes et la présence
|
||||
de l'option qui conserve l'habillage. Les sauvegardes et fichiers personnels
|
||||
restent refusés dans le ZIP. Le MRpack n'embarque pas les commandes Prism et
|
||||
ne suffit pas à installer cet habillage.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Logo fourni identique octet pour octet ; JAR résultant contrôlé par son
|
||||
SHA-256 `9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875`.
|
||||
- Les deux tests de distribution de `test_packwiz_installer.py` passent :
|
||||
toutes les entrées de l'amont sont relues, seule la classe de fenêtre change,
|
||||
et le véritable JAR vérifié est retrouvé dans l'export Prism.
|
||||
- Les dix tests de `test_publish_web_pack.py` passent : outils modifiés refusés,
|
||||
option de conservation obligatoire, contrôles de publication préservés.
|
||||
- Parcours Swing réel en FR et EN réussi avec le `GUIHandler` officiel :
|
||||
progression déterminée/indéterminée, options, Annuler, Continuer et compte à
|
||||
rebours. Les captures `preview-fr.png` et `preview-en.png` sont inspectées.
|
||||
Le test ne lance ni Minecraft ni un microphone.
|
||||
- Deux exécutions de `check build assemblePack` avec le profil
|
||||
`whale290,recipes,progression` échouent sur le même test serveur hérité :
|
||||
`eggQuotaNaturalSpawnAndFlightUseActualServerEntities`, quota après utilisation
|
||||
de l'œuf entre Overworld et Nether (18 réussites sur 19). Le profil isolé
|
||||
`whale290` échoue également sur ce cas (6 réussites sur 7). Les sources du
|
||||
comportement et de son test ne sont pas modifiées dans ce chantier. Cet échec
|
||||
reste ouvert ; aucune validation complète des baleines n'est revendiquée.
|
||||
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` réussit en **2 min 42 s**, avec **13 tests serveur
|
||||
requis réussis**, les contrôles purs de `check`, le JAR et les profils client
|
||||
et serveur assemblés. Ce périmètre ne valide pas le test de baleines en échec.
|
||||
- Comparaison des JAR beta.290/beta.291 : **2 658 classes de jeu identiques**,
|
||||
aucune classe ajoutée. Le compteur et les notices évoluent, sans changement
|
||||
du code de jeu.
|
||||
- Deux lancements du véritable bootstrap et de l'installateur adapté réussis
|
||||
depuis le ZIP extrait dans `native instance with spaces/`, avec les arguments
|
||||
de sa commande Prism et un canal HTTP local beta.291. Un seul JAR Sanctuary,
|
||||
fichiers gérés identiques au second passage, installateur avec logo conservé,
|
||||
trois fichiers témoins personnels inchangés. Aucun monde ouvert. Le mode
|
||||
sans interface vérifie la synchronisation ; les tests Swing ci-dessus
|
||||
vérifient la fenêtre. Reçu `native-validation.json`.
|
||||
|
||||
Archive locale `build/Sanctuary-Prism-beta.291.zip` : **3 928 525 octets**,
|
||||
SHA-256 `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9`.
|
||||
JAR Sanctuary beta.291 :
|
||||
`bd130418a05f820dffd86b00595c5b7ce96293ec82867926f5493a95736aee73`.
|
||||
Les interfaces Linux/Windows, le lancement Minecraft depuis Prism et une
|
||||
mise à jour de l'instance personnelle ne sont pas revendiqués par ces essais
|
||||
macOS. Le test de quota des baleines reste à résoudre avant une validation
|
||||
complète de cette base de jeu.
|
||||
|
||||
Les reçus et captures restent sous `build/packwiz-branding/`.
|
||||
|
||||
Les mods évoqués pendant ce chantier sont seulement
|
||||
[préparés en tickets pour la passe suivante](integrations-mods-next.md).
|
||||
Aucune publication du canal, du site, du serveur Beta ou de l'instance
|
||||
personnelle n'est demandée ou effectuée par cette préparation locale.
|
||||
@@ -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
|
||||
|
||||
@@ -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.**
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# beta.293 — livraison intégrée
|
||||
|
||||
Livraison demandée le **26 octobre 2026** pour essayer le parcours complet :
|
||||
site → ZIP Prism → packwiz avec logo → jeu et serveur Beta.
|
||||
Branche `codex/release-beta293`.
|
||||
|
||||
## Contenu et provenance
|
||||
|
||||
La base conserve les baleines de `a93e2611`, l'habillage packwiz et les quatre
|
||||
mods facultatifs de `97c601cd`. Les Happy Creepers sont repris du commit
|
||||
`0ffd371a` de `codex/happy-creepers`, sans réimplémentation : les règles et la
|
||||
texture restent celles de cette branche dédiée. Les conflits d'intégration
|
||||
portaient sur les versions, les listes de tests et les documents communs.
|
||||
La livraison utilise le prochain compteur libre **beta.293** ; les préparations
|
||||
locales beta.291 et beta.292 ne sont pas publiées séparément.
|
||||
|
||||
[Notes de version](notes-beta293.md), [Happy Creepers](happy-creepers.md),
|
||||
[logo packwiz](packwiz-logo-beta291.md), [mods facultatifs](optional-mods-beta292.md).
|
||||
|
||||
Le test du quota interdimensionnel des baleines attend désormais la fin du
|
||||
chargement des entités du chunk Nether via `waitForEntities`. Ses deux fixtures
|
||||
restent immobiles : l'une au centre du chunk forcé, l'autre en hauteur pour que
|
||||
sa boîte de 36 blocs ne croise pas le nettoyage des tests voisins. Le test
|
||||
d'origine pouvait perdre une fixture avant de vérifier le quota. Le code de
|
||||
jeu reste identique ; **79 tests serveur** passent sur la base réunie, avec
|
||||
Lithium et la voix chargés. Le déplacement de la baleine naturelle reste testé
|
||||
avec son IA active.
|
||||
|
||||
## Cibles de livraison
|
||||
|
||||
- Artefacts immuables et canal client/serveur Gitea ;
|
||||
- CT **350** et service `sanctuary-beta.service`, après arrêt et sauvegarde ;
|
||||
- téléchargement direct de Sanctuary Web, CT **320**, sans modification du code live ;
|
||||
- même instance Prism **Sanctuary Beta**, avec sauvegarde, installateur habillé
|
||||
vérifié et conservation des données personnelles.
|
||||
|
||||
La règle NAT Beta existante `25567 → 192.168.1.122:25565` couvre TCP et UDP.
|
||||
La voix utilise **UDP 25565 dans la CT**, annoncé comme
|
||||
`beta.sanctuary-minecraft.net:25567`. Aucun changement de Bbox, UPnP, Alpha,
|
||||
RCON ou règles des autres serveurs n'est nécessaire.
|
||||
|
||||
## Validation de la livraison
|
||||
|
||||
`check build assemblePack assembleServerPack` : **réussi en 2 min 46 s**,
|
||||
avec les contrôles purs et **79 tests serveur requis**. Profil natif :
|
||||
`whale290,happycreeper,cosmetics,headblocks,recipes,progression,smoker285,hunter286`,
|
||||
`sanctuaryAtlasOnly=true`, `sanctuaryOptional292ServerTests=true`.
|
||||
Journal : `build/release293/check-build-final.log`.
|
||||
|
||||
Parcours Happy Creepers **Vulkan avec Sodium et les quatre mods chargés** :
|
||||
toutes les assertions en jeu réussissent sur deux essais, capture des quatre
|
||||
états inspectée (`build/release293/happy-four-states-vulkan.png`). Équipement et
|
||||
retrait au clavier/souris, fleur exacte synchronisée, visage restauré, IA
|
||||
passive près du joueur et charge indépendante vérifiés. La fermeture du monde
|
||||
reste bloquée dans la synchronisation `Phaser` de Fabric Client GameTest ; ce
|
||||
contrôle graphique n'est donc pas un succès de tâche Gradle. Le premier dump
|
||||
montre le thread de test dans `TestSingleplayerContextImpl.close`, le serveur
|
||||
dans `ThreadingImpl.enterPhase` et le rendu dans `IntegratedServer.halt`.
|
||||
Ce blocage de l'infrastructure de test est consigné séparément des assertions
|
||||
réussies et du build complet réussi. Les parcours des quatre mods avec et sans
|
||||
Sodium ont leur reçu dans [beta.292](optional-mods-beta292.md).
|
||||
|
||||
Les archives ont été contrôlées avant publication : sept dépendances externes
|
||||
et un seul JAR Sanctuary dans le MRpack ; huit mods du client et quatre du
|
||||
serveur ; JEI embarqué, aucun test ni dépendance facultative imposée dans le
|
||||
JAR Sanctuary. Texture happy identique à la branche dédiée, classes creeper
|
||||
et baleine présentes. ZIP Prism sans mods ni sauvegardes personnelles.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.293.jar` | 18171689 | `4a0a3941f651ba3566ec41940c2f4c820d917121312e3aead23b39f99633ebe3` |
|
||||
| `Sanctuary-beta.293.mrpack` | 16761300 | `80e1d9a54165f279738a6486c8c3cd7e5e90ad7ad4ca81d32a4d4a04b15f0e90` |
|
||||
| `Sanctuary-Prism-beta.293.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.293.zip` | 4036536 | `5f3135f7f5b5448023f9873c5242e3c2ab9444c197e1b551d356c8b1120bf01d` |
|
||||
|
||||
## État opérationnel
|
||||
|
||||
**Livraison complète effectuée le 26 octobre 2026.**
|
||||
|
||||
| Cible | État vérifié |
|
||||
| --- | --- |
|
||||
| Source et tag `beta.293` | `678d77fb91ef5e3ffee96cac5c88ebafa0a77976` |
|
||||
| Canal client/serveur | `687d07e57ec008e1865a0081f317c4f3e3159e14` |
|
||||
| Release Gitea | [beta.293](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.293), quatre artefacts publics vérifiés |
|
||||
| CT 350 | Service actif, Sanctuary beta.293, Fabric API, Lithium et Voice Chat chargés ; démarrage sans erreur |
|
||||
| CT 320 | Publication atomique `releases/beta.293/prism`, ancienne beta.289 conservée |
|
||||
| Prism personnel | Même instance **Sanctuary Beta**, huit mods et un seul JAR Sanctuary beta.293 ; deux synchronisations identiques |
|
||||
|
||||
Le serveur était vide (0/20) avant arrêt. Sauvegarde complète privée :
|
||||
`/root/sanctuary-beta-backups/before-beta293-20261026T032339Z/instance.tar.gz`,
|
||||
SHA-256 `b9bd9fa7b10d7e54b3595d52061529a3da8a5c9193f1854966a60fda975e9f11`.
|
||||
Les **240 fichiers protégés sont identiques après synchronisation, avant
|
||||
redémarrage**, de même que l'environnement privé. Après redémarrage, Minecraft
|
||||
réécrit le commentaire horodaté de `server.properties` ; toutes les clés et
|
||||
valeurs restent identiques. Seul le nouveau fichier vocal a été configuré :
|
||||
`port=25565`, `bind_address=` vide,
|
||||
`voice_host=beta.sanctuary-minecraft.net:25567`, `force_voice_chat=false`.
|
||||
Aucun changement de monde, de génération ou de format n'est effectué par
|
||||
l'outil de mise à jour ; le serveur reprend son monde existant.
|
||||
|
||||
La voix écoute en UDP 25565. Un ping applicatif vocal valide répond via le
|
||||
nom public et le port 25567 depuis le Mac (68 ms). Le statut Minecraft via
|
||||
DNS/SRV répond également : 26.3, 0/20. Ces contrôles partent du LAN via
|
||||
l'adresse publique : **l'accès depuis un autre réseau et l'audio audible à
|
||||
deux joueurs restent à essayer**. L'authentification vocale dans les mondes de
|
||||
test est réussie ; elle ne remplace pas cet essai vocal distant.
|
||||
L'API d'admission authentifiée refuse le code volontairement invalide
|
||||
(`HTTP 200`, `valid=false`, `reason=invalid`), sans appel à `redeem`.
|
||||
Les métriques Web sont fraîches, en marche, avec le même UUID communautaire
|
||||
`cc6f4f5a-e58d-409a-879f-81daecf59461` et la même base de jeu.
|
||||
|
||||
Le [ZIP direct du site](https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip)
|
||||
répond HTTP 200 sous le nom **Sanctuary-Beta.zip**, avec le hash du tableau.
|
||||
Le lien MRpack historique répond aussi HTTP 200 avec son hash exact.
|
||||
Le bouton, son survol et le footer affichent beta.293. Le code Web live est
|
||||
resté sur `eadc30e8c7b6dda50bc810584af057235e7fdba8` ; les **73 entrées Git
|
||||
préexistantes sont identiques**, sans pull, reset ni reconstruction de cache.
|
||||
Une importation isolée du ZIP a récupéré les huit mods depuis le vrai canal
|
||||
HTTPS, puis une deuxième synchronisation a conservé exactement leurs hashes.
|
||||
|
||||
Sauvegarde Prism complète sous le dossier historique de l'instance :
|
||||
`sanctuary-backups/before-beta293-20261026T030401Z/instance.tar.gz`, SHA-256
|
||||
`9bfc9b963551885d3e124c865aabe0dc9750ecb6cfed89b7d936a3e14abe51bb`.
|
||||
Les **999 fichiers personnels contrôlés sont inchangés**, et aucun monde
|
||||
personnel n'a été ouvert. Seules les deux commandes `PreLaunchCommand` de
|
||||
`instance.cfg` et le JAR de l'installateur ont reçu l'habillage ; les autres
|
||||
lignes de configuration sont identiques. L'outil reste épinglé par
|
||||
`--bootstrap-no-update`, et le pack continue de suivre le canal stable.
|
||||
Sodium et Sound Physics ont gardé leurs sélections ; les quatre nouveaux
|
||||
choix sont activés par défaut et restent facultatifs côté client.
|
||||
Installateur SHA-256 :
|
||||
`9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875`.
|
||||
|
||||
Les journaux et reçus détaillés sont sous `build/release293/`, hors de Git.
|
||||
Le worktree original `codex/happy-creepers` est resté propre au commit
|
||||
`0ffd371adac0c4f950237448625673b83a94bee5`.
|
||||
@@ -0,0 +1,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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Adapted from packwiz-installer v0.5.14 InstallWindow.kt.
|
||||
* Copyright (c) 2021 comp500. MIT license, see LICENSE.
|
||||
* Sanctuary branding: KOKA99CAB, 2026.
|
||||
*/
|
||||
package link.infra.packwiz.installer.ui.gui;
|
||||
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Image;
|
||||
import java.util.Locale;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
/** Presentation only: GUIHandler retains all packwiz installation decisions. */
|
||||
public final class InstallWindow extends JFrame {
|
||||
private final boolean french = "fr".equals(Locale.getDefault().getLanguage());
|
||||
private final JLabel progressLabel;
|
||||
private final JProgressBar progressBar;
|
||||
private final JButton options;
|
||||
private final JButton cancel;
|
||||
private final JButton ok;
|
||||
private final JPanel buttons;
|
||||
|
||||
public InstallWindow(GUIHandler handler) {
|
||||
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||
setResizable(false);
|
||||
|
||||
ImageIcon logo = new ImageIcon(InstallWindow.class.getResource("/sanctuary/packwiz/logo.png"));
|
||||
JPanel header = new JPanel(new BorderLayout());
|
||||
header.setBackground(Color.BLACK);
|
||||
header.setBorder(new EmptyBorder(14, 24, 4, 24));
|
||||
JLabel wordmark = new JLabel(new ImageIcon(
|
||||
logo.getImage().getScaledInstance(640, 160, Image.SCALE_AREA_AVERAGING)));
|
||||
wordmark.getAccessibleContext().setAccessibleName("Sanctuary");
|
||||
header.add(wordmark, BorderLayout.CENTER);
|
||||
add(header, BorderLayout.NORTH);
|
||||
|
||||
JPanel progress = new JPanel(new BorderLayout(0, 10));
|
||||
progress.setBorder(new EmptyBorder(20, 24, 12, 24));
|
||||
progressBar = new JProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.getAccessibleContext().setAccessibleName(text("Progression", "Progress"));
|
||||
progressLabel = new JLabel(text("Chargement du pack…", "Loading the pack…"));
|
||||
// Keep long filenames from widening the window beyond the screen.
|
||||
progressLabel.setPreferredSize(new Dimension(640, 24));
|
||||
progress.add(progressBar, BorderLayout.NORTH);
|
||||
progress.add(progressLabel, BorderLayout.SOUTH);
|
||||
add(progress, BorderLayout.CENTER);
|
||||
|
||||
buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
|
||||
buttons.setBorder(new EmptyBorder(0, 16, 18, 16));
|
||||
options = new JButton(text("Mods optionnels…", "Optional mods…"));
|
||||
options.addActionListener(event -> {
|
||||
options.setText(text("Chargement…", "Loading…"));
|
||||
options.setEnabled(false);
|
||||
handler.setOptionsButtonPressed(true);
|
||||
});
|
||||
cancel = new JButton(text("Annuler", "Cancel"));
|
||||
cancel.addActionListener(event -> {
|
||||
cancel.setEnabled(false);
|
||||
handler.setCancelButtonPressed(true);
|
||||
});
|
||||
ok = new JButton(text("Continuer", "Continue"));
|
||||
ok.addActionListener(event -> handler.setOkButtonPressed(true));
|
||||
buttons.add(options);
|
||||
buttons.add(cancel);
|
||||
add(buttons, BorderLayout.SOUTH);
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
private String text(String fr, String en) {
|
||||
return french ? fr : en;
|
||||
}
|
||||
|
||||
public final void displayProgress(InstallProgress progress) {
|
||||
progressBar.setIndeterminate(!progress.getHasProgress());
|
||||
progressBar.setStringPainted(progress.getHasProgress());
|
||||
if (progress.getHasProgress()) {
|
||||
progressBar.setMaximum(progress.getProgressTotal());
|
||||
progressBar.setValue(progress.getProgress());
|
||||
progressBar.setString(progress.getProgress() + " / " + progress.getProgressTotal());
|
||||
} else {
|
||||
progressBar.setValue(0);
|
||||
}
|
||||
progressLabel.setText(progress.getMessage());
|
||||
progressLabel.setToolTipText(progress.getMessage());
|
||||
}
|
||||
|
||||
public final void disableOptionsButton(boolean hasOptions) {
|
||||
options.setText(hasOptions ? text("Mods optionnels…", "Optional mods…")
|
||||
: text("Aucun mod optionnel", "No optional mods"));
|
||||
options.setEnabled(false);
|
||||
}
|
||||
|
||||
public final void showOk(boolean hideCancel) {
|
||||
if (hideCancel) {
|
||||
buttons.remove(cancel);
|
||||
}
|
||||
if (ok.getParent() != buttons) {
|
||||
buttons.add(ok);
|
||||
}
|
||||
buttons.revalidate();
|
||||
buttons.repaint();
|
||||
}
|
||||
|
||||
public final void hideOk() {
|
||||
buttons.remove(ok);
|
||||
if (cancel.getParent() != buttons) {
|
||||
buttons.add(cancel);
|
||||
}
|
||||
buttons.revalidate();
|
||||
buttons.repaint();
|
||||
}
|
||||
|
||||
public final void timeoutOk(long remaining) {
|
||||
// Upstream GUIHandler invokes this callback from its countdown timer.
|
||||
if (!SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(() -> timeoutOk(remaining));
|
||||
return;
|
||||
}
|
||||
ok.setText(text("Continuer", "Continue") + " (" + remaining + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Swing integration check for the adapted window and the upstream GUIHandler. */
|
||||
import java.awt.Component;
|
||||
import java.awt.Container;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Window;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingUtilities;
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress;
|
||||
import link.infra.packwiz.installer.ui.gui.GUIHandler;
|
||||
import link.infra.packwiz.installer.ui.gui.InstallWindow;
|
||||
|
||||
public final class InstallWindowCheck {
|
||||
private static <T extends Component> T find(Container root, Class<T> type, String text) {
|
||||
for (Component child : root.getComponents()) {
|
||||
if (type.isInstance(child) && (text == null ||
|
||||
(child instanceof JButton && ((JButton) child).getText().equals(text)))) {
|
||||
return type.cast(child);
|
||||
}
|
||||
if (child instanceof Container) {
|
||||
T nested = find((Container) child, type, text);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
boolean fr = "fr".equals(Locale.getDefault().getLanguage());
|
||||
GUIHandler handler = new GUIHandler();
|
||||
try {
|
||||
handler.setTitle("Sanctuary · packwiz");
|
||||
handler.show();
|
||||
SwingUtilities.invokeAndWait(() -> {});
|
||||
InstallWindow window = null;
|
||||
for (Window candidate : Window.getWindows()) {
|
||||
if (candidate instanceof InstallWindow) window = (InstallWindow) candidate;
|
||||
}
|
||||
final InstallWindow frame = window;
|
||||
require(frame != null && frame.isVisible(), "Upstream handler did not show branded window");
|
||||
handler.submitProgress(new InstallProgress(fr ? "Téléchargement des mods…" : "Downloading mods…", 3, 10));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
JProgressBar bar = find(frame, JProgressBar.class, null);
|
||||
require(bar != null && !bar.isIndeterminate() && bar.getValue() == 3
|
||||
&& bar.getMaximum() == 10, "Upstream progress was not preserved");
|
||||
frame.showOk(false);
|
||||
});
|
||||
// Exercise the timer callback from outside the Swing thread.
|
||||
frame.timeoutOk(5);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
JButton next = find(frame, JButton.class, fr ? "Continuer (5)" : "Continue (5)");
|
||||
require(next != null, "Countdown or locale broken");
|
||||
next.doClick();
|
||||
require(handler.getOkButtonPressed(), "Continue no longer reaches upstream handler");
|
||||
frame.hideOk();
|
||||
// Capture actual Swing layout, including the bundled logo, in both locales.
|
||||
BufferedImage image = new BufferedImage(frame.getWidth(), frame.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
frame.paint(graphics);
|
||||
graphics.dispose();
|
||||
try { ImageIO.write(image, "png", new File(args[0])); }
|
||||
catch (Exception error) { throw new RuntimeException(error); }
|
||||
frame.showOk(true);
|
||||
require(find(frame, JButton.class, fr ? "Annuler" : "Cancel") == null,
|
||||
"Cancel should be hidden during the optional-mod countdown");
|
||||
frame.hideOk();
|
||||
JButton options = find(frame, JButton.class, fr ? "Mods optionnels…" : "Optional mods…");
|
||||
options.doClick();
|
||||
require(handler.getOptionsButtonPressed() && !options.isEnabled(), "Options callback broken");
|
||||
frame.disableOptionsButton(false);
|
||||
require(find(frame, JButton.class, fr ? "Aucun mod optionnel" : "No optional mods") != null,
|
||||
"Empty options label broken");
|
||||
find(frame, JButton.class, fr ? "Annuler" : "Cancel").doClick();
|
||||
require(handler.getCancelButtonPressed(), "Cancellation callback broken");
|
||||
frame.displayProgress(new InstallProgress("Loading…"));
|
||||
require(find(frame, JProgressBar.class, null).isIndeterminate(), "Indeterminate progress broken");
|
||||
});
|
||||
System.out.println("PASS: " + Locale.getDefault() + " logo/progress/options/continue/countdown/cancel");
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 comp500
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Fenêtre packwiz Sanctuary
|
||||
|
||||
`sanctuary.png` est le bandeau fourni par le créateur, conservé sans modification.
|
||||
`InstallWindow.java` adapte la seule fenêtre de progression de packwiz-installer
|
||||
v0.5.14, sous MIT. Le moteur officiel et ses autres interfaces restent intacts.
|
||||
Le contrat de distribution est dans [le ticket beta.291](../../docs/packwiz-logo-beta291.md).
|
||||
|
||||
Construire avec le JDK 25 utilisé par le dépôt :
|
||||
|
||||
```sh
|
||||
python3.14 scripts/packwiz_installer.py
|
||||
python3.14 scripts/test_packwiz_installer.py
|
||||
```
|
||||
|
||||
Le premier appel télécharge l'amont vérifié dans `build/packwiz-branding/`.
|
||||
Le JAR Sanctuary généré doit correspondre à `installer.json` ; ce verrou et
|
||||
`INSTALLER_SHA256` de `scripts/publish_web_pack.py` évoluent ensemble après revue
|
||||
et vérification de la nouvelle fenêtre. Ne pas remplacer le moteur ni désactiver
|
||||
le contrôle de hash pour faire passer une compilation différente.
|
||||
|
||||
Le parcours Swing réel FR/EN se vérifie sur un poste graphique, sans ouvrir
|
||||
Minecraft :
|
||||
|
||||
```sh
|
||||
javac -encoding UTF-8 -classpath build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
-d build/packwiz-branding/check launcher/packwiz/InstallWindowCheck.java
|
||||
java -Duser.language=fr -classpath build/packwiz-branding/check:build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
InstallWindowCheck build/packwiz-branding/preview-fr.png
|
||||
java -Duser.language=en -classpath build/packwiz-branding/check:build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
InstallWindowCheck build/packwiz-branding/preview-en.png
|
||||
```
|
||||
|
||||
Le séparateur de classpath est `;` sous Windows. Le parcours contrôle les
|
||||
callbacks du vrai `GUIHandler`, la progression et le compte à rebours ; il
|
||||
capture la fenêtre et la ferme. Les captures ne font pas partie du JAR livré.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"upstream_version": "v0.5.14",
|
||||
"upstream_url": "https://github.com/packwiz/packwiz-installer/releases/download/v0.5.14/packwiz-installer.jar",
|
||||
"upstream_sha256": "c9f646908d340d84773948a9a7d98bc1dae250d35e1016dc6e2b8459760b5598",
|
||||
"logo_sha256": "efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157",
|
||||
"installer_sha256": "9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -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') }
|
||||
|
||||
+42
@@ -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");
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.whale.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real pack renderer, animated ghost and native egg under Vulkan. Fresh development world only. */
|
||||
public final class FlyingWhale290ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
|
||||
var whaleId = new AtomicInteger();
|
||||
c.runOnClient(m -> {
|
||||
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
|
||||
check(device.backendName().equalsIgnoreCase("vulkan"), "Whale validation requires Vulkan: " + device.backendName());
|
||||
System.out.println("WHALE290_BACKEND: " + device.backendName() + " / " + device.name());
|
||||
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("FlyingWhale290-" + UUID.randomUUID()); s.setSeed("290"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.getInventory().clearContent(); p.teleportTo(.5, 32, .5); p.setYRot(0); p.setXRot(-8);
|
||||
p.getAbilities().flying = true; p.onUpdateAbilities();
|
||||
p.getInventory().setItem(0, new ItemStack(FlyingWhales.SPAWN_EGG));
|
||||
p.getInventory().setItem(1, new ItemStack(Items.EGG)); p.inventoryMenu.broadcastChanges();
|
||||
p.level().setBlockAndUpdate(new BlockPos(0, 30, 0), Blocks.GLASS.defaultBlockState());
|
||||
for (int i = 0; i < 2; i++) {
|
||||
var whale = FlyingWhales.TYPE.create(p.level(), EntitySpawnReason.COMMAND);
|
||||
whale.setNoAi(true); whale.setPos(i == 0 ? -4 : 30, i == 0 ? 35 : 47, i == 0 ? 43 : 92);
|
||||
whale.setYRot(i == 0 ? 105 : 250); whale.setYHeadRot(whale.getYRot()); whale.setYBodyRot(whale.getYRot());
|
||||
p.level().addFreshEntity(whale); if (i == 0) whaleId.set(whale.getId());
|
||||
}
|
||||
});
|
||||
c.waitFor(m -> m.level.getEntity(whaleId.get()) instanceof FlyingWhaleEntity, 400);
|
||||
c.waitTicks(20);
|
||||
server.runOnServer(s -> check(FlyingWhales.countLoaded(s) == 2, "Two real whales are loaded after the player chunks arrive"));
|
||||
c.runOnClient(m -> {
|
||||
m.player.setYRot(0); m.player.setXRot(-8); m.getWindow().setWindowed(1280, 960);
|
||||
var whale = (FlyingWhaleEntity)m.level.getEntity(whaleId.get());
|
||||
check(m.getEntityRenderDispatcher().getRenderer(whale) instanceof FlyingWhaleRenderer, "Historical renderer is installed");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/flying_whale/flying_whale.png")).isPresent(), "Historical cloud texture is packaged");
|
||||
var renderer = (FlyingWhaleRenderer)m.getEntityRenderDispatcher().getRenderer(whale);
|
||||
var state = renderer.createRenderState(whale, 1);
|
||||
check((renderer.getModelTint(state) >>> 24) < 255, "The whale keeps its translucent ghost tint");
|
||||
var model = new FlyingWhaleModel(m.getEntityModels().bakeLayer(FlyingWhaleModel.LAYER_LOCATION));
|
||||
state.ageInTicks = 0; model.setupAnim(state);
|
||||
float fin = model.root().getChild("whale").getChild("left_fin").zRot;
|
||||
state.ageInTicks = 60; model.setupAnim(state);
|
||||
check(Math.abs(fin - model.root().getChild("whale").getChild("left_fin").zRot) > .01, "Fins animate on the actual model");
|
||||
});
|
||||
c.waitTicks(35); c.takeScreenshot("whale290-native-egg-and-two-whales");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1); c.waitTicks(3);
|
||||
c.takeScreenshot("whale290-ghosts");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
String name = new ItemStack(FlyingWhales.SPAWN_EGG).getHoverName().getString();
|
||||
check(!name.contains("item.sanctuary") && name.contains(locale.equals("fr_fr") ? "baleine" : "Whale"), "Native egg label is translated");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("whale290-egg-" + locale);
|
||||
}
|
||||
var start = new AtomicReference<Vec3>();
|
||||
server.runOnServer(s -> {
|
||||
var whale = (FlyingWhaleEntity)s.overworld().getEntity(whaleId.get());
|
||||
start.set(whale.position()); whale.setNoAi(false);
|
||||
});
|
||||
c.waitTicks(60);
|
||||
server.runOnServer(s -> {
|
||||
var whale = (FlyingWhaleEntity)s.overworld().getEntity(whaleId.get());
|
||||
check(whale.position().distanceToSqr(start.get()) > .01, "Historical AI flies around the real connected player");
|
||||
});
|
||||
c.takeScreenshot("whale290-flight");
|
||||
System.out.println("WHALE290_CLIENT_PASS: Vulkan, two giant translucent animated whales, native egg, FR/EN, actual historical flight");
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.creeper.HappyCreepers;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.renderer.entity.CreeperRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real sneak-clicks, synced flower/face, charged combination and real server AI under Vulkan. */
|
||||
public final class HappyCreeperClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
|
||||
c.runOnClient(m -> {
|
||||
m.options.renderDistance().set(5); m.options.simulationDistance().set(5); m.options.pauseOnLostFocus = false;
|
||||
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("voicechat")) {
|
||||
var voice = de.maxhenkel.voicechat.VoicechatClient.CLIENT_CONFIG;
|
||||
voice.muted.set(true); voice.disabled.set(true); voice.onboardingFinished.set(true);
|
||||
voice.macosCheckMicrophonePermission.set(false);
|
||||
}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> { if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("HappyCreeper-" + UUID.randomUUID()); s.setSeed("290"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null && e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
int id = server.computeOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); p.getAbilities().invulnerable = true;
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL, true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.getInventory().clearContent(); p.teleportTo(.5, 1, .5); p.setYRot(0); p.setXRot(0);
|
||||
fr.koka.sanctuary.inventory.AccessoryAccess.of(p).clearContent(); FamiliarService.tick(p);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.POPPY)); p.inventoryMenu.broadcastChanges();
|
||||
for (int x = -9; x <= 9; x++) for (int z = -3; z <= 12; z++)
|
||||
p.level().setBlockAndUpdate(new BlockPos(x, 0, z), Blocks.STONE.defaultBlockState());
|
||||
var mob = EntityTypes.CREEPER.create(p.level(), EntitySpawnReason.COMMAND); mob.setNoAi(true);
|
||||
mob.setPos(.5, 1, 2.5); mob.setYRot(180); mob.yHeadRot = mob.yBodyRot = 180; p.level().addFreshEntity(mob); return mob.getId();
|
||||
});
|
||||
c.runOnClient(m -> m.getWindow().setWindowed(1280, 960));
|
||||
c.waitFor(m -> m.level.getEntity(id) != null && m.player.getMainHandItem().is(Items.POPPY), 200);
|
||||
c.getInput().holdKey(o -> o.keyShift); c.waitTicks(5); click(c, id); c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> m.level.getEntity(id) instanceof Creeper mob && HappyCreepers.isHappy(mob) && m.player.getMainHandItem().isEmpty(), 200);
|
||||
c.runOnClient(m -> {
|
||||
var mob = (Creeper) m.level.getEntity(id); var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
var state = renderer.createRenderState(mob, 1); var hat = ((MobHeadState.Access) state).sanctuary$mobHat();
|
||||
check(hat.active && !hat.head.block.isEmpty(), "Exact equipped flower reaches the native head layer");
|
||||
check(renderer.getTextureLocation(state).equals(SanctuaryMod.id("textures/entity/creeper/happy.png")), "Synced flower selects the restored smile");
|
||||
check(m.getResourceManager().getResource(renderer.getTextureLocation(state)).isPresent(), "Historical happy texture is packaged");
|
||||
});
|
||||
server.runOnServer(s -> { var mob = (Creeper) s.getPlayerList().getPlayers().getFirst().level().getEntity(id); mob.setNoAi(false); });
|
||||
c.waitTicks(85);
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); var mob = (Creeper) p.level().getEntity(id);
|
||||
check(mob != null && mob.isAlive() && mob.getTarget() == null && !mob.isIgnited(), "Real happy AI survives next to a survival player without attacking");
|
||||
mob.setNoAi(true); mob.teleportTo(.5, 1, 2.5);
|
||||
}); c.waitTicks(5);
|
||||
c.getInput().holdKey(o -> o.keyShift); c.waitTicks(5); click(c, id); c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> m.level.getEntity(id) instanceof Creeper mob && !HappyCreepers.isHappy(mob), 200);
|
||||
c.runOnClient(m -> {
|
||||
var mob = (Creeper) m.level.getEntity(id); var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
check(!((MobHeadState.Access) renderer.createRenderState(mob, 1)).sanctuary$mobHat().active, "Removing the flower clears its render state");
|
||||
check(!renderer.getTextureLocation(renderer.createRenderState(mob, 1)).getNamespace().equals("sanctuary"), "Removal restores the native face");
|
||||
});
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); check(p.getInventory().countItem(Items.POPPY) == 1, "Real empty-hand gesture returns the one flower");
|
||||
p.level().getEntity(id).discard(); p.teleportTo(.5, 1, -2.5);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
var mob = EntityTypes.CREEPER.create(p.level(), EntitySpawnReason.COMMAND); mob.setNoAi(true);
|
||||
mob.setPos((i - 1.5) * 2 + .5, 1, 4.5); mob.setYRot(180); mob.yHeadRot = mob.yBodyRot = 180;
|
||||
if (i == 1 || i == 2) HeadEquipment.set(mob, new ItemStack(i == 1 ? Items.POPPY : Items.PINK_TULIP));
|
||||
if (i >= 2) mob.thunderHit(p.level(), EntityTypes.LIGHTNING_BOLT.create(p.level(), EntitySpawnReason.COMMAND));
|
||||
mob.clearFire(); p.level().addFreshEntity(mob);
|
||||
}
|
||||
}); c.waitTicks(20);
|
||||
c.runOnClient(m -> { m.player.setYRot(0); m.player.setXRot(4);
|
||||
int happy = 0, chargedHappy = 0;
|
||||
for (var mob : m.level.getEntitiesOfClass(Creeper.class, m.player.getBoundingBox().inflate(20))) {
|
||||
var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob); var state = renderer.createRenderState(mob, 1);
|
||||
if (((MobHeadState.Access) state).sanctuary$mobHat().happyCreeper) { happy++; if (state.isPowered) chargedHappy++; }
|
||||
}
|
||||
check(happy == 2 && chargedHappy == 1, "Normal, happy, charged-happy and charged states coexist on the real client");
|
||||
});
|
||||
c.getInput().pressKey(InputConstants.KEY_F1); c.waitTicks(3); c.takeScreenshot("happy-creepers-four-states"); c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
System.out.println("HAPPY_CREEPER_CLIENT_PASS: Vulkan, real sneak-click equipment/removal, exact synced flower, restored face, passive server AI, independent charge");
|
||||
} finally { intro.set(false); c.getInput().releaseKey(o -> o.keyShift); }
|
||||
}
|
||||
|
||||
private static void click(ClientGameTestContext c, int id) {
|
||||
c.runOnClient(m -> {
|
||||
var e = m.level.getEntity(id); var delta = e.getBoundingBox().getCenter().subtract(m.player.getEyePosition());
|
||||
m.player.setYRot((float) Math.toDegrees(Math.atan2(-delta.x, delta.z)));
|
||||
m.player.setXRot((float) -Math.toDegrees(Math.atan2(delta.y, Math.sqrt(delta.x * delta.x + delta.z * delta.z))));
|
||||
}); c.waitTicks(3);
|
||||
c.runOnClient(m -> check(m.hitResult instanceof net.minecraft.world.phys.EntityHitResult hit && hit.getEntity().getId() == id, "Mouse actually aims at the creeper"));
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_RIGHT); c.waitTicks(7);
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import de.johni0702.minecraft.bobby.Bobby;
|
||||
import de.johni0702.minecraft.bobby.ext.ClientChunkCacheExt;
|
||||
import de.maxhenkel.voicechat.VoicechatClient;
|
||||
import de.maxhenkel.voicechat.gui.VoiceChatScreen;
|
||||
import de.maxhenkel.voicechat.voice.client.ClientManager;
|
||||
import dev.tr7zw.entityculling.EntityCullingModBase;
|
||||
import dev.tr7zw.entityculling.access.Cullable;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Full optional pack on Vulkan, with cache and culling exercised in a fresh lab world. */
|
||||
public final class Optional292ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro = new AtomicBoolean(true);
|
||||
var hidden = new AtomicInteger();
|
||||
c.runOnClient(m -> {
|
||||
check(com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName()
|
||||
.equalsIgnoreCase("vulkan"), "Only Vulkan is validated");
|
||||
for (String id : new String[]{"bobby", "lithium", "entityculling", "voicechat", "sound_physics_remastered"})
|
||||
check(FabricLoader.getInstance().isModLoaded(id), "Missing pack mod " + id);
|
||||
check(VoicechatClient.CLIENT_CONFIG != null, "Voice Chat initialized with Sound Physics");
|
||||
// Test configuration only: no capture of the user's microphone.
|
||||
VoicechatClient.CLIENT_CONFIG.muted.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.disabled.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.onboardingFinished.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.macosCheckMicrophonePermission.set(false);
|
||||
m.options.renderDistance().set(10); m.options.simulationDistance().set(8);
|
||||
m.options.pauseOnLostFocus = false;
|
||||
// Fabric's client runner resets configuration before initialization.
|
||||
// Exercise Bobby's live reload in the development directory instead.
|
||||
try {
|
||||
var path = FabricLoader.getInstance().getConfigDir().resolve("bobby.conf");
|
||||
Files.writeString(path, Files.readString(path)
|
||||
.replaceAll("(?m)^view-distance-overwrite=.*$", "view-distance-overwrite=4")
|
||||
.replaceAll("(?m)^unload-delay-secs=.*$", "unload-delay-secs=0"));
|
||||
} catch (java.io.IOException error) { throw new AssertionError(error); }
|
||||
});
|
||||
c.waitFor(m -> Bobby.getInstance().getConfig().getViewDistanceOverwrite() == 4, 200);
|
||||
if (Boolean.getBoolean("sanctuary.test.optional292WhaleOnly")) {
|
||||
new FlyingWhale290ClientChecks().runTest(c);
|
||||
System.out.println("OPTIONAL292_WHALE_PASS: four optional mods with the translucent Sanctuary renderer");
|
||||
return;
|
||||
}
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Optional292-" + UUID.randomUUID()); s.setSeed("292"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null
|
||||
&& !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.teleportTo(.5, 32, .5); p.setYRot(0); p.setXRot(0);
|
||||
p.getAbilities().flying = true; p.onUpdateAbilities();
|
||||
for (int x = -5; x <= 5; x++) for (int y = 29; y <= 39; y++)
|
||||
p.level().setBlockAndUpdate(new BlockPos(x, y, 6), Blocks.STONE.defaultBlockState());
|
||||
var pig = EntityTypes.PIG.create(p.level(), EntitySpawnReason.COMMAND);
|
||||
pig.setNoAi(true); pig.setNoGravity(true); pig.setPos(.5, 32, 10.5);
|
||||
p.level().addFreshEntity(pig); hidden.set(pig.getId());
|
||||
});
|
||||
c.waitFor(m -> m.level.getEntity(hidden.get()) != null, 400);
|
||||
c.runOnClient(m -> {
|
||||
check(Bobby.getInstance().isEnabled(), "Bobby is active in the limited-view lab");
|
||||
check(((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager() != null,
|
||||
"Bobby attached its cache to the actual client chunk manager");
|
||||
m.player.setYRot(0); m.player.setXRot(0); m.getWindow().setWindowed(1280, 960);
|
||||
check(EntityCullingModBase.instance.cullTask != null, "Culling worker initialized on Vulkan");
|
||||
});
|
||||
c.waitFor(m -> ((Cullable)m.level.getEntity(hidden.get())).isCulled(), 400);
|
||||
c.takeScreenshot("optional292-occluded");
|
||||
server.runOnServer(s -> {
|
||||
for (int x = -5; x <= 5; x++) for (int y = 29; y <= 39; y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x, y, 6), Blocks.AIR.defaultBlockState());
|
||||
});
|
||||
c.waitFor(m -> !((Cullable)m.level.getEntity(hidden.get())).isCulled(), 400);
|
||||
c.takeScreenshot("optional292-visible-again");
|
||||
// Traverse beyond the short server view, then verify persisted chunk-cache data.
|
||||
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().teleportTo(128.5, 32, .5));
|
||||
c.waitTicks(160);
|
||||
c.waitFor(m -> !((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager()
|
||||
.getFakeChunks().isEmpty(), 600);
|
||||
c.runOnClient(m -> {
|
||||
var manager = ((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager();
|
||||
check(manager != null && manager.getStorage() != null, "Bobby has persistent storage after traversal");
|
||||
System.out.println("OPTIONAL292_BOBBY " + manager.getDebugString());
|
||||
});
|
||||
c.waitFor(m -> ClientManager.getClient() != null && ClientManager.getClient().getConnection() != null
|
||||
&& ClientManager.getClient().getConnection().isConnected(), 600);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.gui.setScreen(new VoiceChatScreen());
|
||||
check(m.gui.screen() instanceof VoiceChatScreen, "Voice Chat screen opens with Sound Physics");
|
||||
check(ClientManager.getPlayerStateManager().isMuted(), "Voice chat stays muted during testing");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("optional292-voice-" + locale);
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
}
|
||||
System.out.println("OPTIONAL292_CLIENT_PASS: Vulkan, four mods, Sound Physics, culling/uncover, Bobby cache, UDP connection and FR/EN voice UI");
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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");
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.whale.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class FlyingWhale290GameTests {
|
||||
@GameTest public void cloudKeepsHistoricalAttributesAndGhostRules(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(whale.getMaxHealth() == 500 && whale.getAttributeValue(Attributes.FOLLOW_RANGE) == 520,
|
||||
"Historical internal health and player range are preserved");
|
||||
h.assertTrue(whale.getBbWidth() == 36 && whale.getBbHeight() == 11 && whale.isNoGravity()
|
||||
&& whale.isInvulnerable() && whale.noPhysics && whale.fireImmune(), "Giant flying cloud is invulnerable and intangible");
|
||||
h.assertTrue(!whale.blocksBuilding && !whale.isAttackable() && !whale.isPickable()
|
||||
&& !whale.isPushable() && !whale.canBeHitByProjectile() && !whale.isPushedByFluid(),
|
||||
"Whale cannot obstruct, attack or intercept projectiles");
|
||||
float health = whale.getHealth();
|
||||
h.assertTrue(!whale.hurtServer(h.getLevel(), h.getLevel().damageSources().genericKill(), 1000)
|
||||
&& whale.getHealth() == health, "Even direct server damage is refused");
|
||||
whale.push(2, 3, 4);
|
||||
h.assertTrue(whale.getDeltaMovement().equals(Vec3.ZERO) && !whale.removeWhenFarAway(1_000_000),
|
||||
"No knockback or vanilla distance despawn");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void nativeCollisionDoesNotBreakTerrain(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
var pos = h.absolutePos(new BlockPos(2, 2, 2));
|
||||
h.getLevel().setBlockAndUpdate(pos, Blocks.STONE.defaultBlockState());
|
||||
whale.setPos(Vec3.atCenterOf(pos).add(-2, 0, 0));
|
||||
var start = whale.position();
|
||||
whale.move(MoverType.SELF, new Vec3(4, 0, 0));
|
||||
h.assertTrue(whale.position().distanceTo(start.add(4, 0, 0)) < .00001
|
||||
&& h.getLevel().getBlockState(pos).is(Blocks.STONE), "Native movement passes through stone without editing it");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void onlyNamedWhalesPersistAndNativeReloadRestoresGhostRules(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(!whale.shouldBeSaved(), "Unnamed visitors are transient, as in 26.2");
|
||||
whale.setCustomName(Component.literal("Nuage")); whale.seedOrbit(2.25);
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
|
||||
h.assertTrue(whale.shouldBeSaved() && whale.save(output), "Named whale can be saved natively");
|
||||
var saved = output.buildResult();
|
||||
h.assertTrue(saved.getDoubleOr("OrbitAngle", 0) == 2.25, "Historical orbit angle survives native save");
|
||||
saved.putBoolean("NoGravity", false); saved.putBoolean("Invulnerable", false);
|
||||
var input = TagValueInput.create(ProblemReporter.DISCARDING, h.getLevel().registryAccess(), saved);
|
||||
var loaded = EntityType.loadEntityRecursive(input, h.getLevel(), EntitySpawnReason.LOAD, e -> e);
|
||||
h.assertTrue(loaded instanceof FlyingWhaleEntity ghost && ghost.getUUID().equals(whale.getUUID())
|
||||
&& ghost.getName().getString().equals("Nuage") && ghost.isNoGravity() && ghost.isInvulnerable()
|
||||
&& ghost.noPhysics, "Identity, name and ghost physics survive a native save/reload");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void scheduleHasThreeWindowsAndNoCrossServerStaticCounter(GameTestHelper h) {
|
||||
var schedule = new FlyingWhaleSpawner.Schedule();
|
||||
for (int i = 0; i < 399; i++) h.assertTrue(!schedule.due(), "No spawn check before 400 ticks");
|
||||
h.assertTrue(schedule.due() && !schedule.due(), "Check at 400 ticks then reset");
|
||||
h.assertTrue(schedule.window(0) == 0 && schedule.available(0), "First window starts available");
|
||||
schedule.spawned(0);
|
||||
h.assertTrue(!schedule.available(0) && schedule.window(7999) == 0 && !schedule.available(0), "One success per daily window");
|
||||
h.assertTrue(schedule.window(8000) == 1 && schedule.available(1), "Second window starts at 8000");
|
||||
schedule.spawned(1);
|
||||
h.assertTrue(schedule.window(16000) == 2 && schedule.available(2), "Third window starts at 16000");
|
||||
schedule.spawned(2);
|
||||
h.assertTrue(schedule.window(24000) == 0 && schedule.available(0), "New day resets windows");
|
||||
h.assertTrue(new FlyingWhaleSpawner.Schedule().available(0), "A new server has an independent schedule");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void naturalPlacementKeepsOverworldAndHeightBounds(GameTestHelper h) {
|
||||
var level = h.getLevel(); var random = RandomSource.create(290);
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMinY() + 18, 0), random), "Lower altitude margin is excluded");
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMaxY() - 18, 0), random), "Upper altitude margin is excluded");
|
||||
h.assertTrue(FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMinY() + 30, 0), random), "Free Overworld altitude is accepted");
|
||||
var nether = level.getServer().getLevel(Level.NETHER);
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, nether, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, 64, 0), random), "Natural visitors stay in the Overworld");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100) public void eggQuotaNaturalSpawnAndFlightUseActualServerEntities(GameTestHelper h) {
|
||||
var level = h.getLevel(); var server = level.getServer(); var p = Progression003GameTests.player(h);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(FlyingWhales.SPAWN_EGG, 3));
|
||||
var at = p.blockPosition().below();
|
||||
level.setBlockAndUpdate(at, Blocks.STONE.defaultBlockState());
|
||||
var context = new UseOnContext(p, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(Vec3.atCenterOf(at).add(0, .5, 0), Direction.UP, at, false));
|
||||
h.assertTrue(SpawnEggItem.getType(p.getMainHandItem()) == FlyingWhales.TYPE, "Native egg mapping resolves the whale");
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 1 && p.getMainHandItem().getCount() == 2,
|
||||
"Real survival egg use spawns one whale and consumes one egg");
|
||||
var first = level.getEntities(FlyingWhales.TYPE, Entity::isAlive).getFirst();
|
||||
first.setNoAi(true);
|
||||
// Its 36-block hitbox must not overlap neighbouring tests' cleanup areas.
|
||||
first.setPos(p.position().add(0, 80, 0));
|
||||
var nether = server.getLevel(Level.NETHER); nether.setChunkForced(0, 0, true); nether.getChunk(0, 0);
|
||||
nether.waitForEntities(new net.minecraft.world.level.ChunkPos(0, 0), 0);
|
||||
var other = FlyingWhales.TYPE.create(nether, EntitySpawnReason.COMMAND);
|
||||
// Keep the quota fixture inside its single forced chunk during the delay.
|
||||
other.setNoAi(true); other.setPos(8, 64, 8); nether.addFreshEntity(other);
|
||||
h.runAfterDelay(5, () -> {
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2,
|
||||
"Both quota fixtures remain loaded before egg use; firstRemoved=" + first.isRemoved()
|
||||
+ ", netherRemoved=" + other.isRemoved());
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2 && p.getMainHandItem().getCount() == 2,
|
||||
"Two across dimensions block the third egg without consuming it; count=" + FlyingWhales.countLoaded(server)
|
||||
+ ", eggs=" + p.getMainHandItem().getCount() + ", netherRemoved=" + other.isRemoved());
|
||||
var third = FlyingWhales.TYPE.create(level, EntitySpawnReason.COMMAND);
|
||||
third.setPos(p.position()); level.addFreshEntity(third);
|
||||
h.assertTrue(third.isRemoved() && FlyingWhales.countLoaded(server) == 2, "Commands cannot exceed the shared limit");
|
||||
other.discard();
|
||||
nether.setChunkForced(0, 0, false);
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2 && p.getMainHandItem().getCount() == 1,
|
||||
"Removing a whale releases its place immediately");
|
||||
level.getEntities(FlyingWhales.TYPE, Entity::isAlive).forEach(Entity::discard);
|
||||
// Load only development chunks for this deterministic radius test.
|
||||
for (int x = -10; x <= 10; x++) for (int z = -10; z <= 10; z++)
|
||||
level.getChunk(p.blockPosition().getX() / 16 + x, p.blockPosition().getZ() / 16 + z);
|
||||
h.assertTrue(FlyingWhaleSpawner.spawnFor(level, p, RandomSource.create(290)), "Actual natural spawner creates a visitor");
|
||||
var whale = level.getEntities(FlyingWhales.TYPE, Entity::isAlive).getFirst();
|
||||
double radius = whale.position().subtract(p.position()).horizontalDistance();
|
||||
h.assertTrue(radius >= 92 && radius < 138 && whale.getY() >= level.getMinY() + 22
|
||||
&& whale.getY() <= level.getMaxY() - 22, "Native spawn retains historical radius and altitude");
|
||||
whale.setPos(p.position().add(0, 30, 0));
|
||||
var start = whale.position();
|
||||
h.runAfterDelay(30, () -> {
|
||||
h.assertTrue(whale.position().distanceToSqr(start) > .001 && whale.isNoGravity() && whale.noPhysics,
|
||||
"Historical AI actually moves through the air on native server ticks");
|
||||
whale.tickCount = 14400;
|
||||
});
|
||||
h.runAfterDelay(33, () -> {
|
||||
h.assertTrue(whale.isRemoved() && FlyingWhales.countLoaded(server) == 0,
|
||||
"Historical twelve minute lifetime fades and releases quota");
|
||||
h.succeed();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.creeper.HappyCreepers;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
|
||||
public final class HappyCreeperGameTests {
|
||||
@GameTest public void ordinaryHeadGestureConsumesAndReturnsTheSameFlower(GameTestHelper h) {
|
||||
var p = player(h); var mob = creeper(h); mob.setPos(p.position().add(0, 0, 1));
|
||||
var flower = new ItemStack(Items.POPPY, 3);
|
||||
flower.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Rose"));
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, flower); p.setShiftKeyDown(true);
|
||||
h.assertTrue(MobHeadService.equip(p, mob), "Existing head gesture equips the flower");
|
||||
h.assertTrue(flower.getCount() == 2 && HappyCreepers.isHappy(mob), "One real flower is consumed and calms the creeper");
|
||||
h.assertTrue(ItemStack.matches(HeadCosmeticService.appearance(mob), flower.copyWithCount(1)), "Exact flower and components are synchronized");
|
||||
h.assertTrue(mob.requiresCustomPersistence(), "Equipped creeper does not despawn at distance");
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
h.assertTrue(MobHeadService.equip(p, mob) && !HappyCreepers.isHappy(mob), "Empty-hand head gesture removes happiness");
|
||||
h.assertTrue(p.getInventory().countItem(Items.POPPY) == 1 && p.getInventory().getNonEquipmentItems().stream()
|
||||
.anyMatch(s -> s.is(Items.POPPY) && ItemStack.matches(s, flower.copyWithCount(1))), "The same named flower returns once");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void everyTaggedFlowerWorksButOtherHeadObjectsDoNot(GameTestHelper h) {
|
||||
var mob = creeper(h); int flowers = 0;
|
||||
for (var item : BuiltInRegistries.ITEM) {
|
||||
var stack = new ItemStack(item);
|
||||
if (!HappyCreepers.isFlower(stack)) continue;
|
||||
HeadEquipment.set(mob, stack);
|
||||
h.assertTrue(HappyCreepers.isHappy(mob), "Tagged flower is accepted: " + item); flowers++;
|
||||
}
|
||||
h.assertTrue(flowers >= 15, "The exact Minecraft version resolves its flower tag");
|
||||
for (var item : java.util.List.of(Items.FLOWER_POT, Items.OAK_LEAVES, Items.WHEAT_SEEDS, Items.STONE)) {
|
||||
HeadEquipment.set(mob, new ItemStack(item));
|
||||
h.assertTrue(!HappyCreepers.isHappy(mob), "A different head object keeps the ordinary temperament");
|
||||
}
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void flowerImmediatelyCancelsAnAlreadyIgnitedFuse(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h);
|
||||
mob.setTarget(p); mob.ignite();
|
||||
for (int i = 0; i < 20; i++) mob.tick();
|
||||
h.assertTrue(mob.isIgnited() && mob.getSwelling(1) > 0, "Real native fuse is already progressing");
|
||||
HeadEquipment.set(mob, new ItemStack(Items.DANDELION));
|
||||
h.assertTrue(!mob.isIgnited() && mob.getTarget() == null && mob.getSwelling(1) == 0, "Equipment immediately clears target, ignition and accumulated fuse");
|
||||
for (int i = 0; i < 60; i++) { mob.setTarget(p); mob.setSwellDir(1); mob.ignite(); mob.tick(); }
|
||||
h.assertTrue(mob.isAlive() && mob.getTarget() == null && !mob.isIgnited() && mob.getSwellDir() < 0 && mob.getSwelling(1) == 0,
|
||||
"Repeated targeting and ignition cannot make a happy creeper explode");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.tick();
|
||||
h.assertTrue(mob.isAlive() && !mob.isIgnited() && mob.getSwelling(1) == 0, "Removing the flower cannot resume an old fuse");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void happyCreeperRejectsRetaliationAndKeepsItsOrdinaryMovement(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h); mob.setNoAi(false);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.CORNFLOWER));
|
||||
mob.hurtServer(h.getLevel(), mob.damageSources().playerAttack(p), 1);
|
||||
for (int i = 0; i < 40; i++) mob.tick();
|
||||
h.assertTrue(mob.getTarget() == null && !mob.isAggressive() && mob.isAlive() && !mob.isNoAi(), "Damage does not restore hostility or disable the native AI");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.setTarget(p);
|
||||
h.assertTrue(mob.getTarget() == p, "Removing the flower restores normal targeting");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void flintAndFireChargeCannotIgniteOrSpendItemsWhileHappy(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.BLUE_ORCHID));
|
||||
for (var hand : InteractionHand.values()) for (var item : java.util.List.of(Items.FLINT_AND_STEEL, Items.FIRE_CHARGE)) {
|
||||
var stack = new ItemStack(item, item == Items.FIRE_CHARGE ? 3 : 1);
|
||||
var before = stack.copy(); p.setItemInHand(hand, stack);
|
||||
mob.interact(p, hand, net.minecraft.world.phys.Vec3.ZERO);
|
||||
h.assertTrue(!mob.isIgnited() && ItemStack.matches(stack, before), "Igniter remains intact in either hand");
|
||||
}
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void chargedHappyCreeperKeepsIdentityFlowerAndChargeAfterNativeReload(GameTestHelper h) {
|
||||
var mob = creeper(h);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.PINK_TULIP));
|
||||
mob.thunderHit(h.getLevel(), EntityTypes.LIGHTNING_BOLT.create(h.getLevel(), EntitySpawnReason.COMMAND));
|
||||
h.assertTrue(HappyCreepers.isHappy(mob) && mob.isPowered(), "Lightning charges a happy creeper without changing its temperament");
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
|
||||
h.assertTrue(mob.save(output), "Vanilla entity saves normally");
|
||||
var loaded = (Creeper) EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,
|
||||
h.getLevel().registryAccess(), output.buildResult()), h.getLevel(), EntitySpawnReason.LOAD, e -> e);
|
||||
h.assertTrue(loaded.getType() == EntityTypes.CREEPER && loaded.getUUID().equals(mob.getUUID()) && loaded.isPowered()
|
||||
&& HappyCreepers.isHappy(loaded) && HeadEquipment.get(loaded).is(Items.PINK_TULIP), "Existing head save preserves the same creeper, flower and independent electric charge");
|
||||
loaded.ignite(); loaded.tick(); h.assertTrue(!loaded.isIgnited() && loaded.isAlive(), "Reloaded flower still prevents explosions");
|
||||
HeadEquipment.set(loaded, ItemStack.EMPTY);
|
||||
h.assertTrue(loaded.isPowered() && !HappyCreepers.isHappy(loaded), "Taking the flower leaves the electric charge intact");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); loaded.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void ordinaryAndUnequippedCreepersStillExplodeNormally(GameTestHelper h) {
|
||||
for (boolean previouslyHappy : new boolean[]{false, true}) {
|
||||
var mob = creeper(h);
|
||||
if (previouslyHappy) { HeadEquipment.set(mob, new ItemStack(Items.POPPY)); HeadEquipment.set(mob, ItemStack.EMPTY); }
|
||||
mob.ignite(); h.assertTrue(mob.isIgnited(), "Normal ignition is available without a flower");
|
||||
for (int i = 0; i < 31 && !mob.isRemoved(); i++) mob.tick();
|
||||
h.assertTrue(mob.isRemoved(), "Native fuse and explosion still finish without a flower");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
private static Creeper creeper(GameTestHelper h) {
|
||||
var mob = EntityTypes.CREEPER.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
mob.setNoAi(true); mob.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(2, 2, 2))); return mob;
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h) {
|
||||
var p = Progression003GameTests.player(h); p.setUUID(java.util.UUID.randomUUID());
|
||||
p.setAttached(ProgressionService.RECORD, ProgressRecord.INITIAL.json()); ProgressEffects.apply(p, ProgressRecord.INITIAL); return p;
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
@@ -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…",
|
||||
|
||||
+6
@@ -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 |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
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
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user