Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f823af67 | ||
|
|
4552cc347b | ||
|
|
49131b4c53 | ||
|
|
edc7fc3dfb | ||
|
|
9b0104d27a |
@@ -20,11 +20,13 @@ 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.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 version **beta.123** limite la clé dorée aux orientations natives et confie
|
||||
l'édition libre à la **Clé à molette de Steve**, utilisable aussi en survie si
|
||||
elle est récupérée, avec sa texture bleue fournie par le créateur.
|
||||
Le [ticket de livraison](docs/wrench-safety-beta123.md) détaille les protections,
|
||||
les commandes et les vérifications client et archives réussies. La release est
|
||||
publiée et l’instance **Sanctuary Beta** est synchronisée, sauvegardes et réglages
|
||||
conservés.
|
||||
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.
|
||||
@@ -35,10 +37,27 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.123 — Clé dorée et clé de Steve
|
||||
|
||||
Orientations cohérentes, inventaires et eau préservés ; clé de Steve pour
|
||||
l’édition libre, utilisable aussi en survie. [Contrat et vérifications](docs/wrench-safety-beta123.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-beta.123.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-Test-beta.123.mrpack).
|
||||
|
||||
## beta.122 — Sculptures : forme et lumière
|
||||
|
||||
Collisions et sélection sur les voxels réels, creux traversables, lumière sans
|
||||
occlusion cubique. [Contrat et vérifications](docs/sculpture-shape-light-beta122.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-beta.122.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Test-beta.122.mrpack).
|
||||
|
||||
## beta.121 — Texture de clé et pack personnel
|
||||
|
||||
Copie complète des ressources, création depuis le jeu et guide FR/EN inclus.
|
||||
[Contrat et vérifications](docs/resourcepack-template-beta121.md).
|
||||
[Contrat et vérifications](docs/resourcepack-template-beta121.md) ·
|
||||
[Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
|
||||
|
||||
## beta.120 — Clé dorée et états de blocs
|
||||
|
||||
@@ -2138,7 +2157,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.121 |
|
||||
| Sanctuary / pack | beta.122 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -2167,7 +2186,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.121.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.122.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -14,6 +14,41 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## beta.123 — clé dorée et clé de Steve
|
||||
|
||||
La clé dorée se limite aux orientations cohérentes ; la clé de Steve reprend
|
||||
les états et textures libres, utilisables aussi en survie dès sa récupération.
|
||||
[Contrat et vérifications](wrench-safety-beta123.md).
|
||||
[Release beta.123](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.123)
|
||||
publiée ; canal `f988d01b901f2d3eda0b4ab47a48540dc627d1b5`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.123/`.
|
||||
|
||||
## beta.122 — formes et lumière des sculptures
|
||||
|
||||
Sélection et collisions sur les voxels réels ; suppression de l'occlusion cubique.
|
||||
[Contrat et vérifications](sculpture-shape-light-beta122.md).
|
||||
[Release beta.122](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.122)
|
||||
publiée ; canal `3d340bea0093b709c6018624795a1b3ca557b72d`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.122/`.
|
||||
|
||||
## beta.121 — texture de clé et pack personnel
|
||||
|
||||
PNG du créateur et copie complète modifiable depuis l'écran des packs.
|
||||
[Contrat et vérifications](resourcepack-template-beta121.md).
|
||||
[Release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
|
||||
publiée ; canal `e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.121/`. Le template personnel est créé par le
|
||||
joueur, jamais géré ou réécrit par packwiz.
|
||||
|
||||
## beta.120 — clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation des textures et assemblages prioritaires.
|
||||
|
||||
@@ -106,4 +106,20 @@ JAR Sanctuary : SHA-256
|
||||
|
||||
## Publication et installation
|
||||
|
||||
Artefacts prêts ; publication du canal et synchronisation Prism à effectuer.
|
||||
La [release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
|
||||
est publiée depuis `d9b3dd6bedf32448f8c6e5bfa4da10cb69968bc7`. Tag et artefacts immuables,
|
||||
téléchargements publics vérifiés. Canal packwiz :
|
||||
`e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.121 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés,
|
||||
y compris les packs de ressources. Aucune copie personnelle n'est créée par
|
||||
la mise à jour : chaque joueur utilise le bouton quand il le souhaite.
|
||||
Aucun monde personnel ouvert. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.121/` de l'instance existante ; reçus
|
||||
`build/template121-isolated.json` et `build/template121-prism.json`.
|
||||
|
||||
- [Template personnel complet](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip).
|
||||
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack).
|
||||
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# SCULPT-09 — Forme physique et lumière — beta.122
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-shape-light-beta122`.
|
||||
Le créateur demande des bounding boxes adaptées aux sculptures et la suppression
|
||||
des ombres cubiques visibles lorsqu'on en pose plusieurs côte à côte.
|
||||
|
||||
La sélection, le contact avec les entités et les collisions suivent les voxels
|
||||
occupés de chaque sculpture, y compris les creux. Le rendu et la forme physique
|
||||
partagent leur placement entier sur la grille 1/16, après orientation et ancrage.
|
||||
Le centre, les quatre bords et les dimensions impaires gardent leur alignement.
|
||||
La pose vérifie aussi les voxels portés avant que l’entité de bloc existe :
|
||||
elle refuse une intersection avec le joueur et autorise les espaces libres.
|
||||
La forme est construite directement sur une grille native 16³ et mise en cache
|
||||
par entité de bloc, modèle et état ; aucun calcul complet à chaque interrogation.
|
||||
|
||||
Les sculptures transmettent la lumière et ne sont plus utilisées comme cubes
|
||||
pleins par l'occlusion ambiante. Leurs faces restent éclairées par le monde,
|
||||
sans émission de lumière et sans rendu à luminosité maximale. Cette règle est
|
||||
commune aux sculptures : l'éclairage natif stocké par bloc ne permet pas une
|
||||
ombre distincte pour chaque petit voxel. L'atelier d'argile reste inchangé.
|
||||
|
||||
Aucun identifiant, blockstate, format d'item ou schéma sauvegardé ne change.
|
||||
Les formes sont recalculées à partir des voxels existants ; aucune migration,
|
||||
aucun parcours de monde ni régénération de chunk. Les données inconnues gardent
|
||||
leur conservation opaque et une petite base sélectionnable pour récupérer l'item.
|
||||
Le travail de documentation déjà présent dans l'autre branche reste intact.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours `Sculpture122ClientChecks` passe en **37 s** sur Minecraft 26.3,
|
||||
client macOS et serveur intégré, dans un monde plat jetable de graine 122 :
|
||||
|
||||
- Pose par la commande native d'utilisation ; sélection du pilier et visée
|
||||
à travers le trou d'une arche jusqu'au bloc derrière, captures relues.
|
||||
- Requêtes natives de collision côté serveur et client : voxels occupés solides,
|
||||
espace vide libre, aucune profondeur artificielle d'un bloc entier.
|
||||
- Pose refusée dans le joueur, sans consommer l'objet ; petite sculpture acceptée
|
||||
dans la partie libre du même bloc, sans intersection avec le joueur.
|
||||
- **80 cas** confrontent le volume et les faces de collision au maillage réellement
|
||||
émis : arche creuse, marges vides, cube de 4 096 voxels, voxel unique, quatre
|
||||
orientations et cinq ancrages. Chaque interrogation répétée réutilise la forme.
|
||||
- Régression de **5 120 placements** du rendu existant : dimensions paires/impaires,
|
||||
orientations et ancrages, plus marqueurs de direction et modèles avec marges.
|
||||
- Changement d'orientation/ancrage puis changement des voxels sur la même entité :
|
||||
cache recalculé et forme client synchronisée.
|
||||
- **75 sculptures en trois couches** : skylight natif conservé à 15 partout.
|
||||
Un toit de pierre témoin produit toujours une ombre. La lumière d'un bloc
|
||||
traverse une sculpture avec l'atténuation de distance native (13 à deux blocs).
|
||||
- Le rendu reçoit la lumière réelle du monde : 15 au ciel, **0 dans une pièce
|
||||
fermée et obscure**. Aucun rendu artificiellement lumineux.
|
||||
- Sauvegarde/reconnexion : modèle, collisions et lumière conservés sur les
|
||||
deux côtés, sans fichier 3D source.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuarySculpture122ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `SCULPTURE122_PASS`, `SCULPTURE122_GEOMETRY_PASS`,
|
||||
`SCULPTURE122_PLACEMENT_PASS`, `SCULPTURE122_LIGHT_PASS` et
|
||||
`SCULPTURE113_GEOMETRY_PASS` dans `build/sculpture122-client.log` ; captures
|
||||
conservées dans `build/sculpture122-evidence/`. Le test prépare explicitement
|
||||
son sol à Y=0 : le preset plat nu commence à Y=-1.
|
||||
Aucun monde personnel ouvert ; pas d'essai Windows ni depuis deux ordinateurs,
|
||||
ni de garantie sur les ombres ajoutées par des shaders tiers.
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 13 s**, 126 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. Par rapport à beta.121, les différences de production se limitent
|
||||
aux classes des sculptures et à la nouvelle géométrie partagée (14 entrées,
|
||||
y compris le déplacement du sélecteur synthétique d'orientation). Toutes les
|
||||
ressources client, données serveur et configurations de mixins sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR, sans sauvegarde ni fixture.
|
||||
Le template beta.122 contient les mêmes ressources que beta.121, octet pour octet.
|
||||
|
||||
Reçu local : `build/sculpture122-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.122.mrpack` : 10267459 octets, SHA-256
|
||||
`b6e24d65797eb7f16670d2f3a591b5f2476820608f72241ecd37fb78a486008e`.
|
||||
- `Sanctuary-Test-beta.122.mrpack` : 10286380 octets, SHA-256
|
||||
`90487ee3ad4298fccb8c24e8781af4cbf043d88dc0379fba5c21d99733dac92d`.
|
||||
|
||||
JAR Sanctuary : SHA-256
|
||||
`ff8ebf89f39a64809fae6912253386793e9b5e66ec0adb9fa7521729893d406e`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.122](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.122)
|
||||
est publiée depuis `edc7fc3dfb117cde61be42c1d79b03af11f5dec6`. Tag et artefacts immuables,
|
||||
téléchargements publics vérifiés. Canal packwiz :
|
||||
`3d340bea0093b709c6018624795a1b3ca557b72d`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.122 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés.
|
||||
Aucun monde personnel ouvert. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.122/` de l'instance existante ; reçus
|
||||
`build/sculpture122-isolated.json` et `build/sculpture122-prism.json`.
|
||||
|
||||
Le ticket a été réalisé dans le worktree `sanctuary-sculpture-beta122` afin de
|
||||
préserver les changements de `docs/vision.md` et `docs/villageois-illageois.md`
|
||||
déjà présents sur la branche de documentation du checkout principal.
|
||||
|
||||
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-beta.122.mrpack).
|
||||
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Test-beta.122.mrpack).
|
||||
- [Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.122/Sanctuary-Template-beta.122.zip).
|
||||
@@ -0,0 +1,129 @@
|
||||
# TOOL-02 — Clé dorée et clé de Steve — beta.123
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/wrench-safety-beta123`.
|
||||
Ce ticket remplace le contrat de la clé dorée de beta.120 : l'édition libre
|
||||
reste disponible dans un objet distinct, la **Clé à molette de Steve**.
|
||||
Le créateur précise que la clé de Steve reste utilisable en survie dès qu’un
|
||||
joueur la récupère, sans condition opérateur. Elle n’a pas de recette.
|
||||
|
||||
## Clé dorée : orientations natives
|
||||
|
||||
L'identifiant `sanctuary:golden_wrench`, sa recette et sa texture restent stables.
|
||||
Clic droit parcourt l'orientation native ; Maj parcourt en sens inverse.
|
||||
Clic gauche rappelle les gestes, sans casser le bloc ni sélectionner un état.
|
||||
Les familles de propriétés autorisées sont les orientations horizontales,
|
||||
les six directions, les trois axes, la rotation native des panneaux et
|
||||
l'orientation native des blocs qui la proposent. La règle porte sur les
|
||||
propriétés Minecraft, sans catalogue à maintenir pour chaque type de bloc.
|
||||
|
||||
L'eau, la moitié d'un escalier, les inventaires, les états de croissance,
|
||||
les quantités, la puissance et les autres propriétés ne sont jamais proposées.
|
||||
Aucune transformation indépendante des textures n'est créée par la clé dorée.
|
||||
Les textures suivent seulement le modèle orienté par Minecraft.
|
||||
Les connexions et formes sont recalculées à partir des voisins réels, avec les
|
||||
mises à jour natives actives. Une orientation privée de support est refusée
|
||||
sans casser le bloc. L'eau déjà contenue dans un bloc est conservée.
|
||||
|
||||
Les coffres doubles, portes, lits, étagères reliées, pistons sortis et leurs
|
||||
pièces mobiles ne peuvent pas être tournés individuellement. Ils restent dans
|
||||
leur état courant et une indication explique le refus. Il faut replacer les
|
||||
éléments concernés ou rétracter le piston ; ce ticket ne déplace pas un
|
||||
ensemble lié. Les coffres simples conservent leur entité et leur inventaire.
|
||||
|
||||
L'assemblage, l'ouverture et la dissociation des Métablis, Fûts, Fourneaux et
|
||||
super pistons restent prioritaires avec les deux clés. Les parties techniques
|
||||
d'un multibloc Sanctuary assemblé ne sont pas éditées individuellement.
|
||||
|
||||
## Clé de Steve
|
||||
|
||||
Nouvel identifiant `sanctuary:steve_wrench`, dans la section créative des outils,
|
||||
sans recette de survie. La possession de cet objet donne accès à ses pouvoirs,
|
||||
y compris en survie sans commandes et sans statut opérateur, dans les deux
|
||||
mains. Les restrictions natives de construction, aventure, spectateur, portée,
|
||||
protection et verrouillage restent appliquées côté serveur.
|
||||
|
||||
Les gestes de beta.120 sont conservés : clic gauche choisit la propriété,
|
||||
clic droit la modifie, Maj inverse le parcours. Tous les états natifs et la
|
||||
rotation indépendante des textures y restent volontairement accessibles.
|
||||
La clé de Steve est donc volontairement capable de produire des états non
|
||||
naturels, y compris pour le joueur qui la récupère en survie.
|
||||
|
||||
La texture finale est le PNG 16 × 16 bleu fourni par le créateur dans ce ticket,
|
||||
copié sans transformation dans `assets/sanctuary/textures/item/steve_wrench.png`.
|
||||
SHA-256 : `0084c41e1f46958903075e21866facd97d85e03fb8e06742e25b2ab254956fa3`.
|
||||
Le modèle reste un objet Minecraft 2D tenu en main, avec rareté épique et reflet.
|
||||
La proposition imagegen intermédiaire n'est pas livrée. Les noms, messages et
|
||||
indications des deux outils existent en français et en anglais.
|
||||
|
||||
## Sauvegardes et ressources
|
||||
|
||||
Aucune migration, modification de génération ni parcours des mondes existants.
|
||||
Les transformations de texture précédemment sauvegardées sont conservées ;
|
||||
la clé de Steve permet de les modifier. Les états non naturels déjà présents
|
||||
ne sont pas automatiquement réécrits. Les clés dorées existantes reçoivent
|
||||
immédiatement les nouvelles restrictions par leur identifiant inchangé.
|
||||
Le template personnel inclut le nouvel objet à sa prochaine création ; une
|
||||
copie personnelle existante n'est jamais écrasée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `Wrench123ClientChecks` passe en **1 min 12 s**, client macOS et
|
||||
serveur intégré, Minecraft 26.3, dans des mondes plats jetables de graines
|
||||
123 et 120. Il reprend le parcours avancé de beta.120 sur la clé de Steve :
|
||||
|
||||
- Vrais clics dorés répétés sur escalier, coffre et terre : pas de nouvelle eau,
|
||||
pas de sélection libre, pas de faux coffre double ni de rotation de texture.
|
||||
- Rotation inverse, eau existante conservée, formes recalculées selon les voisins,
|
||||
trois axes de bûches et six orientations des pistons isolés.
|
||||
- Coffre double avec ses deux inventaires intacts ; portes, lits, pistons sortis
|
||||
et pièces mobiles protégés ; torche murale sans support laissée intacte.
|
||||
- Vrais clics Steve en survie sans commandes : états et textures accessibles,
|
||||
dans les deux mains. Créatif accessible aussi sans commandes ; restrictions
|
||||
aventure/spectateur/portée conservées.
|
||||
- **2 522 propriétés natives** toujours réversibles pour Steve, UV réellement
|
||||
tournés sans déplacer la géométrie, rechargement des ressources, inventaires
|
||||
et sauvegarde/reconnexion conservés.
|
||||
- Assemblage et dissociation des quatre familles avec la clé dorée par vrais
|
||||
clics ; le PNG de Steve apparaît en main et dans la barre rapide.
|
||||
|
||||
Marqueurs `WRENCH123_SAFE_PASS`, `WRENCH123_PASS`, `WRENCH120_PASS` dans
|
||||
`build/wrench123-client.log`. Captures relues dans `build/wrench123-evidence/`.
|
||||
Pas de validation Windows, de client distant ou de shader tiers.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 19 s**, 126 tâches (103 exécutées, 23 à jour).
|
||||
Les sources des JAR correspondent aux sources du dépôt. La comparaison avec
|
||||
beta.122 limite les dix entrées de production modifiées aux classes des clés,
|
||||
aux libellés et aux ressources du nouvel objet. Les autres ressources,
|
||||
notamment les sculptures et la texture dorée, sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR Sanctuary, sans sauvegarde ni
|
||||
fixture de test. Le template ZIP correspond exactement aux ressources du JAR
|
||||
et à ses ressources embarquées, dont le PNG bleu du créateur.
|
||||
|
||||
- Pack normal : 10 270 954 octets, SHA-256
|
||||
`a7401dbef1bd1b888da2686368943eddc82d0d43e20449330c4849b4dff93e5f`.
|
||||
- Pack Test : 10 289 877 octets, SHA-256
|
||||
`c57cc6fc47f887b90850473e51407b610a6c5639c35fc2b78dc9ddb74f795b80`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`23e1a81c793a048c6375a0da07eb0bbe1a18f833a9ceb61a3c1dc5f1a3e65a30`.
|
||||
|
||||
Reçu local : `build/wrench123-artifact.json`.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Aucun monde personnel n'est ouvert par les essais.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.123](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.123)
|
||||
est publiée depuis `4552cc347b7fded610b1eda7a67ffaad56a5e201`.
|
||||
Le tag exact et les artefacts sont immuables ; les téléchargements publics
|
||||
ont été vérifiés. Canal packwiz : `f988d01b901f2d3eda0b4ab47a48540dc627d1b5`.
|
||||
Le template personnel publié inclut la clé de Steve ; SHA-256
|
||||
`f84d3c1c0e38232add90fb943dc39fd3e89138576df86024ceaa311478ed5cb9`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.123 est actif, conforme au hash vérifié ; le second
|
||||
passage ne change rien. Les **923 fichiers personnels et réglages suivis**
|
||||
restent inchangés, sans ouverture de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.123/` de l'instance existante.
|
||||
Reçus : `build/wrench123-isolated.json` et `build/wrench123-prism.json`.
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.121
|
||||
pack_version=beta.121
|
||||
mod_version=beta.123
|
||||
pack_version=beta.123
|
||||
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 wrench123Client = providers.gradleProperty('sanctuaryWrench123ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWrench123ClientTests', wrench123Client)
|
||||
if (wrench123Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench123ClientChecks') }
|
||||
|
||||
def sculpture122Client = providers.gradleProperty('sanctuarySculpture122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture122ClientTests', sculpture122Client)
|
||||
if (sculpture122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture122ClientChecks') }
|
||||
|
||||
def template121Client = providers.gradleProperty('sanctuaryResourceTemplate121ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryResourceTemplate121ClientTests', template121Client)
|
||||
if (template121Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ResourceTemplate121ClientChecks') }
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.phys.shapes.*;
|
||||
|
||||
/** Native picking/collision and light propagation, compared with the actual emitted sculpture mesh. */
|
||||
public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
private static final String MODEL="sanctuary:model/"+"1".repeat(64);
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),SKY=new BlockPos(10,2,2),CONTROL=new BlockPos(18,2,2);
|
||||
private static final VoxelSculpture ARCH=arch();
|
||||
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture122 "+UUID.randomUUID());s.setSeed("122");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-14;x<=21;x++)for(int z=-6;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);p.setYRot(0);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));p.inventoryMenu.broadcastFullState();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(TARGET.below()).is(Blocks.STONE),150);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,.999,.5)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.below())&&hit.getDirection()==Direction.UP,"Cursor aims at placement support"));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(3);
|
||||
server.runOnServer(s->System.out.println("SCULPTURE122_PLACED "+player(s).level().getBlockState(TARGET)+" "+player(s).level().getBlockEntity(TARGET)));
|
||||
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),150);
|
||||
server.runOnServer(s->{physical(player(s).level(),TARGET);placementOverlap(player(s));});
|
||||
c.runOnClient(m->{physical(m.level,TARGET);geometry(m);Sculpture113ClientChecks.geometry(m,MODEL);});
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.0625,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects an occupied pillar"));
|
||||
c.takeScreenshot("sculpture122-voxel-outline");
|
||||
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET.north(),Blocks.REDSTONE_BLOCK.defaultBlockState()));
|
||||
c.waitFor(m->m.level.getBlockState(TARGET.north()).is(Blocks.REDSTONE_BLOCK),100);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.north()),"Native cursor passes through the sculpture hole to the block behind"));
|
||||
c.takeScreenshot("sculpture122-pick-through-hole");
|
||||
// A state edit and fresh entity payload must invalidate geometry on both sides.
|
||||
server.runOnServer(s->{var l=player(s).level();l.setBlockAndUpdate(TARGET,l.getBlockState(TARGET).setValue(Statuary.FACING,Direction.WEST).setValue(Statuary.ANCHOR,SculptureAnchor.EAST));});
|
||||
c.waitFor(m->m.level.getBlockState(TARGET).getValue(Statuary.FACING)==Direction.WEST,100);
|
||||
server.runOnServer(s->{var l=player(s).level();var bounds=l.getBlockState(TARGET).getShape(l,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Server shape rotates and moves to aimed edge");});
|
||||
c.runOnClient(m->{var bounds=m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Client shape follows changed blockstate");});
|
||||
var small=new VoxelSculpture(MODEL,"Small",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1)));
|
||||
server.runOnServer(s->place(player(s).level(),TARGET,small,Direction.WEST,SculptureAnchor.EAST));
|
||||
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&small.equals(e.sculpture()),100);
|
||||
c.runOnClient(m->check(m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds().getYsize()==1/16d,"Changed block entity payload invalidates cached shape"));
|
||||
server.runOnServer(s->{var l=player(s).level();place(l,TARGET,ARCH,Direction.SOUTH,SculptureAnchor.CENTER);l.setBlockAndUpdate(TARGET.north(),Blocks.AIR.defaultBlockState());
|
||||
for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)place(l,new BlockPos(x,y,z),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
for(int x=16;x<=20;x++)for(int z=0;z<=4;z++)l.setBlockAndUpdate(new BlockPos(x,3,z),Blocks.STONE.defaultBlockState());
|
||||
l.setBlockAndUpdate(new BlockPos(-8,2,0),Blocks.GLOWSTONE.defaultBlockState());place(l,new BlockPos(-7,2,0),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
});
|
||||
server.waitFor(s->{var l=player(s).level();return l.getBrightness(LightLayer.SKY,SKY)==15&&l.getBrightness(LightLayer.SKY,CONTROL)<15&&l.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13;},200);
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15&&m.level.getBrightness(LightLayer.SKY,CONTROL)<15&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13,200);
|
||||
server.runOnServer(s->{var l=player(s).level();for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)check(l.getBrightness(LightLayer.SKY,new BlockPos(x,y,z))==15,"All 75 sculptures keep full native skylight");});
|
||||
c.runOnClient(m->{var e=(StatuaryEntity)m.level.getBlockEntity(new BlockPos(10,4,2));var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(e,state,0,Vec3.ZERO,null);check(net.minecraft.util.LightCoordsUtil.sky(state.lightCoords)==15,"Actual renderer receives full skylight inside a group");});
|
||||
// Visual crowd at ground level: no whole-block AO squares between the models.
|
||||
server.runOnServer(s->{var p=player(s);for(int x=-3;x<=3;x++)for(int z=-5;z<=-2;z++)place(p.level(),new BlockPos(x,1,z),ARCH,Direction.from2DDataValue(Math.floorMod(x+z,4)),SculptureAnchor.CENTER);p.teleportTo(.5,3,5.5);p.getAbilities().flying=true;p.onUpdateAbilities();});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,3,5.5)<.1,100);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1,-3)));c.waitTicks(12);c.takeScreenshot("sculpture122-crowd-light");
|
||||
// Transparent-to-light is not emissive: in a closed room sculpture faces stay dark.
|
||||
server.runOnServer(s->{var l=player(s).level();for(int x=-13;x<=-9;x++)for(int y=1;y<=5;y++)for(int z=5;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,y,z),x==-13||x==-9||y==1||y==5||z==5||z==9?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());place(l,new BlockPos(-11,2,7),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);});
|
||||
c.waitFor(m->m.level.getBlockEntity(new BlockPos(-11,2,7)) instanceof StatuaryEntity&&m.level.getBrightness(LightLayer.SKY,new BlockPos(-11,2,7))==0&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-11,2,7))==0,200);
|
||||
c.runOnClient(m->{var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState((StatuaryEntity)m.level.getBlockEntity(new BlockPos(-11,2,7)),state,0,Vec3.ZERO,null);check(state.lightCoords==0,"No fullbright workaround: a dark room produces dark faces");});
|
||||
System.out.println("SCULPTURE122_LIGHT_PASS: 75 stacked models sky=15; opaque control shadow; block light=13 across sculpture; dark room=0; shade brightness=1");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),1200);
|
||||
world.getServer().runOnServer(s->physical(player(s).level(),TARGET));c.runOnClient(m->physical(m.level,TARGET));
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15,200);
|
||||
System.out.println("SCULPTURE122_PASS: native placement, voxel outline and picking through holes, authoritative collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
|
||||
}
|
||||
}
|
||||
private static void placementOverlap(ServerPlayer player){
|
||||
var pos=new BlockPos(3,1,0);var level=player.level();player.teleportTo(3.75,1,.5);player.setYRot(0);
|
||||
var filled=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)filled.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var dense=new VoxelSculpture(MODEL,"Solid",16,16,16,filled);var stack=StatuaryEntity.item(dense);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var hit=new BlockHitResult(new Vec3(3.01,1,.5),Direction.UP,pos.below(),false);
|
||||
var blocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(!blocked.consumesAction()&&level.getBlockState(pos).isAir()&&stack.getCount()==1,"Native placement refuses occupied voxels inside player without consuming sculpture");
|
||||
var postCells=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x=0;x<2;x++)for(int z=0;z<2;z++)postCells.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var post=new VoxelSculpture(MODEL,"Post",2,16,2,postCells);stack=StatuaryEntity.item(post);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var allowed=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(allowed.consumesAction()&&level.getBlockState(pos).is(Statuary.BLOCK),"Native placement allows empty part of block beside player");
|
||||
check(level.noCollision(player,player.getBoundingBox()),"Accepted sculpture does not intersect player");
|
||||
level.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());player.teleportTo(.5,1,3.2);player.getInventory().setItem(player.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));player.inventoryMenu.broadcastFullState();
|
||||
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied voxels, allows empty space, preserves stack on rejection");
|
||||
}
|
||||
private static void physical(Level level,BlockPos pos){
|
||||
var state=level.getBlockState(pos);var shape=state.getShape(level,pos);var collision=state.getCollisionShape(level,pos);
|
||||
check(shape==collision,"Selection and native collision share cached shape");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.MISS,"Outline ray passes through empty arch center");
|
||||
check(level.clip(new ClipContext(new Vec3(.0625,1.5,2),new Vec3(.0625,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Collision ray hits pillar");
|
||||
check(level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query leaves hole empty");
|
||||
check(!level.noCollision(new AABB(.02,1.2,.4,.1,1.7,.6)),"Native collision query blocks occupied pillar");
|
||||
check(!state.isCollisionShapeFullBlock(level,pos)&&!state.isSolidRender()&&!state.canOcclude(),"Sculpture is not a solid occluding cube");
|
||||
check(state.getOcclusionShape().isEmpty()&&state.propagatesSkylightDown()&&state.getLightDampening()==0&&state.getShadeBrightness(level,pos)==1,"Light metadata never reintroduces full-block occlusion");
|
||||
check(!state.isSuffocating(level,pos)&&state.getLightEmission()==0,"Decorative shape neither suffocates nor emits light");
|
||||
}
|
||||
private static void geometry(Minecraft m){
|
||||
var padded=new VoxelSculpture(MODEL,"Padded",13,5,15,List.of(new VoxelSculpture.Cell(2,2,3,-1),new VoxelSculpture.Cell(6,4,10,-1)));
|
||||
var dense=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)dense.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var models=List.of(ARCH,padded,new VoxelSculpture(MODEL,"Dense",16,16,16,dense),new VoxelSculpture(MODEL,"One voxel",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1))));
|
||||
int cases=0;for(var model:models)for(var direction:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor);var entity=new StatuaryEntity(BlockPos.ZERO,state);entity.setLevel(m.level);load(entity,model,m.level);
|
||||
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(volume==model.cells().size()/4096d,"Shape contains exactly the occupied volume");
|
||||
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
|
||||
for(var q:render.mesh){var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
|
||||
var normal=switch(q.side()){case 0->Direction.NORTH;case 1->Direction.SOUTH;case 2->Direction.WEST;case 3->Direction.EAST;case 4->Direction.UP;default->Direction.DOWN;};var neighbor=center.add(normal.getStepX()/16d,normal.getStepY()/16d,normal.getStepZ()/16d);check(boxes.stream().noneMatch(b->b.contains(neighbor)),"Rendered exposed face is also exposed physically");}
|
||||
cases++;
|
||||
}
|
||||
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native voxel shapes agree with emitted meshes, including holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
|
||||
}
|
||||
private static void place(ServerLevel level,BlockPos pos,VoxelSculpture model,Direction facing,SculptureAnchor anchor){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);level.setBlockAndUpdate(pos,state);var entity=(StatuaryEntity)level.getBlockEntity(pos);load(entity,model,level);entity.setChanged();level.sendBlockUpdated(pos,state,state,3);
|
||||
}
|
||||
private static void load(StatuaryEntity entity,VoxelSculpture model,Level level){var tag=new CompoundTag();tag.put("sculpture",StatuaryEntity.encode(model));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,level.registryAccess(),tag));}
|
||||
private static VoxelSculpture arch(){var cells=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<4;z++)if(x<2||x>=14||y>=14)cells.add(new VoxelSculpture.Cell(x,y,z,0xffc99679));return new VoxelSculpture(MODEL,"Arch",16,16,4,cells);}
|
||||
private static ServerPlayer player(MinecraftServer server){return server.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+6
-6
@@ -43,7 +43,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench120 "+UUID.randomUUID());s.setSeed("120");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.STEVE_WRENCH));p.inventoryMenu.broadcastFullState();
|
||||
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
int tested=0;for(var block:BuiltInRegistries.BLOCK)for(var property:block.getStateDefinition().getProperties()){
|
||||
@@ -52,7 +52,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
check(tested>1000,"Broad block property coverage");System.out.println("WRENCH120_PROPERTIES "+tested);
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));click(c,TARGET,false,false);
|
||||
server.waitFor(s->player(s).level().getBlockState(TARGET)!=stairs,100);c.waitFor(m->m.level.getBlockState(TARGET)!=stairs,100);
|
||||
server.runOnServer(s->{var actual=player(s).level().getBlockState(TARGET);check(actual.getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING)&&actual.getValue(StairBlock.HALF)==stairs.getValue(StairBlock.HALF),"Right click changes facing only");});
|
||||
@@ -64,8 +64,8 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
c.takeScreenshot("wrench120-stairs");
|
||||
// A container rotates in place; inventory and block entity survive the full native interaction.
|
||||
click(c,CHEST,false,false);server.runOnServer(s->{var p=player(s);var chest=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(chest.getItem(0).is(Items.DIAMOND)&&chest.getItem(0).getCount()==23,"Chest contents retained");check(p.containerMenu==p.inventoryMenu,"Wrench does not open ordinary chest");check(chest.getBlockState().getValue(ChestBlock.FACING)!=Blocks.CHEST.defaultBlockState().getValue(ChestBlock.FACING),"Chest rotates");});
|
||||
// Survival uses exactly the same gestures; dirt keeps its native ID, shape and loot.
|
||||
server.runOnServer(s->player(s).setGameMode(GameType.SURVIVAL));c.waitFor(m->!m.player.isCreative(),100);
|
||||
// Ordinary survival without commands retains Steve’s full powers; dirt keeps its native ID, shape and loot.
|
||||
server.runOnServer(s->{s.getWorldData().setAllowCommands(false);player(s).setGameMode(GameType.SURVIVAL);});c.waitFor(m->!m.player.isCreative(),100);
|
||||
var original=c.computeOnClient(m->geometry(m,DIRT));c.takeScreenshot("wrench120-dirt-before");
|
||||
click(c,DIRT,false,false);server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
var rotated=c.computeOnClient(m->geometry(m,DIRT));checkUvs(original,rotated);c.takeScreenshot("wrench120-dirt-90");
|
||||
@@ -81,7 +81,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
var p=player(s);p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure cannot edit");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.SURVIVAL);check(!GoldenWrench.allowed(p,TARGET.offset(100,0,0),InteractionHand.MAIN_HAND,Direction.UP),"Out-of-range edit refused");
|
||||
});
|
||||
// A full Metabli still assembles with two normal right clicks, then separates with sneak-use.
|
||||
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
|
||||
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
|
||||
c.waitFor(m->m.player.distanceToSqr(7.5,1,4)<.1&&m.level.getBlockState(table).is(Blocks.CRAFTING_TABLE),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.waitFor(s->player(s).level().getBlockState(table).is(Metatable.BLOCK),100);c.waitFor(m->m.level.getBlockState(table).is(Metatable.BLOCK),100);
|
||||
@@ -116,7 +116,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
System.out.println("WRENCH120_PASS native creative/survival clicks, reverse, properties, textures and resource reload, unchanged geometry, filled chest, multiblock assembly priority, permissions, cleanup and save/reconnect");
|
||||
}
|
||||
private static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
|
||||
static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
|
||||
if(sneak){c.getInput().holdKey(o->o.keyShift);c.waitTicks(3);}
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()+.3,pos.getZ()+.5)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos),"Native cursor hits target "+pos+" instead of "+m.hitResult));
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.piston.PistonBaseBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native golden-wrench clicks cannot create resources or split linked blocks. */
|
||||
public final class Wrench123ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),CHEST=new BlockPos(2,1,0),DIRT=new BlockPos(-2,1,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench123 "+UUID.randomUUID());s.setSeed("123");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-5;x<=8;x++)for(int z=-4;z<=6;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);equip(p,Multiblocks.WRENCH);
|
||||
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));
|
||||
Wrench120ClientChecks.click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING),"Golden wrench changes native stair facing"));
|
||||
Wrench120ClientChecks.click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
|
||||
for(int i=0;i<5;i++){Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);}
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(TARGET);check(!state.getValue(StairBlock.WATERLOGGED)&&state.getValue(StairBlock.HALF)==Half.BOTTOM&&state.getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT,"Repeated native clicks never select water, half or fake stair shapes");});
|
||||
for(int i=0;i<4;i++)Wrench120ClientChecks.click(c,CHEST,false,false);
|
||||
server.runOnServer(s->{var p=player(s);var be=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(be.getItem(0).getCount()==23&&be.getBlockState()==Blocks.CHEST.defaultBlockState(),"Single chest remains single, dry and filled after four rotations");check(p.containerMenu==p.inventoryMenu,"Ordinary rotation does not open chest");});
|
||||
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
|
||||
server.runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==0&&l.getBlockState(DIRT).is(Blocks.DIRT),"Golden wrench never adds a texture transform");regressions(player(s));});
|
||||
c.takeScreenshot("wrench123-golden-safe");
|
||||
// Finding Steve's wrench grants its full powers in ordinary survival, without operator status.
|
||||
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.SURVIVAL);s.getWorldData().setAllowCommands(false);equip(p,Multiblocks.STEVE_WRENCH);});
|
||||
c.waitFor(m->!m.player.isCreative()&&m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH),100);
|
||||
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
|
||||
server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
Wrench120ClientChecks.click(c,TARGET,false,false);Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Native Steve edit works for ordinary survival without commands");
|
||||
for(var hand:InteractionHand.values()){
|
||||
p.setItemInHand(hand,new ItemStack(Multiblocks.STEVE_WRENCH));check(GoldenWrench.allowed(p,TARGET,hand,Direction.UP),"Survival use allowed in both hands");
|
||||
GoldenWrench.change(p,DIRT,hand,Direction.UP);
|
||||
}
|
||||
check(WrenchTextures.rotation(p.level().getChunkAt(DIRT),DIRT)==3,"Both hands can freely edit textures in survival");
|
||||
p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);p.setGameMode(GameType.CREATIVE);check(GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Creative access without commands enabled");
|
||||
p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure restrictions remain");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.CREATIVE);
|
||||
var steve=new ItemStack(Multiblocks.STEVE_WRENCH);check(Boolean.TRUE.equals(steve.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE)),"Distinct enchanted Steve item");
|
||||
});
|
||||
c.waitFor(m->m.player.isCreative(),100);c.takeScreenshot("wrench123-steve-texture");
|
||||
System.out.println("WRENCH123_SAFE_PASS native clicks, water/inventory preservation, linked blocks, real neighbour shapes, support checks, ordinary survival and creative use, adventure/spectator restrictions and both hands");
|
||||
}
|
||||
// Keep the full former debug-stick contract on Steve, including UVs/save/reload, and assembly on gold.
|
||||
new Wrench120ClientChecks().runTest(c);
|
||||
System.out.println("WRENCH123_PASS safe golden wrench and Steve wrench with native regression coverage");
|
||||
}
|
||||
private static void regressions(ServerPlayer p){
|
||||
var l=p.level();
|
||||
// Water stays present when rotating a legitimately waterlogged block.
|
||||
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.WATERLOGGED,true),18);
|
||||
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Existing water is preserved");
|
||||
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING,Direction.WEST).setValue(StairBlock.SHAPE,StairsShape.INNER_LEFT),18);
|
||||
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT&&!l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Native shape recomputed against actual empty neighbours");
|
||||
var a=CHEST;var b=CHEST.east();var base=Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING,Direction.NORTH);
|
||||
l.setBlock(a,base.setValue(ChestBlock.TYPE,ChestType.LEFT),18);l.setBlock(b,base.setValue(ChestBlock.TYPE,ChestType.RIGHT),18);
|
||||
var first=(ChestBlockEntity)l.getBlockEntity(a);var second=(ChestBlockEntity)l.getBlockEntity(b);first.setItem(0,new ItemStack(Items.DIAMOND,23));second.setItem(0,new ItemStack(Items.EMERALD,17));
|
||||
var left=l.getBlockState(a);var right=l.getBlockState(b);use(p,a);use(p,b);
|
||||
check(l.getBlockState(a)==left&&l.getBlockState(b)==right&&l.getBlockEntity(a)==first&&l.getBlockEntity(b)==second&&first.getItem(0).getCount()==23&&second.getItem(0).getCount()==17,"Double chest remains linked with both original inventories");
|
||||
var q=TARGET;var door=Blocks.OAK_DOOR.defaultBlockState();l.setBlock(q,door.setValue(DoorBlock.HALF,DoubleBlockHalf.LOWER),18);l.setBlock(q.above(),door.setValue(DoorBlock.HALF,DoubleBlockHalf.UPPER),18);
|
||||
var lower=l.getBlockState(q);var upper=l.getBlockState(q.above());use(p,q);use(p,q.above());check(l.getBlockState(q)==lower&&l.getBlockState(q.above())==upper,"Both door halves protected");l.setBlock(q.above(),Blocks.AIR.defaultBlockState(),18);
|
||||
var bed=Blocks.BED.red().defaultBlockState();l.setBlock(q,bed,18);use(p,q);check(l.getBlockState(q)==bed,"Bed half protected even if malformed already");
|
||||
var extended=Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.EXTENDED,true);l.setBlock(q,extended,18);use(p,q);check(l.getBlockState(q)==extended,"Extended piston protected");
|
||||
for(var block:List.of(Blocks.PISTON_HEAD,Blocks.MOVING_PISTON)){l.setBlock(q,block.defaultBlockState(),18);use(p,q);check(l.getBlockState(q)==block.defaultBlockState(),"Attached/moving piston part protected");}
|
||||
l.setBlock(q,Blocks.OAK_LOG.defaultBlockState(),18);var axes=EnumSet.noneOf(Direction.Axis.class);for(int i=0;i<3;i++){use(p,q);axes.add(l.getBlockState(q).getValue(RotatedPillarBlock.AXIS));}check(axes.size()==3,"All three native log axes supported");
|
||||
l.setBlock(q,Blocks.PISTON.defaultBlockState(),18);var facings=EnumSet.noneOf(Direction.class);for(int i=0;i<6;i++){use(p,q);facings.add(l.getBlockState(q).getValue(PistonBaseBlock.FACING));}check(facings.size()==6,"Isolated piston supports all six native orientations");
|
||||
// Turning a wall torch toward empty air must not break it or its support.
|
||||
l.setBlock(q.north(),Blocks.STONE.defaultBlockState(),18);var torch=Blocks.WALL_TORCH.defaultBlockState().setValue(WallTorchBlock.FACING,Direction.SOUTH);l.setBlock(q,torch,18);use(p,q);check(l.getBlockState(q)==torch,"Unsupported rotation leaves wall-mounted block intact");l.setBlock(q.north(),Blocks.AIR.defaultBlockState(),18);
|
||||
l.setBlock(q,Blocks.OAK_STAIRS.defaultBlockState(),18);
|
||||
var top=GoldenWrench.cycle(l.getBlockState(q),StairBlock.HALF,false);check(top.getValue(StairBlock.HALF)==Half.TOP,"Steve still has native debug property cycle");
|
||||
// Existing visual edits are preserved; the ordinary key cannot create or reset them.
|
||||
WrenchTextures.turn(l,q,1);use(p,q);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"Golden rotation preserves saved Steve texture data");WrenchTextures.turn(l,q,-1);
|
||||
}
|
||||
private static void use(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
|
||||
private static void equip(ServerPlayer p,Item item){p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(item));p.inventoryMenu.broadcastFullState();}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -47,16 +47,9 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
||||
}
|
||||
private static List<Quad> mesh(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
|
||||
if(sculpture==null)return List.of();var key=new Placement(sculpture,facing,anchor);var cached=MESHES.get(key);if(cached!=null)return cached;
|
||||
// Rotate integer cells before anchoring: odd dimensions never introduce half-pixel offsets.
|
||||
int turns=switch(facing){case WEST -> 1;case NORTH -> 2;case EAST -> 3;default -> 0;};
|
||||
for(int i=0;i<turns;i++)sculpture=sculpture.rotate();
|
||||
var placed=SculptureGeometry.place(sculpture,facing,anchor);sculpture=placed.sculpture();
|
||||
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
||||
int minX=16,minZ=16,maxX=0,maxZ=0;
|
||||
for(var cell:sculpture.cells()){
|
||||
minX=Math.min(minX,cell.x());minZ=Math.min(minZ,cell.z());
|
||||
maxX=Math.max(maxX,cell.x()+1);maxZ=Math.max(maxZ,cell.z()+1);
|
||||
}
|
||||
float ox=(anchor.originX(maxX-minX)-minX)/16f,oz=(anchor.originZ(maxZ-minZ)-minZ)/16f;
|
||||
float ox=placed.offsetX()/16f,oz=placed.offsetZ()/16f;
|
||||
for(var cell:sculpture.cells())for(int side=0;side<6;side++){
|
||||
var s=STEP[side];int x=cell.x()+s[0],y=cell.y()+s[1],z=cell.z()+s[2];
|
||||
if(x>=0&&x<16&&y>=0&&y<16&&z>=0&&z<16&&occupied.get(x|y<<4|z<<8))continue;
|
||||
|
||||
@@ -15,7 +15,7 @@ import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
/** Server-owned debug-stick gestures without changing a block's type or its inventory. */
|
||||
/** Ordinary rotations and Steve debug gestures, both owned by the server. */
|
||||
public final class GoldenWrench {
|
||||
public static final String TEXTURE="sanctuary_texture";
|
||||
private static final Map<ServerPlayer,Map<Block,String>> SELECTED=new WeakHashMap<>();
|
||||
@@ -27,14 +27,19 @@ public final class GoldenWrench {
|
||||
public static void register(){
|
||||
WrenchTextures.register();
|
||||
AttackBlockCallback.EVENT.register((player,level,hand,pos,face)->{
|
||||
if(!player.getItemInHand(hand).is(Multiblocks.WRENCH))return InteractionResult.PASS;
|
||||
if(!isWrench(player.getItemInHand(hand)))return InteractionResult.PASS;
|
||||
if(player.isSpectator())return InteractionResult.FAIL;
|
||||
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;select(p,pos);}
|
||||
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;
|
||||
if(p.getItemInHand(hand).is(Multiblocks.STEVE_WRENCH))select(p,pos);
|
||||
else p.sendSystemMessage(text("rotate_help"),true);}
|
||||
return InteractionResult.SUCCESS;
|
||||
});
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler,server)->{SELECTED.remove(handler.player);LAST_SELECTION.remove(handler.player);});
|
||||
}
|
||||
public static boolean isWrench(ItemStack stack){return stack.is(Multiblocks.WRENCH)||stack.is(Multiblocks.STEVE_WRENCH);}
|
||||
public static boolean allowed(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||
var tool=p.getItemInHand(hand);
|
||||
if(!isWrench(tool))return false;
|
||||
return p.isAlive()&&!p.isSpectator()&&p.getAbilities().mayBuild&&p.level().hasChunkAt(pos)
|
||||
&&p.isWithinBlockInteractionRange(pos,0)&&p.level().mayInteract(p,pos)
|
||||
&&p.mayUseItemAt(pos,face,p.getItemInHand(hand))
|
||||
@@ -66,8 +71,12 @@ public final class GoldenWrench {
|
||||
var property=choices.get(next);selected.put(state.getBlock(),property);
|
||||
p.sendSystemMessage(text("selected",label(property),value(p,pos,state,property)),true);
|
||||
}
|
||||
public static void change(ServerPlayer p,BlockPos pos){
|
||||
public static void change(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||
if(!allowed(p,pos,hand,face))return;
|
||||
var state=p.level().getBlockState(pos);if(state.isAir()||assembled(p,pos,state))return;
|
||||
if(p.getItemInHand(hand).is(Multiblocks.WRENCH)){
|
||||
p.sendSystemMessage(text(WrenchRotation.turn(p.level(),pos,p.isShiftKeyDown())),true);return;
|
||||
}
|
||||
var name=selected(p,state);SELECTED.computeIfAbsent(p,key->new HashMap<>()).put(state.getBlock(),name);
|
||||
if(name.equals(TEXTURE))WrenchTextures.turn(p.level(),pos,p.isShiftKeyDown()?-1:1);
|
||||
else {
|
||||
|
||||
@@ -23,7 +23,15 @@ import static fr.koka.sanctuary.multiblock.MultiblockData.Machine;
|
||||
|
||||
public final class Multiblocks {
|
||||
public static final Item WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("golden_wrench"),
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1)
|
||||
.component(net.minecraft.core.component.DataComponents.LORE,new net.minecraft.world.item.component.ItemLore(List.of(Component.translatable("sanctuary.wrench.rotate_help"))))
|
||||
.setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
||||
public static final Item STEVE_WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("steve_wrench"),
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC)
|
||||
.component(net.minecraft.core.component.DataComponents.ENCHANTMENT_GLINT_OVERRIDE,true)
|
||||
.component(net.minecraft.core.component.DataComponents.LORE,new net.minecraft.world.item.component.ItemLore(List.of(
|
||||
Component.translatable("sanctuary.wrench.steve_power"),Component.translatable("sanctuary.wrench.steve_help"))))
|
||||
.setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("steve_wrench")))));
|
||||
public static final MenuType<MultiblockMenu> MENU=Registry.register(BuiltInRegistries.MENU,SanctuaryMod.id("multiblock"),
|
||||
new ExtendedMenuType<>(MultiblockMenu::new,MultiblockMenu.View.CODEC));
|
||||
private record Preview(ServerLevel level,BlockPos origin,boolean furnace,long until){}
|
||||
@@ -34,9 +42,9 @@ public final class Multiblocks {
|
||||
GoldenWrench.register();
|
||||
SuperPistons.register();
|
||||
Metatable.register();
|
||||
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->out.accept(WRENCH));
|
||||
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->{out.accept(WRENCH);out.accept(STEVE_WRENCH);});
|
||||
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
|
||||
boolean wrench=player.getItemInHand(hand).is(WRENCH);
|
||||
boolean wrench=GoldenWrench.isWrench(player.getItemInHand(hand));
|
||||
if(world.isClientSide())return wrench?InteractionResult.SUCCESS:InteractionResult.PASS;
|
||||
if(!(player instanceof ServerPlayer p)||p.isSpectator())return InteractionResult.PASS;
|
||||
var level=p.level();var pos=hit.getBlockPos();var machine=MultiblockData.get(level).at(pos);
|
||||
@@ -45,7 +53,7 @@ public final class Multiblocks {
|
||||
if(!wrench&&p.isShiftKeyDown()&&(!p.getMainHandItem().isEmpty()||!p.getOffhandItem().isEmpty()))return InteractionResult.PASS;
|
||||
if(!level.mayInteract(p,pos)||!p.mayUseItemAt(pos,hit.getDirection(),p.getItemInHand(hand)))return InteractionResult.FAIL;
|
||||
if(wrench){if(!GoldenWrench.allowed(p,pos,hand,hit.getDirection()))return InteractionResult.FAIL;
|
||||
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos);
|
||||
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos,hand,hit.getDirection());
|
||||
return InteractionResult.SUCCESS;}
|
||||
if(ready(level,machine)&&accessible(p,machine))open(p,machine,pos,0,"",0);
|
||||
else p.sendSystemMessage(text("unavailable"),true);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.koka.sanctuary.multiblock;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import static net.minecraft.world.level.block.state.properties.BlockStateProperties.*;
|
||||
|
||||
/** Only native orientation properties; fluid, contents and gameplay state are never selectable. */
|
||||
public final class WrenchRotation {
|
||||
private static final List<Property<?>> ORIENTATIONS=List.of(HORIZONTAL_FACING,FACING,FACING_HOPPER,AXIS,HORIZONTAL_AXIS,ROTATION_16,ORIENTATION);
|
||||
|
||||
public static String turn(ServerLevel level,BlockPos pos,boolean reverse){
|
||||
var before=level.getBlockState(pos);
|
||||
if(linked(before))return "linked";
|
||||
var property=ORIENTATIONS.stream().filter(before::hasProperty).findFirst().orElse(null);
|
||||
if(property==null)return "no_rotation";
|
||||
var oriented=GoldenWrench.cycle(before,property,reverse);
|
||||
if(!oriented.canSurvive(level,pos))return "unsupported";
|
||||
// Stairs and connections must reflect their real neighbours, not a debug-stick shape.
|
||||
var after=Block.updateFromNeighbourShapes(oriented,level,pos);
|
||||
if(!after.is(before.getBlock())||linked(after)||after.getFluidState()!=before.getFluidState()
|
||||
||!after.canSurvive(level,pos))return "unsupported";
|
||||
level.setBlock(pos,after,Block.UPDATE_ALL);
|
||||
return "rotated";
|
||||
}
|
||||
private static boolean linked(BlockState state){
|
||||
// A single-block operation cannot rotate another half, an attached head or a joined inventory.
|
||||
return state.hasProperty(DOUBLE_BLOCK_HALF)||state.hasProperty(BED_PART)||state.hasProperty(SIDE_CHAIN_PART)&&state.getValue(SIDE_CHAIN_PART).isConnected()
|
||||
||state.hasProperty(PISTON_TYPE)||state.hasProperty(EXTENDED)&&state.getValue(EXTENDED)
|
||||
||state.hasProperty(CHEST_TYPE)&&state.getValue(CHEST_TYPE)!=ChestType.SINGLE;
|
||||
}
|
||||
private WrenchRotation(){}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
|
||||
import net.minecraft.world.phys.shapes.CubeVoxelShape;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** One integer placement shared by rendering, picking and server-side collisions. */
|
||||
public final class SculptureGeometry {
|
||||
public record Placement(VoxelSculpture sculpture, int offsetX, int offsetZ) {}
|
||||
|
||||
private SculptureGeometry() {}
|
||||
|
||||
public static Placement place(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
|
||||
int turns = switch (facing) { case WEST -> 1; case NORTH -> 2; case EAST -> 3; default -> 0; };
|
||||
for (int i = 0; i < turns; i++) sculpture = sculpture.rotate();
|
||||
int minX = 16, minZ = 16, maxX = 0, maxZ = 0;
|
||||
for (var cell : sculpture.cells()) {
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x() + 1); maxZ = Math.max(maxZ, cell.z() + 1);
|
||||
}
|
||||
return new Placement(sculpture, anchor.originX(maxX - minX) - minX,
|
||||
anchor.originZ(maxZ - minZ) - minZ);
|
||||
}
|
||||
|
||||
public static VoxelShape shape(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
|
||||
var placed = place(sculpture, facing, anchor);
|
||||
var grid = new BitSetDiscreteVoxelShape(16, 16, 16);
|
||||
for (var cell : placed.sculpture().cells()) {
|
||||
grid.fill(cell.x() + placed.offsetX(), cell.y(), cell.z() + placed.offsetZ());
|
||||
}
|
||||
// Build the bounded native voxel grid directly, without thousands of box unions.
|
||||
return new CubeVoxelShape(grid);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import net.minecraft.world.level.material.PushReaction;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.phys.shapes.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
|
||||
@@ -57,10 +58,31 @@ public final class Statuary {
|
||||
});
|
||||
}
|
||||
private static Block block(String name,Block block){return Registry.register(BuiltInRegistries.BLOCK,SanctuaryMod.id(name),block);}
|
||||
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
|
||||
private static Item blockItem(String name,Block block){
|
||||
var properties=new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix();
|
||||
return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),block==BLOCK?new SculptureItem(block,properties):new BlockItem(block,properties));
|
||||
}
|
||||
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
|
||||
private static final class SculptureItem extends BlockItem {
|
||||
SculptureItem(Block block,Item.Properties properties){super(block,properties);}
|
||||
@Override protected boolean canPlace(BlockPlaceContext context,BlockState state){
|
||||
if(!super.canPlace(context,state))return false;
|
||||
// The destination has no block entity yet: use the carried model for the native entity-overlap check.
|
||||
var model=StatuaryEntity.sculpture(context.getItemInHand());
|
||||
return model==null||context.getLevel().isUnobstructed(null,SculptureGeometry.shape(model,state.getValue(FACING),state.getValue(ANCHOR)).move(context.getClickedPos()));
|
||||
}
|
||||
}
|
||||
private static final class SculptureBlock extends Block implements EntityBlock {
|
||||
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
|
||||
SculptureBlock(){super(Statuary.properties("statuary").dynamicShape().isSuffocating((s,l,p)->false).isViewBlocking((s,l,p,box)->false));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
|
||||
@Override protected VoxelShape getShape(BlockState state,BlockGetter level,BlockPos pos,CollisionContext context){
|
||||
return level.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.shape(state):Shapes.empty();
|
||||
}
|
||||
// Lighting metadata is cached per blockstate, so it cannot describe each block entity's voxels.
|
||||
// Decorative sculptures transmit light; their faces still receive ordinary world lighting.
|
||||
@Override protected VoxelShape getOcclusionShape(BlockState state){return Shapes.empty();}
|
||||
@Override protected boolean propagatesSkylightDown(BlockState state){return true;}
|
||||
@Override protected int getLightDampening(BlockState state){return 0;}
|
||||
@Override protected float getShadeBrightness(BlockState state,BlockGetter level,BlockPos pos){return 1;}
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING,ANCHOR);}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection()).setValue(ANCHOR,SculptureAnchor.placement(context));}
|
||||
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING))).setValue(ANCHOR,state.getValue(ANCHOR).rotate(rotation));}
|
||||
|
||||
@@ -9,13 +9,26 @@ import net.minecraft.world.item.component.TypedEntityData;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** New, additive block data only. No ticking or mutation of pre-existing world formats. */
|
||||
public final class StatuaryEntity extends BlockEntity {
|
||||
private VoxelSculpture sculpture;
|
||||
private CompoundTag opaque;
|
||||
private record ShapeCache(VoxelSculpture sculpture,BlockState state,VoxelShape shape){}
|
||||
private volatile ShapeCache shapeCache;
|
||||
// Keep unknown/future data recoverable by picking the small base, without a phantom full cube.
|
||||
private static final VoxelShape MISSING_SHAPE=Block.box(6,0,6,10,4,10);
|
||||
public StatuaryEntity(BlockPos pos,BlockState state){super(Statuary.TYPE,pos,state);}
|
||||
public VoxelSculpture sculpture(){return sculpture;}
|
||||
public VoxelShape shape(BlockState state){
|
||||
var model=sculpture;if(model==null)return MISSING_SHAPE;
|
||||
var cached=shapeCache;
|
||||
if(cached!=null&&cached.sculpture()==model&&cached.state()==state)return cached.shape();
|
||||
var shape=SculptureGeometry.shape(model,state.getValue(Statuary.FACING),state.getValue(Statuary.ANCHOR));
|
||||
shapeCache=new ShapeCache(model,state,shape);return shape;
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput out){
|
||||
super.saveAdditional(out);
|
||||
if(opaque!=null){out.store("sculpture",CompoundTag.CODEC,opaque);return;}
|
||||
@@ -23,7 +36,7 @@ public final class StatuaryEntity extends BlockEntity {
|
||||
out.store("sculpture",CompoundTag.CODEC,encode(sculpture));
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput in){
|
||||
super.loadAdditional(in);sculpture=null;opaque=null;
|
||||
super.loadAdditional(in);sculpture=null;opaque=null;shapeCache=null;
|
||||
var data=in.read("sculpture",CompoundTag.CODEC).orElse(null);if(data==null)return;
|
||||
try{
|
||||
if(data.getIntOr("schema",-1)!=1)throw new IllegalArgumentException("schema");
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:item/steve_wrench"
|
||||
}
|
||||
}
|
||||
@@ -2233,5 +2233,13 @@
|
||||
"sanctuary.wrench.selected": "Selected property: %s = %s · right click to change",
|
||||
"sanctuary.wrench.changed": "%s = %s",
|
||||
"sanctuary.wrench.texture": "Texture rotation",
|
||||
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components."
|
||||
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components.",
|
||||
"item.sanctuary.steve_wrench": "Steve’s Wrench",
|
||||
"sanctuary.wrench.rotate_help": "Right click: orient · Sneak: reverse · Multiblocks: assemble/separate",
|
||||
"sanctuary.wrench.steve_help": "Left click: select property · Right click: change · Sneak: reverse",
|
||||
"sanctuary.wrench.rotated": "Orientation changed",
|
||||
"sanctuary.wrench.no_rotation": "This block has no vanilla orientation.",
|
||||
"sanctuary.wrench.linked": "This block has linked parts: separate or retract it before rotating.",
|
||||
"sanctuary.wrench.unsupported": "This orientation does not fit the neighbouring blocks or support.",
|
||||
"sanctuary.wrench.steve_power": "Free editing of block states and textures"
|
||||
}
|
||||
|
||||
@@ -2233,5 +2233,13 @@
|
||||
"sanctuary.wrench.selected": "État choisi : %s = %s · clic droit pour modifier",
|
||||
"sanctuary.wrench.changed": "%s = %s",
|
||||
"sanctuary.wrench.texture": "Rotation des textures",
|
||||
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants."
|
||||
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants.",
|
||||
"item.sanctuary.steve_wrench": "Clé à molette de Steve",
|
||||
"sanctuary.wrench.rotate_help": "Clic droit : orienter · Maj : sens inverse · Multiblocs : assembler/dissocier",
|
||||
"sanctuary.wrench.steve_help": "Clic gauche : choisir un état · Clic droit : modifier · Maj : sens inverse",
|
||||
"sanctuary.wrench.rotated": "Orientation modifiée",
|
||||
"sanctuary.wrench.no_rotation": "Ce bloc ne possède pas d’orientation vanilla.",
|
||||
"sanctuary.wrench.linked": "Bloc lié à une autre partie : dissocie-le ou rétracte-le avant de le tourner.",
|
||||
"sanctuary.wrench.unsupported": "Cette orientation ne convient pas aux blocs voisins ou au support.",
|
||||
"sanctuary.wrench.steve_power": "Édition libre des états et des textures"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"parent": "minecraft:item/handheld",
|
||||
"textures": {"layer0": "sanctuary:item/steve_wrench"}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 323 B |
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.121"
|
||||
version = "beta.123"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user