Snap clay sculptures to aimed block edges on the voxel grid (beta.113)
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-17 06:38:04 +02:00
parent 064701f210
commit 6166d0aa9e
13 changed files with 267 additions and 44 deletions
+10 -4
View File
@@ -20,10 +20,10 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter. à implémenter.
La version **beta.112** limite latelier dargile à la hotbar active du joueur, La version **beta.113** nomme les miniatures « sculptures dargile » et les
dans un cadre compact. Les statuaires suivent toujours le regard à la pose. cale contre le bord visé du bloc, sur la grille exacte de 1/16 de bloc.
Le [ticket](docs/clay-workshop-hotbar-beta112.md) décrit les essais, la publication Le [ticket](docs/clay-sculpture-grid-beta113.md) décrit laccroche, sa sauvegarde
et la mise à jour de linstance **Sanctuary Beta**. et les vérifications. Latelier conserve son menu limité à la hotbar.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà 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**. 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 [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -34,6 +34,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres. l’étude de Xaero et des alternatives libres.
## beta.113 — Sculptures dargile 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. Laccroche est
conservée à la reconnexion. [Contrat et vérifications](docs/clay-sculpture-grid-beta113.md).
## beta.112 — Atelier dargile et hotbar ## beta.112 — Atelier dargile et hotbar
Neuf cases de hotbar et deux emplacements de fabrication, dans un cadre fixe. Neuf cases de hotbar et deux emplacements de fabrication, dans un cadre fixe.
+78
View File
@@ -0,0 +1,78 @@
# STAT-04 — Sculptures dargile et accroche au bord — beta.113
Contrat du 17 septembre 2026, branche `codex/clay-sculpture-grid-beta113`.
Les miniatures de latelier deviennent des « sculptures dargile » / « 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. Lautre 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, laxe Z départage ; au centre exact,
le bord côté joueur est retenu. Lorientation 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 ny a aucun parcours ni réécriture de chunks existants. La sauvegarde native
conserve laccroche ; 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 dune 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 lobjet 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 daccroche et dorientation ; 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
na é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 synchronisation de linstance à effectuer après le commit source.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3 fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md). # Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.112 mod_version=beta.113
pack_version=beta.112 pack_version=beta.113
resource_pack_version=beta.090 resource_pack_version=beta.090
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3 jei_version=30.32.0-sanctuary.3
@@ -0,0 +1,90 @@
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 edge:Direction.Plane.HORIZONTAL){
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);}
}
@@ -66,10 +66,12 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
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()); 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"); 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.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"); 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");}); 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.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))); 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); 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"); server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");
@@ -79,6 +81,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
c.waitFor(m->m.player.containerMenu.getSlot(ClayWorkshopMenu.HOTBAR_START).getItem().getCount()==3,100); 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.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.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("Clay Sculpture"),"Crafted sculpture has translated type name EN"));
c.takeScreenshot("statuary106-workshop-en"); c.takeScreenshot("statuary106-workshop-en");
checkWorkshopLayout(c); checkWorkshopLayout(c);
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3"); c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
@@ -114,17 +117,17 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
for(var direction:Direction.Plane.HORIZONTAL){ 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; 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();}); 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); c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos,direction.getOpposite());
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100); 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); 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.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(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");}); 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.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"); 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); check(entity.getBlockState().getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Client receives aimed edge");
var forward=new org.joml.Vector3f(0,0,1).rotate(com.mojang.math.Axis.YP.rotationDegrees(state.yaw)); Sculpture113ClientChecks.rendered(entity);});
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()); c.takeScreenshot("statuary111-facing-"+direction.getSerializedName());
} }
// Re-place a dropped north-facing model while looking west: no baked or cumulative turn. // Re-place a dropped north-facing model while looking west: no baked or cumulative turn.
@@ -133,7 +136,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
var drop=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(north).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow(); 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"); 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();}); p.getInventory().setItem(p.getInventory().getSelectedSlot(),drop.getItem().copy());drop.discard();p.teleportTo(west.getX()+2.5,1,west.getZ()+.5);p.inventoryMenu.broadcastFullState();});
c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west); c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west,Direction.EAST);
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100); server.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->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); server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
@@ -141,7 +144,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c); 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); 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();}); server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.takeScreenshot("statuary106-workshop-fr-blue"); c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.runOnClient(m->check(m.player.containerMenu.getSlot(1).getItem().getHoverName().getString().equals("Sculpture dargile"),"Crafted sculpture has translated type name FR"));c.takeScreenshot("statuary106-workshop-fr-blue");
checkWorkshopLayout(c); 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");}); 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); c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
@@ -180,6 +183,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
for(var direction:List.of(Direction.SOUTH,Direction.EAST,Direction.WEST)){ for(var direction:List.of(Direction.SOUTH,Direction.EAST,Direction.WEST)){
var pos=DIRECTIONS.get(direction); 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.FACING)==direction&&((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(ORIGINAL.get()),"Direction and model survive server save/reload"));
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.ANCHOR)==SculptureAnchor.of(direction.getOpposite()),"Anchor survives server save/reload"));
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&ORIGINAL.get().equals(e.sculpture())&&e.getBlockState().getValue(Statuary.FACING)==direction,150); c.waitFor(m->m.level.getBlockEntity(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.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"));
@@ -187,6 +191,7 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
} }
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("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("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("STATUARY112_PASS fixed compact hotbar-only menu, native input transfer, hidden slots/offhand rejected, full hotbar no consumption, partial merge exact cost, closing restitution"); 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){}} }catch(IOException e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(IOException ignored){}}
} }
@@ -200,8 +205,9 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
});} });}
private static void close(ClientGameTestContext c){c.runOnClient(m->m.gui.screen().onClose());c.waitFor(m->m.gui.screen()==null,100);} 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 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){ private static void place(ClientGameTestContext c,BlockPos pos){place(c,pos,null);}
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,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.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); c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);
} }
@@ -29,7 +29,9 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
public static void register(){ public static void register(){
net.minecraft.client.gui.screens.MenuScreens.register(Statuary.MENU,ClayWorkshopScreen::new);StatuaryRenderer.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)->{ 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(){ @Override protected void init(){
@@ -4,7 +4,6 @@ import fr.koka.sanctuary.SanctuaryMod;
import fr.koka.sanctuary.plans.*; import fr.koka.sanctuary.plans.*;
import java.util.*; import java.util.*;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.minecraft.client.renderer.*; import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.blockentity.*; import net.minecraft.client.renderer.blockentity.*;
@@ -15,6 +14,7 @@ import net.minecraft.client.renderer.special.SpecialModelRenderer;
import net.minecraft.client.renderer.state.level.CameraRenderState; import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.TypedEntityData; import net.minecraft.world.item.component.TypedEntityData;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
@@ -23,8 +23,9 @@ import org.joml.Vector3fc;
/** Shared block/item render geometry: opaque visible faces, generated once per cached sculpture. */ /** Shared block/item render geometry: opaque visible faces, generated once per cached sculpture. */
public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntity,StatuaryRenderer.State> { public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntity,StatuaryRenderer.State> {
private record Quad(float x,float y,float z,int side,int color){} 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); 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 Map<TypedEntityData<?>,VoxelSculpture> ITEMS=new LinkedHashMap<>(32,.75f,true);
private static final float[][] FACES={ private static final float[][] FACES={
{0,0,0, 0,1,0, 1,1,0, 1,0,0, 0,0,-1}, {0,0,0, 0,1,0, 1,1,0, 1,0,0, 0,0,-1},
@@ -35,26 +36,33 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
{0,0,1, 0,0,0, 1,0,0, 1,0,1, 0,-1,0} {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}}; private static final int[][] STEP={{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}};
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();float yaw;} public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();}
public State createRenderState(){return new State();} public State createRenderState(){return new State();}
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){ @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.yaw=-entity.getBlockState().getValue(Statuary.FACING).toYRot(); 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){ 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);
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);pose.popPose();
} }
private static List<Quad> mesh(VoxelSculpture sculpture){ private static List<Quad> mesh(VoxelSculpture sculpture,Direction facing,SculptureAnchor anchor){
if(sculpture==null)return List.of();var cached=MESHES.get(sculpture);if(cached!=null)return cached; if(sculpture==null)return List.of();var key=new Placement(sculpture,facing,anchor);var cached=MESHES.get(key);if(cached!=null)return cached;
// Rotate integer cells before anchoring: odd dimensions never introduce half-pixel offsets.
int turns=switch(facing){case WEST -> 1;case NORTH -> 2;case EAST -> 3;default -> 0;};
for(int i=0;i<turns;i++)sculpture=sculpture.rotate();
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>(); var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
float ox=(16-sculpture.width())/32f,oz=(16-sculpture.depth())/32f; int minX=16,minZ=16,maxX=0,maxZ=0;
for(var cell:sculpture.cells()){
minX=Math.min(minX,cell.x());minZ=Math.min(minZ,cell.z());
maxX=Math.max(maxX,cell.x()+1);maxZ=Math.max(maxZ,cell.z()+1);
}
float ox=(anchor.originX(maxX-minX)-minX)/16f,oz=(anchor.originZ(maxZ-minZ)-minZ)/16f;
for(var cell:sculpture.cells())for(int side=0;side<6;side++){ 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]; 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; 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())); 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){ private static void draw(List<Quad> mesh,PoseStack pose,SubmitNodeCollector collector,int light,int overlay){
if(mesh.isEmpty())return; if(mesh.isEmpty())return;
@@ -68,7 +76,7 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
var data=stack.get(DataComponents.BLOCK_ENTITY_DATA);if(data==null)return List.of(); var data=stack.get(DataComponents.BLOCK_ENTITY_DATA);if(data==null)return List.of();
var sculpture=ITEMS.get(data); 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());} 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 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));} 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,32 @@
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 edge of the occupied block; CENTER is the legacy placement. */
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;
if(Math.abs(x)<1e-7&&Math.abs(z)<1e-7)return of(context.getHorizontalDirection().getOpposite());
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;}
}
@@ -28,6 +28,7 @@ import net.minecraft.network.chat.Component;
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */ /** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
public final class Statuary { public final class Statuary {
public static final EnumProperty<Direction> FACING=BlockStateProperties.HORIZONTAL_FACING; 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 Block BLOCK=block("statuary",new SculptureBlock());
public static final Item ITEM=blockItem("statuary",BLOCK); public static final Item ITEM=blockItem("statuary",BLOCK);
public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock()); public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock());
@@ -59,11 +60,11 @@ public final class Statuary {
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));} private static 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 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 { private static final class SculptureBlock extends Block implements EntityBlock {
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH));} SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH).setValue(ANCHOR,SculptureAnchor.CENTER));}
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING);} @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());} @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)));} @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.rotate(mirror.getRotation(state.getValue(FACING)));} @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);} 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 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);} @Override protected ItemStack getCloneItemStack(LevelReader l,BlockPos pos,BlockState s,boolean data){return l.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM);}
@@ -49,7 +49,7 @@ public final class StatuaryEntity extends BlockEntity {
public static ItemStack item(VoxelSculpture sculpture){ public static ItemStack item(VoxelSculpture sculpture){
var item=new ItemStack(Statuary.ITEM);var tag=new CompoundTag();tag.put("sculpture",encode(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.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){ 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; var data=item.get(DataComponents.BLOCK_ENTITY_DATA);if(!item.is(Statuary.ITEM)||data==null||data.type()!=Statuary.TYPE)return null;
@@ -2150,7 +2150,7 @@
"sanctuary.weather.season.summer": "Summer", "sanctuary.weather.season.summer": "Summer",
"sanctuary.weather.season.autumn": "Autumn", "sanctuary.weather.season.autumn": "Autumn",
"sanctuary.weather.season.winter": "Winter", "sanctuary.weather.season.winter": "Winter",
"block.sanctuary.statuary": "Statuary", "block.sanctuary.statuary": "Clay Sculpture",
"block.sanctuary.clay_workshop": "Clay workshop", "block.sanctuary.clay_workshop": "Clay workshop",
"sanctuary.plans.imported_models": "My 3D models", "sanctuary.plans.imported_models": "My 3D models",
"sanctuary.plans.native_models": "Discovered creatures", "sanctuary.plans.native_models": "Discovered creatures",
@@ -2161,13 +2161,13 @@
"sanctuary.statuary.folder": "Shared folder", "sanctuary.statuary.folder": "Shared folder",
"sanctuary.statuary.refresh": "Refresh", "sanctuary.statuary.refresh": "Refresh",
"sanctuary.statuary.models": "GLB models", "sanctuary.statuary.models": "GLB models",
"sanctuary.statuary.placement": "The statue is placed facing the direction you look. Rotate adjusts the model before crafting.", "sanctuary.statuary.placement": "The sculpture faces your gaze and snaps to the aimed block edge, on the pixel grid. Rotate adjusts the model before crafting.",
"sanctuary.statuary.rotate": "Rotate", "sanctuary.statuary.rotate": "Rotate",
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…", "sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
"sanctuary.statuary.ready": "1 clay = 1 statue. Plain clay keeps model colors. Colored clay gives a solid color.", "sanctuary.statuary.ready": "1 clay = 1 sculpture. Plain: model colors. Colored: solid tint.",
"sanctuary.statuary.empty": "No .glb files", "sanctuary.statuary.empty": "No .glb files",
"sanctuary.statuary.input": "Clay", "sanctuary.statuary.input": "Clay",
"sanctuary.statuary.output": "Statue", "sanctuary.statuary.output": "Sculpture",
"sanctuary.statuary.model_name": "Model: %s", "sanctuary.statuary.model_name": "Model: %s",
"sanctuary.statuary.error.file": "Cannot read this local file.", "sanctuary.statuary.error.file": "Cannot read this local file.",
"sanctuary.statuary.error.format": "Invalid or unsupported GLB 2.0. Export a self-contained static model.", "sanctuary.statuary.error.format": "Invalid or unsupported GLB 2.0. Export a self-contained static model.",
@@ -2150,7 +2150,7 @@
"sanctuary.weather.season.summer": "Été", "sanctuary.weather.season.summer": "Été",
"sanctuary.weather.season.autumn": "Automne", "sanctuary.weather.season.autumn": "Automne",
"sanctuary.weather.season.winter": "Hiver", "sanctuary.weather.season.winter": "Hiver",
"block.sanctuary.statuary": "Statuaire", "block.sanctuary.statuary": "Sculpture dargile",
"block.sanctuary.clay_workshop": "Atelier dargile", "block.sanctuary.clay_workshop": "Atelier dargile",
"sanctuary.plans.imported_models": "Mes modèles 3D", "sanctuary.plans.imported_models": "Mes modèles 3D",
"sanctuary.plans.native_models": "Créatures découvertes", "sanctuary.plans.native_models": "Créatures découvertes",
@@ -2161,13 +2161,13 @@
"sanctuary.statuary.folder": "Dossier commun", "sanctuary.statuary.folder": "Dossier commun",
"sanctuary.statuary.refresh": "Actualiser", "sanctuary.statuary.refresh": "Actualiser",
"sanctuary.statuary.models": "Modèles GLB", "sanctuary.statuary.models": "Modèles GLB",
"sanctuary.statuary.placement": "La statue se pose dans le sens de votre regard. Tourner ajuste le modèle avant fabrication.", "sanctuary.statuary.placement": "La sculpture suit votre regard et se cale contre le bord visé du bloc, sur la grille de pixels. Tourner ajuste le modèle avant fabrication.",
"sanctuary.statuary.rotate": "Tourner", "sanctuary.statuary.rotate": "Tourner",
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…", "sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
"sanctuary.statuary.ready": "1 argile = 1 statue. Normale : couleurs du modèle. Colorée : teinte unie.", "sanctuary.statuary.ready": "1 argile = 1 sculpture. Normale : couleurs du modèle. Colorée : teinte unie.",
"sanctuary.statuary.empty": "Aucun fichier .glb", "sanctuary.statuary.empty": "Aucun fichier .glb",
"sanctuary.statuary.input": "Argile", "sanctuary.statuary.input": "Argile",
"sanctuary.statuary.output": "Statue", "sanctuary.statuary.output": "Sculpture",
"sanctuary.statuary.model_name": "Modèle : %s", "sanctuary.statuary.model_name": "Modèle : %s",
"sanctuary.statuary.error.file": "Impossible de lire ce fichier local.", "sanctuary.statuary.error.file": "Impossible de lire ce fichier local.",
"sanctuary.statuary.error.format": "GLB 2.0 invalide ou non pris en charge. Exportez un modèle statique autonome.", "sanctuary.statuary.error.format": "GLB 2.0 invalide ou non pris en charge. Exportez un modèle statique autonome.",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "beta.112" version = "beta.113"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]