Compare commits

..
5 Commits
Author SHA1 Message Date
koka 1eaa976ecf Use only the active hotbar in the clay workshop for beta.112
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 03:59:13 +02:00
koka 8c0020c774 Document verified beta.111 publication and Prism update
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 03:45:23 +02:00
koka 289697becf Refine clay workshop and orient statues on placement for beta.111
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 03:43:36 +02:00
koka e38f6cbdbe Record beta.110 publication and verified Prism synchronization
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 03:15:20 +02:00
koka 2090380712 Allow the indexed pack icon in packwiz channel publication
Build Sanctuary / build (push) Canceled after 0s
2026-09-17 03:12:24 +02:00
22 changed files with 418 additions and 50 deletions
+27 -7
View File
@@ -20,9 +20,11 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter.
La version **beta.110** réunit les développements jusqu’à latelier dargile,
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 linstance.
La version **beta.112** limite latelier dargile à la hotbar active du joueur,
dans un cadre compact. Les statuaires suivent toujours le regard à la pose.
Le [ticket](docs/clay-workshop-hotbar-beta112.md) décrit le comportement et les essais.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
Les [archives pour le site](archives/web/README.md) conservent la maquette
@@ -31,13 +33,31 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres.
## beta.112 — Atelier dargile 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 dargile.
[Contrat et vérifications](docs/clay-workshop-hotbar-beta112.md) ·
[Pack normal](build/Sanctuary-beta.112.mrpack) ·
[Pack de test](build/Sanctuary-Test-beta.112.mrpack).
## beta.111 — Atelier dargile et pose orientée
Cadre nine-slice, aperçu, fabrication et inventaire dans un menu commun.
Les statuaires sorientent 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 dargile et neige saisonnière réunis
Le Clay Workshop / atelier dargile, les statuaires et les modèles 3D rejoignent
les œufs, laccumulation 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 +2071,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.112 |
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
uniquement la bibliothèque standard et repère aussi une installation Python
@@ -2080,7 +2100,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.112.jar` : mod à installer avec
Fabric API sur la version Minecraft indiquée.
- `build/packwiz/` : pack de développement complet, avec le mod construit et
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
+14 -1
View File
@@ -1,5 +1,17 @@
# Backlog Sanctuary
## STAT-03 — Atelier dargile 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 dargile 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 dargile 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
latelier, des œufs et de la neige, build et archives réussis. Publication
et synchronisation de linstance demandées ensuite, en cours.
et synchronisation de linstance Sanctuary Beta terminées ; 923 fichiers
personnels et réglages suivis conservés, aucun monde ouvert.
-1
View File
@@ -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.
+66
View File
@@ -0,0 +1,66 @@
# STAT-03 — Atelier dargile 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 louverture 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 dinterface.
Il ne grandit plus avec la capacité de linventaire. Le Maj-clic du résultat
cherche uniquement une place dans la hotbar ; sans place, il ne consomme pas
dargile. Les touches 19 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 nest 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 dinventaire évolutif.
- Fabrication par Maj-clic depuis la hotbar, avec de largile aussi présente
dans la réserve, lextension 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 dun seul objet,
coût dune argile, aucune statue transférée dans une case cachée.
- Restitution de largile restante à la fermeture, seize teintes, quatre
orientations, butin/repose et sauvegarde/reconnexion sans fichier GLB.
- Régression du Métabli et annulation dimport 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 na é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 mise à jour
de linstance stable préparées après ces contrôles.
+32 -1
View File
@@ -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.
+99
View File
@@ -0,0 +1,99 @@
# STAT-02 — Atelier dargile 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 dorigine. Catalogue, aperçu,
fabrication, explications et inventaire évolutif tiennent dans ce cadre.
La texture reste remplaçable par un pack de ressources.
La pose dun statuaire suit le regard horizontal du joueur, dans les quatre
directions. Le bouton Tourner règle toujours lorientation du modèle importé.
Le placement ajoute ensuite lorientation 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 nest 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. Lidentifiant
du sprite est `sanctuary:container/clay_workshop/frame` ; aucun sprite global
Minecraft nest 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 dinventaire à 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 dun ancien état sans `facing`.
- Butin dune statue orientée au nord puis repose vers louest : nouvelle
orientation, mêmes voxels. Sauvegarde/réouverture et transmission sans GLB.
- Conversion au Métabli et annulation dimport 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 nest 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 linstance 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 na é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`.
-1
View File
@@ -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é.
+8 -3
View File
@@ -8,7 +8,8 @@ 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 licô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.
@@ -17,8 +18,12 @@ de l'ancien pack 26.2.
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
linstance 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
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.110
pack_version=beta.110
mod_version=beta.112
pack_version=beta.112
resource_pack_version=beta.090
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
@@ -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<>();
@@ -61,16 +63,37 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
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);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");
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();});
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.takeScreenshot("statuary106-workshop-en");
checkWorkshopLayout(c);
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
// Hidden inventory, overflow and offhand must never join this menu or feed its input.
c.runOnClient(m->check(!((SanctuaryContainerScreenGeometry)(Object)m.gui.screen()).sanctuary$hasIntegratedInventory(),"No expanding player-inventory panel"));
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;
check(menu.slots.size()==11&&menu.getSlot(0).getItem().getCount()==3,"Two crafting slots plus exactly nine hotbar slots");
check(!((fr.koka.sanctuary.inventory.SanctuaryExtendedInventoryMenu)(Object)menu).sanctuary$hasExtendedInventory(),"No hidden overflow destination");
menu.clicked(0,Inventory.SLOT_OFFHAND,ContainerInput.SWAP,p);menu.clicked(11,0,ContainerInput.PICKUP,p);menu.clicked(-2,0,ContainerInput.PICKUP,p);
check(menu.quickMoveStack(p,11).isEmpty()&&menu.getCarried().isEmpty(),"Out-of-menu slot rejected");
check(menu.getSlot(0).getItem().getCount()==3&&p.getOffhandItem().getCount()==12,"Offhand swap refused");
check(p.getInventory().getItem(18).getCount()==64&&fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).getItem(17).getCount()==64,"Reserve clay untouched");
p.getInventory().setItem(18,ItemStack.EMPTY);p.getInventory().setItem(Inventory.SLOT_OFFHAND,ItemStack.EMPTY);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);
});
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay keeps exact colors and model ID");
for(var family:ColoredBricks.families()){var colored=ClayWorkshopMenu.material(source,new ItemStack(family.clay()));check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"All 16 clays keep geometry and ID, recolor uniformly");}});
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
@@ -87,11 +110,39 @@ 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);
// 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,"Native placement faces gaze: "+direction);
check(state.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_90).getValue(Statuary.FACING)==direction.getClockWise(),"Structure rotation follows native directions");
check(state.mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT).getValue(Statuary.FACING)==(direction.getAxis()==Direction.Axis.Z?direction.getOpposite():direction),"Structure mirror follows native directions");
check(((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(source),"Placement never rewrites model voxels");});
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
c.runOnClient(m->{var entity=(StatuaryEntity)m.level.getBlockEntity(pos);check(entity.getBlockState().getValue(Statuary.FACING)==direction,"Client receives direction");
var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(entity,state,0,Vec3.ZERO,null);
var forward=new org.joml.Vector3f(0,0,1).rotate(com.mojang.math.Axis.YP.rotationDegrees(state.yaw));
check(Math.abs(forward.x()-direction.getStepX())<.001&&Math.abs(forward.z()-direction.getStepZ())<.001,"Rendered model faces gaze: "+direction);});
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);
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100);
server.runOnServer(s->check(player(s).level().getBlockState(west).getValue(Statuary.FACING)==Direction.WEST,"Dropped statue takes new gaze"));
server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
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");
checkWorkshopLayout(c);
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.modelId().equals(MODEL.get())&&blue.cells().stream().allMatch(v->v.color()==0xFFA0A7BC),"Blue input overrides uploaded model colors");});
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);
@@ -101,8 +152,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,13 +177,27 @@ 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");});
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"));
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&ORIGINAL.get().equals(e.sculpture())&&e.getBlockState().getValue(Statuary.FACING)==direction,150);
}
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file"));
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("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){
@@ -17,11 +17,14 @@ 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 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();
@@ -34,12 +37,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){
@@ -62,19 +66,25 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
}
@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);
}
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;
@@ -4,6 +4,7 @@ import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.plans.*;
import java.util.*;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.mojang.serialization.MapCodec;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.blockentity.*;
@@ -34,12 +35,16 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
{0,0,1, 0,0,0, 1,0,0, 1,0,1, 0,-1,0}
};
private static final int[][] STEP={{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}};
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();}
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();float yaw;}
public State createRenderState(){return new State();}
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);state.mesh=mesh(entity.sculpture());
state.yaw=-entity.getBlockState().getValue(Statuary.FACING).toYRot();
}
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){
pose.pushPose();pose.translate(.5,0,.5);pose.rotateDegrees(Axis.YP,state.yaw);pose.translate(-.5,0,-.5);
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);pose.popPose();
}
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;
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
@@ -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;
@@ -18,6 +18,7 @@ import net.minecraft.world.phys.*;
/** Native input/result slots. Server recomputes the color from the actual clay 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()));
}
@@ -45,13 +46,14 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
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 clay(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 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};
@@ -81,13 +83,17 @@ 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;}
if(index<HOTBAR_START){if(!moveItemStackTo(stack,HOTBAR_START,HOTBAR_END,true))return ItemStack.EMPTY;}
else if(clay(stack)){if(!moveItemStackTo(stack,0,1,false))return ItemStack.EMPTY;}
else return ItemStack.EMPTY;
if(stack.isEmpty())slot.set(ItemStack.EMPTY);else slot.setChanged();
@@ -13,10 +13,12 @@ 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;
@@ -25,6 +27,7 @@ import net.minecraft.network.chat.Component;
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
public final class Statuary {
public static final EnumProperty<Direction> FACING=BlockStateProperties.HORIZONTAL_FACING;
public static final Block BLOCK=block("statuary",new SculptureBlock());
public static final Item ITEM=blockItem("statuary",BLOCK);
public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock());
@@ -56,7 +59,11 @@ public final class Statuary {
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
private static final class SculptureBlock extends Block implements EntityBlock {
SculptureBlock(){super(Statuary.properties("statuary"));}
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH));}
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING);}
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection());}
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING)));}
@Override protected BlockState mirror(BlockState state,Mirror mirror){return state.rotate(mirror.getRotation(state.getValue(FACING)));}
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);}
@@ -2157,9 +2157,11 @@
"sanctuary.plans.imported_model": "3D model → block statue",
"sanctuary.plans.model_files_help": "GLB models in schematics/sanctuary/, alongside your schematics. Create a full-block statue here. Craft 16³ miniatures at the clay workshop.",
"sanctuary.plans.empty_models": "Place a .glb file in the shared folder, then refresh.",
"sanctuary.statuary.choose_model": "Choose a GLB model. 1 clay block = 1 statue. Plain clay keeps model colors; colored clay gives a solid color.",
"sanctuary.statuary.choose_model": "Choose a GLB model, then add clay from your hotbar.",
"sanctuary.statuary.folder": "Shared folder",
"sanctuary.statuary.refresh": "Refresh",
"sanctuary.statuary.models": "GLB models",
"sanctuary.statuary.placement": "The statue is placed facing the direction you look. Rotate adjusts the model before crafting.",
"sanctuary.statuary.rotate": "Rotate",
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
"sanctuary.statuary.ready": "1 clay = 1 statue. Plain clay keeps model colors. Colored clay gives a solid color.",
@@ -2157,9 +2157,11 @@
"sanctuary.plans.imported_model": "Modèle 3D → statue en blocs",
"sanctuary.plans.model_files_help": "Modèles GLB dans schematics/sanctuary/, avec vos schémas. Ici, créez une grande statue en blocs. Les miniatures 16³ se fabriquent à latelier dargile.",
"sanctuary.plans.empty_models": "Déposez un fichier .glb dans le dossier commun, puis actualisez.",
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB. 1 bloc dargile = 1 statue. Argile normale : couleurs du modèle ; argile colorée : teinte unie.",
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB, puis déposez de largile depuis votre barre rapide.",
"sanctuary.statuary.folder": "Dossier commun",
"sanctuary.statuary.refresh": "Actualiser",
"sanctuary.statuary.models": "Modèles GLB",
"sanctuary.statuary.placement": "La statue se pose dans le sens de votre regard. Tourner ajuste le modèle avant fabrication.",
"sanctuary.statuary.rotate": "Tourner",
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
"sanctuary.statuary.ready": "1 argile = 1 statue. Normale : couleurs du modèle. Colorée : teinte unie.",
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

@@ -0,0 +1,11 @@
{
"gui": {
"scaling": {
"type": "nine_slice",
"width": 26,
"height": 26,
"border": 4,
"stretch_inner": true
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.110"
version = "beta.112"
pack-format = "packwiz:1.1.0"
[index]
+16 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Publish a verified build to Gitea and advance the metadata-only packwiz branch.
"""Publish a verified build to Gitea and advance the packwiz manifests and indexed icon.
This command publishes externally. Run check/build/assemblePack, commit, and push
the source branch first. Git's configured credential helper supplies credentials
@@ -115,12 +115,21 @@ def remote_tag_commit(tag):
return found.get('refs/tags/' + tag + '^{}') or found.get('refs/tags/' + tag)
def channel_file(relative):
"""The indexed pack icon is the only non-TOML file allowed on the channel."""
path = Path(relative)
return path.suffix == '.toml' or path.as_posix() == 'icon.png'
def publish_channel(origin, version, source_head):
staged = ROOT / 'build/packwiz-release'
pack.check_index(staged)
files = [path for path in staged.rglob('*') if path.is_file()]
pack.require(all(path.suffix == '.toml' for path in files),
'Distribution branch must contain only generated TOML metadata')
pack.require(all(channel_file(path.relative_to(staged)) for path in files),
'Distribution branch must contain only generated TOML metadata and the pack icon')
if (staged / 'icon.png').exists():
pack.require((staged / 'icon.png').read_bytes() == (ROOT / 'packwiz/icon.png').read_bytes(),
'Distribution icon differs from the verified source icon')
remote = git('ls-remote', '--heads', 'origin', 'refs/heads/' + CHANNEL)
with tempfile.TemporaryDirectory(prefix='sanctuary-pack-channel-') as temp:
directory = Path(temp)
@@ -134,7 +143,7 @@ def publish_channel(origin, version, source_head):
git('fetch', '--quiet', 'origin', CHANNEL, cwd=directory)
git('checkout', '--quiet', '-b', CHANNEL, 'FETCH_HEAD', cwd=directory)
tracked = git('ls-files', cwd=directory).splitlines()
pack.require(all(name.endswith('.toml') for name in tracked),
pack.require(all(channel_file(name) for name in tracked),
'Existing distribution branch contains unexpected files')
for name in tracked:
(directory / name).unlink()
@@ -204,7 +213,9 @@ def main():
remote_manifest = response.read()
pack.require(remote_manifest == (ROOT / 'build/packwiz-release/pack.toml').read_bytes(),
'Published manifest differs from this build; inspect the distribution channel')
for path in (ROOT / 'build/packwiz-release').rglob('*.toml'):
for path in (ROOT / 'build/packwiz-release').rglob('*'):
if not path.is_file():
continue
relative = path.relative_to(ROOT / 'build/packwiz-release').as_posix()
with urlopen(stable_url.removesuffix('pack.toml') + quote(relative), timeout=60) as response:
published = response.read()