Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc7fc3dfb | ||
|
|
9b0104d27a | ||
|
|
d9b3dd6bed | ||
|
|
e732da8800 | ||
|
|
248dff47b7 | ||
|
|
6aaabf1cb9 | ||
|
|
72e9263339 | ||
|
|
3fd64e1f2a | ||
|
|
8af406a028 | ||
|
|
0a81b3c147 | ||
|
|
68fd97d287 | ||
|
|
0f1efdec83 | ||
|
|
5c666a4ebc | ||
|
|
e3ca9fcd63 | ||
|
|
22e58b4b91 | ||
|
|
ed0e0b26aa | ||
|
|
21845bd736 | ||
|
|
4a6f63c52d | ||
|
|
6166d0aa9e | ||
|
|
064701f210 | ||
|
|
1eaa976ecf | ||
|
|
8c0020c774 | ||
|
|
289697becf | ||
|
|
e38f6cbdbe | ||
|
|
2090380712 |
@@ -20,9 +20,13 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
La version **beta.110** réunit les développements jusqu’à l’atelier d’argile,
|
||||
aux œufs et à la neige saisonnière. Le [suivi de livraison](docs/clay-workshop-integration-beta110.md)
|
||||
précise les vérifications, la publication et la synchronisation de l’instance.
|
||||
La version **beta.122** adapte la sélection et les collisions des sculptures
|
||||
à leurs voxels réels et supprime leurs ombres de blocs pleins. Les modèles
|
||||
restent alignés au centre ou au bord choisi, dans les quatre orientations.
|
||||
Le [ticket de livraison](docs/sculpture-shape-light-beta122.md) détaille la
|
||||
forme physique, les règles de lumière et les vérifications réussies.
|
||||
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||
|
||||
Les [archives pour le site](archives/web/README.md) conservent la maquette
|
||||
@@ -31,13 +35,104 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.122 — Sculptures : forme et lumière
|
||||
|
||||
Collisions et sélection sur les voxels réels, creux traversables, lumière sans
|
||||
occlusion cubique. [Contrat et vérifications](docs/sculpture-shape-light-beta122.md).
|
||||
|
||||
## beta.121 — Texture de clé et pack personnel
|
||||
|
||||
Copie complète des ressources, création depuis le jeu et guide FR/EN inclus.
|
||||
[Contrat et vérifications](docs/resourcepack-template-beta121.md) ·
|
||||
[Template personnel](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
|
||||
|
||||
## beta.120 — Clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation de textures et assemblage des machines conservé.
|
||||
[Contrat et vérifications](docs/golden-wrench-beta120.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.120/Sanctuary-beta.120.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.120/Sanctuary-Test-beta.120.mrpack).
|
||||
|
||||
## beta.119 — Sculptures au centre ou au bord
|
||||
|
||||
Pose selon le point visé, centrage sur la grille entière et ancrage conservé
|
||||
en sauvegarde. [Contrat et vérifications](docs/sculpture-center-beta119.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.119/Sanctuary-beta.119.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.119/Sanctuary-Test-beta.119.mrpack).
|
||||
|
||||
## beta.118 — Murets en briques colorées
|
||||
|
||||
Seize couleurs, fabrication et tailleur de pierre, eau, raccords et collisions
|
||||
natifs. [Contrat et vérifications](docs/colored-brick-walls-beta118.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.118/Sanctuary-beta.118.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.118/Sanctuary-Test-beta.118.mrpack).
|
||||
|
||||
## beta.117 — Grain des textures et sculptures en chapeau
|
||||
|
||||
Les petits cubes d'une sculpture reprennent plusieurs nuances des textures
|
||||
de la matière. Le modèle reste intact dans le slot chapeau et au retrait.
|
||||
[Contrat et vérifications](docs/sculpture-texture-grain-beta117.md) ·
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.117/Sanctuary-beta.117.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.117/Sanctuary-Test-beta.117.mrpack).
|
||||
|
||||
## beta.116 — Matières de sculpture et Fourneau
|
||||
|
||||
Un bloc de matière devient une sculpture aux couleurs de sa texture. Hotbar,
|
||||
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](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
|
||||
|
||||
Le Clay Workshop / atelier d’argile, les statuaires et les modèles 3D rejoignent
|
||||
les œufs, l’accumulation et la fonte saisonnière dans un seul pack.
|
||||
[Contrat et vérifications](docs/clay-workshop-integration-beta110.md).
|
||||
[Pack normal](build/Sanctuary-beta.110.mrpack) ·
|
||||
[Pack de test](build/Sanctuary-Test-beta.110.mrpack).
|
||||
[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.110/Sanctuary-beta.110.mrpack) ·
|
||||
[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.110/Sanctuary-Test-beta.110.mrpack) ·
|
||||
[Release et installation Prism](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110).
|
||||
|
||||
## beta.109 — Fonte saisonnière
|
||||
|
||||
@@ -2051,7 +2146,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.101 |
|
||||
| Sanctuary / pack | beta.122 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -2080,7 +2175,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.092.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.122.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -127,3 +127,12 @@ de modèles, butin et états reprennent leurs équivalents natifs 26.3.
|
||||
`tools/generate-colored-bricks.py` documente cette transformation ;
|
||||
`tools/colored-bricks-palette.json` conserve couleurs et empreintes des PNG fournis.
|
||||
Ces ressources dérivées ne sont pas présentées comme des créations originales.
|
||||
|
||||
## Clé dorée beta.121
|
||||
|
||||
Le PNG 16 × 16 `golden_wrench.png` a été fourni par le créateur de Sanctuary
|
||||
le 17 septembre 2026. Il est conservé octet pour octet dans le mod, le pack
|
||||
intégré et le template personnel (SHA-256
|
||||
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`).
|
||||
Le template regroupe les ressources client déjà distribuées par Sanctuary ;
|
||||
leurs crédits et conditions respectives restent applicables.
|
||||
|
||||
+19
-1
@@ -15,7 +15,7 @@ tasks.register('verifyPack', Exec) {
|
||||
tasks.register('assemblePack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Assemble a local packwiz pack including the built Sanctuary mod.'
|
||||
dependsOn(':sanctuary:build', 'verifyPack', 'assembleResourcePack')
|
||||
dependsOn(':sanctuary:build', 'verifyPack', 'assembleResourcePack', 'assembleResourceTemplate')
|
||||
commandLine('python3', 'scripts/pack.py', 'assemble')
|
||||
}
|
||||
|
||||
@@ -46,3 +46,21 @@ tasks.register('assembleTestPack', Exec) {
|
||||
dependsOn('assemblePack', ':sanctuary-test:build')
|
||||
commandLine('python3', 'scripts/test_pack.py')
|
||||
}
|
||||
|
||||
tasks.register('assembleResourceTemplate', Zip) {
|
||||
group = 'distribution'
|
||||
description = 'Export all Sanctuary client assets as an editable personal resource pack.'
|
||||
dependsOn(':sanctuary:processResources', 'verifyResourcePack')
|
||||
def resources = project(':sanctuary').layout.buildDirectory.dir('resources/main')
|
||||
// Same precedence as the native exporter: built-in textures override mod defaults.
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
from(resources.map { it.dir('resourcepacks/textures/assets') }) { into 'assets' }
|
||||
from(resources.map { it.dir('assets') }) { into 'assets' }
|
||||
from(resources.map { it.dir('resourcepacks/template') })
|
||||
from(resources.map { it.file('resourcepacks/textures/pack.png') })
|
||||
exclude '**/.DS_Store'
|
||||
destinationDirectory = layout.buildDirectory
|
||||
archiveFileName = "Sanctuary-Template-${mod_version}.zip"
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
+14
-1
@@ -1,5 +1,17 @@
|
||||
# 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).
|
||||
Cadre nine-slice fourni, inventaire intégré et pose selon le regard horizontal.
|
||||
Propriété `facing` additive ; les anciens statuaires gardent leur apparence.
|
||||
|
||||
## STAT-01 — Atelier d’argile et modèles 3D — beta.106
|
||||
|
||||
Branche `codex/statuary-import-beta106`. [Contrat et validation](statuary-beta106.md).
|
||||
@@ -1847,4 +1859,5 @@ Branche `codex/clay-workshop-integration-beta110`.
|
||||
[Ticket](clay-workshop-integration-beta110.md) : assemblage commun de
|
||||
beta.106 et beta.109, avec tous les systèmes intermédiaires. Essais natifs de
|
||||
l’atelier, des œufs et de la neige, build et archives réussis. Publication
|
||||
et synchronisation de l’instance demandées ensuite, en cours.
|
||||
et synchronisation de l’instance Sanctuary Beta terminées ; 923 fichiers
|
||||
personnels et réglages suivis conservés, aucun monde ouvert.
|
||||
|
||||
@@ -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`.
|
||||
@@ -38,4 +38,3 @@ Reçu : `build/beta104-artifact.json`.
|
||||
|
||||
Les GameTests dédiés restent exclus. Aucun monde, canal ou installation
|
||||
personnelle modifié ; archives locales uniquement.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Demande du 17 septembre 2026 : inclure le Clay Workshop dans la version avec
|
||||
la fonte saisonnière. Branche `codex/clay-workshop-integration-beta110`.
|
||||
État : assemblage commun vérifié, publication et synchronisation en cours.
|
||||
État : version commune vérifiée, publiée et synchronisée dans Sanctuary Beta.
|
||||
|
||||
## Périmètre et compatibilité
|
||||
|
||||
@@ -95,3 +95,34 @@ Reçu : `build/integration110-artifact.json`, manifeste des sources :
|
||||
Les essais natifs sont macOS avec serveur intégré ; pas de test Windows ni
|
||||
connexion depuis un second ordinateur. La neige antérieure non suivie garde
|
||||
la limite de fonte documentée dans beta.109.
|
||||
|
||||
Le script de publication a été corrigé pour autoriser uniquement `icon.png`
|
||||
en plus des manifestes TOML, après contrôle de l'index et comparaison à l'icône
|
||||
source. Les JAR restent exclusivement des pièces jointes de release. Cette
|
||||
correction de distribution ne change aucun binaire ni le tag source beta.110.
|
||||
|
||||
## Publication et installation terminées
|
||||
|
||||
[Release beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110),
|
||||
tag source `468743bfe75087ecb0160b85a0864fa81bcdded5`. Les sources cumulatives
|
||||
depuis beta.092 sont enregistrées ; les anciens tags et artefacts sont conservés.
|
||||
Les corrections du script de distribution et ce reçu complètent la branche
|
||||
source sans déplacer ce tag ni remplacer les binaires.
|
||||
|
||||
Canal packwiz : `828353a6e2bd747f4f473ae9fb9e965d38032419`. Chaque manifeste et
|
||||
l'icône publiés correspondent aux fichiers validés localement. JAR, archives
|
||||
normale/Test et ZIP d'amorçage Prism sont disponibles dans la release.
|
||||
|
||||
Deux synchronisations de l'installateur packwiz dans un dossier neuf, puis deux
|
||||
dans la même instance **Sanctuary Beta**, ont réussi. Le second passage ne
|
||||
change aucun fichier géré. Un seul JAR Sanctuary beta.110 est actif, Fabric API
|
||||
0.160.5+26.3 est installé, Minecraft est réglé sur 26.3 et Loader sur 0.19.5.
|
||||
L'icône déclarée dans le pack est ajoutée. `instance.cfg` et les **923 fichiers
|
||||
personnels et réglages suivis** conservent leurs empreintes. Aucun monde ouvert,
|
||||
converti ou régénéré ; aucune autre instance ni serveur personnel modifié.
|
||||
|
||||
Sauvegarde ciblée dans l'instance : `sanctuary-backups/before-beta.110/`.
|
||||
Reçus ignorés : `build/integration110-publication.json`,
|
||||
`build/integration110-isolated.json`, `build/integration110-prism.json`.
|
||||
Les journaux de chaque synchronisation et les listes d'empreintes avant/après
|
||||
sont conservés dans le dossier de développement et dans la sauvegarde ciblée.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# STAT-02 — Atelier d’argile et orientation — beta.111
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/clay-workshop-ui-beta111`.
|
||||
|
||||
## Résultat attendu
|
||||
|
||||
Le menu utilise le cadre `task_frame_unobtained.png` fourni par le créateur,
|
||||
sans retouche, comme sprite Sanctuary avec les métadonnées nine-slice natives
|
||||
de Minecraft. Les coins restent à leur taille d’origine. Catalogue, aperçu,
|
||||
fabrication, explications et inventaire évolutif tiennent dans ce cadre.
|
||||
La texture reste remplaçable par un pack de ressources.
|
||||
|
||||
La pose d’un statuaire suit le regard horizontal du joueur, dans les quatre
|
||||
directions. Le bouton Tourner règle toujours l’orientation du modèle importé.
|
||||
Le placement ajoute ensuite l’orientation du bloc sans réécrire ses voxels.
|
||||
|
||||
## Contrat de compatibilité préalable
|
||||
|
||||
Ajout de la propriété native `facing` au bloc `sanctuary:statuary`, avec `south`
|
||||
par défaut : cette orientation correspond au rendu historique, sans rotation.
|
||||
Les anciens états sans propriété prennent cette valeur à la lecture et gardent
|
||||
leur apparence. Aucun parcours ni réécriture des anciens chunks n’est effectué.
|
||||
Le schéma 1 de la sculpture, ses voxels, ses identifiants et les données des
|
||||
objets restent identiques. Casser puis reposer utilise le nouveau regard,
|
||||
sans cumuler les orientations précédentes. Rotation et miroir de structures
|
||||
transforment l’état du bloc. La collision cubique reste celle déjà livrée.
|
||||
|
||||
## Texture
|
||||
|
||||
Le fichier fourni, 26 × 26 pixels, est conservé octet pour octet sous
|
||||
`assets/sanctuary/textures/gui/sprites/container/clay_workshop/frame.png`.
|
||||
Le fichier adjacent `.png.mcmeta` utilise le moteur `nine_slice` de Minecraft
|
||||
avec une bordure fixe de quatre pixels et un centre extensible. L’identifiant
|
||||
du sprite est `sanctuary:container/clay_workshop/frame` ; aucun sprite global
|
||||
Minecraft n’est remplacé. Les emplacements utilisent `minecraft:container/slot`.
|
||||
|
||||
## Vérifications natives
|
||||
|
||||
`Statuary106ClientChecks`, étendu aux cas beta.111, passe en **1 min 3 s** sur
|
||||
Minecraft 26.3, avec serveur intégré et monde plat jetable de graine 106 :
|
||||
|
||||
- Fabrication payée, restitutions, Maj-clic et teintes des seize argiles.
|
||||
- Menu FR/EN aux échelles 2 et 3, cases et actions contenues dans le cadre.
|
||||
- Six rangées d’inventaire à l’échelle 3 : affichage borné et défilement.
|
||||
- Vrais clics de pose aux quatre points cardinaux : état côté serveur,
|
||||
transmission au client et rotation du rendu alignés sur le regard.
|
||||
- Rotation et miroir de structures ; lecture d’un ancien état sans `facing`.
|
||||
- Butin d’une statue orientée au nord puis repose vers l’ouest : nouvelle
|
||||
orientation, mêmes voxels. Sauvegarde/réouverture et transmission sans GLB.
|
||||
- Conversion au Métabli et annulation d’import conservées.
|
||||
|
||||
Commande :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `STATUARY106_PASS`, `STATUARY106_INTEROP_PASS` et `STATUARY111_PASS`
|
||||
dans `build/clay-ui111-client.log`. Captures dans `build/clay-ui111-evidence/`,
|
||||
dont les menus français et anglais relus visuellement.
|
||||
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
Les essais tournent sur macOS avec serveur intégré ; pas de validation depuis
|
||||
deux ordinateurs ni sur Windows. Aucun monde personnel n’est ouvert.
|
||||
|
||||
## Livraison vérifiée
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 33 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux fichiers Java du dépôt. Le contenu du mod hors classes et ressources de
|
||||
ce ticket est identique à beta.110 ; œufs, neige et fonte restent inclus.
|
||||
Les deux MRpack contiennent le même JAR vérifié, sans monde, GLB ni classe de test.
|
||||
|
||||
- Pack normal : `build/Sanctuary-beta.111.mrpack`, 10 169 372 octets,
|
||||
SHA-256 `f83098f80ac2edb8e32877bf8d5a362cd75f2fa9020593c2c7e4f9abe8b3e9f6`.
|
||||
- Pack Test : `build/Sanctuary-Test-beta.111.mrpack`, 10 188 298 octets,
|
||||
SHA-256 `1234b10eb4685d99aa7421de46bc54fd114b791f1b4d63803a09afc926d2c9d9`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`b3e967db7b336b970e4121981e3ef8140fe90f0ecc5850ce90a70caaf3772684`.
|
||||
|
||||
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`.
|
||||
@@ -31,4 +31,3 @@ de débogage changent). Reçu : `build/beta103-artifact.json`.
|
||||
|
||||
Les GameTests du serveur dédié restent exclus. Aucun monde personnel,
|
||||
aucune installation ni aucun canal de distribution modifié.
|
||||
|
||||
|
||||
@@ -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,109 @@
|
||||
# TOOL-01 — Clé dorée et états de blocs — beta.120
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/golden-wrench-beta120`.
|
||||
Le créateur valide les gestes du debug stick : clic gauche choisit la propriété,
|
||||
clic droit parcourt ses valeurs, Maj inverse le parcours. L'orientation est
|
||||
proposée en premier, puis les propriétés de forme et les autres états natifs.
|
||||
La sélection est personnelle, par type de bloc, pour la session courante.
|
||||
Une indication dans la barre d'action affiche la propriété et sa valeur.
|
||||
La clé ne casse pas le bloc, y compris en créatif, et ne remplace pas son type.
|
||||
|
||||
Les propriétés natives sont disponibles en survie avec la clé, sans condition
|
||||
opérateur, dans le respect des droits de construction, protections, portée et
|
||||
verrous de conteneurs. Comme avec le debug stick, les formes choisies restent
|
||||
en place immédiatement ; les mises à jour ultérieures du jeu peuvent recalculer
|
||||
les états dynamiques (redstone, connexions, cultures…).
|
||||
|
||||
Les multiblocs assemblés gardent leurs commandes d'ouverture/dissociation et
|
||||
leurs propriétés techniques ne sont pas éditées individuellement. Un assemblage
|
||||
complet garde priorité au clic droit ; Maj permet de régler les composants
|
||||
non assemblés. Hors d'un assemblage complet, même un four, un baril, un piston
|
||||
ou un établi reçoit l'action générique.
|
||||
|
||||
## Rotation de texture et contrat de stockage
|
||||
|
||||
La terre n'a pas d'orientation dans son blockstate vanilla. Une propriété
|
||||
supplémentaire de la clé, « Rotation des textures », parcourt 0°, 90°, 180° et
|
||||
270° sur les faces des modèles de blocs. La géométrie, l'éclairage, les teintes,
|
||||
les ressources actives, le bloc, son inventaire et son butin restent natifs.
|
||||
Les rendus spéciaux d'entités de blocs ne sont pas transformés par ce réglage
|
||||
des faces ; leur orientation demeure modifiable par leurs états natifs.
|
||||
|
||||
Nouvelle donnée facultative `sanctuary:wrench_textures_v1`, attachée au chunk :
|
||||
table immuable position entière → quart de tour (1 à 3). Une absence signifie
|
||||
0°. Elle n'est créée qu'après une action de clé ; revenir à 0° retire l'entrée,
|
||||
puis l'attachement s'il est vide. Aucun état vanilla ni format existant n'est
|
||||
modifié ; aucun parcours, conversion ou régénération des anciens mondes.
|
||||
Les modifications de blockstate du même type conservent la rotation. Casser,
|
||||
remplacer ou déplacer par piston retire la rotation à l'ancienne position ;
|
||||
les objets récupérés restent ordinaires. Les nouveaux clients la reçoivent
|
||||
après le chunk natif, par lots bornés de 1 024 entrées au maximum.
|
||||
Déconnexion, changement de dimension et déchargement libèrent le cache visuel.
|
||||
Revenir à une ancienne version du mod supprime seulement cet effet visuel ;
|
||||
les blocs conservent leurs identifiants, données et états natifs.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours `Wrench120ClientChecks` passe en **47 s** sur Minecraft 26.3,
|
||||
client macOS et serveur intégré, dans un monde plat jetable de graine 120 :
|
||||
|
||||
- **2 522 propriétés natives** parcourues, avec valeurs inverses et type conservé.
|
||||
- Vrais clics sur les escaliers : orientation, moitié supérieure et forme,
|
||||
sans casser le bloc en créatif ; gestes inverses et sélection distincte.
|
||||
Les doubles événements créatifs d'un même clic sont filtrés à la cadence
|
||||
native de cinq ticks.
|
||||
- Terre en survie : rotation réelle des UV émis, positions des sommets et nombre
|
||||
de faces identiques ; retour exact après quatre quarts de tour ou un inverse.
|
||||
- Rechargement des ressources conservant les UV tournés ; capture de terre relue.
|
||||
- Coffre tourné avec ses 23 diamants intacts, sans ouverture accidentelle.
|
||||
- Assemblage/dissociation par vrais clics des quatre familles : Métabli, Fût,
|
||||
Fourneau et super piston. Un clic gauche sur un assemblage ne modifie pas
|
||||
les numéros de composants ; barils et pistons isolés tournent normalement.
|
||||
- Refus en aventure, spectateur ou hors portée ; retrait des données visuelles
|
||||
après remplacement, maintien lors d'un changement d'état du même bloc.
|
||||
- Sauvegarde/reconnexion : rotation de terre côté serveur et client, état
|
||||
natif de l'escalier et inventaire du coffre conservés.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryWrench120ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `WRENCH120_PASS` et `WRENCH120_PROPERTIES 2522` dans
|
||||
`build/wrench120-client.log`, captures dans `build/wrench120-evidence/`.
|
||||
Aucun monde personnel ouvert ; pas de validation Windows ni depuis deux
|
||||
ordinateurs. Les rendus de faces natifs et le rechargement du pack sont vérifiés ;
|
||||
pas de validation spécifique d'un shader tiers.
|
||||
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 12 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||
aux sources du dépôt. La comparaison avec beta.119 limite les différences de
|
||||
production aux classes de clé/assemblage, aux deux nouveaux hooks, au rendu
|
||||
et aux libellés prévus ; toutes les autres ressources sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR Sanctuary et aucune sauvegarde
|
||||
ni fixture de test.
|
||||
|
||||
- Sanctuary-beta.120.mrpack : 10237973 octets, SHA-256
|
||||
`86b5e480a12ba20b5c1e308c77dbb54bdf8a56806915fb32ca2d16f9a72cc9c9`.
|
||||
- Sanctuary-Test-beta.120.mrpack : 10256895 octets, SHA-256
|
||||
`2e3818483ba7dfa6e5652b97933e105a44e7cc4b269fc567116bf895d54bd8d9`.
|
||||
- JAR Sanctuary : SHA-256
|
||||
`8e35ef0f134a46b7760604175ad768264946d31fdd0694982e049e7a928fde2a`.
|
||||
|
||||
Reçu local : `build/wrench120-artifact.json`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.120](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.120)
|
||||
est publiée depuis `248dff47b7a0ebd11026230581fe69641dbde76d`. Le tag exact et les
|
||||
artefacts sont immuables ; leurs téléchargements publics sont vérifiés.
|
||||
Le canal packwiz avance à `e8060764a444a58b0a17318a3821dfeb1a0c33c6`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.120 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés, sans
|
||||
ouvrir de monde personnel. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.120/` de l'instance existante. Reçus locaux :
|
||||
`build/wrench120-isolated.json` et `build/wrench120-prism.json`.
|
||||
+77
-3
@@ -8,17 +8,91 @@ https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Les sources sont sur la branche de travail du ticket. La branche `packwiz`
|
||||
contient uniquement les manifestes de distribution produits par le build ; les
|
||||
contient les manifestes de distribution produits par le build et l’icône
|
||||
`icon.png` déclarée dans leur index ; les
|
||||
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.121 — texture de clé et pack personnel
|
||||
|
||||
PNG du créateur et copie complète modifiable depuis l'écran des packs.
|
||||
[Contrat et vérifications](resourcepack-template-beta121.md).
|
||||
[Release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
|
||||
publiée ; canal `e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.121/`. Le template personnel est créé par le
|
||||
joueur, jamais géré ou réécrit par packwiz.
|
||||
|
||||
## beta.120 — clé dorée et états de blocs
|
||||
|
||||
Gestes du debug stick, rotation des textures et assemblages prioritaires.
|
||||
[Contrat et vérifications](golden-wrench-beta120.md).
|
||||
[Release beta.120](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.120)
|
||||
publiée ; canal `e8060764a444a58b0a17318a3821dfeb1a0c33c6`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.120/`.
|
||||
|
||||
## beta.119 — pose centrée des sculptures
|
||||
|
||||
Centre accessible au point visé, bords conservés et grille entière de 1/16.
|
||||
[Contrat et vérifications](sculpture-center-beta119.md).
|
||||
[Release beta.119](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.119)
|
||||
publiée ; canal `5e823af7807f524f6394acad402fe62328c7e4ab`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.119/`.
|
||||
|
||||
## beta.118 — murets en briques colorées
|
||||
|
||||
Seize murets natifs, recettes, tailleur, eau et série créative par couleurs.
|
||||
[Contrat et vérifications](colored-brick-walls-beta118.md).
|
||||
[Release beta.118](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.118)
|
||||
publiée ; canal `e6bab433bea9528f67d39af0d6c7bcb6443e4817`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.118/`.
|
||||
|
||||
## beta.117 — grain des textures et sculptures en chapeau
|
||||
|
||||
Échantillonnage des pixels de plusieurs textures, grain stable même sur un
|
||||
modèle uni et conservation des données du chapeau.
|
||||
[Contrat et vérifications](sculpture-texture-grain-beta117.md).
|
||||
[Release beta.117](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.117)
|
||||
publiée ; canal `a5b75a70f86e42afb8e06ba4daa9880d61c6373e`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.117/`.
|
||||
|
||||
## beta.116 — palettes de sculpture et Fourneau
|
||||
|
||||
Le Clay Workshop utilise les nuances des textures de blocs ; le Fourneau
|
||||
corrige les décalages de cases et explique la chauffe.
|
||||
[Contrat et vérifications](sculpture-materials-beta116.md).
|
||||
[Release beta.116](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.116)
|
||||
publiée ; canal `c741870160f8a21f0a9793f20f0e624ebdbacdb6`. Deux
|
||||
synchronisations isolées puis deux dans la même instance **Sanctuary Beta**
|
||||
réussies. Les 923 fichiers personnels et réglages suivis sont inchangés ;
|
||||
sauvegardes restées fermées. Copie préalable des fichiers remplacés dans
|
||||
`sanctuary-backups/before-beta.116/`.
|
||||
|
||||
## beta.110 — version commune
|
||||
|
||||
Clay Workshop, Statuaire et import GLB intégrés aux œufs et à la neige saisonnière.
|
||||
[Contrat, vérifications et suivi de mise à jour](clay-workshop-integration-beta110.md).
|
||||
Archives normal/Test vérifiées. Publication du canal et synchronisation de
|
||||
l’instance Sanctuary Beta demandées ; leur résultat est consigné dans le ticket.
|
||||
[Release beta.110](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.110)
|
||||
publiée ; canal `828353a6e2bd747f4f473ae9fb9e965d38032419`. Deux synchronisations
|
||||
isolées puis deux dans la même instance **Sanctuary Beta** réussies. Minecraft
|
||||
26.3 finale, Loader 0.19.5, Fabric API 0.160.5+26.3. Les 923 fichiers personnels
|
||||
et réglages suivis sont inchangés ; sauvegardes restées fermées. Copie préalable
|
||||
des fichiers remplacés : `sanctuary-backups/before-beta.110/`.
|
||||
|
||||
## beta.105 — archives locales
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# RP-02 — Clé dorée et pack personnel — beta.121
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/resourcepack-template-beta121`.
|
||||
Le créateur fournit `golden_wrench.png` et valide un bouton qui crée une copie
|
||||
personnelle complète, modifiable et jamais écrasée par les mises à jour.
|
||||
|
||||
## Résultat
|
||||
|
||||
La clé dorée reprend exactement le PNG fourni, 16 × 16, sans conversion ni
|
||||
redessin. Le modèle reste un item plat `minecraft:item/handheld`. Empreinte :
|
||||
`a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af`.
|
||||
Le même fichier est fourni par le mod et par « Textures Sanctuary » ; désactiver
|
||||
le pack intégré conserve donc aussi la nouvelle clé.
|
||||
|
||||
Dans **Options → Packs de ressources**, **Personnaliser Sanctuary** crée
|
||||
`resourcepacks/Sanctuary-Custom/`. Le bouton devient **Ouvrir ma copie**.
|
||||
La copie contient toutes les ressources client de Sanctuary : textures, modèles,
|
||||
étiquettes d'items, blockstates, sons, shaders, langues et remplacements Minecraft
|
||||
présents dans le pack intégré. Celui-ci prend priorité sur les assets de base
|
||||
lors de l'export. Un guide FR/EN, la licence et les crédits accompagnent la copie.
|
||||
|
||||
Le joueur active sa copie avec les commandes natives et la place au-dessus des
|
||||
packs qu'il veut remplacer. Créer la copie ne change ni l'ordre ni la sélection.
|
||||
Une modification se recharge avec F3 + T ; désactiver le pack retrouve les
|
||||
ressources fournies par le mod. L'écran de sélection des datapacks n'est pas modifié.
|
||||
|
||||
La copie est un **instantané**. Elle ne se synchronise pas avec les mises à jour :
|
||||
les fichiers personnalisés restent toujours intacts. Le guide recommande de
|
||||
supprimer les fichiers non personnalisés pour laisser les prochaines ressources
|
||||
du mod s'appliquer. Pour repartir d'un instantané récent, renommer le dossier
|
||||
existant puis créer une nouvelle copie. Aucun fichier utilisateur n'est écrasé.
|
||||
|
||||
La copie s'effectue hors du fil de rendu, dans un dossier temporaire voisin,
|
||||
puis le résultat complet rejoint la liste native. Une erreur de copie est
|
||||
signalée par le bouton et le journal du jeu ; un chemin occupé par un fichier
|
||||
n'est pas remplacé. Aucune extraction au démarrage ni pendant les mises à jour.
|
||||
|
||||
## Distribution
|
||||
|
||||
`./gradlew assembleResourceTemplate` produit
|
||||
`build/Sanctuary-Template-beta.121.zip`, un pack personnel complet à décompresser
|
||||
dans `resourcepacks/Sanctuary-Custom/`. `assemblePack` le produit aussi.
|
||||
`build/Sanctuary-Resource-Pack-beta.121.zip` reste l'export du pack graphique
|
||||
intégré ; son manifeste versionné contient 151 fichiers et les images historiques
|
||||
sont conservées. L'identifiant intégré `sanctuary:textures` reste stable et son
|
||||
activation reste contrôlée par le joueur.
|
||||
|
||||
Le template ne contient pas tous les assets vanilla ni ceux des autres mods.
|
||||
Il ne contient pas les recettes, règles serveur ou sauvegardes. Il conserve
|
||||
les crédits et conditions des ressources d'origine. Aucune génération, aucun
|
||||
identifiant de bloc et aucun format de sauvegarde ne changent.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le parcours `ResourceTemplate121ClientChecks` passe en **35 s** sur le client
|
||||
Minecraft 26.3 macOS, sans ouvrir de monde :
|
||||
|
||||
- Bouton natif et disposition FR/EN aux échelles 2 et 3, captures relues.
|
||||
- Export complet de **1 202 fichiers**, empreintes identiques aux ressources
|
||||
sources après application des priorités du pack intégré.
|
||||
- PNG fourni vérifié par SHA-256 et affiché avec sa transparence.
|
||||
- Création sans changer la sélection ni l'ordre des packs.
|
||||
- Modification d'une texture et ajout d'un fichier personnel : aucun octet
|
||||
réécrit lors d'un second export ni au rechargement des ressources.
|
||||
- Priorité réelle de la texture personnalisée, puis retour à la clé fournie
|
||||
après désactivation. Activation et désactivation conservées à la redécouverte.
|
||||
- Chemin occupé par un fichier refusé sans le modifier ; aucun bouton ajouté
|
||||
à l'écran des datapacks.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuaryResourceTemplate121ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueur `TEMPLATE121_PASS` dans `build/template121-client.log` ; captures et
|
||||
empreintes de la copie dans `build/template121-evidence/`. Le premier essai
|
||||
avait échoué dans l'aperçu du test qui créait un ItemStack avant connexion à
|
||||
un monde ; cet aperçu affiche maintenant directement la texture résolue.
|
||||
Le modèle plat existant n'est pas modifié. Pas d'essai Windows ni d'ouverture
|
||||
automatisée du gestionnaire de fichiers. Le GameTest serveur dédié reste exclu
|
||||
conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 10 s**, 126 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. La comparaison avec beta.120 limite les différences de production
|
||||
aux deux nouvelles classes, au hook d'interface, aux libellés et aux ressources
|
||||
prévues (12 entrées). Aucun changement de gameplay, de données serveur ou de
|
||||
monde. Les packs normal/Test contiennent le même JAR Sanctuary, sans fixture
|
||||
ni sauvegarde. Le ZIP du template et l'export natif ont exactement les mêmes
|
||||
1 202 fichiers et empreintes. Le pack intégré conserve ses 151 fichiers vérifiés.
|
||||
|
||||
Reçu : `build/template121-artifact.json`. Artefacts locaux validés :
|
||||
|
||||
- `Sanctuary-beta.121.mrpack` : 10261791 octets, SHA-256
|
||||
`040d68f342155120fa21e0ffcdf34aa38e3225c78862960536622de53ed9fd9f`.
|
||||
- `Sanctuary-Test-beta.121.mrpack` : 10280716 octets, SHA-256
|
||||
`7f5bbd4e13aea4c245d072725c094ca64826ee8debc8607623783c114d437685`.
|
||||
- `Sanctuary-Resource-Pack-beta.121.zip` : 3757952 octets, SHA-256
|
||||
`bbf3c18ea6abc64d951911215975cb6c48c7fc5e0246f6e5f5192a3d061c211d`.
|
||||
- `Sanctuary-Template-beta.121.zip` : 4315131 octets, SHA-256
|
||||
`6fad376c53474fa8bcb0a2e6ae4c13ea61ab91bdd78735be443032d485ed541a`.
|
||||
|
||||
JAR Sanctuary : SHA-256
|
||||
`4276f4fc9834805e3e6d12cd19adba7837e3007656b77f31d525c7b976f9925d`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
La [release beta.121](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.121)
|
||||
est publiée depuis `d9b3dd6bedf32448f8c6e5bfa4da10cb69968bc7`. Tag et artefacts immuables,
|
||||
téléchargements publics vérifiés. Canal packwiz :
|
||||
`e3e6572da9288fbbcfa36bf6b7dd3aa0d1c54f5e`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans **Sanctuary Beta** réussissent.
|
||||
Un seul JAR Sanctuary beta.121 est actif ; le second passage est identique.
|
||||
Les **923 fichiers personnels et réglages suivis** restent inchangés,
|
||||
y compris les packs de ressources. Aucune copie personnelle n'est créée par
|
||||
la mise à jour : chaque joueur utilise le bouton quand il le souhaite.
|
||||
Aucun monde personnel ouvert. Copie préalable dans
|
||||
`sanctuary-backups/before-beta.121/` de l'instance existante ; reçus
|
||||
`build/template121-isolated.json` et `build/template121-prism.json`.
|
||||
|
||||
- [Template personnel complet](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Template-beta.121.zip).
|
||||
- [Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-beta.121.mrpack).
|
||||
- [Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.121/Sanctuary-Test-beta.121.mrpack).
|
||||
@@ -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,90 @@
|
||||
# SCULPT-09 — Forme physique et lumière — beta.122
|
||||
|
||||
Contrat du 17 septembre 2026, branche `codex/sculpture-shape-light-beta122`.
|
||||
Le créateur demande des bounding boxes adaptées aux sculptures et la suppression
|
||||
des ombres cubiques visibles lorsqu'on en pose plusieurs côte à côte.
|
||||
|
||||
La sélection, le contact avec les entités et les collisions suivent les voxels
|
||||
occupés de chaque sculpture, y compris les creux. Le rendu et la forme physique
|
||||
partagent leur placement entier sur la grille 1/16, après orientation et ancrage.
|
||||
Le centre, les quatre bords et les dimensions impaires gardent leur alignement.
|
||||
La pose vérifie aussi les voxels portés avant que l’entité de bloc existe :
|
||||
elle refuse une intersection avec le joueur et autorise les espaces libres.
|
||||
La forme est construite directement sur une grille native 16³ et mise en cache
|
||||
par entité de bloc, modèle et état ; aucun calcul complet à chaque interrogation.
|
||||
|
||||
Les sculptures transmettent la lumière et ne sont plus utilisées comme cubes
|
||||
pleins par l'occlusion ambiante. Leurs faces restent éclairées par le monde,
|
||||
sans émission de lumière et sans rendu à luminosité maximale. Cette règle est
|
||||
commune aux sculptures : l'éclairage natif stocké par bloc ne permet pas une
|
||||
ombre distincte pour chaque petit voxel. L'atelier d'argile reste inchangé.
|
||||
|
||||
Aucun identifiant, blockstate, format d'item ou schéma sauvegardé ne change.
|
||||
Les formes sont recalculées à partir des voxels existants ; aucune migration,
|
||||
aucun parcours de monde ni régénération de chunk. Les données inconnues gardent
|
||||
leur conservation opaque et une petite base sélectionnable pour récupérer l'item.
|
||||
Le travail de documentation déjà présent dans l'autre branche reste intact.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
Le parcours `Sculpture122ClientChecks` passe en **37 s** sur Minecraft 26.3,
|
||||
client macOS et serveur intégré, dans un monde plat jetable de graine 122 :
|
||||
|
||||
- Pose par la commande native d'utilisation ; sélection du pilier et visée
|
||||
à travers le trou d'une arche jusqu'au bloc derrière, captures relues.
|
||||
- Requêtes natives de collision côté serveur et client : voxels occupés solides,
|
||||
espace vide libre, aucune profondeur artificielle d'un bloc entier.
|
||||
- Pose refusée dans le joueur, sans consommer l'objet ; petite sculpture acceptée
|
||||
dans la partie libre du même bloc, sans intersection avec le joueur.
|
||||
- **80 cas** confrontent le volume et les faces de collision au maillage réellement
|
||||
émis : arche creuse, marges vides, cube de 4 096 voxels, voxel unique, quatre
|
||||
orientations et cinq ancrages. Chaque interrogation répétée réutilise la forme.
|
||||
- Régression de **5 120 placements** du rendu existant : dimensions paires/impaires,
|
||||
orientations et ancrages, plus marqueurs de direction et modèles avec marges.
|
||||
- Changement d'orientation/ancrage puis changement des voxels sur la même entité :
|
||||
cache recalculé et forme client synchronisée.
|
||||
- **75 sculptures en trois couches** : skylight natif conservé à 15 partout.
|
||||
Un toit de pierre témoin produit toujours une ombre. La lumière d'un bloc
|
||||
traverse une sculpture avec l'atténuation de distance native (13 à deux blocs).
|
||||
- Le rendu reçoit la lumière réelle du monde : 15 au ciel, **0 dans une pièce
|
||||
fermée et obscure**. Aucun rendu artificiellement lumineux.
|
||||
- Sauvegarde/reconnexion : modèle, collisions et lumière conservés sur les
|
||||
deux côtés, sans fichier 3D source.
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest \
|
||||
-PsanctuaryClientTests=true -PsanctuarySculpture122ClientTests=true \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||
```
|
||||
|
||||
Marqueurs `SCULPTURE122_PASS`, `SCULPTURE122_GEOMETRY_PASS`,
|
||||
`SCULPTURE122_PLACEMENT_PASS`, `SCULPTURE122_LIGHT_PASS` et
|
||||
`SCULPTURE113_GEOMETRY_PASS` dans `build/sculpture122-client.log` ; captures
|
||||
conservées dans `build/sculpture122-evidence/`. Le test prépare explicitement
|
||||
son sol à Y=0 : le preset plat nu commence à Y=-1.
|
||||
Aucun monde personnel ouvert ; pas d'essai Windows ni depuis deux ordinateurs,
|
||||
ni de garantie sur les ombres ajoutées par des shaders tiers.
|
||||
Le GameTest serveur dédié reste exclu conformément au refus antérieur de son EULA.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||
passe en **2 min 13 s**, 126 tâches. Les sources JAR correspondent aux sources
|
||||
Java du dépôt. Par rapport à beta.121, les différences de production se limitent
|
||||
aux classes des sculptures et à la nouvelle géométrie partagée (14 entrées,
|
||||
y compris le déplacement du sélecteur synthétique d'orientation). Toutes les
|
||||
ressources client, données serveur et configurations de mixins sont identiques.
|
||||
Les packs normal/Test contiennent le même JAR, sans sauvegarde ni fixture.
|
||||
Le template beta.122 contient les mêmes ressources que beta.121, octet pour octet.
|
||||
|
||||
Reçu local : `build/sculpture122-artifact.json`.
|
||||
|
||||
- `Sanctuary-beta.122.mrpack` : 10267459 octets, SHA-256
|
||||
`b6e24d65797eb7f16670d2f3a591b5f2476820608f72241ecd37fb78a486008e`.
|
||||
- `Sanctuary-Test-beta.122.mrpack` : 10286380 octets, SHA-256
|
||||
`90487ee3ad4298fccb8c24e8781af4cbf043d88dc0379fba5c21d99733dac92d`.
|
||||
|
||||
JAR Sanctuary : SHA-256
|
||||
`ff8ebf89f39a64809fae6912253386793e9b5e66ec0adb9fa7521729893d406e`.
|
||||
|
||||
## Publication et installation
|
||||
|
||||
Artefacts prêts ; publication et synchronisation à effectuer.
|
||||
@@ -0,0 +1,110 @@
|
||||
# beta.117 — Grain des textures et sculptures en chapeau
|
||||
|
||||
Ticket sur `codex/sculpture-texture-grain-beta117`, Minecraft 26.3.
|
||||
|
||||
En beta.116, une couleur du modèle source était associée à une seule nuance
|
||||
de matière. Un modèle uni restait donc uni malgré les pixels différents de la
|
||||
texture. La sélection utilise maintenant les textures des faces du modèle de
|
||||
bloc, avec leurs propres teintes du pack actif. Un échantillonnage de pixels
|
||||
répartit leurs couleurs exactes dans les voxels, sans couleur interpolée.
|
||||
|
||||
## Coût et comportement
|
||||
|
||||
Au maximum 32 matériaux de faces et 16 × 16 échantillons par matériau sont lus,
|
||||
puis ramenés à la palette existante de 256 entrées. Les répétitions conservent
|
||||
la fréquence des nuances. Les pixels transparents sont écartés ; les textures
|
||||
animées utilisent la première image. Les blocs sans texture lisible gardent
|
||||
le repli antérieur sur la texture représentative, puis la couleur de carte.
|
||||
|
||||
La sélection d'une nuance utilise un mélange entier déterministe des coordonnées
|
||||
de chaque voxel. Un modèle uni utilise toute la palette ; un modèle ombré
|
||||
utilise des plages de nuances qui suivent ses zones claires et sombres.
|
||||
Ce grain est un transfert de palette, pas une projection UV conservant les
|
||||
motifs ou les veinures du bloc. Les voxels restent opaques.
|
||||
|
||||
Le calcul utilise Java standard et les modèles/textures déjà chargés par
|
||||
Minecraft, sans bibliothèque supplémentaire. Aucun calcul de matière dans
|
||||
le rendu par image. Le client ne rééchantillonne qu'au changement de matière
|
||||
ou de ressources ; une rotation réutilise la palette. Le serveur conserve
|
||||
le modèle recoloré pour les fabrications suivantes avec la même sélection.
|
||||
|
||||
## Chapeaux et données
|
||||
|
||||
Le système de blocs portés retirait `BLOCK_ENTITY_DATA` des objets pour isoler
|
||||
les inventaires des coffres et fourneaux. Sur une sculpture, ce composant est
|
||||
la géométrie elle-même : le slot contenait encore un objet, devenu invisible.
|
||||
Les sculptures conservent désormais ce composant à l'équipement et au retrait.
|
||||
|
||||
Si une ancienne sculpture équipée a perdu ce composant, mais que sa session
|
||||
de chapeau possède encore le modèle, il est restauré depuis cette donnée
|
||||
serveur. La restauration ne fabrique aucune géométrie manquante. Les objets
|
||||
déjà retirés et dépourvus de toute copie du modèle ne sont pas reconstructibles
|
||||
par ce correctif. Les règles de contenu des coffres restent inchangées.
|
||||
|
||||
Identifiants, schéma 1 des sculptures, structure des accessoires et paquet
|
||||
de sélection inchangés. Les couleurs déjà sauvegardées restent intactes.
|
||||
Les deux côtés utilisent beta.117 pour le même calcul de prévisualisation et
|
||||
de fabrication. Aucun monde personnel ouvert ni migration de sauvegarde.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Le scénario client natif complet passe en 2 min 1 s sur macOS avec serveur
|
||||
intégré, monde plat jetable de graine 116. Il reprend le scénario beta.116
|
||||
avec un GLB entièrement uni, plus les régressions de l'atelier 106/111/112/113 :
|
||||
|
||||
- 1 207 objets-blocs échantillonnés, huit fabrications payées avec aperçu
|
||||
identique au résultat serveur, consommation exacte d'un bloc.
|
||||
- Répartition 25 % / 75 % de deux couleurs de texture sur un modèle uni,
|
||||
aucune couleur inventée, conservation des zones claires et sombres.
|
||||
- Couleurs réellement présentes dans les textures natives des faces, avec
|
||||
plusieurs matériaux d'une bûche ; grain présent sur pierre, bois et minerai.
|
||||
- Motif indépendant de l'ordre des voxels, stable et sauvegardé ; rechargement
|
||||
des ressources, palette bornée, inventaire plein et sélections périmées.
|
||||
- Vrais clics des slots hotbar/chapeau, conservation exacte des composants
|
||||
et du nom personnalisé, icône et modèle porté visibles, retrait/rééquipement.
|
||||
- Restauration d'un objet privé de sa géométrie depuis une ancienne session
|
||||
équipée, puis sauvegarde et reconnexion du client avec le chapeau intact.
|
||||
- Pose, bord visé, orientation, casse/repose, reconnexion sans fichier source,
|
||||
menus FR/EN et génération du plan du Métabli inchangés.
|
||||
|
||||
Mesure indicative après échauffement, modèle maximal de 4 096 voxels,
|
||||
101 applications sur le Mac de développement : médiane **0,283 ms**, p95
|
||||
**0,971 ms**. Ce n'est pas une garantie de temps sur d'autres machines ; le
|
||||
calcul n'est exécuté ni chaque image ni à chaque objet fabriqué identique.
|
||||
|
||||
Marqueurs `SCULPTURE117_GRAIN_PASS`, `SCULPTURE117_HAT_PASS`,
|
||||
`SCULPTURE117_HAT_RECONNECT_PASS`, `SCULPTURE116_PASS` dans
|
||||
`build/sculpture117-final-client.log`. Captures relues dans
|
||||
`build/sculpture117-evidence/`. Pas d'essai Windows ou LAN. Le GameTest dédié
|
||||
reste exclu selon le refus antérieur de son EULA.
|
||||
|
||||
`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit
|
||||
en **3 min 7 s**, 125 tâches dont 105 exécutées. JAR sources concordants,
|
||||
versions, contenu et intégrité des deux archives vérifiés. La comparaison
|
||||
avec beta.116 limite les changements de production au calcul des matières,
|
||||
à son cache dans le menu et au traitement des sculptures en chapeau.
|
||||
Les modèles de test et classes de GameTest sont absents de la livraison.
|
||||
|
||||
- `Sanctuary-beta.117.mrpack` : 10191137 octets, SHA-256
|
||||
`adbe5a40bbd050ebc84ff8aca61a7a3643dcf25ec8f2ac59b91561e48275cbbb`.
|
||||
- `Sanctuary-Test-beta.117.mrpack` : 10210059 octets, SHA-256
|
||||
`0dacb12b30bde66c647871101a4f4c6b0b58e7ceb29f7743398f8a569b958a6e`.
|
||||
- JAR Sanctuary :
|
||||
`1ce63f97f17c6098f3c86c6753f9b912bce39d88e05ba240423c38bdc97307ee`.
|
||||
|
||||
Reçu : `build/sculpture117-artifact.json`.
|
||||
|
||||
## Publication et instance
|
||||
|
||||
La [release beta.117](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.117)
|
||||
est publiée : JAR, MRpack normal/Test et amorçage Prism, téléchargements publics
|
||||
et empreintes vérifiés. Le tag immuable désigne
|
||||
`0f1efdec830342e25d3316b356330ad937d0d942`. Le canal packwiz avance au commit
|
||||
`a5b75a70f86e42afb8e06ba4daa9880d61c6373e`.
|
||||
|
||||
Deux synchronisations isolées puis deux dans l'unique instance **Sanctuary
|
||||
Beta** réussissent, la seconde passe laissant les fichiers gérés identiques.
|
||||
Un seul JAR Sanctuary beta.117 est actif. Les 923 fichiers personnels et
|
||||
réglages suivis sont inchangés ; aucun monde personnel n'a été ouvert.
|
||||
Copie préalable des fichiers remplacés dans `sanctuary-backups/before-beta.117/`.
|
||||
Reçus : `build/sculpture117-isolated.json`, `build/sculpture117-prism.json`.
|
||||
+3
-3
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.110
|
||||
pack_version=beta.110
|
||||
resource_pack_version=beta.090
|
||||
mod_version=beta.122
|
||||
pack_version=beta.122
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -93,6 +93,28 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def sculpture122Client = providers.gradleProperty('sanctuarySculpture122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture122ClientTests', sculpture122Client)
|
||||
if (sculpture122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture122ClientChecks') }
|
||||
|
||||
def template121Client = providers.gradleProperty('sanctuaryResourceTemplate121ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryResourceTemplate121ClientTests', template121Client)
|
||||
if (template121Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ResourceTemplate121ClientChecks') }
|
||||
|
||||
def wrench120Client = providers.gradleProperty('sanctuaryWrench120ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWrench120ClientTests', wrench120Client)
|
||||
if (wrench120Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench120ClientChecks') }
|
||||
|
||||
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') }
|
||||
@@ -430,6 +452,15 @@ tasks.withType(JavaCompile).configureEach {
|
||||
}
|
||||
|
||||
processResources {
|
||||
from(rootProject.file('ressources-pack/template')) {
|
||||
exclude '**/.DS_Store'
|
||||
into 'resourcepacks/template'
|
||||
}
|
||||
from(rootProject.file('LICENSE')) {
|
||||
rename { 'LICENSE_sanctuary' }
|
||||
into 'resourcepacks/template'
|
||||
}
|
||||
from(rootProject.file('THIRD_PARTY_NOTICES.md')) { into 'resourcepacks/template' }
|
||||
from(rootProject.file('ressources-pack/sanctuary')) {
|
||||
include 'assets/**', 'pack.mcmeta', 'pack.png'
|
||||
exclude '**/.DS_Store'
|
||||
|
||||
+12
-11
@@ -37,10 +37,10 @@ public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
world.getServer().runOnServer(s->{
|
||||
var p=s.getPlayerList().getPlayers().getFirst();var level=p.level();var pos=new BlockPos(0,3,0);
|
||||
check(ColoredBricks.families().size()==16,"All colors registered");int index=0;
|
||||
check(ColoredBricks.families().size()==16,"All colors registered");Walls118Checks.server(s);int index=0;
|
||||
for(var f:ColoredBricks.families()){
|
||||
String color=f.color().getName();
|
||||
for(var block:List.of(f.bricks(),f.slab(),f.stairs(),f.clay()))check(block.asItem()!=Items.AIR,"Block item exists "+block);
|
||||
for(var block:List.of(f.bricks(),f.slab(),f.stairs(),f.wall(),f.clay()))check(block.asItem()!=Items.AIR,"Block item exists "+block);
|
||||
check(f.bricks().defaultBlockState().is(BlockTags.MINEABLE_WITH_PICKAXE),"Pickaxe tag "+color);
|
||||
check(f.clay().defaultBlockState().is(BlockTags.MINEABLE_WITH_SHOVEL),"Shovel tag "+color);
|
||||
var shovel=new ItemStack(Items.DIAMOND_SHOVEL);var silk=shovel.copy();silk.enchant(level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT).getOrThrow(Enchantments.SILK_TOUCH),1);
|
||||
@@ -52,7 +52,7 @@ public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
drop(Block.getDrops(f.stairs().defaultBlockState(),level,pos,null),f.stairs().asItem(),1);
|
||||
check(f.slab().defaultBlockState().setValue(SlabBlock.WATERLOGGED,true).getFluidState().isSource(),"Slab water "+color);
|
||||
check(f.stairs().defaultBlockState().setValue(StairBlock.WATERLOGGED,true).getFluidState().isSource(),"Stair water "+color);
|
||||
for(String suffix:List.of("clay_ball_from_dye","clay_from_dye","brick_from_dye","bricks_from_dye","clay","bricks","brick_slab","brick_stairs","brick_slab_from_stonecutting","brick_stairs_from_stonecutting","clay_ball_smelting","clay_smelting"))check(s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+suffix))).isPresent(),"Recipe loaded "+color+"_"+suffix);
|
||||
for(String suffix:List.of("clay_ball_from_dye","clay_from_dye","brick_from_dye","bricks_from_dye","clay","bricks","brick_slab","brick_stairs","brick_slab_from_stonecutting","brick_stairs_from_stonecutting","brick_wall","brick_wall_from_stonecutting","clay_ball_smelting","clay_smelting"))check(s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+suffix))).isPresent(),"Recipe loaded "+color+"_"+suffix);
|
||||
craft(s,level,2,2,List.of(f.brick(),f.brick(),f.brick(),f.brick()),f.bricks().asItem(),1);
|
||||
craft(s,level,2,2,List.of(f.clayBall(),f.clayBall(),f.clayBall(),f.clayBall()),f.clay().asItem(),1);
|
||||
craft(s,level,3,1,List.of(f.bricks().asItem(),f.bricks().asItem(),f.bricks().asItem()),f.slab().asItem(),6);
|
||||
@@ -62,39 +62,40 @@ public final class Bricks102ClientChecks implements FabricClientGameTest {
|
||||
var input=new SingleRecipeInput(new ItemStack(f.clayBall()));var cooked=s.getRecipeManager().getRecipeFor(RecipeType.SMELTING,input,level).orElseThrow().value().assemble(input);check(cooked.is(f.brick()),"Smelt brick "+color);
|
||||
input=new SingleRecipeInput(new ItemStack(f.clay()));cooked=s.getRecipeManager().getRecipeFor(RecipeType.SMELTING,input,level).orElseThrow().value().assemble(input);check(cooked.is(BuiltInRegistries.ITEM.getValue(net.minecraft.resources.Identifier.withDefaultNamespace(color+"_terracotta"))),"Smelt terracotta "+color);
|
||||
var stoneInput=new SingleRecipeInput(new ItemStack(f.bricks()));
|
||||
for(var variant:List.of(new Object[]{"brick_slab",f.slab().asItem(),2},new Object[]{"brick_stairs",f.stairs().asItem(),1})){
|
||||
for(var variant:List.of(new Object[]{"brick_slab",f.slab().asItem(),2},new Object[]{"brick_stairs",f.stairs().asItem(),1},new Object[]{"brick_wall",f.wall().asItem(),1})){
|
||||
var recipe=(StonecutterRecipe)s.getRecipeManager().byKey(ResourceKey.create(Registries.RECIPE,SanctuaryMod.id(color+"_"+variant[0]+"_from_stonecutting"))).orElseThrow().value();check(recipe.matches(stoneInput,level),"Stonecutting accepts input");var cut=recipe.assemble(stoneInput);check(cut.is((Item)variant[1])&&cut.getCount()==(int)variant[2],"Stonecutting result");
|
||||
}
|
||||
int x=(index%8)*3,z=(index/8)*5;level.setBlockAndUpdate(new BlockPos(x,1,z),f.bricks().defaultBlockState());level.setBlockAndUpdate(new BlockPos(x,2,z),f.slab().defaultBlockState().setValue(SlabBlock.TYPE,SlabType.TOP));level.setBlockAndUpdate(new BlockPos(x+1,1,z),f.stairs().defaultBlockState().setValue(StairBlock.FACING,Direction.SOUTH));level.setBlockAndUpdate(new BlockPos(x,1,z+1),f.clay().defaultBlockState());index++;
|
||||
}
|
||||
fr.koka.sanctuary.recipes.RecipeService.discover(p,Items.CLAY_BALL);fr.koka.sanctuary.recipes.RecipeService.refresh(p);
|
||||
var known=fr.koka.sanctuary.recipes.RecipeService.knowledge(p);
|
||||
for(var f:ColoredBricks.families())check(known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_stairs_from_stonecutting"),"Collection unlock "+f.color());
|
||||
check(known.recipes.contains("sanctuary:clay_workshop"),"Existing workshop recipe remains discoverable");
|
||||
for(var f:ColoredBricks.families())check(known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_stairs_from_stonecutting")&&known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_wall")&&known.recipes.contains("sanctuary:"+f.color().getName()+"_brick_wall_from_stonecutting"),"Collection unlock "+f.color());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
p.teleportTo(11,8,19);p.setYRot(180);p.setXRot(26);p.getAbilities().flying=true;p.onUpdateAbilities();
|
||||
});
|
||||
c.runOnClient(m->{
|
||||
CreativeModeTabs.tryRebuildTabContents(m.level.enabledFeatures(),true,m.level.registryAccess());
|
||||
var blocks=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.COLORED_BLOCKS).value().getDisplayItems();
|
||||
var ingredients=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.INGREDIENTS).value().getDisplayItems();
|
||||
var ingredients=BuiltInRegistries.CREATIVE_MODE_TAB.getOrThrow(CreativeModeTabs.INGREDIENTS).value().getDisplayItems();Walls118Checks.creative(new ArrayList<>(blocks));
|
||||
for(var f:ColoredBricks.families()){
|
||||
for(var b:List.of(f.bricks(),f.slab(),f.stairs(),f.clay())){
|
||||
for(var b:List.of(f.bricks(),f.slab(),f.stairs(),f.wall(),f.clay())){
|
||||
check(blocks.stream().anyMatch(i->i.is(b.asItem())),"Creative color tab "+b);
|
||||
for(var state:b.getStateDefinition().getPossibleStates()){
|
||||
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);check(!parts.isEmpty(),"Baked model "+state);
|
||||
var parts=new ArrayList<net.minecraft.client.renderer.block.dispatch.BlockStateModelPart>();m.getModelManager().getBlockStateModelSet().get(state).collectParts(net.minecraft.util.RandomSource.create(0),parts);check(b instanceof WallBlock||!parts.isEmpty(),"Baked model "+state);
|
||||
for(var part:parts)for(var direction:new Direction[]{Direction.UP,Direction.DOWN,Direction.NORTH,Direction.SOUTH,Direction.EAST,Direction.WEST,null})for(var quad:part.getQuads(direction))check(!quad.materialInfo().sprite().contents().name().toString().contains("missingno"),"No missing texture "+state);
|
||||
}
|
||||
}
|
||||
for(var item:List.of(f.clayBall(),f.brick()))check(ingredients.stream().anyMatch(i->i.is(item)),"Creative ingredients "+item);
|
||||
}
|
||||
});
|
||||
c.waitTicks(20);c.runOnClient(m->{m.player.setYRot(180);m.player.setXRot(26);});c.waitTicks(4);c.takeScreenshot("bricks102-gallery");saved=world.getWorldSave();System.out.println("BRICKS102_NATIVE_PASS colors=16 recipes=192 blocks=64 items=96");
|
||||
c.waitTicks(20);c.runOnClient(m->{m.player.setYRot(180);m.player.setXRot(26);});c.waitTicks(4);c.takeScreenshot("bricks102-gallery");saved=world.getWorldSave();System.out.println("BRICKS102_NATIVE_PASS colors=16 recipes=224 blocks=80 items=112");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);
|
||||
world.getServer().runOnServer(s->{var level=s.getPlayerList().getPlayers().getFirst().level();int i=0;for(var f:ColoredBricks.families()){var pos=new BlockPos((i%8)*3,1,(i/8)*5);check(level.getBlockState(pos).is(f.bricks()),"Saved bricks "+f.color());check(level.getBlockState(pos.above()).getValue(SlabBlock.TYPE)==SlabType.TOP,"Saved slab state");i++;}});
|
||||
world.getServer().runOnServer(s->{var level=s.getPlayerList().getPlayers().getFirst().level();Walls118Checks.saved(level);int i=0;for(var f:ColoredBricks.families()){var pos=new BlockPos((i%8)*3,1,(i/8)*5);check(level.getBlockState(pos).is(f.bricks()),"Saved bricks "+f.color());check(level.getBlockState(pos.above()).getValue(SlabBlock.TYPE)==SlabType.TOP,"Saved slab state");i++;}});
|
||||
}
|
||||
System.out.println("BRICKS102_PASS");
|
||||
System.out.println("BRICKS102_PASS");System.out.println("WALLS118_PASS native placement, mixed connections, tall sides, waterlogging, collision, crafting, stonecutting, loot, collection, creative order, baked models and reconnect");
|
||||
}
|
||||
private static void craft(net.minecraft.server.MinecraftServer s,net.minecraft.server.level.ServerLevel level,int w,int h,List<Item> items,Item result,int count){var input=CraftingInput.of(w,h,items.stream().map(ItemStack::new).toList());var stack=s.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,input,level).orElseThrow().value().assemble(input);check(stack.is(result)&&stack.getCount()==count,"Craft "+result);}
|
||||
private static void drop(List<ItemStack> drops,Item item,int count){check(drops.size()==1&&drops.getFirst().is(item)&&drops.getFirst().getCount()==count,"Loot "+item+" expected "+count+" got "+drops);}
|
||||
|
||||
+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);}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.packs.PackSelectionScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.repository.PackCompatibility;
|
||||
|
||||
/** Native screen/export, complete snapshot, real resource precedence and preservation of edits. */
|
||||
public final class ResourceTemplate121ClientChecks implements FabricClientGameTest {
|
||||
private static final String BUILTIN = "sanctuary:textures";
|
||||
private static final String PERSONAL = "file/" + ResourcePackTemplate.FOLDER;
|
||||
private static final String WRENCH = "sanctuary:textures/item/golden_wrench.png";
|
||||
private static final String WRENCH_PATH = "assets/sanctuary/textures/item/golden_wrench.png";
|
||||
private static final String WRENCH_SHA = "a97570908db75caf8e6fc2f5ecabf12c54fd99bfea7be634df66c4310dca22af";
|
||||
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
try { run(c); } catch (IOException error) { throw new AssertionError(error); }
|
||||
}
|
||||
|
||||
private void run(ClientGameTestContext c) throws IOException {
|
||||
var previous = c.computeOnClient(m -> m.gui.screen());
|
||||
var packDirectory = c.computeOnClient(Minecraft::getResourcePackDirectory);
|
||||
var selected = c.computeOnClient(m -> List.copyOf(m.getResourcePackRepository().getSelectedIds()));
|
||||
var target = packDirectory.resolve(ResourcePackTemplate.FOLDER);
|
||||
check(!Files.exists(target), "Fresh disposable client directory");
|
||||
c.runOnClient(m -> {
|
||||
check(source(m).equals(BUILTIN), "Built-in pack active");
|
||||
check(hash(resource(m, WRENCH)).equals(WRENCH_SHA), "Exact creator PNG active");
|
||||
m.getWindow().setWindowed(1280, 800);
|
||||
});
|
||||
for (int scale : new int[]{2, 3}) for (String language : List.of("fr_fr", "en_us")) {
|
||||
c.runOnClient(m -> {
|
||||
m.options.guiScale().set(scale); m.resizeGui();
|
||||
m.getLanguageManager().setSelected(language);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
screen(m, packDirectory);
|
||||
var button = button(m, "customize");
|
||||
check(button.getX() >= 0 && button.getRight() <= m.gui.screen().width, "Template button fits width");
|
||||
check(button.getY() > 60 && button.getBottom() < m.gui.screen().height, "Template button fits height");
|
||||
var done = m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> key(b, "gui.done")).findFirst().orElseThrow();
|
||||
check(done.getY() >= button.getBottom() + 4, "Separate native footer row");
|
||||
check(m.font.width(button.getMessage()) < button.getWidth() - 8, "Localized label fits");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("template121-create-" + language + "-" + scale);
|
||||
}
|
||||
c.runOnClient(m -> button(m, "customize").onPress(new KeyEvent(InputConstants.KEY_RETURN, 0, 0)));
|
||||
c.waitFor(m -> m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.anyMatch(b -> b.active && key(b, "sanctuary.resource_pack.open_copy")), 1200);
|
||||
c.waitFor(m -> m.getResourcePackRepository().getAvailableIds().contains(PERSONAL), 1200);
|
||||
c.runOnClient(m -> {
|
||||
check(List.copyOf(m.getResourcePackRepository().getSelectedIds()).equals(selected), "Creating never changes selection");
|
||||
check(m.getResourcePackRepository().getPack(PERSONAL).getCompatibility() == PackCompatibility.COMPATIBLE,
|
||||
"Template format accepted by exact Minecraft version");
|
||||
});
|
||||
var mod = FabricLoader.getInstance().getModContainer("sanctuary").orElseThrow();
|
||||
Map<String, String> expected = new TreeMap<>();
|
||||
merge(expected, mod.findPath("assets").orElseThrow(), "assets/");
|
||||
merge(expected, mod.findPath("resourcepacks/textures/assets").orElseThrow(), "assets/");
|
||||
merge(expected, mod.findPath("resourcepacks/template").orElseThrow(), "");
|
||||
expected.put("pack.png", hash(Files.readAllBytes(mod.findPath("resourcepacks/textures/pack.png").orElseThrow())));
|
||||
check(snapshot(target).equals(expected), "Complete byte-identical snapshot with built-in precedence");
|
||||
Files.writeString(packDirectory.getParent().resolve("template121-export-sha256.json"),
|
||||
new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(expected));
|
||||
check(expected.containsKey("README.md") && expected.containsKey("LICENSE_sanctuary")
|
||||
&& expected.containsKey("THIRD_PARTY_NOTICES.md"), "Editable copy includes guide and credits");
|
||||
check(expected.keySet().stream().noneMatch(p -> p.startsWith("data/") || p.endsWith(".class")), "Client assets only");
|
||||
c.waitTicks(4); c.takeScreenshot("template121-copy-ready");
|
||||
c.runOnClient(m -> m.gui.setScreen(new Screen(Component.literal("Golden wrench")) {
|
||||
@Override public void extractRenderState(GuiGraphicsExtractor g, int x, int y, float delta) {
|
||||
g.fill(0, 0, width, height, 0xff292929);
|
||||
g.centeredText(font, Component.translatable("item.sanctuary.golden_wrench"), width / 2, 55, 0xffffffff);
|
||||
g.blit(RenderPipelines.GUI_TEXTURED, Identifier.parse(WRENCH), width / 2 - 48, 85, 0, 0, 96, 96, 16, 16, 16, 16);
|
||||
}
|
||||
}));
|
||||
c.waitTicks(4); c.takeScreenshot("template121-wrench-exact-texture");
|
||||
|
||||
byte[] replacement = c.computeOnClient(m -> resource(m, "minecraft:textures/item/golden_pickaxe.png"));
|
||||
Files.write(target.resolve(WRENCH_PATH), replacement);
|
||||
Files.writeString(target.resolve("my-notes.txt"), "Personal edits must survive all subsequent exports.");
|
||||
var edited = snapshot(target);
|
||||
check(ResourcePackTemplate.create(packDirectory).equals(target.toAbsolutePath().normalize()), "Existing copy returned");
|
||||
check(snapshot(target).equals(edited), "Second export preserves every edited byte and extra file");
|
||||
c.runOnClient(m -> {
|
||||
var repo = m.getResourcePackRepository(); repo.addPack(PERSONAL); m.options.updateResourcePacks(repo);
|
||||
});
|
||||
c.waitFor(m -> source(m).equals(PERSONAL) && m.gui.overlay() == null, 1200);
|
||||
c.runOnClient(m -> {
|
||||
check(Arrays.equals(resource(m, WRENCH), replacement), "Personal edit overrides built-in texture");
|
||||
var repo = m.getResourcePackRepository(); repo.reload(); m.options.loadSelectedResourcePacks(repo);
|
||||
check(repo.getSelectedIds().contains(PERSONAL), "Personal activation persists after rediscovery");
|
||||
screen(m, packDirectory); button(m, "open_copy");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("template121-personal-active");
|
||||
var reload = c.computeOnClient(Minecraft::reloadResourcePacks);
|
||||
c.waitFor(m -> reload.isDone() && m.gui.overlay() == null, 1200); reload.join();
|
||||
check(snapshot(target).equals(edited), "Reload never writes personal assets");
|
||||
c.runOnClient(m -> {
|
||||
check(Arrays.equals(resource(m, WRENCH), replacement), "Reload still uses personal PNG");
|
||||
var repo = m.getResourcePackRepository(); repo.removePack(PERSONAL); m.options.updateResourcePacks(repo);
|
||||
});
|
||||
c.waitFor(m -> source(m).equals(BUILTIN) && m.gui.overlay() == null, 1200);
|
||||
c.runOnClient(m -> {
|
||||
check(hash(resource(m, WRENCH)).equals(WRENCH_SHA), "Disabling restores creator texture");
|
||||
var repo = m.getResourcePackRepository(); repo.reload(); m.options.loadSelectedResourcePacks(repo);
|
||||
check(!repo.getSelectedIds().contains(PERSONAL), "Explicit opt-out retained");
|
||||
});
|
||||
|
||||
Path collision = Files.createTempDirectory(packDirectory.getParent(), "template121-collision-");
|
||||
Files.writeString(collision.resolve(ResourcePackTemplate.FOLDER), "Do not replace this file");
|
||||
try { ResourcePackTemplate.create(collision); throw new AssertionError("Existing file must be refused"); }
|
||||
catch (IOException expectedError) { check(Files.readString(collision.resolve(ResourcePackTemplate.FOLDER)).equals("Do not replace this file"), "Collision leaves file intact"); }
|
||||
Path datapacks = Files.createTempDirectory(packDirectory.getParent(), "template121-datapacks-");
|
||||
c.runOnClient(m -> {
|
||||
screen(m, datapacks);
|
||||
check(m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.noneMatch(b -> key(b, "sanctuary.resource_pack.customize") || key(b, "sanctuary.resource_pack.open_copy")), "No button in datapack selector");
|
||||
m.gui.setScreen(previous);
|
||||
});
|
||||
System.out.println("TEMPLATE121_PASS: " + expected.size() + " exact files; native button FR/EN at scales 2/3; complete copy; edits and selection preserved; actual override, reload and opt-out; collision safe; datapacks untouched");
|
||||
}
|
||||
|
||||
private static void screen(Minecraft m, Path directory) {
|
||||
m.gui.setScreen(new PackSelectionScreen(m.getResourcePackRepository(), repo -> {}, directory, Component.translatable("resourcePack.title")));
|
||||
}
|
||||
private static Button button(Minecraft m, String suffix) {
|
||||
return m.gui.screen().children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> key(b, "sanctuary.resource_pack." + suffix)).findFirst().orElseThrow();
|
||||
}
|
||||
private static boolean key(Button b, String key) { return b.getMessage().getContents() instanceof TranslatableContents t && t.getKey().equals(key); }
|
||||
private static String source(Minecraft m) { return m.getResourceManager().getResource(Identifier.parse(WRENCH)).orElseThrow().sourcePackId(); }
|
||||
private static byte[] resource(Minecraft m, String name) {
|
||||
try (var in = m.getResourceManager().getResource(Identifier.parse(name)).orElseThrow().open()) { return in.readAllBytes(); }
|
||||
catch (IOException error) { throw new AssertionError(error); }
|
||||
}
|
||||
private static String hash(byte[] bytes) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
|
||||
catch (java.security.NoSuchAlgorithmException error) { throw new AssertionError(error); }
|
||||
}
|
||||
private static Map<String, String> snapshot(Path root) throws IOException { Map<String, String> result = new TreeMap<>(); merge(result, root, ""); return result; }
|
||||
private static void merge(Map<String, String> result, Path root, String prefix) throws IOException {
|
||||
try (var files = Files.walk(root)) { for (Path file : files.filter(Files::isRegularFile).toList()) result.put(prefix + root.relativize(file).toString().replace('\\', '/'), hash(Files.readAllBytes(file))); }
|
||||
}
|
||||
private static void check(boolean value, String message) { if (!value) throw new AssertionError(message); }
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.plans.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.phys.shapes.*;
|
||||
|
||||
/** Native picking/collision and light propagation, compared with the actual emitted sculpture mesh. */
|
||||
public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
||||
private static final String MODEL="sanctuary:model/"+"1".repeat(64);
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),SKY=new BlockPos(10,2,2),CONTROL=new BlockPos(18,2,2);
|
||||
private static final VoxelSculpture ARCH=arch();
|
||||
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
||||
}});
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture122 "+UUID.randomUUID());s.setSeed("122");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-14;x<=21;x++)for(int z=-6;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);p.setYRot(0);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));p.inventoryMenu.broadcastFullState();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(TARGET.below()).is(Blocks.STONE),150);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,.999,.5)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.below())&&hit.getDirection()==Direction.UP,"Cursor aims at placement support"));
|
||||
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(3);
|
||||
server.runOnServer(s->System.out.println("SCULPTURE122_PLACED "+player(s).level().getBlockState(TARGET)+" "+player(s).level().getBlockEntity(TARGET)));
|
||||
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),150);
|
||||
server.runOnServer(s->{physical(player(s).level(),TARGET);placementOverlap(player(s));});
|
||||
c.runOnClient(m->{physical(m.level,TARGET);geometry(m);Sculpture113ClientChecks.geometry(m,MODEL);});
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.0625,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects an occupied pillar"));
|
||||
c.takeScreenshot("sculpture122-voxel-outline");
|
||||
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET.north(),Blocks.REDSTONE_BLOCK.defaultBlockState()));
|
||||
c.waitFor(m->m.level.getBlockState(TARGET.north()).is(Blocks.REDSTONE_BLOCK),100);
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1.5,.5)));c.waitTicks(5);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.north()),"Native cursor passes through the sculpture hole to the block behind"));
|
||||
c.takeScreenshot("sculpture122-pick-through-hole");
|
||||
// A state edit and fresh entity payload must invalidate geometry on both sides.
|
||||
server.runOnServer(s->{var l=player(s).level();l.setBlockAndUpdate(TARGET,l.getBlockState(TARGET).setValue(Statuary.FACING,Direction.WEST).setValue(Statuary.ANCHOR,SculptureAnchor.EAST));});
|
||||
c.waitFor(m->m.level.getBlockState(TARGET).getValue(Statuary.FACING)==Direction.WEST,100);
|
||||
server.runOnServer(s->{var l=player(s).level();var bounds=l.getBlockState(TARGET).getShape(l,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Server shape rotates and moves to aimed edge");});
|
||||
c.runOnClient(m->{var bounds=m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Client shape follows changed blockstate");});
|
||||
var small=new VoxelSculpture(MODEL,"Small",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1)));
|
||||
server.runOnServer(s->place(player(s).level(),TARGET,small,Direction.WEST,SculptureAnchor.EAST));
|
||||
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&small.equals(e.sculpture()),100);
|
||||
c.runOnClient(m->check(m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds().getYsize()==1/16d,"Changed block entity payload invalidates cached shape"));
|
||||
server.runOnServer(s->{var l=player(s).level();place(l,TARGET,ARCH,Direction.SOUTH,SculptureAnchor.CENTER);l.setBlockAndUpdate(TARGET.north(),Blocks.AIR.defaultBlockState());
|
||||
for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)place(l,new BlockPos(x,y,z),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
for(int x=16;x<=20;x++)for(int z=0;z<=4;z++)l.setBlockAndUpdate(new BlockPos(x,3,z),Blocks.STONE.defaultBlockState());
|
||||
l.setBlockAndUpdate(new BlockPos(-8,2,0),Blocks.GLOWSTONE.defaultBlockState());place(l,new BlockPos(-7,2,0),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
||||
});
|
||||
server.waitFor(s->{var l=player(s).level();return l.getBrightness(LightLayer.SKY,SKY)==15&&l.getBrightness(LightLayer.SKY,CONTROL)<15&&l.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13;},200);
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15&&m.level.getBrightness(LightLayer.SKY,CONTROL)<15&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13,200);
|
||||
server.runOnServer(s->{var l=player(s).level();for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)check(l.getBrightness(LightLayer.SKY,new BlockPos(x,y,z))==15,"All 75 sculptures keep full native skylight");});
|
||||
c.runOnClient(m->{var e=(StatuaryEntity)m.level.getBlockEntity(new BlockPos(10,4,2));var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(e,state,0,Vec3.ZERO,null);check(net.minecraft.util.LightCoordsUtil.sky(state.lightCoords)==15,"Actual renderer receives full skylight inside a group");});
|
||||
// Visual crowd at ground level: no whole-block AO squares between the models.
|
||||
server.runOnServer(s->{var p=player(s);for(int x=-3;x<=3;x++)for(int z=-5;z<=-2;z++)place(p.level(),new BlockPos(x,1,z),ARCH,Direction.from2DDataValue(Math.floorMod(x+z,4)),SculptureAnchor.CENTER);p.teleportTo(.5,3,5.5);p.getAbilities().flying=true;p.onUpdateAbilities();});
|
||||
c.waitFor(m->m.player.distanceToSqr(.5,3,5.5)<.1,100);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1,-3)));c.waitTicks(12);c.takeScreenshot("sculpture122-crowd-light");
|
||||
// Transparent-to-light is not emissive: in a closed room sculpture faces stay dark.
|
||||
server.runOnServer(s->{var l=player(s).level();for(int x=-13;x<=-9;x++)for(int y=1;y<=5;y++)for(int z=5;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,y,z),x==-13||x==-9||y==1||y==5||z==5||z==9?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());place(l,new BlockPos(-11,2,7),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);});
|
||||
c.waitFor(m->m.level.getBlockEntity(new BlockPos(-11,2,7)) instanceof StatuaryEntity&&m.level.getBrightness(LightLayer.SKY,new BlockPos(-11,2,7))==0&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-11,2,7))==0,200);
|
||||
c.runOnClient(m->{var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState((StatuaryEntity)m.level.getBlockEntity(new BlockPos(-11,2,7)),state,0,Vec3.ZERO,null);check(state.lightCoords==0,"No fullbright workaround: a dark room produces dark faces");});
|
||||
System.out.println("SCULPTURE122_LIGHT_PASS: 75 stacked models sky=15; opaque control shadow; block light=13 across sculpture; dark room=0; shade brightness=1");
|
||||
}
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),1200);
|
||||
world.getServer().runOnServer(s->physical(player(s).level(),TARGET));c.runOnClient(m->physical(m.level,TARGET));
|
||||
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15,200);
|
||||
System.out.println("SCULPTURE122_PASS: native placement, voxel outline and picking through holes, authoritative collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
|
||||
}
|
||||
}
|
||||
private static void placementOverlap(ServerPlayer player){
|
||||
var pos=new BlockPos(3,1,0);var level=player.level();player.teleportTo(3.75,1,.5);player.setYRot(0);
|
||||
var filled=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)filled.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var dense=new VoxelSculpture(MODEL,"Solid",16,16,16,filled);var stack=StatuaryEntity.item(dense);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var hit=new BlockHitResult(new Vec3(3.01,1,.5),Direction.UP,pos.below(),false);
|
||||
var blocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(!blocked.consumesAction()&&level.getBlockState(pos).isAir()&&stack.getCount()==1,"Native placement refuses occupied voxels inside player without consuming sculpture");
|
||||
var postCells=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x=0;x<2;x++)for(int z=0;z<2;z++)postCells.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var post=new VoxelSculpture(MODEL,"Post",2,16,2,postCells);stack=StatuaryEntity.item(post);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
||||
var allowed=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
||||
check(allowed.consumesAction()&&level.getBlockState(pos).is(Statuary.BLOCK),"Native placement allows empty part of block beside player");
|
||||
check(level.noCollision(player,player.getBoundingBox()),"Accepted sculpture does not intersect player");
|
||||
level.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());player.teleportTo(.5,1,3.2);player.getInventory().setItem(player.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));player.inventoryMenu.broadcastFullState();
|
||||
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied voxels, allows empty space, preserves stack on rejection");
|
||||
}
|
||||
private static void physical(Level level,BlockPos pos){
|
||||
var state=level.getBlockState(pos);var shape=state.getShape(level,pos);var collision=state.getCollisionShape(level,pos);
|
||||
check(shape==collision,"Selection and native collision share cached shape");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
|
||||
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.MISS,"Outline ray passes through empty arch center");
|
||||
check(level.clip(new ClipContext(new Vec3(.0625,1.5,2),new Vec3(.0625,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Collision ray hits pillar");
|
||||
check(level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query leaves hole empty");
|
||||
check(!level.noCollision(new AABB(.02,1.2,.4,.1,1.7,.6)),"Native collision query blocks occupied pillar");
|
||||
check(!state.isCollisionShapeFullBlock(level,pos)&&!state.isSolidRender()&&!state.canOcclude(),"Sculpture is not a solid occluding cube");
|
||||
check(state.getOcclusionShape().isEmpty()&&state.propagatesSkylightDown()&&state.getLightDampening()==0&&state.getShadeBrightness(level,pos)==1,"Light metadata never reintroduces full-block occlusion");
|
||||
check(!state.isSuffocating(level,pos)&&state.getLightEmission()==0,"Decorative shape neither suffocates nor emits light");
|
||||
}
|
||||
private static void geometry(Minecraft m){
|
||||
var padded=new VoxelSculpture(MODEL,"Padded",13,5,15,List.of(new VoxelSculpture.Cell(2,2,3,-1),new VoxelSculpture.Cell(6,4,10,-1)));
|
||||
var dense=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)dense.add(new VoxelSculpture.Cell(x,y,z,-1));
|
||||
var models=List.of(ARCH,padded,new VoxelSculpture(MODEL,"Dense",16,16,16,dense),new VoxelSculpture(MODEL,"One voxel",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1))));
|
||||
int cases=0;for(var model:models)for(var direction:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor);var entity=new StatuaryEntity(BlockPos.ZERO,state);entity.setLevel(m.level);load(entity,model,m.level);
|
||||
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(volume==model.cells().size()/4096d,"Shape contains exactly the occupied volume");
|
||||
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
|
||||
for(var q:render.mesh){var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
|
||||
var normal=switch(q.side()){case 0->Direction.NORTH;case 1->Direction.SOUTH;case 2->Direction.WEST;case 3->Direction.EAST;case 4->Direction.UP;default->Direction.DOWN;};var neighbor=center.add(normal.getStepX()/16d,normal.getStepY()/16d,normal.getStepZ()/16d);check(boxes.stream().noneMatch(b->b.contains(neighbor)),"Rendered exposed face is also exposed physically");}
|
||||
cases++;
|
||||
}
|
||||
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native voxel shapes agree with emitted meshes, including holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
|
||||
}
|
||||
private static void place(ServerLevel level,BlockPos pos,VoxelSculpture model,Direction facing,SculptureAnchor anchor){
|
||||
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);level.setBlockAndUpdate(pos,state);var entity=(StatuaryEntity)level.getBlockEntity(pos);load(entity,model,level);entity.setChanged();level.sendBlockUpdated(pos,state,state,3);
|
||||
}
|
||||
private static void load(StatuaryEntity entity,VoxelSculpture model,Level level){var tag=new CompoundTag();tag.put("sculpture",StatuaryEntity.encode(model));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,level.registryAccess(),tag));}
|
||||
private static VoxelSculpture arch(){var cells=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<4;z++)if(x<2||x>=14||y>=14)cells.add(new VoxelSculpture.Cell(x,y,z,0xffc99679));return new VoxelSculpture(MODEL,"Arch",16,16,4,cells);}
|
||||
private static ServerPlayer player(MinecraftServer server){return server.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
||||
}
|
||||
+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);}
|
||||
}
|
||||
+122
-18
@@ -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;
|
||||
@@ -31,6 +32,7 @@ import net.minecraft.world.phys.*;
|
||||
|
||||
/** Actual GLB texture/scene import, paid native crafting, placement, drops, reconnect and plan coexistence. */
|
||||
public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
private static final Map<Direction,BlockPos> DIRECTIONS=Map.of(Direction.NORTH,new BlockPos(0,1,4),Direction.SOUTH,new BlockPos(4,1,4),Direction.EAST,new BlockPos(-4,1,4),Direction.WEST,new BlockPos(4,1,-2));
|
||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2),SCULPTURE=new BlockPos(2,1,0),TABLE=new BlockPos(-3,1,2);
|
||||
private static final AtomicReference<VoxelSculpture> ORIGINAL=new AtomicReference<>();
|
||||
private static final AtomicReference<String> MODEL=new AtomicReference<>();
|
||||
@@ -44,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");
|
||||
@@ -55,24 +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=-4;x<=6;x++)for(int z=-2;z<=6;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
||||
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");
|
||||
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");}});
|
||||
checkWorkshopLayout(c);
|
||||
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
|
||||
// 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 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);
|
||||
@@ -87,12 +115,48 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(SCULPTURE,true);var dropped=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(SCULPTURE).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||
check(StatuaryEntity.sculpture(dropped.getItem()).equals(source),"Native drop keeps name, ID and voxels");p.getInventory().setItem(p.getInventory().getSelectedSlot(),dropped.getItem().copy());dropped.discard();p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.level.getBlockState(SCULPTURE).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,SCULPTURE);server.waitFor(s->player(s).level().getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
|
||||
// Real use-key placements must follow the gaze on both sides of the network.
|
||||
for(var direction:Direction.Plane.HORIZONTAL){
|
||||
var pos=DIRECTIONS.get(direction);double px=pos.getX()+.5-direction.getStepX()*2,pz=pos.getZ()+.5-direction.getStepZ()*2;
|
||||
server.runOnServer(s->{var p=player(s);p.teleportTo(px,1,pz);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||
c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos);
|
||||
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");
|
||||
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.
|
||||
var north=DIRECTIONS.get(Direction.NORTH);var west=DIRECTIONS.get(Direction.WEST);
|
||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(north,true);p.level().removeBlock(west,false);
|
||||
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,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 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");
|
||||
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");});
|
||||
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);
|
||||
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.
|
||||
@@ -101,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");
|
||||
}
|
||||
@@ -110,17 +190,36 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||
try(var world=saved.open()){
|
||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var entity=(StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE);check(entity.sculpture().equals(ORIGINAL.get()),"Server save and reload preserve source-independent voxels");check(count(player(s),Statuary.ITEM)==2,"Colored inventory statues survive reconnect");});
|
||||
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"));
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -137,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->{
|
||||
|
||||
@@ -17,16 +17,22 @@ import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */
|
||||
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,242);inventoryLabelX=138;inventoryLabelY=130;}
|
||||
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(){
|
||||
@@ -34,12 +40,13 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
||||
try{files=CreationLibrary.files(CreationLibrary.Kind.MODEL);}catch(Exception e){status=ModelImportErrors.message(e);}
|
||||
page=Math.min(page,Math.max(0,(files.size()-1)/4));
|
||||
for(int i=0;i<4;i++){int at=page*4+i;if(at>=files.size())break;var file=files.get(at);
|
||||
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+29+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
||||
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+103,22,18).build()).active=page>0;
|
||||
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+103,78,18).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+103,22,18).build()).active=(page+1)*4<files.size();
|
||||
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+123,128,18).build());
|
||||
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+215,topPos+112,92,18).build());rotate.active=source!=null&&!busy;
|
||||
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+32+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
||||
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+106,22,18).build()).active=page>0;
|
||||
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+106,78,18).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+106,22,18).build()).active=(page+1)*4<files.size();
|
||||
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+127,128,18).build());
|
||||
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+148,topPos+127,152,18).build());rotate.active=source!=null&&!busy;
|
||||
rotate.setTooltip(Tooltip.create(text("placement")));
|
||||
}
|
||||
private void folder(){try{Files.createDirectories(CreationLibrary.directory());Blaze3D.openPath(CreationLibrary.directory());}catch(Exception e){status=ModelImportErrors.message(e);}}
|
||||
public void load(Path file){
|
||||
@@ -53,29 +60,47 @@ 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(){return imageHeight;}
|
||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float partial){
|
||||
g.fill(leftPos,topPos,leftPos+imageWidth,topPos+142,0xFFE2DDD3);g.fill(leftPos+2,topPos+2,leftPos+imageWidth-2,topPos+140,0xFFC6C6C6);
|
||||
g.fill(leftPos+147,topPos+24,leftPos+imageWidth-10,topPos+87,0xFF92958E);
|
||||
for(var slot:menu.slots)if(slot.x> -1000)g.blitSprite(RenderPipelines.GUI_TEXTURED,Identifier.withDefaultNamespace("container/slot"),leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
||||
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+195,topPos+28);g.pose().scale(3.5f,3.5f);g.item(preview,0,0);g.pose().popMatrix();
|
||||
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();
|
||||
}
|
||||
g.text(font,Component.literal("→"),leftPos+254,topPos+97,0xFF404040,false);
|
||||
var detail=font.split(status,128);for(int i=0;i<Math.min(4,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFFE7E7E7);
|
||||
g.text(font,Component.literal("→"),leftPos+254,topPos+111,0xFF404040,false);
|
||||
var detail=font.split(status,290);for(int i=0;i<Math.min(2,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFF404040,false);
|
||||
if(detail.size()>2&&mx>=leftPos+10&&mx<leftPos+300&&my>=topPos+148&&my<topPos+174)g.setTooltipForNextFrame(font,status,mx,my);
|
||||
if(files.isEmpty())g.text(font,text("empty"),leftPos+10,topPos+32,0xFF404040,false);
|
||||
}
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor g,int mx,int my){
|
||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("input"),203,80,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),80,0xFF303030,false);
|
||||
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);
|
||||
}
|
||||
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));});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.Blaze3D;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Comparator;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Util;
|
||||
|
||||
/** Explicit, one-time export. The player's copy is never a managed mod resource. */
|
||||
public final class ResourcePackTemplate {
|
||||
public static final String FOLDER = "Sanctuary-Custom";
|
||||
|
||||
private ResourcePackTemplate() {}
|
||||
|
||||
public static Button button(Path packDirectory) {
|
||||
Path target = packDirectory.resolve(FOLDER);
|
||||
return Button.builder(label(Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)
|
||||
? "open_copy" : "customize"), button -> {
|
||||
if (Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Blaze3D.openPath(target);
|
||||
return;
|
||||
}
|
||||
button.active = false;
|
||||
button.setMessage(label("creating"));
|
||||
Util.ioPool().execute(() -> {
|
||||
try {
|
||||
create(packDirectory);
|
||||
Minecraft.getInstance().execute(() -> {
|
||||
button.active = true;
|
||||
button.setMessage(label("open_copy"));
|
||||
button.setTooltip(Tooltip.create(label("copy_ready")));
|
||||
});
|
||||
} catch (IOException | RuntimeException error) {
|
||||
SanctuaryMod.LOGGER.error("Could not create personal Sanctuary resource pack", error);
|
||||
Minecraft.getInstance().execute(() -> {
|
||||
button.active = true;
|
||||
button.setMessage(label("copy_failed"));
|
||||
button.setTooltip(Tooltip.create(label("copy_failed_help")));
|
||||
});
|
||||
}
|
||||
});
|
||||
}).width(308).tooltip(Tooltip.create(label("customize_help"))).build();
|
||||
}
|
||||
|
||||
/** Returns an existing personal directory untouched, including after mod upgrades. */
|
||||
public static synchronized Path create(Path packDirectory) throws IOException {
|
||||
Path directory = packDirectory.toAbsolutePath().normalize();
|
||||
Files.createDirectories(directory);
|
||||
Path target = directory.resolve(FOLDER);
|
||||
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) return existing(target);
|
||||
var mod = FabricLoader.getInstance().getModContainer(SanctuaryMod.ID).orElseThrow();
|
||||
// Keep incomplete packs outside the directory watched by the native pack screen.
|
||||
Path staging = Files.createTempDirectory(directory.getParent(), ".sanctuary-template-");
|
||||
try {
|
||||
copyTree(mod.findPath("assets").orElseThrow(), staging.resolve("assets"));
|
||||
copyTree(mod.findPath("resourcepacks/textures/assets").orElseThrow(), staging.resolve("assets"));
|
||||
copyTree(mod.findPath("resourcepacks/template").orElseThrow(), staging);
|
||||
Files.copy(mod.findPath("resourcepacks/textures/pack.png").orElseThrow(), staging.resolve("pack.png"));
|
||||
try {
|
||||
Files.move(staging, target);
|
||||
} catch (FileAlreadyExistsException race) {
|
||||
return existing(target);
|
||||
}
|
||||
return target;
|
||||
} finally {
|
||||
if (Files.exists(staging)) {
|
||||
try (var files = Files.walk(staging)) {
|
||||
for (Path file : files.sorted(Comparator.reverseOrder()).toList()) Files.delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Path existing(Path target) throws IOException {
|
||||
if (!Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Personal pack path already exists and is not a directory: " + target);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static void copyTree(Path source, Path destination) throws IOException {
|
||||
try (var files = Files.walk(source)) {
|
||||
for (Path file : files.toList()) {
|
||||
if (Files.isSymbolicLink(file)) throw new IOException("Unexpected asset symlink: " + file);
|
||||
// String paths also work when the mod is loaded from a ZIP filesystem.
|
||||
Path output = destination.resolve(source.relativize(file).toString()).normalize();
|
||||
if (!output.startsWith(destination)) throw new IOException("Invalid asset path: " + file);
|
||||
if (Files.isDirectory(file)) Files.createDirectories(output);
|
||||
else Files.copy(file, output, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Component label(String key) {
|
||||
return Component.translatable("sanctuary.resource_pack." + key);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
FoodKnowledgeClient.register();
|
||||
MultiblockScreen.register();
|
||||
FourneauModels.register();
|
||||
WrenchTextureModels.register();
|
||||
MetatableClient.register();
|
||||
ClayWorkshopScreen.register();
|
||||
GraveScreen.register();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,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;
|
||||
@@ -22,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},
|
||||
@@ -37,19 +39,23 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
||||
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());
|
||||
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){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;
|
||||
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){
|
||||
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);
|
||||
}
|
||||
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;
|
||||
var placed=SculptureGeometry.place(sculpture,facing,anchor);sculpture=placed.sculpture();
|
||||
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
||||
float ox=(16-sculpture.width())/32f,oz=(16-sculpture.depth())/32f;
|
||||
float ox=placed.offsetX()/16f,oz=placed.offsetZ()/16f;
|
||||
for(var cell:sculpture.cells())for(int side=0;side<6;side++){
|
||||
var s=STEP[side];int x=cell.x()+s[0],y=cell.y()+s[1],z=cell.z()+s[2];
|
||||
if(x>=0&&x<16&&y>=0&&y<16&&z>=0&&z<16&&occupied.get(x|y<<4|z<<8))continue;
|
||||
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;
|
||||
@@ -63,7 +69,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
@@ -331,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 142;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import fr.koka.sanctuary.client.ResourcePackTemplate;
|
||||
import java.nio.file.Path;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.layouts.HeaderAndFooterLayout;
|
||||
import net.minecraft.client.gui.layouts.LayoutElement;
|
||||
import net.minecraft.client.gui.layouts.LinearLayout;
|
||||
import net.minecraft.client.gui.screens.packs.PackSelectionScreen;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Extra footer row only in resource packs, never in the world datapack selector. */
|
||||
@Mixin(PackSelectionScreen.class)
|
||||
public abstract class PackSelectionScreenMixin {
|
||||
@Shadow @Final private Path packDir;
|
||||
|
||||
@WrapOperation(method = "init", at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/client/gui/layouts/HeaderAndFooterLayout;addToFooter(Lnet/minecraft/client/gui/layouts/LayoutElement;)Lnet/minecraft/client/gui/layouts/LayoutElement;"))
|
||||
private LayoutElement sanctuary$personalPack(HeaderAndFooterLayout layout, LayoutElement nativeButtons,
|
||||
Operation<LayoutElement> original) {
|
||||
if (!packDir.toAbsolutePath().normalize().equals(
|
||||
Minecraft.getInstance().getResourcePackDirectory().toAbsolutePath().normalize())) {
|
||||
return original.call(layout, nativeButtons);
|
||||
}
|
||||
layout.setFooterHeight(56);
|
||||
LinearLayout rows = LinearLayout.vertical().spacing(4);
|
||||
rows.addChild(ResourcePackTemplate.button(packDir));
|
||||
rows.addChild(nativeButtons);
|
||||
original.call(layout, rows);
|
||||
// Vanilla still adds its Open folder / Done buttons to the original horizontal row.
|
||||
return nativeButtons;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package fr.koka.sanctuary.multiblock;
|
||||
|
||||
import java.util.*;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
/** Server-owned debug-stick gestures without changing a block's type or its inventory. */
|
||||
public final class GoldenWrench {
|
||||
public static final String TEXTURE="sanctuary_texture";
|
||||
private static final Map<ServerPlayer,Map<Block,String>> SELECTED=new WeakHashMap<>();
|
||||
private static final Map<ServerPlayer,Integer> LAST_SELECTION=new WeakHashMap<>();
|
||||
public static final class Tool extends Item {
|
||||
public Tool(Properties properties){super(properties);}
|
||||
@Override public boolean canDestroyBlock(ItemStack stack,BlockState state,Level level,BlockPos pos,LivingEntity entity){return false;}
|
||||
}
|
||||
public static void register(){
|
||||
WrenchTextures.register();
|
||||
AttackBlockCallback.EVENT.register((player,level,hand,pos,face)->{
|
||||
if(!player.getItemInHand(hand).is(Multiblocks.WRENCH))return InteractionResult.PASS;
|
||||
if(player.isSpectator())return InteractionResult.FAIL;
|
||||
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;select(p,pos);}
|
||||
return InteractionResult.SUCCESS;
|
||||
});
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler,server)->{SELECTED.remove(handler.player);LAST_SELECTION.remove(handler.player);});
|
||||
}
|
||||
public static boolean allowed(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||
return p.isAlive()&&!p.isSpectator()&&p.getAbilities().mayBuild&&p.level().hasChunkAt(pos)
|
||||
&&p.isWithinBlockInteractionRange(pos,0)&&p.level().mayInteract(p,pos)
|
||||
&&p.mayUseItemAt(pos,face,p.getItemInHand(hand))
|
||||
&&!p.blockActionRestricted(p.level(),pos,p.gameMode.getGameModeForPlayer())
|
||||
&&(!(p.level().getBlockEntity(pos) instanceof BaseContainerBlockEntity container)||container.canOpen(p));
|
||||
}
|
||||
private static boolean assembled(ServerPlayer p,BlockPos pos,BlockState state){
|
||||
return state.is(Metatable.BLOCK)||state.getBlock() instanceof SuperPistonBlocks.Base
|
||||
||state.getBlock() instanceof SuperPistonBlocks.Part||MultiblockData.get(p.level()).at(pos)!=null;
|
||||
}
|
||||
public static List<String> properties(BlockState state){
|
||||
var result=new ArrayList<>(state.getProperties().stream().map(Property::getName).sorted(Comparator.comparingInt(GoldenWrench::priority).thenComparing(s->s)).toList());
|
||||
result.add(TEXTURE);return List.copyOf(result);
|
||||
}
|
||||
private static int priority(String name){return switch(name){case "facing","axis","rotation","orientation"->0;case "half","type","shape"->1;default->2;};}
|
||||
public static String selected(ServerPlayer p,BlockState state){
|
||||
var choices=properties(state);var value=SELECTED.getOrDefault(p,Map.of()).get(state.getBlock());
|
||||
return value!=null&&choices.contains(value)?value:choices.getFirst();
|
||||
}
|
||||
private static void select(ServerPlayer p,BlockPos pos){
|
||||
var state=p.level().getBlockState(pos);if(state.isAir())return;
|
||||
if(assembled(p,pos,state)){p.sendSystemMessage(text("assembled"),true);return;}
|
||||
// Creative start/continue callbacks can describe the same click twice. Match its native 5-tick repeat cadence.
|
||||
int tick=p.level().getServer().getTickCount();var last=LAST_SELECTION.get(p);
|
||||
if(last!=null&&tick-last<5)return;LAST_SELECTION.put(p,tick);
|
||||
var choices=properties(state);var selected=SELECTED.computeIfAbsent(p,key->new HashMap<>());
|
||||
var before=selected.get(state.getBlock());int previous=before==null?-1:choices.indexOf(before);
|
||||
int next=previous<0?0:Math.floorMod(previous+(p.isShiftKeyDown()?-1:1),choices.size());
|
||||
var property=choices.get(next);selected.put(state.getBlock(),property);
|
||||
p.sendSystemMessage(text("selected",label(property),value(p,pos,state,property)),true);
|
||||
}
|
||||
public static void change(ServerPlayer p,BlockPos pos){
|
||||
var state=p.level().getBlockState(pos);if(state.isAir()||assembled(p,pos,state))return;
|
||||
var name=selected(p,state);SELECTED.computeIfAbsent(p,key->new HashMap<>()).put(state.getBlock(),name);
|
||||
if(name.equals(TEXTURE))WrenchTextures.turn(p.level(),pos,p.isShiftKeyDown()?-1:1);
|
||||
else {
|
||||
var property=state.getBlock().getStateDefinition().getProperty(name);
|
||||
// Same flags as the vanilla debug stick: synchronize, preserve deliberately chosen shapes.
|
||||
p.level().setBlock(pos,cycle(state,property,p.isShiftKeyDown()),Block.UPDATE_CLIENTS|Block.UPDATE_KNOWN_SHAPE);
|
||||
}
|
||||
p.sendSystemMessage(text("changed",label(name),value(p,pos,p.level().getBlockState(pos),name)),true);
|
||||
}
|
||||
public static <T extends Comparable<T>> BlockState cycle(BlockState state,Property<T> property,boolean reverse){
|
||||
var values=new ArrayList<>(property.getPossibleValues());int index=values.indexOf(state.getValue(property));
|
||||
return state.setValue(property,values.get(Math.floorMod(index+(reverse?-1:1),values.size())));
|
||||
}
|
||||
private static Object value(ServerPlayer p,BlockPos pos,BlockState state,String name){
|
||||
return name.equals(TEXTURE)?(WrenchTextures.rotation(p.level().getChunkAt(pos),pos)*90)+"°":propertyValue(state,state.getBlock().getStateDefinition().getProperty(name));
|
||||
}
|
||||
private static <T extends Comparable<T>> String propertyValue(BlockState state,Property<T> property){return property.getName(state.getValue(property));}
|
||||
private static Component label(String name){return name.equals(TEXTURE)?text("texture"):Component.literal(name);}
|
||||
private static Component text(String name,Object...args){return Component.translatable("sanctuary.wrench."+name,args);}
|
||||
private GoldenWrench(){}
|
||||
}
|
||||
@@ -80,10 +80,11 @@ public final class Metatable {
|
||||
if(p.isShiftKeyDown()){dissolve(level,origin);p.sendSystemMessage(Multiblocks.text("separated"),true);}else open(p,origin);
|
||||
return true;
|
||||
}
|
||||
if(!s.is(Blocks.CRAFTING_TABLE))return false;
|
||||
if(!s.is(Blocks.CRAFTING_TABLE)||p.isShiftKeyDown())return false;
|
||||
var candidates=new ArrayList<BlockPos>();
|
||||
for(int z=0;z<3;z++)for(int x=0;x<3;x++){var o=pos.offset(-x,0,-z);if(state(level,o,false)==1)candidates.add(o);}
|
||||
if(candidates.size()!=1){previews.remove(p);p.sendSystemMessage(Multiblocks.text(candidates.isEmpty()?"incomplete":"ambiguous"),true);return true;}
|
||||
if(candidates.isEmpty()){previews.remove(p);return false;}
|
||||
if(candidates.size()!=1){previews.remove(p);p.sendSystemMessage(Multiblocks.text("ambiguous"),true);return true;}
|
||||
var origin=candidates.getFirst();var before=previews.remove(p);
|
||||
if(before!=null&&before.level==level&&before.origin.equals(origin)&&before.until>=level.getGameTime()){
|
||||
p.sendSystemMessage(Multiblocks.text(assemble(p,origin)?"assembled":"unavailable"),true);return true;
|
||||
|
||||
@@ -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,92){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
||||
addSlot(new Slot(result,0,282,92){
|
||||
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,138,142);
|
||||
// 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,35 @@
|
||||
package fr.koka.sanctuary.plans;
|
||||
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape;
|
||||
import net.minecraft.world.phys.shapes.CubeVoxelShape;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** One integer placement shared by rendering, picking and server-side collisions. */
|
||||
public final class SculptureGeometry {
|
||||
public record Placement(VoxelSculpture sculpture, int offsetX, int offsetZ) {}
|
||||
|
||||
private SculptureGeometry() {}
|
||||
|
||||
public static Placement place(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
|
||||
int turns = switch (facing) { case WEST -> 1; case NORTH -> 2; case EAST -> 3; default -> 0; };
|
||||
for (int i = 0; i < turns; i++) sculpture = sculpture.rotate();
|
||||
int minX = 16, minZ = 16, maxX = 0, maxZ = 0;
|
||||
for (var cell : sculpture.cells()) {
|
||||
minX = Math.min(minX, cell.x()); minZ = Math.min(minZ, cell.z());
|
||||
maxX = Math.max(maxX, cell.x() + 1); maxZ = Math.max(maxZ, cell.z() + 1);
|
||||
}
|
||||
return new Placement(sculpture, anchor.originX(maxX - minX) - minX,
|
||||
anchor.originZ(maxZ - minZ) - minZ);
|
||||
}
|
||||
|
||||
public static VoxelShape shape(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) {
|
||||
var placed = place(sculpture, facing, anchor);
|
||||
var grid = new BitSetDiscreteVoxelShape(16, 16, 16);
|
||||
for (var cell : placed.sculpture().cells()) {
|
||||
grid.fill(cell.x() + placed.offsetX(), cell.y(), cell.z() + placed.offsetZ());
|
||||
}
|
||||
// Build the bounded native voxel grid directly, without thousands of box unions.
|
||||
return new CubeVoxelShape(grid);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -13,18 +13,23 @@ import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.*;
|
||||
import net.minecraft.world.level.block.state.*;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.level.material.PushReaction;
|
||||
import net.minecraft.world.level.storage.loot.LootParams;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import net.minecraft.world.phys.*;
|
||||
import net.minecraft.world.phys.shapes.*;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
|
||||
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());
|
||||
@@ -35,7 +40,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){
|
||||
@@ -53,10 +58,35 @@ public final class Statuary {
|
||||
});
|
||||
}
|
||||
private static Block block(String name,Block block){return Registry.register(BuiltInRegistries.BLOCK,SanctuaryMod.id(name),block);}
|
||||
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
|
||||
private static Item blockItem(String name,Block block){
|
||||
var properties=new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix();
|
||||
return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),block==BLOCK?new SculptureItem(block,properties):new BlockItem(block,properties));
|
||||
}
|
||||
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
|
||||
private static final class SculptureItem extends BlockItem {
|
||||
SculptureItem(Block block,Item.Properties properties){super(block,properties);}
|
||||
@Override protected boolean canPlace(BlockPlaceContext context,BlockState state){
|
||||
if(!super.canPlace(context,state))return false;
|
||||
// The destination has no block entity yet: use the carried model for the native entity-overlap check.
|
||||
var model=StatuaryEntity.sculpture(context.getItemInHand());
|
||||
return model==null||context.getLevel().isUnobstructed(null,SculptureGeometry.shape(model,state.getValue(FACING),state.getValue(ANCHOR)).move(context.getClickedPos()));
|
||||
}
|
||||
}
|
||||
private static final class SculptureBlock extends Block implements EntityBlock {
|
||||
SculptureBlock(){super(Statuary.properties("statuary"));}
|
||||
SculptureBlock(){super(Statuary.properties("statuary").dynamicShape().isSuffocating((s,l,p)->false).isViewBlocking((s,l,p,box)->false));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
|
||||
@Override protected VoxelShape getShape(BlockState state,BlockGetter level,BlockPos pos,CollisionContext context){
|
||||
return level.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.shape(state):Shapes.empty();
|
||||
}
|
||||
// Lighting metadata is cached per blockstate, so it cannot describe each block entity's voxels.
|
||||
// Decorative sculptures transmit light; their faces still receive ordinary world lighting.
|
||||
@Override protected VoxelShape getOcclusionShape(BlockState state){return Shapes.empty();}
|
||||
@Override protected boolean propagatesSkylightDown(BlockState state){return true;}
|
||||
@Override protected int getLightDampening(BlockState state){return 0;}
|
||||
@Override protected float getShadeBrightness(BlockState state,BlockGetter level,BlockPos pos){return 1;}
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING,ANCHOR);}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection()).setValue(ANCHOR,SculptureAnchor.placement(context));}
|
||||
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING))).setValue(ANCHOR,state.getValue(ANCHOR).rotate(rotation));}
|
||||
@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);}
|
||||
|
||||
@@ -9,13 +9,26 @@ import net.minecraft.world.item.component.TypedEntityData;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** New, additive block data only. No ticking or mutation of pre-existing world formats. */
|
||||
public final class StatuaryEntity extends BlockEntity {
|
||||
private VoxelSculpture sculpture;
|
||||
private CompoundTag opaque;
|
||||
private record ShapeCache(VoxelSculpture sculpture,BlockState state,VoxelShape shape){}
|
||||
private volatile ShapeCache shapeCache;
|
||||
// Keep unknown/future data recoverable by picking the small base, without a phantom full cube.
|
||||
private static final VoxelShape MISSING_SHAPE=Block.box(6,0,6,10,4,10);
|
||||
public StatuaryEntity(BlockPos pos,BlockState state){super(Statuary.TYPE,pos,state);}
|
||||
public VoxelSculpture sculpture(){return sculpture;}
|
||||
public VoxelShape shape(BlockState state){
|
||||
var model=sculpture;if(model==null)return MISSING_SHAPE;
|
||||
var cached=shapeCache;
|
||||
if(cached!=null&&cached.sculpture()==model&&cached.state()==state)return cached.shape();
|
||||
var shape=SculptureGeometry.shape(model,state.getValue(Statuary.FACING),state.getValue(Statuary.ANCHOR));
|
||||
shapeCache=new ShapeCache(model,state,shape);return shape;
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput out){
|
||||
super.saveAdditional(out);
|
||||
if(opaque!=null){out.store("sculpture",CompoundTag.CODEC,opaque);return;}
|
||||
@@ -23,7 +36,7 @@ public final class StatuaryEntity extends BlockEntity {
|
||||
out.store("sculpture",CompoundTag.CODEC,encode(sculpture));
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput in){
|
||||
super.loadAdditional(in);sculpture=null;opaque=null;
|
||||
super.loadAdditional(in);sculpture=null;opaque=null;shapeCache=null;
|
||||
var data=in.read("sculpture",CompoundTag.CODEC).orElse(null);if(data==null)return;
|
||||
try{
|
||||
if(data.getIntOr("schema",-1)!=1)throw new IllegalArgumentException("schema");
|
||||
@@ -49,7 +62,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"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user