Snap clay sculptures to aimed block edges on the voxel grid (beta.113)
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -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 l’atelier d’argile à la hotbar active du joueur,
|
La version **beta.113** nomme les miniatures « sculptures d’argile » 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 l’accroche, sa sauvegarde
|
||||||
et la mise à jour de l’instance **Sanctuary Beta**.
|
et les vérifications. L’atelier 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à
|
||||||
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
Les [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 d’argile et alignement
|
||||||
|
|
||||||
|
Pose contre le bord visé, orientation selon le regard et coordonnées entières
|
||||||
|
pour chaque voxel, y compris les modèles de largeur impaire. L’accroche est
|
||||||
|
conservée à la reconnexion. [Contrat et vérifications](docs/clay-sculpture-grid-beta113.md).
|
||||||
|
|
||||||
## beta.112 — Atelier d’argile et hotbar
|
## beta.112 — Atelier d’argile 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.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# STAT-04 — Sculptures d’argile et accroche au bord — beta.113
|
||||||
|
|
||||||
|
Contrat du 17 septembre 2026, branche `codex/clay-sculpture-grid-beta113`.
|
||||||
|
|
||||||
|
Les miniatures de l’atelier deviennent des « sculptures d’argile » / « Clay
|
||||||
|
sculptures ». Le nom du modèle reste dans leur infobulle. Les statues en blocs
|
||||||
|
du Métabli restent une catégorie distincte.
|
||||||
|
|
||||||
|
À la pose, le bord horizontal le plus proche du point visé accueille la
|
||||||
|
sculpture. L’autre axe est centré au pixel entier le plus proche, sur la grille
|
||||||
|
de 1/16 de bloc. Sur une face latérale, elle touche le bord du bloc support.
|
||||||
|
À égalité exacte entre deux bords, l’axe Z départage ; au centre exact,
|
||||||
|
le bord côté joueur est retenu. L’orientation suit toujours le regard.
|
||||||
|
Le bouton Tourner prépare le modèle avant fabrication. Les marges horizontales vides du modèle ne créent pas de décalage au bord.
|
||||||
|
Tous les sommets restent
|
||||||
|
sur la grille entière, même avec des dimensions impaires et après rotation.
|
||||||
|
|
||||||
|
## Compatibilité préalable
|
||||||
|
|
||||||
|
La propriété native additive `anchor` du bloc `sanctuary:statuary` mémorise
|
||||||
|
`center`, `north`, `south`, `east` ou `west`. Un ancien état sans cette propriété
|
||||||
|
prend `center` : il reste centré, avec la correction visuelle du demi-voxel.
|
||||||
|
Le schéma 1, les voxels, noms de modèles et identifiants ne changent pas.
|
||||||
|
Il n’y a aucun parcours ni réécriture de chunks existants. La sauvegarde native
|
||||||
|
conserve l’accroche ; les rotations et miroirs de structures la transforment.
|
||||||
|
Casser et reposer choisit une nouvelle accroche sans modifier le modèle.
|
||||||
|
Les nouveaux objets emploient le nom traduit de leur type ; les noms déjà
|
||||||
|
enregistrés et les noms personnalisés restent conservés. La collision cubique
|
||||||
|
et la limite d’une sculpture par bloc restent inchangées. Client et serveur
|
||||||
|
doivent utiliser ensemble beta.113.
|
||||||
|
|
||||||
|
## Vérifications natives
|
||||||
|
|
||||||
|
Le parcours `Statuary106ClientChecks`, étendu par `Sculpture113ClientChecks`,
|
||||||
|
passe en **1 min 25 s**, sur Minecraft 26.3 et un monde plat jetable de graine 106 :
|
||||||
|
|
||||||
|
- 5 120 combinaisons de largeur/profondeur, orientation et accroche : contrôle
|
||||||
|
de la géométrie émise, grille exacte de 1/16 et absence de dépassement du bloc.
|
||||||
|
- Modèles avec marges transparentes, repères colorés pour vérifier le sens du
|
||||||
|
rendu et égalité de la géométrie centrée entre l’objet et le bloc.
|
||||||
|
- Vrais clics de pose sur les quatre bords : orientation, bord côté serveur,
|
||||||
|
réception côté client, butin/repose et conservation après reconnexion.
|
||||||
|
- Contextes natifs de pose sur les faces latérales et coordonnées négatives.
|
||||||
|
- Rotation/miroir de chaque combinaison d’accroche et d’orientation ; lecture
|
||||||
|
des anciens états sans `anchor`, avec et sans `facing`.
|
||||||
|
- Noms français/anglais des objets fabriqués, menu aux échelles 2 et 3,
|
||||||
|
fabrication payée, Maj-clic limité à la hotbar et régression Métabli.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew :sanctuary:runClientGameTest \
|
||||||
|
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||||
|
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Marqueurs `STATUARY113_PASS` et `SCULPTURE113_GEOMETRY_PASS` dans
|
||||||
|
`build/clay-grid113-client.log`. Captures relues dans
|
||||||
|
`build/clay-grid113-evidence/`. Le GameTest dédié reste exclu conformément au
|
||||||
|
refus antérieur de son EULA ; ces essais utilisent le serveur intégré macOS.
|
||||||
|
Pas de validation Windows ni depuis deux ordinateurs. Aucun monde personnel
|
||||||
|
n’a été ouvert.
|
||||||
|
|
||||||
|
## Livraison
|
||||||
|
|
||||||
|
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||||
|
passe en **3 min 11 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||||
|
aux sources du dépôt. Les classes et ressources hors du ticket sont identiques
|
||||||
|
à beta.112. Les deux packs contiennent le même JAR, sans monde, GLB ni test.
|
||||||
|
|
||||||
|
- Sanctuary-beta.113.mrpack : 10173807 octets, SHA-256
|
||||||
|
`ed082ff29a0e9a70fd6786bc024241b276cb516bc09fb5151d6cb324604429bb`.
|
||||||
|
- Sanctuary-Test-beta.113.mrpack : 10192733 octets, SHA-256
|
||||||
|
`387f54dc77884a9c9bccdcc1904778ca7f0b2fbda126754fdc49b10a492e4602`.
|
||||||
|
- JAR Sanctuary : SHA-256
|
||||||
|
`e242d9fd2edeede922a72a10e988e29c2b49dea52105cfc9fc8573154849ead3`.
|
||||||
|
|
||||||
|
Reçu local : `build/clay-grid113-artifact.json`.
|
||||||
|
|
||||||
|
Publication et synchronisation de l’instance à effectuer après le commit source.
|
||||||
+2
-2
@@ -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
|
||||||
|
|||||||
+90
@@ -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);}
|
||||||
|
}
|
||||||
+15
-9
@@ -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 d’argile"),"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 d’argile",
|
||||||
"block.sanctuary.clay_workshop": "Atelier d’argile",
|
"block.sanctuary.clay_workshop": "Atelier d’argile",
|
||||||
"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
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user