Compare commits

...
Author SHA1 Message Date
koka 00179e30ef Use one cached bounding box per sculpture (beta.124)
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 16:00:52 +02:00
koka 87f823af67 Record beta.123 publication and preserved Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 15:30:04 +02:00
koka 4552cc347b Separate safe golden rotations from Steve wrench powers (beta.123)
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 15:28:06 +02:00
koka 49131b4c53 Record beta.122 publication and preserved Prism sync
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 15:04:59 +02:00
koka edc7fc3dfb Match sculpture collisions to voxels and fix light occlusion (beta.122)
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 15:02:05 +02:00
koka 9b0104d27a Record beta.121 publication and preserved Prism sync
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 14:11:39 +02:00
koka d9b3dd6bed Add editable Sanctuary resource template and new wrench texture (beta.121)
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 14:09:11 +02:00
38 changed files with 1709 additions and 46 deletions
+37 -8
View File
@@ -20,12 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter.
La version **beta.120** étend la clé dorée aux états des blocs : clic gauche
pour choisir une propriété, clic droit pour changer sa valeur, Maj pour inverser.
La terre et les modèles de blocs peuvent aussi tourner leurs textures.
Le [ticket de livraison](docs/golden-wrench-beta120.md) détaille les contrôles,
la compatibilité et la distribution. La release est publiée et linstance
**Sanctuary Beta** est synchronisée, sauvegardes et réglages conservés.
La version **beta.124** utilise une seule boîte ajustée au volume de chaque
sculpture, commune à la sélection et aux collisions. Les détails ne créent plus
de petites marches ; l'orientation, l'ancrage et la lumière restent cohérents.
Le [ticket de livraison](docs/sculpture-bounds-beta124.md) détaille le calcul
mis en cache et les vérifications en jeu et archives réussies. La publication
et la synchronisation de linstance sont pparées.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -36,6 +36,35 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## beta.124 — Une boîte par sculpture
Sélection et collisions simplifiées, dimensions ajustées et lumière conservée.
[Contrat et vérifications](docs/sculpture-bounds-beta124.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.124/Sanctuary-beta.124.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.124/Sanctuary-Test-beta.124.mrpack).
## beta.123 — Clé dorée et clé de Steve
Orientations cohérentes, inventaires et eau préservés ; clé de Steve pour
l’édition libre, utilisable aussi en survie. [Contrat et vérifications](docs/wrench-safety-beta123.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-beta.123.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-Test-beta.123.mrpack).
## beta.122 — Sculptures : forme et lumière
Collisions et sélection sur les voxels réels, creux traversables, lumière sans
occlusion cubique. [Contrat et vérifications](docs/sculpture-shape-light-beta122.md) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-beta.122.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Test-beta.122.mrpack).
## beta.121 — Texture de clé et pack personnel
Copie complète des ressources, création depuis le jeu et guide FR/EN inclus.
[Contrat et vérifications](docs/resourcepack-template-beta121.md) ·
[Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip) ·
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack) ·
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
## beta.120 — Clé dorée et états de blocs
Gestes du debug stick, rotation de textures et assemblage des machines conservé.
@@ -2134,7 +2163,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.112 |
| Sanctuary / pack | beta.122 |
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
@@ -2163,7 +2192,7 @@ décrits dans [Validation](docs/testing.md).
Résultats :
- `mods/sanctuary/build/libs/sanctuary-beta.112.jar` : mod à installer avec
- `mods/sanctuary/build/libs/sanctuary-beta.122.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).
+9
View File
@@ -127,3 +127,12 @@ de modèles, butin et états reprennent leurs équivalents natifs 26.3.
`tools/generate-colored-bricks.py` documente cette transformation ;
`tools/colored-bricks-palette.json` conserve couleurs et empreintes des PNG fournis.
Ces ressources dérivées ne sont pas présentées comme des créations originales.
## Clé dorée beta.121
Le PNG 16 × 16 `golden_wrench.png` a été fourni par le créateur de Sanctuary
le 17 septembre 2026. Il est conservé octet pour octet dans le mod, le pack
intégré et le template personnel (SHA-256
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`).
Le template regroupe les ressources client déjà distribuées par Sanctuary ;
leurs crédits et conditions respectives restent applicables.
+19 -1
View File
@@ -15,7 +15,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', 'verifyPack', 'assembleResourcePack')
dependsOn(':sanctuary:build', 'verifyPack', 'assembleResourcePack', 'assembleResourceTemplate')
commandLine('python3', 'scripts/pack.py', 'assemble')
}
@@ -46,3 +46,21 @@ tasks.register('assembleTestPack', Exec) {
dependsOn('assemblePack', ':sanctuary-test:build')
commandLine('python3', 'scripts/test_pack.py')
}
tasks.register('assembleResourceTemplate', Zip) {
group = 'distribution'
description = 'Export all Sanctuary client assets as an editable personal resource pack.'
dependsOn(':sanctuary:processResources', 'verifyResourcePack')
def resources = project(':sanctuary').layout.buildDirectory.dir('resources/main')
// Same precedence as the native exporter: built-in textures override mod defaults.
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(resources.map { it.dir('resourcepacks/textures/assets') }) { into 'assets' }
from(resources.map { it.dir('assets') }) { into 'assets' }
from(resources.map { it.dir('resourcepacks/template') })
from(resources.map { it.file('resourcepacks/textures/pack.png') })
exclude '**/.DS_Store'
destinationDirectory = layout.buildDirectory
archiveFileName = "Sanctuary-Template-${mod_version}.zip"
preserveFileTimestamps = false
reproducibleFileOrder = true
}
+35
View File
@@ -14,6 +14,41 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
de l'ancien pack 26.2.
## beta.123 — clé dorée et clé de Steve
La clé dorée se limite aux orientations cohérentes ; la clé de Steve reprend
les états et textures libres, utilisables aussi en survie dès sa récupération.
[Contrat et vérifications](wrench-safety-beta123.md).
[Release beta.123](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.123)
publiée ; canal `f988d01b901f2d3eda0b4ab47a48540dc627d1b5`. Deux
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
`sanctuary-backups/before-beta.123/`.
## beta.122 — formes et lumière des sculptures
Sélection et collisions sur les voxels réels ; suppression de l'occlusion cubique.
[Contrat et vérifications](sculpture-shape-light-beta122.md).
[Release beta.122](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.122)
publiée ; canal `3d340bea0093b709c6018624795a1b3ca557b72d`. Deux
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
`sanctuary-backups/before-beta.122/`.
## beta.121 — texture de clé et pack personnel
PNG du créateur et copie complète modifiable depuis l'écran des packs.
[Contrat et vérifications](resourcepack-template-beta121.md).
[Release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
publiée ; canal `e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`. Deux
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
`sanctuary-backups/before-beta.121/`. Le template personnel est créé par le
joueur, jamais géré ou réécrit par packwiz.
## beta.120 — clé dorée et états de blocs
Gestes du debug stick, rotation des textures et assemblages prioritaires.
+125
View File
@@ -0,0 +1,125 @@
# RP-02 — Clé dorée et pack personnel — beta.121
Contrat du 17 septembre 2026, branche `codex/resourcepack-template-beta121`.
Le créateur fournit `golden_wrench.png` et valide un bouton qui crée une copie
personnelle complète, modifiable et jamais écrasée par les mises à jour.
## Résultat
La clé dorée reprend exactement le PNG fourni, 16 × 16, sans conversion ni
redessin. Le modèle reste un item plat `minecraft:item/handheld`. Empreinte :
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`.
Le même fichier est fourni par le mod et par « Textures Sanctuary » ; désactiver
le pack intégré conserve donc aussi la nouvelle clé.
Dans **Options → Packs de ressources**, **Personnaliser Sanctuary** crée
`resourcepacks/Sanctuary-Custom/`. Le bouton devient **Ouvrir ma copie**.
La copie contient toutes les ressources client de Sanctuary : textures, modèles,
étiquettes d'items, blockstates, sons, shaders, langues et remplacements Minecraft
présents dans le pack intégré. Celui-ci prend priorité sur les assets de base
lors de l'export. Un guide FR/EN, la licence et les crédits accompagnent la copie.
Le joueur active sa copie avec les commandes natives et la place au-dessus des
packs qu'il veut remplacer. Créer la copie ne change ni l'ordre ni la sélection.
Une modification se recharge avec F3 + T ; désactiver le pack retrouve les
ressources fournies par le mod. L'écran de sélection des datapacks n'est pas modifié.
La copie est un **instantané**. Elle ne se synchronise pas avec les mises à jour :
les fichiers personnalisés restent toujours intacts. Le guide recommande de
supprimer les fichiers non personnalisés pour laisser les prochaines ressources
du mod s'appliquer. Pour repartir d'un instantané récent, renommer le dossier
existant puis créer une nouvelle copie. Aucun fichier utilisateur n'est écrasé.
La copie s'effectue hors du fil de rendu, dans un dossier temporaire voisin,
puis le résultat complet rejoint la liste native. Une erreur de copie est
signalée par le bouton et le journal du jeu ; un chemin occupé par un fichier
n'est pas remplacé. Aucune extraction au démarrage ni pendant les mises à jour.
## Distribution
`./gradlew assembleResourceTemplate` produit
`build/Sanctuary-Template-beta.121.zip`, un pack personnel complet à décompresser
dans `resourcepacks/Sanctuary-Custom/`. `assemblePack` le produit aussi.
`build/Sanctuary-Resource-Pack-beta.121.zip` reste l'export du pack graphique
intégré ; son manifeste versionné contient 151 fichiers et les images historiques
sont conservées. L'identifiant intégré `sanctuary:textures` reste stable et son
activation reste contrôlée par le joueur.
Le template ne contient pas tous les assets vanilla ni ceux des autres mods.
Il ne contient pas les recettes, règles serveur ou sauvegardes. Il conserve
les crédits et conditions des ressources d'origine. Aucune génération, aucun
identifiant de bloc et aucun format de sauvegarde ne changent.
## Vérifications
Le parcours `ResourceTemplate121ClientChecks` passe en **35 s** sur le client
Minecraft 26.3 macOS, sans ouvrir de monde :
- Bouton natif et disposition FR/EN aux échelles 2 et 3, captures relues.
- Export complet de **1 202 fichiers**, empreintes identiques aux ressources
sources après application des priorités du pack intégré.
- PNG fourni vérifié par SHA-256 et affiché avec sa transparence.
- Création sans changer la sélection ni l'ordre des packs.
- Modification d'une texture et ajout d'un fichier personnel : aucun octet
réécrit lors d'un second export ni au rechargement des ressources.
- Priorité réelle de la texture personnalisée, puis retour à la clé fournie
après désactivation. Activation et désactivation conservées à la redécouverte.
- Chemin occupé par un fichier refusé sans le modifier ; aucun bouton ajouté
à l'écran des datapacks.
```sh
./gradlew :sanctuary:runClientGameTest \
-PsanctuaryClientTests=true -PsanctuaryResourceTemplate121ClientTests=true \
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
```
Marqueur `TEMPLATE121_PASS` dans `build/template121-client.log` ; captures et
empreintes de la copie dans `build/template121-evidence/`. Le premier essai
avait échoué dans l'aperçu du test qui créait un ItemStack avant connexion à
un monde ; cet aperçu affiche maintenant directement la texture résolue.
Le modèle plat existant n'est pas modifié. Pas d'essai Windows ni d'ouverture
automatisée du gestionnaire de fichiers. Le GameTest serveur dédié reste exclu
conformément au refus antérieur de son EULA.
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
passe en **2 min 10 s**, 126 tâches. Les sources JAR correspondent aux sources
Java du dépôt. La comparaison avec beta.120 limite les différences de production
aux deux nouvelles classes, au hook d'interface, aux libellés et aux ressources
prévues (12 entrées). Aucun changement de gameplay, de données serveur ou de
monde. Les packs normal/Test contiennent le même JAR Sanctuary, sans fixture
ni sauvegarde. Le ZIP du template et l'export natif ont exactement les mêmes
1 202 fichiers et empreintes. Le pack intégré conserve ses 151 fichiers vérifiés.
Reçu : `build/template121-artifact.json`. Artefacts locaux validés :
- `Sanctuary-beta.121.mrpack` : 10261791 octets, SHA-256
`040d68f342155120fa21e0ffcdf34aa38e3225c78862960536622de53ed9fd9f`.
- `Sanctuary-Test-beta.121.mrpack` : 10280716 octets, SHA-256
`7f5bbd4e13aea4c245d072725c094ca64826ee8debc8607623783c114d437685`.
- `Sanctuary-Resource-Pack-beta.121.zip` : 3757952 octets, SHA-256
`bbf3c18ea6abc64d951911215975cb6c48c7fc5e0246f6e5f5192a3d061c211d`.
- `Sanctuary-Template-beta.121.zip` : 4315131 octets, SHA-256
`6fad376c53474fa8bcb0a2e6ae4c13ea61ab91bdd78735be443032d485ed541a`.
JAR Sanctuary : SHA-256
`4276f4fc9834805e3e6d12cd19adba7837e3007656b77f31d525c7b976f9925d`.
## Publication et installation
La [release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
est publiée depuis `d9b3dd6bedf32448f8c6e5bfa4da10cb69968bc7`. Tag et artefacts immuables,
téléchargements publics vérifiés. Canal packwiz :
`e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`.
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
Un seul JAR Sanctuary beta.121 est actif ; le second passage est identique.
Les **923 fichiers personnels et réglages suivis** restent inchangés,
y compris les packs de ressources. Aucune copie personnelle n'est créée par
la mise à jour : chaque joueur utilise le bouton quand il le souhaite.
Aucun monde personnel ouvert. Copie préalable dans
`sanctuary-backups/before-beta.121/` de l'instance existante ; reçus
`build/template121-isolated.json` et `build/template121-prism.json`.
- [Template personnel complet](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip).
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack).
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
+79
View File
@@ -0,0 +1,79 @@
# SCULPT-10 — Une boîte par sculpture — beta.124
Contrat du 17 septembre 2026, branche `codex/sculpture-bounds-beta124`.
Le créateur choisit **une seule boîte pour la sélection et les collisions**.
Ce ticket remplace la forme physique détaillée de beta.122.
La boîte englobe exactement les dimensions occupées par la sculpture, après
orientation et ancrage au centre ou au bord. Elle suit toujours la grille 1/16,
y compris les tailles impaires et les modèles comportant des marges vides.
Les trous et petits détails restent visibles mais n'ont plus leurs propres
surfaces de collision : on ne grimpe plus sur leurs petites marches. Une arche
se sélectionne et bloque désormais sur tout son volume englobant.
La pose utilise la même boîte avant la création de l'entité de bloc : elle
refuse une intersection avec le joueur, y compris dans les trous visuels.
Les petites sculptures laissent libre le reste du bloc autour de leur boîte.
La forme reste mise en cache par modèle et état ; elle ne se recalcule qu'après
un changement de modèle, d'orientation ou d'ancrage. Les requêtes physiques et
le contour partagent cette même forme native.
Le calcul parcourt les cellules une seule fois, sans créer de copies tournées
ni de grille de collisions 16³. Le contour émet toujours douze arêtes.
Il s'agit d'une simplification du travail géométrique ; aucun gain de FPS global
n'est annoncé sans mesure sur une scène représentative.
Le rendu des sculptures et les règles de lumière de beta.122 restent inchangés :
pas d'occlusion de cube plein, pas d'émission artificielle de lumière. Aucun
identifiant, format d'item ou de sauvegarde ne change. Les anciennes sculptures
utilisent la nouvelle forme à partir de leurs données existantes, sans réécriture
des mondes ni régénération de chunks. L'atelier et les clés restent inchangés.
## Vérifications
Le parcours `Sculpture122ClientChecks`, adapté au nouveau contrat, passe en
**39 s** sur Minecraft 26.3, client macOS et serveur intégré jetable de graine 122.
- Sélection native de tout le volume, y compris le trou visuel d'une arche ;
contour rectangulaire relu dans la capture.
- Même boîte physique et de sélection sur client et serveur ; intérieur rempli,
dimensions exactes sans profondeur artificielle de bloc entier.
- **80 cas** : boîte unique comparée aux limites du maillage réellement émis,
douze arêtes exactement, même objet réutilisé lors des requêtes répétées.
Arche, marges vides, cube de 4 096 cellules et voxel unique, dans les quatre
orientations et cinq ancrages.
- **5 120 placements** de rendu toujours alignés sur la grille entière.
- Pose refusée dans le joueur, y compris entre deux piliers séparés ; petite
sculpture autorisée à côté du joueur, hors de sa boîte physique.
- Cache renouvelé après changement d'orientation/ancrage ou de modèle ;
conservation après sauvegarde/reconnexion.
- **75 sculptures superposées** : ciel à 15 ; toit de pierre témoin sombre ;
lumière de bloc transmise avec atténuation normale ; pièce close à 0.
Marqueurs dans `build/sculpture124-client.log`, notamment
`SCULPTURE124_BOUNDS_PASS 80`, plus les contrôles de régression de beta.122.
Captures relues dans `build/sculpture124-evidence/`. Aucun essai Windows,
client distant ou shader tiers ; le coût des boîtes est simplifié sans benchmark
FPS global.
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
passe en **2 min 9 s**, 126 tâches (110 exécutées, 16 à jour).
Les sources des JAR correspondent aux sources du dépôt. La comparaison avec
beta.123 limite les différences de production aux trois classes de
`SculptureGeometry` ; ressources, clés, interfaces, données et éclairage restent
identiques. Les packs normal/Test contiennent le même JAR, sans sauvegarde
ni fixture. Le template personnel conserve exactement les ressources de beta.123.
- Pack normal : 10 271 059 octets, SHA-256
`2075e4ac7a7e7e2effcd82586d46769607ca1a67e8d9110b000fa378d1bc5506`.
- Pack Test : 10 289 985 octets, SHA-256
`88263e6896590ea9694b18084666e7234e934064e8d7d596d34866a68cc5ea23`.
- JAR Sanctuary : SHA-256
`b3969b1a3738b8a5d332b2083db0dd722900239f73035c335b738f70328d43f1`.
Reçu local : `build/sculpture124-artifact.json`. Les packs normal/Test et le
template sont aussi copiés, avec vérification octet pour octet, dans le dossier
habituel `sanctuary-beta/build/`.
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
Aucun monde personnel n'est ouvert par les essais.
+108
View File
@@ -0,0 +1,108 @@
# SCULPT-09 — Forme physique et lumière — beta.122
Contrat du 17 septembre 2026, branche `codex/sculpture-shape-light-beta122`.
Le créateur demande des bounding boxes adaptées aux sculptures et la suppression
des ombres cubiques visibles lorsqu'on en pose plusieurs côte à côte.
La sélection, le contact avec les entités et les collisions suivent les voxels
occupés de chaque sculpture, y compris les creux. Le rendu et la forme physique
partagent leur placement entier sur la grille 1/16, après orientation et ancrage.
Le centre, les quatre bords et les dimensions impaires gardent leur alignement.
La pose vérifie aussi les voxels portés avant que lentité de bloc existe :
elle refuse une intersection avec le joueur et autorise les espaces libres.
La forme est construite directement sur une grille native 16³ et mise en cache
par entité de bloc, modèle et état ; aucun calcul complet à chaque interrogation.
Les sculptures transmettent la lumière et ne sont plus utilisées comme cubes
pleins par l'occlusion ambiante. Leurs faces restent éclairées par le monde,
sans émission de lumière et sans rendu à luminosité maximale. Cette règle est
commune aux sculptures : l'éclairage natif stocké par bloc ne permet pas une
ombre distincte pour chaque petit voxel. L'atelier d'argile reste inchangé.
Aucun identifiant, blockstate, format d'item ou schéma sauvegardé ne change.
Les formes sont recalculées à partir des voxels existants ; aucune migration,
aucun parcours de monde ni régénération de chunk. Les données inconnues gardent
leur conservation opaque et une petite base sélectionnable pour récupérer l'item.
Le travail de documentation déjà présent dans l'autre branche reste intact.
## Vérifications et livraison
Le parcours `Sculpture122ClientChecks` passe en **37 s** sur Minecraft 26.3,
client macOS et serveur intégré, dans un monde plat jetable de graine 122 :
- Pose par la commande native d'utilisation ; sélection du pilier et visée
à travers le trou d'une arche jusqu'au bloc derrière, captures relues.
- Requêtes natives de collision côté serveur et client : voxels occupés solides,
espace vide libre, aucune profondeur artificielle d'un bloc entier.
- Pose refusée dans le joueur, sans consommer l'objet ; petite sculpture acceptée
dans la partie libre du même bloc, sans intersection avec le joueur.
- **80 cas** confrontent le volume et les faces de collision au maillage réellement
émis : arche creuse, marges vides, cube de 4 096 voxels, voxel unique, quatre
orientations et cinq ancrages. Chaque interrogation répétée réutilise la forme.
- Régression de **5 120 placements** du rendu existant : dimensions paires/impaires,
orientations et ancrages, plus marqueurs de direction et modèles avec marges.
- Changement d'orientation/ancrage puis changement des voxels sur la même entité :
cache recalculé et forme client synchronisée.
- **75 sculptures en trois couches** : skylight natif conservé à 15 partout.
Un toit de pierre témoin produit toujours une ombre. La lumière d'un bloc
traverse une sculpture avec l'atténuation de distance native (13 à deux blocs).
- Le rendu reçoit la lumière réelle du monde : 15 au ciel, **0 dans une pièce
fermée et obscure**. Aucun rendu artificiellement lumineux.
- Sauvegarde/reconnexion : modèle, collisions et lumière conservés sur les
deux côtés, sans fichier 3D source.
```sh
./gradlew :sanctuary:runClientGameTest \
-PsanctuaryClientTests=true -PsanctuarySculpture122ClientTests=true \
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
```
Marqueurs `SCULPTURE122_PASS`, `SCULPTURE122_GEOMETRY_PASS`,
`SCULPTURE122_PLACEMENT_PASS`, `SCULPTURE122_LIGHT_PASS` et
`SCULPTURE113_GEOMETRY_PASS` dans `build/sculpture122-client.log` ; captures
conservées dans `build/sculpture122-evidence/`. Le test prépare explicitement
son sol à Y=0 : le preset plat nu commence à Y=-1.
Aucun monde personnel ouvert ; pas d'essai Windows ni depuis deux ordinateurs,
ni de garantie sur les ombres ajoutées par des shaders tiers.
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son EULA.
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
passe en **2 min 13 s**, 126 tâches. Les sources JAR correspondent aux sources
Java du dépôt. Par rapport à beta.121, les différences de production se limitent
aux classes des sculptures et à la nouvelle géométrie partagée (14 entrées,
y compris le déplacement du sélecteur synthétique d'orientation). Toutes les
ressources client, données serveur et configurations de mixins sont identiques.
Les packs normal/Test contiennent le même JAR, sans sauvegarde ni fixture.
Le template beta.122 contient les mêmes ressources que beta.121, octet pour octet.
Reçu local : `build/sculpture122-artifact.json`.
- `Sanctuary-beta.122.mrpack` : 10267459 octets, SHA-256
`b6e24d65797eb7f16670d2f3a591b5f2476820608f72241ecd37fb78a486008e`.
- `Sanctuary-Test-beta.122.mrpack` : 10286380 octets, SHA-256
`90487ee3ad4298fccb8c24e8781af4cbf043d88dc0379fba5c21d99733dac92d`.
JAR Sanctuary : SHA-256
`ff8ebf89f39a64809fae6912253386793e9b5e66ec0adb9fa7521729893d406e`.
## Publication et installation
La [release beta.122](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.122)
est publiée depuis `edc7fc3dfb117cde61be42c1d79b03af11f5dec6`. Tag et artefacts immuables,
téléchargements publics vérifiés. Canal packwiz :
`3d340bea0093b709c6018624795a1b3ca557b72d`.
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
Un seul JAR Sanctuary beta.122 est actif ; le second passage est identique.
Les **923 fichiers personnels et réglages suivis** restent inchangés.
Aucun monde personnel ouvert. Copie préalable dans
`sanctuary-backups/before-beta.122/` de l'instance existante ; reçus
`build/sculpture122-isolated.json` et `build/sculpture122-prism.json`.
Le ticket a été réalisé dans le worktree `sanctuary-sculpture-beta122` afin de
préserver les changements de `docs/vision.md` et `docs/villageois-illageois.md`
déjà présents sur la branche de documentation du checkout principal.
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-beta.122.mrpack).
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Test-beta.122.mrpack).
- [Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Template-beta.122.zip).
+129
View File
@@ -0,0 +1,129 @@
# TOOL-02 — Clé dorée et clé de Steve — beta.123
Contrat du 17 septembre 2026, branche `codex/wrench-safety-beta123`.
Ce ticket remplace le contrat de la clé dorée de beta.120 : l'édition libre
reste disponible dans un objet distinct, la **Clé à molette de Steve**.
Le créateur précise que la clé de Steve reste utilisable en survie dès quun
joueur la récupère, sans condition opérateur. Elle na pas de recette.
## Clé dorée : orientations natives
L'identifiant `sanctuary:golden_wrench`, sa recette et sa texture restent stables.
Clic droit parcourt l'orientation native ; Maj parcourt en sens inverse.
Clic gauche rappelle les gestes, sans casser le bloc ni sélectionner un état.
Les familles de propriétés autorisées sont les orientations horizontales,
les six directions, les trois axes, la rotation native des panneaux et
l'orientation native des blocs qui la proposent. La règle porte sur les
propriétés Minecraft, sans catalogue à maintenir pour chaque type de bloc.
L'eau, la moitié d'un escalier, les inventaires, les états de croissance,
les quantités, la puissance et les autres propriétés ne sont jamais proposées.
Aucune transformation indépendante des textures n'est créée par la clé dorée.
Les textures suivent seulement le modèle orienté par Minecraft.
Les connexions et formes sont recalculées à partir des voisins réels, avec les
mises à jour natives actives. Une orientation privée de support est refusée
sans casser le bloc. L'eau déjà contenue dans un bloc est conservée.
Les coffres doubles, portes, lits, étagères reliées, pistons sortis et leurs
pièces mobiles ne peuvent pas être tournés individuellement. Ils restent dans
leur état courant et une indication explique le refus. Il faut replacer les
éléments concernés ou rétracter le piston ; ce ticket ne déplace pas un
ensemble lié. Les coffres simples conservent leur entité et leur inventaire.
L'assemblage, l'ouverture et la dissociation des Métablis, Fûts, Fourneaux et
super pistons restent prioritaires avec les deux clés. Les parties techniques
d'un multibloc Sanctuary assemblé ne sont pas éditées individuellement.
## Clé de Steve
Nouvel identifiant `sanctuary:steve_wrench`, dans la section créative des outils,
sans recette de survie. La possession de cet objet donne accès à ses pouvoirs,
y compris en survie sans commandes et sans statut opérateur, dans les deux
mains. Les restrictions natives de construction, aventure, spectateur, portée,
protection et verrouillage restent appliquées côté serveur.
Les gestes de beta.120 sont conservés : clic gauche choisit la propriété,
clic droit la modifie, Maj inverse le parcours. Tous les états natifs et la
rotation indépendante des textures y restent volontairement accessibles.
La clé de Steve est donc volontairement capable de produire des états non
naturels, y compris pour le joueur qui la récupère en survie.
La texture finale est le PNG 16 × 16 bleu fourni par le créateur dans ce ticket,
copié sans transformation dans `assets/sanctuary/textures/item/steve_wrench.png`.
SHA-256 : `0084c41e1f46958903075e21866facd97d85e03fb8e06742e25b2ab254956fa3`.
Le modèle reste un objet Minecraft 2D tenu en main, avec rareté épique et reflet.
La proposition imagegen intermédiaire n'est pas livrée. Les noms, messages et
indications des deux outils existent en français et en anglais.
## Sauvegardes et ressources
Aucune migration, modification de génération ni parcours des mondes existants.
Les transformations de texture précédemment sauvegardées sont conservées ;
la clé de Steve permet de les modifier. Les états non naturels déjà présents
ne sont pas automatiquement réécrits. Les clés dorées existantes reçoivent
immédiatement les nouvelles restrictions par leur identifiant inchangé.
Le template personnel inclut le nouvel objet à sa prochaine création ; une
copie personnelle existante n'est jamais écrasée.
## Vérifications
Le parcours `Wrench123ClientChecks` passe en **1 min 12 s**, client macOS et
serveur intégré, Minecraft 26.3, dans des mondes plats jetables de graines
123 et 120. Il reprend le parcours avancé de beta.120 sur la clé de Steve :
- Vrais clics dorés répétés sur escalier, coffre et terre : pas de nouvelle eau,
pas de sélection libre, pas de faux coffre double ni de rotation de texture.
- Rotation inverse, eau existante conservée, formes recalculées selon les voisins,
trois axes de bûches et six orientations des pistons isolés.
- Coffre double avec ses deux inventaires intacts ; portes, lits, pistons sortis
et pièces mobiles protégés ; torche murale sans support laissée intacte.
- Vrais clics Steve en survie sans commandes : états et textures accessibles,
dans les deux mains. Créatif accessible aussi sans commandes ; restrictions
aventure/spectateur/portée conservées.
- **2 522 propriétés natives** toujours réversibles pour Steve, UV réellement
tournés sans déplacer la géométrie, rechargement des ressources, inventaires
et sauvegarde/reconnexion conservés.
- Assemblage et dissociation des quatre familles avec la clé dorée par vrais
clics ; le PNG de Steve apparaît en main et dans la barre rapide.
Marqueurs `WRENCH123_SAFE_PASS`, `WRENCH123_PASS`, `WRENCH120_PASS` dans
`build/wrench123-client.log`. Captures relues dans `build/wrench123-evidence/`.
Pas de validation Windows, de client distant ou de shader tiers.
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
passe en **2 min 19 s**, 126 tâches (103 exécutées, 23 à jour).
Les sources des JAR correspondent aux sources du dépôt. La comparaison avec
beta.122 limite les dix entrées de production modifiées aux classes des clés,
aux libellés et aux ressources du nouvel objet. Les autres ressources,
notamment les sculptures et la texture dorée, sont identiques.
Les packs normal/Test contiennent le même JAR Sanctuary, sans sauvegarde ni
fixture de test. Le template ZIP correspond exactement aux ressources du JAR
et à ses ressources embarquées, dont le PNG bleu du créateur.
- Pack normal : 10 270 954 octets, SHA-256
`a7401dbef1bd1b888da2686368943eddc82d0d43e20449330c4849b4dff93e5f`.
- Pack Test : 10 289 877 octets, SHA-256
`c57cc6fc47f887b90850473e51407b610a6c5639c35fc2b78dc9ddb74f795b80`.
- JAR Sanctuary : SHA-256
`23e1a81c793a048c6375a0da07eb0bbe1a18f833a9ceb61a3c1dc5f1a3e65a30`.
Reçu local : `build/wrench123-artifact.json`.
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
Aucun monde personnel n'est ouvert par les essais.
## Publication et installation
La [release beta.123](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.123)
est publiée depuis `4552cc347b7fded610b1eda7a67ffaad56a5e201`.
Le tag exact et les artefacts sont immuables ; les téléchargements publics
ont été vérifiés. Canal packwiz : `f988d01b901f2d3eda0b4ab47a48540dc627d1b5`.
Le template personnel publié inclut la clé de Steve ; SHA-256
`f84d3c1c0e38232add90fb943dc39fd3e89138576df86024ceaa311478ed5cb9`.
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
Un seul JAR Sanctuary beta.123 est actif, conforme au hash vérifié ; le second
passage ne change rien. Les **923 fichiers personnels et réglages suivis**
restent inchangés, sans ouverture de monde personnel. Copie préalable dans
`sanctuary-backups/before-beta.123/` de l'instance existante.
Reçus : `build/wrench123-isolated.json` et `build/wrench123-prism.json`.
+3 -3
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.120
pack_version=beta.120
resource_pack_version=beta.090
mod_version=beta.124
pack_version=beta.124
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+21
View File
@@ -93,6 +93,18 @@ tasks.named('runGameTest') {
}
tasks.named('processGametestResources') {
def wrench123Client = providers.gradleProperty('sanctuaryWrench123ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryWrench123ClientTests', wrench123Client)
if (wrench123Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench123ClientChecks') }
def sculpture122Client = providers.gradleProperty('sanctuarySculpture122ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuarySculpture122ClientTests', sculpture122Client)
if (sculpture122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture122ClientChecks') }
def template121Client = providers.gradleProperty('sanctuaryResourceTemplate121ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryResourceTemplate121ClientTests', template121Client)
if (template121Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ResourceTemplate121ClientChecks') }
def wrench120Client = providers.gradleProperty('sanctuaryWrench120ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryWrench120ClientTests', wrench120Client)
if (wrench120Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench120ClientChecks') }
@@ -444,6 +456,15 @@ tasks.withType(JavaCompile).configureEach {
}
processResources {
from(rootProject.file('ressources-pack/template')) {
exclude '**/.DS_Store'
into 'resourcepacks/template'
}
from(rootProject.file('LICENSE')) {
rename { 'LICENSE_sanctuary' }
into 'resourcepacks/template'
}
from(rootProject.file('THIRD_PARTY_NOTICES.md')) { into 'resourcepacks/template' }
from(rootProject.file('ressources-pack/sanctuary')) {
include 'assets/**', 'pack.mcmeta', 'pack.png'
exclude '**/.DS_Store'
@@ -0,0 +1,165 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.packs.PackSelectionScreen;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.repository.PackCompatibility;
/** Native screen/export, complete snapshot, real resource precedence and preservation of edits. */
public final class ResourceTemplate121ClientChecks implements FabricClientGameTest {
private static final String BUILTIN = "sanctuary:textures";
private static final String PERSONAL = "file/" + ResourcePackTemplate.FOLDER;
private static final String WRENCH = "sanctuary:textures/item/golden_wrench.png";
private static final String WRENCH_PATH = "assets/sanctuary/textures/item/golden_wrench.png";
private static final String WRENCH_SHA = "a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af";
public void runTest(ClientGameTestContext c) {
try { run(c); } catch (IOException error) { throw new AssertionError(error); }
}
private void run(ClientGameTestContext c) throws IOException {
var previous = c.computeOnClient(m -> m.gui.screen());
var packDirectory = c.computeOnClient(Minecraft::getResourcePackDirectory);
var selected = c.computeOnClient(m -> List.copyOf(m.getResourcePackRepository().getSelectedIds()));
var target = packDirectory.resolve(ResourcePackTemplate.FOLDER);
check(!Files.exists(target), "Fresh disposable client directory");
c.runOnClient(m -> {
check(source(m).equals(BUILTIN), "Built-in pack active");
check(hash(resource(m, WRENCH)).equals(WRENCH_SHA), "Exact creator PNG active");
m.getWindow().setWindowed(1280, 800);
});
for (int scale : new int[]{2, 3}) for (String language : List.of("fr_fr", "en_us")) {
c.runOnClient(m -> {
m.options.guiScale().set(scale); m.resizeGui();
m.getLanguageManager().setSelected(language);
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
screen(m, packDirectory);
var button = button(m, "customize");
check(button.getX() >= 0 && button.getRight() <= m.gui.screen().width, "Template button fits width");
check(button.getY() > 60 && button.getBottom() < m.gui.screen().height, "Template button fits height");
var done = m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
.filter(b -> key(b, "gui.done")).findFirst().orElseThrow();
check(done.getY() >= button.getBottom() + 4, "Separate native footer row");
check(m.font.width(button.getMessage()) < button.getWidth() - 8, "Localized label fits");
});
c.waitTicks(3); c.takeScreenshot("template121-create-" + language + "-" + scale);
}
c.runOnClient(m -> button(m, "customize").onPress(new KeyEvent(InputConstants.KEY_RETURN, 0, 0)));
c.waitFor(m -> m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
.anyMatch(b -> b.active && key(b, "sanctuary.resource_pack.open_copy")), 1200);
c.waitFor(m -> m.getResourcePackRepository().getAvailableIds().contains(PERSONAL), 1200);
c.runOnClient(m -> {
check(List.copyOf(m.getResourcePackRepository().getSelectedIds()).equals(selected), "Creating never changes selection");
check(m.getResourcePackRepository().getPack(PERSONAL).getCompatibility() == PackCompatibility.COMPATIBLE,
"Template format accepted by exact Minecraft version");
});
var mod = FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
Map<String, String> expected = new TreeMap<>();
merge(expected, mod.findPath("assets").orElseThrow(), "assets/");
merge(expected, mod.findPath("resourcepacks/textures/assets").orElseThrow(), "assets/");
merge(expected, mod.findPath("resourcepacks/template").orElseThrow(), "");
expected.put("pack.png", hash(Files.readAllBytes(mod.findPath("resourcepacks/textures/pack.png").orElseThrow())));
check(snapshot(target).equals(expected), "Complete byte-identical snapshot with built-in precedence");
Files.writeString(packDirectory.getParent().resolve("template121-export-sha256.json"),
new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(expected));
check(expected.containsKey("README.md") && expected.containsKey("LICENSE_sanctuary")
&& expected.containsKey("THIRD_PARTY_NOTICES.md"), "Editable copy includes guide and credits");
check(expected.keySet().stream().noneMatch(p -> p.startsWith("data/") || p.endsWith(".class")), "Client assets only");
c.waitTicks(4); c.takeScreenshot("template121-copy-ready");
c.runOnClient(m -> m.gui.setScreen(new Screen(Component.literal("Golden wrench")) {
@Override public void extractRenderState(GuiGraphicsExtractor g, int x, int y, float delta) {
g.fill(0, 0, width, height, 0xff292929);
g.centeredText(font, Component.translatable("item.sanctuary.golden_wrench"), width / 2, 55, 0xffffffff);
g.blit(RenderPipelines.GUI_TEXTURED, Identifier.parse(WRENCH), width / 2 - 48, 85, 0, 0, 96, 96, 16, 16, 16, 16);
}
}));
c.waitTicks(4); c.takeScreenshot("template121-wrench-exact-texture");
byte[] replacement = c.computeOnClient(m -> resource(m, "minecraft:textures/item/golden_pickaxe.png"));
Files.write(target.resolve(WRENCH_PATH), replacement);
Files.writeString(target.resolve("my-notes.txt"), "Personal edits must survive all subsequent exports.");
var edited = snapshot(target);
check(ResourcePackTemplate.create(packDirectory).equals(target.toAbsolutePath().normalize()), "Existing copy returned");
check(snapshot(target).equals(edited), "Second export preserves every edited byte and extra file");
c.runOnClient(m -> {
var repo = m.getResourcePackRepository(); repo.addPack(PERSONAL); m.options.updateResourcePacks(repo);
});
c.waitFor(m -> source(m).equals(PERSONAL) && m.gui.overlay() == null, 1200);
c.runOnClient(m -> {
check(Arrays.equals(resource(m, WRENCH), replacement), "Personal edit overrides built-in texture");
var repo = m.getResourcePackRepository(); repo.reload(); m.options.loadSelectedResourcePacks(repo);
check(repo.getSelectedIds().contains(PERSONAL), "Personal activation persists after rediscovery");
screen(m, packDirectory); button(m, "open_copy");
});
c.waitTicks(3); c.takeScreenshot("template121-personal-active");
var reload = c.computeOnClient(Minecraft::reloadResourcePacks);
c.waitFor(m -> reload.isDone() && m.gui.overlay() == null, 1200); reload.join();
check(snapshot(target).equals(edited), "Reload never writes personal assets");
c.runOnClient(m -> {
check(Arrays.equals(resource(m, WRENCH), replacement), "Reload still uses personal PNG");
var repo = m.getResourcePackRepository(); repo.removePack(PERSONAL); m.options.updateResourcePacks(repo);
});
c.waitFor(m -> source(m).equals(BUILTIN) && m.gui.overlay() == null, 1200);
c.runOnClient(m -> {
check(hash(resource(m, WRENCH)).equals(WRENCH_SHA), "Disabling restores creator texture");
var repo = m.getResourcePackRepository(); repo.reload(); m.options.loadSelectedResourcePacks(repo);
check(!repo.getSelectedIds().contains(PERSONAL), "Explicit opt-out retained");
});
Path collision = Files.createTempDirectory(packDirectory.getParent(), "template121-collision-");
Files.writeString(collision.resolve(ResourcePackTemplate.FOLDER), "Do not replace this file");
try { ResourcePackTemplate.create(collision); throw new AssertionError("Existing file must be refused"); }
catch (IOException expectedError) { check(Files.readString(collision.resolve(ResourcePackTemplate.FOLDER)).equals("Do not replace this file"), "Collision leaves file intact"); }
Path datapacks = Files.createTempDirectory(packDirectory.getParent(), "template121-datapacks-");
c.runOnClient(m -> {
screen(m, datapacks);
check(m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
.noneMatch(b -> key(b, "sanctuary.resource_pack.customize") || key(b, "sanctuary.resource_pack.open_copy")), "No button in datapack selector");
m.gui.setScreen(previous);
});
System.out.println("TEMPLATE121_PASS: " + expected.size() + " exact files; native button FR/EN at scales 2/3; complete copy; edits and selection preserved; actual override, reload and opt-out; collision safe; datapacks untouched");
}
private static void screen(Minecraft m, Path directory) {
m.gui.setScreen(new PackSelectionScreen(m.getResourcePackRepository(), repo -> {}, directory, Component.translatable("resourcePack.title")));
}
private static Button button(Minecraft m, String suffix) {
return m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
.filter(b -> key(b, "sanctuary.resource_pack." + suffix)).findFirst().orElseThrow();
}
private static boolean key(Button b, String key) { return b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key); }
private static String source(Minecraft m) { return m.getResourceManager().getResource(Identifier.parse(WRENCH)).orElseThrow().sourcePackId(); }
private static byte[] resource(Minecraft m, String name) {
try (var in = m.getResourceManager().getResource(Identifier.parse(name)).orElseThrow().open()) { return in.readAllBytes(); }
catch (IOException error) { throw new AssertionError(error); }
}
private static String hash(byte[] bytes) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
catch (java.security.NoSuchAlgorithmException error) { throw new AssertionError(error); }
}
private static Map<String, String> snapshot(Path root) throws IOException { Map<String, String> result = new TreeMap<>(); merge(result, root, ""); return result; }
private static void merge(Map<String, String> result, Path root, String prefix) throws IOException {
try (var files = Files.walk(root)) { for (Path file : files.filter(Files::isRegularFile).toList()) result.put(prefix + root.relativize(file).toString().replace('\\', '/'), hash(Files.readAllBytes(file))); }
}
private static void check(boolean value, String message) { if (!value) throw new AssertionError(message); }
}
@@ -0,0 +1,161 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.plans.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.core.*;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.phys.*;
import net.minecraft.world.phys.shapes.*;
/** Single-box selection/collision bounds and light propagation compared with the real sculpture mesh. */
public final class Sculpture122ClientChecks implements FabricClientGameTest {
private static final String MODEL="sanctuary:model/"+"1".repeat(64);
private static final BlockPos TARGET=new BlockPos(0,1,0),SKY=new BlockPos(10,2,2),CONTROL=new BlockPos(18,2,2);
private static final VoxelSculpture ARCH=arch();
public void runTest(ClientGameTestContext c){
var intro=new AtomicBoolean(true);
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
}});
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture122 "+UUID.randomUUID());s.setSeed("122");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()){
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-14;x<=21;x++)for(int z=-6;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);p.setYRot(0);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));p.inventoryMenu.broadcastFullState();
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");
});
c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(TARGET.below()).is(Blocks.STONE),150);
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,.999,.5)));c.waitTicks(3);
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.below())&&hit.getDirection()==Direction.UP,"Cursor aims at placement support"));
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(3);
server.runOnServer(s->System.out.println("SCULPTURE122_PLACED "+player(s).level().getBlockState(TARGET)+" "+player(s).level().getBlockEntity(TARGET)));
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),150);
server.runOnServer(s->{physical(player(s).level(),TARGET);placementOverlap(player(s));});
c.runOnClient(m->{physical(m.level,TARGET);geometry(m);Sculpture113ClientChecks.geometry(m,MODEL);});
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.0625,1.5,.5)));c.waitTicks(5);
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects an occupied pillar"));
c.takeScreenshot("sculpture124-single-outline");
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET.north(),Blocks.REDSTONE_BLOCK.defaultBlockState()));
c.waitFor(m->m.level.getBlockState(TARGET.north()).is(Blocks.REDSTONE_BLOCK),100);
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1.5,.5)));c.waitTicks(5);
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects the sculpture bounds even through its visual hole"));
c.takeScreenshot("sculpture124-select-volume");
// A state edit and fresh entity payload must invalidate geometry on both sides.
server.runOnServer(s->{var l=player(s).level();l.setBlockAndUpdate(TARGET,l.getBlockState(TARGET).setValue(Statuary.FACING,Direction.WEST).setValue(Statuary.ANCHOR,SculptureAnchor.EAST));});
c.waitFor(m->m.level.getBlockState(TARGET).getValue(Statuary.FACING)==Direction.WEST,100);
server.runOnServer(s->{var l=player(s).level();var bounds=l.getBlockState(TARGET).getShape(l,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Server shape rotates and moves to aimed edge");});
c.runOnClient(m->{var bounds=m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Client shape follows changed blockstate");});
var small=new VoxelSculpture(MODEL,"Small",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1)));
server.runOnServer(s->place(player(s).level(),TARGET,small,Direction.WEST,SculptureAnchor.EAST));
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&small.equals(e.sculpture()),100);
c.runOnClient(m->check(m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds().getYsize()==1/16d,"Changed block entity payload invalidates cached shape"));
server.runOnServer(s->{var l=player(s).level();place(l,TARGET,ARCH,Direction.SOUTH,SculptureAnchor.CENTER);l.setBlockAndUpdate(TARGET.north(),Blocks.AIR.defaultBlockState());
for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)place(l,new BlockPos(x,y,z),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
for(int x=16;x<=20;x++)for(int z=0;z<=4;z++)l.setBlockAndUpdate(new BlockPos(x,3,z),Blocks.STONE.defaultBlockState());
l.setBlockAndUpdate(new BlockPos(-8,2,0),Blocks.GLOWSTONE.defaultBlockState());place(l,new BlockPos(-7,2,0),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
});
server.waitFor(s->{var l=player(s).level();return l.getBrightness(LightLayer.SKY,SKY)==15&&l.getBrightness(LightLayer.SKY,CONTROL)<15&&l.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13;},200);
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15&&m.level.getBrightness(LightLayer.SKY,CONTROL)<15&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13,200);
server.runOnServer(s->{var l=player(s).level();for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)check(l.getBrightness(LightLayer.SKY,new BlockPos(x,y,z))==15,"All 75 sculptures keep full native skylight");});
c.runOnClient(m->{var e=(StatuaryEntity)m.level.getBlockEntity(new BlockPos(10,4,2));var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(e,state,0,Vec3.ZERO,null);check(net.minecraft.util.LightCoordsUtil.sky(state.lightCoords)==15,"Actual renderer receives full skylight inside a group");});
// Visual crowd at ground level: no whole-block AO squares between the models.
server.runOnServer(s->{var p=player(s);for(int x=-3;x<=3;x++)for(int z=-5;z<=-2;z++)place(p.level(),new BlockPos(x,1,z),ARCH,Direction.from2DDataValue(Math.floorMod(x+z,4)),SculptureAnchor.CENTER);p.teleportTo(.5,3,5.5);p.getAbilities().flying=true;p.onUpdateAbilities();});
c.waitFor(m->m.player.distanceToSqr(.5,3,5.5)<.1,100);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1,-3)));c.waitTicks(12);c.takeScreenshot("sculpture122-crowd-light");
// Transparent-to-light is not emissive: in a closed room sculpture faces stay dark.
server.runOnServer(s->{var l=player(s).level();for(int x=-13;x<=-9;x++)for(int y=1;y<=5;y++)for(int z=5;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,y,z),x==-13||x==-9||y==1||y==5||z==5||z==9?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());place(l,new BlockPos(-11,2,7),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);});
c.waitFor(m->m.level.getBlockEntity(new BlockPos(-11,2,7)) instanceof StatuaryEntity&&m.level.getBrightness(LightLayer.SKY,new BlockPos(-11,2,7))==0&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-11,2,7))==0,200);
c.runOnClient(m->{var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState((StatuaryEntity)m.level.getBlockEntity(new BlockPos(-11,2,7)),state,0,Vec3.ZERO,null);check(state.lightCoords==0,"No fullbright workaround: a dark room produces dark faces");});
System.out.println("SCULPTURE122_LIGHT_PASS: 75 stacked models sky=15; opaque control shadow; block light=13 across sculpture; dark room=0; shade brightness=1");
}
try(var world=saved.open()){
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),1200);
world.getServer().runOnServer(s->physical(player(s).level(),TARGET));c.runOnClient(m->physical(m.level,TARGET));
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15,200);
System.out.println("SCULPTURE122_PASS: native placement, one-box outline including holes, single-box physical collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
}
}
private static void placementOverlap(ServerPlayer player){
var pos=new BlockPos(3,1,0);var level=player.level();player.teleportTo(3.75,1,.5);player.setYRot(0);
var filled=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)filled.add(new VoxelSculpture.Cell(x,y,z,-1));
var dense=new VoxelSculpture(MODEL,"Solid",16,16,16,filled);var stack=StatuaryEntity.item(dense);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
var hit=new BlockHitResult(new Vec3(3.01,1,.5),Direction.UP,pos.below(),false);
var blocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
check(!blocked.consumesAction()&&level.getBlockState(pos).isAir()&&stack.getCount()==1,"Native placement refuses occupied voxels inside player without consuming sculpture");
player.teleportTo(3.5,1,.5);var pillars=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x:new int[]{0,15})pillars.add(new VoxelSculpture.Cell(x,y,0,-1));
var hollow=new VoxelSculpture(MODEL,"Two pillars",16,16,1,pillars);stack=StatuaryEntity.item(hollow);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
var hollowBlocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
check(!hollowBlocked.consumesAction()&&level.getBlockState(pos).isAir(),"Placement also rejects a player inside the enclosing box between separated pillars");player.teleportTo(3.75,1,.5);
var postCells=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x=0;x<2;x++)for(int z=0;z<2;z++)postCells.add(new VoxelSculpture.Cell(x,y,z,-1));
var post=new VoxelSculpture(MODEL,"Post",2,16,2,postCells);stack=StatuaryEntity.item(post);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
var allowed=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
check(allowed.consumesAction()&&level.getBlockState(pos).is(Statuary.BLOCK),"Native placement allows empty part of block beside player");
check(level.noCollision(player,player.getBoundingBox()),"Accepted sculpture does not intersect player");
level.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());player.teleportTo(.5,1,3.2);player.getInventory().setItem(player.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));player.inventoryMenu.broadcastFullState();
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied bounds including visual holes, allows space outside the box, preserves stack on rejection");
}
private static void physical(Level level,BlockPos pos){
var state=level.getBlockState(pos);var shape=state.getShape(level,pos);var collision=state.getCollisionShape(level,pos);
check(shape==collision&&shape.toAabbs().size()==1,"Selection and collision share one cached bounding box");check(shape==state.getShape(level,pos),"Selection queries reuse the cached box");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Outline ray selects the whole sculpture volume");
check(level.clip(new ClipContext(new Vec3(.0625,1.5,2),new Vec3(.0625,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Collision ray hits pillar");
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Physical collision uses the enclosing volume, including visual holes");
check(!level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query fills the inner volume instead of following tiny details");
check(!level.noCollision(new AABB(.02,1.2,.4,.1,1.7,.6)),"Native collision query blocks occupied pillar");
check(!state.isCollisionShapeFullBlock(level,pos)&&!state.isSolidRender()&&!state.canOcclude(),"Sculpture is not a solid occluding cube");
check(state.getOcclusionShape().isEmpty()&&state.propagatesSkylightDown()&&state.getLightDampening()==0&&state.getShadeBrightness(level,pos)==1,"Light metadata never reintroduces full-block occlusion");
check(!state.isSuffocating(level,pos)&&state.getLightEmission()==0,"Decorative shape neither suffocates nor emits light");
}
private static void geometry(Minecraft m){
var padded=new VoxelSculpture(MODEL,"Padded",13,5,15,List.of(new VoxelSculpture.Cell(2,2,3,-1),new VoxelSculpture.Cell(6,4,10,-1)));
var dense=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)dense.add(new VoxelSculpture.Cell(x,y,z,-1));
var models=List.of(ARCH,padded,new VoxelSculpture(MODEL,"Dense",16,16,16,dense),new VoxelSculpture(MODEL,"One voxel",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1))));
int cases=0;for(var model:models)for(var direction:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor);var entity=new StatuaryEntity(BlockPos.ZERO,state);entity.setLevel(m.level);load(entity,model,m.level);
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(boxes.size()==1&&volume>=model.cells().size()/4096d,"One box encloses all occupied cells");
var outline=entity.shape(state);check(outline==entity.shape(state),"Repeated outline queries reuse the cached box");
check(outline.toAabbs().size()==1&&outline.bounds().equals(shape.bounds()),"One outline box matches exact rendered extents for every orientation and anchor");
int[] edges={0};outline.forAllEdges((x1,y1,z1,x2,y2,z2)->edges[0]++);check(edges[0]==12,"Native outline emits only the 12 enclosing edges, independent of voxel count");
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
double minX=1,minY=1,minZ=1,maxX=0,maxY=0,maxZ=0;
for(var q:render.mesh){minX=Math.min(minX,q.x());minY=Math.min(minY,q.y());minZ=Math.min(minZ,q.z());maxX=Math.max(maxX,q.x()+1/16d);maxY=Math.max(maxY,q.y()+1/16d);maxZ=Math.max(maxZ,q.z()+1/16d);var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
}
check(shape.bounds().equals(new AABB(minX,minY,minZ,maxX,maxY,maxZ)),"Single collision box exactly matches visible mesh extents");
cases++;
}
System.out.println("SCULPTURE124_BOUNDS_PASS "+cases+" cached single boxes agree with exact renderer extents; 12 outline edges each");
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native bounding boxes agree with emitted mesh extents, plus cached 12-edge selection bounds for holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
}
private static void place(ServerLevel level,BlockPos pos,VoxelSculpture model,Direction facing,SculptureAnchor anchor){
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);level.setBlockAndUpdate(pos,state);var entity=(StatuaryEntity)level.getBlockEntity(pos);load(entity,model,level);entity.setChanged();level.sendBlockUpdated(pos,state,state,3);
}
private static void load(StatuaryEntity entity,VoxelSculpture model,Level level){var tag=new CompoundTag();tag.put("sculpture",StatuaryEntity.encode(model));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,level.registryAccess(),tag));}
private static VoxelSculpture arch(){var cells=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<4;z++)if(x<2||x>=14||y>=14)cells.add(new VoxelSculpture.Cell(x,y,z,0xffc99679));return new VoxelSculpture(MODEL,"Arch",16,16,4,cells);}
private static ServerPlayer player(MinecraftServer server){return server.getPlayerList().getPlayers().getFirst();}
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
}
@@ -43,7 +43,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench120 "+UUID.randomUUID());s.setSeed("120");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.STEVE_WRENCH));p.inventoryMenu.broadcastFullState();
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
int tested=0;for(var block:BuiltInRegistries.BLOCK)for(var property:block.getStateDefinition().getProperties()){
@@ -52,7 +52,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
}
check(tested>1000,"Broad block property coverage");System.out.println("WRENCH120_PROPERTIES "+tested);
});
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));click(c,TARGET,false,false);
server.waitFor(s->player(s).level().getBlockState(TARGET)!=stairs,100);c.waitFor(m->m.level.getBlockState(TARGET)!=stairs,100);
server.runOnServer(s->{var actual=player(s).level().getBlockState(TARGET);check(actual.getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING)&&actual.getValue(StairBlock.HALF)==stairs.getValue(StairBlock.HALF),"Right click changes facing only");});
@@ -64,8 +64,8 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
c.takeScreenshot("wrench120-stairs");
// A container rotates in place; inventory and block entity survive the full native interaction.
click(c,CHEST,false,false);server.runOnServer(s->{var p=player(s);var chest=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(chest.getItem(0).is(Items.DIAMOND)&&chest.getItem(0).getCount()==23,"Chest contents retained");check(p.containerMenu==p.inventoryMenu,"Wrench does not open ordinary chest");check(chest.getBlockState().getValue(ChestBlock.FACING)!=Blocks.CHEST.defaultBlockState().getValue(ChestBlock.FACING),"Chest rotates");});
// Survival uses exactly the same gestures; dirt keeps its native ID, shape and loot.
server.runOnServer(s->player(s).setGameMode(GameType.SURVIVAL));c.waitFor(m->!m.player.isCreative(),100);
// Ordinary survival without commands retains Steves full powers; dirt keeps its native ID, shape and loot.
server.runOnServer(s->{s.getWorldData().setAllowCommands(false);player(s).setGameMode(GameType.SURVIVAL);});c.waitFor(m->!m.player.isCreative(),100);
var original=c.computeOnClient(m->geometry(m,DIRT));c.takeScreenshot("wrench120-dirt-before");
click(c,DIRT,false,false);server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
var rotated=c.computeOnClient(m->geometry(m,DIRT));checkUvs(original,rotated);c.takeScreenshot("wrench120-dirt-90");
@@ -81,7 +81,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
var p=player(s);p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure cannot edit");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.SURVIVAL);check(!GoldenWrench.allowed(p,TARGET.offset(100,0,0),InteractionHand.MAIN_HAND,Direction.UP),"Out-of-range edit refused");
});
// A full Metabli still assembles with two normal right clicks, then separates with sneak-use.
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
c.waitFor(m->m.player.distanceToSqr(7.5,1,4)<.1&&m.level.getBlockState(table).is(Blocks.CRAFTING_TABLE),100);
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
server.waitFor(s->player(s).level().getBlockState(table).is(Metatable.BLOCK),100);c.waitFor(m->m.level.getBlockState(table).is(Metatable.BLOCK),100);
@@ -116,7 +116,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
}
System.out.println("WRENCH120_PASS native creative/survival clicks, reverse, properties, textures and resource reload, unchanged geometry, filled chest, multiblock assembly priority, permissions, cleanup and save/reconnect");
}
private static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
if(sneak){c.getInput().holdKey(o->o.keyShift);c.waitTicks(3);}
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()+.3,pos.getZ()+.5)));c.waitTicks(3);
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos),"Native cursor hits target "+pos+" instead of "+m.hitResult));
@@ -0,0 +1,111 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
import fr.koka.sanctuary.gametest.Starter039TestSupport;
import fr.koka.sanctuary.multiblock.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
import net.fabricmc.fabric.api.event.player.*;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.core.*;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.piston.PistonBaseBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.*;
import net.minecraft.world.phys.*;
/** Native golden-wrench clicks cannot create resources or split linked blocks. */
public final class Wrench123ClientChecks implements FabricClientGameTest {
private static final BlockPos TARGET=new BlockPos(0,1,0),CHEST=new BlockPos(2,1,0),DIRT=new BlockPos(-2,1,0);
public void runTest(ClientGameTestContext c){
var intro=new AtomicBoolean(true);
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
Starter039TestSupport.selectFirst(screen);
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench123 "+UUID.randomUUID());s.setSeed("123");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-5;x<=8;x++)for(int z=-4;z<=6;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);equip(p,Multiblocks.WRENCH);
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
});
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));
Wrench120ClientChecks.click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING),"Golden wrench changes native stair facing"));
Wrench120ClientChecks.click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
for(int i=0;i<5;i++){Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);}
server.runOnServer(s->{var state=player(s).level().getBlockState(TARGET);check(!state.getValue(StairBlock.WATERLOGGED)&&state.getValue(StairBlock.HALF)==Half.BOTTOM&&state.getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT,"Repeated native clicks never select water, half or fake stair shapes");});
for(int i=0;i<4;i++)Wrench120ClientChecks.click(c,CHEST,false,false);
server.runOnServer(s->{var p=player(s);var be=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(be.getItem(0).getCount()==23&&be.getBlockState()==Blocks.CHEST.defaultBlockState(),"Single chest remains single, dry and filled after four rotations");check(p.containerMenu==p.inventoryMenu,"Ordinary rotation does not open chest");});
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
server.runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==0&&l.getBlockState(DIRT).is(Blocks.DIRT),"Golden wrench never adds a texture transform");regressions(player(s));});
c.takeScreenshot("wrench123-golden-safe");
// Finding Steve's wrench grants its full powers in ordinary survival, without operator status.
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.SURVIVAL);s.getWorldData().setAllowCommands(false);equip(p,Multiblocks.STEVE_WRENCH);});
c.waitFor(m->!m.player.isCreative()&&m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH),100);
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
Wrench120ClientChecks.click(c,TARGET,false,false);Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Native Steve edit works for ordinary survival without commands");
for(var hand:InteractionHand.values()){
p.setItemInHand(hand,new ItemStack(Multiblocks.STEVE_WRENCH));check(GoldenWrench.allowed(p,TARGET,hand,Direction.UP),"Survival use allowed in both hands");
GoldenWrench.change(p,DIRT,hand,Direction.UP);
}
check(WrenchTextures.rotation(p.level().getChunkAt(DIRT),DIRT)==3,"Both hands can freely edit textures in survival");
p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);p.setGameMode(GameType.CREATIVE);check(GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Creative access without commands enabled");
p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure restrictions remain");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.CREATIVE);
var steve=new ItemStack(Multiblocks.STEVE_WRENCH);check(Boolean.TRUE.equals(steve.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE)),"Distinct enchanted Steve item");
});
c.waitFor(m->m.player.isCreative(),100);c.takeScreenshot("wrench123-steve-texture");
System.out.println("WRENCH123_SAFE_PASS native clicks, water/inventory preservation, linked blocks, real neighbour shapes, support checks, ordinary survival and creative use, adventure/spectator restrictions and both hands");
}
// Keep the full former debug-stick contract on Steve, including UVs/save/reload, and assembly on gold.
new Wrench120ClientChecks().runTest(c);
System.out.println("WRENCH123_PASS safe golden wrench and Steve wrench with native regression coverage");
}
private static void regressions(ServerPlayer p){
var l=p.level();
// Water stays present when rotating a legitimately waterlogged block.
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.WATERLOGGED,true),18);
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Existing water is preserved");
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING,Direction.WEST).setValue(StairBlock.SHAPE,StairsShape.INNER_LEFT),18);
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT&&!l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Native shape recomputed against actual empty neighbours");
var a=CHEST;var b=CHEST.east();var base=Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING,Direction.NORTH);
l.setBlock(a,base.setValue(ChestBlock.TYPE,ChestType.LEFT),18);l.setBlock(b,base.setValue(ChestBlock.TYPE,ChestType.RIGHT),18);
var first=(ChestBlockEntity)l.getBlockEntity(a);var second=(ChestBlockEntity)l.getBlockEntity(b);first.setItem(0,new ItemStack(Items.DIAMOND,23));second.setItem(0,new ItemStack(Items.EMERALD,17));
var left=l.getBlockState(a);var right=l.getBlockState(b);use(p,a);use(p,b);
check(l.getBlockState(a)==left&&l.getBlockState(b)==right&&l.getBlockEntity(a)==first&&l.getBlockEntity(b)==second&&first.getItem(0).getCount()==23&&second.getItem(0).getCount()==17,"Double chest remains linked with both original inventories");
var q=TARGET;var door=Blocks.OAK_DOOR.defaultBlockState();l.setBlock(q,door.setValue(DoorBlock.HALF,DoubleBlockHalf.LOWER),18);l.setBlock(q.above(),door.setValue(DoorBlock.HALF,DoubleBlockHalf.UPPER),18);
var lower=l.getBlockState(q);var upper=l.getBlockState(q.above());use(p,q);use(p,q.above());check(l.getBlockState(q)==lower&&l.getBlockState(q.above())==upper,"Both door halves protected");l.setBlock(q.above(),Blocks.AIR.defaultBlockState(),18);
var bed=Blocks.BED.red().defaultBlockState();l.setBlock(q,bed,18);use(p,q);check(l.getBlockState(q)==bed,"Bed half protected even if malformed already");
var extended=Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.EXTENDED,true);l.setBlock(q,extended,18);use(p,q);check(l.getBlockState(q)==extended,"Extended piston protected");
for(var block:List.of(Blocks.PISTON_HEAD,Blocks.MOVING_PISTON)){l.setBlock(q,block.defaultBlockState(),18);use(p,q);check(l.getBlockState(q)==block.defaultBlockState(),"Attached/moving piston part protected");}
l.setBlock(q,Blocks.OAK_LOG.defaultBlockState(),18);var axes=EnumSet.noneOf(Direction.Axis.class);for(int i=0;i<3;i++){use(p,q);axes.add(l.getBlockState(q).getValue(RotatedPillarBlock.AXIS));}check(axes.size()==3,"All three native log axes supported");
l.setBlock(q,Blocks.PISTON.defaultBlockState(),18);var facings=EnumSet.noneOf(Direction.class);for(int i=0;i<6;i++){use(p,q);facings.add(l.getBlockState(q).getValue(PistonBaseBlock.FACING));}check(facings.size()==6,"Isolated piston supports all six native orientations");
// Turning a wall torch toward empty air must not break it or its support.
l.setBlock(q.north(),Blocks.STONE.defaultBlockState(),18);var torch=Blocks.WALL_TORCH.defaultBlockState().setValue(WallTorchBlock.FACING,Direction.SOUTH);l.setBlock(q,torch,18);use(p,q);check(l.getBlockState(q)==torch,"Unsupported rotation leaves wall-mounted block intact");l.setBlock(q.north(),Blocks.AIR.defaultBlockState(),18);
l.setBlock(q,Blocks.OAK_STAIRS.defaultBlockState(),18);
var top=GoldenWrench.cycle(l.getBlockState(q),StairBlock.HALF,false);check(top.getValue(StairBlock.HALF)==Half.TOP,"Steve still has native debug property cycle");
// Existing visual edits are preserved; the ordinary key cannot create or reset them.
WrenchTextures.turn(l,q,1);use(p,q);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"Golden rotation preserves saved Steve texture data");WrenchTextures.turn(l,q,-1);
}
private static void use(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
private static void equip(ServerPlayer p,Item item){p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(item));p.inventoryMenu.broadcastFullState();}
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -0,0 +1,107 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.Blaze3D;
import fr.koka.sanctuary.SanctuaryMod;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Util;
/** Explicit, one-time export. The player's copy is never a managed mod resource. */
public final class ResourcePackTemplate {
public static final String FOLDER = "Sanctuary-Custom";
private ResourcePackTemplate() {}
public static Button button(Path packDirectory) {
Path target = packDirectory.resolve(FOLDER);
return Button.builder(label(Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)
? "open_copy" : "customize"), button -> {
if (Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
Blaze3D.openPath(target);
return;
}
button.active = false;
button.setMessage(label("creating"));
Util.ioPool().execute(() -> {
try {
create(packDirectory);
Minecraft.getInstance().execute(() -> {
button.active = true;
button.setMessage(label("open_copy"));
button.setTooltip(Tooltip.create(label("copy_ready")));
});
} catch (IOException | RuntimeException error) {
SanctuaryMod.LOGGER.error("Could not create personal Sanctuary resource pack", error);
Minecraft.getInstance().execute(() -> {
button.active = true;
button.setMessage(label("copy_failed"));
button.setTooltip(Tooltip.create(label("copy_failed_help")));
});
}
});
}).width(308).tooltip(Tooltip.create(label("customize_help"))).build();
}
/** Returns an existing personal directory untouched, including after mod upgrades. */
public static synchronized Path create(Path packDirectory) throws IOException {
Path directory = packDirectory.toAbsolutePath().normalize();
Files.createDirectories(directory);
Path target = directory.resolve(FOLDER);
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) return existing(target);
var mod = FabricLoader.getInstance().getModContainer(SanctuaryMod.ID).orElseThrow();
// Keep incomplete packs outside the directory watched by the native pack screen.
Path staging = Files.createTempDirectory(directory.getParent(), ".sanctuary-template-");
try {
copyTree(mod.findPath("assets").orElseThrow(), staging.resolve("assets"));
copyTree(mod.findPath("resourcepacks/textures/assets").orElseThrow(), staging.resolve("assets"));
copyTree(mod.findPath("resourcepacks/template").orElseThrow(), staging);
Files.copy(mod.findPath("resourcepacks/textures/pack.png").orElseThrow(), staging.resolve("pack.png"));
try {
Files.move(staging, target);
} catch (FileAlreadyExistsException race) {
return existing(target);
}
return target;
} finally {
if (Files.exists(staging)) {
try (var files = Files.walk(staging)) {
for (Path file : files.sorted(Comparator.reverseOrder()).toList()) Files.delete(file);
}
}
}
}
private static Path existing(Path target) throws IOException {
if (!Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Personal pack path already exists and is not a directory: " + target);
}
return target;
}
private static void copyTree(Path source, Path destination) throws IOException {
try (var files = Files.walk(source)) {
for (Path file : files.toList()) {
if (Files.isSymbolicLink(file)) throw new IOException("Unexpected asset symlink: " + file);
// String paths also work when the mod is loaded from a ZIP filesystem.
Path output = destination.resolve(source.relativize(file).toString()).normalize();
if (!output.startsWith(destination)) throw new IOException("Invalid asset path: " + file);
if (Files.isDirectory(file)) Files.createDirectories(output);
else Files.copy(file, output, StandardCopyOption.REPLACE_EXISTING);
}
}
}
private static Component label(String key) {
return Component.translatable("sanctuary.resource_pack." + key);
}
}
@@ -47,16 +47,9 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
}
private static List<Quad> mesh(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
if(sculpture==null)return List.of();var key=new Placement(sculpture,facing,anchor);var cached=MESHES.get(key);if(cached!=null)return cached;
// Rotate integer cells before anchoring: odd dimensions never introduce half-pixel offsets.
int turns=switch(facing){case WEST -> 1;case NORTH -> 2;case EAST -> 3;default -> 0;};
for(int i=0;i<turns;i++)sculpture=sculpture.rotate();
var placed=SculptureGeometry.place(sculpture,facing,anchor);sculpture=placed.sculpture();
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
int minX=16,minZ=16,maxX=0,maxZ=0;
for(var cell:sculpture.cells()){
minX=Math.min(minX,cell.x());minZ=Math.min(minZ,cell.z());
maxX=Math.max(maxX,cell.x()+1);maxZ=Math.max(maxZ,cell.z()+1);
}
float ox=(anchor.originX(maxX-minX)-minX)/16f,oz=(anchor.originZ(maxZ-minZ)-minZ)/16f;
float ox=placed.offsetX()/16f,oz=placed.offsetZ()/16f;
for(var cell:sculpture.cells())for(int side=0;side<6;side++){
var s=STEP[side];int x=cell.x()+s[0],y=cell.y()+s[1],z=cell.z()+s[2];
if(x>=0&&x<16&&y>=0&&y<16&&z>=0&&z<16&&occupied.get(x|y<<4|z<<8))continue;
@@ -0,0 +1,38 @@
package fr.koka.sanctuary.mixin.client;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import fr.koka.sanctuary.client.ResourcePackTemplate;
import java.nio.file.Path;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.layouts.HeaderAndFooterLayout;
import net.minecraft.client.gui.layouts.LayoutElement;
import net.minecraft.client.gui.layouts.LinearLayout;
import net.minecraft.client.gui.screens.packs.PackSelectionScreen;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
/** Extra footer row only in resource packs, never in the world datapack selector. */
@Mixin(PackSelectionScreen.class)
public abstract class PackSelectionScreenMixin {
@Shadow @Final private Path packDir;
@WrapOperation(method = "init", at = @At(value = "INVOKE", target =
"Lnet/minecraft/client/gui/layouts/HeaderAndFooterLayout;addToFooter(Lnet/minecraft/client/gui/layouts/LayoutElement;)Lnet/minecraft/client/gui/layouts/LayoutElement;"))
private LayoutElement sanctuary$personalPack(HeaderAndFooterLayout layout, LayoutElement nativeButtons,
Operation<LayoutElement> original) {
if (!packDir.toAbsolutePath().normalize().equals(
Minecraft.getInstance().getResourcePackDirectory().toAbsolutePath().normalize())) {
return original.call(layout, nativeButtons);
}
layout.setFooterHeight(56);
LinearLayout rows = LinearLayout.vertical().spacing(4);
rows.addChild(ResourcePackTemplate.button(packDir));
rows.addChild(nativeButtons);
original.call(layout, rows);
// Vanilla still adds its Open folder / Done buttons to the original horizontal row.
return nativeButtons;
}
}
@@ -15,7 +15,7 @@ import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
/** Server-owned debug-stick gestures without changing a block's type or its inventory. */
/** Ordinary rotations and Steve debug gestures, both owned by the server. */
public final class GoldenWrench {
public static final String TEXTURE="sanctuary_texture";
private static final Map<ServerPlayer,Map<Block,String>> SELECTED=new WeakHashMap<>();
@@ -27,14 +27,19 @@ public final class GoldenWrench {
public static void register(){
WrenchTextures.register();
AttackBlockCallback.EVENT.register((player,level,hand,pos,face)->{
if(!player.getItemInHand(hand).is(Multiblocks.WRENCH))return InteractionResult.PASS;
if(!isWrench(player.getItemInHand(hand)))return InteractionResult.PASS;
if(player.isSpectator())return InteractionResult.FAIL;
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;select(p,pos);}
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;
if(p.getItemInHand(hand).is(Multiblocks.STEVE_WRENCH))select(p,pos);
else p.sendSystemMessage(text("rotate_help"),true);}
return InteractionResult.SUCCESS;
});
ServerPlayConnectionEvents.DISCONNECT.register((handler,server)->{SELECTED.remove(handler.player);LAST_SELECTION.remove(handler.player);});
}
public static boolean isWrench(ItemStack stack){return stack.is(Multiblocks.WRENCH)||stack.is(Multiblocks.STEVE_WRENCH);}
public static boolean allowed(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
var tool=p.getItemInHand(hand);
if(!isWrench(tool))return false;
return p.isAlive()&&!p.isSpectator()&&p.getAbilities().mayBuild&&p.level().hasChunkAt(pos)
&&p.isWithinBlockInteractionRange(pos,0)&&p.level().mayInteract(p,pos)
&&p.mayUseItemAt(pos,face,p.getItemInHand(hand))
@@ -66,8 +71,12 @@ public final class GoldenWrench {
var property=choices.get(next);selected.put(state.getBlock(),property);
p.sendSystemMessage(text("selected",label(property),value(p,pos,state,property)),true);
}
public static void change(ServerPlayer p,BlockPos pos){
public static void change(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
if(!allowed(p,pos,hand,face))return;
var state=p.level().getBlockState(pos);if(state.isAir()||assembled(p,pos,state))return;
if(p.getItemInHand(hand).is(Multiblocks.WRENCH)){
p.sendSystemMessage(text(WrenchRotation.turn(p.level(),pos,p.isShiftKeyDown())),true);return;
}
var name=selected(p,state);SELECTED.computeIfAbsent(p,key->new HashMap<>()).put(state.getBlock(),name);
if(name.equals(TEXTURE))WrenchTextures.turn(p.level(),pos,p.isShiftKeyDown()?-1:1);
else {
@@ -23,7 +23,15 @@ import static fr.koka.sanctuary.multiblock.MultiblockData.Machine;
public final class Multiblocks {
public static final Item WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("golden_wrench"),
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
new GoldenWrench.Tool(new Item.Properties().stacksTo(1)
.component(net.minecraft.core.component.DataComponents.LORE,new net.minecraft.world.item.component.ItemLore(List.of(Component.translatable("sanctuary.wrench.rotate_help"))))
.setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
public static final Item STEVE_WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("steve_wrench"),
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC)
.component(net.minecraft.core.component.DataComponents.ENCHANTMENT_GLINT_OVERRIDE,true)
.component(net.minecraft.core.component.DataComponents.LORE,new net.minecraft.world.item.component.ItemLore(List.of(
Component.translatable("sanctuary.wrench.steve_power"),Component.translatable("sanctuary.wrench.steve_help"))))
.setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("steve_wrench")))));
public static final MenuType<MultiblockMenu> MENU=Registry.register(BuiltInRegistries.MENU,SanctuaryMod.id("multiblock"),
new ExtendedMenuType<>(MultiblockMenu::new,MultiblockMenu.View.CODEC));
private record Preview(ServerLevel level,BlockPos origin,boolean furnace,long until){}
@@ -34,9 +42,9 @@ public final class Multiblocks {
GoldenWrench.register();
SuperPistons.register();
Metatable.register();
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->out.accept(WRENCH));
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->{out.accept(WRENCH);out.accept(STEVE_WRENCH);});
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
boolean wrench=player.getItemInHand(hand).is(WRENCH);
boolean wrench=GoldenWrench.isWrench(player.getItemInHand(hand));
if(world.isClientSide())return wrench?InteractionResult.SUCCESS:InteractionResult.PASS;
if(!(player instanceof ServerPlayer p)||p.isSpectator())return InteractionResult.PASS;
var level=p.level();var pos=hit.getBlockPos();var machine=MultiblockData.get(level).at(pos);
@@ -45,7 +53,7 @@ public final class Multiblocks {
if(!wrench&&p.isShiftKeyDown()&&(!p.getMainHandItem().isEmpty()||!p.getOffhandItem().isEmpty()))return InteractionResult.PASS;
if(!level.mayInteract(p,pos)||!p.mayUseItemAt(pos,hit.getDirection(),p.getItemInHand(hand)))return InteractionResult.FAIL;
if(wrench){if(!GoldenWrench.allowed(p,pos,hand,hit.getDirection()))return InteractionResult.FAIL;
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos);
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos,hand,hit.getDirection());
return InteractionResult.SUCCESS;}
if(ready(level,machine)&&accessible(p,machine))open(p,machine,pos,0,"",0);
else p.sendSystemMessage(text("unavailable"),true);
@@ -0,0 +1,36 @@
package fr.koka.sanctuary.multiblock;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.*;
import static net.minecraft.world.level.block.state.properties.BlockStateProperties.*;
/** Only native orientation properties; fluid, contents and gameplay state are never selectable. */
public final class WrenchRotation {
private static final List<Property<?>> ORIENTATIONS=List.of(HORIZONTAL_FACING,FACING,FACING_HOPPER,AXIS,HORIZONTAL_AXIS,ROTATION_16,ORIENTATION);
public static String turn(ServerLevel level,BlockPos pos,boolean reverse){
var before=level.getBlockState(pos);
if(linked(before))return "linked";
var property=ORIENTATIONS.stream().filter(before::hasProperty).findFirst().orElse(null);
if(property==null)return "no_rotation";
var oriented=GoldenWrench.cycle(before,property,reverse);
if(!oriented.canSurvive(level,pos))return "unsupported";
// Stairs and connections must reflect their real neighbours, not a debug-stick shape.
var after=Block.updateFromNeighbourShapes(oriented,level,pos);
if(!after.is(before.getBlock())||linked(after)||after.getFluidState()!=before.getFluidState()
||!after.canSurvive(level,pos))return "unsupported";
level.setBlock(pos,after,Block.UPDATE_ALL);
return "rotated";
}
private static boolean linked(BlockState state){
// A single-block operation cannot rotate another half, an attached head or a joined inventory.
return state.hasProperty(DOUBLE_BLOCK_HALF)||state.hasProperty(BED_PART)||state.hasProperty(SIDE_CHAIN_PART)&&state.getValue(SIDE_CHAIN_PART).isConnected()
||state.hasProperty(PISTON_TYPE)||state.hasProperty(EXTENDED)&&state.getValue(EXTENDED)
||state.hasProperty(CHEST_TYPE)&&state.getValue(CHEST_TYPE)!=ChestType.SINGLE;
}
private WrenchRotation(){}
}
@@ -0,0 +1,37 @@
package fr.koka.sanctuary.plans;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.phys.shapes.VoxelShape;
/** Integer placement shared by rendering and tight, single-box selection/collision bounds. */
public final class SculptureGeometry {
public record Placement(VoxelSculpture sculpture, int offsetX, int offsetZ) {}
private SculptureGeometry() {}
public static Placement place(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
int turns = switch (facing) { case WEST -> 1; case NORTH -> 2; case EAST -> 3; default -> 0; };
for (int i = 0; i < turns; i++) sculpture = sculpture.rotate();
int minX = 16, minZ = 16, maxX = 0, maxZ = 0;
for (var cell : sculpture.cells()) {
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
maxX = Math.max(maxX, cell.x() + 1); maxZ = Math.max(maxZ, cell.z() + 1);
}
return new Placement(sculpture, anchor.originX(maxX - minX) - minX,
anchor.originZ(maxZ - minZ) - minZ);
}
/** Tight integer bounds, without rotating/copying cells or constructing a voxel grid. */
public static VoxelShape shape(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
int minX=16,minY=16,minZ=16,maxX=0,maxY=0,maxZ=0;
for(var cell:sculpture.cells()) {
minX=Math.min(minX,cell.x()); minY=Math.min(minY,cell.y()); minZ=Math.min(minZ,cell.z());
maxX=Math.max(maxX,cell.x()+1); maxY=Math.max(maxY,cell.y()+1); maxZ=Math.max(maxZ,cell.z()+1);
}
int width=maxX-minX,depth=maxZ-minZ;
if(facing.getAxis()==Direction.Axis.X){int swap=width;width=depth;depth=swap;}
int x=anchor.originX(width),z=anchor.originZ(depth);
return Block.box(x,minY,z,x+width,maxY,z+depth);
}
}
@@ -23,6 +23,7 @@ import net.minecraft.world.level.material.PushReaction;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.*;
import net.minecraft.world.phys.shapes.*;
import net.minecraft.network.chat.Component;
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
@@ -57,10 +58,31 @@ public final class Statuary {
});
}
private static Block block(String name,Block block){return Registry.register(BuiltInRegistries.BLOCK,SanctuaryMod.id(name),block);}
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
private static Item blockItem(String name,Block block){
var properties=new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix();
return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),block==BLOCK?new SculptureItem(block,properties):new BlockItem(block,properties));
}
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
private static final class SculptureItem extends BlockItem {
SculptureItem(Block block,Item.Properties properties){super(block,properties);}
@Override protected boolean canPlace(BlockPlaceContext context,BlockState state){
if(!super.canPlace(context,state))return false;
// The destination has no block entity yet: use the carried model for the native entity-overlap check.
var model=StatuaryEntity.sculpture(context.getItemInHand());
return model==null||context.getLevel().isUnobstructed(null,SculptureGeometry.shape(model,state.getValue(FACING),state.getValue(ANCHOR)).move(context.getClickedPos()));
}
}
private static final class SculptureBlock extends Block implements EntityBlock {
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
SculptureBlock(){super(Statuary.properties("statuary").dynamicShape().isSuffocating((s,l,p)->false).isViewBlocking((s,l,p,box)->false));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
@Override protected VoxelShape getShape(BlockState state,BlockGetter level,BlockPos pos,CollisionContext context){
return level.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.shape(state):Shapes.empty();
}
// Lighting metadata is cached per blockstate, so it cannot describe each block entity's voxels.
// Decorative sculptures transmit light; their faces still receive ordinary world lighting.
@Override protected VoxelShape getOcclusionShape(BlockState state){return Shapes.empty();}
@Override protected boolean propagatesSkylightDown(BlockState state){return true;}
@Override protected int getLightDampening(BlockState state){return 0;}
@Override protected float getShadeBrightness(BlockState state,BlockGetter level,BlockPos pos){return 1;}
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING,ANCHOR);}
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection()).setValue(ANCHOR,SculptureAnchor.placement(context));}
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING))).setValue(ANCHOR,state.getValue(ANCHOR).rotate(rotation));}
@@ -9,13 +9,26 @@ import net.minecraft.world.item.component.TypedEntityData;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.*;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.phys.shapes.VoxelShape;
/** New, additive block data only. No ticking or mutation of pre-existing world formats. */
public final class StatuaryEntity extends BlockEntity {
private VoxelSculpture sculpture;
private CompoundTag opaque;
private record ShapeCache(VoxelSculpture sculpture,BlockState state,VoxelShape shape){}
private volatile ShapeCache shapeCache;
// Keep unknown/future data recoverable by picking the small base, without a phantom full cube.
private static final VoxelShape MISSING_SHAPE=Block.box(6,0,6,10,4,10);
public StatuaryEntity(BlockPos pos,BlockState state){super(Statuary.TYPE,pos,state);}
public VoxelSculpture sculpture(){return sculpture;}
public VoxelShape shape(BlockState state){
var model=sculpture;if(model==null)return MISSING_SHAPE;
var cached=shapeCache;
if(cached!=null&&cached.sculpture()==model&&cached.state()==state)return cached.shape();
var shape=SculptureGeometry.shape(model,state.getValue(Statuary.FACING),state.getValue(Statuary.ANCHOR));
shapeCache=new ShapeCache(model,state,shape);return shape;
}
@Override protected void saveAdditional(ValueOutput out){
super.saveAdditional(out);
if(opaque!=null){out.store("sculpture",CompoundTag.CODEC,opaque);return;}
@@ -23,7 +36,7 @@ public final class StatuaryEntity extends BlockEntity {
out.store("sculpture",CompoundTag.CODEC,encode(sculpture));
}
@Override protected void loadAdditional(ValueInput in){
super.loadAdditional(in);sculpture=null;opaque=null;
super.loadAdditional(in);sculpture=null;opaque=null;shapeCache=null;
var data=in.read("sculpture",CompoundTag.CODEC).orElse(null);if(data==null)return;
try{
if(data.getIntOr("schema",-1)!=1)throw new IllegalArgumentException("schema");
@@ -0,0 +1,6 @@
{
"model": {
"type": "minecraft:model",
"model": "sanctuary:item/steve_wrench"
}
}
@@ -1753,6 +1753,14 @@
"sanctuary.preload.cities": "Preparing settlements",
"sanctuary.preload.cache_write": "Saving plans",
"sanctuary.familiar.dragon.lift": "On your head: hold %s to rise, release to glide.",
"sanctuary.resource_pack.customize": "Customize Sanctuary",
"sanctuary.resource_pack.open_copy": "Open my copy",
"sanctuary.resource_pack.creating": "Creating your copy…",
"sanctuary.resource_pack.customize_help": "Create an editable copy of Sanctuary resources. Enable it and move it to the top of the list. Updates preserve your changes.",
"sanctuary.resource_pack.copy_ready": "Copy ready! Enable Sanctuary-Custom and move it above other packs. Click to open its folder and guide.",
"sanctuary.resource_pack.copy_failed": "Copy failed — retry",
"sanctuary.resource_pack.copy_failed_help": "Check write permissions and the resourcepacks/Sanctuary-Custom path. See the game log for details.",
"sanctuary.resource_pack.custom_description": "Sanctuary — your editable copy",
"sanctuary.resource_pack.name": "Sanctuary textures",
"sanctuary.resource_pack.description": "Sanctuary textures — %s",
"sanctuary.familiar.personality": "Personality: %s",
@@ -2225,5 +2233,13 @@
"sanctuary.wrench.selected": "Selected property: %s = %s · right click to change",
"sanctuary.wrench.changed": "%s = %s",
"sanctuary.wrench.texture": "Texture rotation",
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components."
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components.",
"item.sanctuary.steve_wrench": "Steves Wrench",
"sanctuary.wrench.rotate_help": "Right click: orient · Sneak: reverse · Multiblocks: assemble/separate",
"sanctuary.wrench.steve_help": "Left click: select property · Right click: change · Sneak: reverse",
"sanctuary.wrench.rotated": "Orientation changed",
"sanctuary.wrench.no_rotation": "This block has no vanilla orientation.",
"sanctuary.wrench.linked": "This block has linked parts: separate or retract it before rotating.",
"sanctuary.wrench.unsupported": "This orientation does not fit the neighbouring blocks or support.",
"sanctuary.wrench.steve_power": "Free editing of block states and textures"
}
@@ -1753,6 +1753,14 @@
"sanctuary.preload.cities": "Préparation des implantations",
"sanctuary.preload.cache_write": "Enregistrement des plans",
"sanctuary.familiar.dragon.lift": "Sur la tête : maintenir %s pour monter, relâcher pour planer.",
"sanctuary.resource_pack.customize": "Personnaliser Sanctuary",
"sanctuary.resource_pack.open_copy": "Ouvrir ma copie",
"sanctuary.resource_pack.creating": "Création de la copie…",
"sanctuary.resource_pack.customize_help": "Crée une copie modifiable des ressources de Sanctuary. Activez-la et placez-la en haut de la liste. Vos modifications sont conservées lors des mises à jour.",
"sanctuary.resource_pack.copy_ready": "Copie prête ! Activez Sanctuary-Custom et placez-le au-dessus des autres packs. Cliquez pour ouvrir son dossier et son guide.",
"sanctuary.resource_pack.copy_failed": "Copie impossible — réessayer",
"sanctuary.resource_pack.copy_failed_help": "Vérifiez les droits d’écriture et le chemin resourcepacks/Sanctuary-Custom. Les détails sont dans le journal du jeu.",
"sanctuary.resource_pack.custom_description": "Sanctuary — votre copie modifiable",
"sanctuary.resource_pack.name": "Textures Sanctuary",
"sanctuary.resource_pack.description": "Textures Sanctuary — %s",
"sanctuary.familiar.personality": "Personnalité : %s",
@@ -2225,5 +2233,13 @@
"sanctuary.wrench.selected": "État choisi : %s = %s · clic droit pour modifier",
"sanctuary.wrench.changed": "%s = %s",
"sanctuary.wrench.texture": "Rotation des textures",
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants."
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants.",
"item.sanctuary.steve_wrench": "Clé à molette de Steve",
"sanctuary.wrench.rotate_help": "Clic droit : orienter · Maj : sens inverse · Multiblocs : assembler/dissocier",
"sanctuary.wrench.steve_help": "Clic gauche : choisir un état · Clic droit : modifier · Maj : sens inverse",
"sanctuary.wrench.rotated": "Orientation modifiée",
"sanctuary.wrench.no_rotation": "Ce bloc ne possède pas dorientation vanilla.",
"sanctuary.wrench.linked": "Bloc lié à une autre partie : dissocie-le ou rétracte-le avant de le tourner.",
"sanctuary.wrench.unsupported": "Cette orientation ne convient pas aux blocs voisins ou au support.",
"sanctuary.wrench.steve_power": "Édition libre des états et des textures"
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/handheld",
"textures": {"layer0": "sanctuary:item/steve_wrench"}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

@@ -184,6 +184,7 @@
"NaturalSnowEditMixin"
],
"client": [
"client.PackSelectionScreenMixin",
"client.FourneauFireMixin",
"client.BoatPassengerPickMixin",
"client.PreloadScreenMixin",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.120"
version = "beta.124"
pack-format = "packwiz:1.1.0"
[index]
+8 -1
View File
@@ -3,11 +3,18 @@
Sources des resource packs propres à Sanctuary. Le nom de ce dossier existant
est conservé ; le dossier installé dans Minecraft s'appelle `resourcepacks/`.
Le bouton **Personnaliser Sanctuary**, dans Options → Packs de ressources,
crée maintenant une copie personnelle complète `resourcepacks/Sanctuary-Custom/`.
Elle réunit les assets du mod et ceux du pack intégré ; les mises à jour ne
l'écrasent jamais. Le [guide du template](template/README.md) détaille les chemins
à modifier. `./gradlew assembleResourceTemplate` produit le ZIP complet.
Voir le [ticket beta.121](../docs/resourcepack-template-beta121.md).
Les exports ZIP à la racine restent locaux et ignorés par Git. Les dossiers
décompressés contiennent les sources versionnées.
[`sanctuary/`](sanctuary/README.md) est la base modifiable pour Minecraft
26.3 : pack beta.090 versionné, embarqué et activé par défaut dans le mod.
26.3 : pack beta.121 versionné, embarqué et activé par défaut dans le mod.
Il comprend les menus, 47 cœurs vanilla et les nouveaux dessins de faim et
de saturation fournis par le créateur. Les manifests `sanctuary/releases/`
fixent les empreintes de chaque révision.
+11 -3
View File
@@ -5,7 +5,15 @@ Le manifeste déclare le format de ressources **97.1**, relevé dans le client
exact utilisé par Sanctuary. Les menus, le panorama et les textures Sanctuary
déjà présents sont conservés.
## Version beta.090
## Version beta.121
La révision [beta.121](releases/beta.121.json) ajoute la clé dorée fournie par
le créateur, octet pour octet. Les autres textures et le format 97.1 restent
identiques. Le [template personnel](../template/README.md) exporte également
les ressources récentes du mod : blocs, items, modèles, sons et traductions.
Il se crée depuis l'écran des packs ou avec `assembleResourceTemplate`.
## Version beta.090 (historique)
La révision [beta.090](releases/beta.090.json) déclare Minecraft 26.3 finale,
sans changer le format 97.1 ni les images. Le manifeste beta.070 et son
@@ -26,7 +34,7 @@ bulles de respiration proviennent du jeu.
La source unique est ce dossier. Gradle le copie dans le mod sous
`resourcepacks/textures/` et produit le même contenu dans
`build/Sanctuary-Resource-Pack-beta.090.zip`. Lidentifiant stable du pack
`build/Sanctuary-Resource-Pack-beta.121.zip`. Lidentifiant stable du pack
est `sanctuary:textures`. Son numéro graphique est indépendant des correctifs
de code : `resource_pack_version` dans `gradle.properties` ne change quavec
une nouvelle révision du pack.
@@ -41,7 +49,7 @@ de `pack.mcmeta` et `resource_pack_version`. Ne pas réécrire un ancien manifes
Le contrôle refuse un fichier ajouté, oublié ou modifié sans mise à jour de
révision. Ne pas éditer les copies générées dans `build/`.
**English:** This source is the beta.090 built-in pack for Minecraft 26.3, enabled by default and
**English:** This source is the beta.121 built-in pack for Minecraft 26.3, enabled by default and
still user-disableable. Incoming PNGs are preserved exactly. `releases/` tracks
immutable file hashes; the mod and standalone ZIP consume this same source.
Keep a new release manifest whenever changing the graphics.
Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

+1 -1
View File
@@ -3,7 +3,7 @@
"description": {
"translate": "sanctuary.resource_pack.description",
"with": [
"beta.090"
"beta.121"
],
"fallback": "Sanctuary textures — %s"
},
@@ -0,0 +1,287 @@
{
"version": "beta.121",
"minecraft": "26.3",
"format": [
97,
1
],
"builtinId": "sanctuary:textures",
"activation": "DEFAULT_ENABLED",
"files": {
"assets/minecraft/textures/gui/sprites/hud/food_empty.png": "80ea14a291330f3253fa4ba3c7aa5eeea73ac3df71c85eec649ae75762574cd2",
"assets/minecraft/textures/gui/sprites/hud/food_empty_hunger.png": "f9f725d5f76f09c9284ec8ef3eab3bab714b04306ea5b88d2774c6d96c65e525",
"assets/minecraft/textures/gui/sprites/hud/food_full.png": "29f5cddd23a6383973690101b3d1d7ab10e421b8bff8c5359e8d2623576e2f14",
"assets/minecraft/textures/gui/sprites/hud/food_full_hunger.png": "1c1969632df22e5c3be196ace904174e7797222b4d6514f442fcce0cc9fb2361",
"assets/minecraft/textures/gui/sprites/hud/food_half.png": "e5eda03de917caca665dc5ac5210fdf90af745cad0ee17ddccddc4365a7ee55a",
"assets/minecraft/textures/gui/sprites/hud/food_half_hunger.png": "51be68df64c4d01c2dff3c300f9e1833bccbc19007d7a18b567a42160740fa8e",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_full.png": "4afb2035139c659ff7ed2fbf16bb4c4cc12158c664e422cbb964d4c926f40844",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_full_blinking.png": "4afb2035139c659ff7ed2fbf16bb4c4cc12158c664e422cbb964d4c926f40844",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_half.png": "ab37c9a7b3ded346b5ab125a6a87045ff6b599a8e1f1796290aae344d8f6189b",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_half_blinking.png": "ab37c9a7b3ded346b5ab125a6a87045ff6b599a8e1f1796290aae344d8f6189b",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_hardcore_full.png": "aa4a51a65cf6fd6b76dbc040f68b7a90ee70b8b23298d2628a8e0a2ea54d924d",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_hardcore_full_blinking.png": "aa4a51a65cf6fd6b76dbc040f68b7a90ee70b8b23298d2628a8e0a2ea54d924d",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_hardcore_half.png": "f88b24663f7c14e58490e1a8a60f44339f05c284fb89dbbf6da6bd9890291e06",
"assets/minecraft/textures/gui/sprites/hud/heart/absorbing_hardcore_half_blinking.png": "f88b24663f7c14e58490e1a8a60f44339f05c284fb89dbbf6da6bd9890291e06",
"assets/minecraft/textures/gui/sprites/hud/heart/container.png": "b92355b32f566dd5ec209dcf6f25ddd239fdf967a70af42308ef9607aab3b194",
"assets/minecraft/textures/gui/sprites/hud/heart/container_blinking.png": "1b1094514a73d8673110595050003fff6a7da1d33e6457ceeae004634c2c0963",
"assets/minecraft/textures/gui/sprites/hud/heart/container_hardcore.png": "b92355b32f566dd5ec209dcf6f25ddd239fdf967a70af42308ef9607aab3b194",
"assets/minecraft/textures/gui/sprites/hud/heart/container_hardcore_blinking.png": "1b1094514a73d8673110595050003fff6a7da1d33e6457ceeae004634c2c0963",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_full.png": "66a22e5a9c1be724078bf6392970917ce74548a75242cb9a8694a8cc788893ed",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_full_blinking.png": "66a22e5a9c1be724078bf6392970917ce74548a75242cb9a8694a8cc788893ed",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_half.png": "dda4ed21c8f12f4c5f28fea52d19fda8e09451adbb568bdc5aa4b4243cad9795",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_half_blinking.png": "dda4ed21c8f12f4c5f28fea52d19fda8e09451adbb568bdc5aa4b4243cad9795",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_hardcore_full.png": "8b6d31ee17b3eacbbfe69a6c8cf165d5efee9af12aae763a073818bfcc47d2fd",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_hardcore_full_blinking.png": "8b6d31ee17b3eacbbfe69a6c8cf165d5efee9af12aae763a073818bfcc47d2fd",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_hardcore_half.png": "55448c040468dab90f48e03392e90b7f4f635109349c1524619babeb875177ac",
"assets/minecraft/textures/gui/sprites/hud/heart/frozen_hardcore_half_blinking.png": "55448c040468dab90f48e03392e90b7f4f635109349c1524619babeb875177ac",
"assets/minecraft/textures/gui/sprites/hud/heart/full.png": "acb11da7611dc8239d4da2fe500cca2fe88a2c4facccf42fa7207c0e9dba4a16",
"assets/minecraft/textures/gui/sprites/hud/heart/full_blinking.png": "d43889d4c676a6e2498b0510bf4f77c0050b792884952c67475f4bff097a498b",
"assets/minecraft/textures/gui/sprites/hud/heart/half.png": "98d1fca665a309879c19c06b0658ba5407520f13a02d4bf7321b060534491efc",
"assets/minecraft/textures/gui/sprites/hud/heart/half_blinking.png": "f63cf9331c3087e5e403b96e2456721cbbbc4be05b86c3443b863efda40b6c88",
"assets/minecraft/textures/gui/sprites/hud/heart/hardcore_full.png": "0a7788d62fb70523880ca26120add4fb397a35ea70e7deca036c8fc2afcbada7",
"assets/minecraft/textures/gui/sprites/hud/heart/hardcore_full_blinking.png": "d66cf76e587d7bbd435d05d4c8a4f3835a90dbfdd63a5e825066e0cf2de53d52",
"assets/minecraft/textures/gui/sprites/hud/heart/hardcore_half.png": "92dd5fe2e4b355b627cf1340a569923d3cd5d6c3c7963e60e6abee207a1abed5",
"assets/minecraft/textures/gui/sprites/hud/heart/hardcore_half_blinking.png": "380da417eeefbaccf0f9f9bf03efd6d2ae713567ddcd47dfbf2c7298d0c2203b",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_full.png": "5c47760c8770cbed12392e65fdd2a3acd33836a6a8275f6abd4ce9f56ac01e9f",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_full_blinking.png": "bb427c5811d0eaa255f124076e4609333f8301cd5c7094fb344c5480cbb4a995",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_half.png": "7e0d0118a3e3a3d4a11dc1b65a4c66af0abdded95d227e3e2fbd97364b4c7ca6",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_half_blinking.png": "0b90d19c270047be3b1d502c3f2def99f5899ef430ed6bfff45cc35911e75076",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_hardcore_full.png": "8900928082f3ca73c312b8bc78357f283d4101f7dc147f563a09a3ad4c3b8937",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_hardcore_full_blinking.png": "354b9975971caf7f3ac28162b4ced0adc9edf9c11e82797bc13e4d9c5bc566bb",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_hardcore_half.png": "a3462b71265ef37c0bacff741d106c23f341696ab922049933aaa7751e24c33d",
"assets/minecraft/textures/gui/sprites/hud/heart/poisoned_hardcore_half_blinking.png": "b705fe0b9811ded037b08d4a639710e8829a03d373b5e86087323c811f9654e3",
"assets/minecraft/textures/gui/sprites/hud/heart/vehicle_container.png": "b92355b32f566dd5ec209dcf6f25ddd239fdf967a70af42308ef9607aab3b194",
"assets/minecraft/textures/gui/sprites/hud/heart/vehicle_full.png": "7d676dd277663d3d87bdbe52632235946e484301fb47b6f8b27633d2d4efe1e3",
"assets/minecraft/textures/gui/sprites/hud/heart/vehicle_half.png": "e2171726715fba71f86b6fee9714ec8e3e3c08e75beda62bf411b1d363363ae9",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_full.png": "5c5475bdb3a917b6414c1a1ea6400c1f00bdc1e5906d6781d6abbb9411747fa6",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_full_blinking.png": "8d8f5c897fdd6e3f6e9ca1b7c4e55abbf3af9437ea24d094b80ce9e134a1b27d",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_half.png": "a368bc604caa0aab258d1f9d4313db6031c5462361c41578dd718fae3f677a76",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_half_blinking.png": "758ee154d7879a64c6903e0a0ecb9dd068ca16aef225a9ca8055f68d79ff8e17",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_hardcore_full.png": "a6b4b53b39e85e5758b3ed99e4303f4f7f24d530e0f27510daa5b6c456ab725d",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_hardcore_full_blinking.png": "0fd9f717cbad4111e10c0f848af41e7e66c92d3225fd2ef0b0df48fdf397306c",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_hardcore_half.png": "3e069dcbca3a61492c6480a60c985f21ce39bf851ad870f69f8612689e2efd6b",
"assets/minecraft/textures/gui/sprites/hud/heart/withered_hardcore_half_blinking.png": "c9bf1f18f28aad32777dacd1b38ff93263592648bd0a7b9cc2e6633597c625a7",
"assets/minecraft/textures/gui/sprites/server_list/join.png": "cdf278b928ac6ecf32b4c28304dcace1ef9d1df017d6bf22610896a41b71f39e",
"assets/minecraft/textures/gui/sprites/server_list/join_highlighted.png": "e92353fbd181db587f3c5c4e688638709e69487e9c9eae076918489ef8d2f99e",
"assets/minecraft/textures/gui/sprites/server_list/move_down.png": "5443d2f341d7f6ecf00d24759d21b9f11d3c00b6b3464cf3209aad1893fbec35",
"assets/minecraft/textures/gui/sprites/server_list/move_down_highlighted.png": "8bc3722212402866e0b83e30893e5681e0173843140d51d54d0381d81fda0923",
"assets/minecraft/textures/gui/sprites/server_list/move_up.png": "12a353f27c85943b9e6f78b0534ad2f0315f8fc76af8fbdf25f6ddad674566e1",
"assets/minecraft/textures/gui/sprites/server_list/move_up_highlighted.png": "6e0632d7b5cde7be54035ef65a2976f38a960b834ee36e0c7c5450d6f9ffaa4b",
"assets/minecraft/textures/gui/sprites/transferable_list/move_down.png": "829a14cbe49c70284572ba3f6a7fc812b401ed4bac910d46a4b37fa048dc4a0e",
"assets/minecraft/textures/gui/sprites/transferable_list/move_down_highlighted.png": "f0621fda65ad07e989776b554458e941adff9ca7b9f48a64f18840a04e104567",
"assets/minecraft/textures/gui/sprites/transferable_list/move_up.png": "63ffe9417b1ca97d27e6da8bc1d0817b8b282f1489c47a64cba90e9cd6601e81",
"assets/minecraft/textures/gui/sprites/transferable_list/move_up_highlighted.png": "6b7f7ea182728d8ddacd9930f97aa4b6ab69897008eaf655cb4ab00f1b094498",
"assets/minecraft/textures/gui/sprites/transferable_list/select.png": "77979b167e05e92423c35cc3ee72458710e5573833cbdc943ba11d8d50b1b294",
"assets/minecraft/textures/gui/sprites/transferable_list/select_highlighted.png": "2e7fefa4299a08ea3b21d08c23e0fdaa669b25ae24cdce519206abf59e0d68b3",
"assets/minecraft/textures/gui/sprites/transferable_list/unselect.png": "a0b5817ea41b15f72f725101d25581cf1ed5f78069d791d8f679f451e992e840",
"assets/minecraft/textures/gui/sprites/transferable_list/unselect_highlighted.png": "f49ff96621c3e997ef3582a23f8bd727d65d2483e7854545ee87c1ed4d3c3232",
"assets/minecraft/textures/gui/sprites/widget/button.png": "79fbfe504e72598ba5f61a8c8e5fe6dbc3ec6c8a530fd6089efee17f89fb7b10",
"assets/minecraft/textures/gui/sprites/widget/button.png.mcmeta": "ebdf3a9e4f6dfd37d72ad6af4213d9f61fe8f8342e8f12f00bb6888f3ae28acb",
"assets/minecraft/textures/gui/sprites/widget/button_disabled.png": "55f92a6efea542d73ea19ed26147b4ce3aabe405b56e37db0d3cc75dc18a1f47",
"assets/minecraft/textures/gui/sprites/widget/button_disabled.png.mcmeta": "979a3e114ffb0213932b71873db149c32a8c1c8ba65a4cbe5d30a1f677564fa2",
"assets/minecraft/textures/gui/sprites/widget/button_highlighted.png": "b658d1c9b38057aee2c63f49e19a734763eba720bd88be703ef9835c7f5f7f84",
"assets/minecraft/textures/gui/sprites/widget/button_highlighted.png.mcmeta": "ebdf3a9e4f6dfd37d72ad6af4213d9f61fe8f8342e8f12f00bb6888f3ae28acb",
"assets/minecraft/textures/gui/sprites/widget/checkbox.png": "af426f3d1e5b32f74efc0aa522db6472a18ee4f2d87608acea04d3960168b5d9",
"assets/minecraft/textures/gui/sprites/widget/checkbox_highlighted.png": "57ec86b031c3a480140a60799bde4ca1ba7ab9c14d8a7a37eb48ff64f01e29c6",
"assets/minecraft/textures/gui/sprites/widget/checkbox_selected.png": "6d70776447e4ad66d6606b9f7d670d4aa18238429f9c68db51c7f10be01c12ec",
"assets/minecraft/textures/gui/sprites/widget/checkbox_selected_highlighted.png": "96875351b998a1e7f730c6231ff4638c7df7a80986d7be94511878ec89c8fbfe",
"assets/minecraft/textures/gui/sprites/widget/cross_button.png": "922c64fb3c504e39cbcdf874a0a91ce20b84041724dd8a5ddf318af7d24ca539",
"assets/minecraft/textures/gui/sprites/widget/cross_button_highlighted.png": "016e049e726b24fad10fc9d40c85873af92ef02eec5b4ec9793665b80347e02d",
"assets/minecraft/textures/gui/sprites/widget/locked_button.png": "9bf2c84eed89da70f7757aee27973834ab421d782ee60ea017e833eeceb46f93",
"assets/minecraft/textures/gui/sprites/widget/locked_button_disabled.png": "e77079180fc95b0e75adc280ee0c916f2d315276103a8b88b7440b81e3ea3772",
"assets/minecraft/textures/gui/sprites/widget/locked_button_highlighted.png": "9163318a63b1f5c31b115d644020fbeae5fc3e6fd231f877f4c35a82c4a3c414",
"assets/minecraft/textures/gui/sprites/widget/page_backward.png": "af6220ed0297e43365a402b2e524ca4ce92f3e8f1b7e746e7f23a4ddf7a7b738",
"assets/minecraft/textures/gui/sprites/widget/page_backward_highlighted.png": "ff927d1fd93dbbb226c1693cebd6510ca6a0a1cea526dbf67a7b59c60313af0c",
"assets/minecraft/textures/gui/sprites/widget/page_forward.png": "8a2c12ad9dd5877a764e968aa3015f5a69e31bc55585edd788eac9c15bbaf4b9",
"assets/minecraft/textures/gui/sprites/widget/page_forward_highlighted.png": "eb983beae120839fbb3c8c96c862beb24f97ccd172d1f31b7e44f3a9fa483fe2",
"assets/minecraft/textures/gui/sprites/widget/scroller.png": "066af6679ba68b0e67335cb6ce48ca3e1a058c9b074411573d7ec0418bdea600",
"assets/minecraft/textures/gui/sprites/widget/scroller.png.mcmeta": "f6a77d3537ff679b691e3d4b7bab0a1e076f71d1dec602bd65fa16a505092c53",
"assets/minecraft/textures/gui/sprites/widget/scroller_background.png": "be03c9e0fcabe74d352539157353497497845669491fb5b2ac07ad27116df7f9",
"assets/minecraft/textures/gui/sprites/widget/scroller_background.png.mcmeta": "f6a77d3537ff679b691e3d4b7bab0a1e076f71d1dec602bd65fa16a505092c53",
"assets/minecraft/textures/gui/sprites/widget/slider.png": "55f92a6efea542d73ea19ed26147b4ce3aabe405b56e37db0d3cc75dc18a1f47",
"assets/minecraft/textures/gui/sprites/widget/slider.png.mcmeta": "979a3e114ffb0213932b71873db149c32a8c1c8ba65a4cbe5d30a1f677564fa2",
"assets/minecraft/textures/gui/sprites/widget/slider_handle.png": "3b22d9afcea59fd265a75ef8ba547b8f56ae8d53ca7675ccb9bc963abfa3cccb",
"assets/minecraft/textures/gui/sprites/widget/slider_handle.png.mcmeta": "9ecf324f340ac117631939372d93ec59043f22d6caec7b7ea7c140b4d8d77c22",
"assets/minecraft/textures/gui/sprites/widget/slider_handle_highlighted.png": "a89e733c03068dad39c642738508b1e41f7c263e56b32939ff26f72db2fedcd6",
"assets/minecraft/textures/gui/sprites/widget/slider_handle_highlighted.png.mcmeta": "9ecf324f340ac117631939372d93ec59043f22d6caec7b7ea7c140b4d8d77c22",
"assets/minecraft/textures/gui/sprites/widget/slider_highlighted.png": "ba0d5d1e9b3bbb1af5311457a45372d2c89b5dac32a74de40815db945b45d2a5",
"assets/minecraft/textures/gui/sprites/widget/slider_highlighted.png.mcmeta": "979a3e114ffb0213932b71873db149c32a8c1c8ba65a4cbe5d30a1f677564fa2",
"assets/minecraft/textures/gui/sprites/widget/slot_frame.png": "8428676f5a55c8fb1f68d693b8b4e8809f03dfa00b062fd1005a307cff9c0a75",
"assets/minecraft/textures/gui/sprites/widget/unlocked_button.png": "c4b7b0d01cc458e8d623af1b7cab971f98f5fc5f239cb0d1b0e90b7428406688",
"assets/minecraft/textures/gui/sprites/widget/unlocked_button_disabled.png": "0e4e936c191d125ad2abc4d5e253fb9bab7a36d290abd998d752224a70cbff13",
"assets/minecraft/textures/gui/sprites/widget/unlocked_button_highlighted.png": "36c81bc881f23d3d377128022c47e43403fe9848690c430ae8427198be0d6e1d",
"assets/minecraft/textures/gui/sprites/world_list/error.png": "e4e219517e9b8db2b018efc532f08de50b65019eb094aea0a8814f7d5358433d",
"assets/minecraft/textures/gui/sprites/world_list/error_highlighted.png": "37ac98fc99850c78f65ad3dc1b759777db030ca859c8047ffeb453491107b65e",
"assets/minecraft/textures/gui/sprites/world_list/join.png": "cb437430e8122c0f791d9834491e7cee11b3c514b25e809ae7f9578879be1e84",
"assets/minecraft/textures/gui/sprites/world_list/join_highlighted.png": "e92353fbd181db587f3c5c4e688638709e69487e9c9eae076918489ef8d2f99e",
"assets/minecraft/textures/gui/sprites/world_list/marked_join.png": "8732a1c1419bd4046ca8eec12d6c9fdc6ddbd764cce373e12e17a334aed039ed",
"assets/minecraft/textures/gui/sprites/world_list/marked_join_highlighted.png": "23c4e5b123be8b555ba8f130a03acde63e2a6fb48f1c1180a031fb077687b631",
"assets/minecraft/textures/gui/sprites/world_list/warning.png": "e4e219517e9b8db2b018efc532f08de50b65019eb094aea0a8814f7d5358433d",
"assets/minecraft/textures/gui/sprites/world_list/warning_highlighted.png": "49fee0502fab6ea38031664916ba328a1e4d68d8507761ba260efb67e640adde",
"assets/minecraft/textures/gui/title/background/panorama_0.png": "af5f25cd7db1b878806cacfc6eabb6a896e37f41340787db0de79d222ec1dbea",
"assets/minecraft/textures/gui/title/background/panorama_0.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_1.png": "3323296b81ce46d3dff93abc11289657cacaf9183b0f0663a8fa56d38ae436c8",
"assets/minecraft/textures/gui/title/background/panorama_1.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_2.png": "751f2aacfcb382dff3c57be00ece8e3a3cdfe0479374451ecbf70aa95d915b4d",
"assets/minecraft/textures/gui/title/background/panorama_2.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_3.png": "bffc916a16c3709d01e10870c329e8b915807177166f271e91ed72aa591d4f70",
"assets/minecraft/textures/gui/title/background/panorama_3.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_4.png": "d750f000ff1df1680e30c3c2c4e6921bdd5b69911e1d4e8aec527ef58841edce",
"assets/minecraft/textures/gui/title/background/panorama_4.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_5.png": "26f967071cde84299ba747dba7aa2db73784e3d89b158fb9092d0cddd45d215c",
"assets/minecraft/textures/gui/title/background/panorama_5.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/edition.png": "44d032976ac08bb01847de99d62cfea98298825fd75a36dbec319eba76081cbb",
"assets/minecraft/textures/gui/title/minecraft.png": "efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157",
"assets/sanctuary/items/zero_cape.json": "dcdcb599fe8f645ea27429da1c45ac5925b4672578f01cb6b93b2cd03f1f8b7f",
"assets/sanctuary/models/item/zero_cape.json": "2e1c1b1030fd054d60cbc2544041bd80a44ac3f2ea89d098958666b97251b56a",
"assets/sanctuary/textures/entity/cape/zero_cape.png": "f9a76537647989f9a0b6d001e320dac591c359e9e61a31f4ce11c88f207f0ad4",
"assets/sanctuary/textures/gui/aptitudes/familiar.png": "c855f9cddb68cdb4a7110e70af60afbe068bc0176b9b6c69f38e052fcdbfda6c",
"assets/sanctuary/textures/gui/aptitudes/familiarpower.png": "9ee75fd66f68bb2ce7b4158d2511ef3a9cd7a20f2b105520c4a4fcec2fee378b",
"assets/sanctuary/textures/gui/aptitudes/hungerknown.png": "1e5cd57d12592f03dc2d2071a8c2873a30939f07b0db021fcb7fc34468709acc",
"assets/sanctuary/textures/gui/aptitudes/names.png": "77dcea1c72970f87043b551c3506bfec64290b478a4d2981f280079d98158628",
"assets/sanctuary/textures/gui/aptitudes/recipes.png": "88ec437776844c15104e0856af8fd768f511b951c79802a151d6ce4ffbbb4357",
"assets/sanctuary/textures/gui/aptitudes/rest.png": "f343fd835ed20b9feb5195cd3727c0a3882090b33caf08c3f43120b250c8786e",
"assets/sanctuary/textures/gui/aptitudes/sorting.png": "fa6689f4ffdac7ffa58900afc8b264e8106cde7a992167ada7e8debda4d09618",
"assets/sanctuary/textures/gui/aptitudes/swim.png": "1e98150e08f2f47208ac0b14517760bddb55be4c013bfc3693dc474d951a9f9a",
"assets/sanctuary/textures/gui/aptitudes/worldmap.png": "a413da0a34110d4b7d4422ca2f59defc1c6394184186068974a5ff6c68a4e4b0",
"assets/sanctuary/textures/gui/aptitudes/zoom.png": "07e1c2dc51ff86e5f6903a5fc058603a886876600b01d124770336deefffa19f",
"assets/sanctuary/textures/gui/food_details.png": "1ebc30ae7394393b5b7b8a0383102bcd035e5e92a602b89db05eeab66d2ef134",
"assets/sanctuary/textures/gui/inventory/cape.png": "cd32c5f61d088740a11da8dd2767fda11f8db6b1fe65587a7c5b043177befb17",
"assets/sanctuary/textures/gui/inventory/cosmetics.png": "fcb2bca5f01a6fa6028cf6748d06320acb02011d7a7a93103d5ee24be13dca4b",
"assets/sanctuary/textures/gui/inventory/egg.png": "344a8624bd03b45472acb045fa607172d00dae603594bfa5e411d5f126fc0975",
"assets/sanctuary/textures/gui/progression/breathing.png": "2b1bfe8e0a33ae957f87dc4086b848ac26e209a4b90998fce153b2f394201157",
"assets/sanctuary/textures/gui/progression/building.png": "2d143ca35c5eac3d2a07943fe440b6893e189ff2389fd1b300d4f08bd98309ec",
"assets/sanctuary/textures/gui/progression/heart.png": "224b4162d500a632fe6861613026941a7dcd0eee97987e0c92483733f2fa0a1e",
"assets/sanctuary/textures/gui/progression/hunger.png": "9b3cb1e8206e7d17306fd461660b8ba154d729325f17181f6a045c175674ea97",
"assets/sanctuary/textures/gui/progression/inventory.png": "d66b68dda35538639f956d670b3ff25e064b00ab022a6292ce8c0d3e3ab473a3",
"assets/sanctuary/textures/gui/progression/mining.png": "c0134e182d63031ba2b53ab4477f8e1c96352375998c1c0bc54fc452816d2599",
"assets/sanctuary/textures/item/cape/zero_cape.png": "d3386907d5cc90cdf1911c911faa26dbcb6ea16ce0dc7321abb482f3322255fb",
"assets/sanctuary/textures/item/golden_wrench.png": "a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af",
"pack.mcmeta": "70189fadacaa715704105dd1adea84bd4b4bd2ec650cf048644ab379707afeaf",
"pack.png": "33adb8f689d55b7bf8d0824b02942ea3823f128b27fab458713ab9582061a1a0"
},
"import": {
"folder": "Sanctuary-Resource-Pack-base-26-3",
"files": {
"FOOD.md": "e271c13d8044ee54b7bd9f0e34627480dab14df3f91a17e2a6fcb87e8cba0424",
"HUD-SOURCES.json": "40b74fe75a581abdf7c621fbccd44a2acf1a8d2526266bb0b36ff703c404dd3c",
"HUD.md": "300eb32ece21171818d97078cc16e9c341fbf19d84d70d33edc898c8479d7b7b",
"README.md": "8ce801201583de8ac330e64973d61a4eaed4ecb89d58e31318c5d76883f1cc98",
"assets/minecraft/textures/gui/sprites/hud/food_empty.png": "80ea14a291330f3253fa4ba3c7aa5eeea73ac3df71c85eec649ae75762574cd2",
"assets/minecraft/textures/gui/sprites/hud/food_empty_hunger.png": "f9f725d5f76f09c9284ec8ef3eab3bab714b04306ea5b88d2774c6d96c65e525",
"assets/minecraft/textures/gui/sprites/hud/food_full.png": "29f5cddd23a6383973690101b3d1d7ab10e421b8bff8c5359e8d2623576e2f14",
"assets/minecraft/textures/gui/sprites/hud/food_full_hunger.png": "1c1969632df22e5c3be196ace904174e7797222b4d6514f442fcce0cc9fb2361",
"assets/minecraft/textures/gui/sprites/hud/food_half.png": "e5eda03de917caca665dc5ac5210fdf90af745cad0ee17ddccddc4365a7ee55a",
"assets/minecraft/textures/gui/sprites/hud/food_half_hunger.png": "51be68df64c4d01c2dff3c300f9e1833bccbc19007d7a18b567a42160740fa8e",
"assets/minecraft/textures/gui/sprites/server_list/join.png": "cdf278b928ac6ecf32b4c28304dcace1ef9d1df017d6bf22610896a41b71f39e",
"assets/minecraft/textures/gui/sprites/server_list/join_highlighted.png": "e92353fbd181db587f3c5c4e688638709e69487e9c9eae076918489ef8d2f99e",
"assets/minecraft/textures/gui/sprites/server_list/move_down.png": "5443d2f341d7f6ecf00d24759d21b9f11d3c00b6b3464cf3209aad1893fbec35",
"assets/minecraft/textures/gui/sprites/server_list/move_down_highlighted.png": "8bc3722212402866e0b83e30893e5681e0173843140d51d54d0381d81fda0923",
"assets/minecraft/textures/gui/sprites/server_list/move_up.png": "12a353f27c85943b9e6f78b0534ad2f0315f8fc76af8fbdf25f6ddad674566e1",
"assets/minecraft/textures/gui/sprites/server_list/move_up_highlighted.png": "6e0632d7b5cde7be54035ef65a2976f38a960b834ee36e0c7c5450d6f9ffaa4b",
"assets/minecraft/textures/gui/sprites/transferable_list/move_down.png": "829a14cbe49c70284572ba3f6a7fc812b401ed4bac910d46a4b37fa048dc4a0e",
"assets/minecraft/textures/gui/sprites/transferable_list/move_down_highlighted.png": "f0621fda65ad07e989776b554458e941adff9ca7b9f48a64f18840a04e104567",
"assets/minecraft/textures/gui/sprites/transferable_list/move_up.png": "63ffe9417b1ca97d27e6da8bc1d0817b8b282f1489c47a64cba90e9cd6601e81",
"assets/minecraft/textures/gui/sprites/transferable_list/move_up_highlighted.png": "6b7f7ea182728d8ddacd9930f97aa4b6ab69897008eaf655cb4ab00f1b094498",
"assets/minecraft/textures/gui/sprites/transferable_list/select.png": "77979b167e05e92423c35cc3ee72458710e5573833cbdc943ba11d8d50b1b294",
"assets/minecraft/textures/gui/sprites/transferable_list/select_highlighted.png": "2e7fefa4299a08ea3b21d08c23e0fdaa669b25ae24cdce519206abf59e0d68b3",
"assets/minecraft/textures/gui/sprites/transferable_list/unselect.png": "a0b5817ea41b15f72f725101d25581cf1ed5f78069d791d8f679f451e992e840",
"assets/minecraft/textures/gui/sprites/transferable_list/unselect_highlighted.png": "f49ff96621c3e997ef3582a23f8bd727d65d2483e7854545ee87c1ed4d3c3232",
"assets/minecraft/textures/gui/sprites/widget/button.png": "79fbfe504e72598ba5f61a8c8e5fe6dbc3ec6c8a530fd6089efee17f89fb7b10",
"assets/minecraft/textures/gui/sprites/widget/button.png.mcmeta": "ebdf3a9e4f6dfd37d72ad6af4213d9f61fe8f8342e8f12f00bb6888f3ae28acb",
"assets/minecraft/textures/gui/sprites/widget/button_disabled.png": "55f92a6efea542d73ea19ed26147b4ce3aabe405b56e37db0d3cc75dc18a1f47",
"assets/minecraft/textures/gui/sprites/widget/button_disabled.png.mcmeta": "979a3e114ffb0213932b71873db149c32a8c1c8ba65a4cbe5d30a1f677564fa2",
"assets/minecraft/textures/gui/sprites/widget/button_highlighted.png": "b658d1c9b38057aee2c63f49e19a734763eba720bd88be703ef9835c7f5f7f84",
"assets/minecraft/textures/gui/sprites/widget/button_highlighted.png.mcmeta": "ebdf3a9e4f6dfd37d72ad6af4213d9f61fe8f8342e8f12f00bb6888f3ae28acb",
"assets/minecraft/textures/gui/sprites/widget/checkbox.png": "af426f3d1e5b32f74efc0aa522db6472a18ee4f2d87608acea04d3960168b5d9",
"assets/minecraft/textures/gui/sprites/widget/checkbox_highlighted.png": "57ec86b031c3a480140a60799bde4ca1ba7ab9c14d8a7a37eb48ff64f01e29c6",
"assets/minecraft/textures/gui/sprites/widget/checkbox_selected.png": "6d70776447e4ad66d6606b9f7d670d4aa18238429f9c68db51c7f10be01c12ec",
"assets/minecraft/textures/gui/sprites/widget/checkbox_selected_highlighted.png": "96875351b998a1e7f730c6231ff4638c7df7a80986d7be94511878ec89c8fbfe",
"assets/minecraft/textures/gui/sprites/widget/cross_button.png": "922c64fb3c504e39cbcdf874a0a91ce20b84041724dd8a5ddf318af7d24ca539",
"assets/minecraft/textures/gui/sprites/widget/cross_button_highlighted.png": "016e049e726b24fad10fc9d40c85873af92ef02eec5b4ec9793665b80347e02d",
"assets/minecraft/textures/gui/sprites/widget/locked_button.png": "9bf2c84eed89da70f7757aee27973834ab421d782ee60ea017e833eeceb46f93",
"assets/minecraft/textures/gui/sprites/widget/locked_button_disabled.png": "e77079180fc95b0e75adc280ee0c916f2d315276103a8b88b7440b81e3ea3772",
"assets/minecraft/textures/gui/sprites/widget/locked_button_highlighted.png": "9163318a63b1f5c31b115d644020fbeae5fc3e6fd231f877f4c35a82c4a3c414",
"assets/minecraft/textures/gui/sprites/widget/page_backward.png": "af6220ed0297e43365a402b2e524ca4ce92f3e8f1b7e746e7f23a4ddf7a7b738",
"assets/minecraft/textures/gui/sprites/widget/page_backward_highlighted.png": "ff927d1fd93dbbb226c1693cebd6510ca6a0a1cea526dbf67a7b59c60313af0c",
"assets/minecraft/textures/gui/sprites/widget/page_forward.png": "8a2c12ad9dd5877a764e968aa3015f5a69e31bc55585edd788eac9c15bbaf4b9",
"assets/minecraft/textures/gui/sprites/widget/page_forward_highlighted.png": "eb983beae120839fbb3c8c96c862beb24f97ccd172d1f31b7e44f3a9fa483fe2",
"assets/minecraft/textures/gui/sprites/widget/scroller.png": "066af6679ba68b0e67335cb6ce48ca3e1a058c9b074411573d7ec0418bdea600",
"assets/minecraft/textures/gui/sprites/widget/scroller.png.mcmeta": "f6a77d3537ff679b691e3d4b7bab0a1e076f71d1dec602bd65fa16a505092c53",
"assets/minecraft/textures/gui/sprites/widget/scroller_background.png": "be03c9e0fcabe74d352539157353497497845669491fb5b2ac07ad27116df7f9",
"assets/minecraft/textures/gui/sprites/widget/scroller_background.png.mcmeta": "f6a77d3537ff679b691e3d4b7bab0a1e076f71d1dec602bd65fa16a505092c53",
"assets/minecraft/textures/gui/sprites/widget/slider.png": "55f92a6efea542d73ea19ed26147b4ce3aabe405b56e37db0d3cc75dc18a1f47",
"assets/minecraft/textures/gui/sprites/widget/slider.png.mcmeta": "979a3e114ffb0213932b71873db149c32a8c1c8ba65a4cbe5d30a1f677564fa2",
"assets/minecraft/textures/gui/sprites/widget/slider_handle.png": "3b22d9afcea59fd265a75ef8ba547b8f56ae8d53ca7675ccb9bc963abfa3cccb",
"assets/minecraft/textures/gui/sprites/widget/slider_handle.png.mcmeta": "9ecf324f340ac117631939372d93ec59043f22d6caec7b7ea7c140b4d8d77c22",
"assets/minecraft/textures/gui/sprites/widget/slider_handle_highlighted.png": "a89e733c03068dad39c642738508b1e41f7c263e56b32939ff26f72db2fedcd6",
"assets/minecraft/textures/gui/sprites/widget/slider_handle_highlighted.png.mcmeta": "9ecf324f340ac117631939372d93ec59043f22d6caec7b7ea7c140b4d8d77c22",
"assets/minecraft/textures/gui/sprites/widget/slider_highlighted.png": "ba0d5d1e9b3bbb1af5311457a45372d2c89b5dac32a74de40815db945b45d2a5",
"assets/minecraft/textures/gui/sprites/widget/slider_highlighted.png.mcmeta": "979a3e114ffb0213932b71873db149c32a8c1c8ba65a4cbe5d30a1f677564fa2",
"assets/minecraft/textures/gui/sprites/widget/slot_frame.png": "8428676f5a55c8fb1f68d693b8b4e8809f03dfa00b062fd1005a307cff9c0a75",
"assets/minecraft/textures/gui/sprites/widget/unlocked_button.png": "c4b7b0d01cc458e8d623af1b7cab971f98f5fc5f239cb0d1b0e90b7428406688",
"assets/minecraft/textures/gui/sprites/widget/unlocked_button_disabled.png": "0e4e936c191d125ad2abc4d5e253fb9bab7a36d290abd998d752224a70cbff13",
"assets/minecraft/textures/gui/sprites/widget/unlocked_button_highlighted.png": "36c81bc881f23d3d377128022c47e43403fe9848690c430ae8427198be0d6e1d",
"assets/minecraft/textures/gui/sprites/world_list/error.png": "e4e219517e9b8db2b018efc532f08de50b65019eb094aea0a8814f7d5358433d",
"assets/minecraft/textures/gui/sprites/world_list/error_highlighted.png": "37ac98fc99850c78f65ad3dc1b759777db030ca859c8047ffeb453491107b65e",
"assets/minecraft/textures/gui/sprites/world_list/join.png": "cb437430e8122c0f791d9834491e7cee11b3c514b25e809ae7f9578879be1e84",
"assets/minecraft/textures/gui/sprites/world_list/join_highlighted.png": "e92353fbd181db587f3c5c4e688638709e69487e9c9eae076918489ef8d2f99e",
"assets/minecraft/textures/gui/sprites/world_list/marked_join.png": "8732a1c1419bd4046ca8eec12d6c9fdc6ddbd764cce373e12e17a334aed039ed",
"assets/minecraft/textures/gui/sprites/world_list/marked_join_highlighted.png": "23c4e5b123be8b555ba8f130a03acde63e2a6fb48f1c1180a031fb077687b631",
"assets/minecraft/textures/gui/sprites/world_list/warning.png": "e4e219517e9b8db2b018efc532f08de50b65019eb094aea0a8814f7d5358433d",
"assets/minecraft/textures/gui/sprites/world_list/warning_highlighted.png": "49fee0502fab6ea38031664916ba328a1e4d68d8507761ba260efb67e640adde",
"assets/minecraft/textures/gui/title/background/panorama_0.png": "af5f25cd7db1b878806cacfc6eabb6a896e37f41340787db0de79d222ec1dbea",
"assets/minecraft/textures/gui/title/background/panorama_0.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_1.png": "3323296b81ce46d3dff93abc11289657cacaf9183b0f0663a8fa56d38ae436c8",
"assets/minecraft/textures/gui/title/background/panorama_1.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_2.png": "751f2aacfcb382dff3c57be00ece8e3a3cdfe0479374451ecbf70aa95d915b4d",
"assets/minecraft/textures/gui/title/background/panorama_2.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_3.png": "bffc916a16c3709d01e10870c329e8b915807177166f271e91ed72aa591d4f70",
"assets/minecraft/textures/gui/title/background/panorama_3.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_4.png": "d750f000ff1df1680e30c3c2c4e6921bdd5b69911e1d4e8aec527ef58841edce",
"assets/minecraft/textures/gui/title/background/panorama_4.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/background/panorama_5.png": "26f967071cde84299ba747dba7aa2db73784e3d89b158fb9092d0cddd45d215c",
"assets/minecraft/textures/gui/title/background/panorama_5.png.mcmeta": "6de2cb00c45ccc7edc47a8ebac73b003af81877efa31c755e2ee89adb97e0f0f",
"assets/minecraft/textures/gui/title/edition.png": "44d032976ac08bb01847de99d62cfea98298825fd75a36dbec319eba76081cbb",
"assets/minecraft/textures/gui/title/minecraft.png": "efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157",
"assets/sanctuary/items/zero_cape.json": "dcdcb599fe8f645ea27429da1c45ac5925b4672578f01cb6b93b2cd03f1f8b7f",
"assets/sanctuary/models/item/zero_cape.json": "2e1c1b1030fd054d60cbc2544041bd80a44ac3f2ea89d098958666b97251b56a",
"assets/sanctuary/textures/entity/cape/zero_cape.png": "f9a76537647989f9a0b6d001e320dac591c359e9e61a31f4ce11c88f207f0ad4",
"assets/sanctuary/textures/gui/aptitudes/familiar.png": "c855f9cddb68cdb4a7110e70af60afbe068bc0176b9b6c69f38e052fcdbfda6c",
"assets/sanctuary/textures/gui/aptitudes/familiarpower.png": "9ee75fd66f68bb2ce7b4158d2511ef3a9cd7a20f2b105520c4a4fcec2fee378b",
"assets/sanctuary/textures/gui/aptitudes/hungerknown.png": "1e5cd57d12592f03dc2d2071a8c2873a30939f07b0db021fcb7fc34468709acc",
"assets/sanctuary/textures/gui/aptitudes/names.png": "77dcea1c72970f87043b551c3506bfec64290b478a4d2981f280079d98158628",
"assets/sanctuary/textures/gui/aptitudes/recipes.png": "88ec437776844c15104e0856af8fd768f511b951c79802a151d6ce4ffbbb4357",
"assets/sanctuary/textures/gui/aptitudes/rest.png": "f343fd835ed20b9feb5195cd3727c0a3882090b33caf08c3f43120b250c8786e",
"assets/sanctuary/textures/gui/aptitudes/sorting.png": "fa6689f4ffdac7ffa58900afc8b264e8106cde7a992167ada7e8debda4d09618",
"assets/sanctuary/textures/gui/aptitudes/swim.png": "1e98150e08f2f47208ac0b14517760bddb55be4c013bfc3693dc474d951a9f9a",
"assets/sanctuary/textures/gui/aptitudes/worldmap.png": "a413da0a34110d4b7d4422ca2f59defc1c6394184186068974a5ff6c68a4e4b0",
"assets/sanctuary/textures/gui/aptitudes/zoom.png": "07e1c2dc51ff86e5f6903a5fc058603a886876600b01d124770336deefffa19f",
"assets/sanctuary/textures/gui/food_details.png": "1ebc30ae7394393b5b7b8a0383102bcd035e5e92a602b89db05eeab66d2ef134",
"assets/sanctuary/textures/gui/inventory/cape.png": "cd32c5f61d088740a11da8dd2767fda11f8db6b1fe65587a7c5b043177befb17",
"assets/sanctuary/textures/gui/inventory/cosmetics.png": "fcb2bca5f01a6fa6028cf6748d06320acb02011d7a7a93103d5ee24be13dca4b",
"assets/sanctuary/textures/gui/inventory/egg.png": "344a8624bd03b45472acb045fa607172d00dae603594bfa5e411d5f126fc0975",
"assets/sanctuary/textures/gui/progression/breathing.png": "2b1bfe8e0a33ae957f87dc4086b848ac26e209a4b90998fce153b2f394201157",
"assets/sanctuary/textures/gui/progression/building.png": "2d143ca35c5eac3d2a07943fe440b6893e189ff2389fd1b300d4f08bd98309ec",
"assets/sanctuary/textures/gui/progression/heart.png": "224b4162d500a632fe6861613026941a7dcd0eee97987e0c92483733f2fa0a1e",
"assets/sanctuary/textures/gui/progression/hunger.png": "9b3cb1e8206e7d17306fd461660b8ba154d729325f17181f6a045c175674ea97",
"assets/sanctuary/textures/gui/progression/inventory.png": "d66b68dda35538639f956d670b3ff25e064b00ab022a6292ce8c0d3e3ab473a3",
"assets/sanctuary/textures/gui/progression/mining.png": "c0134e182d63031ba2b53ab4477f8e1c96352375998c1c0bc54fc452816d2599",
"assets/sanctuary/textures/item/cape/zero_cape.png": "d3386907d5cc90cdf1911c911faa26dbcb6ea16ce0dc7321abb482f3322255fb",
"pack.mcmeta": "62d437b8b38a48162b3602b37d4492e239575099bdd22998ff6930afa41e5f1a",
"pack.png": "33adb8f689d55b7bf8d0824b02942ea3823f128b27fab458713ab9582061a1a0"
}
},
"changes": [
"Golden wrench: original creator PNG, unchanged bytes.",
"Editable personal template exports current Sanctuary assets overlaid by the built-in pack."
],
"golden_wrench_source": {
"file": "golden_wrench.png",
"sha256": "a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af",
"size": [
16,
16
]
}
}
+55
View File
@@ -0,0 +1,55 @@
# Sanctuary — votre pack personnel / your personal pack
Cette copie est à vous : Sanctuary ne la réécrit jamais lors d'une mise à jour.
Dans Options → Packs de ressources, activez **Sanctuary-Custom** et placez-le
au-dessus de « Textures Sanctuary » et des autres packs à remplacer.
1. Modifiez les fichiers dans `assets/`, en gardant leurs noms et chemins.
2. Rechargez avec F3 + T (ou désactivez puis réactivez le pack).
3. Pour revenir à l'apparence fournie par Sanctuary, désactivez votre pack.
Quelques points de départ :
| Ressource | Chemin |
| --- | --- |
| Clé dorée | `assets/sanctuary/textures/item/golden_wrench.png` |
| Briques et argiles colorées | `assets/sanctuary/textures/block/` |
| Menus et icônes Sanctuary | `assets/sanctuary/textures/gui/` |
| Vie et faim | `assets/minecraft/textures/gui/sprites/hud/` |
| Modèles des objets | `assets/sanctuary/models/item/` et `assets/sanctuary/items/` |
| Traductions | `assets/sanctuary/lang/fr_fr.json` et `en_us.json` |
Les PNG peuvent garder la transparence. Préservez les dimensions des atlas de
menus et les découpes utilisées par le jeu. Vous pouvez également ajouter
des remplacements Minecraft dans `assets/minecraft/` ; cette base ne contient
pas toutes les ressources du jeu ni celles des autres mods.
Cette copie est un instantané complet des ressources Sanctuary à sa création.
**Supprimez de votre copie les fichiers que vous ne personnalisez pas** pour
laisser les prochaines mises à jour du mod s'appliquer à ces ressources.
Pour repartir d'une base récente, renommez votre dossier personnel puis
cliquez de nouveau sur « Personnaliser Sanctuary » : l'ancien reste intact.
Le bouton devient « Ouvrir ma copie » tant que `Sanctuary-Custom` existe.
N'éditez pas directement le JAR du mod.
Le pack modifie l'apparence, les modèles, les sons et les textes disponibles
dans `assets/` ; les règles de jeu, sauvegardes et recettes côté serveur restent
dans le mod. Les crédits figurent dans `THIRD_PARTY_NOTICES.md`.
## English
This is your editable copy. Sanctuary updates never overwrite it. Enable
**Sanctuary-Custom** in Options → Resource Packs and move it above Sanctuary
textures and any other pack you want to override. Edit files under `assets/`
without renaming their resource paths, then reload with F3 + T or toggle the
pack off and on. Disable this pack to return to Sanctuary's supplied appearance.
The table above lists useful starting paths. Keep GUI atlas dimensions and
sprite layouts intact. You can add Minecraft overrides under `assets/minecraft/`;
this template does not include every vanilla asset or other mods' resources.
This is a full snapshot of Sanctuary's assets when created. **Delete any files
you do not customize** so future mod updates can supply them. To start a fresh
copy later, rename your existing folder and click “Customize Sanctuary” again.
“Open my copy” opens the existing folder without changing it. Do not edit the
mod JAR. This pack changes client resources, not server gameplay or save data.
+10
View File
@@ -0,0 +1,10 @@
{
"pack": {
"description": {
"translate": "sanctuary.resource_pack.custom_description",
"fallback": "Sanctuary — your editable copy"
},
"min_format": [97, 1],
"max_format": [97, 1]
}
}