Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9b3dd6bed | ||
|
|
e732da8800 | ||
|
|
248dff47b7 | ||
|
|
6aaabf1cb9 | ||
|
|
72e9263339 | ||
|
|
3fd64e1f2a | ||
|
|
8af406a028 | ||
|
|
0a81b3c147 | ||
|
|
68fd97d287 | ||
|
|
0f1efdec83 | ||
|
|
5c666a4ebc |
@@ -20,12 +20,11 @@ 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.116** ouvre le Clay Workshop aux blocs de matière : les
|
||||
nuances de leur texture sont appliquées au modèle, avec aperçu et fabrication
|
||||
payée. Elle réunit aussi le correctif du Fourneau (cases décalées par trois,
|
||||
combustible stable et explication de la chauffe).
|
||||
Le [ticket de livraison](docs/sculpture-materials-beta116.md) conserve les
|
||||
contrôles et le suivi de distribution.
|
||||
La version **beta.121** reprend la nouvelle texture de clé dorée et ajoute
|
||||
**Personnaliser Sanctuary** à l'écran des packs de ressources. Le bouton crée
|
||||
une copie complète, modifiable, que les mises à jour ne réécrivent jamais.
|
||||
Le [ticket de livraison](docs/resourcepack-template-beta121.md) détaille le
|
||||
fonctionnement et les vérifications client et archives réussies.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
@@ -36,6 +35,40 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
## beta.120 — Clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation de textures et assemblage des machines conservé.
|
||||
[Contrat et vérifications](docs/golden-wrench-beta120.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.120/Sanctuary-beta.120.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.120/Sanctuary-Test-beta.120.mrpack).
|
||||
|
||||
## beta.119 — Sculptures au centre ou au bord
|
||||
|
||||
Pose selon le point visé, centrage sur la grille entière et ancrage conservé
|
||||
en sauvegarde. [Contrat et vérifications](docs/sculpture-center-beta119.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.119/Sanctuary-beta.119.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.119/Sanctuary-Test-beta.119.mrpack).
|
||||
|
||||
## beta.118 — Murets en briques colorées
|
||||
|
||||
Seize couleurs, fabrication et tailleur de pierre, eau, raccords et collisions
|
||||
natifs. [Contrat et vérifications](docs/colored-brick-walls-beta118.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.118/Sanctuary-beta.118.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.118/Sanctuary-Test-beta.118.mrpack).
|
||||
|
||||
## beta.117 — Grain des textures et sculptures en chapeau
|
||||
|
||||
Les petits cubes d'une sculpture reprennent plusieurs nuances des textures
|
||||
de la matière. Le modèle reste intact dans le slot chapeau et au retrait.
|
||||
[Contrat et vérifications](docs/sculpture-texture-grain-beta117.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.117/Sanctuary-beta.117.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.117/Sanctuary-Test-beta.117.mrpack).
|
||||
|
||||
## beta.116 — Matières de sculpture et Fourneau
|
||||
|
||||
Un bloc de matière devient une sculpture aux couleurs de sa texture. Hotbar,
|
||||
@@ -2105,7 +2138,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.121 |
|
||||
|
||||
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
|
||||
@@ -2134,7 +2167,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.121.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).
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# beta.118 — Murets en briques de couleur
|
||||
|
||||
Ticket sur `codex/colored-brick-walls-beta118`, Minecraft 26.3.
|
||||
|
||||
Les seize familles de briques reçoivent un muret natif `WallBlock`, sous
|
||||
l'identifiant `sanctuary:<couleur>_brick_wall`. Chaque muret réutilise la texture
|
||||
de briques fournie de sa couleur, sans nouvelle image ni recoloration.
|
||||
|
||||
Les murets forment une série de seize dans **Blocs colorés**, après les dalles
|
||||
et avant les argiles, dans l'ordre des couleurs vanilla retenu en beta.103.
|
||||
Les libellés sont traduits FR/EN.
|
||||
|
||||
Recettes : six briques de même couleur, en deux rangées de trois, donnent six
|
||||
murets ; le tailleur de pierre donne un muret par bloc de briques. Les deux
|
||||
recettes par couleur rejoignent la collection Argile et briques et leurs
|
||||
déblocages. Totaux des ajouts colorés : 80 blocs, 112 objets dont ces blocs,
|
||||
224 recettes. Les recettes et identifiants préexistants sont conservés.
|
||||
|
||||
Piliers, raccords entre couleurs et aux murets vanilla, côtés hauts/bas,
|
||||
collisions et immersion dans l'eau viennent du jeu. Tags natifs `walls`
|
||||
pour blocs et objets, pioche et butin de la bonne couleur. États, modèles
|
||||
multipart et butins sont repris des ressources exactes Minecraft 26.3.
|
||||
|
||||
La génération reste reproductible dans `tools/generate-colored-bricks.py` ;
|
||||
elle conserve les ajouts externes à ses briques, notamment la recette du
|
||||
Clay Workshop et les membres Sculpture/Atelier. Le compilateur des collections
|
||||
reste leur seul générateur. Aucun ajout au
|
||||
terrain, changement de format ou migration de sauvegarde. Les dépendances
|
||||
et le pack de textures intégré beta.090 restent inchangés.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le scénario natif `Bricks102ClientChecks`, complété par `Walls118Checks`,
|
||||
passe en **38 secondes** sur un nouveau monde plat de graine 102 avec serveur
|
||||
intégré Minecraft 26.3 :
|
||||
|
||||
- Seize murets réellement posés, dont une seconde série dans l'eau.
|
||||
- Recette de six murets et tailleur de pierre pour chaque couleur ; les
|
||||
anciennes recettes de briques, dalles, escaliers et argiles passent aussi.
|
||||
- Raccords entre couleurs et au muret vanilla, retrait des piliers intermédiaires,
|
||||
côtés relevés sous un bloc plein, collisions identiques au muret vanilla.
|
||||
- Eau conservée, pioche, butin de la bonne couleur, collection et recettes
|
||||
découvertes, recette du Clay Workshop toujours découverte, série créative
|
||||
de seize dans l'ordre natif.
|
||||
- États et modèles de tous les blocs de la famille vérifiés, sans texture
|
||||
manquante ; sauvegarde et reconnexion conservent les murets et leur eau.
|
||||
|
||||
Log : `build/walls118-client.log`, marqueur `WALLS118_PASS`. Capture de la
|
||||
galerie relue dans `build/walls118-evidence/`. Aucun essai Windows ou LAN ;
|
||||
GameTest dédié exclu selon le refus antérieur de son EULA. Aucun monde personnel
|
||||
ouvert. La régénération finale conserve à l'identique 2 024 fichiers de
|
||||
ressources et métadonnées.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en **2 min 5 s**, 125 tâches dont 101 exécutées. Sources et JAR concordants,
|
||||
intégrité ZIP, versions, modèles natifs, recettes et empreintes vérifiés.
|
||||
La comparaison avec beta.117 limite les changements de classes à
|
||||
`ColoredBricks` et ses classes internes ; les sculptures et chapeaux restent
|
||||
identiques. Les textures existantes sont toutes conservées octet pour octet.
|
||||
|
||||
- `Sanctuary-beta.118.mrpack` : 10216437 octets, SHA-256
|
||||
`75bb04aad981644888690aaeda9a293a94c5d32f6439ca24613365a41aba1b6b`.
|
||||
- `Sanctuary-Test-beta.118.mrpack` : 10235359 octets, SHA-256
|
||||
`f7f0a177b8472f0e8c34ad99a1ccef5d61396014017d260e934e910c4e6eb00f`.
|
||||
- JAR Sanctuary :
|
||||
`fc6edf4046a8ace357d561ed942213a22c7a27164fa160cdb4316f72ec302575`.
|
||||
|
||||
Reçu : `build/walls118-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.118](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.118)
|
||||
est publiée : JAR, MRpack normal/Test et amorçage Prism, téléchargements publics
|
||||
et empreintes vérifiés. Le tag immuable désigne
|
||||
`0a81b3c1471c43d705eec4e31a8f6f1820049ca8`. Le canal packwiz avance au commit
|
||||
`e6bab433bea9528f67d39af0d6c7bcb6443e4817`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans l'unique instance **Sanctuary
|
||||
Beta** réussissent, la seconde passe laissant les fichiers gérés identiques.
|
||||
Un seul JAR Sanctuary beta.118 est actif. Les 923 fichiers personnels et
|
||||
réglages suivis sont inchangés ; aucun monde personnel n'a été ouvert.
|
||||
Copie préalable des fichiers remplacés dans `sanctuary-backups/before-beta.118/`.
|
||||
Reçus : `build/walls118-isolated.json`, `build/walls118-prism.json`.
|
||||
@@ -0,0 +1,109 @@
|
||||
# TOOL-01 — Clé dorée et états de blocs — beta.120
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/golden-wrench-beta120`.
|
||||
Le créateur valide les gestes du debug stick : clic gauche choisit la propriété,
|
||||
clic droit parcourt ses valeurs, Maj inverse le parcours. L'orientation est
|
||||
proposée en premier, puis les propriétés de forme et les autres états natifs.
|
||||
La sélection est personnelle, par type de bloc, pour la session courante.
|
||||
Une indication dans la barre d'action affiche la propriété et sa valeur.
|
||||
La clé ne casse pas le bloc, y compris en créatif, et ne remplace pas son type.
|
||||
|
||||
Les propriétés natives sont disponibles en survie avec la clé, sans condition
|
||||
opérateur, dans le respect des droits de construction, protections, portée et
|
||||
verrous de conteneurs. Comme avec le debug stick, les formes choisies restent
|
||||
en place immédiatement ; les mises à jour ultérieures du jeu peuvent recalculer
|
||||
les états dynamiques (redstone, connexions, cultures…).
|
||||
|
||||
Les multiblocs assemblés gardent leurs commandes d'ouverture/dissociation et
|
||||
leurs propriétés techniques ne sont pas éditées individuellement. Un assemblage
|
||||
complet garde priorité au clic droit ; Maj permet de régler les composants
|
||||
non assemblés. Hors d'un assemblage complet, même un four, un baril, un piston
|
||||
ou un établi reçoit l'action générique.
|
||||
|
||||
## Rotation de texture et contrat de stockage
|
||||
|
||||
La terre n'a pas d'orientation dans son blockstate vanilla. Une propriété
|
||||
supplémentaire de la clé, « Rotation des textures », parcourt 0°, 90°, 180° et
|
||||
270° sur les faces des modèles de blocs. La géométrie, l'éclairage, les teintes,
|
||||
les ressources actives, le bloc, son inventaire et son butin restent natifs.
|
||||
Les rendus spéciaux d'entités de blocs ne sont pas transformés par ce réglage
|
||||
des faces ; leur orientation demeure modifiable par leurs états natifs.
|
||||
|
||||
Nouvelle donnée facultative `sanctuary:wrench_textures_v1`, attachée au chunk :
|
||||
table immuable position entière → quart de tour (1 à 3). Une absence signifie
|
||||
0°. Elle n'est créée qu'après une action de clé ; revenir à 0° retire l'entrée,
|
||||
puis l'attachement s'il est vide. Aucun état vanilla ni format existant n'est
|
||||
modifié ; aucun parcours, conversion ou régénération des anciens mondes.
|
||||
Les modifications de blockstate du même type conservent la rotation. Casser,
|
||||
remplacer ou déplacer par piston retire la rotation à l'ancienne position ;
|
||||
les objets récupérés restent ordinaires. Les nouveaux clients la reçoivent
|
||||
après le chunk natif, par lots bornés de 1 024 entrées au maximum.
|
||||
Déconnexion, changement de dimension et déchargement libèrent le cache visuel.
|
||||
Revenir à une ancienne version du mod supprime seulement cet effet visuel ;
|
||||
les blocs conservent leurs identifiants, données et états natifs.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours `Wrench120ClientChecks` passe en **47 s** sur Minecraft 26.3,
|
||||
client macOS et serveur intégré, dans un monde plat jetable de graine 120 :
|
||||
|
||||
- **2 522 propriétés natives** parcourues, avec valeurs inverses et type conservé.
|
||||
- Vrais clics sur les escaliers : orientation, moitié supérieure et forme,
|
||||
sans casser le bloc en créatif ; gestes inverses et sélection distincte.
|
||||
Les doubles événements créatifs d'un même clic sont filtrés à la cadence
|
||||
native de cinq ticks.
|
||||
- Terre en survie : rotation réelle des UV émis, positions des sommets et nombre
|
||||
de faces identiques ; retour exact après quatre quarts de tour ou un inverse.
|
||||
- Rechargement des ressources conservant les UV tournés ; capture de terre relue.
|
||||
- Coffre tourné avec ses 23 diamants intacts, sans ouverture accidentelle.
|
||||
- Assemblage/dissociation par vrais clics des quatre familles : Métabli, Fût,
|
||||
Fourneau et super piston. Un clic gauche sur un assemblage ne modifie pas
|
||||
les numéros de composants ; barils et pistons isolés tournent normalement.
|
||||
- Refus en aventure, spectateur ou hors portée ; retrait des données visuelles
|
||||
après remplacement, maintien lors d'un changement d'état du même bloc.
|
||||
- Sauvegarde/reconnexion : rotation de terre côté serveur et client, état
|
||||
natif de l'escalier et inventaire du coffre conservés.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryWrench120ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `WRENCH120_PASS` et `WRENCH120_PROPERTIES 2522` dans
|
||||
`build/wrench120-client.log`, captures dans `build/wrench120-evidence/`.
|
||||
Aucun monde personnel ouvert ; pas de validation Windows ni depuis deux
|
||||
ordinateurs. Les rendus de faces natifs et le rechargement du pack sont vérifiés ;
|
||||
pas de validation spécifique d'un shader tiers.
|
||||
Le GameTest 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 12 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. La comparaison avec beta.119 limite les différences de
|
||||
production aux classes de clé/assemblage, aux deux nouveaux hooks, au rendu
|
||||
et aux libellés prévus ; toutes les autres ressources sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR Sanctuary et aucune sauvegarde
|
||||
ni fixture de test.
|
||||
|
||||
- Sanctuary-beta.120.mrpack : 10237973 octets, SHA-256
|
||||
`86b5e480a12ba20b5c1e308c77dbb54bdf8a56806915fb32ca2d16f9a72cc9c9`.
|
||||
- Sanctuary-Test-beta.120.mrpack : 10256895 octets, SHA-256
|
||||
`2e3818483ba7dfa6e5652b97933e105a44e7cc4b269fc567116bf895d54bd8d9`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`8e35ef0f134a46b7760604175ad768264946d31fdd0694982e049e7a928fde2a`.
|
||||
|
||||
Reçu local : `build/wrench120-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.120](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.120)
|
||||
est publiée depuis `248dff47b7a0ebd11026230581fe69641dbde76d`. Le tag exact et les
|
||||
artefacts sont immuables ; leurs téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `e8060764a444a58b0a17318a3821dfeb1a0c33c6`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.120 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés, sans
|
||||
ouvrir de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.120/` de l'instance existante. Reçus locaux :
|
||||
`build/wrench120-isolated.json` et `build/wrench120-prism.json`.
|
||||
@@ -14,6 +14,63 @@ 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.120 — clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation des textures et assemblages prioritaires.
|
||||
[Contrat et vérifications](golden-wrench-beta120.md).
|
||||
[Release beta.120](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.120)
|
||||
publiée ; canal `e8060764a444a58b0a17318a3821dfeb1a0c33c6`. 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.120/`.
|
||||
|
||||
## beta.119 — pose centrée des sculptures
|
||||
|
||||
Centre accessible au point visé, bords conservés et grille entière de 1/16.
|
||||
[Contrat et vérifications](sculpture-center-beta119.md).
|
||||
[Release beta.119](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.119)
|
||||
publiée ; canal `5e823af7807f524f6394acad402fe62328c7e4ab`. 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.119/`.
|
||||
|
||||
## beta.118 — murets en briques colorées
|
||||
|
||||
Seize murets natifs, recettes, tailleur, eau et série créative par couleurs.
|
||||
[Contrat et vérifications](colored-brick-walls-beta118.md).
|
||||
[Release beta.118](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.118)
|
||||
publiée ; canal `e6bab433bea9528f67d39af0d6c7bcb6443e4817`. 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.118/`.
|
||||
|
||||
## beta.117 — grain des textures et sculptures en chapeau
|
||||
|
||||
Échantillonnage des pixels de plusieurs textures, grain stable même sur un
|
||||
modèle uni et conservation des données du chapeau.
|
||||
[Contrat et vérifications](sculpture-texture-grain-beta117.md).
|
||||
[Release beta.117](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.117)
|
||||
publiée ; canal `a5b75a70f86e42afb8e06ba4daa9880d61c6373e`. 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.117/`.
|
||||
|
||||
## beta.116 — palettes de sculpture et Fourneau
|
||||
|
||||
Le Clay Workshop utilise les nuances des textures de blocs ; le Fourneau
|
||||
corrige les décalages de cases et explique la chauffe.
|
||||
[Contrat et vérifications](sculpture-materials-beta116.md).
|
||||
[Release beta.116](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.116)
|
||||
publiée ; canal `c741870160f8a21f0a9793f20f0e624ebdbacdb6`. 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.116/`.
|
||||
|
||||
## beta.110 — version commune
|
||||
|
||||
Clay Workshop, Statuaire et import GLB intégrés aux œufs et à la neige saisonnière.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# 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
|
||||
|
||||
Artefacts prêts ; publication du canal et synchronisation Prism à effectuer.
|
||||
@@ -0,0 +1,84 @@
|
||||
# STAT-07 — Pose centrée des sculptures — beta.119
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-center-beta119`.
|
||||
|
||||
Viser le carré central de 8 × 8 pixels de la face supérieure ou inférieure
|
||||
du bloc place la sculpture au centre. Les limites de cette zone sont incluses.
|
||||
Autour, le bord horizontal le plus proche reste choisi ; une pose latérale
|
||||
continue à s'appuyer contre le bloc support. L'orientation suit le regard.
|
||||
L'aide du Clay Workshop explique les deux possibilités en français et anglais.
|
||||
|
||||
Le centrage utilise l'emprise réelle des voxels après rotation, en ignorant les
|
||||
marges transparentes. Tous les sommets restent sur la grille de 1/16 de bloc.
|
||||
Une dimension impaire ne peut pas être parfaitement symétrique dans un bloc de
|
||||
16 pixels sans décaler ses voxels : elle prend donc le centrage entier le plus
|
||||
proche, avec un pixel de différence entre les deux marges. Aucune position à
|
||||
un demi-voxel n'est introduite.
|
||||
|
||||
## Compatibilité
|
||||
|
||||
La pose réutilise `anchor=center`, déjà présent depuis beta.113. Aucun nouvel
|
||||
état, identifiant, composant ou format de sauvegarde. Les sculptures existantes
|
||||
gardent leur ancrage ; casser et reposer choisit l'emplacement visé. Le rendu,
|
||||
les couleurs, les chapeaux et les collisions restent inchangés. Aucun parcours
|
||||
de monde existant, aucune régénération. Client et serveur doivent utiliser
|
||||
ensemble la nouvelle version pour le choix de pose.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours natif `Statuary106ClientChecks`, avec `Sculpture113ClientChecks`,
|
||||
passe en **54 s** sur Minecraft 26.3/macOS :
|
||||
|
||||
- Vrais clics de pose centrée dans quatre orientations, sur client et serveur,
|
||||
puis pose contre les quatre bords sans modifier les couleurs du modèle.
|
||||
- Limites incluses et points immédiatement de part et d'autre de la zone,
|
||||
faces supérieure/inférieure et positions positives/négatives ; faces
|
||||
latérales toujours calées contre le support.
|
||||
- 5 120 combinaisons de largeur/profondeur, orientation et ancrage : géométrie
|
||||
effectivement rendue sur la grille exacte, emprise sans dépassement,
|
||||
centrage pair/impair et marges transparentes.
|
||||
- Sauvegarde/reconnexion des sculptures centrées et aux bords, sans fichier
|
||||
source ; rotations/miroirs, anciens états, fabrication et hotbar vérifiés.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `SCULPTURE119_PASS`, `STATUARY113_PASS` et
|
||||
`SCULPTURE113_GEOMETRY_PASS` dans `build/sculpture119-client.log`.
|
||||
Captures conservées dans `build/sculpture119-evidence/`, vue centrée relue.
|
||||
Monde plat jetable de graine 106 ; aucun monde personnel ouvert. Pas de
|
||||
validation Windows ni de test depuis deux ordinateurs.
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA ;
|
||||
le parcours natif utilise le serveur intégré.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 24 s**, 125 tâches. Les sources JAR correspondent aux sources
|
||||
du dépôt ; seuls `SculptureAnchor.class` et le texte d'aide FR/EN diffèrent
|
||||
parmi les classes et ressources de production de beta.118. Les deux packs
|
||||
contiennent le même JAR, sans monde ni fixture de test.
|
||||
|
||||
- Sanctuary-beta.119.mrpack : 10216447 octets, SHA-256
|
||||
`ce29702eaaac7301c77fb6af63a76c6bc1c0c704313ce1b8243c95b5fe59d893`.
|
||||
- Sanctuary-Test-beta.119.mrpack : 10235369 octets, SHA-256
|
||||
`a1a2845f9678b1144d7d9eab4867970cc1776fe0ad5d51722c8110b7c1d4c682`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`d353bd89b16c53ee1ee48836fa2ff4b311249b280992300c28bf03048d805bff`.
|
||||
|
||||
Reçu local : `build/sculpture119-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.119](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.119)
|
||||
est publiée depuis `3fd64e1f2a77ea2584b087d6339cf605d8ee0e9b`. Le tag exact et les
|
||||
artefacts sont immuables ; leurs téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `5e823af7807f524f6394acad402fe62328c7e4ab`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.119 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés, sans
|
||||
ouvrir de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.119/` de l'instance existante. Reçus locaux :
|
||||
`build/sculpture119-isolated.json` et `build/sculpture119-prism.json`.
|
||||
@@ -83,5 +83,20 @@ Les classes de test et modèles GLB de test sont absents des archives.
|
||||
- JAR Sanctuary :
|
||||
`76a9205783c8b18506c32c9eef6c481d95fd3ba0746a2bdd558a194534a51a32`.
|
||||
|
||||
Reçu local : `build/sculpture116-artifact.json`. Publication et synchronisation
|
||||
de l'instance en cours ; leurs résultats seront consignés après vérification.
|
||||
Reçu local : `build/sculpture116-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.116](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.116)
|
||||
est publiée avec le JAR, les deux MRpack et l'amorçage Prism. Le tag immuable
|
||||
désigne `e3ca9fcd6383f7646e4fea7e5098e7097ebb30aa` ; téléchargements publics
|
||||
et empreintes vérifiés. Le canal packwiz avance au commit
|
||||
`c741870160f8a21f0a9793f20f0e624ebdbacdb6`.
|
||||
|
||||
Deux synchronisations isolées, puis deux dans l'unique instance **Sanctuary
|
||||
Beta**, réussissent. La seconde passe laisse les fichiers gérés inchangés,
|
||||
avec un seul JAR Sanctuary beta.116. Minecraft 26.3, Loader 0.19.5 et Fabric
|
||||
API 0.160.5+26.3 conservés. Les 923 fichiers personnels et réglages suivis
|
||||
restent identiques ; aucun monde personnel n'a été ouvert. Sauvegarde des
|
||||
fichiers remplacés dans `sanctuary-backups/before-beta.116/` de cette instance.
|
||||
Reçus : `build/sculpture116-isolated.json`, `build/sculpture116-prism.json`.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# beta.117 — Grain des textures et sculptures en chapeau
|
||||
|
||||
Ticket sur `codex/sculpture-texture-grain-beta117`, Minecraft 26.3.
|
||||
|
||||
En beta.116, une couleur du modèle source était associée à une seule nuance
|
||||
de matière. Un modèle uni restait donc uni malgré les pixels différents de la
|
||||
texture. La sélection utilise maintenant les textures des faces du modèle de
|
||||
bloc, avec leurs propres teintes du pack actif. Un échantillonnage de pixels
|
||||
répartit leurs couleurs exactes dans les voxels, sans couleur interpolée.
|
||||
|
||||
## Coût et comportement
|
||||
|
||||
Au maximum 32 matériaux de faces et 16 × 16 échantillons par matériau sont lus,
|
||||
puis ramenés à la palette existante de 256 entrées. Les répétitions conservent
|
||||
la fréquence des nuances. Les pixels transparents sont écartés ; les textures
|
||||
animées utilisent la première image. Les blocs sans texture lisible gardent
|
||||
le repli antérieur sur la texture représentative, puis la couleur de carte.
|
||||
|
||||
La sélection d'une nuance utilise un mélange entier déterministe des coordonnées
|
||||
de chaque voxel. Un modèle uni utilise toute la palette ; un modèle ombré
|
||||
utilise des plages de nuances qui suivent ses zones claires et sombres.
|
||||
Ce grain est un transfert de palette, pas une projection UV conservant les
|
||||
motifs ou les veinures du bloc. Les voxels restent opaques.
|
||||
|
||||
Le calcul utilise Java standard et les modèles/textures déjà chargés par
|
||||
Minecraft, sans bibliothèque supplémentaire. Aucun calcul de matière dans
|
||||
le rendu par image. Le client ne rééchantillonne qu'au changement de matière
|
||||
ou de ressources ; une rotation réutilise la palette. Le serveur conserve
|
||||
le modèle recoloré pour les fabrications suivantes avec la même sélection.
|
||||
|
||||
## Chapeaux et données
|
||||
|
||||
Le système de blocs portés retirait `BLOCK_ENTITY_DATA` des objets pour isoler
|
||||
les inventaires des coffres et fourneaux. Sur une sculpture, ce composant est
|
||||
la géométrie elle-même : le slot contenait encore un objet, devenu invisible.
|
||||
Les sculptures conservent désormais ce composant à l'équipement et au retrait.
|
||||
|
||||
Si une ancienne sculpture équipée a perdu ce composant, mais que sa session
|
||||
de chapeau possède encore le modèle, il est restauré depuis cette donnée
|
||||
serveur. La restauration ne fabrique aucune géométrie manquante. Les objets
|
||||
déjà retirés et dépourvus de toute copie du modèle ne sont pas reconstructibles
|
||||
par ce correctif. Les règles de contenu des coffres restent inchangées.
|
||||
|
||||
Identifiants, schéma 1 des sculptures, structure des accessoires et paquet
|
||||
de sélection inchangés. Les couleurs déjà sauvegardées restent intactes.
|
||||
Les deux côtés utilisent beta.117 pour le même calcul de prévisualisation et
|
||||
de fabrication. Aucun monde personnel ouvert ni migration de sauvegarde.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le scénario client natif complet passe en 2 min 1 s sur macOS avec serveur
|
||||
intégré, monde plat jetable de graine 116. Il reprend le scénario beta.116
|
||||
avec un GLB entièrement uni, plus les régressions de l'atelier 106/111/112/113 :
|
||||
|
||||
- 1 207 objets-blocs échantillonnés, huit fabrications payées avec aperçu
|
||||
identique au résultat serveur, consommation exacte d'un bloc.
|
||||
- Répartition 25 % / 75 % de deux couleurs de texture sur un modèle uni,
|
||||
aucune couleur inventée, conservation des zones claires et sombres.
|
||||
- Couleurs réellement présentes dans les textures natives des faces, avec
|
||||
plusieurs matériaux d'une bûche ; grain présent sur pierre, bois et minerai.
|
||||
- Motif indépendant de l'ordre des voxels, stable et sauvegardé ; rechargement
|
||||
des ressources, palette bornée, inventaire plein et sélections périmées.
|
||||
- Vrais clics des slots hotbar/chapeau, conservation exacte des composants
|
||||
et du nom personnalisé, icône et modèle porté visibles, retrait/rééquipement.
|
||||
- Restauration d'un objet privé de sa géométrie depuis une ancienne session
|
||||
équipée, puis sauvegarde et reconnexion du client avec le chapeau intact.
|
||||
- Pose, bord visé, orientation, casse/repose, reconnexion sans fichier source,
|
||||
menus FR/EN et génération du plan du Métabli inchangés.
|
||||
|
||||
Mesure indicative après échauffement, modèle maximal de 4 096 voxels,
|
||||
101 applications sur le Mac de développement : médiane **0,283 ms**, p95
|
||||
**0,971 ms**. Ce n'est pas une garantie de temps sur d'autres machines ; le
|
||||
calcul n'est exécuté ni chaque image ni à chaque objet fabriqué identique.
|
||||
|
||||
Marqueurs `SCULPTURE117_GRAIN_PASS`, `SCULPTURE117_HAT_PASS`,
|
||||
`SCULPTURE117_HAT_RECONNECT_PASS`, `SCULPTURE116_PASS` dans
|
||||
`build/sculpture117-final-client.log`. Captures relues dans
|
||||
`build/sculpture117-evidence/`. Pas d'essai Windows ou LAN. Le GameTest dédié
|
||||
reste exclu selon le refus antérieur de son EULA.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en **3 min 7 s**, 125 tâches dont 105 exécutées. JAR sources concordants,
|
||||
versions, contenu et intégrité des deux archives vérifiés. La comparaison
|
||||
avec beta.116 limite les changements de production au calcul des matières,
|
||||
à son cache dans le menu et au traitement des sculptures en chapeau.
|
||||
Les modèles de test et classes de GameTest sont absents de la livraison.
|
||||
|
||||
- `Sanctuary-beta.117.mrpack` : 10191137 octets, SHA-256
|
||||
`adbe5a40bbd050ebc84ff8aca61a7a3643dcf25ec8f2ac59b91561e48275cbbb`.
|
||||
- `Sanctuary-Test-beta.117.mrpack` : 10210059 octets, SHA-256
|
||||
`0dacb12b30bde66c647871101a4f4c6b0b58e7ceb29f7743398f8a569b958a6e`.
|
||||
- JAR Sanctuary :
|
||||
`1ce63f97f17c6098f3c86c6753f9b912bce39d88e05ba240423c38bdc97307ee`.
|
||||
|
||||
Reçu : `build/sculpture117-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.117](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.117)
|
||||
est publiée : JAR, MRpack normal/Test et amorçage Prism, téléchargements publics
|
||||
et empreintes vérifiés. Le tag immuable désigne
|
||||
`0f1efdec830342e25d3316b356330ad937d0d942`. Le canal packwiz avance au commit
|
||||
`a5b75a70f86e42afb8e06ba4daa9880d61c6373e`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans l'unique instance **Sanctuary
|
||||
Beta** réussissent, la seconde passe laissant les fichiers gérés identiques.
|
||||
Un seul JAR Sanctuary beta.117 est actif. Les 923 fichiers personnels et
|
||||
réglages suivis sont inchangés ; aucun monde personnel n'a été ouvert.
|
||||
Copie préalable des fichiers remplacés dans `sanctuary-backups/before-beta.117/`.
|
||||
Reçus : `build/sculpture117-isolated.json`, `build/sculpture117-prism.json`.
|
||||
+3
-3
@@ -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.116
|
||||
pack_version=beta.116
|
||||
resource_pack_version=beta.090
|
||||
mod_version=beta.121
|
||||
pack_version=beta.121
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,14 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
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') }
|
||||
|
||||
def sculpture116Client = providers.gradleProperty('sanctuarySculpture116ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture116ClientTests', sculpture116Client)
|
||||
if (sculpture116Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture116ClientChecks') }
|
||||
@@ -440,6 +448,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'
|
||||
|
||||
+12
-11
@@ -37,10 +37,10 @@ public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var level=p.level();var pos=new BlockPos(0,3,0);
|
||||
check(ColoredBricks.families().size()==16,"All colors registered");int index=0;
|
||||
check(ColoredBricks.families().size()==16,"All colors registered");Walls118Checks.server(s);int index=0;
|
||||
for(var f:ColoredBricks.families()){
|
||||
String color=f.color().getName();
|
||||
for(var block:List.of(f.bricks(),f.slab(),f.stairs(),f.clay()))check(block.asItem()!=Items.AIR,"Block item exists "+block);
|
||||
for(var block:List.of(f.bricks(),f.slab(),f.stairs(),f.wall(),f.clay()))check(block.asItem()!=Items.AIR,"Block item exists "+block);
|
||||
check(f.bricks().defaultBlockState().is(BlockTags.MINEABLE_WITH_PICKAXE),"Pickaxe tag "+color);
|
||||
check(f.clay().defaultBlockState().is(BlockTags.MINEABLE_WITH_SHOVEL),"Shovel tag "+color);
|
||||
var shovel=new ItemStack(Items.DIAMOND_SHOVEL);var silk=shovel.copy();silk.enchant(level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH),1);
|
||||
@@ -52,7 +52,7 @@ public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
drop(Block.getDrops(f.stairs().defaultBlockState(),level,pos,null),f.stairs().asItem(),1);
|
||||
check(f.slab().defaultBlockState().setValue(SlabBlock.WATERLOGGED,true).getFluidState().isSource(),"Slab water "+color);
|
||||
check(f.stairs().defaultBlockState().setValue(StairBlock.WATERLOGGED,true).getFluidState().isSource(),"Stair water "+color);
|
||||
for(String suffix:List.of("clay_ball_from_dye","clay_from_dye","brick_from_dye","bricks_from_dye","clay","bricks","brick_slab","brick_stairs","brick_slab_from_stonecutting","brick_stairs_from_stonecutting","clay_ball_smelting","clay_smelting"))check(s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+suffix))).isPresent(),"Recipe loaded "+color+"_"+suffix);
|
||||
for(String suffix:List.of("clay_ball_from_dye","clay_from_dye","brick_from_dye","bricks_from_dye","clay","bricks","brick_slab","brick_stairs","brick_slab_from_stonecutting","brick_stairs_from_stonecutting","brick_wall","brick_wall_from_stonecutting","clay_ball_smelting","clay_smelting"))check(s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+suffix))).isPresent(),"Recipe loaded "+color+"_"+suffix);
|
||||
craft(s,level,2,2,List.of(f.brick(),f.brick(),f.brick(),f.brick()),f.bricks().asItem(),1);
|
||||
craft(s,level,2,2,List.of(f.clayBall(),f.clayBall(),f.clayBall(),f.clayBall()),f.clay().asItem(),1);
|
||||
craft(s,level,3,1,List.of(f.bricks().asItem(),f.bricks().asItem(),f.bricks().asItem()),f.slab().asItem(),6);
|
||||
@@ -62,39 +62,40 @@ public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
var input=new SingleRecipeInput(new ItemStack(f.clayBall()));var cooked=s.getRecipeManager().getRecipeFor(RecipeType.SMELTING,input,level).orElseThrow().value().assemble(input);check(cooked.is(f.brick()),"Smelt brick "+color);
|
||||
input=new SingleRecipeInput(new ItemStack(f.clay()));cooked=s.getRecipeManager().getRecipeFor(RecipeType.SMELTING,input,level).orElseThrow().value().assemble(input);check(cooked.is(BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(color+"_terracotta"))),"Smelt terracotta "+color);
|
||||
var stoneInput=new SingleRecipeInput(new ItemStack(f.bricks()));
|
||||
for(var variant:List.of(new Object[]{"brick_slab",f.slab().asItem(),2},new Object[]{"brick_stairs",f.stairs().asItem(),1})){
|
||||
for(var variant:List.of(new Object[]{"brick_slab",f.slab().asItem(),2},new Object[]{"brick_stairs",f.stairs().asItem(),1},new Object[]{"brick_wall",f.wall().asItem(),1})){
|
||||
var recipe=(StonecutterRecipe)s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+variant[0]+"_from_stonecutting"))).orElseThrow().value();check(recipe.matches(stoneInput,level),"Stonecutting accepts input");var cut=recipe.assemble(stoneInput);check(cut.is((Item)variant[1])&&cut.getCount()==(int)variant[2],"Stonecutting result");
|
||||
}
|
||||
int x=(index%8)*3,z=(index/8)*5;level.setBlockAndUpdate(new BlockPos(x,1,z),f.bricks().defaultBlockState());level.setBlockAndUpdate(new BlockPos(x,2,z),f.slab().defaultBlockState().setValue(SlabBlock.TYPE,SlabType.TOP));level.setBlockAndUpdate(new BlockPos(x+1,1,z),f.stairs().defaultBlockState().setValue(StairBlock.FACING,Direction.SOUTH));level.setBlockAndUpdate(new BlockPos(x,1,z+1),f.clay().defaultBlockState());index++;
|
||||
}
|
||||
fr.koka.sanctuary.recipes.RecipeService.discover(p,Items.CLAY_BALL);fr.koka.sanctuary.recipes.RecipeService.refresh(p);
|
||||
var known=fr.koka.sanctuary.recipes.RecipeService.knowledge(p);
|
||||
for(var f:ColoredBricks.families())check(known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_stairs_from_stonecutting"),"Collection unlock "+f.color());
|
||||
check(known.recipes.contains("sanctuary:clay_workshop"),"Existing workshop recipe remains discoverable");
|
||||
for(var f:ColoredBricks.families())check(known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_stairs_from_stonecutting")&&known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_wall")&&known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_wall_from_stonecutting"),"Collection unlock "+f.color());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
p.teleportTo(11,8,19);p.setYRot(180);p.setXRot(26);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
});
|
||||
c.runOnClient(m->{
|
||||
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());
|
||||
var blocks=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.COLORED_BLOCKS).value().getDisplayItems();
|
||||
var ingredients=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.INGREDIENTS).value().getDisplayItems();
|
||||
var ingredients=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.INGREDIENTS).value().getDisplayItems();Walls118Checks.creative(new ArrayList<>(blocks));
|
||||
for(var f:ColoredBricks.families()){
|
||||
for(var b:List.of(f.bricks(),f.slab(),f.stairs(),f.clay())){
|
||||
for(var b:List.of(f.bricks(),f.slab(),f.stairs(),f.wall(),f.clay())){
|
||||
check(blocks.stream().anyMatch(i->i.is(b.asItem())),"Creative color tab "+b);
|
||||
for(var state:b.getStateDefinition().getPossibleStates()){
|
||||
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);check(!parts.isEmpty(),"Baked model "+state);
|
||||
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);check(b instanceof WallBlock||!parts.isEmpty(),"Baked model "+state);
|
||||
for(var part:parts)for(var direction:new Direction[]{Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST,null})for(var quad:part.getQuads(direction))check(!quad.materialInfo().sprite().contents().name().toString().contains("missingno"),"No missing texture "+state);
|
||||
}
|
||||
}
|
||||
for(var item:List.of(f.clayBall(),f.brick()))check(ingredients.stream().anyMatch(i->i.is(item)),"Creative ingredients "+item);
|
||||
}
|
||||
});
|
||||
c.waitTicks(20);c.runOnClient(m->{m.player.setYRot(180);m.player.setXRot(26);});c.waitTicks(4);c.takeScreenshot("bricks102-gallery");saved=world.getWorldSave();System.out.println("BRICKS102_NATIVE_PASS colors=16 recipes=192 blocks=64 items=96");
|
||||
c.waitTicks(20);c.runOnClient(m->{m.player.setYRot(180);m.player.setXRot(26);});c.waitTicks(4);c.takeScreenshot("bricks102-gallery");saved=world.getWorldSave();System.out.println("BRICKS102_NATIVE_PASS colors=16 recipes=224 blocks=80 items=112");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var level=s.getPlayerList().getPlayers().getFirst().level();int i=0;for(var f:ColoredBricks.families()){var pos=new BlockPos((i%8)*3,1,(i/8)*5);check(level.getBlockState(pos).is(f.bricks()),"Saved bricks "+f.color());check(level.getBlockState(pos.above()).getValue(SlabBlock.TYPE)==SlabType.TOP,"Saved slab state");i++;}});
|
||||
world.getServer().runOnServer(s->{var level=s.getPlayerList().getPlayers().getFirst().level();Walls118Checks.saved(level);int i=0;for(var f:ColoredBricks.families()){var pos=new BlockPos((i%8)*3,1,(i/8)*5);check(level.getBlockState(pos).is(f.bricks()),"Saved bricks "+f.color());check(level.getBlockState(pos.above()).getValue(SlabBlock.TYPE)==SlabType.TOP,"Saved slab state");i++;}});
|
||||
}
|
||||
System.out.println("BRICKS102_PASS");
|
||||
System.out.println("BRICKS102_PASS");System.out.println("WALLS118_PASS native placement, mixed connections, tall sides, waterlogging, collision, crafting, stonecutting, loot, collection, creative order, baked models and reconnect");
|
||||
}
|
||||
private static void craft(net.minecraft.server.MinecraftServer s,net.minecraft.server.level.ServerLevel level,int w,int h,List<Item> items,Item result,int count){var input=CraftingInput.of(w,h,items.stream().map(ItemStack::new).toList());var stack=s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,level).orElseThrow().value().assemble(input);check(stack.is(result)&&stack.getCount()==count,"Craft "+result);}
|
||||
private static void drop(List<ItemStack> drops,Item item,int count){check(drops.size()==1&&drops.getFirst().is(item)&&drops.getFirst().getCount()==count,"Loot "+item+" expected "+count+" got "+drops);}
|
||||
|
||||
+165
@@ -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); }
|
||||
}
|
||||
+13
@@ -18,7 +18,20 @@ import net.minecraft.world.phys.*;
|
||||
final class Sculpture113ClientChecks {
|
||||
static void placementContexts(ServerPlayer player){
|
||||
for(var pos:List.of(new BlockPos(2,0,-2),new BlockPos(-2,0,2))){
|
||||
for(var face:List.of(Direction.UP,Direction.DOWN)){
|
||||
for(double x:new double[]{.25,.5,.75})for(double z:new double[]{.25,.5,.75}){
|
||||
var hit=new Vec3(pos.getX()+x,pos.getY()+(face==Direction.UP?1:0),pos.getZ()+z);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(hit,face,pos,false));
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Middle square including boundaries selects center on top/bottom faces");
|
||||
}
|
||||
}
|
||||
for(var edge:Direction.Plane.HORIZONTAL){
|
||||
for(double offset:new double[]{.25-1e-6,.25+1e-6}){
|
||||
var hit=new Vec3(pos.getX()+.5+edge.getStepX()*offset,pos.getY()+1,pos.getZ()+.5+edge.getStepZ()*offset);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(hit,Direction.UP,pos,false));
|
||||
var expected=offset<.25?SculptureAnchor.CENTER:SculptureAnchor.of(edge);
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==expected,"Center boundary switches to the aimed edge");
|
||||
}
|
||||
var top=new Vec3(pos.getX()+.5+edge.getStepX()*.4,pos.getY()+1,pos.getZ()+.5+edge.getStepZ()*.4);
|
||||
var context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(top,Direction.UP,pos,false));
|
||||
var state=Statuary.BLOCK.getStateForPlacement(context);
|
||||
|
||||
+10
-3
@@ -32,18 +32,19 @@ public final class Sculpture116ClientChecks implements FabricClientGameTest {
|
||||
}});
|
||||
Path file=PlanClient.directory().resolve("sculpture116-"+UUID.randomUUID()+".glb");
|
||||
try{
|
||||
Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture());var asset=GlbModel.load(file);var source=VoxelSculpture.of(asset.id(),asset.name(),StatueVoxelizer.miniature(asset.faces()));
|
||||
Files.createDirectories(file.getParent());Files.write(file,Statuary106ClientChecks.fixture(true));var asset=GlbModel.load(file);var source=VoxelSculpture.of(asset.id(),asset.name(),StatueVoxelizer.miniature(asset.faces()));
|
||||
try{new SculpturePalette(List.of());throw new AssertionError("Empty palette accepted");}catch(IllegalArgumentException expected){}
|
||||
try{new SculpturePalette(Collections.nCopies(257,-1));throw new AssertionError("Oversized palette accepted");}catch(IllegalArgumentException expected){}
|
||||
try{new SculpturePalette(List.of(0));throw new AssertionError("Transparent color accepted");}catch(IllegalArgumentException expected){}
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;ItemStack hat;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture116 "+UUID.randomUUID());s.setSeed("116");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
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=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.getInventory().clearContent();p.teleportTo(.5,1,.5);for(int x=-4;x<8;x++)for(int z=-4;z<8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());Statuary.open(p,WORKSHOP);});
|
||||
c.waitForScreen(ClayWorkshopScreen.class);c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();((ClayWorkshopScreen)m.gui.screen()).load(file);});
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);
|
||||
c.runOnClient(m->{int count=0;for(var item:BuiltInRegistries.ITEM)if(item instanceof BlockItem){var stack=new ItemStack(item);check(ClayWorkshopMenu.material(stack),"Empty registered block accepted: "+item);var palette=SculptureMaterialSampler.sample(stack);check(palette!=null&&palette.colors().size()<=256&&palette.colors().stream().allMatch(v->(v>>>24)==255),"Bounded opaque palette: "+item);count++;}check(count>1000,"Whole block registry sampled");System.out.println("SCULPTURE116_PALETTES "+count+" registered block items");
|
||||
var dark=new ItemStack(Items.SHULKER_BOX);dark.set(DataComponents.CONTAINER,ItemContainerContents.fromItems(List.of(new ItemStack(Items.DIAMOND))));check(!ClayWorkshopMenu.material(dark),"Filled container cannot lose contents");check(!ClayWorkshopMenu.material(new ItemStack(Items.DIAMOND)),"Non-block item refused");
|
||||
var shades=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++){int v=x*17;shades.add(new VoxelSculpture.Cell(x,0,0,0xFF000000|v<<16|v<<8|v));}var ramp=new VoxelSculpture(source.modelId(),"Ramp",16,1,1,shades);var palette=SculptureMaterialSampler.sample(new ItemStack(Items.OAK_PLANKS));var recolored=palette.apply(ramp);check(recolored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>2,"Material retains multiple texture shades");for(int x=1;x<16;x++)check(SculpturePalette.lightness(recolored.cells().get(x).color())>=SculpturePalette.lightness(recolored.cells().get(x-1).color()),"Model shading order retained");});
|
||||
check(source.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"Native paid crafts start from a uniform model");Sculpture117Checks.run(source);});
|
||||
var rendered=new HashSet<VoxelSculpture>();
|
||||
for(var item:List.of(Items.STONE,Items.OAK_PLANKS,Items.GOLD_BLOCK,Items.WOOL.pick(DyeColor.RED),Items.GLASS,Items.OAK_LEAVES,Items.CLAY,Items.DEEPSLATE)){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(item,2));check(menu.getSlot(1).getItem().isEmpty(),"Previous material result invalidates immediately");menu.broadcastChanges();});
|
||||
@@ -66,8 +67,14 @@ public final class Sculpture116ClientChecks implements FabricClientGameTest {
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)s.getPlayerList().getPlayers().getFirst().containerMenu;check(diamond.equals(StatuaryEntity.sculpture(menu.getSlot(1).getItem()))&&menu.getSlot(0).getItem().getCount()==1,"Forged menu token cannot change recipe or spend input");});
|
||||
var before=c.computeOnClient(m->m.getModelManager().getBlockStateModelSet());var reload=c.computeOnClient(m->m.reloadResourcePacks());c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(8);
|
||||
c.runOnClient(m->{check(before!=m.getModelManager().getBlockStateModelSet(),"Actual resource reload replaces sampling generation");check(diamond.equals(StatuaryEntity.sculpture(((ClayWorkshopScreen)m.gui.screen()).preview())),"Preview refreshes after pack reload");});
|
||||
hat=SculptureHat117Checks.exercise(c,server,diamond);
|
||||
System.out.println("SCULPTURE116_MATERIALS_PASS all block palettes, eight native paid materials, preview equality, shading, stale selections, input identity, token, palette bounds, containers and resource reload");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);c.waitTicks(12);
|
||||
SculptureHat117Checks.verifyEquipped(c,world.getServer(),hat);
|
||||
System.out.println("SCULPTURE117_HAT_RECONNECT_PASS equipped sculpture survives native save and fresh client reconnect");
|
||||
}
|
||||
}catch(java.io.IOException e){throw new AssertionError(e);}finally{intro.set(false);try{Files.deleteIfExists(file);}catch(java.io.IOException ignored){}}
|
||||
new Statuary106ClientChecks().runTest(c);
|
||||
System.out.println("SCULPTURE116_PASS materials plus full workshop/hotbar, placement, anchoring, drops, reconnect, FR/EN and Metabli regression");
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Regression for flat-color models, exact texture colors, deterministic grain and bounded CPU work. */
|
||||
final class Sculpture117Checks {
|
||||
static void run(VoxelSculpture source){
|
||||
var cells=new ArrayList<VoxelSculpture.Cell>();
|
||||
for(int z=0;z<16;z++)for(int y=0;y<16;y++)for(int x=0;x<16;x++)cells.add(new VoxelSculpture.Cell(x,y,z,0xFF808080));
|
||||
var flat=new VoxelSculpture(source.modelId(),"Flat",16,16,16,cells);
|
||||
var colors=new ArrayList<Integer>();colors.addAll(Collections.nCopies(64,0xFF223344));colors.addAll(Collections.nCopies(192,0xFFCCBBAA));
|
||||
var weighted=new SculpturePalette(colors);var out=weighted.apply(flat);
|
||||
long dark=out.cells().stream().filter(v->v.color()==0xFF223344).count();
|
||||
check(dark>900&&dark<1150,"Uniform source retains sampled 25% dark / 75% light texture distribution");
|
||||
check(out.cells().stream().allMatch(v->colors.contains(v.color())),"Never invent an averaged color");
|
||||
var reversed=new ArrayList<>(flat.cells());Collections.reverse(reversed);
|
||||
var shuffled=weighted.apply(new VoxelSculpture(flat.modelId(),flat.name(),16,16,16,reversed));
|
||||
check(Arrays.equals(out.packed(),shuffled.cells().reversed().stream().flatMapToInt(v->java.util.stream.IntStream.of(v.key(),v.color())).toArray()),"Sampling is independent of cell iteration order");
|
||||
check(out.equals(weighted.apply(flat))&&out.equals(StatuaryEntity.decode(StatuaryEntity.encode(out))),"Stable pattern and persisted final colors");
|
||||
var gradient=new ArrayList<Integer>();for(int i=0;i<256;i++)gradient.add(0xFF000000|i<<16|i<<8|i);
|
||||
var palette=new SculpturePalette(gradient);
|
||||
var shaded=new VoxelSculpture(flat.modelId(),flat.name(),16,16,16,cells.stream().map(v->new VoxelSculpture.Cell(v.x(),v.y(),v.z(),v.x()<8?0xFF000000:0xFFFFFFFF)).toList());
|
||||
var result=palette.apply(shaded);
|
||||
check(result.cells().stream().filter(v->v.x()<8).allMatch(v->SculpturePalette.lightness(v.color())<.5),"Dark regions sample dark shades");
|
||||
check(result.cells().stream().filter(v->v.x()>=8).allMatch(v->SculpturePalette.lightness(v.color())>=.5),"Light regions sample light shades");
|
||||
check(result.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>200,"Grain remains inside source shading regions");
|
||||
var single=new SculpturePalette(List.of(0xFF123456));check(single.apply(flat).cells().stream().allMatch(v->v.color()==0xFF123456),"Truly uniform block textures remain uniform");
|
||||
for(var item:List.of(Items.STONE,Items.OAK_PLANKS,Items.COAL_ORE,Items.OAK_LOG)){
|
||||
var sampled=SculptureMaterialSampler.sample(new ItemStack(item));var sculpture=sampled.apply(flat);
|
||||
check(sculpture.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>2,"Actual texture grain on flat model: "+item);
|
||||
check(sculpture.cells().stream().allMatch(v->sampled.colors().contains(v.color())),"Exact sampled pixels: "+item);
|
||||
}
|
||||
// Log tops and bark are separate native face materials; both must supply exact colors.
|
||||
var mc=Minecraft.getInstance();var state=Blocks.OAK_LOG.defaultBlockState();var parts=new ArrayList<BlockStateModelPart>();
|
||||
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(state.getSeed(BlockPos.ZERO)),parts);
|
||||
var bySprite=new HashMap<String,Set<Integer>>();
|
||||
for(var part:parts)for(var face:Direction.values())for(var quad:part.getQuads(face)){
|
||||
var sample=PlanMaterials.faceSample(state,quad.materialInfo());var set=bySprite.computeIfAbsent(quad.materialInfo().sprite().contents().name().toString(),k->new HashSet<>());
|
||||
for(int pixel:sample.pixels().argb()){int color=ARGB.multiply(pixel,sample.tint());if((color>>>24)>=128)set.add(color|0xFF000000);}
|
||||
}
|
||||
check(bySprite.size()>=2,"Native log has distinct face textures");var sampled=SculptureMaterialSampler.sample(new ItemStack(Items.OAK_LOG));
|
||||
var union=new HashSet<Integer>();bySprite.values().forEach(union::addAll);
|
||||
check(sampled.colors().stream().allMatch(union::contains),"All palette colors belong to baked face textures");
|
||||
for(var faceColors:bySprite.values())check(sampled.colors().stream().anyMatch(faceColors::contains),"Each log texture contributes pixels");
|
||||
for(int i=0;i<100;i++)palette.apply(flat);
|
||||
long[] times=new long[101];for(int i=0;i<times.length;i++){long start=System.nanoTime();var measured=palette.apply(flat);times[i]=System.nanoTime()-start;check(measured.cells().size()==4096,"Full bounded model");}
|
||||
Arrays.sort(times);
|
||||
System.out.printf(java.util.Locale.ROOT,"SCULPTURE117_PERF 4096 voxels median %.3f ms, p95 %.3f ms%n",times[50]/1e6,times[95]/1e6);
|
||||
System.out.println("SCULPTURE117_GRAIN_PASS flat model, weighted exact pixels, dark/light regions, deterministic cells, persistence and native face materials");
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Actual accessory-slot transfers and recovery from a saved beta.116 head session. */
|
||||
final class SculptureHat117Checks {
|
||||
static ItemStack exercise(ClientGameTestContext c,TestServerContext server,VoxelSculpture sculpture){
|
||||
var original=StatuaryEntity.item(sculpture);original.set(DataComponents.CUSTOM_NAME,Component.literal("Sculpture chapeau 117"));
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
server.waitFor(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.containerMenu==p.inventoryMenu;},100);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.getInventory().setItem(0,original.copy());p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->ItemStack.matches(m.player.inventoryMenu.getSlot(36).getItem(),original),100);
|
||||
c.runOnClient(m->m.gui.setScreen(new InventoryScreen(m.player)));
|
||||
click(c,36);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Statuary.ITEM),100);
|
||||
click(c,66);c.waitTicks(12);verifyEquipped(c,server,original);
|
||||
c.takeScreenshot("sculpture117-hat-inventory");
|
||||
click(c,66);c.waitFor(m->m.player.inventoryMenu.getCarried().is(Statuary.ITEM),100);
|
||||
click(c,36);
|
||||
server.waitFor(s->{var p=s.getPlayerList().getPlayers().getFirst();return p.inventoryMenu.getCarried().isEmpty()&&HeadEquipment.get(p).isEmpty()&&ItemStack.matches(p.getInventory().getItem(0),original);},100);
|
||||
c.waitFor(m->ItemStack.matches(m.player.inventoryMenu.getSlot(36).getItem(),original)&&m.player.inventoryMenu.getSlot(66).getItem().isEmpty(),100);
|
||||
c.runOnClient(m->check(!new StatuaryRenderer.ItemRenderer().extractArgument(m.player.inventoryMenu.getSlot(36).getItem()).isEmpty(),"Returned sculpture still renders in hotbar"));
|
||||
click(c,36);click(c,66);c.waitTicks(8);verifyEquipped(c,server,original);
|
||||
server.runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var saved=HeadBlockService.save(p);check(saved!=null,"Authoritative worn model is saved");
|
||||
var damaged=original.copy();damaged.remove(DataComponents.BLOCK_ENTITY_DATA);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,damaged);HeadEquipment.retainData(p,saved);
|
||||
HeadBlockService.session(p);p.inventoryMenu.broadcastChanges();
|
||||
check(ItemStack.matches(HeadEquipment.get(p),original),"Recover already stripped sculpture from its saved worn model");
|
||||
});
|
||||
c.waitTicks(8);verifyEquipped(c,server,original);
|
||||
c.runOnClient(m->m.gui.screen().onClose());
|
||||
System.out.println("SCULPTURE117_HAT_PASS native equip/remove/re-equip, slot mesh, exact components and old worn-session recovery");
|
||||
return original;
|
||||
}
|
||||
static void verifyEquipped(ClientGameTestContext c,TestServerContext server,ItemStack original){
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(ItemStack.matches(HeadEquipment.get(p),original),"Equipped stack retains its geometry and name");check(p.getInventory().getItem(0).isEmpty()&&p.inventoryMenu.getCarried().isEmpty(),"Exactly one sculpture moved into head slot");});
|
||||
c.waitFor(m->ItemStack.matches(m.player.inventoryMenu.getSlot(66).getItem(),original)&&StatuaryEntity.sculpture(original).equals(StatuaryEntity.sculpture(HeadCosmeticService.appearance(m.player))),150);
|
||||
c.runOnClient(m->check(!new StatuaryRenderer.ItemRenderer().extractArgument(m.player.inventoryMenu.getSlot(66).getItem()).isEmpty(),"Equipped slot retains visible item geometry"));
|
||||
}
|
||||
private static void click(ClientGameTestContext c,int slot){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.inventoryMenu.containerId,slot,0,ContainerInput.PICKUP,m.player));}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+17
-3
@@ -119,7 +119,14 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
for(var direction:Direction.Plane.HORIZONTAL){
|
||||
var pos=DIRECTIONS.get(direction);double px=pos.getX()+.5-direction.getStepX()*2,pz=pos.getZ()+.5-direction.getStepZ()*2;
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(px,1,pz);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos,direction.getOpposite());
|
||||
c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(pos);check(state.getValue(Statuary.FACING)==direction&&state.getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Native center placement follows gaze on server");});
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture())&&e.getBlockState().getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,100);
|
||||
c.runOnClient(m->Sculpture113ClientChecks.rendered((StatuaryEntity)m.level.getBlockEntity(pos)));
|
||||
c.takeScreenshot("sculpture119-center-"+direction.getSerializedName());
|
||||
server.runOnServer(s->{var p=player(s);p.level().removeBlock(pos,false);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.level.getBlockState(pos).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,pos,direction.getOpposite());
|
||||
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(pos);check(state.getValue(Statuary.FACING)==direction,"Native placement faces gaze: "+direction);
|
||||
check(state.getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Aimed edge stored on server");
|
||||
@@ -189,12 +196,14 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Anchor survives server save/reload"));
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&ORIGINAL.get().equals(e.sculpture())&&e.getBlockState().getValue(Statuary.FACING)==direction,150);
|
||||
}
|
||||
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file"));
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(SCULPTURE).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Centered placement survives server save/reload"));
|
||||
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->{check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file");check(m.level.getBlockState(SCULPTURE).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Fresh client receives centered anchor");});
|
||||
c.getInput().lookAt(SCULPTURE);c.takeScreenshot("statuary106-reload-no-source");
|
||||
}
|
||||
System.out.println("STATUARY106_PASS GLB textured scene, bounded import, shared folder filters, native paid crafting, 16 clay colors, model ID, shift-click, place/drop/re-place, Metabli plan, cancellation, server save/reconnect without source");
|
||||
System.out.println("STATUARY111_PASS nine-slice GUI FR/EN scales 2/3, four native gaze placements, renderer directions, structure rotation/mirror, legacy state default, drop/re-place and persisted orientation");
|
||||
System.out.println("STATUARY113_PASS clay sculpture FR/EN, native aimed edges, voxel grid across every footprint/orientation/anchor, side placement, legacy states, rotated/mirrored anchor, save/reconnect");
|
||||
System.out.println("SCULPTURE119_PASS native centered placements in four directions, center/edge boundaries, odd/even voxel grid, side anchoring and center save/reconnect");
|
||||
System.out.println("STATUARY112_PASS fixed compact hotbar-only menu, native input transfer, hidden slots/offhand rejected, full hotbar no consumption, partial merge exact cost, closing restitution");
|
||||
}catch(IOException e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(IOException ignored){}}
|
||||
}
|
||||
@@ -228,13 +237,18 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
/** A tiny authored cat built from triangle boxes, embedded 2×2 texture and a translated scene node. */
|
||||
static byte[] fixture()throws IOException{
|
||||
return fixture(false);
|
||||
}
|
||||
static byte[] fixture(boolean uniform)throws IOException{
|
||||
var xyz=new ArrayList<Float>();var uv=new ArrayList<Float>();
|
||||
for(float[] box:List.of(new float[]{.2f,0,.1f,.8f,1,.65f},new float[]{.1f,1,.05f,.9f,1.7f,.7f},new float[]{.1f,1.7f,.1f,.3f,2.1f,.5f},new float[]{.7f,1.7f,.1f,.9f,2.1f,.5f},new float[]{.75f,.1f,.2f,1.1f,.65f,.5f})){
|
||||
float x=box[0],y=box[1],z=box[2],X=box[3],Y=box[4],Z=box[5];float[][] v={{x,y,z},{X,y,z},{X,Y,z},{x,Y,z},{x,y,Z},{X,y,Z},{X,Y,Z},{x,Y,Z}};
|
||||
int[][] quads={{0,3,2,1},{5,6,7,4},{4,7,3,0},{1,2,6,5},{3,7,6,2},{4,0,1,5}};
|
||||
for(var q:quads)for(int i:new int[]{0,1,2,0,2,3}){for(float f:v[q[i]])xyz.add(f);uv.add(i==0||i==1?0f:1f);uv.add(i==0||i==3?1f:0f);}
|
||||
}
|
||||
var image=new java.awt.image.BufferedImage(2,2,java.awt.image.BufferedImage.TYPE_INT_ARGB);image.setRGB(0,0,0xFFFF9933);image.setRGB(1,0,0xFF44CCAA);image.setRGB(0,1,0xFF4477EE);image.setRGB(1,1,0xFFFFFFEE);var png=new ByteArrayOutputStream();javax.imageio.ImageIO.write(image,"png",png);
|
||||
var image=new java.awt.image.BufferedImage(2,2,java.awt.image.BufferedImage.TYPE_INT_ARGB);image.setRGB(0,0,0xFFFF9933);image.setRGB(1,0,0xFF44CCAA);image.setRGB(0,1,0xFF4477EE);image.setRGB(1,1,0xFFFFFFEE);
|
||||
if(uniform)for(int y=0;y<2;y++)for(int x=0;x<2;x++)image.setRGB(x,y,0xFF808080);
|
||||
var png=new ByteArrayOutputStream();javax.imageio.ImageIO.write(image,"png",png);
|
||||
int pbytes=xyz.size()*4,ubytes=uv.size()*4,ibytes=png.size(),total=pbytes+ubytes+ibytes;var binary=ByteBuffer.allocate((total+3)/4*4).order(ByteOrder.LITTLE_ENDIAN);for(float f:xyz)binary.putFloat(f);for(float f:uv)binary.putFloat(f);binary.put(png.toByteArray());
|
||||
String json="""
|
||||
{"asset":{"version":"2.0"},"scene":0,"scenes":[{"nodes":[0]}],"nodes":[{"mesh":0,"translation":[3,2,-1]}],
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.building.ColoredBricks;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.tags.*;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.item.crafting.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.state.properties.WallSide;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Gameplay regressions using native placement, neighbor updates and recipe matching. */
|
||||
final class Walls118Checks {
|
||||
static void server(MinecraftServer server){
|
||||
var player=server.getPlayerList().getPlayers().getFirst();var level=player.level();
|
||||
for(int x=-1;x<=17;x++)for(int z=9;z<=13;z++)level.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
int index=0;
|
||||
for(var family:ColoredBricks.families()){
|
||||
var wall=family.wall();var state=wall.defaultBlockState();
|
||||
check(wall instanceof WallBlock&&state.is(BlockTags.WALLS)&&state.is(BlockTags.MINEABLE_WITH_PICKAXE),"Native wall type, connection and mining tags");
|
||||
check(new ItemStack(wall).is(ItemTags.WALLS),"Native wall item tag");
|
||||
var input=CraftingInput.of(3,2,Collections.nCopies(6,new ItemStack(family.bricks())));
|
||||
var recipe=server.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,level).orElseThrow().value();var result=recipe.assemble(input);
|
||||
check(result.is(wall.asItem())&&result.getCount()==6,"Six matching bricks craft six walls");
|
||||
var drops=Block.getDrops(state,level,new BlockPos(0,2,0),null,player,new ItemStack(Items.DIAMOND_PICKAXE));
|
||||
check(drops.size()==1&&drops.getFirst().is(wall.asItem())&&drops.getFirst().getCount()==1,"Wall drops its own color");
|
||||
for(int z:new int[]{10,12}){
|
||||
var pos=new BlockPos(index,1,z);
|
||||
player.teleportTo(index+.5,1,z+2.5);
|
||||
if(z==12)level.setBlockAndUpdate(pos,Blocks.WATER.defaultBlockState());
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(wall));
|
||||
var hit=new BlockHitResult(Vec3.atBottomCenterOf(pos),Direction.UP,pos.below(),false);
|
||||
var placed=player.getMainHandItem().useOn(new UseOnContext(player,InteractionHand.MAIN_HAND,hit));
|
||||
check(placed.consumesAction(),"Native wall item placement: "+family.color()+" at "+pos+" = "+placed+", existing "+level.getBlockState(pos));
|
||||
check(level.getBlockState(pos).is(wall),"Placed wall keeps its color");
|
||||
check(level.getBlockState(pos).getValue(WallBlock.WATERLOGGED)==(z==12),"Native waterlogging on placement");
|
||||
}
|
||||
index++;
|
||||
}
|
||||
for(int i=1;i<15;i++){
|
||||
var pos=new BlockPos(i,1,10);var state=level.getBlockState(pos);
|
||||
check(state.getValue(WallBlock.EAST)==WallSide.LOW&&state.getValue(WallBlock.WEST)==WallSide.LOW&&!state.getValue(WallBlock.UP),"Mixed-color straight wall removes middle post");
|
||||
level.setBlockAndUpdate(pos.above(),Blocks.STONE.defaultBlockState());
|
||||
state=level.getBlockState(pos);check(state.getValue(WallBlock.EAST)==WallSide.TALL&&state.getValue(WallBlock.WEST)==WallSide.TALL,"Full block above raises connected sides");
|
||||
var vanilla=Blocks.BRICK_WALL.withPropertiesOf(state);
|
||||
check(state.getCollisionShape(level,pos).toAabbs().equals(vanilla.getCollisionShape(level,pos).toAabbs()),"Collision equals native brick wall");
|
||||
level.removeBlock(pos.above(),false);
|
||||
}
|
||||
var end=new BlockPos(16,1,10);level.setBlockAndUpdate(end,Blocks.BRICK_WALL.defaultBlockState());
|
||||
check(level.getBlockState(end.west()).getValue(WallBlock.EAST)==WallSide.LOW,"Connects to vanilla brick wall");
|
||||
saved(level);
|
||||
}
|
||||
static void creative(List<ItemStack> display){
|
||||
var colors=List.of(DyeColor.WHITE,DyeColor.LIGHT_GRAY,DyeColor.GRAY,DyeColor.BLACK,DyeColor.BROWN,DyeColor.RED,DyeColor.ORANGE,DyeColor.YELLOW,DyeColor.LIME,DyeColor.GREEN,DyeColor.CYAN,DyeColor.LIGHT_BLUE,DyeColor.BLUE,DyeColor.PURPLE,DyeColor.MAGENTA,DyeColor.PINK);
|
||||
var families=ColoredBricks.families().stream().sorted(Comparator.comparingInt(f->colors.indexOf(f.color()))).toList();
|
||||
int start=-1;for(int i=0;i<display.size();i++)if(display.get(i).is(families.getFirst().wall().asItem())){start=i;break;}
|
||||
check(start>0,"Wall series is visible in Colored Blocks");
|
||||
for(int i=0;i<16;i++)check(display.get(start+i).is(families.get(i).wall().asItem()),"Contiguous series of sixteen in native color order");
|
||||
check(display.get(start-1).is(families.getLast().slab().asItem())&&display.get(start+16).is(families.getFirst().clay().asItem()),"Wall series follows slabs and precedes clay");
|
||||
}
|
||||
static void saved(ServerLevel level){
|
||||
int i=0;for(var family:ColoredBricks.families()){
|
||||
check(level.getBlockState(new BlockPos(i,1,10)).is(family.wall()),"Persisted wall color");
|
||||
var wet=level.getBlockState(new BlockPos(i++,1,12));check(wet.is(family.wall())&&wet.getValue(WallBlock.WATERLOGGED)&&wet.getFluidState().isSource(),"Persisted waterlogged wall");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import 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.client.renderer.v1.Renderer;
|
||||
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.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
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.*;
|
||||
|
||||
/** Real client gestures and persisted server state; rendered UVs must change without moving geometry. */
|
||||
public final class Wrench120ClientChecks 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(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
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();
|
||||
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()){
|
||||
var before=block.defaultBlockState();var after=GoldenWrench.cycle(before,property,false);
|
||||
check(after.is(block)&&GoldenWrench.cycle(after,property,true)==before,"Every registered native property has reversible values");tested++;
|
||||
}
|
||||
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);
|
||||
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");});
|
||||
click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
|
||||
click(c,TARGET,true,false);server.runOnServer(s->check(GoldenWrench.selected(player(s),stairs).equals("half"),"Left click selects half without breaking creative block: "+GoldenWrench.selected(player(s),stairs)));
|
||||
click(c,TARGET,false,false);server.waitFor(s->player(s).level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,100);
|
||||
click(c,TARGET,true,false);server.runOnServer(s->check(GoldenWrench.selected(player(s),stairs).equals("shape"),"Next property is stair shape"));
|
||||
click(c,TARGET,false,false);server.waitFor(s->player(s).level().getBlockState(TARGET).getValue(StairBlock.SHAPE)!=StairsShape.STRAIGHT,100);
|
||||
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);
|
||||
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");
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(DIRT)==Blocks.DIRT.defaultBlockState(),"Dirt type and native state unchanged"));
|
||||
click(c,DIRT,true,false);server.runOnServer(s->check(player(s).level().getBlockState(DIRT).is(Blocks.DIRT),"Left click never breaks dirt in survival"));
|
||||
click(c,DIRT,false,true);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==0,100);checkGeometry(original,c.computeOnClient(m->geometry(m,DIRT)),"Reverse restores exact UVs");
|
||||
for(int i=1;i<=4;i++){click(c,DIRT,false,false);int expected=i%4;c.waitFor(m->WrenchTextureModels.rotation(DIRT)==expected,100);}
|
||||
checkGeometry(original,c.computeOnClient(m->geometry(m,DIRT)),"Four turns restore exact original geometry and UVs");
|
||||
click(c,DIRT,false,false);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
var reload=c.computeOnClient(Minecraft::reloadResourcePacks);c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();checkGeometry(rotated,c.computeOnClient(m->geometry(m,DIRT)),"Resource reload retains texture rotation");
|
||||
// Replacing a block clears the visual receipt; blockstate edits retain it.
|
||||
server.runOnServer(s->{var l=player(s).level();var q=new BlockPos(-3,1,0);l.setBlockAndUpdate(q,Blocks.OAK_LOG.defaultBlockState());WrenchTextures.turn(l,q,1);l.setBlock(q,Blocks.OAK_LOG.defaultBlockState().setValue(RotatedPillarBlock.AXIS,Direction.Axis.X),18);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"State edit preserves texture rotation");l.removeBlock(q,false);l.setBlockAndUpdate(q,Blocks.OAK_LOG.defaultBlockState());check(WrenchTextures.rotation(l.getChunkAt(q),q)==0,"Break/replacement clears rotation");
|
||||
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);});
|
||||
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);
|
||||
click(c,table.offset(1,0,2),true,false);server.runOnServer(s->check(Metatable.state(player(s).level(),table,true)==1,"Left click cannot corrupt a multiblock's part states"));
|
||||
click(c,table.offset(1,0,2),false,true);server.waitFor(s->Metatable.state(player(s).level(),table,false)==1,100);
|
||||
// Both container assemblers and the piston assembler must win over generic state editing.
|
||||
for(var component:List.of(Blocks.BARREL,Blocks.FURNACE)){
|
||||
server.runOnServer(s->{var l=player(s).level();for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++)l.setBlockAndUpdate(table.offset(x,y,z),component.defaultBlockState());});
|
||||
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(component),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.runOnServer(s->check(MultiblockData.get(player(s).level()).at(table)!=null,"Complete container machine assembles before generic rotation"));
|
||||
click(c,table.offset(1,0,2),false,true);
|
||||
server.runOnServer(s->check(MultiblockData.get(player(s).level()).at(table)==null,"Container machine still separates"));
|
||||
}
|
||||
server.runOnServer(s->{var l=player(s).level();for(int y=2;y>=0;y--)for(int z=0;z<3;z++)for(int x=0;x<3;x++)l.setBlockAndUpdate(table.offset(x,y,z),y==0?Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.FACING,Direction.UP):Blocks.AIR.defaultBlockState());});
|
||||
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(Blocks.PISTON),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.waitFor(s->SuperPistons.status(player(s).level(),table.offset(1,0,1),Direction.UP,false)==1,100);
|
||||
c.waitFor(m->m.level.getBlockState(table.offset(1,0,2)).is(SuperPistons.NORMAL),100);
|
||||
click(c,table.offset(1,0,2),false,true);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(table.offset(1,0,2)).is(Blocks.PISTON),"Super piston still separates before editing its parts"));
|
||||
// An isolated barrel and piston now use generic states instead of an incomplete-structure error.
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,3.2);p.level().setBlockAndUpdate(TARGET,Blocks.BARREL.defaultBlockState());});c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.BARREL),100);
|
||||
click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(BarrelBlock.FACING)!=Blocks.BARREL.defaultBlockState().getValue(BarrelBlock.FACING),"Isolated barrel rotates"));
|
||||
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET,Blocks.PISTON.defaultBlockState()));c.waitFor(m->m.level.getBlockState(TARGET).is(Blocks.PISTON),100);
|
||||
click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(PistonBaseBlock.FACING)!=Blocks.PISTON.defaultBlockState().getValue(PistonBaseBlock.FACING),"Isolated piston rotates"));
|
||||
server.runOnServer(s->player(s).level().setBlock(TARGET,stairs.setValue(StairBlock.HALF,Half.TOP),18));saved=world.getWorldSave();
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);world.getServer().runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==1,"Server persisted dirt texture");check(l.getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Server persisted native edited state");check(((ChestBlockEntity)l.getBlockEntity(CHEST)).getItem(0).getCount()==23,"Saved chest keeps inventory");});
|
||||
c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,150);c.getInput().lookAt(DIRT);c.takeScreenshot("wrench120-reconnect");
|
||||
}
|
||||
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){
|
||||
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));
|
||||
if(left){c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);}else{c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);}
|
||||
c.waitTicks(5);if(sneak){c.getInput().releaseKey(o->o.keyShift);c.waitTicks(3);}
|
||||
}
|
||||
private static List<float[]> geometry(Minecraft m,BlockPos pos){
|
||||
var result=new ArrayList<float[]>();var state=m.level.getBlockState(pos);
|
||||
var emitter=Renderer.get().quadEmitter(q->{var data=new float[20];for(int i=0;i<4;i++){data[i*5]=q.x(i);data[i*5+1]=q.y(i);data[i*5+2]=q.z(i);data[i*5+3]=q.u(i);data[i*5+4]=q.v(i);}result.add(data);});
|
||||
m.getModelManager().getBlockStateModelSet().get(state).emitQuads(emitter,m.level,pos,state,RandomSource.create(state.getSeed(pos)),face->false);check(!result.isEmpty(),"Native model emits real textured geometry");return result;
|
||||
}
|
||||
private static void checkUvs(List<float[]> before,List<float[]> after){
|
||||
check(before.size()==after.size(),"Same face count");boolean changed=false;
|
||||
for(int q=0;q<before.size();q++)for(int v=0;v<4;v++){for(int axis=0;axis<3;axis++)check(before.get(q)[v*5+axis]==after.get(q)[v*5+axis],"Texture turn never moves geometry");changed|=before.get(q)[v*5+3]!=after.get(q)[v*5+3]||before.get(q)[v*5+4]!=after.get(q)[v*5+4];}
|
||||
check(changed,"Dirt UVs actually change");
|
||||
}
|
||||
private static void checkGeometry(List<float[]> a,List<float[]> b,String message){check(a.size()==b.size(),message);for(int i=0;i<a.size();i++)check(Arrays.equals(a.get(i),b.get(i)),message);}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
|
||||
/** Native building blocks; no world-generation, ticking entities or custom inventory. */
|
||||
public final class ColoredBricks {
|
||||
public record Family(DyeColor color,Block bricks,Block slab,Block stairs,Block clay,Item clayBall,Item brick){}
|
||||
public record Family(DyeColor color,Block bricks,Block slab,Block stairs,Block wall,Block clay,Item clayBall,Item brick){}
|
||||
private static final List<Family> FAMILIES=new ArrayList<>();
|
||||
// The order used by the native 26.3 Colored Blocks tab, rather than dye numeric IDs.
|
||||
private static final List<DyeColor> CREATIVE_COLORS=List.of(
|
||||
@@ -27,15 +27,17 @@ public final class ColoredBricks {
|
||||
var bricks=block(name+"_bricks",new Block(properties(name+"_bricks",Blocks.BRICKS,color)));
|
||||
var slab=block(name+"_brick_slab",new SlabBlock(properties(name+"_brick_slab",Blocks.BRICK_SLAB,color)));
|
||||
var stairs=block(name+"_brick_stairs",new Stairs(bricks,properties(name+"_brick_stairs",Blocks.BRICK_STAIRS,color)));
|
||||
var wall=block(name+"_brick_wall",new WallBlock(properties(name+"_brick_wall",Blocks.BRICK_WALL,color)));
|
||||
var clay=block(name+"_clay",new Block(properties(name+"_clay",Blocks.CLAY,color)));
|
||||
var clayBall=item(name+"_clay_ball");var brick=item(name+"_brick");
|
||||
FAMILIES.add(new Family(color,bricks,slab,stairs,clay,clayBall,brick));
|
||||
FAMILIES.add(new Family(color,bricks,slab,stairs,wall,clay,clayBall,brick));
|
||||
}
|
||||
var display=FAMILIES.stream().sorted(Comparator.comparingInt(f->CREATIVE_COLORS.indexOf(f.color()))).toList();
|
||||
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.COLORED_BLOCKS).register(out->{
|
||||
for(var f:display)out.accept(f.bricks);
|
||||
for(var f:display)out.accept(f.stairs);
|
||||
for(var f:display)out.accept(f.slab);
|
||||
for(var f:display)out.accept(f.wall);
|
||||
for(var f:display)out.accept(f.clay);
|
||||
});
|
||||
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.INGREDIENTS).register(out->{
|
||||
|
||||
@@ -61,9 +61,10 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
@Override protected void containerTick(){
|
||||
super.containerTick();rotate.active=source!=null&&!busy;
|
||||
var material=menu.getSlot(0).getItem();var resources=minecraft.getModelManager().getBlockStateModelSet();
|
||||
if(previewSource!=source||!ItemStack.isSameItemSameComponents(lastMaterial,material)||sampledResources!=resources){
|
||||
boolean changedMaterial=!ItemStack.isSameItemSameComponents(lastMaterial,material)||sampledResources!=resources;
|
||||
if(previewSource!=source||changedMaterial){
|
||||
previewSource=source;lastMaterial=material.copy();sampledResources=resources;
|
||||
palette=SculptureMaterialSampler.sample(material);
|
||||
if(changedMaterial)palette=SculptureMaterialSampler.sample(material);
|
||||
var colored=source==null?null:palette==null?source:palette.apply(source);
|
||||
preview=colored==null?ItemStack.EMPTY:StatuaryEntity.item(colored);pending=true;
|
||||
if(source!=null&&!busy)status=palette==null?text("choose_material"):text("material_ready",material.getHoverName());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
FoodKnowledgeClient.register();
|
||||
MultiblockScreen.register();
|
||||
FourneauModels.register();
|
||||
WrenchTextureModels.register();
|
||||
MetatableClient.register();
|
||||
ClayWorkshopScreen.register();
|
||||
GraveScreen.register();
|
||||
|
||||
+27
-6
@@ -2,25 +2,46 @@ package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart;
|
||||
import net.minecraft.client.resources.model.geometry.BakedQuad;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.ARGB;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.*;
|
||||
|
||||
/** Representative texture pixels from the active pack, including biome tint and animated first frame. */
|
||||
/** Bounded samples of the baked faces' actual textures, with their own tint from the active pack. */
|
||||
final class SculptureMaterialSampler {
|
||||
private static final int MAX_MATERIALS=32,SAMPLE_EDGE=16;
|
||||
static SculpturePalette sample(ItemStack stack){
|
||||
if(!ClayWorkshopMenu.material(stack))return null;
|
||||
var state=((BlockItem)stack.getItem()).getBlock().defaultBlockState();var sample=PlanMaterials.sample(state);
|
||||
var mc=net.minecraft.client.Minecraft.getInstance();var tints=mc.getBlockColors().getTintSources(state);
|
||||
int tint=tints.isEmpty()?0xFFFFFFFF:tints.getFirst().colorInWorld(state,mc.level,mc.player.blockPosition());
|
||||
var mc=net.minecraft.client.Minecraft.getInstance();
|
||||
var parts=new ArrayList<BlockStateModelPart>();
|
||||
mc.getModelManager().getBlockStateModelSet().get(state).collectParts(RandomSource.create(state.getSeed(BlockPos.ZERO)),parts);
|
||||
var materials=new LinkedHashSet<BakedQuad.MaterialInfo>();
|
||||
for(var part:parts)for(int face=0;face<=6;face++)for(var quad:part.getQuads(face==6?null:Direction.values()[face])){
|
||||
if(materials.size()<MAX_MATERIALS)materials.add(quad.materialInfo());
|
||||
}
|
||||
var pixels=new ArrayList<Integer>();
|
||||
for(int pixel:sample.pixels().argb()){
|
||||
int color=ARGB.multiply(pixel,tint);
|
||||
if((color>>>24)>=128)pixels.add(color|0xFF000000);
|
||||
for(var material:materials)add(pixels,PlanMaterials.faceSample(state,material));
|
||||
if(pixels.isEmpty()){
|
||||
var tints=mc.getBlockColors().getTintSources(state);
|
||||
int tint=tints.isEmpty()?0xFFFFFFFF:tints.getFirst().colorInWorld(state,mc.level,mc.player.blockPosition());
|
||||
add(pixels,new PlanMaterials.Sample(sample.pixels(),tint,sample.average()));
|
||||
}
|
||||
if(pixels.isEmpty())pixels.add(sample.average()|0xFF000000);
|
||||
// Keep repeated pixels: they retain the sampled frequency of each shade.
|
||||
pixels.sort(Comparator.comparingDouble(SculpturePalette::lightness).thenComparingInt(Integer::intValue));
|
||||
var palette=new ArrayList<Integer>();int count=Math.min(SculpturePalette.MAX_COLORS,pixels.size());
|
||||
for(int i=0;i<count;i++)palette.add(pixels.get((int)((i+.5)*pixels.size()/count)));
|
||||
return new SculpturePalette(palette);
|
||||
}
|
||||
private static void add(List<Integer> pixels,PlanMaterials.Sample sample){
|
||||
var texture=sample.pixels();int width=Math.min(SAMPLE_EDGE,texture.width()),height=Math.min(SAMPLE_EDGE,texture.height());
|
||||
for(int y=0;y<height;y++)for(int x=0;x<width;x++){
|
||||
int sx=(2*x+1)*texture.width()/(2*width),sy=(2*y+1)*texture.height()/(2*height);
|
||||
int color=ARGB.multiply(texture.argb()[sy*texture.width()+sx],sample.tint());
|
||||
if((color>>>24)>=128)pixels.add(color|0xFF000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.*;
|
||||
import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
|
||||
import net.fabricmc.fabric.api.client.model.loading.v1.wrapper.WrapperBlockStateModel;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.*;
|
||||
import net.fabricmc.fabric.api.client.renderer.v1.mesh.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.block.BlockAndTintGetter;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Immutable visual snapshots, usable from mesh workers and rebuilt after resource reloads. */
|
||||
public final class WrenchTextureModels {
|
||||
private static volatile Map<Long,Map<BlockPos,Integer>> chunks=Map.of();
|
||||
public static int rotation(BlockPos pos){return chunks.getOrDefault(ChunkPos.pack(pos),Map.of()).getOrDefault(pos,0);}
|
||||
public static void register(){
|
||||
ClientPlayNetworking.registerGlobalReceiver(WrenchTextures.Update.TYPE,(packet,context)->context.client().execute(()->apply(packet)));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((handler,client)->chunks=Map.of());
|
||||
ClientLevelEvents.AFTER_CLIENT_LEVEL_CHANGE.register((client,level)->chunks=Map.of());
|
||||
ClientChunkEvents.CHUNK_UNLOAD.register((level,chunk)->{var next=new HashMap<>(chunks);next.remove(chunk.getPos().pack());chunks=Map.copyOf(next);});
|
||||
ModelLoadingPlugin.register(context->context.modifyBlockModelAfterBake().register((model,bake)->new Rotated(model)));
|
||||
}
|
||||
private static void apply(WrenchTextures.Update packet){
|
||||
var client=Minecraft.getInstance();if(client.level==null||!client.level.dimension().identifier().equals(packet.dimension()))return;
|
||||
long key=packet.chunk().pack();var old=chunks.getOrDefault(key,Map.of());var dirty=new HashSet<BlockPos>();
|
||||
var entries=packet.clear()?new HashMap<BlockPos,Integer>():new HashMap<>(old);if(packet.clear())dirty.addAll(old.keySet());
|
||||
for(var entry:packet.entries()){
|
||||
var before=entry.rotation()==0?entries.remove(entry.pos()):entries.put(entry.pos(),entry.rotation());
|
||||
if(!Objects.equals(before,entry.rotation()))dirty.add(entry.pos());
|
||||
}
|
||||
var next=new HashMap<>(chunks);if(entries.isEmpty())next.remove(key);else next.put(key,Map.copyOf(entries));chunks=Map.copyOf(next);
|
||||
var sections=new HashSet<SectionPos>();for(var pos:dirty)sections.add(SectionPos.of(pos));
|
||||
for(var section:sections)client.levelExtractor.setSectionDirty(section.x(),section.y(),section.z());
|
||||
}
|
||||
static boolean rotate(MutableQuadView quad,int turns){
|
||||
// Rotate texture coordinates only: geometry, light, tints and materials stay native.
|
||||
for(int turn=0;turn<turns;turn++){
|
||||
float u=quad.u(0),v=quad.v(0);
|
||||
for(int vertex=0;vertex<3;vertex++)quad.uv(vertex,quad.u(vertex+1),quad.v(vertex+1));
|
||||
quad.uv(3,u,v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private record Geometry(Object original,int rotation){}
|
||||
private static final class Rotated extends WrapperBlockStateModel {
|
||||
Rotated(BlockStateModel original){super(original);}
|
||||
@Override public void emitQuads(QuadEmitter emitter,BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random,Predicate<Direction> cull){
|
||||
int turns=rotation(pos);if(turns==0){wrapped.emitQuads(emitter,view,pos,state,random,cull);return;}
|
||||
emitter.pushTransform(quad->rotate(quad,turns));
|
||||
try{wrapped.emitQuads(emitter,view,pos,state,random,cull);}finally{emitter.popTransform();}
|
||||
}
|
||||
@Override public Object createGeometryKey(BlockAndTintGetter view,BlockPos pos,BlockState state,RandomSource random){
|
||||
var key=wrapped.createGeometryKey(view,pos,state,random);int turns=rotation(pos);
|
||||
return turns==0||key==null?key:new Geometry(key,turns);
|
||||
}
|
||||
}
|
||||
private WrenchTextureModels(){}
|
||||
}
|
||||
@@ -264,8 +264,13 @@ public final class HeadBlockService {
|
||||
var data=stack.get(DataComponents.BLOCK_ENTITY_DATA);
|
||||
if(data!=null)data.loadInto(entity,level().registryAccess());
|
||||
}
|
||||
// Ordinary inventory data belongs to the equipped session, not its transferable item.
|
||||
if(!keepsContents()){stack.remove(DataComponents.CONTAINER);stack.remove(DataComponents.BLOCK_ENTITY_DATA);}
|
||||
// Storage belongs to the equipped session. A sculpture's block-entity data is
|
||||
// its actual geometry, not inventory: keep it on the transferable item.
|
||||
if(!keepsContents()){
|
||||
stack.remove(DataComponents.CONTAINER);
|
||||
if(!(entity instanceof fr.koka.sanctuary.plans.StatuaryEntity))stack.remove(DataComponents.BLOCK_ENTITY_DATA);
|
||||
}
|
||||
restoreSculptureData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,6 +326,15 @@ public final class HeadBlockService {
|
||||
}
|
||||
public void flushItem(){
|
||||
if((keepsContents()||entity instanceof BeehiveBlockEntity)&&entity!=null)stack.applyComponents(entity.collectComponents());
|
||||
restoreSculptureData();
|
||||
}
|
||||
private void restoreSculptureData(){
|
||||
// Older equipped sessions may still hold the model after the stack lost it.
|
||||
// Repair only from that authoritative data, and only once; never invent geometry.
|
||||
if(entity instanceof fr.koka.sanctuary.plans.StatuaryEntity sculpture&&!stack.has(DataComponents.BLOCK_ENTITY_DATA)){
|
||||
var data=sculpture.item().get(DataComponents.BLOCK_ENTITY_DATA);
|
||||
if(data!=null)stack.set(DataComponents.BLOCK_ENTITY_DATA,data);
|
||||
}
|
||||
}
|
||||
public CompoundTag save(){
|
||||
flushItem();var tag=new CompoundTag();tag.putInt("schema",1);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(LevelChunk.class)
|
||||
public abstract class WrenchTextureEditMixin {
|
||||
@Inject(method="setBlockState(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Lnet/minecraft/world/level/block/state/BlockState;",at=@At("RETURN"))
|
||||
private void sanctuary$clearTexture(BlockPos pos,BlockState state,int flags,CallbackInfoReturnable<BlockState> ci){
|
||||
var before=ci.getReturnValue();
|
||||
if(before!=null&&!before.is(state.getBlock())&&((LevelChunk)(Object)this).getLevel() instanceof ServerLevel)
|
||||
WrenchTextures.forget((LevelChunk)(Object)this,pos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.multiblock.WrenchTextures;
|
||||
import net.minecraft.server.network.*;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(PlayerChunkSender.class)
|
||||
public abstract class WrenchTextureSyncMixin {
|
||||
@Inject(method="sendChunk",at=@At("RETURN"))
|
||||
private static void sanctuary$textures(ServerGamePacketListenerImpl connection,ServerLevel level,LevelChunk chunk,CallbackInfo ci){
|
||||
WrenchTextures.sendChunk(connection.player,chunk);
|
||||
}
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package fr.koka.sanctuary.multiblock;
|
||||
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
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. */
|
||||
public final class GoldenWrench {
|
||||
public static final String TEXTURE="sanctuary_texture";
|
||||
private static final Map<ServerPlayer,Map<Block,String>> SELECTED=new WeakHashMap<>();
|
||||
private static final Map<ServerPlayer,Integer> LAST_SELECTION=new WeakHashMap<>();
|
||||
public static final class Tool extends Item {
|
||||
public Tool(Properties properties){super(properties);}
|
||||
@Override public boolean canDestroyBlock(ItemStack stack,BlockState state,Level level,BlockPos pos,LivingEntity entity){return false;}
|
||||
}
|
||||
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(player.isSpectator())return InteractionResult.FAIL;
|
||||
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;select(p,pos);}
|
||||
return InteractionResult.SUCCESS;
|
||||
});
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler,server)->{SELECTED.remove(handler.player);LAST_SELECTION.remove(handler.player);});
|
||||
}
|
||||
public static boolean allowed(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||
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))
|
||||
&&!p.blockActionRestricted(p.level(),pos,p.gameMode.getGameModeForPlayer())
|
||||
&&(!(p.level().getBlockEntity(pos) instanceof BaseContainerBlockEntity container)||container.canOpen(p));
|
||||
}
|
||||
private static boolean assembled(ServerPlayer p,BlockPos pos,BlockState state){
|
||||
return state.is(Metatable.BLOCK)||state.getBlock() instanceof SuperPistonBlocks.Base
|
||||
||state.getBlock() instanceof SuperPistonBlocks.Part||MultiblockData.get(p.level()).at(pos)!=null;
|
||||
}
|
||||
public static List<String> properties(BlockState state){
|
||||
var result=new ArrayList<>(state.getProperties().stream().map(Property::getName).sorted(Comparator.comparingInt(GoldenWrench::priority).thenComparing(s->s)).toList());
|
||||
result.add(TEXTURE);return List.copyOf(result);
|
||||
}
|
||||
private static int priority(String name){return switch(name){case "facing","axis","rotation","orientation"->0;case "half","type","shape"->1;default->2;};}
|
||||
public static String selected(ServerPlayer p,BlockState state){
|
||||
var choices=properties(state);var value=SELECTED.getOrDefault(p,Map.of()).get(state.getBlock());
|
||||
return value!=null&&choices.contains(value)?value:choices.getFirst();
|
||||
}
|
||||
private static void select(ServerPlayer p,BlockPos pos){
|
||||
var state=p.level().getBlockState(pos);if(state.isAir())return;
|
||||
if(assembled(p,pos,state)){p.sendSystemMessage(text("assembled"),true);return;}
|
||||
// Creative start/continue callbacks can describe the same click twice. Match its native 5-tick repeat cadence.
|
||||
int tick=p.level().getServer().getTickCount();var last=LAST_SELECTION.get(p);
|
||||
if(last!=null&&tick-last<5)return;LAST_SELECTION.put(p,tick);
|
||||
var choices=properties(state);var selected=SELECTED.computeIfAbsent(p,key->new HashMap<>());
|
||||
var before=selected.get(state.getBlock());int previous=before==null?-1:choices.indexOf(before);
|
||||
int next=previous<0?0:Math.floorMod(previous+(p.isShiftKeyDown()?-1:1),choices.size());
|
||||
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){
|
||||
var state=p.level().getBlockState(pos);if(state.isAir()||assembled(p,pos,state))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 {
|
||||
var property=state.getBlock().getStateDefinition().getProperty(name);
|
||||
// Same flags as the vanilla debug stick: synchronize, preserve deliberately chosen shapes.
|
||||
p.level().setBlock(pos,cycle(state,property,p.isShiftKeyDown()),Block.UPDATE_CLIENTS|Block.UPDATE_KNOWN_SHAPE);
|
||||
}
|
||||
p.sendSystemMessage(text("changed",label(name),value(p,pos,p.level().getBlockState(pos),name)),true);
|
||||
}
|
||||
public static <T extends Comparable<T>> BlockState cycle(BlockState state,Property<T> property,boolean reverse){
|
||||
var values=new ArrayList<>(property.getPossibleValues());int index=values.indexOf(state.getValue(property));
|
||||
return state.setValue(property,values.get(Math.floorMod(index+(reverse?-1:1),values.size())));
|
||||
}
|
||||
private static Object value(ServerPlayer p,BlockPos pos,BlockState state,String name){
|
||||
return name.equals(TEXTURE)?(WrenchTextures.rotation(p.level().getChunkAt(pos),pos)*90)+"°":propertyValue(state,state.getBlock().getStateDefinition().getProperty(name));
|
||||
}
|
||||
private static <T extends Comparable<T>> String propertyValue(BlockState state,Property<T> property){return property.getName(state.getValue(property));}
|
||||
private static Component label(String name){return name.equals(TEXTURE)?text("texture"):Component.literal(name);}
|
||||
private static Component text(String name,Object...args){return Component.translatable("sanctuary.wrench."+name,args);}
|
||||
private GoldenWrench(){}
|
||||
}
|
||||
@@ -80,10 +80,11 @@ public final class Metatable {
|
||||
if(p.isShiftKeyDown()){dissolve(level,origin);p.sendSystemMessage(Multiblocks.text("separated"),true);}else open(p,origin);
|
||||
return true;
|
||||
}
|
||||
if(!s.is(Blocks.CRAFTING_TABLE))return false;
|
||||
if(!s.is(Blocks.CRAFTING_TABLE)||p.isShiftKeyDown())return false;
|
||||
var candidates=new ArrayList<BlockPos>();
|
||||
for(int z=0;z<3;z++)for(int x=0;x<3;x++){var o=pos.offset(-x,0,-z);if(state(level,o,false)==1)candidates.add(o);}
|
||||
if(candidates.size()!=1){previews.remove(p);p.sendSystemMessage(Multiblocks.text(candidates.isEmpty()?"incomplete":"ambiguous"),true);return true;}
|
||||
if(candidates.isEmpty()){previews.remove(p);return false;}
|
||||
if(candidates.size()!=1){previews.remove(p);p.sendSystemMessage(Multiblocks.text("ambiguous"),true);return true;}
|
||||
var origin=candidates.getFirst();var before=previews.remove(p);
|
||||
if(before!=null&&before.level==level&&before.origin.equals(origin)&&before.until>=level.getGameTime()){
|
||||
p.sendSystemMessage(Multiblocks.text(assemble(p,origin)?"assembled":"unavailable"),true);return true;
|
||||
|
||||
@@ -17,14 +17,13 @@ import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import java.util.*;
|
||||
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 Item(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_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){}
|
||||
@@ -32,6 +31,7 @@ public final class Multiblocks {
|
||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.multiblock."+key,args);}
|
||||
public static void register(){
|
||||
FourneauAppearance.register();
|
||||
GoldenWrench.register();
|
||||
SuperPistons.register();
|
||||
Metatable.register();
|
||||
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->out.accept(WRENCH));
|
||||
@@ -44,7 +44,9 @@ public final class Multiblocks {
|
||||
// Preserve vanilla sneak-placement (for example, attaching a hopper to the facade).
|
||||
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(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection()))useWrench(p,pos,hit.getDirection());return InteractionResult.SUCCESS;}
|
||||
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);
|
||||
return InteractionResult.SUCCESS;}
|
||||
if(ready(level,machine)&&accessible(p,machine))open(p,machine,pos,0,"",0);
|
||||
else p.sendSystemMessage(text("unavailable"),true);
|
||||
return InteractionResult.SUCCESS;
|
||||
@@ -102,31 +104,28 @@ public final class Multiblocks {
|
||||
if(!MultiblockData.get(p.level()).add(m))return false;
|
||||
closeViewers(p.level(),m);FourneauAppearance.changed(p.level(),m,true);return true;
|
||||
}
|
||||
private static void useWrench(ServerPlayer p,BlockPos pos,Direction face){
|
||||
private static boolean useWrench(ServerPlayer p,BlockPos pos,Direction face){
|
||||
var level=p.level();var data=MultiblockData.get(level);var m=data.at(pos);
|
||||
if(m!=null){
|
||||
if(!ready(level,m)||!accessible(p,m)){p.sendSystemMessage(text("unavailable"),true);return;}
|
||||
if(!ready(level,m)||!accessible(p,m)){p.sendSystemMessage(text("unavailable"),true);return true;}
|
||||
if(p.isShiftKeyDown()){dissolve(level,m);p.sendSystemMessage(text("separated"),true);}
|
||||
else open(p,m,pos,0,"",0);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
var state=level.getBlockState(pos);
|
||||
if(!state.is(Blocks.BARREL)&&!state.is(Blocks.FURNACE)){p.sendSystemMessage(text("hint"),true);return;}
|
||||
if(!(level.getBlockEntity(pos) instanceof BaseContainerBlockEntity be)||!be.canOpen(p))return;
|
||||
if(p.isShiftKeyDown()){
|
||||
var property=state.hasProperty(BlockStateProperties.HORIZONTAL_FACING)?BlockStateProperties.HORIZONTAL_FACING:BlockStateProperties.FACING;
|
||||
var current=state.getValue(property);var next=current.getAxis().isVertical()?p.getDirection():current.getClockWise();
|
||||
level.setBlockAndUpdate(pos,state.setValue(property,next));p.sendSystemMessage(text("rotated"),true);return;
|
||||
}
|
||||
if(!state.is(Blocks.BARREL)&&!state.is(Blocks.FURNACE))return false;
|
||||
if(!(level.getBlockEntity(pos) instanceof BaseContainerBlockEntity be)||!be.canOpen(p))return true;
|
||||
if(p.isShiftKeyDown())return false;
|
||||
boolean furnace=state.is(Blocks.FURNACE);var candidates=new ArrayList<Machine>();
|
||||
for(int y=0;y<3;y++)for(int z=0;z<3;z++)for(int x=0;x<3;x++){
|
||||
var candidate=new Machine(UUID.randomUUID(),pos.offset(-x,-y,-z),furnace,p.getDirection().getOpposite());
|
||||
if(state(level,candidate)==1&&candidate.members().stream().noneMatch(q->data.at(q)!=null))candidates.add(candidate);
|
||||
}
|
||||
if(candidates.size()!=1){PREVIEWS.remove(p);p.sendSystemMessage(text(candidates.isEmpty()?"incomplete":"ambiguous"),true);return;}
|
||||
if(candidates.isEmpty()){PREVIEWS.remove(p);return false;}
|
||||
if(candidates.size()!=1){PREVIEWS.remove(p);p.sendSystemMessage(text("ambiguous"),true);return true;}
|
||||
var candidate=candidates.getFirst();var previous=PREVIEWS.remove(p);
|
||||
if(previous!=null&&previous.level()==level&&previous.origin().equals(candidate.origin())&&previous.furnace()==furnace&&previous.until()>=level.getGameTime()){
|
||||
p.sendSystemMessage(text(assemble(p,candidate)?"assembled":"unavailable"),true);return;
|
||||
p.sendSystemMessage(text(assemble(p,candidate)?"assembled":"unavailable"),true);return true;
|
||||
}
|
||||
PREVIEWS.put(p,new Preview(level,candidate.origin(),furnace,level.getGameTime()+200));
|
||||
for(int axis=0;axis<3;axis++)for(int a=0;a<=3;a+=3)for(int b=0;b<=3;b+=3)for(int t=0;t<=6;t++){
|
||||
@@ -134,6 +133,7 @@ public final class Multiblocks {
|
||||
level.sendParticles(p,ParticleTypes.END_ROD,false,false,candidate.origin().getX()+px,candidate.origin().getY()+py,candidate.origin().getZ()+pz,1,0,0,0,0);
|
||||
}
|
||||
p.sendSystemMessage(text("confirm",text(furnace?"furnace":"barrel")),false);
|
||||
return true;
|
||||
}
|
||||
public static void open(ServerPlayer p,Machine m,BlockPos anchor,int page,String query,int section){
|
||||
open(p,m,anchor,page,query,section,new int[0]);
|
||||
|
||||
@@ -79,11 +79,10 @@ public final class SuperPistons {
|
||||
}
|
||||
if(!s.is(Blocks.PISTON)&&!s.is(Blocks.STICKY_PISTON))return false;
|
||||
var f=s.getValue(FACING);boolean sticky=s.is(Blocks.STICKY_PISTON);
|
||||
if(p.isShiftKeyDown()){
|
||||
if(!s.getValue(EXTENDED)&&!l.hasNeighborSignal(clicked))l.setBlockAndUpdate(clicked,s.setValue(FACING,face));return true;
|
||||
}
|
||||
if(p.isShiftKeyDown())return false;
|
||||
var found=new ArrayList<BlockPos>();for(int a=-1;a<=1;a++)for(int b=-1;b<=1;b++){var c=clicked.relative(u(f),a).relative(v(f),b);if(candidates(l,c,f,sticky))found.add(c);}
|
||||
if(found.size()!=1){previews.remove(p);message(p,found.isEmpty()?"incomplete":"ambiguous");return true;}
|
||||
if(found.isEmpty()){previews.remove(p);return false;}
|
||||
if(found.size()!=1){previews.remove(p);message(p,"ambiguous");return true;}
|
||||
var c=found.getFirst();for(var pos:tiles(c,f,0))if(!l.mayInteract(p,pos)||!p.mayUseItemAt(pos,face,p.getMainHandItem())){previews.remove(p);return true;}
|
||||
var old=previews.remove(p);
|
||||
if(old!=null&&old.level()==l&&old.center().equals(c)&&old.facing()==f&&old.sticky()==sticky&&old.until()>=l.getGameTime()){
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package fr.koka.sanctuary.multiblock;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||
import net.fabricmc.fabric.api.networking.v1.*;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.*;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
|
||||
/** Optional per-chunk presentation data. Absent means original vanilla textures. */
|
||||
public final class WrenchTextures {
|
||||
public static final AttachmentType<Map<String,Integer>> ROTATIONS=AttachmentRegistry.createPersistent(
|
||||
SanctuaryMod.id("wrench_textures_v1"),Codec.unboundedMap(Codec.STRING,Codec.intRange(1,3)).xmap(Map::copyOf,map->map));
|
||||
public record Entry(BlockPos pos,int rotation){}
|
||||
public record Update(Identifier dimension,ChunkPos chunk,boolean clear,List<Entry> entries) implements CustomPacketPayload {
|
||||
public Update{entries=List.copyOf(entries);if(entries.size()>1024)throw new IllegalArgumentException("Texture batch too large");}
|
||||
public static final Type<Update> TYPE=new Type<>(SanctuaryMod.id("wrench_textures_v1"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Update> CODEC=StreamCodec.of((b,p)->{
|
||||
b.writeIdentifier(p.dimension);b.writeChunkPos(p.chunk);b.writeBoolean(p.clear);b.writeVarInt(p.entries.size());
|
||||
for(var entry:p.entries){b.writeBlockPos(entry.pos);b.writeByte(entry.rotation);}
|
||||
},b->{
|
||||
var dimension=b.readIdentifier();var chunk=b.readChunkPos();boolean clear=b.readBoolean();int n=b.readVarInt();
|
||||
if(n<0||n>1024)throw new IllegalArgumentException("Texture batch size");var entries=new ArrayList<Entry>(n);
|
||||
for(int i=0;i<n;i++){var pos=b.readBlockPos();int rotation=b.readUnsignedByte();
|
||||
if(rotation>3||ChunkPos.pack(pos)!=chunk.pack())throw new IllegalArgumentException("Invalid texture entry");entries.add(new Entry(pos,rotation));}
|
||||
return new Update(dimension,chunk,clear,entries);
|
||||
});
|
||||
@Override public Type<? extends CustomPacketPayload> type(){return TYPE;}
|
||||
}
|
||||
public static void register(){PayloadTypeRegistry.clientboundPlay().register(Update.TYPE,Update.CODEC);}
|
||||
public static int rotation(LevelChunk chunk,BlockPos pos){var map=chunk.getAttached(ROTATIONS);return map==null?0:map.getOrDefault(Long.toString(pos.asLong()),0);}
|
||||
public static void turn(ServerLevel level,BlockPos pos,int delta){
|
||||
var chunk=level.getChunkAt(pos);set(chunk,pos,Math.floorMod(rotation(chunk,pos)+delta,4));
|
||||
}
|
||||
public static void forget(LevelChunk chunk,BlockPos pos){if(rotation(chunk,pos)!=0)set(chunk,pos,0);}
|
||||
private static void set(LevelChunk chunk,BlockPos pos,int value){
|
||||
if(!(chunk.getLevel() instanceof ServerLevel level))return;
|
||||
var old=chunk.getAttached(ROTATIONS);var next=old==null?new HashMap<String,Integer>():new HashMap<>(old);
|
||||
if(value==0)next.remove(Long.toString(pos.asLong()));else next.put(Long.toString(pos.asLong()),value);
|
||||
if(next.isEmpty())chunk.removeAttached(ROTATIONS);else chunk.setAttached(ROTATIONS,Map.copyOf(next));chunk.markUnsaved();
|
||||
var packet=new Update(level.dimension().identifier(),chunk.getPos(),false,List.of(new Entry(pos.immutable(),value)));
|
||||
for(var player:PlayerLookup.tracking(level,chunk.getPos()))send(player,packet);
|
||||
}
|
||||
public static void sendChunk(ServerPlayer player,LevelChunk chunk){
|
||||
var entries=chunk.getAttached(ROTATIONS);var batch=new ArrayList<Entry>(1024);boolean clear=true;
|
||||
if(entries!=null)for(var entry:entries.entrySet()){
|
||||
BlockPos pos;try{pos=BlockPos.of(Long.parseLong(entry.getKey()));}catch(NumberFormatException ignored){continue;}
|
||||
if(ChunkPos.pack(pos)!=chunk.getPos().pack()||chunk.getBlockState(pos).isAir())continue;
|
||||
batch.add(new Entry(pos,entry.getValue()));
|
||||
if(batch.size()==1024){send(player,new Update(player.level().dimension().identifier(),chunk.getPos(),clear,batch));clear=false;batch.clear();}
|
||||
}
|
||||
if(clear||!batch.isEmpty())send(player,new Update(player.level().dimension().identifier(),chunk.getPos(),clear,batch));
|
||||
}
|
||||
private static void send(ServerPlayer p,Update packet){if(ServerPlayNetworking.canSend(p,Update.TYPE))ServerPlayNetworking.send(p,packet);}
|
||||
private WrenchTextures(){}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
private final SimpleContainer input=new SimpleContainer(1){@Override public void setChanged(){super.setChanged();ClayWorkshopMenu.this.slotsChanged(this);}};
|
||||
private final SimpleContainer result=new SimpleContainer(1);
|
||||
private VoxelSculpture selected;
|
||||
private VoxelSculpture selectedResult;
|
||||
private ItemStack lastMaterial=ItemStack.EMPTY;
|
||||
private Item selectedMaterial=Items.AIR;
|
||||
private SculpturePalette selectedPalette;
|
||||
@@ -73,11 +74,13 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
if(UseBlockCallback.EVENT.invoker().interact(p,level,InteractionHand.MAIN_HAND,hit)!=InteractionResult.PASS||!stillValid(p))return;
|
||||
lastSelection=level.getGameTime();
|
||||
if(sculpture!=null&&(!material(input.getItem(0))||!input.getItem(0).is(material)||palette==null))return;
|
||||
if(!Objects.equals(selected,sculpture)||!Objects.equals(selectedPalette,palette))
|
||||
selectedResult=sculpture==null||palette==null?null:palette.apply(sculpture);
|
||||
selected=sculpture;selectedMaterial=material;selectedPalette=palette;refresh();broadcastChanges();
|
||||
}
|
||||
private void refresh(){
|
||||
if(level==null)return;var item=input.getItem(0);
|
||||
var sculpture=selected!=null&&selectedPalette!=null&&material(item)&&item.is(selectedMaterial)?selectedPalette.apply(selected):null;lastMaterial=item.copy();
|
||||
var sculpture=material(item)&&item.is(selectedMaterial)?selectedResult:null;lastMaterial=item.copy();
|
||||
result.setItem(0,sculpture==null?ItemStack.EMPTY:StatuaryEntity.item(sculpture));
|
||||
}
|
||||
@Override public void slotsChanged(Container container){if(container==input)refresh();super.slotsChanged(container);}
|
||||
|
||||
@@ -7,7 +7,7 @@ import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
|
||||
/** World-space edge of the occupied block; CENTER is the legacy placement. */
|
||||
/** World-space anchor of the occupied block, always on the whole-voxel grid. */
|
||||
public enum SculptureAnchor implements StringRepresentable {
|
||||
CENTER(null), NORTH(Direction.NORTH), SOUTH(Direction.SOUTH), EAST(Direction.EAST), WEST(Direction.WEST);
|
||||
|
||||
@@ -23,7 +23,8 @@ public enum SculptureAnchor implements StringRepresentable {
|
||||
public static SculptureAnchor placement(BlockPlaceContext context){
|
||||
var pos=context.getClickedPos();var hit=context.getClickLocation();
|
||||
double x=hit.x-pos.getX()-.5,z=hit.z-pos.getZ()-.5;
|
||||
if(Math.abs(x)<1e-7&&Math.abs(z)<1e-7)return of(context.getHorizontalDirection().getOpposite());
|
||||
// The middle 8 x 8 pixels select the existing grid-aligned center.
|
||||
if(Math.abs(x)<=.25&&Math.abs(z)<=.25)return CENTER;
|
||||
if(Math.abs(x)>Math.abs(z))return x<0?WEST:EAST;
|
||||
return z<0?NORTH:SOUTH;
|
||||
}
|
||||
|
||||
@@ -8,16 +8,24 @@ public record SculpturePalette(List<Integer> colors) {
|
||||
public SculpturePalette {
|
||||
if(colors==null||colors.isEmpty()||colors.size()>MAX_COLORS||colors.stream().anyMatch(c->c==null||(c>>>24)!=255))
|
||||
throw new IllegalArgumentException("sculpture_palette");
|
||||
colors=colors.stream().sorted(Comparator.comparingDouble(SculpturePalette::lightness).thenComparingInt(Integer::intValue)).toList();
|
||||
colors=colors.stream().sorted(Comparator.comparingInt(SculpturePalette::luminance).thenComparingInt(Integer::intValue)).toList();
|
||||
}
|
||||
public static double lightness(int color){return StatueVoxelizer.colorSpace(color)[0];}
|
||||
// Integer Rec.709 weights: no color-space arrays or transcendental math per voxel.
|
||||
private static int luminance(int color){return 54*(color>>16&255)+183*(color>>8&255)+19*(color&255);}
|
||||
public static double lightness(int color){return luminance(color)/65280.;}
|
||||
public VoxelSculpture apply(VoxelSculpture source){
|
||||
if(source==null)return null;
|
||||
var shades=new HashMap<Integer,Double>();for(var cell:source.cells())shades.computeIfAbsent(cell.color(),SculpturePalette::lightness);
|
||||
double min=shades.values().stream().mapToDouble(Double::doubleValue).min().orElse(0),max=shades.values().stream().mapToDouble(Double::doubleValue).max().orElse(0);
|
||||
var matches=new HashMap<Integer,Integer>();
|
||||
shades.forEach((color,l)->{double fraction=max-min<.00001?.5:(l-min)/(max-min);matches.put(color,colors.get(Math.clamp((int)Math.round(fraction*(colors.size()-1)),0,colors.size()-1)));});
|
||||
return new VoxelSculpture(source.modelId(),source.name(),source.width(),source.height(),source.depth(),source.cells().stream()
|
||||
.map(c->new VoxelSculpture.Cell(c.x(),c.y(),c.z(),matches.get(c.color()))).toList());
|
||||
int min=65280,max=0;for(var cell:source.cells()){int l=luminance(cell.color());min=Math.min(min,l);max=Math.max(max,l);}
|
||||
// Flat models sample the entire texture distribution. Shaded models use overlapping
|
||||
// half-palette bands: retain large light/dark regions while giving each region grain.
|
||||
int span=max==min?colors.size():(colors.size()+1)/2;
|
||||
var cells=new ArrayList<VoxelSculpture.Cell>(source.cells().size());
|
||||
for(var cell:source.cells()){
|
||||
int start=max==min?0:(luminance(cell.color())-min)*(colors.size()-span)/(max-min);
|
||||
int hash=cell.key()+0x9E3779B9;hash=(hash^(hash>>>16))*0x7FEB352D;hash=(hash^(hash>>>15))*0x846CA68B;hash^=hash>>>16;
|
||||
int offset=(int)((Integer.toUnsignedLong(hash)*span)>>>32);
|
||||
cells.add(new VoxelSculpture.Cell(cell.x(),cell.y(),cell.z(),colors.get(start+offset)));
|
||||
}
|
||||
return new VoxelSculpture(source.modelId(),source.name(),source.width(),source.height(),source.depth(),cells);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/black_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/blue_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/brown_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/cyan_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/gray_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/green_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_blue_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/light_gray_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/lime_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/magenta_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/orange_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/pink_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/purple_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/red_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/white_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_post"
|
||||
},
|
||||
"when": {
|
||||
"up": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side_tall",
|
||||
"uvlock": true
|
||||
},
|
||||
"when": {
|
||||
"north": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 90
|
||||
},
|
||||
"when": {
|
||||
"east": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 180
|
||||
},
|
||||
"when": {
|
||||
"south": "tall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apply": {
|
||||
"model": "sanctuary:block/yellow_brick_wall_side_tall",
|
||||
"uvlock": true,
|
||||
"y": 270
|
||||
},
|
||||
"when": {
|
||||
"west": "tall"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/black_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/blue_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/brown_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/cyan_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/gray_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/green_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/light_blue_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/light_gray_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/lime_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/magenta_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/orange_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/pink_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/purple_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/red_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/white_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:block/yellow_brick_wall_inventory"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -1979,7 +1987,7 @@
|
||||
"sanctuary.multiblock.outputs": "Results",
|
||||
"sanctuary.multiblock.heat": "Reserve: %s s",
|
||||
"sanctuary.multiblock.progress": "Cooking: %s %%",
|
||||
"sanctuary.multiblock.hint": "Right click: assemble / open. Sneak: rotate / separate.",
|
||||
"sanctuary.multiblock.hint": "Left click: select a property. Right click: change its value. Sneak: reverse. Complete multiblock: assemble / open; sneak: separate.",
|
||||
"sanctuary.multiblock.unavailable": "Machine inaccessible, incomplete or partly unloaded.",
|
||||
"sanctuary.multiblock.separated": "Machine separated. Components retain their contents.",
|
||||
"sanctuary.multiblock.rotated": "Component rotated.",
|
||||
@@ -2162,7 +2170,7 @@
|
||||
"sanctuary.statuary.folder": "Shared folder",
|
||||
"sanctuary.statuary.refresh": "Refresh",
|
||||
"sanctuary.statuary.models": "GLB models",
|
||||
"sanctuary.statuary.placement": "The sculpture faces your gaze and snaps to the aimed block edge, on the pixel grid. Rotate adjusts the model before crafting.",
|
||||
"sanctuary.statuary.placement": "Aim at the middle of the block to center the sculpture, or at an edge to snap to it. It faces your gaze and stays on the pixel grid. Rotate adjusts the model before crafting.",
|
||||
"sanctuary.statuary.rotate": "Rotate",
|
||||
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
|
||||
"sanctuary.statuary.ready": "1 block = 1 sculpture. Shades come from your material’s texture.",
|
||||
@@ -2205,5 +2213,25 @@
|
||||
"sanctuary.multiblock.state.blocked": "Empty the matching slot in Results.",
|
||||
"sanctuary.multiblock.state.no_recipe": "This item has no furnace recipe.",
|
||||
"sanctuary.statuary.choose_material": "Add a block to apply its palette to the model. Containers must be empty.",
|
||||
"sanctuary.statuary.material_ready": "1 block = 1 sculpture. Palette: %s."
|
||||
"sanctuary.statuary.material_ready": "1 block = 1 sculpture. Palette: %s.",
|
||||
"block.sanctuary.white_brick_wall": "White Brick Wall",
|
||||
"block.sanctuary.orange_brick_wall": "Orange Brick Wall",
|
||||
"block.sanctuary.magenta_brick_wall": "Magenta Brick Wall",
|
||||
"block.sanctuary.light_blue_brick_wall": "Light Blue Brick Wall",
|
||||
"block.sanctuary.yellow_brick_wall": "Yellow Brick Wall",
|
||||
"block.sanctuary.lime_brick_wall": "Lime Brick Wall",
|
||||
"block.sanctuary.pink_brick_wall": "Pink Brick Wall",
|
||||
"block.sanctuary.gray_brick_wall": "Gray Brick Wall",
|
||||
"block.sanctuary.light_gray_brick_wall": "Light Gray Brick Wall",
|
||||
"block.sanctuary.cyan_brick_wall": "Cyan Brick Wall",
|
||||
"block.sanctuary.purple_brick_wall": "Purple Brick Wall",
|
||||
"block.sanctuary.blue_brick_wall": "Blue Brick Wall",
|
||||
"block.sanctuary.brown_brick_wall": "Brown Brick Wall",
|
||||
"block.sanctuary.green_brick_wall": "Green Brick Wall",
|
||||
"block.sanctuary.red_brick_wall": "Red Brick Wall",
|
||||
"block.sanctuary.black_brick_wall": "Black Brick Wall",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -1979,7 +1987,7 @@
|
||||
"sanctuary.multiblock.outputs": "Résultats",
|
||||
"sanctuary.multiblock.heat": "Réserve : %s s",
|
||||
"sanctuary.multiblock.progress": "Cuisson : %s %%",
|
||||
"sanctuary.multiblock.hint": "Clic droit : assembler / ouvrir. Maj : tourner / dissocier.",
|
||||
"sanctuary.multiblock.hint": "Clic gauche : choisir un état. Clic droit : changer sa valeur. Maj : sens inverse. Multibloc complet : assembler / ouvrir ; Maj : dissocier.",
|
||||
"sanctuary.multiblock.unavailable": "Machine inaccessible, incomplète ou partiellement déchargée.",
|
||||
"sanctuary.multiblock.separated": "Machine dissociée. Les composants conservent leur contenu.",
|
||||
"sanctuary.multiblock.rotated": "Composant tourné.",
|
||||
@@ -2162,7 +2170,7 @@
|
||||
"sanctuary.statuary.folder": "Dossier commun",
|
||||
"sanctuary.statuary.refresh": "Actualiser",
|
||||
"sanctuary.statuary.models": "Modèles GLB",
|
||||
"sanctuary.statuary.placement": "La sculpture suit votre regard et se cale contre le bord visé du bloc, sur la grille de pixels. Tourner ajuste le modèle avant fabrication.",
|
||||
"sanctuary.statuary.placement": "Visez le milieu du bloc pour centrer la sculpture, ou un bord pour l’y caler. Elle suit votre regard et reste sur la grille de pixels. Tourner ajuste le modèle avant fabrication.",
|
||||
"sanctuary.statuary.rotate": "Tourner",
|
||||
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
|
||||
"sanctuary.statuary.ready": "1 bloc = 1 sculpture. Les nuances viennent de la texture de votre matière.",
|
||||
@@ -2205,5 +2213,25 @@
|
||||
"sanctuary.multiblock.state.blocked": "Vider la case correspondante dans Résultats.",
|
||||
"sanctuary.multiblock.state.no_recipe": "Cet objet ne possède pas de recette au four.",
|
||||
"sanctuary.statuary.choose_material": "Ajoutez un bloc pour appliquer sa palette au modèle. Les conteneurs doivent être vides.",
|
||||
"sanctuary.statuary.material_ready": "1 bloc = 1 sculpture. Palette : %s."
|
||||
"sanctuary.statuary.material_ready": "1 bloc = 1 sculpture. Palette : %s.",
|
||||
"block.sanctuary.white_brick_wall": "Muret de briques blanches",
|
||||
"block.sanctuary.orange_brick_wall": "Muret de briques orange",
|
||||
"block.sanctuary.magenta_brick_wall": "Muret de briques magenta",
|
||||
"block.sanctuary.light_blue_brick_wall": "Muret de briques bleu clair",
|
||||
"block.sanctuary.yellow_brick_wall": "Muret de briques jaunes",
|
||||
"block.sanctuary.lime_brick_wall": "Muret de briques vert clair",
|
||||
"block.sanctuary.pink_brick_wall": "Muret de briques roses",
|
||||
"block.sanctuary.gray_brick_wall": "Muret de briques grises",
|
||||
"block.sanctuary.light_gray_brick_wall": "Muret de briques gris clair",
|
||||
"block.sanctuary.cyan_brick_wall": "Muret de briques cyan",
|
||||
"block.sanctuary.purple_brick_wall": "Muret de briques violettes",
|
||||
"block.sanctuary.blue_brick_wall": "Muret de briques bleues",
|
||||
"block.sanctuary.brown_brick_wall": "Muret de briques marron",
|
||||
"block.sanctuary.green_brick_wall": "Muret de briques vertes",
|
||||
"block.sanctuary.red_brick_wall": "Muret de briques rouges",
|
||||
"block.sanctuary.black_brick_wall": "Muret de briques noires",
|
||||
"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."
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/black_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/black_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/black_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side_tall",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/black_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/blue_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/blue_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/blue_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side_tall",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/blue_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/brown_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/brown_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/brown_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side_tall",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/brown_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/cyan_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/cyan_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/cyan_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side_tall",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/cyan_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/gray_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/gray_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/gray_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side_tall",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/gray_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/green_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/green_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/green_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side_tall",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/green_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/wall_inventory",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/light_blue_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_post",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/light_blue_bricks"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/template_wall_side",
|
||||
"textures": {
|
||||
"wall": "sanctuary:block/light_blue_bricks"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user