Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
248dff47b7 | ||
|
|
6aaabf1cb9 | ||
|
|
72e9263339 | ||
|
|
3fd64e1f2a | ||
|
|
8af406a028 | ||
|
|
0a81b3c147 | ||
|
|
68fd97d287 | ||
|
|
0f1efdec83 | ||
|
|
5c666a4ebc | ||
|
|
e3ca9fcd63 | ||
|
|
22e58b4b91 | ||
|
|
ed0e0b26aa | ||
|
|
21845bd736 | ||
|
|
4a6f63c52d | ||
|
|
6166d0aa9e | ||
|
|
064701f210 | ||
|
|
1eaa976ecf | ||
|
|
8c0020c774 |
@@ -20,9 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.111** reprend ce socle et réorganise l’atelier d’argile avec un
|
||||
cadre Minecraft nine-slice ; les statuaires suivent le regard à la pose.
|
||||
Le [ticket](docs/clay-workshop-ui-beta111.md) décrit la compatibilité et les essais.
|
||||
La version **beta.120** étend la clé dorée aux états des blocs : clic gauche
|
||||
pour choisir une propriété, clic droit pour changer sa valeur, Maj pour inverser.
|
||||
La terre et les modèles de blocs peuvent aussi tourner leurs textures.
|
||||
Le [ticket de livraison](docs/golden-wrench-beta120.md) détaille les contrôles,
|
||||
la compatibilité et la distribution. Tests natifs, build complet et deux packs
|
||||
vérifiés localement.
|
||||
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.
|
||||
@@ -33,13 +36,80 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
## 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,
|
||||
orientation et ancrage conservés. Le Fourneau accepte les dépôts dans chacune
|
||||
de ses 27 cases et explique sa chauffe commune.
|
||||
[Contrat et vérifications](docs/sculpture-materials-beta116.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.116/Sanctuary-beta.116.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.116/Sanctuary-Test-beta.116.mrpack).
|
||||
|
||||
## beta.115 — Fourneau : dépôts et chauffe
|
||||
|
||||
Correctif du décalage par trois cases, combustible stable et progression
|
||||
par cuisson. [Diagnostic et essais](docs/fourneau-transfers-beta115.md).
|
||||
Étape locale intégrée à la livraison commune beta.116.
|
||||
|
||||
## beta.114 — Animaux portés et fuite en vol
|
||||
|
||||
Un animal volant porté entraîne son porteur après un coup. La trajectoire
|
||||
aléatoire prend brièvement le dessus sur les commandes ; déposer l’animal
|
||||
interrompt le vol. [Contrat et vérifications](docs/carried-flight-panic-beta114.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.114/Sanctuary-beta.114.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.114/Sanctuary-Test-beta.114.mrpack).
|
||||
|
||||
## beta.113 — Sculptures d’argile et alignement
|
||||
|
||||
Pose contre le bord visé, orientation selon le regard et coordonnées entières
|
||||
pour chaque voxel, y compris les modèles de largeur impaire. L’accroche est
|
||||
conservée à la reconnexion. [Contrat et vérifications](docs/clay-sculpture-grid-beta113.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.113/Sanctuary-beta.113.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.113/Sanctuary-Test-beta.113.mrpack).
|
||||
|
||||
## beta.112 — Atelier d’argile et hotbar
|
||||
|
||||
Neuf cases de hotbar et deux emplacements de fabrication, dans un cadre fixe.
|
||||
Les transferts restent dans la hotbar ; une barre pleine ne consomme pas d’argile.
|
||||
[Contrat et vérifications](docs/clay-workshop-hotbar-beta112.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.112/Sanctuary-beta.112.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.112/Sanctuary-Test-beta.112.mrpack).
|
||||
|
||||
## beta.111 — Atelier d’argile et pose orientée
|
||||
|
||||
Cadre nine-slice, aperçu, fabrication et inventaire dans un menu commun.
|
||||
Les statuaires s’orientent selon le regard horizontal à chaque pose ; les
|
||||
anciens modèles gardent leur apparence. [Contrat et vérifications](docs/clay-workshop-ui-beta111.md).
|
||||
[Pack normal](build/Sanctuary-beta.111.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.111.mrpack).
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.111/Sanctuary-beta.111.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.111/Sanctuary-Test-beta.111.mrpack) ·
|
||||
[Release](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.111).
|
||||
|
||||
## beta.110 — Atelier d’argile et neige saisonnière réunis
|
||||
|
||||
@@ -2062,7 +2132,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.111 |
|
||||
| Sanctuary / pack | beta.112 |
|
||||
|
||||
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
|
||||
@@ -2091,7 +2161,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.111.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.112.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).
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## STAT-03 — Atelier d’argile et hotbar — beta.112
|
||||
|
||||
Branche `codex/clay-workshop-hotbar-beta112`. [Contrat et vérifications](clay-workshop-hotbar-beta112.md).
|
||||
Onze cases : argile, résultat et hotbar active ; hauteur fixe et transferts
|
||||
limités aux neuf cases, même avec un inventaire agrandi.
|
||||
|
||||
## STAT-02 — Atelier d’argile et orientation — beta.111
|
||||
|
||||
Branche `codex/clay-workshop-ui-beta111`. [Contrat et vérifications](clay-workshop-ui-beta111.md).
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# CARRY-08 — Fuite des animaux volants portés — beta.114
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/carried-flight-panic-beta114`.
|
||||
|
||||
Un coup reçu par un animal volant porté sur la tête déclenche une fuite de
|
||||
40 à 60 ticks : il entraîne son porteur vers le haut, dans une direction
|
||||
aléatoire, avec des embardées. Le serveur choisit le départ et la trajectoire,
|
||||
transmis au client pour la physique native. Les touches de déplacement et le
|
||||
regard ne pilotent pas cette brève fuite. La caméra reste libre. Un nouveau coup peut relancer la fuite avec un autre
|
||||
cap après dix ticks, sans cumuler les vitesses.
|
||||
|
||||
Le porteur peut viser et frapper son animal volant. Les animaux ordinaires
|
||||
reçoivent les dégâts habituels ; le familier conserve son coup amical sans
|
||||
perte de santé. Les attaques extérieures acceptées déclenchent aussi la fuite.
|
||||
Les autres protections entre passagers et porteurs restent en place.
|
||||
Les espèces reprennent le groupe déjà utilisé pour planer, poule comprise.
|
||||
|
||||
La vitesse reste bornée, les collisions sont natives et aucun bloc n’est
|
||||
traversé ni déplacé. Déposer l’animal (Maj + clic droit), le perdre, mourir,
|
||||
se déconnecter ou changer de dimension arrête la propulsion. Immersion,
|
||||
mode spectateur, vol créatif, élytres et porteur lui-même passager l’inhibent.
|
||||
La fin de fuite rend le contrôle au joueur et conserve le planeur existant
|
||||
si l’animal reste porté. Les montures colossales gardent leur pilotage actuel.
|
||||
|
||||
L’état de fuite est temporaire, synchronisé et non sauvegardé. Aucun format
|
||||
persistant, identifiant existant ou monde personnel n’est modifié. Les nouveaux
|
||||
retours d’interface sont traduits FR/EN. Client et serveur utilisent beta.114.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`CarryPanic114ClientChecks` passe en **56 secondes** avec un serveur intégré
|
||||
Minecraft 26.3 et un monde plat jetable de graine 114 :
|
||||
|
||||
- Véritable ciblage du perroquet sur la tête et frappe avec la touche native,
|
||||
perte de santé de l’animal ordinaire, décollage et déplacement du joueur.
|
||||
- Trajectoire synchronisée et priorité sur les touches avant/droite/saut et
|
||||
le regard ; comparaison exacte avec la vitesse du tick de physique.
|
||||
- Expiration, suppression de l’état temporaire et retour au planeur normal.
|
||||
- 32 trajectoires tirées côté serveur couvrent les quatre quadrants, toutes
|
||||
dans les bornes de durée et de vitesse ; Maj + clic droit natif interrompt.
|
||||
- Sept espèces ordinaires : poule, chauve-souris, abeille, allay, perroquet,
|
||||
blaze et breeze. Une vache portée reste protégée et ne déclenche aucun vol.
|
||||
- Attaque extérieure, mort, activation du vol créatif et détachement arrêtent
|
||||
ou déclenchent la réaction selon le contrat.
|
||||
- Plafond bas : aucune traversée ; le passager bloqué est déposé par les
|
||||
contrôles de place existants et la propulsion s’interrompt.
|
||||
- Véritable coup amical sur un familier perroquet : lancement sans perte de
|
||||
santé, puis arrêt immédiat lors du détachement.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryCarryPanic114ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `CARRY114_NATIVE_FLIGHT_PASS` et `CARRY114_PASS` dans
|
||||
`build/carry-panic114-client.log`. Capture en jeu relue dans
|
||||
`build/carry-panic114-evidence/`. Le GameTest dédié reste exclu conformément
|
||||
au refus antérieur de son EULA. Pas de test Windows, de réseau distant ou de
|
||||
latence simulée ; aucun monde personnel n’a été ouvert.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 22 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. Les classes et ressources hors de ce ticket restent
|
||||
identiques à beta.113, notamment les sculptures d’argile. Les deux packs
|
||||
contiennent le même JAR, sans monde, GLB ni classes de test.
|
||||
|
||||
- Sanctuary-beta.114.mrpack : 10179428 octets, SHA-256
|
||||
`92aac61c2c00a2206f3f5ac3d170ab058f6765526cf5a2a64c5336261db3f5bf`.
|
||||
- Sanctuary-Test-beta.114.mrpack : 10198350 octets, SHA-256
|
||||
`5e8ec1f18e34ca51e09e2738f2b04f9a1bbac5d14c7a7c2d766218a4cff6021f`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`18105b76f63008210c62d5728f999c7abc083a861ecc1de2c7ee87262dde5a40`.
|
||||
|
||||
Reçu local : `build/carry-panic114-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.114](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.114)
|
||||
est publiée depuis `21845bd7367a260ee6a3d189417a8620aa5bf738`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `c26f3bb59491c9bb4bbdd27f59571ef84f417f7b`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.114 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.114/` dans l’instance existante. Reçus locaux :
|
||||
`build/carry-panic114-isolated.json` et `build/carry-panic114-prism.json`.
|
||||
@@ -0,0 +1,90 @@
|
||||
# STAT-04 — Sculptures d’argile et accroche au bord — beta.113
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-sculpture-grid-beta113`.
|
||||
|
||||
Les miniatures de l’atelier deviennent des « sculptures d’argile » / « Clay
|
||||
sculptures ». Le nom du modèle reste dans leur infobulle. Les statues en blocs
|
||||
du Métabli restent une catégorie distincte.
|
||||
|
||||
À la pose, le bord horizontal le plus proche du point visé accueille la
|
||||
sculpture. L’autre axe est centré au pixel entier le plus proche, sur la grille
|
||||
de 1/16 de bloc. Sur une face latérale, elle touche le bord du bloc support.
|
||||
À égalité exacte entre deux bords, l’axe Z départage ; au centre exact,
|
||||
le bord côté joueur est retenu. L’orientation suit toujours le regard.
|
||||
Le bouton Tourner prépare le modèle avant fabrication. Les marges horizontales vides du modèle ne créent pas de décalage au bord.
|
||||
Tous les sommets restent
|
||||
sur la grille entière, même avec des dimensions impaires et après rotation.
|
||||
|
||||
## Compatibilité préalable
|
||||
|
||||
La propriété native additive `anchor` du bloc `sanctuary:statuary` mémorise
|
||||
`center`, `north`, `south`, `east` ou `west`. Un ancien état sans cette propriété
|
||||
prend `center` : il reste centré, avec la correction visuelle du demi-voxel.
|
||||
Le schéma 1, les voxels, noms de modèles et identifiants ne changent pas.
|
||||
Il n’y a aucun parcours ni réécriture de chunks existants. La sauvegarde native
|
||||
conserve l’accroche ; les rotations et miroirs de structures la transforment.
|
||||
Casser et reposer choisit une nouvelle accroche sans modifier le modèle.
|
||||
Les nouveaux objets emploient le nom traduit de leur type ; les noms déjà
|
||||
enregistrés et les noms personnalisés restent conservés. La collision cubique
|
||||
et la limite d’une sculpture par bloc restent inchangées. Client et serveur
|
||||
doivent utiliser ensemble beta.113.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Statuary106ClientChecks`, étendu par `Sculpture113ClientChecks`,
|
||||
passe en **1 min 25 s**, sur Minecraft 26.3 et un monde plat jetable de graine 106 :
|
||||
|
||||
- 5 120 combinaisons de largeur/profondeur, orientation et accroche : contrôle
|
||||
de la géométrie émise, grille exacte de 1/16 et absence de dépassement du bloc.
|
||||
- Modèles avec marges transparentes, repères colorés pour vérifier le sens du
|
||||
rendu et égalité de la géométrie centrée entre l’objet et le bloc.
|
||||
- Vrais clics de pose sur les quatre bords : orientation, bord côté serveur,
|
||||
réception côté client, butin/repose et conservation après reconnexion.
|
||||
- Contextes natifs de pose sur les faces latérales et coordonnées négatives.
|
||||
- Rotation/miroir de chaque combinaison d’accroche et d’orientation ; lecture
|
||||
des anciens états sans `anchor`, avec et sans `facing`.
|
||||
- Noms français/anglais des objets fabriqués, menu aux échelles 2 et 3,
|
||||
fabrication payée, Maj-clic limité à la hotbar et régression Métabli.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `STATUARY113_PASS` et `SCULPTURE113_GEOMETRY_PASS` dans
|
||||
`build/clay-grid113-client.log`. Captures relues dans
|
||||
`build/clay-grid113-evidence/`. Le GameTest dédié reste exclu conformément au
|
||||
refus antérieur de son EULA ; ces essais utilisent le serveur intégré macOS.
|
||||
Pas de validation Windows ni depuis deux ordinateurs. Aucun monde personnel
|
||||
n’a été ouvert.
|
||||
|
||||
## Livraison
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 11 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. Les classes et ressources hors du ticket sont identiques
|
||||
à beta.112. Les deux packs contiennent le même JAR, sans monde, GLB ni test.
|
||||
|
||||
- Sanctuary-beta.113.mrpack : 10173807 octets, SHA-256
|
||||
`ed082ff29a0e9a70fd6786bc024241b276cb516bc09fb5151d6cb324604429bb`.
|
||||
- Sanctuary-Test-beta.113.mrpack : 10192733 octets, SHA-256
|
||||
`387f54dc77884a9c9bccdcc1904778ca7f0b2fbda126754fdc49b10a492e4602`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`e242d9fd2edeede922a72a10e988e29c2b49dea52105cfc9fc8573154849ead3`.
|
||||
|
||||
Reçu local : `build/clay-grid113-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.113](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.113)
|
||||
est publiée depuis `6166d0aa9eba3c3dd84b8b9f1501e3346231ac82`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `e4c16d517bfda4f2b688604cc61945a46a4513a9`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.113 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.113/` dans l’instance existante. Reçus locaux :
|
||||
`build/clay-grid113-isolated.json` et `build/clay-grid113-prism.json`.
|
||||
@@ -0,0 +1,79 @@
|
||||
# STAT-03 — Atelier d’argile limité à la hotbar — beta.112
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-workshop-hotbar-beta112`.
|
||||
|
||||
Le menu utilise uniquement les neuf cases de la hotbar active du joueur,
|
||||
avec les deux emplacements de fabrication. Les rangées de réserve, y compris
|
||||
les extensions Sanctuary, ne sont ni ajoutées au menu ni utilisées par ses
|
||||
transferts. La rangée choisie avant l’ouverture reste la hotbar courante.
|
||||
La touche Tab retrouve la navigation des boutons dans ce menu.
|
||||
|
||||
Le cadre nine-slice tient dans une hauteur fixe de 202 pixels d’interface.
|
||||
Il ne grandit plus avec la capacité de l’inventaire. Le Maj-clic du résultat
|
||||
cherche uniquement une place dans la hotbar ; sans place, il ne consomme pas
|
||||
d’argile. Les touches 1–9 restent utilisables, pas l’échange avec la seconde
|
||||
main. À la fermeture, la restitution native des objets restants est conservée.
|
||||
|
||||
Les identifiants, sauvegardes, données de sculpture et orientations beta.111
|
||||
restent inchangés. Aucun monde existant n’est ouvert ou réécrit. Client et
|
||||
serveur doivent utiliser ensemble beta.112 pour les nouveaux indices du menu
|
||||
temporaire (11 cases au total).
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
Le parcours `Statuary106ClientChecks`, étendu à beta.112, passe en **1 min 13 s**
|
||||
sur Minecraft 26.3 avec serveur intégré et monde plat jetable de graine 106 :
|
||||
|
||||
- Menu FR/EN aux échelles 2 et 3 : hauteur fixe, neuf cases de hotbar native,
|
||||
deux cases de fabrication et absence de panneau d’inventaire évolutif.
|
||||
- Fabrication par Maj-clic depuis la hotbar, avec de l’argile aussi présente
|
||||
dans la réserve, l’extension et la seconde main : ces stocks restent intacts.
|
||||
- Rejet des indices de cases absents et de l’échange avec la seconde main.
|
||||
- Hotbar remplie de neuf piles : Maj-clic sans fabrication ni consommation.
|
||||
- Une seule place dans une pile de 63 statuaires : production d’un seul objet,
|
||||
coût d’une argile, aucune statue transférée dans une case cachée.
|
||||
- Restitution de l’argile restante à la fermeture, seize teintes, quatre
|
||||
orientations, butin/repose et sauvegarde/reconnexion sans fichier GLB.
|
||||
- Régression du Métabli et annulation d’import conservées.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueur `STATUARY112_PASS` dans `build/clay-hotbar112-client.log` ; captures
|
||||
relues dans `build/clay-hotbar112-evidence/`. Le GameTest dédié reste exclu
|
||||
conformément au refus antérieur de son EULA. Pas de test Windows ni depuis
|
||||
deux ordinateurs ; aucun monde personnel n’a été ouvert.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **3 min 6 s**, 125 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. Les classes et ressources hors de ce ticket sont identiques
|
||||
à beta.111, notamment les données et le rendu orienté des statuaires.
|
||||
Les deux packs contiennent le même JAR, sans monde, GLB ni classes de tests.
|
||||
|
||||
- Pack normal : 10 169 337 octets, SHA-256
|
||||
`55723a5d7cb99e350ad8fe150e92e0567430b5261ca4685abd900be34068658f`.
|
||||
- Pack Test : 10 188 262 octets, SHA-256
|
||||
`987818bf817ad80fd055b97e84bf4775fca54820d7c3055d6fd4089dc7a5a3c0`.
|
||||
- JAR : SHA-256
|
||||
`9007a770bbe69db5846f93140d910514f10839cc34ec7c6315a9381d8850adaa`.
|
||||
|
||||
Reçu local : `build/clay-hotbar112-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.112](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.112)
|
||||
est publiée depuis `1eaa976ecf328c6c20afe5e4eb67c48118f87f95`. Le tag exact
|
||||
et les artefacts sont immuables ; les téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `573c5b0d84fcb3e9fec7cc8a83981cf35eaa1ead`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans **Sanctuary Beta** ont
|
||||
réussi. Un seul JAR Sanctuary beta.112 est actif, et le second passage conserve
|
||||
les mêmes hashes. Les **923 fichiers personnels et réglages suivis** restent
|
||||
identiques. Aucun monde personnel n’a été ouvert. La sauvegarde ciblée est
|
||||
`sanctuary-backups/before-beta.112/` dans l’instance existante. Reçus locaux :
|
||||
`build/clay-hotbar112-isolated.json` et `build/clay-hotbar112-prism.json`.
|
||||
@@ -80,5 +80,20 @@ Les deux MRpack contiennent le même JAR vérifié, sans monde, GLB ni classe de
|
||||
- JAR Sanctuary : SHA-256
|
||||
`b3e967db7b336b970e4121981e3ef8140fe90f0ecc5850ce90a70caaf3772684`.
|
||||
|
||||
Reçu local : `build/clay-ui111-artifact.json`. Publication et synchronisation
|
||||
de l’instance stable préparées après ces contrôles.
|
||||
Reçu local : `build/clay-ui111-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.111](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.111)
|
||||
est publiée depuis le commit source `289697becf19db4acf50d224b1dd3d753eff1153`.
|
||||
Le tag exact et les artefacts sont immuables ; leurs téléchargements publics
|
||||
ont été vérifiés. Le canal packwiz avance au commit
|
||||
`87c9f81761309316c601ed32fa922c1a5893952c`.
|
||||
|
||||
Deux synchronisations isolées puis deux passages dans l’instance existante
|
||||
**Sanctuary Beta** ont réussi. Un seul JAR Sanctuary beta.111 est actif ; le
|
||||
second passage conserve les mêmes hashes. Les **923 fichiers personnels et
|
||||
réglages suivis** restent identiques. Aucun monde personnel n’a été ouvert.
|
||||
La sauvegarde ciblée est dans `sanctuary-backups/before-beta.111/` de cette
|
||||
instance. Reçus : `build/clay-ui111-isolated.json` et
|
||||
`build/clay-ui111-prism.json`.
|
||||
|
||||
@@ -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,74 @@
|
||||
# beta.115 — Dépôts et chauffe du Fourneau
|
||||
|
||||
Ticket sur `codex/fourneau-transfers-beta115`, après le signalement de piles
|
||||
qui se réalignent pendant les dépôts rapides avec Maj, dans Ingrédients et
|
||||
Combustibles, et d'une chauffe difficile à comprendre.
|
||||
|
||||
## Diagnostic
|
||||
|
||||
La méthode `mayPlace` de la case serveur utilisait `index`, masqué par le
|
||||
champ hérité `Slot.index` (position affichée). Elle validait donc successivement
|
||||
une entrée, un combustible et une sortie, au lieu des 27 entrées ou des
|
||||
27 combustibles. Le client acceptait le dépôt puis le serveur le refusait.
|
||||
Le test reproduit le refus d’un déplacement de la case 1 vers la case 23.
|
||||
Le correctif utilise explicitement `physicalIndex` pour la validation.
|
||||
|
||||
## Comportement
|
||||
|
||||
Les transferts Maj dans les vues complètes du Fourneau sont anticipés sur le
|
||||
client, avec les mêmes destinations et règles de combustible que le serveur.
|
||||
Les cases se remplissent dans l'ordre, après fusion des piles compatibles.
|
||||
Un dépôt manuel conserve la case choisie ; aucun tri périodique des ingrédients
|
||||
n'est ajouté. Les recherches masquent une partie du stock : leurs transferts
|
||||
entrants continuent d'attendre la réponse du serveur, qui connaît tout le stock.
|
||||
|
||||
La chauffe emprunte du combustible à une autre case uniquement pendant le tick
|
||||
natif. La pile non brûlée retourne immédiatement à sa case d'origine, avec tous
|
||||
ses composants. Les restes natifs restent associés à leur cuisson : un seau de
|
||||
lave devient un seau, puis un seau d'eau avec une éponge mouillée.
|
||||
|
||||
Les 27 entrées ont chacune une barre et une infobulle : cuisson en cours,
|
||||
manque de combustible avec progression conservée, résultat bloqué, absence de
|
||||
recette. Le panneau indique le nombre de cuissons et la réserve allumée en
|
||||
secondes de travail cumulées. Ce n'est pas une durée réelle restante : les
|
||||
cuissons parallèles se partagent la réserve. « Fonctionnement » explique les
|
||||
onglets, les gestes et le rendement normal du combustible en FR/EN.
|
||||
|
||||
Les recettes, résultats, expérience et règles de trémies restent natifs.
|
||||
Aucun changement de format, de monde ou de génération ; les 27 fours physiques
|
||||
conservent l'unique copie des objets et de leurs temps de chauffe.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le test natif `Furnace115ClientChecks` passe avec un serveur intégré, sur un
|
||||
nouveau monde plat de graine 115 : vrai glisser avec Maj sur neuf piles nommées,
|
||||
remplissage consécutif, déplacement manuel vers la case 23 et répartition native
|
||||
sur six cases de colonnes différentes. Les 27 cases de chaque onglet valident
|
||||
leur rôle physique. Le combustible nommé conserve sa position et ses composants.
|
||||
|
||||
La cuisson parallèle, ses états synchronisés, la progression conservée à court
|
||||
de combustible, les 27 cuissons ravitaillées, le seau de lave et l'éponge,
|
||||
l'extraction des résultats et la sauvegarde native passent. Captures FR/EN
|
||||
relues dans `build/furnace115-evidence/`. Marqueur `FURNACE115_PASS` dans
|
||||
`build/furnace115-client.log`.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en 2 min 29 s, 125 tâches. Le panneau d'aide reçoit ensuite une dernière
|
||||
vérification client et une reconstruction ; la livraison commune beta.116
|
||||
repasse les contrôles complets en 2 min 23 s et le scénario Fourneau en 42 s.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Pas d'essai Windows ou de réseau distant avec latence simulée. Aucun monde
|
||||
personnel ouvert. Cette étape reste locale et rejoint la livraison commune
|
||||
avec les matériaux de sculpture : [beta.116](sculpture-materials-beta116.md).
|
||||
|
||||
Archives locales vérifiées (même JAR Sanctuary, sources concordantes, aucune
|
||||
classe de test ni changement de production extérieur au ticket) :
|
||||
|
||||
- `Sanctuary-beta.115.mrpack` : 10184430 octets, SHA-256
|
||||
`8fcac63b1bf5a01f680fb46666924705db3a2675e102037278e9d650d04cbf22`.
|
||||
- `Sanctuary-Test-beta.115.mrpack` : 10203354 octets, SHA-256
|
||||
`5b98fdb8842690158a20b422d6aa5496d74e572b3429498ffb25476d24e87f8a`.
|
||||
- JAR Sanctuary : `2066d3191d7ee27904927fab269d8160136f048742c3223713e8ed4fe90dd8f5`.
|
||||
|
||||
Reçu : `build/furnace115-artifact.json`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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 synchronisation
|
||||
Prism à consigner après leur réussite.
|
||||
@@ -14,6 +14,52 @@ 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.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,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`.
|
||||
@@ -0,0 +1,102 @@
|
||||
# beta.116 — Sculptures à partir des textures de blocs
|
||||
|
||||
Ticket sur `codex/sculpture-materials-beta116`, comprenant aussi le correctif
|
||||
Fourneau beta.115 dans la livraison commune.
|
||||
|
||||
Le Clay Workshop accepte les objets-blocs comme matière. Le client prélève une
|
||||
palette bornée dans la texture du bloc issue du pack actif, avec sa teinte, puis
|
||||
le modèle conserve sa géométrie et transpose ses valeurs claires/sombres dans
|
||||
cette palette. L'aperçu et le résultat utilisent le même calcul. Un bloc est
|
||||
consommé par sculpture ; les neuf cases de hotbar restent les seules accessibles.
|
||||
|
||||
## Contrat des données et du réseau
|
||||
|
||||
Les sculptures existantes conservent exactement leurs voxels sauvegardés :
|
||||
schéma 1 inchangé, identifiants `sanctuary:*` stables. Les nouvelles sculptures
|
||||
stockent elles aussi leurs couleurs finales dans ce format. Aucun chargement
|
||||
ne recolore les objets ou constructions existants. Orientation et ancrage
|
||||
beta.113 inchangés, sans migration ni modification d'un monde personnel.
|
||||
|
||||
La sélection du menu ajoute l'identité de la matière et une palette de 256
|
||||
couleurs opaques au maximum. Ce sont des données d'apparence fournies par le
|
||||
client, comme le modèle déjà importé ; elles n'octroient ni objets ni propriétés
|
||||
de bloc. Le serveur vérifie le menu, son jeton, l'accès à l'atelier, les limites
|
||||
et l'objet effectivement présent. Il applique lui-même la palette et facture
|
||||
chaque résultat. Un changement de matière invalide immédiatement l'ancien
|
||||
résultat jusqu'à réception de la sélection correspondante. Les deux côtés
|
||||
doivent utiliser beta.116. Aucun nouveau format de sauvegarde.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`Sculpture116ClientChecks` passe en 1 min 7 s, sur un nouveau monde plat de
|
||||
Minecraft 26.3, graine 116, puis la régression complète de l'atelier :
|
||||
|
||||
- Les 1 207 objets-blocs enregistrés ont une palette opaque, non vide et bornée.
|
||||
- Fabrication payée avec pierre, planches de chêne, or, laine rouge, verre,
|
||||
feuilles de chêne, argile et ardoise ; aperçu identique au résultat serveur.
|
||||
- Conservation des tons clairs/sombres du modèle et de ses coordonnées, sans
|
||||
convertir les 16 argiles colorées en une simple couleur unie.
|
||||
- Une matière différente efface aussitôt l'ancien résultat ; une sélection
|
||||
périmée ou un jeton incorrect ne fabrique rien et ne consomme aucun objet.
|
||||
- Les conteneurs contenant des objets sont refusés ; ils peuvent servir de
|
||||
matière une fois vidés. Les objets qui ne sont pas des blocs sont refusés.
|
||||
- Bornes et opacité des palettes, véritable rechargement des ressources,
|
||||
neuf cases de hotbar, stock plein, consommation exacte et restitution.
|
||||
- Poses selon les quatre regards, accroche au bord, grille de voxels,
|
||||
casse/repose, sauvegarde et reconnexion après suppression du fichier GLB.
|
||||
- Interface FR/EN aux échelles 2/3, génération de plan au Métabli inchangée.
|
||||
|
||||
Marqueurs `SCULPTURE116_PALETTES`, `SCULPTURE116_MATERIALS_PASS`,
|
||||
`SCULPTURE116_PASS` et régressions 106/111/112/113 dans
|
||||
`build/sculpture116-client.log`. Captures relues dans
|
||||
`build/sculpture116-evidence/`. Un libellé est raccourci en « Bloc » pour éviter
|
||||
le contact avec « Sculpture » dans le cadre compact.
|
||||
|
||||
La palette vient de la texture représentative du modèle de bloc du pack actif,
|
||||
avec la teinte locale ; une texture animée utilise sa première image. Les blocs
|
||||
sans texture lisible utilisent leur couleur de carte. Il s'agit d'un transfert
|
||||
de couleurs sur les voxels opaques, sans transparence, émission ou fonction
|
||||
spéciale du bloc d'origine. Les sculptures existantes gardent leurs couleurs.
|
||||
|
||||
Essais natifs macOS avec serveur intégré ; pas de client Windows ni de test
|
||||
LAN à plusieurs joueurs. Le serveur dédié de GameTest reste exclu selon le
|
||||
refus antérieur de son EULA. Aucun monde personnel ouvert.
|
||||
|
||||
La régression native du Fourneau passe également dans beta.116, en 42 s :
|
||||
`build/sculpture116-furnace.log`, captures FR/EN dans
|
||||
`build/sculpture116-furnace-evidence/`. Les 27 cases, le glisser-déposer, les
|
||||
combustibles et les états de cuisson restent conformes au ticket beta.115.
|
||||
|
||||
## Construction et archives
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en 2 min 23 s : 125 tâches, dont 105 exécutées. Sources Java concordantes avec
|
||||
les JAR sources, versions alignées et intégrité des deux MRpack vérifiées.
|
||||
Le JAR conserve à l'identique le correctif Fourneau beta.115 ; seuls les
|
||||
classes de matière de sculpture et libellés FR/EN changent dans la production.
|
||||
Les classes de test et modèles GLB de test sont absents des archives.
|
||||
|
||||
- `Sanctuary-beta.116.mrpack` : 10189831 octets, SHA-256
|
||||
`7df04a6e2a57e76a55ca57bd7520b05cbb573392690b401b868837498807d252`.
|
||||
- `Sanctuary-Test-beta.116.mrpack` : 10208754 octets, SHA-256
|
||||
`f4c9293e20384b05366d092fc7ee1abea275940a02373ee4036f73f0caa43747`.
|
||||
- JAR Sanctuary :
|
||||
`76a9205783c8b18506c32c9eef6c481d95fd3ba0746a2bdd558a194534a51a32`.
|
||||
|
||||
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`.
|
||||
+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.111
|
||||
pack_version=beta.111
|
||||
mod_version=beta.120
|
||||
pack_version=beta.120
|
||||
resource_pack_version=beta.090
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,20 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
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') }
|
||||
def furnace115Client = providers.gradleProperty('sanctuaryFurnace115ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryFurnace115ClientTests', furnace115Client)
|
||||
if (furnace115Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Furnace115ClientChecks') }
|
||||
def carryPanic114Client = providers.gradleProperty('sanctuaryCarryPanic114ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryCarryPanic114ClientTests', carryPanic114Client)
|
||||
if (carryPanic114Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.CarryPanic114ClientChecks') }
|
||||
|
||||
def statuary106Client = providers.gradleProperty('sanctuaryStatuary106ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryStatuary106ClientTests', statuary106Client)
|
||||
if (statuary106Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Statuary106ClientChecks') }
|
||||
|
||||
+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);}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.CameraType;
|
||||
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.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real cursor/attack packets, synchronized player travel, interruption and collision regression. */
|
||||
public final class CarryPanic114ClientChecks implements FabricClientGameTest {
|
||||
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);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("CarryPanic114 "+UUID.randomUUID());s.setSeed("114");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();
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;p.onUpdateAbilities();reset(p);
|
||||
for(int x=-24;x<=24;x++)for(int z=-24;z<=24;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
int parrot=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
|
||||
c.waitFor(m->m.level.getEntity(parrot)!=null&&CarryService.carried(m.level.getEntity(parrot)),150);
|
||||
float health=server.computeOnServer(s->((LivingEntity)player(s).level().getEntity(parrot)).getHealth());
|
||||
attack(c,parrot);c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(CarryPanic.active(p),"Real native attack triggers server panic");check(((LivingEntity)p.level().getEntity(parrot)).getHealth()<health,"Ordinary carried animal takes native damage");});
|
||||
c.getInput().holdKey(o->o.keyUp);c.getInput().holdKey(o->o.keyRight);c.getInput().holdKey(o->o.keyJump);
|
||||
c.runOnClient(m->{m.player.setYRot(170);m.player.setXRot(15);});c.waitTicks(12);
|
||||
c.runOnClient(m->{check(m.player.getY()>2.5&&m.player.position().subtract(new Vec3(.5,1,.5)).horizontalDistance()>1,"Actual client moves upward and sideways");
|
||||
check(CarryPanic.active(m.player),"Panic remains active through steering keys");
|
||||
var wanted=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime());
|
||||
// Level time can advance immediately after the player physics tick. Match either exact tick.
|
||||
var previous=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime()-1);
|
||||
check(Math.min(m.player.getDeltaMovement().distanceTo(wanted),m.player.getDeltaMovement().distanceTo(previous))<.00001,"Movement follows animal, not camera or held keys: actual="+m.player.getDeltaMovement()+", expected="+wanted+", flight="+m.player.getAttached(CarryPanic.FLIGHT)+", time="+m.level.getGameTime());check(m.player.fallDistance<.2,"Flight clears falling distance");
|
||||
m.options.setCameraType(CameraType.THIRD_PERSON_BACK);});c.waitTicks(2);c.takeScreenshot("carry114-chaotic-flight");
|
||||
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);
|
||||
c.waitTicks(65);c.waitFor(m->!CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(!p.hasAttached(CarryPanic.FLIGHT)&&CarryGlide.supported(p),"Timeout removes panic and retains ordinary glider");check(p.connection.isAcceptingMessages(),"Flight is accepted by server");});
|
||||
c.runOnClient(m->{check(m.player.getDeltaMovement().y<=0&&m.player.getDeltaMovement().y>=-CarryGlide.DESCENT-.02,"Normal gentle descent resumes");m.options.setCameraType(CameraType.FIRST_PERSON);});
|
||||
System.out.println("CARRY114_NATIVE_FLIGHT_PASS cursor, real attack, damage, movement, input override, timeout, glide");
|
||||
|
||||
server.runOnServer(s->{var p=player(s);p.getRandom().setSeed(114);var bird=p.level().getEntity(parrot);var quadrants=new HashSet<Integer>();
|
||||
for(int i=0;i<32;i++){
|
||||
CarryPanic.clear(p);CarryPanic.start(bird);var flight=p.getAttached(CarryPanic.FLIGHT);check(flight.duration()>=40&&flight.duration()<=60,"Duration bounded at 2–3 seconds");quadrants.add((int)(flight.yaw()/(Math.PI/2)));
|
||||
for(int t=0;t<=flight.duration();t++){var v=flight.velocity(flight.start()+t);check(v.horizontalDistance()<=.331&&v.y>=0&&v.y<=.241,"Velocity bounded without cumulative impulses");}
|
||||
}
|
||||
check(quadrants.size()==4,"Server random headings cover every quadrant");
|
||||
});c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(2);c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitFor(m->!CarryService.carrying(m.player)&&!CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{check(!player(s).hasAttached(CarryPanic.FLIGHT),"Native drop clears flight immediately");player(s).level().getEntity(parrot).discard();reset(player(s));});
|
||||
|
||||
int cow=server.computeOnServer(s->carry(player(s),EntityTypes.COW).getId());
|
||||
server.runOnServer(s->{var p=player(s);var animal=(LivingEntity)p.level().getEntity(cow);float before=animal.getHealth();p.attack(animal);
|
||||
check(CarryService.blocksAttack(p,animal)&&animal.getHealth()==before&&!animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&!CarryPanic.active(p),"Terrestrial carry pair stays protected");CarryPanic.start(animal);check(!CarryPanic.active(p),"Ground animal cannot launch player");CarryService.release(animal,true);animal.discard();reset(p);});
|
||||
|
||||
// Every supported appearance can trigger the shared flight, including the gliding chicken.
|
||||
server.runOnServer(s->{var p=player(s);for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.BAT,EntityTypes.BEE,EntityTypes.ALLAY,EntityTypes.PARROT,EntityTypes.BLAZE,EntityTypes.BREEZE)){
|
||||
var animal=carry(p,type);check(CarryGlide.flyer(animal)&&CarryPanic.canStrike(p,animal),"Carried flying species can be struck: "+type);
|
||||
check(animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&CarryPanic.active(p),"Accepted damage launches carrier: "+type);CarryService.release(animal,true);animal.discard();check(!p.hasAttached(CarryPanic.FLIGHT),"Release clears each flight");
|
||||
}});
|
||||
int external=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
|
||||
server.runOnServer(s->{var p=player(s);var bird=(LivingEntity)p.level().getEntity(external);var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);
|
||||
check(bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),1)&&CarryPanic.active(p),"External accepted hit also triggers flight");
|
||||
p.getAbilities().flying=true;CarryGlide.tick(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Creative flying invalidates propulsion");p.getAbilities().flying=false;
|
||||
CarryPanic.start(bird);bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),100);CarryGlide.tick(p);check(!CarryPanic.active(p),"Death stops propulsion");CarryService.detach(p);bird.discard();reset(p);});
|
||||
|
||||
int ceiling=server.computeOnServer(s->{var p=player(s);var bird=carry(p,EntityTypes.PARROT);for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().setBlockAndUpdate(new BlockPos(x,4,z),Blocks.STONE.defaultBlockState());return bird.getId();});
|
||||
c.waitFor(m->m.level.getEntity(ceiling)!=null&&CarryService.carried(m.level.getEntity(ceiling)),100);
|
||||
server.runOnServer(s->{var p=player(s);((LivingEntity)p.level().getEntity(ceiling)).hurtServer(p.level(),p.damageSources().playerAttack(p),1);});c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getY()<2&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Player cannot fly through low ceiling");check(!CarryService.carrying(p)&&!CarryPanic.active(p),"Blocked carried animal is safely released");
|
||||
p.level().getEntity(ceiling).discard();for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().removeBlock(new BlockPos(x,4,z),false);reset(p);
|
||||
var egg=new ItemStack(Items.PARROT_SPAWN_EGG);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(25);
|
||||
int familiar=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Familiar spawned");pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Flying familiar picked up");return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(familiar)!=null&&CarryService.carried(m.level.getEntity(familiar)),100);
|
||||
float petHealth=server.computeOnServer(s->FamiliarService.active(player(s)).getHealth());attack(c,familiar);c.waitFor(m->CarryPanic.active(m.player),100);
|
||||
server.runOnServer(s->{var p=player(s);check(FamiliarService.active(p).getHealth()==petHealth&&CarryPanic.active(p),"Friendly familiar tap launches without hurting it");CarryService.detach(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Lifecycle detach clears synchronized flight");});
|
||||
System.out.println("CARRY114_PASS random flight, native ordinary/familiar strikes, seven species, outside hits, ground protection, drop/death/creative cancellation, ceiling collisions, timeout and normal glide");
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);c.getInput().releaseKey(o->o.keyShift);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(o->o.keyUse);}
|
||||
}
|
||||
private static void attack(ClientGameTestContext c,int id){
|
||||
c.waitFor(m->m.level.getEntity(id).position().distanceTo(CarryService.head(m.player,m.level.getEntity(id)))<.01,100);
|
||||
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,m.level.getEntity(id).getBoundingBox().getCenter());});c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof EntityHitResult hit&&hit.getEntity().getId()==id,"Native cursor can pick own carried flyer: hit="+m.hitResult+", target="+m.level.getEntity(id).getBoundingBox()+", eye="+m.player.getEyePosition()+", view="+m.player.getLookAngle()+", eligible="+CarryPanic.canStrike(m.player,m.level.getEntity(id))));
|
||||
c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);
|
||||
}
|
||||
private static Mob carry(ServerPlayer p,EntityType<? extends Mob> type){var mob=type.create(p.level(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(p.position().add(0,0,2));p.level().addFreshEntity(mob);check(CarryService.interact(p,mob),"Animal picked up: "+type);return mob;}
|
||||
private static void reset(ServerPlayer p){CarryService.detach(p);p.teleportTo(.5,1,.5);p.setDeltaMovement(Vec3.ZERO);p.resetFallDistance();p.setShiftKeyDown(false);}
|
||||
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);}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess;
|
||||
import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor;
|
||||
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.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
|
||||
/** Native quick-transfer, arbitrary slot placement, shared cooking and synchronized feedback. */
|
||||
public final class Furnace115ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos ORIGIN=new BlockPos(2,1,2);
|
||||
private static MultiblockData.Machine oven;
|
||||
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 net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Furnace115 "+UUID.randomUUID());s.setSeed("115");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();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.getInventory().clearContent();p.teleportTo(1.5,1,1.5);
|
||||
for(int x=-2;x<8;x++)for(int z=-2;z<8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
oven=new MultiblockData.Machine(UUID.randomUUID(),ORIGIN,true,Direction.NORTH);for(var pos:oven.members())p.level().setBlockAndUpdate(pos,Blocks.FURNACE.defaultBlockState());check(Multiblocks.assemble(p,oven),"Assemble");
|
||||
for(int i=0;i<9;i++){var stack=new ItemStack(Items.RAW_IRON,8);stack.set(DataComponents.CUSTOM_NAME,Component.literal("Batch "+i));p.getInventory().setItem(i,stack);}Multiblocks.open(p,oven,ORIGIN,0,"",0);
|
||||
});
|
||||
c.waitFor(m->m.gui.screen() instanceof MultiblockScreen&&m.player.containerMenu.getSlot(54).hasItem(),200);
|
||||
// One native Shift-drag crosses nine hotbar stacks, with no mouse release between them.
|
||||
c.getInput().holdKey(o->o.keyShift);move(c,54);c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),1));
|
||||
for(int i=55;i<63;i++)move(c,i);
|
||||
c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),0));c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitTicks(8);c.takeScreenshot("furnace115-inputs");
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(8).hasItem()&&!m.player.containerMenu.getSlot(62).hasItem(),150);
|
||||
c.runOnClient(m->{for(int i=0;i<9;i++)check(m.player.containerMenu.getSlot(i).getItem().getHoverName().getString().equals("Batch "+i),"Shift-drag fills consecutive UI slots, no three-column gaps");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);for(int i=0;i<9;i++)check(stock.getItem(i*3).getCount()==8,"Physical inputs correspond to visible slots");});
|
||||
// Move an ingredient to a distant, arbitrary slot using real inventory packets.
|
||||
click(c,1,0,ContainerInput.PICKUP);click(c,23,0,ContainerInput.PICKUP);c.waitTicks(10);
|
||||
c.runOnClient(m->{check(m.player.containerMenu.getSlot(1).getItem().isEmpty()&&m.player.containerMenu.getSlot(23).getItem().getCount()==8,"Manual placement remains in chosen cell: "+m.player.containerMenu.slots.stream().filter(slot->slot.hasItem()).map(slot->slot.index+":"+slot.getItem()).toList()+", cursor="+m.player.containerMenu.getCarried());});
|
||||
click(c,23,0,ContainerInput.PICKUP);
|
||||
click(c,-999,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(0,0),ContainerInput.QUICK_CRAFT);
|
||||
for(int i:new int[]{1,9,10,13,14,22})click(c,i,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(1,0),ContainerInput.QUICK_CRAFT);
|
||||
click(c,-999,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(2,0),ContainerInput.QUICK_CRAFT);click(c,23,0,ContainerInput.PICKUP);c.waitTicks(6);
|
||||
c.runOnClient(m->{for(int i:new int[]{1,9,10,13,14,22})check(m.player.containerMenu.getSlot(i).getItem().getCount()==1,"Native drag distribution keeps every chosen column");check(m.player.containerMenu.getSlot(23).getItem().getCount()==2&&m.player.containerMenu.getCarried().isEmpty(),"Distributed count conserved");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);check(stock.getItem(69).getCount()==2&&stock.getItem(3).getCount()==1,"Manual distribution matches server");for(int i=0;i<27;i++)check(p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.RAW_IRON)),"Every visible input validates its physical input slot, not Slot.index");reset(stock);stock.setItem(0,new ItemStack(Items.RAW_IRON,4));stock.setItem(18,new ItemStack(Items.RAW_GOLD,4));
|
||||
var coal=new ItemStack(Items.COAL,5);coal.set(DataComponents.CUSTOM_NAME,Component.literal("Stationary fuel"));stock.setItem(79,coal);
|
||||
for(int tick=0;tick<205;tick++)MultiblockCooking.tick(p.level(),oven);
|
||||
check(stock.getItem(79).getCount()==4&&stock.getItem(79).getHoverName().getString().equals("Stationary fuel"),"Named unburnt fuel stays in original cell");
|
||||
for(int i=1;i<79;i+=3)check(stock.getItem(i).isEmpty(),"No displaced coal in other slots");
|
||||
check(stock.getItem(2).is(Items.IRON_INGOT)&&stock.getItem(20).is(Items.GOLD_INGOT),"Shared heat cooks both arbitrary lanes");
|
||||
Multiblocks.open(p,oven,ORIGIN,0,"",0);
|
||||
});
|
||||
c.waitTicks(6);c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.cooking==2&&menu.heat>0&&menu.laneState(6)==MultiblockMenu.COOKING,"Real per-lane feedback synchronizes");});c.takeScreenshot("furnace115-cooking");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);reset(stock);
|
||||
stock.setItem(0,new ItemStack(Items.RAW_IRON));stock.setItem(3,new ItemStack(Items.DIAMOND));stock.setItem(6,new ItemStack(Items.RAW_GOLD));stock.setItem(8,new ItemStack(Items.GOLD_INGOT,64));
|
||||
var a=(MultiblockFurnaceAccess)stock.cell(0);a.sanctuary$progress(80);a.sanctuary$total(200);p.containerMenu.broadcastChanges();
|
||||
});c.waitTicks(6);
|
||||
c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.laneState(0)==MultiblockMenu.NO_FUEL&&menu.laneProgress(0)==40,"Paused progress is visible and preserved");check(menu.laneState(1)==MultiblockMenu.NO_RECIPE,"Invalid recipe explained");check(menu.laneState(2)==MultiblockMenu.BLOCKED,"Blocked output explained");check(menu.cooking==0&&menu.heat==0,"Cold furnace clearly shown");});c.takeScreenshot("furnace115-paused");
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();cookingRegressions(p);var stock=new MultiblockContainer(p.level(),oven);reset(stock);
|
||||
for(int i=0;i<9;i++){var stack=new ItemStack(Items.COAL,5);stack.set(DataComponents.CUSTOM_NAME,Component.literal("Fuel "+i));p.getInventory().setItem(i,stack);}Multiblocks.open(p,oven,ORIGIN,0,"",1);
|
||||
});c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.section()==1&&menu.getSlot(54).hasItem(),100);
|
||||
// No cooking in this fixture: compare the predicted UI in the same call, before any server reply.
|
||||
c.runOnClient(m->{var menu=m.player.containerMenu;for(int i=0;i<9;i++){
|
||||
m.gameMode.handleContainerInput(menu.containerId,54+i,0,ContainerInput.QUICK_MOVE,m.player);
|
||||
check(menu.getSlot(54+i).getItem().isEmpty()&&menu.getSlot(i).getItem().getCount()==5,"Immediate Shift prediction uses consecutive fuel cells");}
|
||||
check(!menu.getSlot(9).mayPlace(new ItemStack(Items.DIAMOND)),"Client rejects invalid fuel like server");});
|
||||
c.waitTicks(8);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);for(int i=0;i<9;i++)check(stock.getItem(i*3+1).getCount()==5,"Fuel server mapping matches prediction");for(int i=0;i<27;i++){check(p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.COAL)),"All 27 fuel cells accept fuel");check(!p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.DIAMOND)),"All 27 fuel cells refuse other items");}});c.takeScreenshot("furnace115-fuel");
|
||||
c.runOnClient(m->{var screen=(MultiblockScreen)m.gui.screen();screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(Multiblocks.text("how_to_cook").getString())).findFirst().orElseThrow().onPress(enter());check(screen.helpVisible(),"Help button opens its explanation");});c.waitTicks(5);c.takeScreenshot("furnace115-help");
|
||||
var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected("fr_fr");return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(3);c.takeScreenshot("furnace115-help-fr");
|
||||
System.out.println("FURNACE115_PASS native Shift-drag, consecutive input/fuel mapping, immediate prediction, arbitrary placement, stationary named fuel, heat/output/pause/recipe feedback, fuel budget, refuelling, buckets, save and result extraction");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void cookingRegressions(ServerPlayer p){
|
||||
var stock=new MultiblockContainer(p.level(),oven);reset(stock);stock.setItem(0,new ItemStack(Items.WET_SPONGE));stock.setItem(79,new ItemStack(Items.LAVA_BUCKET));for(int t=0;t<205;t++)MultiblockCooking.tick(p.level(),oven);
|
||||
check(stock.getItem(2).is(Items.SPONGE)&&stock.getItem(1).is(Items.WATER_BUCKET)&&stock.getItem(79).isEmpty(),"Native lava and wet sponge remainder");
|
||||
reset(stock);for(int i=0;i<81;i+=3)stock.setItem(i,new ItemStack(Items.RAW_IRON));stock.setItem(79,new ItemStack(Items.COAL));for(int t=0;t<90;t++)MultiblockCooking.tick(p.level(),oven);
|
||||
int partial=((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress();check(partial>0&&partial<200,"One coal cannot cook all 27 items");for(int t=0;t<30;t++)MultiblockCooking.tick(p.level(),oven);check(((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress()==partial,"No lost paid progress when out of fuel");
|
||||
stock.setItem(79,new ItemStack(Items.COAL,4));for(int t=0;t<205;t++)MultiblockCooking.tick(p.level(),oven);int produced=0;for(int i=2;i<81;i+=3)produced+=stock.getItem(i).getCount();check(produced==27,"Refuelling completes all lanes exactly once");
|
||||
var f=(AbstractFurnaceBlockEntity)stock.cell(0);var restored=(AbstractFurnaceBlockEntity)BlockEntity.loadStatic(f.getBlockPos(),f.getBlockState(),f.saveWithFullMetadata(p.level().registryAccess()),p.level().registryAccess());check(((MultiblockFurnaceAccess)restored).sanctuary$heat()==((MultiblockFurnaceAccess)f).sanctuary$heat(),"Native heat survives save");
|
||||
Multiblocks.open(p,oven,ORIGIN,0,"",2);var menu=(MultiblockMenu)p.containerMenu;check(!menu.getSlot(0).mayPlace(new ItemStack(Items.STONE)),"Results refuse input");check(!menu.quickMoveStack(p,0).isEmpty()&&stock.getItem(2).isEmpty(),"Native result slot can extract");
|
||||
}
|
||||
private static void reset(MultiblockContainer stock){stock.clearContent();for(int i=0;i<81;i+=3){var a=(MultiblockFurnaceAccess)stock.cell(i);a.sanctuary$heat(0);a.sanctuary$progress(0);}}
|
||||
private static void click(ClientGameTestContext c,int slot,int button,ContainerInput type){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,slot,button,type,m.player));}
|
||||
private static void move(ClientGameTestContext c,int index){c.runOnClient(m->{var a=(AbstractContainerScreenAccessor)m.gui.screen();var slot=m.player.containerMenu.getSlot(index);var w=m.getWindow();double x=a.sanctuary$getLeftPos()+slot.x+8,y=a.sanctuary$getTopPos()+slot.y+8;double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});c.waitTick();}
|
||||
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);}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Checks emitted render geometry, not a duplicate of the placement implementation. */
|
||||
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);
|
||||
check(context.getClickedPos().equals(pos.above())&&state.getValue(Statuary.ANCHOR)==SculptureAnchor.of(edge),"Top hit selects aimed edge at positive/negative coordinates");
|
||||
var side=Vec3.atCenterOf(pos).add(edge.getStepX()*.5,0,edge.getStepZ()*.5);
|
||||
context=new BlockPlaceContext(player,InteractionHand.MAIN_HAND,new ItemStack(Statuary.ITEM),new BlockHitResult(side,edge,pos,false));
|
||||
check(Statuary.BLOCK.getStateForPlacement(context).getValue(Statuary.ANCHOR)==SculptureAnchor.of(edge.getOpposite()),"Side hit anchors against supporting block");
|
||||
}
|
||||
}
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);
|
||||
for(var rotation:Rotation.values()){
|
||||
var actual=state.rotate(rotation);
|
||||
check(actual.getValue(Statuary.ANCHOR)==(anchor==SculptureAnchor.CENTER?anchor:SculptureAnchor.of(rotation.rotate(Direction.valueOf(anchor.name())))),"Structure rotates anchor independently of facing");
|
||||
}
|
||||
for(var mirror:Mirror.values()){
|
||||
var actual=state.mirror(mirror);
|
||||
check(actual.getValue(Statuary.ANCHOR)==(anchor==SculptureAnchor.CENTER?anchor:SculptureAnchor.of(mirror.mirror(Direction.valueOf(anchor.name())))),"Structure mirrors anchor independently of facing");
|
||||
}
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(state);legacy.getCompoundOrEmpty("properties").remove("anchor");
|
||||
var loaded=net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy);
|
||||
check(loaded.getValue(Statuary.FACING)==facing&&loaded.getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Beta.111/112 state keeps facing and defaults anchor");
|
||||
}
|
||||
}
|
||||
static void geometry(Minecraft client,String model){
|
||||
// Every footprint, odd and even, through all orientations and saved anchor states.
|
||||
for(int w=1;w<=16;w++)for(int d=1;d<=16;d++){
|
||||
var sculpture=new VoxelSculpture(model,"Grid fixture",w,2,d,List.of(new VoxelSculpture.Cell(0,0,0,0xFFFF0000),new VoxelSculpture.Cell(w-1,1,d-1,0xFF0000FF)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())rendered(entity(client,sculpture,facing,anchor));
|
||||
}
|
||||
// Transparent margins in imported models must not leave a gap at the aimed edge.
|
||||
var padded=new VoxelSculpture(model,"Padded fixture",13,2,15,List.of(new VoxelSculpture.Cell(2,0,3,-1),new VoxelSculpture.Cell(6,1,10,-1)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values())rendered(entity(client,padded,facing,anchor));
|
||||
// Colored markers identify forward motion after rotation, independent of the origin shift.
|
||||
var sculpture=new VoxelSculpture(model,"Direction fixture",3,2,7,List.of(new VoxelSculpture.Cell(0,0,0,0xFFFF0000),new VoxelSculpture.Cell(0,0,6,0xFF0000FF),new VoxelSculpture.Cell(2,1,0,0xFF00FF00)));
|
||||
for(var facing:Direction.Plane.HORIZONTAL){
|
||||
var mesh=mesh(entity(client,sculpture,facing,SculptureAnchor.CENTER));
|
||||
var red=mesh.stream().filter(q->q.color()==0xFFFF0000).findFirst().orElseThrow();var blue=mesh.stream().filter(q->q.color()==0xFF0000FF).findFirst().orElseThrow();
|
||||
check(blue.x()-red.x()==facing.getStepX()*6/16f&&blue.z()-red.z()==facing.getStepZ()*6/16f,"Colored front faces gaze");
|
||||
}
|
||||
var itemMesh=new StatuaryRenderer.ItemRenderer().extractArgument(StatuaryEntity.item(sculpture));
|
||||
check(itemMesh.equals(mesh(entity(client,sculpture,Direction.SOUTH,SculptureAnchor.CENTER))),"Item preview shares exact centered grid geometry");
|
||||
System.out.println("SCULPTURE113_GEOMETRY_PASS 5120 footprints/orientations/anchors and colored direction markers");
|
||||
}
|
||||
private static StatuaryEntity entity(Minecraft client,VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
|
||||
var entity=new StatuaryEntity(BlockPos.ZERO,Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor));entity.setLevel(client.level);
|
||||
var data=new CompoundTag();data.put("sculpture",StatuaryEntity.encode(sculpture));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,client.level.registryAccess(),data));return entity;
|
||||
}
|
||||
private static List<StatuaryRenderer.Quad> mesh(StatuaryEntity entity){
|
||||
var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(entity,state,0,Vec3.ZERO,null);return state.mesh;
|
||||
}
|
||||
static void rendered(StatuaryEntity entity){
|
||||
var mesh=mesh(entity);check(!mesh.isEmpty(),"Sculpture has visible geometry");
|
||||
float minX=1,minZ=1,maxX=0,maxZ=0;
|
||||
for(var q:mesh){
|
||||
check(q.x()*16==Math.rint(q.x()*16)&&q.y()*16==Math.rint(q.y()*16)&&q.z()*16==Math.rint(q.z()*16),"Every rendered voxel is on the exact 1/16 grid");
|
||||
check(q.x()>=0&&q.y()>=0&&q.z()>=0&&q.x()+1/16f<=1&&q.y()+1/16f<=1&&q.z()+1/16f<=1,"No voxel spills out of its block");
|
||||
minX=Math.min(minX,q.x());maxX=Math.max(maxX,q.x()+1/16f);minZ=Math.min(minZ,q.z());maxZ=Math.max(maxZ,q.z()+1/16f);
|
||||
}
|
||||
var anchor=entity.getBlockState().getValue(Statuary.ANCHOR);
|
||||
switch(anchor){
|
||||
case NORTH -> check(minZ==0,"North edge flush");case SOUTH -> check(maxZ==1,"South edge flush");
|
||||
case WEST -> check(minX==0,"West edge flush");case EAST -> check(maxX==1,"East edge flush");default -> {}
|
||||
}
|
||||
if(anchor!=SculptureAnchor.WEST&&anchor!=SculptureAnchor.EAST)check(Math.abs(minX-(1-maxX))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
if(anchor!=SculptureAnchor.NORTH&&anchor!=SculptureAnchor.SOUTH)check(Math.abs(minZ-(1-maxZ))<=1/16f,"Other axis centered to nearest whole voxel");
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.nio.file.*;
|
||||
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.*;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
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.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.ItemContainerContents;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** All registered block materials, native paid crafts, stale input rejection and pack reload. */
|
||||
public final class Sculpture116ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2);
|
||||
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 net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
Path file=PlanClient.directory().resolve("sculpture116-"+UUID.randomUUID()+".glb");
|
||||
try{
|
||||
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()){
|
||||
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");
|
||||
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();});
|
||||
var expected=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(item)).apply(source));rendered.add(expected);
|
||||
c.waitFor(m->expected.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
|
||||
c.runOnClient(m->check(ItemStack.matches(((ClayWorkshopScreen)m.gui.screen()).preview(),m.player.containerMenu.getSlot(1).getItem()),"Preview equals server-owned result"));
|
||||
check(StatuaryEntity.decode(StatuaryEntity.encode(expected)).equals(expected),"Palette colors persist in unchanged schema 1");
|
||||
c.takeScreenshot("sculpture116-"+BuiltInRegistries.ITEM.getKey(item).getPath());
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(0).getItem().getCount()==1,100);
|
||||
server.waitFor(s->{var menu=s.getPlayerList().getPlayers().getFirst().containerMenu;return menu.getCarried().is(Statuary.ITEM)&&menu.getSlot(0).getItem().getCount()==1;},100);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)s.getPlayerList().getPlayers().getFirst().containerMenu;check(expected.equals(StatuaryEntity.sculpture(menu.getCarried()))&&menu.getSlot(0).getItem().getCount()==1,"Exactly one actual block consumed per sculpture");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.PICKUP,m.player));c.waitFor(m->m.player.containerMenu.getCarried().isEmpty(),100);server.waitFor(s->s.getPlayerList().getPlayers().getFirst().containerMenu.getCarried().isEmpty(),100);
|
||||
}
|
||||
check(rendered.size()>=7,"Distinct materials produce distinct sculptures");
|
||||
c.waitTicks(6);var stone=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.STONE)));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var menu=(ClayWorkshopMenu)p.containerMenu;menu.getSlot(0).set(new ItemStack(Items.DIAMOND_BLOCK));menu.select(p,source,Items.STONE,stone);check(menu.getSlot(1).getItem().isEmpty(),"Stale material palette rejected by server");});
|
||||
var diamond=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.DIAMOND_BLOCK)).apply(source));c.waitFor(m->diamond.equals(StatuaryEntity.sculpture(m.player.containerMenu.getSlot(1).getItem())),180);
|
||||
c.runOnClient(m->{var menu=(ClayWorkshopMenu)m.player.containerMenu;ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,UUID.randomUUID(),source,Items.DIAMOND_BLOCK,stone));});c.waitTicks(6);
|
||||
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");
|
||||
}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void check(boolean ok,String why){if(!ok)throw new AssertionError(why);}
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+80
-29
@@ -23,6 +23,7 @@ import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
@@ -45,8 +46,8 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try{
|
||||
System.out.println("STATUARY106_LIBRARY "+file);Files.createDirectories(file.getParent());byte[] fixture=fixture();Files.write(file,fixture);
|
||||
var asset=GlbModel.load(file);var pixels=StatueVoxelizer.miniature(asset.faces());var source=VoxelSculpture.of(asset.id(),asset.name(),pixels);
|
||||
MODEL.set(source.modelId());check(source.height()==16&&source.width()<16,"Proportions, 16 voxel longest axis");check(source.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Embedded PNG retains multiple colors");
|
||||
var asset=GlbModel.load(file);var pixels=StatueVoxelizer.miniature(asset.faces());var originalSource=VoxelSculpture.of(asset.id(),asset.name(),pixels);
|
||||
MODEL.set(originalSource.modelId());check(originalSource.height()==16&&originalSource.width()<16,"Proportions, 16 voxel longest axis");check(originalSource.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()>1,"Embedded PNG retains multiple colors");
|
||||
check(CreationLibrary.files(CreationLibrary.Kind.MODEL).contains(file)&&!CreationLibrary.files(CreationLibrary.Kind.SCHEMATIC).contains(file),"One folder, different filters");
|
||||
var invalid=fixture.clone();ByteBuffer.wrap(invalid).order(ByteOrder.LITTLE_ENDIAN).putInt(8,1);reject(invalid,"GLB header length");
|
||||
var oversize=new byte[GlbModel.MAX_BYTES+1];reject(oversize,"File bound");
|
||||
@@ -56,37 +57,50 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
reject(edit(fixture,j->{var a=new JsonArray();a.add(0);j.getAsJsonArray("nodes").get(0).getAsJsonObject().add("children",a);}),"Cyclic scene");
|
||||
var interop=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("../../../../../build/statuary106-evidence/BoxTextured.glb").normalize();
|
||||
if(Files.exists(interop)){var box=GlbModel.read(interop);check(box.size()==12,"Independent Khronos indexed GLB");check(!StatueVoxelizer.miniature(box).isEmpty(),"Independent textured box voxelizes");System.out.println("STATUARY106_INTEROP_PASS Khronos BoxTextured GLB");}
|
||||
check(source.rotate().rotate().rotate().rotate().equals(source),"Four rotations preserve ID and data");
|
||||
try{new VoxelSculpture(source.modelId(),"bad",16,16,16,List.of(new VoxelSculpture.Cell(0,0,0,-1),new VoxelSculpture.Cell(0,0,0,-1)));throw new AssertionError("Duplicate cell accepted");}catch(IllegalArgumentException expected){}
|
||||
check(originalSource.rotate().rotate().rotate().rotate().equals(originalSource),"Four rotations preserve ID and data");
|
||||
try{new VoxelSculpture(originalSource.modelId(),"bad",16,16,16,List.of(new VoxelSculpture.Cell(0,0,0,-1),new VoxelSculpture.Cell(0,0,0,-1)));throw new AssertionError("Duplicate cell accepted");}catch(IllegalArgumentException expected){}
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Statuary106 "+UUID.randomUUID());s.setSeed("106");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 source=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(Items.CLAY)).apply(originalSource));
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-7;x<=9;x++)for(int z=-5;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
||||
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties");
|
||||
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.FACING)==Direction.SOUTH,"Legacy state defaults to unchanged model orientation");
|
||||
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.ANCHOR)==SculptureAnchor.CENTER,"Legacy state keeps centered anchor");
|
||||
Sculpture113ClientChecks.placementContexts(p);
|
||||
for(var q:Metatable.members(TABLE))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());check(Metatable.assemble(p,TABLE),"Metabli");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||
c.waitFor(m->m.level.getBlockState(WORKSHOP).is(Statuary.WORKSHOP)&&!m.player.isCreative(),200);
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.getWindow().setWindowed(1280,800);m.resizeGui();Sculpture113ClientChecks.geometry(m,source.modelId());});
|
||||
open(c);c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).onFilesDrop(List.of(file)));
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");menu.getSlot(0).set(new ItemStack(Items.CLAY,3));menu.broadcastChanges();});
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");
|
||||
menu.getSlot(ClayWorkshopMenu.HOTBAR_START).set(new ItemStack(Items.CLAY,3));
|
||||
p.getInventory().setItem(18,new ItemStack(Items.CLAY,64));p.getInventory().setItem(Inventory.SLOT_OFFHAND,new ItemStack(Items.CLAY,12));
|
||||
fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.CLAY,64));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(ClayWorkshopMenu.HOTBAR_START).getItem().getCount()==3,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,ClayWorkshopMenu.HOTBAR_START,0,ContainerInput.QUICK_MOVE,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);
|
||||
c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture"),"Crafted sculpture has translated type name EN"));
|
||||
c.takeScreenshot("statuary106-workshop-en");
|
||||
checkWorkshopLayout(c);
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
|
||||
// Six recovery rows still fit a small GUI and remain scrollable.
|
||||
server.runOnServer(s->{var p=player(s);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.STONE));p.containerMenu.broadcastChanges();});
|
||||
c.waitFor(m->SanctuaryInventoryClient.visibleRows()==6,100);c.runOnClient(m->button(m.gui.screen(),"sanctuary.statuary.refresh").onPress(enter()));checkWorkshopLayout(c);
|
||||
c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var geometry=(SanctuaryContainerScreenGeometry)(Object)screen;var refresh=button(screen,"sanctuary.statuary.refresh");
|
||||
check(screen.mouseScrolled(refresh.getX()-10+geometry.sanctuary$panelLeft()+30,refresh.getY()-127+geometry.sanctuary$panelTop()+35,0,1),"Extended workshop inventory can scroll");});
|
||||
c.takeScreenshot("statuary111-workshop-en-six-rows");
|
||||
server.runOnServer(s->{var p=player(s);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);p.containerMenu.broadcastChanges();});
|
||||
c.waitFor(m->SanctuaryInventoryClient.visibleRows()==1,100);
|
||||
// Hidden inventory, overflow and offhand must never join this menu or feed its input.
|
||||
c.runOnClient(m->check(!((SanctuaryContainerScreenGeometry)(Object)m.gui.screen()).sanctuary$hasIntegratedInventory(),"No expanding player-inventory panel"));
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;
|
||||
check(menu.slots.size()==11&&menu.getSlot(0).getItem().getCount()==3,"Two crafting slots plus exactly nine hotbar slots");
|
||||
check(!((fr.koka.sanctuary.inventory.SanctuaryExtendedInventoryMenu)(Object)menu).sanctuary$hasExtendedInventory(),"No hidden overflow destination");
|
||||
menu.clicked(0,Inventory.SLOT_OFFHAND,ContainerInput.SWAP,p);menu.clicked(11,0,ContainerInput.PICKUP,p);menu.clicked(-2,0,ContainerInput.PICKUP,p);
|
||||
check(menu.quickMoveStack(p,11).isEmpty()&&menu.getCarried().isEmpty(),"Out-of-menu slot rejected");
|
||||
check(menu.getSlot(0).getItem().getCount()==3&&p.getOffhandItem().getCount()==12,"Offhand swap refused");
|
||||
check(p.getInventory().getItem(18).getCount()==64&&fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).getItem(17).getCount()==64,"Reserve clay untouched");
|
||||
p.getInventory().setItem(18,ItemStack.EMPTY);p.getInventory().setItem(Inventory.SLOT_OFFHAND,ItemStack.EMPTY);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);
|
||||
});
|
||||
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay keeps exact colors and model ID");
|
||||
for(var family:ColoredBricks.families()){var colored=ClayWorkshopMenu.material(source,new ItemStack(family.clay()));check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"All 16 clays keep geometry and ID, recolor uniformly");}});
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay applies its sampled palette and preserves model ID");
|
||||
});
|
||||
c.runOnClient(m->{for(var family:ColoredBricks.families()){var palette=SculptureMaterialSampler.sample(new ItemStack(family.clay()));var colored=palette.apply(originalSource);check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().allMatch(cell->palette.colors().contains(cell.color())),"All 16 clays keep geometry and use actual texture shades");}});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||
c.waitFor(m->m.player.containerMenu.getCarried().is(Statuary.ITEM),100);
|
||||
c.waitTicks(6);
|
||||
@@ -107,15 +121,22 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
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);
|
||||
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");
|
||||
check(state.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_90).getValue(Statuary.FACING)==direction.getClockWise(),"Structure rotation follows native directions");
|
||||
check(state.mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT).getValue(Statuary.FACING)==(direction.getAxis()==Direction.Axis.Z?direction.getOpposite():direction),"Structure mirror follows native directions");
|
||||
check(((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(source),"Placement never rewrites model voxels");});
|
||||
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||
c.runOnClient(m->{var entity=(StatuaryEntity)m.level.getBlockEntity(pos);check(entity.getBlockState().getValue(Statuary.FACING)==direction,"Client receives direction");
|
||||
var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(entity,state,0,Vec3.ZERO,null);
|
||||
var forward=new org.joml.Vector3f(0,0,1).rotate(com.mojang.math.Axis.YP.rotationDegrees(state.yaw));
|
||||
check(Math.abs(forward.x()-direction.getStepX())<.001&&Math.abs(forward.z()-direction.getStepZ())<.001,"Rendered model faces gaze: "+direction);});
|
||||
check(entity.getBlockState().getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Client receives aimed edge");
|
||||
Sculpture113ClientChecks.rendered(entity);});
|
||||
c.takeScreenshot("statuary111-facing-"+direction.getSerializedName());
|
||||
}
|
||||
// Re-place a dropped north-facing model while looking west: no baked or cumulative turn.
|
||||
@@ -124,17 +145,18 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
var drop=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(north).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||
check(source.equals(StatuaryEntity.sculpture(drop.getItem())),"Direction does not alter dropped item");
|
||||
p.getInventory().setItem(p.getInventory().getSelectedSlot(),drop.getItem().copy());drop.discard();p.teleportTo(west.getX()+2.5,1,west.getZ()+.5);p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west);
|
||||
c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west,Direction.EAST);
|
||||
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(west).getValue(Statuary.FACING)==Direction.WEST,"Dropped statue takes new gaze"));
|
||||
server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
|
||||
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
|
||||
// Reuse the file with colored clay; server applies the palette bound to that actual material.
|
||||
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c);
|
||||
c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture"),"Crafted sculpture has translated type name FR"));c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||
checkWorkshopLayout(c);
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.modelId().equals(MODEL.get())&&blue.cells().stream().allMatch(v->v.color()==0xFFA0A7BC),"Blue input overrides uploaded model colors");});
|
||||
var expectedBlue=c.computeOnClient(m->SculptureMaterialSampler.sample(new ItemStack(ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow().clay())).apply(originalSource));
|
||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.equals(expectedBlue),"Blue input uses sampled palette and model shading");});
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
|
||||
server.runOnServer(s->{var p=player(s);check(p.containerMenu.getSlot(0).getItem().isEmpty(),"Shift crafting consumes stock");check(count(p,Statuary.ITEM)==2,"Shift crafting produces exactly paid quantity");});close(c);
|
||||
// At the Metabli, the same source creates a full-sized construction plan, with no placement in survival.
|
||||
@@ -143,8 +165,24 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
c.runOnClient(m->button(m.gui.screen(),"sanctuary.plans.generate").onPress(enter()));c.waitFor(m->!PlanClient.busy&&PlanClient.plan!=null&&m.gui.screen()==null,400);
|
||||
c.runOnClient(m->check(PlanClient.plan.height()==32&&PlanClient.plan.cells().size()>source.cells().size(),"Same source produces real-block statue at selected height"));
|
||||
server.runOnServer(s->check(((StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE)).sculpture().equals(source),"Plan generation leaves existing miniature intact"));
|
||||
// Saturated hotbar must not spill crafted statues into hidden inventory or consume unpaid clay.
|
||||
c.runOnClient(m->PlanClient.clear());open(c);c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));
|
||||
c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||
var savedHotbar=new AtomicReference<List<ItemStack>>();
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;savedHotbar.set(java.util.stream.IntStream.range(0,9).mapToObj(i->p.getInventory().getItem(i).copy()).toList());
|
||||
for(int i=0;i<9;i++)p.getInventory().setItem(i,new ItemStack(Items.STONE,64));menu.getSlot(0).set(new ItemStack(Items.CLAY,2));menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM)&&m.player.containerMenu.getSlot(2).getItem().is(Items.STONE),100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(0).getItem().getCount()==2&&count(p,Statuary.ITEM)==0,"Full hotbar consumes nothing and does not fill reserve");
|
||||
var nearlyFull=StatuaryEntity.item(source);nearlyFull.setCount(63);p.getInventory().setItem(8,nearlyFull);menu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.containerMenu.getSlot(10).getItem().getCount()==63,100);
|
||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(6);
|
||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(p.getInventory().getItem(8).getCount()==64&&count(p,Statuary.ITEM)==64&&menu.getSlot(0).getItem().getCount()==1,"Partial merge crafts only the one statue that fits");
|
||||
p.getInventory().setItem(0,ItemStack.EMPTY);menu.broadcastChanges();});
|
||||
close(c);server.waitFor(s->player(s).containerMenu==player(s).inventoryMenu,100);
|
||||
server.runOnServer(s->{var p=player(s);check(p.getInventory().getItem(0).is(Items.CLAY)&&p.getInventory().getItem(0).getCount()==1,"Closing returns the unused clay");for(int i=0;i<9;i++)p.getInventory().setItem(i,savedHotbar.get().get(i));p.inventoryMenu.broadcastFullState();});
|
||||
// Cancel an import by closing the native menu; late worker completion cannot craft in the next session.
|
||||
c.runOnClient(m->PlanClient.clear());open(c);c.runOnClient(m->{((ClayWorkshopScreen)m.gui.screen()).load(file);m.gui.screen().onClose();});c.waitTicks(12);
|
||||
open(c);c.runOnClient(m->{((ClayWorkshopScreen)m.gui.screen()).load(file);m.gui.screen().onClose();});c.waitTicks(12);
|
||||
server.runOnServer(s->check(player(s).containerMenu==player(s).inventoryMenu,"Closed workshop releases crafting session"));
|
||||
check(Arrays.equals(Files.readAllBytes(file),fixture),"Original GLB never rewritten");
|
||||
}
|
||||
@@ -155,25 +193,33 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
for(var direction:List.of(Direction.SOUTH,Direction.EAST,Direction.WEST)){
|
||||
var pos=DIRECTIONS.get(direction);
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.FACING)==direction&&((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(ORIGINAL.get()),"Direction and model survive server save/reload"));
|
||||
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){}}
|
||||
}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void checkWorkshopLayout(ClientGameTestContext c){c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var refresh=button(screen,"sanctuary.statuary.refresh");int left=refresh.getX()-10,top=refresh.getY()-127;
|
||||
check(top>=0&&top+screen.panelHeight()<=screen.height,"Workshop frame fits GUI height");
|
||||
check(screen.panelHeight()==202&&screen.getMenu().slots.size()==11,"Compact height and nine player slots regardless of reserve capacity");
|
||||
for(int i=0;i<9;i++){var slot=screen.getMenu().getSlot(i+2);check(slot.container==m.player.getInventory()&&slot.getContainerSlot()==i,"Displayed player slot belongs to active native hotbar");}
|
||||
for(var slot:screen.getMenu().slots)if(slot.x>=0&&slot.y>=0)check(slot.x>3&&slot.x+17<310&&slot.y>3&&slot.y+17<screen.panelHeight(),"Every visible slot fits inside nine-slice frame");
|
||||
for(var widget:screen.children())if(widget instanceof Button b)check(b.getX()>=left&&b.getRight()<=left+310&&b.getY()>=top&&b.getBottom()<=top+145,"Every action stays clear of explanations and inventory");
|
||||
});}
|
||||
private static void close(ClientGameTestContext c){c.runOnClient(m->m.gui.screen().onClose());c.waitFor(m->m.gui.screen()==null,100);}
|
||||
private static void open(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(WORKSHOP),Direction.UP,WORKSHOP,false)));c.waitForScreen(ClayWorkshopScreen.class);}
|
||||
private static void place(ClientGameTestContext c,BlockPos pos){
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()-.001,pos.getZ()+.5)));c.waitTicks(3);
|
||||
private static void place(ClientGameTestContext c,BlockPos pos){place(c,pos,null);}
|
||||
private static void place(ClientGameTestContext c,BlockPos pos,Direction edge){
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5+(edge==null?0:edge.getStepX()*.4),pos.getY()-.001,pos.getZ()+.5+(edge==null?0:edge.getStepZ()*.4))));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos.below())&&hit.getDirection()==Direction.UP,"Native cursor targets support top face"));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
|
||||
}
|
||||
@@ -190,14 +236,19 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
byte[] text=json.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);int padded=(text.length+3)/4*4;byte[] binary=Arrays.copyOfRange(file,20+length,file.length);var out=ByteBuffer.allocate(20+padded+binary.length).order(ByteOrder.LITTLE_ENDIAN);out.putInt(0x46546C67).putInt(2).putInt(out.capacity()).putInt(padded).putInt(0x4E4F534A).put(text);while(out.position()<20+padded)out.put((byte)' ');out.put(binary);return out.array();
|
||||
}
|
||||
/** A tiny authored cat built from triangle boxes, embedded 2×2 texture and a translated scene node. */
|
||||
private static byte[] fixture()throws IOException{
|
||||
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->{
|
||||
|
||||
@@ -19,16 +19,20 @@ import net.minecraft.world.item.ItemStack;
|
||||
public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> {
|
||||
private static final Identifier FRAME=fr.koka.sanctuary.SanctuaryMod.id("container/clay_workshop/frame");
|
||||
private static final Identifier SLOT=Identifier.withDefaultNamespace("container/slot");
|
||||
private static final Identifier SELECTION=fr.koka.sanctuary.SanctuaryMod.id("hud/hotbar_selection");
|
||||
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary clay models");t.setDaemon(true);return t;});
|
||||
private List<Path> files=List.of();private int page,generation,ticks;private boolean busy,pending;
|
||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastClay=ItemStack.EMPTY;
|
||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastMaterial=ItemStack.EMPTY;
|
||||
private SculpturePalette palette;private Object sampledResources;
|
||||
private Component status=text("choose_model");private Button rotate;
|
||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,286);inventoryLabelX=75;inventoryLabelY=186;}
|
||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,202);}
|
||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.statuary."+key,args);}
|
||||
public static void register(){
|
||||
net.minecraft.client.gui.screens.MenuScreens.register(Statuary.MENU,ClayWorkshopScreen::new);StatuaryRenderer.register();
|
||||
net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{
|
||||
if(!stack.is(Statuary.ITEM))return;var sculpture=StatuaryEntity.sculpture(stack);if(sculpture!=null){lines.add(text("model_name",sculpture.name()));if(flags.isAdvanced())lines.add(Component.literal(sculpture.modelId()));}
|
||||
if(!stack.is(Statuary.ITEM))return;var sculpture=StatuaryEntity.sculpture(stack);if(sculpture!=null){
|
||||
if(!stack.getHoverName().getString().equals(Statuary.BLOCK.getName().getString()))lines.add(Statuary.BLOCK.getName());
|
||||
lines.add(text("model_name",sculpture.name()));if(flags.isAdvanced())lines.add(Component.literal(sculpture.modelId()));}
|
||||
});
|
||||
}
|
||||
@Override protected void init(){
|
||||
@@ -56,24 +60,34 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
}
|
||||
@Override protected void containerTick(){
|
||||
super.containerTick();rotate.active=source!=null&&!busy;
|
||||
if(pending&&++ticks>=5){ticks=0;pending=false;ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,menu.view.token(),source));}
|
||||
var clay=menu.getSlot(0).getItem();
|
||||
if(previewSource!=source||!ItemStack.matches(lastClay,clay)){
|
||||
previewSource=source;lastClay=clay.copy();var colored=ClayWorkshopMenu.material(source,clay);
|
||||
preview=source==null?ItemStack.EMPTY:StatuaryEntity.item(colored==null?source:colored);
|
||||
var material=menu.getSlot(0).getItem();var resources=minecraft.getModelManager().getBlockStateModelSet();
|
||||
boolean changedMaterial=!ItemStack.isSameItemSameComponents(lastMaterial,material)||sampledResources!=resources;
|
||||
if(previewSource!=source||changedMaterial){
|
||||
previewSource=source;lastMaterial=material.copy();sampledResources=resources;
|
||||
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());
|
||||
}
|
||||
// Retry a selection that raced the input packet or server rate limit. The result is the
|
||||
// acknowledgement; never predict consumption or manufacture an item on the client.
|
||||
boolean needsResult=source!=null&&palette!=null&&!ItemStack.matches(preview,menu.getSlot(1).getItem());
|
||||
if(++ticks>=5&&(pending||needsResult)){
|
||||
ticks=0;pending=false;
|
||||
if(source!=null&&palette!=null)ClientPlayNetworking.send(new ClayWorkshopMenu.Select(menu.containerId,menu.view.token(),source,material.getItem(),palette));
|
||||
}
|
||||
|
||||
}
|
||||
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())load(files.getFirst());}
|
||||
@Override public void removed(){generation++;super.removed();}
|
||||
int panelHeight(){
|
||||
// The inventory mixin moves or hides rows according to the player's capacity.
|
||||
return Math.max(177,menu.slots.stream().filter(s->s.x>=0&&s.y>=0).mapToInt(s->s.y+30).max().orElse(177));
|
||||
}
|
||||
int panelHeight(){return imageHeight;}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float partial){
|
||||
g.blitSprite(RenderPipelines.GUI_TEXTURED,FRAME,leftPos,topPos,imageWidth,panelHeight());
|
||||
g.fill(leftPos+148,topPos+23,leftPos+300,topPos+92,0xFF8B8B8B);
|
||||
g.fill(leftPos+149,topPos+24,leftPos+299,topPos+91,0xFF373737);
|
||||
for(var slot:menu.slots)if(slot.x>=0&&slot.y>=0)g.blitSprite(RenderPipelines.GUI_TEXTURED,SLOT,leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
||||
if(minecraft.player!=null)g.blitSprite(RenderPipelines.GUI_TEXTURED,SELECTION,
|
||||
leftPos+ClayWorkshopMenu.HOTBAR_X-3+minecraft.player.getInventory().getSelectedSlot()*18,topPos+ClayWorkshopMenu.HOTBAR_Y-3,22,21);
|
||||
if(!preview.isEmpty()){
|
||||
g.pose().pushMatrix();g.pose().translate(leftPos+194,topPos+27);g.pose().scale(3.75f,3.75f);g.item(preview,0,0);g.pose().popMatrix();
|
||||
}
|
||||
@@ -85,8 +99,8 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor g,int mx,int my){
|
||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("models"),10,22,0xFF404040,false);
|
||||
g.text(font,text("input"),230-font.width(text("input"))/2,95,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),95,0xFF303030,false);
|
||||
if(inventoryLabelY>=0)g.text(font,playerInventoryTitle,inventoryLabelX,inventoryLabelY,0xFF303030,false);
|
||||
}
|
||||
ItemStack preview(){return preview;}
|
||||
public boolean busy(){return busy;}
|
||||
public VoxelSculpture selected(){return source;}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ public final class HotbarClient {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// This workshop exposes only the current hotbar; Tab remains native focus navigation.
|
||||
if (screen instanceof ClayWorkshopScreen) return;
|
||||
ScreenKeyboardEvents.afterKeyRelease(screen).register((s,event) -> {if (key.matches(event)) held=false;});
|
||||
ScreenKeyboardEvents.allowKeyPress(screen).register((s,event) -> {
|
||||
if (!ProgressionClient.active() || !key.matches(event) || event.isEscape() || editing(s.getFocused())) return true;
|
||||
|
||||
@@ -11,6 +11,7 @@ public final class MultiblockScreen extends ContainerScreen {
|
||||
private EditBox search;
|
||||
private Button previous,next,apply;
|
||||
private double wheel;
|
||||
private boolean showHelp;
|
||||
private final java.util.List<Button> sections=new java.util.ArrayList<>();
|
||||
public MultiblockScreen(MultiblockMenu menu,Inventory inv,Component title){super(menu,inv,title);}
|
||||
private MultiblockMenu machine(){return (MultiblockMenu)menu;}
|
||||
@@ -26,7 +27,10 @@ public final class MultiblockScreen extends ContainerScreen {
|
||||
apply=addRenderableWidget(Button.builder(Multiblocks.text("search"),b->change(0,v.section())).bounds(x,topPos+20,w,20).build());
|
||||
previous=addRenderableWidget(Button.builder(Component.literal("<"),b->change(v.page()-1,v.section())).bounds(x,topPos+44,w/2-1,20).build());
|
||||
next=addRenderableWidget(Button.builder(Component.literal(">"),b->change(v.page()+1,v.section())).bounds(x+w/2+1,topPos+44,w/2-1,20).build());
|
||||
if(v.furnace())for(int n=0;n<3;n++){int section=n;var b=addRenderableWidget(Button.builder(Multiblocks.text(new String[]{"inputs","fuel","outputs"}[n]),button->change(0,section)).bounds(x,topPos+84+n*22,w,20).build());sections.add(b);}
|
||||
if(v.furnace())for(int n=0;n<3;n++){int section=n;var b=addRenderableWidget(Button.builder(Multiblocks.text(new String[]{"inputs","fuel","outputs"}[n]),button->change(0,section)).bounds(x,topPos+44+n*22,w,20).build());sections.add(b);}
|
||||
if(v.furnace())addRenderableWidget(Button.builder(Multiblocks.text("how_to_cook"),b->showHelp=!showHelp).bounds(x,topPos+142,w,20)
|
||||
.tooltip(Tooltip.create(Multiblocks.text("cooking_help"))).build());
|
||||
if(v.furnace()){previous.visible=false;next.visible=false;}
|
||||
controls();
|
||||
}
|
||||
@Override public boolean mouseScrolled(double x,double y,double horizontal,double vertical){
|
||||
@@ -41,14 +45,46 @@ public final class MultiblockScreen extends ContainerScreen {
|
||||
@Override protected void containerTick(){super.containerTick();controls();}
|
||||
@Override protected void extractLabels(net.minecraft.client.gui.GuiGraphicsExtractor g,int mouseX,int mouseY){
|
||||
super.extractLabels(g,mouseX,mouseY);var m=machine();int x=imageWidth+4;
|
||||
g.text(font,Multiblocks.text("page",m.view.page()+1,m.view.pages()),x,68,0xFFFFFFFF,false);
|
||||
if(!m.view.furnace())g.text(font,Multiblocks.text("page",m.view.page()+1,m.view.pages()),x,68,0xFFFFFFFF,false);
|
||||
if(m.view.furnace()){
|
||||
g.text(font,Multiblocks.text("heat",m.heat),x,154,0xFFFFFFFF,false);
|
||||
g.text(font,Multiblocks.text("progress",m.progress),x,166,0xFFFFFFFF,false);
|
||||
g.text(font,Multiblocks.text("heat",m.heat),x,116,0xFFFFFFFF,false);
|
||||
g.text(font,Multiblocks.text("cooking_count",m.cooking),x,128,0xFFFFFFFF,false);
|
||||
}
|
||||
for(int i=m.view.indices().length;i<m.displaySlots();i++){var s=menu.getSlot(i);g.fill(s.x,s.y,s.x+16,s.y+16,0xB0181818);}
|
||||
}
|
||||
public java.util.List<net.minecraft.client.renderer.Rect2i> extraAreas(){return search==null?java.util.List.of():java.util.List.of(new net.minecraft.client.renderer.Rect2i(search.getX()-2,search.getY()-2,search.getWidth()+4,machine().view.furnace()?182:82));}
|
||||
@Override protected void extractSlot(net.minecraft.client.gui.GuiGraphicsExtractor g,net.minecraft.world.inventory.Slot slot,int x,int y){
|
||||
super.extractSlot(g,slot,x,y);var m=machine();
|
||||
if(!m.view.furnace()||m.view.section()!=0||slot.index>=m.view.indices().length)return;
|
||||
int lane=m.view.indices()[slot.index]/3,state=m.laneState(lane);if(state==MultiblockMenu.IDLE)return;
|
||||
int color=switch(state){case MultiblockMenu.COOKING->0xFFFFB23F;case MultiblockMenu.NO_FUEL->0xFFE1CE62;default->0xFFE06B5F;};
|
||||
g.fill(slot.x,slot.y+14,slot.x+16,slot.y+16,0xFF343434);
|
||||
g.fill(slot.x,slot.y+14,slot.x+Math.max(1,16*m.laneProgress(lane)/100),slot.y+16,color);
|
||||
}
|
||||
@Override protected java.util.List<Component> getTooltipFromContainerItem(net.minecraft.world.item.ItemStack stack){
|
||||
var lines=new java.util.ArrayList<>(super.getTooltipFromContainerItem(stack));var m=machine();
|
||||
if(m.view.furnace()&&hoveredSlot!=null&&hoveredSlot.index<m.view.indices().length){
|
||||
int lane=m.view.indices()[hoveredSlot.index]/3;
|
||||
if(m.view.section()==1)lines.add(Multiblocks.text("shared_fuel").copy().withStyle(net.minecraft.ChatFormatting.GRAY));
|
||||
else {lines.add(Multiblocks.text("lane",lane+1).copy().withStyle(net.minecraft.ChatFormatting.GRAY));
|
||||
lines.add(Multiblocks.text("state."+new String[]{"idle","cooking","no_fuel","blocked","no_recipe"}[m.laneState(lane)],m.laneProgress(lane)));}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@Override public void extractRenderState(net.minecraft.client.gui.GuiGraphicsExtractor g,int x,int y,float partial){
|
||||
super.extractRenderState(g,x,y,partial);
|
||||
if(showHelp){
|
||||
g.nextStratum();
|
||||
g.tooltip(font,font.split(Multiblocks.text("cooking_help"),Math.min(260,width-32)).stream()
|
||||
.map(net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent::create).toList(),
|
||||
8,24,net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner.INSTANCE,null,false);
|
||||
}
|
||||
}
|
||||
@Override public boolean mouseClicked(net.minecraft.client.input.MouseButtonEvent event,boolean doubleClick){
|
||||
if(showHelp){showHelp=false;return true;}
|
||||
return super.mouseClicked(event,doubleClick);
|
||||
}
|
||||
boolean helpVisible(){return showHelp;}
|
||||
public java.util.List<net.minecraft.client.renderer.Rect2i> extraAreas(){return search==null?java.util.List.of():java.util.List.of(new net.minecraft.client.renderer.Rect2i(search.getX()-2,search.getY()-2,search.getWidth()+4,machine().view.furnace()?166:82));}
|
||||
public static void register(){
|
||||
net.minecraft.client.gui.screens.MenuScreens.<net.minecraft.world.inventory.ChestMenu,MultiblockScreen>register(Multiblocks.MENU,(m,i,t)->new MultiblockScreen((MultiblockMenu)m,i,t));
|
||||
net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{if(stack.is(Multiblocks.WRENCH))lines.add(Multiblocks.text("hint").copy().withStyle(net.minecraft.ChatFormatting.GRAY));});
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
FoodKnowledgeClient.register();
|
||||
MultiblockScreen.register();
|
||||
FourneauModels.register();
|
||||
WrenchTextureModels.register();
|
||||
MetatableClient.register();
|
||||
ClayWorkshopScreen.register();
|
||||
GraveScreen.register();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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.*;
|
||||
|
||||
/** 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 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(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.minecraft.client.renderer.*;
|
||||
import net.minecraft.client.renderer.blockentity.*;
|
||||
@@ -15,6 +14,7 @@ import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.TypedEntityData;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
@@ -23,8 +23,9 @@ import org.joml.Vector3fc;
|
||||
|
||||
/** Shared block/item render geometry: opaque visible faces, generated once per cached sculpture. */
|
||||
public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntity,StatuaryRenderer.State> {
|
||||
private record Quad(float x,float y,float z,int side,int color){}
|
||||
private static final Map<VoxelSculpture,List<Quad>> MESHES=new LinkedHashMap<>(32,.75f,true);
|
||||
record Quad(float x,float y,float z,int side,int color){}
|
||||
private record Placement(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){}
|
||||
private static final Map<Placement,List<Quad>> MESHES=new LinkedHashMap<>(32,.75f,true);
|
||||
private static final Map<TypedEntityData<?>,VoxelSculpture> ITEMS=new LinkedHashMap<>(32,.75f,true);
|
||||
private static final float[][] FACES={
|
||||
{0,0,0, 0,1,0, 1,1,0, 1,0,0, 0,0,-1},
|
||||
@@ -35,26 +36,33 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
||||
{0,0,1, 0,0,0, 1,0,0, 1,0,1, 0,-1,0}
|
||||
};
|
||||
private static final int[][] STEP={{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}};
|
||||
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();float yaw;}
|
||||
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();}
|
||||
public State createRenderState(){return new State();}
|
||||
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){
|
||||
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);state.mesh=mesh(entity.sculpture());
|
||||
state.yaw=-entity.getBlockState().getValue(Statuary.FACING).toYRot();
|
||||
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);
|
||||
state.mesh=mesh(entity.sculpture(),entity.getBlockState().getValue(Statuary.FACING),entity.getBlockState().getValue(Statuary.ANCHOR));
|
||||
}
|
||||
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){
|
||||
pose.pushPose();pose.translate(.5,0,.5);pose.rotateDegrees(Axis.YP,state.yaw);pose.translate(-.5,0,-.5);
|
||||
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);pose.popPose();
|
||||
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);
|
||||
}
|
||||
private static List<Quad> mesh(VoxelSculpture sculpture){
|
||||
if(sculpture==null)return List.of();var cached=MESHES.get(sculpture);if(cached!=null)return cached;
|
||||
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 occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
||||
float ox=(16-sculpture.width())/32f,oz=(16-sculpture.depth())/32f;
|
||||
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;
|
||||
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;
|
||||
quads.add(new Quad(ox+cell.x()/16f,cell.y()/16f,oz+cell.z()/16f,side,cell.color()));
|
||||
}
|
||||
cached=List.copyOf(quads);MESHES.put(sculpture,cached);if(MESHES.size()>32)MESHES.remove(MESHES.keySet().iterator().next());return cached;
|
||||
cached=List.copyOf(quads);MESHES.put(key,cached);if(MESHES.size()>32)MESHES.remove(MESHES.keySet().iterator().next());return cached;
|
||||
}
|
||||
private static void draw(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay){
|
||||
if(mesh.isEmpty())return;
|
||||
@@ -68,7 +76,7 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
||||
var data=stack.get(DataComponents.BLOCK_ENTITY_DATA);if(data==null)return List.of();
|
||||
var sculpture=ITEMS.get(data);
|
||||
if(sculpture==null){sculpture=StatuaryEntity.sculpture(stack);if(sculpture==null)return List.of();ITEMS.put(data,sculpture);if(ITEMS.size()>64)ITEMS.remove(ITEMS.keySet().iterator().next());}
|
||||
return mesh(sculpture);
|
||||
return mesh(sculpture,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
}
|
||||
public void submit(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay,boolean foil,int outline){draw(mesh,pose,collector,light,overlay);}
|
||||
public void getExtents(java.util.function.Consumer<Vector3fc> out){out.accept(new Vector3f(0,0,0));out.accept(new Vector3f(1,1,1));}
|
||||
|
||||
@@ -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(){}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public final class CarryGlide {
|
||||
}
|
||||
public static boolean active(Player p){return supported(p)&&!p.onGround()&&(p.getDeltaMovement().y<=0||lifting(p));}
|
||||
public static void tick(Player p){
|
||||
if(CarryPanic.apply(p))return;
|
||||
boolean lift=lifting(p);
|
||||
if(!lift&&!active(p))return;
|
||||
var velocity=p.getDeltaMovement();
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka.sanctuary.cosmetics;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Server-chosen, temporary flight; both sides use the same bounded native movement. */
|
||||
public final class CarryPanic {
|
||||
public record Flight(int animal,long start,int duration,float yaw,float phase){
|
||||
static final StreamCodec<RegistryFriendlyByteBuf,Flight> CODEC=StreamCodec.of(
|
||||
(b,f)->{b.writeVarInt(f.animal);b.writeLong(f.start);b.writeVarInt(f.duration);b.writeFloat(f.yaw);b.writeFloat(f.phase);},
|
||||
b->new Flight(b.readVarInt(),b.readLong(),b.readVarInt(),b.readFloat(),b.readFloat()));
|
||||
public Vec3 velocity(long now){
|
||||
double t=Math.clamp(now-start,0,duration),angle=yaw+Math.sin(t*.27+phase)*.65+Math.sin(t*.61)*.18;
|
||||
double speed=.28+.05*Math.sin(t*.23+phase);
|
||||
double rise=t<5?.24:.12+.045*Math.sin(t*.32+phase);
|
||||
double fade=Math.min(1,(duration-t)/8.0);
|
||||
return new Vec3(-Math.sin(angle)*speed*fade,rise*fade,Math.cos(angle)*speed*fade);
|
||||
}
|
||||
}
|
||||
public static final AttachmentType<Flight> FLIGHT=AttachmentRegistry.create(SanctuaryMod.id("carry_panic"),
|
||||
b->b.syncWith(Flight.CODEC,AttachmentSyncPredicate.all()));
|
||||
private CarryPanic(){}
|
||||
public static void register(){
|
||||
ServerLivingEntityEvents.AFTER_DAMAGE.register((entity,source,base,taken,blocked)->{
|
||||
if(taken>0&&!blocked)start(entity);
|
||||
});
|
||||
}
|
||||
public static boolean canStrike(Entity actor,Entity target){
|
||||
return actor instanceof Player p&&!p.isSpectator()&&p.isAlive()&&target instanceof Mob
|
||||
&&target.getVehicle()==p&&CarryService.carried(target)&&CarryGlide.flyer(target);
|
||||
}
|
||||
public static void start(Entity animal){
|
||||
if(!(animal.getVehicle() instanceof ServerPlayer p)||!CarryService.carried(animal)||!CarryGlide.flyer(animal)||!CarryGlide.supported(p))return;
|
||||
long now=p.level().getGameTime();var previous=p.getAttached(FLIGHT);
|
||||
if(previous!=null&&now-previous.start()<10)return;
|
||||
var random=p.getRandom();
|
||||
var flight=new Flight(animal.getId(),now,40+random.nextInt(21),random.nextFloat()*(float)(Math.PI*2),random.nextFloat()*(float)(Math.PI*2));
|
||||
p.setAttached(FLIGHT,flight);apply(p);p.syncVelocity=true;
|
||||
p.sendOverlayMessage(Component.translatable("sanctuary.carry.panic"));
|
||||
}
|
||||
public static boolean active(Player p){
|
||||
var flight=p.getAttached(FLIGHT);if(flight==null||!CarryGlide.supported(p))return false;
|
||||
long elapsed=p.level().getGameTime()-flight.start();
|
||||
return elapsed>=0&&elapsed<flight.duration()&&p.getPassengers().stream().anyMatch(e->e.getId()==flight.animal()&&CarryService.carried(e)&&CarryGlide.flyer(e));
|
||||
}
|
||||
public static boolean apply(Player p){
|
||||
if(!active(p)){
|
||||
if(!p.level().isClientSide()&&p.hasAttached(FLIGHT))p.removeAttached(FLIGHT);
|
||||
return false;
|
||||
}
|
||||
p.setDeltaMovement(p.getAttached(FLIGHT).velocity(p.level().getGameTime()));p.resetFallDistance();return true;
|
||||
}
|
||||
public static void clear(Player p){if(p.hasAttached(FLIGHT))p.removeAttached(FLIGHT);}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public final class CarryService {
|
||||
&&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(a,b)
|
||||
&&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(b,a));}
|
||||
private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;}
|
||||
public static boolean blocksAttack(Entity attacker,Entity target){return pair(attacker,target)&&!CarryPanic.canStrike(attacker,target);}
|
||||
/** One seat per head; the chain includes babies even though they add no carrying penalty. */
|
||||
private static Entity passenger(Entity e){
|
||||
return e instanceof Player&&e.getPassengers().size()==1&&carried(e.getFirstPassenger())?e.getFirstPassenger():null;
|
||||
@@ -60,6 +61,7 @@ public final class CarryService {
|
||||
};
|
||||
}
|
||||
public static void register(){
|
||||
CarryPanic.register();
|
||||
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(Drop.TYPE,Drop.CODEC);
|
||||
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(Drop.TYPE,(request,context)->context.server().execute(()->{
|
||||
if(active(context.player()))for(var passenger:List.copyOf(context.player().getPassengers()))if(carried(passenger))release(passenger,true);
|
||||
@@ -72,7 +74,7 @@ public final class CarryService {
|
||||
if(!success)player.sendOverlayMessage(Component.translatable("sanctuary.carry.unavailable"));
|
||||
return InteractionResult.SUCCESS;
|
||||
});
|
||||
AttackEntityCallback.EVENT.register((p,level,hand,target,hit)->pair(p,target)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
AttackEntityCallback.EVENT.register((p,level,hand,target,hit)->blocksAttack(p,target)?InteractionResult.FAIL:InteractionResult.PASS);
|
||||
ServerTickEvents.END_SERVER_TICK.register(s->{
|
||||
for(var rider:new ArrayList<>(RIDES.keySet())){
|
||||
var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue;
|
||||
@@ -187,7 +189,7 @@ public final class CarryService {
|
||||
public static boolean mayDismount(Player p){var ride=RIDES.get(p);return ride==null||ride.releasedShift;}
|
||||
public static void removed(Entity rider){
|
||||
var ride=RIDES.remove(rider);rider.removeAttached(CARRIED);
|
||||
if(ride!=null){refreshBelow(ride.carrier);ride.carrier.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(ride.carrier));}
|
||||
if(ride!=null){CarryPanic.clear(ride.carrier);refreshBelow(ride.carrier);ride.carrier.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(ride.carrier));}
|
||||
}
|
||||
public static boolean release(Entity rider,boolean place){
|
||||
var ride=RIDES.get(rider);if(ride==null){rider.removeAttached(CARRIED);return false;}
|
||||
@@ -209,6 +211,7 @@ public final class CarryService {
|
||||
return true;
|
||||
}
|
||||
public static void detach(ServerPlayer p){
|
||||
CarryPanic.clear(p);
|
||||
// Put down the upper group before removing its support, without orphaning its riders.
|
||||
for(var e:List.copyOf(p.getPassengers()))if(carried(e))release(e,true);
|
||||
if(RIDES.containsKey(p))release(p,true);
|
||||
|
||||
@@ -245,7 +245,7 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
return super.causeFallDamage(distance,multiplier,source);
|
||||
}
|
||||
@Override public boolean hurtServer(ServerLevel level,DamageSource source,float amount) {
|
||||
boolean hurt=fr.koka.sanctuary.familiar.FamiliarBattle.hurt(this,source,amount);if(hurt)playTapSound(source);return hurt;
|
||||
boolean hurt=fr.koka.sanctuary.familiar.FamiliarBattle.hurt(this,source,amount);if(hurt){playTapSound(source);CarryPanic.start(this);}return hurt;
|
||||
}
|
||||
@Override public boolean isIgnoringBlockTriggers() {return true;}
|
||||
@Override public boolean canBeHitByProjectile() {return true;}
|
||||
@@ -316,11 +316,12 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
}
|
||||
/** Native feedback without damage callbacks, enchantments, aggro or durability. */
|
||||
public void tap(ServerPlayer player) {
|
||||
if(hurtTime>0||!isAlive()||player.level()!=level()||player.isSpectator()||CarryService.pair(player,this))return;
|
||||
if(hurtTime>0||!isAlive()||player.level()!=level()||player.isSpectator()||CarryService.blocksAttack(player,this))return;
|
||||
var source=damageSources().playerAttack(player);
|
||||
if(!HeadEquipment.get(this).isEmpty()){MobHeadActions.activate(this,source,true);}
|
||||
animateHurt(player.getYRot());
|
||||
((ServerLevel)level()).broadcastDamageEvent(this,source);
|
||||
if(CarryPanic.canStrike(player,this)){CarryPanic.start(this);playTapSound(source);return;}
|
||||
double dx=player.getX()-getX(),dz=player.getZ()-getZ();
|
||||
if(dx*dx+dz*dz<1.0E-5){var look=player.getLookAngle();dx=-look.x;dz=-look.z;}
|
||||
knockback(.4,dx,dz,source,0);
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class FamiliarService {
|
||||
net.fabricmc.fabric.api.event.player.AttackEntityCallback.EVENT.register((player,level,hand,target,hit)->{
|
||||
if(!(target instanceof FamiliarEntity pet)||player.isSpectator())return net.minecraft.world.InteractionResult.PASS;
|
||||
if(pet.owner()!=null&&fr.koka.sanctuary.familiar.FamiliarDuels.fighting(pet.owner()))return net.minecraft.world.InteractionResult.FAIL;
|
||||
if(CarryService.pair(player,pet))return net.minecraft.world.InteractionResult.FAIL;
|
||||
if(CarryService.blocksAttack(player,pet))return net.minecraft.world.InteractionResult.FAIL;
|
||||
if(player instanceof ServerPlayer attacker&&fr.koka.sanctuary.familiar.FamiliarBattle.enemy(attacker,pet))return net.minecraft.world.InteractionResult.PASS;
|
||||
if(player instanceof ServerPlayer p&&p.isWithinEntityInteractionRange(pet,0)&&p.hasLineOfSight(pet))pet.tap(p);
|
||||
return net.minecraft.world.InteractionResult.SUCCESS;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@ import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Sustained lift is legitimate only while the server sees the equipped, carried large dragon. */
|
||||
/** Flight exemption is limited to server-validated dragon lift or a carried flyer's brief panic. */
|
||||
@Mixin(ServerGamePacketListenerImpl.class)
|
||||
public abstract class CarryFlightConnectionMixin {
|
||||
@Shadow public ServerPlayer player;
|
||||
@Shadow private boolean clientIsFloating;
|
||||
@Inject(method="tick",at=@At("HEAD"))
|
||||
private void sanctuary$carriedFlight(CallbackInfo ci){
|
||||
if(CarryGlide.lifting(player))clientIsFloating=false;
|
||||
if(CarryGlide.lifting(player)||fr.koka.sanctuary.cosmetics.CarryPanic.active(player))clientIsFloating=false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.*;
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class CarryGlideMixin {
|
||||
@ModifyVariable(method="travelInAir",at=@At("HEAD"),argsOnly=true)
|
||||
private Vec3 sanctuary$panicSteering(Vec3 input){return (Object)this instanceof Player p&&fr.koka.sanctuary.cosmetics.CarryPanic.active(p)?Vec3.ZERO:input;}
|
||||
@Inject(method="travelInAir",at={@At("HEAD"),@At("TAIL")})
|
||||
private void sanctuary$glide(Vec3 input,CallbackInfo ci){if((Object)this instanceof Player p)CarryGlide.tick(p);}
|
||||
@Inject(method="getEffectiveGravity",at=@At("RETURN"),cancellable=true)
|
||||
|
||||
@@ -16,6 +16,6 @@ public abstract class CarryLivingMixin {
|
||||
}
|
||||
@Inject(method="hurtServer",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$protectPair(ServerLevel level,DamageSource source,float damage,CallbackInfoReturnable<Boolean> ci){
|
||||
if(CarryService.pair((Entity)(Object)this,source.getEntity()))ci.setReturnValue(false);
|
||||
if(CarryService.blocksAttack(source.getEntity(),(Entity)(Object)this))ci.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ public abstract class CarryPlayerMixin {
|
||||
if(p.getVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet&&!pet.mayDismount())ci.setReturnValue(false);
|
||||
}
|
||||
@Inject(method="attack",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.pair((Entity)(Object)this,target))ci.cancel();}
|
||||
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.blocksAttack((Entity)(Object)this,target))ci.cancel();}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -190,8 +190,7 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
if (!sanctuary$integratedInventory) return;
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
int panelHeight = PANEL_BASE_HEIGHT + sanctuary$visibleRows * BANK_ROW_STEP;
|
||||
// The workshop draws one nine-slice frame around its controls and all visible rows.
|
||||
if (!((Object)this instanceof fr.koka.sanctuary.client.ClayWorkshopScreen)) graphics.blit(RenderPipelines.GUI_TEXTURED,
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED,
|
||||
SanctuaryMod.id("textures/gui/container/" + sanctuary$textureProfile
|
||||
+ "/inventory_" + sanctuary$visibleRows + ".png"),
|
||||
sanctuary$panelLeft, sanctuary$panelTop,
|
||||
@@ -332,7 +331,6 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
// These screens paint previews or controls below their lowest machine
|
||||
// slot. Their explicit anchors keep the common panel from covering them.
|
||||
if (screen instanceof StonecutterScreen) return 70;
|
||||
if (screen instanceof fr.koka.sanctuary.client.ClayWorkshopScreen) return 176;
|
||||
if (screen instanceof CartographyTableScreen) return 81;
|
||||
if (screen instanceof EnchantmentScreen) return 73;
|
||||
if (screen instanceof SmithingScreen) return 82;
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ public abstract class BoatPassengerPickMixin {
|
||||
at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/Entity;getRootVehicle()Lnet/minecraft/world/entity/Entity;"))
|
||||
private static Entity sanctuary$pickSeatedMob(Entity candidate, Operation<Entity> original, Entity source) {
|
||||
// Only the player's cursor: projectile collisions and native range/occlusion stay unchanged.
|
||||
return source instanceof LocalPlayer && BoatPassengerInteractions.aboardTogether(source,candidate)
|
||||
return source instanceof LocalPlayer && (BoatPassengerInteractions.aboardTogether(source,candidate)
|
||||
||fr.koka.sanctuary.cosmetics.CarryPanic.canStrike(source,candidate))
|
||||
? candidate : original.call(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,6 +12,9 @@ public final class MultiblockCooking {
|
||||
private static final ThreadLocal<Boolean> NATIVE=ThreadLocal.withInitial(()->false);
|
||||
public static boolean nativeTick(){return NATIVE.get();}
|
||||
private static MultiblockFurnaceAccess access(AbstractFurnaceBlockEntity f){return (MultiblockFurnaceAccess)f;}
|
||||
public static boolean hasRecipe(ServerLevel level,AbstractFurnaceBlockEntity f){
|
||||
return !f.getItem(0).isEmpty()&&access(f).sanctuary$recipes().getRecipeFor(new SingleRecipeInput(f.getItem(0)),level).isPresent();
|
||||
}
|
||||
public static boolean canCook(ServerLevel level,AbstractFurnaceBlockEntity f){
|
||||
if(f.getItem(0).isEmpty())return false;
|
||||
var input=new SingleRecipeInput(f.getItem(0));var recipe=access(f).sanctuary$recipes().getRecipeFor(input,level).orElse(null);
|
||||
@@ -40,17 +43,27 @@ public final class MultiblockCooking {
|
||||
f.setChanged();donor.setChanged();break;
|
||||
}
|
||||
}
|
||||
AbstractFurnaceBlockEntity fuelSource=null;ItemStack borrowed=ItemStack.EMPTY;
|
||||
if(a.sanctuary$heat()<=1&&a.sanctuary$fuel(level,f.getItem(1))<=0){
|
||||
for(var donor:furnaces)if(donor!=f&&a.sanctuary$fuel(level,donor.getItem(1))>0){
|
||||
// Exchange physical fuel slots, retaining buckets and every component exactly once.
|
||||
// Lend the stack only for this native tick, then return its unburnt contents
|
||||
// to the chosen slot. Never leave whole piles wandering through the fuel tab.
|
||||
fuelSource=donor;borrowed=donor.getItem(1).copy();
|
||||
var old=f.getItem(1);f.setItem(1,donor.getItem(1));donor.setItem(1,old);donor.setChanged();f.setChanged();break;
|
||||
}
|
||||
}
|
||||
// A batch can be refuelled gradually: do not erase partially paid work when heat runs out.
|
||||
if(a.sanctuary$heat()<=1&&a.sanctuary$fuel(level,f.getItem(1))<=0)continue;
|
||||
if(fuelSource==null&&a.sanctuary$heat()<=1&&a.sanctuary$fuel(level,f.getItem(1))<=0)continue;
|
||||
NATIVE.set(true);
|
||||
try{AbstractFurnaceBlockEntity.serverTick(level,f.getBlockPos(),f.getBlockState(),f);}
|
||||
finally{NATIVE.remove();}
|
||||
finally{
|
||||
NATIVE.remove();
|
||||
if(fuelSource!=null&&(f.getItem(1).isEmpty()||ItemStack.isSameItemSameComponents(f.getItem(1),borrowed))){
|
||||
var remaining=f.getItem(1);f.setItem(1,fuelSource.getItem(1));fuelSource.setItem(1,remaining);fuelSource.setChanged();
|
||||
}
|
||||
// A native remainder (e.g. a lava bucket) stays with the cooking lane, so
|
||||
// wet sponges still fill that bucket and no item is copied or discarded.
|
||||
}
|
||||
f.setChanged();used.add(f);
|
||||
}
|
||||
// Idle heat still decays. A blocked output never starts a new fuel item.
|
||||
|
||||
@@ -11,6 +11,8 @@ import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import java.util.*;
|
||||
|
||||
@@ -30,26 +32,35 @@ public final class MultiblockMenu extends ChestMenu {
|
||||
public final MultiblockContainer stock;
|
||||
public final BlockPos anchor;
|
||||
public int displaySlots(){return view.furnace?27:54;}
|
||||
public int heat,progress;
|
||||
public int heat,progress,cooking;
|
||||
public static final int IDLE=0,COOKING=1,NO_FUEL=2,BLOCKED=3,NO_RECIPE=4;
|
||||
private final int[] lanes=new int[27];
|
||||
public int laneState(int lane){return lanes[lane]/101;}
|
||||
public int laneProgress(int lane){return lanes[lane]%101;}
|
||||
public MultiblockMenu(int id,Inventory inv,View view){this(id,inv,view,null,BlockPos.ZERO);}
|
||||
public MultiblockMenu(int id,Inventory inv,View view,MultiblockContainer stock,BlockPos anchor){
|
||||
super(Multiblocks.MENU,id,inv,new SimpleContainer(54),view.furnace?3:6);this.view=view;this.stock=stock;this.anchor=anchor.immutable();
|
||||
for(int i=0;i<displaySlots();i++){
|
||||
var old=slots.get(i);boolean enabled=i<view.indices.length;int index=enabled?view.indices[i]:-1;
|
||||
var old=slots.get(i);boolean enabled=i<view.indices.length;int physicalIndex=enabled?view.indices[i]:-1;
|
||||
Slot slot;
|
||||
if(enabled&&stock!=null){
|
||||
var cell=stock.cell(index);
|
||||
slot=view.furnace&&view.section==2?new FurnaceResultSlot(inv.player,cell,stock.local(index),old.x,old.y):new Slot(stock,index,old.x,old.y){
|
||||
@Override public boolean mayPlace(ItemStack s){return stock.canPlaceItem(index,s);}
|
||||
var cell=stock.cell(physicalIndex);
|
||||
slot=view.furnace&&view.section==2?new FurnaceResultSlot(inv.player,cell,stock.local(physicalIndex),old.x,old.y):new Slot(stock,physicalIndex,old.x,old.y){
|
||||
// Slot.index is the displayed menu ID, not the physical furnace slot.
|
||||
@Override public boolean mayPlace(ItemStack s){return stock.canPlaceItem(physicalIndex,s);}
|
||||
};
|
||||
}else slot=new Slot(getContainer(),i,old.x,old.y){
|
||||
@Override public boolean mayPlace(ItemStack s){return enabled&&(!view.furnace||view.section!=2)&&super.mayPlace(s);}
|
||||
@Override public boolean mayPlace(ItemStack s){return enabled&&(!view.furnace||view.section==0||view.section==1&&(s.has(DataComponents.COOKING_FUEL)||s.is(Items.BUCKET)&&!getItem().is(Items.BUCKET)))&&super.mayPlace(s);}
|
||||
@Override public boolean mayPickup(Player p){return enabled;}
|
||||
};
|
||||
slot.index=i;slots.set(i,slot);
|
||||
}
|
||||
addDataSlot(new DataSlot(){public int get(){return heat;}public void set(int n){heat=n;}});
|
||||
addDataSlot(new DataSlot(){public int get(){return progress;}public void set(int n){progress=n;}});
|
||||
if(view.furnace){
|
||||
addDataSlot(new DataSlot(){public int get(){return cooking;}public void set(int n){cooking=n;}});
|
||||
for(int i=0;i<27;i++){int lane=i;addDataSlot(new DataSlot(){public int get(){return lanes[lane];}public void set(int n){lanes[lane]=n;}});}
|
||||
}
|
||||
}
|
||||
public static View view(MultiblockContainer stock,int wanted,String query,int section){
|
||||
return view(stock,wanted,query,section,new int[0]);
|
||||
@@ -78,20 +89,32 @@ public final class MultiblockMenu extends ChestMenu {
|
||||
}
|
||||
@Override public void broadcastChanges(){
|
||||
if(stock!=null&&stock.machine.furnace()&&Multiblocks.ready(stock.level,stock.machine)){
|
||||
int total=0,max=0;
|
||||
for(int i=0;i<81;i+=3){var a=(fr.koka.sanctuary.mixin.MultiblockFurnaceAccess)stock.cell(i);total+=a.sanctuary$heat();max=Math.max(max,a.sanctuary$progress()*100/Math.max(1,a.sanctuary$total()));}
|
||||
heat=Math.min(32767,total);progress=max;
|
||||
long total=0;boolean fuel=false;cooking=0;progress=0;
|
||||
for(int i=0;i<81;i+=3){var a=(fr.koka.sanctuary.mixin.MultiblockFurnaceAccess)stock.cell(i);total+=a.sanctuary$heat();fuel|=a.sanctuary$fuel(stock.level,stock.getItem(i+1))>0;}
|
||||
heat=(int)Math.min(32767,(total+19)/20); // Seconds of combined burning fuel, not wall-clock time.
|
||||
for(int i=0;i<27;i++){
|
||||
var f=(net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity)stock.cell(i*3);var a=(fr.koka.sanctuary.mixin.MultiblockFurnaceAccess)f;
|
||||
int percent=Math.clamp(a.sanctuary$progress()*100/Math.max(1,a.sanctuary$total()),0,100);
|
||||
int state=f.getItem(0).isEmpty()?IDLE:!MultiblockCooking.hasRecipe(stock.level,f)?NO_RECIPE:!MultiblockCooking.canCook(stock.level,f)?BLOCKED:total>1||fuel?COOKING:NO_FUEL;
|
||||
lanes[i]=state*101+percent;if(state==COOKING)cooking++;progress=Math.max(progress,percent);
|
||||
}
|
||||
}
|
||||
super.broadcastChanges();
|
||||
}
|
||||
@Override public boolean stillValid(Player p){return p.isAlive()&&(stock==null||p.level()==stock.level&&p.distanceToSqr(Vec3.atCenterOf(anchor))<=64&&stock.stillValid(p)&&p instanceof ServerPlayer server&&Multiblocks.accessible(server,stock.machine));}
|
||||
@Override public void clicked(int slot,int button,ContainerInput input,Player p){if(stillValid(p))super.clicked(slot,button,input,p);}
|
||||
@Override public ItemStack quickMoveStack(Player p,int index){
|
||||
if(index<0||index>=slots.size()||!stillValid(p)||stock==null)return ItemStack.EMPTY;
|
||||
if(index<0||index>=slots.size()||!stillValid(p))return ItemStack.EMPTY;
|
||||
var slot=slots.get(index);if(!slot.mayPickup(p))return ItemStack.EMPTY;
|
||||
var source=slot.getItem();if(source.isEmpty())return ItemStack.EMPTY;var original=source.copy();boolean moved;
|
||||
if(index<displaySlots())moved=SanctuaryInventoryTransfer.move(source,slots.subList(displaySlots(),slots.size()));
|
||||
else{
|
||||
else if(stock==null){
|
||||
// The complete furnace tab has the same destinations on both sides. Predict the
|
||||
// native transfer immediately, so rapid gestures do not keep selecting stale stacks.
|
||||
// Filtered views and barrel pages omit destinations: wait for the authoritative reply.
|
||||
if(!view.furnace||!view.query.isEmpty())return ItemStack.EMPTY;
|
||||
moved=SanctuaryInventoryTransfer.move(source,slots.subList(0,displaySlots()));
|
||||
}else{
|
||||
var targets=new ArrayList<Slot>();
|
||||
for(int i=0;i<stock.getContainerSize();i++){
|
||||
if(stock.machine.furnace()&&i%3!=view.section)continue;
|
||||
|
||||
@@ -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(){}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.building.ColoredBricks;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.core.*;
|
||||
@@ -16,22 +17,27 @@ import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native input/result slots. Server recomputes the color from the actual clay and charges each take. */
|
||||
/** Native input/result slots. Server validates the material, applies the bounded palette and charges each take. */
|
||||
public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
public static final int HOTBAR_START=2,HOTBAR_END=HOTBAR_START+Inventory.SELECTION_SIZE,HOTBAR_X=75,HOTBAR_Y=176;
|
||||
public record View(BlockPos pos,UUID token){
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,View> CODEC=StreamCodec.of((b,p)->{b.writeBlockPos(p.pos);b.writeUUID(p.token);},b->new View(b.readBlockPos(),b.readUUID()));
|
||||
}
|
||||
/** At most 24 KiB of cells in one atomic, bounded selection, scoped to one open menu. */
|
||||
public record Select(int menu,UUID token,VoxelSculpture sculpture) implements CustomPacketPayload {
|
||||
/** At most 24 KiB of cells plus 256 palette colors in one atomic, bounded selection, scoped to one open menu. */
|
||||
public record Select(int menu,UUID token,VoxelSculpture sculpture,Item material,SculpturePalette palette) implements CustomPacketPayload {
|
||||
public static final Type<Select> TYPE=new Type<>(SanctuaryMod.id("clay_model"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Select> CODEC=StreamCodec.of((b,p)->{
|
||||
b.writeVarInt(p.menu);b.writeUUID(p.token);b.writeBoolean(p.sculpture!=null);
|
||||
b.writeVarInt(p.menu);b.writeUUID(p.token);b.writeVarInt(BuiltInRegistries.ITEM.getId(p.material));b.writeVarIntArray(p.palette==null?new int[0]:p.palette.colors().stream().mapToInt(Integer::intValue).toArray());b.writeBoolean(p.sculpture!=null);
|
||||
if(p.sculpture!=null){var s=p.sculpture;b.writeUtf(s.modelId(),128);b.writeUtf(s.name(),128);b.writeByte(s.width());b.writeByte(s.height());b.writeByte(s.depth());b.writeVarInt(s.cells().size());for(var c:s.cells()){b.writeShort(c.key());b.writeInt(c.color());}}
|
||||
},b->{
|
||||
int menu=b.readVarInt();var token=b.readUUID();if(!b.readBoolean())return new Select(menu,token,null);
|
||||
int menu=b.readVarInt();var token=b.readUUID();int itemId=b.readVarInt();
|
||||
if(itemId<0||itemId>=BuiltInRegistries.ITEM.size())throw new IllegalArgumentException("sculpture_material");
|
||||
var material=BuiltInRegistries.ITEM.byId(itemId);var colors=b.readVarIntArray(SculpturePalette.MAX_COLORS);
|
||||
var palette=colors.length==0?null:new SculpturePalette(Arrays.stream(colors).boxed().toList());
|
||||
if(!b.readBoolean())return new Select(menu,token,null,material,palette);
|
||||
String model=b.readUtf(128),name=b.readUtf(128);int w=b.readUnsignedByte(),h=b.readUnsignedByte(),d=b.readUnsignedByte(),n=b.readVarInt();
|
||||
if(n<1||n>VoxelSculpture.MAX_CELLS)throw new IllegalArgumentException("sculpture_limit");int[] packed=new int[n*2];for(int i=0;i<n;i++){packed[i*2]=b.readUnsignedShort();packed[i*2+1]=b.readInt();}
|
||||
return new Select(menu,token,VoxelSculpture.unpack(model,name,w,h,d,packed));
|
||||
return new Select(menu,token,VoxelSculpture.unpack(model,name,w,h,d,packed),material,palette);
|
||||
});
|
||||
public Type<? extends CustomPacketPayload> type(){return TYPE;}
|
||||
}
|
||||
@@ -40,40 +46,45 @@ 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 ItemStack lastClay=ItemStack.EMPTY;
|
||||
private VoxelSculpture selectedResult;
|
||||
private ItemStack lastMaterial=ItemStack.EMPTY;
|
||||
private Item selectedMaterial=Items.AIR;
|
||||
private SculpturePalette selectedPalette;
|
||||
private long lastSelection=-100;
|
||||
public ClayWorkshopMenu(int id,Inventory inv,View view){this(id,inv,view,null);}
|
||||
public ClayWorkshopMenu(int id,Inventory inv,View view,Level level){
|
||||
super(Statuary.MENU,id);this.view=view;this.level=level;
|
||||
addSlot(new Slot(input,0,222,106){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
||||
addSlot(new Slot(input,0,222,106){@Override public boolean mayPlace(ItemStack stack){return material(stack);}});
|
||||
addSlot(new Slot(result,0,282,106){
|
||||
@Override public boolean mayPlace(ItemStack stack){return false;}
|
||||
@Override public boolean mayPickup(Player p){return mayCraft(p)&&clay(input.getItem(0))&&!getItem().isEmpty();}
|
||||
@Override public boolean mayPickup(Player p){return mayCraft(p)&&material(input.getItem(0))&&(level==null||input.getItem(0).is(selectedMaterial))&&!getItem().isEmpty();}
|
||||
@Override public void onTake(Player p,ItemStack stack){input.removeItem(0,1);input.setChanged();super.onTake(p,stack);}
|
||||
});
|
||||
addStandardInventorySlots(inv,75,198);
|
||||
// Use the nine carried slots without registering a full inventory/overflow range.
|
||||
addInventoryHotbarSlots(inv,HOTBAR_X,HOTBAR_Y);
|
||||
}
|
||||
// Colors match the published clay palette (tools/colored-bricks-palette.json), in native dye order.
|
||||
private static final int[] CLAY_COLORS={0xC6C4C8,0xD3AA8D,0xD39FD0,0x91B0CA,0xC8BF98,0xA2C69A,0xDAC4DD,0x74767D,0xA9A8AC,0xA8C5C1,0xBE91E3,0xA0A7BC,0xA49A91,0xA0B499,0xB49A99,0x444541};
|
||||
public static boolean clay(ItemStack item){return item.is(Items.CLAY)||ColoredBricks.families().stream().anyMatch(f->item.is(f.clay().asItem()));}
|
||||
public static VoxelSculpture material(VoxelSculpture model,ItemStack clay){
|
||||
if(model==null||!clay(clay))return null;
|
||||
if(clay.is(Items.CLAY))return model;
|
||||
for(var family:ColoredBricks.families())if(clay.is(family.clay().asItem()))return model.color(0xFF000000|CLAY_COLORS[family.color().getId()]);
|
||||
return null;
|
||||
public static boolean material(ItemStack item){
|
||||
if(item.isEmpty()||!(item.getItem() instanceof BlockItem))return false;
|
||||
var contents=item.get(DataComponents.CONTAINER);
|
||||
return contents==null||!contents.nonEmptyItems().iterator().hasNext();
|
||||
}
|
||||
public void select(ServerPlayer p,VoxelSculpture sculpture){
|
||||
public void select(ServerPlayer p,VoxelSculpture sculpture,Item material,SculpturePalette palette){
|
||||
if(level==null||!stillValid(p)||level.getGameTime()-lastSelection<4)return;
|
||||
var hit=new BlockHitResult(Vec3.atCenterOf(view.pos),Direction.UP,view.pos,false);
|
||||
if(UseBlockCallback.EVENT.invoker().interact(p,level,InteractionHand.MAIN_HAND,hit)!=InteractionResult.PASS||!stillValid(p))return;
|
||||
lastSelection=level.getGameTime();selected=sculpture;refresh();broadcastChanges();
|
||||
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 sculpture=material(selected,input.getItem(0));lastClay=input.getItem(0).copy();
|
||||
if(level==null)return;var item=input.getItem(0);
|
||||
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);}
|
||||
@Override public void broadcastChanges(){if(level!=null&&!ItemStack.matches(lastClay,input.getItem(0)))refresh();super.broadcastChanges();}
|
||||
@Override public void broadcastChanges(){if(level!=null&&!ItemStack.matches(lastMaterial,input.getItem(0)))refresh();super.broadcastChanges();}
|
||||
@Override public boolean stillValid(Player p){return level==null?p.isAlive():Statuary.allowed(p,level,view.pos);}
|
||||
private boolean mayCraft(Player p){
|
||||
if(!stillValid(p))return false;
|
||||
@@ -81,14 +92,18 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
||||
var hit=new BlockHitResult(Vec3.atCenterOf(view.pos),Direction.UP,view.pos,false);
|
||||
return UseBlockCallback.EVENT.invoker().interact(p,level,InteractionHand.MAIN_HAND,hit)==InteractionResult.PASS&&stillValid(p);
|
||||
}
|
||||
@Override public void clicked(int slot,int button,ContainerInput input,Player p){if(stillValid(p))super.clicked(slot,button,input,p);}
|
||||
@Override public void clicked(int slot,int button,ContainerInput input,Player p){
|
||||
if(!stillValid(p)||slot>=slots.size()||(slot< -1&&slot!=-999))return;
|
||||
if(input==ContainerInput.SWAP&&(button<0||button>=Inventory.SELECTION_SIZE))return;
|
||||
super.clicked(slot,button,input,p);
|
||||
}
|
||||
@Override public boolean canTakeItemForPickAll(ItemStack stack,Slot slot){return slot.container!=result&&super.canTakeItemForPickAll(stack,slot);}
|
||||
@Override public void removed(Player p){super.removed(p);if(level!=null)clearContainer(p,input);result.clearContent();selected=null;}
|
||||
@Override public ItemStack quickMoveStack(Player p,int index){
|
||||
if(index<0||index>=slots.size()||!stillValid(p))return ItemStack.EMPTY;var slot=slots.get(index);if(!slot.mayPickup(p)||!slot.hasItem())return ItemStack.EMPTY;
|
||||
var stack=slot.getItem();var copy=stack.copy();
|
||||
if(index<2){if(!moveItemStackTo(stack,2,38,true))return ItemStack.EMPTY;}
|
||||
else if(clay(stack)){if(!moveItemStackTo(stack,0,1,false))return ItemStack.EMPTY;}
|
||||
if(index<HOTBAR_START){if(!moveItemStackTo(stack,HOTBAR_START,HOTBAR_END,true))return ItemStack.EMPTY;}
|
||||
else if(material(stack)){if(!moveItemStackTo(stack,0,1,false))return ItemStack.EMPTY;}
|
||||
else return ItemStack.EMPTY;
|
||||
if(stack.isEmpty())slot.set(ItemStack.EMPTY);else slot.setChanged();
|
||||
if(index==1)slot.onTake(p,copy);return copy;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
|
||||
/** 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);
|
||||
|
||||
private final Direction direction;
|
||||
SculptureAnchor(Direction direction){this.direction=direction;}
|
||||
public String getSerializedName(){return name().toLowerCase(Locale.ROOT);}
|
||||
public SculptureAnchor rotate(Rotation rotation){return direction==null?this:of(rotation.rotate(direction));}
|
||||
public SculptureAnchor mirror(Mirror mirror){return direction==null?this:of(mirror.mirror(direction));}
|
||||
public static SculptureAnchor of(Direction direction){return switch(direction){
|
||||
case NORTH -> NORTH;case SOUTH -> SOUTH;case EAST -> EAST;case WEST -> WEST;
|
||||
default -> throw new IllegalArgumentException("Horizontal anchor required");
|
||||
};}
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
public int originX(int width){return this==WEST?0:this==EAST?VoxelSculpture.EDGE-width:(VoxelSculpture.EDGE-width)/2;}
|
||||
public int originZ(int depth){return this==NORTH?0:this==SOUTH?VoxelSculpture.EDGE-depth:(VoxelSculpture.EDGE-depth)/2;}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** Bounded appearance data, applied identically by preview and authoritative crafting. */
|
||||
public record SculpturePalette(List<Integer> colors) {
|
||||
public static final int MAX_COLORS=256;
|
||||
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.comparingInt(SculpturePalette::luminance).thenComparingInt(Integer::intValue)).toList();
|
||||
}
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import net.minecraft.network.chat.Component;
|
||||
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
|
||||
public final class Statuary {
|
||||
public static final EnumProperty<Direction> FACING=BlockStateProperties.HORIZONTAL_FACING;
|
||||
public static final EnumProperty<SculptureAnchor> ANCHOR=EnumProperty.create("anchor",SculptureAnchor.class);
|
||||
public static final Block BLOCK=block("statuary",new SculptureBlock());
|
||||
public static final Item ITEM=blockItem("statuary",BLOCK);
|
||||
public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock());
|
||||
@@ -38,7 +39,7 @@ public final class Statuary {
|
||||
CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.FUNCTIONAL_BLOCKS).register(out->out.accept(WORKSHOP_ITEM));
|
||||
PayloadTypeRegistry.serverboundPlay().register(ClayWorkshopMenu.Select.TYPE,ClayWorkshopMenu.Select.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(ClayWorkshopMenu.Select.TYPE,(packet,c)->c.server().execute(()->{
|
||||
if(c.player().containerMenu instanceof ClayWorkshopMenu menu&&menu.containerId==packet.menu()&&menu.view.token().equals(packet.token()))menu.select(c.player(),packet.sculpture());
|
||||
if(c.player().containerMenu instanceof ClayWorkshopMenu menu&&menu.containerId==packet.menu()&&menu.view.token().equals(packet.token()))menu.select(c.player(),packet.sculpture(),packet.material(),packet.palette());
|
||||
}));
|
||||
}
|
||||
public static boolean allowed(Player p,Level l,BlockPos pos){
|
||||
@@ -59,11 +60,11 @@ public final class Statuary {
|
||||
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 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 SculptureBlock extends Block implements EntityBlock {
|
||||
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH));}
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING);}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection());}
|
||||
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING)));}
|
||||
@Override protected BlockState mirror(BlockState state,Mirror mirror){return state.rotate(mirror.getRotation(state.getValue(FACING)));}
|
||||
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
|
||||
@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));}
|
||||
@Override protected BlockState mirror(BlockState state,Mirror mirror){return state.setValue(FACING,mirror.mirror(state.getValue(FACING))).setValue(ANCHOR,state.getValue(ANCHOR).mirror(mirror));}
|
||||
public BlockEntity newBlockEntity(BlockPos pos,BlockState state){return new StatuaryEntity(pos,state);}
|
||||
@Override protected List<ItemStack> getDrops(BlockState s,LootParams.Builder context){return List.of(context.getOptionalParameter(LootContextParams.BLOCK_ENTITY) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM));}
|
||||
@Override protected ItemStack getCloneItemStack(LevelReader l,BlockPos pos,BlockState s,boolean data){return l.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM);}
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class StatuaryEntity extends BlockEntity {
|
||||
public static ItemStack item(VoxelSculpture sculpture){
|
||||
var item=new ItemStack(Statuary.ITEM);var tag=new CompoundTag();tag.put("sculpture",encode(sculpture));
|
||||
item.set(DataComponents.BLOCK_ENTITY_DATA,TypedEntityData.of(Statuary.TYPE,tag));
|
||||
item.set(DataComponents.ITEM_NAME,net.minecraft.network.chat.Component.literal(sculpture.name()));return item;
|
||||
return item;
|
||||
}
|
||||
public static VoxelSculpture sculpture(ItemStack item){
|
||||
var data=item.get(DataComponents.BLOCK_ENTITY_DATA);if(!item.is(Statuary.ITEM)||data==null||data.type()!=Statuary.TYPE)return null;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1013,6 +1013,7 @@
|
||||
"sanctuary.companion.rename_hint": "Rename its egg at an anvil, or use a name tag on your familiar.",
|
||||
"sanctuary.companion.interactions_version_required": "Sanctuary beta.020 is required on both client and server.",
|
||||
"sanctuary.carry.unavailable": "No free space to carry or put down here.",
|
||||
"sanctuary.carry.panic": "The animal panics and carries you away!",
|
||||
"sanctuary.companion.tooltip_size": "Size: %s",
|
||||
"sanctuary.companion.size.tiny": "Tiny",
|
||||
"sanctuary.companion.size.small": "Small",
|
||||
@@ -1976,9 +1977,9 @@
|
||||
"sanctuary.multiblock.inputs": "Ingredients",
|
||||
"sanctuary.multiblock.fuel": "Fuel",
|
||||
"sanctuary.multiblock.outputs": "Results",
|
||||
"sanctuary.multiblock.heat": "Heat: %s",
|
||||
"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.",
|
||||
@@ -2150,24 +2151,24 @@
|
||||
"sanctuary.weather.season.summer": "Summer",
|
||||
"sanctuary.weather.season.autumn": "Autumn",
|
||||
"sanctuary.weather.season.winter": "Winter",
|
||||
"block.sanctuary.statuary": "Statuary",
|
||||
"block.sanctuary.statuary": "Sculpture",
|
||||
"block.sanctuary.clay_workshop": "Clay workshop",
|
||||
"sanctuary.plans.imported_models": "My 3D models",
|
||||
"sanctuary.plans.native_models": "Discovered creatures",
|
||||
"sanctuary.plans.imported_model": "3D model → block statue",
|
||||
"sanctuary.plans.model_files_help": "GLB models in schematics/sanctuary/, alongside your schematics. Create a full-block statue here. Craft 16³ miniatures at the clay workshop.",
|
||||
"sanctuary.plans.empty_models": "Place a .glb file in the shared folder, then refresh.",
|
||||
"sanctuary.statuary.choose_model": "Choose a GLB model. 1 clay block = 1 statue. Plain clay keeps model colors; colored clay gives a solid color.",
|
||||
"sanctuary.statuary.choose_model": "Choose a GLB model, then a material block from your hotbar.",
|
||||
"sanctuary.statuary.folder": "Shared folder",
|
||||
"sanctuary.statuary.refresh": "Refresh",
|
||||
"sanctuary.statuary.models": "GLB models",
|
||||
"sanctuary.statuary.placement": "The statue is placed facing the direction you look. 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 clay = 1 statue. Plain clay keeps model colors. Colored clay gives a solid color.",
|
||||
"sanctuary.statuary.ready": "1 block = 1 sculpture. Shades come from your material’s texture.",
|
||||
"sanctuary.statuary.empty": "No .glb files",
|
||||
"sanctuary.statuary.input": "Clay",
|
||||
"sanctuary.statuary.output": "Statue",
|
||||
"sanctuary.statuary.input": "Block",
|
||||
"sanctuary.statuary.output": "Sculpture",
|
||||
"sanctuary.statuary.model_name": "Model: %s",
|
||||
"sanctuary.statuary.error.file": "Cannot read this local file.",
|
||||
"sanctuary.statuary.error.format": "Invalid or unsupported GLB 2.0. Export a self-contained static model.",
|
||||
@@ -2192,5 +2193,37 @@
|
||||
"gamerule.sanctuary.snow_accumulation_height": "Sanctuary snow height",
|
||||
"gamerule.sanctuary.snow_accumulation_height.description": "Maximum height of a snow column, in blocks. 0: no accumulation; -1: up to the world height limit. Eight layers become a solid block. The vanilla rule set to 0 still takes priority.",
|
||||
"gamerule.sanctuary.seasonal_snow_melt": "Seasonal snow melt",
|
||||
"gamerule.sanctuary.seasonal_snow_melt.description": "Gradually melts tracked weather deposits outside winter in Real Time weather. Preserves snow constructions."
|
||||
"gamerule.sanctuary.seasonal_snow_melt.description": "Gradually melts tracked weather deposits outside winter in Real Time weather. Preserves snow constructions.",
|
||||
"sanctuary.multiblock.cooking_count": "%s / 27 cooking",
|
||||
"sanctuary.multiblock.how_to_cook": "How it works",
|
||||
"sanctuary.multiblock.cooking_help": "Ingredients: one slot = one recipe, up to 27 at once. Fuel: shared by all slots. Results: collect cooked items in the matching ingredient positions. Each recipe uses fuel at the normal rate: 1 coal cooks about 8 items in total. The reserve shows seconds of already burning fuel, shared between recipes. Bars beneath ingredients show their progress. Shift-click fills the first available slots; mouse placement keeps your chosen slot.",
|
||||
"sanctuary.multiblock.shared_fuel": "Fuel shared between all 27 recipes.",
|
||||
"sanctuary.multiblock.lane": "Cooking slot %s",
|
||||
"sanctuary.multiblock.state.idle": "No ingredient in this slot.",
|
||||
"sanctuary.multiblock.state.cooking": "Cooking: %s %%",
|
||||
"sanctuary.multiblock.state.no_fuel": "Add fuel — %s %% progress kept",
|
||||
"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.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -1013,6 +1013,7 @@
|
||||
"sanctuary.companion.rename_hint": "Renomme son œuf à l’enclume, ou utilise une étiquette sur lui.",
|
||||
"sanctuary.companion.interactions_version_required": "Sanctuary beta.020 est requis côté client et serveur.",
|
||||
"sanctuary.carry.unavailable": "Pas de place libre pour porter ou déposer ici.",
|
||||
"sanctuary.carry.panic": "L’animal s’affole et vous entraîne !",
|
||||
"sanctuary.companion.tooltip_size": "Taille : %s",
|
||||
"sanctuary.companion.size.tiny": "Minuscule",
|
||||
"sanctuary.companion.size.small": "Petit",
|
||||
@@ -1976,9 +1977,9 @@
|
||||
"sanctuary.multiblock.inputs": "Ingrédients",
|
||||
"sanctuary.multiblock.fuel": "Combustibles",
|
||||
"sanctuary.multiblock.outputs": "Résultats",
|
||||
"sanctuary.multiblock.heat": "Chauffe : %s",
|
||||
"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é.",
|
||||
@@ -2150,24 +2151,24 @@
|
||||
"sanctuary.weather.season.summer": "Été",
|
||||
"sanctuary.weather.season.autumn": "Automne",
|
||||
"sanctuary.weather.season.winter": "Hiver",
|
||||
"block.sanctuary.statuary": "Statuaire",
|
||||
"block.sanctuary.statuary": "Sculpture",
|
||||
"block.sanctuary.clay_workshop": "Atelier d’argile",
|
||||
"sanctuary.plans.imported_models": "Mes modèles 3D",
|
||||
"sanctuary.plans.native_models": "Créatures découvertes",
|
||||
"sanctuary.plans.imported_model": "Modèle 3D → statue en blocs",
|
||||
"sanctuary.plans.model_files_help": "Modèles GLB dans schematics/sanctuary/, avec vos schémas. Ici, créez une grande statue en blocs. Les miniatures 16³ se fabriquent à l’atelier d’argile.",
|
||||
"sanctuary.plans.empty_models": "Déposez un fichier .glb dans le dossier commun, puis actualisez.",
|
||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB. 1 bloc d’argile = 1 statue. Argile normale : couleurs du modèle ; argile colorée : teinte unie.",
|
||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB, puis un bloc de matière dans votre barre rapide.",
|
||||
"sanctuary.statuary.folder": "Dossier commun",
|
||||
"sanctuary.statuary.refresh": "Actualiser",
|
||||
"sanctuary.statuary.models": "Modèles GLB",
|
||||
"sanctuary.statuary.placement": "La statue se pose dans le sens de votre regard. 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 argile = 1 statue. Normale : couleurs du modèle. Colorée : teinte unie.",
|
||||
"sanctuary.statuary.ready": "1 bloc = 1 sculpture. Les nuances viennent de la texture de votre matière.",
|
||||
"sanctuary.statuary.empty": "Aucun fichier .glb",
|
||||
"sanctuary.statuary.input": "Argile",
|
||||
"sanctuary.statuary.output": "Statue",
|
||||
"sanctuary.statuary.input": "Bloc",
|
||||
"sanctuary.statuary.output": "Sculpture",
|
||||
"sanctuary.statuary.model_name": "Modèle : %s",
|
||||
"sanctuary.statuary.error.file": "Impossible de lire ce fichier local.",
|
||||
"sanctuary.statuary.error.format": "GLB 2.0 invalide ou non pris en charge. Exportez un modèle statique autonome.",
|
||||
@@ -2192,5 +2193,37 @@
|
||||
"gamerule.sanctuary.snow_accumulation_height": "Hauteur de neige Sanctuary",
|
||||
"gamerule.sanctuary.snow_accumulation_height.description": "Hauteur maximale de la colonne de neige, en blocs. 0 : aucun dépôt ; -1 : jusqu’à la limite du monde. Huit couches deviennent un bloc solide. La règle vanilla à 0 reste prioritaire.",
|
||||
"gamerule.sanctuary.seasonal_snow_melt": "Fonte saisonnière de la neige",
|
||||
"gamerule.sanctuary.seasonal_snow_melt.description": "Fait fondre progressivement les dépôts météo suivis hors hiver, en météo Real Time. Préserve les constructions en neige."
|
||||
"gamerule.sanctuary.seasonal_snow_melt.description": "Fait fondre progressivement les dépôts météo suivis hors hiver, en météo Real Time. Préserve les constructions en neige.",
|
||||
"sanctuary.multiblock.cooking_count": "%s / 27 en cuisson",
|
||||
"sanctuary.multiblock.how_to_cook": "Fonctionnement",
|
||||
"sanctuary.multiblock.cooking_help": "Ingrédients : une case = une cuisson, jusqu’à 27 en parallèle. Combustibles : réserve commune pour toutes les cases. Résultats : récupérer ici les objets cuits, aux mêmes positions que les ingrédients. Chaque cuisson consomme du combustible à vitesse normale : 1 charbon cuit environ 8 objets au total. La réserve affiche les secondes de combustible déjà allumé, partagées entre les cuissons. Les barres sous les ingrédients montrent leur progression. Maj-clic transfère vers les premières cases disponibles ; déposer à la souris conserve la case choisie.",
|
||||
"sanctuary.multiblock.shared_fuel": "Combustible partagé entre les 27 cuissons.",
|
||||
"sanctuary.multiblock.lane": "Cuisson n° %s",
|
||||
"sanctuary.multiblock.state.idle": "Aucun ingrédient dans cette case.",
|
||||
"sanctuary.multiblock.state.cooking": "En cuisson : %s %%",
|
||||
"sanctuary.multiblock.state.no_fuel": "Ajouter du combustible — %s %% conservés",
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user