|
|
|
@@ -0,0 +1,150 @@
|
|
|
|
|
package fr.koka.sanctuary.client;
|
|
|
|
|
|
|
|
|
|
import com.mojang.blaze3d.platform.InputConstants;
|
|
|
|
|
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
|
|
|
|
import fr.koka.sanctuary.plans.*;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
|
|
|
|
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
|
|
|
|
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
|
|
|
|
import net.minecraft.client.Minecraft;
|
|
|
|
|
import net.minecraft.client.gui.components.Button;
|
|
|
|
|
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
|
|
|
|
import net.minecraft.client.input.KeyEvent;
|
|
|
|
|
import net.minecraft.core.*;
|
|
|
|
|
import net.minecraft.nbt.CompoundTag;
|
|
|
|
|
import net.minecraft.network.chat.contents.TranslatableContents;
|
|
|
|
|
import net.minecraft.server.MinecraftServer;
|
|
|
|
|
import net.minecraft.server.level.ServerLevel;
|
|
|
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
|
|
|
import net.minecraft.util.ProblemReporter;
|
|
|
|
|
import net.minecraft.world.InteractionHand;
|
|
|
|
|
import net.minecraft.world.item.BlockItem;
|
|
|
|
|
import net.minecraft.world.item.context.BlockPlaceContext;
|
|
|
|
|
import net.minecraft.world.level.*;
|
|
|
|
|
import net.minecraft.world.level.block.Blocks;
|
|
|
|
|
import net.minecraft.world.level.block.state.BlockState;
|
|
|
|
|
import net.minecraft.world.level.storage.TagValueInput;
|
|
|
|
|
import net.minecraft.world.phys.*;
|
|
|
|
|
import net.minecraft.world.phys.shapes.*;
|
|
|
|
|
|
|
|
|
|
/** Native picking/collision and light propagation, compared with the actual emitted sculpture mesh. */
|
|
|
|
|
public final class Sculpture122ClientChecks implements FabricClientGameTest {
|
|
|
|
|
private static final String MODEL="sanctuary:model/"+"1".repeat(64);
|
|
|
|
|
private static final BlockPos TARGET=new BlockPos(0,1,0),SKY=new BlockPos(10,2,2),CONTROL=new BlockPos(18,2,2);
|
|
|
|
|
private static final VoxelSculpture ARCH=arch();
|
|
|
|
|
|
|
|
|
|
public void runTest(ClientGameTestContext c){
|
|
|
|
|
var intro=new AtomicBoolean(true);
|
|
|
|
|
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
|
|
|
|
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
|
|
|
|
Starter039TestSupport.selectFirst(screen);
|
|
|
|
|
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));
|
|
|
|
|
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(new KeyEvent(InputConstants.KEY_RETURN,0,0));intro.set(false);
|
|
|
|
|
}});
|
|
|
|
|
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
|
|
|
|
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Sculpture122 "+UUID.randomUUID());s.setSeed("122");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
|
|
|
|
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
|
|
|
|
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-14;x<=21;x++)for(int z=-6;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);p.setYRot(0);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));p.inventoryMenu.broadcastFullState();
|
|
|
|
|
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
|
|
|
|
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
|
|
|
|
});
|
|
|
|
|
c.waitFor(m->m.player.distanceToSqr(.5,1,3.2)<.1&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(TARGET.below()).is(Blocks.STONE),150);
|
|
|
|
|
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,.999,.5)));c.waitTicks(3);
|
|
|
|
|
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.below())&&hit.getDirection()==Direction.UP,"Cursor aims at placement support"));
|
|
|
|
|
c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.waitTicks(3);
|
|
|
|
|
server.runOnServer(s->System.out.println("SCULPTURE122_PLACED "+player(s).level().getBlockState(TARGET)+" "+player(s).level().getBlockEntity(TARGET)));
|
|
|
|
|
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),150);
|
|
|
|
|
server.runOnServer(s->{physical(player(s).level(),TARGET);placementOverlap(player(s));});
|
|
|
|
|
c.runOnClient(m->{physical(m.level,TARGET);geometry(m);Sculpture113ClientChecks.geometry(m,MODEL);});
|
|
|
|
|
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.0625,1.5,.5)));c.waitTicks(5);
|
|
|
|
|
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects an occupied pillar"));
|
|
|
|
|
c.takeScreenshot("sculpture122-voxel-outline");
|
|
|
|
|
server.runOnServer(s->player(s).level().setBlockAndUpdate(TARGET.north(),Blocks.REDSTONE_BLOCK.defaultBlockState()));
|
|
|
|
|
c.waitFor(m->m.level.getBlockState(TARGET.north()).is(Blocks.REDSTONE_BLOCK),100);
|
|
|
|
|
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1.5,.5)));c.waitTicks(5);
|
|
|
|
|
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET.north()),"Native cursor passes through the sculpture hole to the block behind"));
|
|
|
|
|
c.takeScreenshot("sculpture122-pick-through-hole");
|
|
|
|
|
// A state edit and fresh entity payload must invalidate geometry on both sides.
|
|
|
|
|
server.runOnServer(s->{var l=player(s).level();l.setBlockAndUpdate(TARGET,l.getBlockState(TARGET).setValue(Statuary.FACING,Direction.WEST).setValue(Statuary.ANCHOR,SculptureAnchor.EAST));});
|
|
|
|
|
c.waitFor(m->m.level.getBlockState(TARGET).getValue(Statuary.FACING)==Direction.WEST,100);
|
|
|
|
|
server.runOnServer(s->{var l=player(s).level();var bounds=l.getBlockState(TARGET).getShape(l,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Server shape rotates and moves to aimed edge");});
|
|
|
|
|
c.runOnClient(m->{var bounds=m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds();check(bounds.maxX==1&&bounds.getXsize()==.25,"Client shape follows changed blockstate");});
|
|
|
|
|
var small=new VoxelSculpture(MODEL,"Small",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1)));
|
|
|
|
|
server.runOnServer(s->place(player(s).level(),TARGET,small,Direction.WEST,SculptureAnchor.EAST));
|
|
|
|
|
c.waitFor(m->m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&small.equals(e.sculpture()),100);
|
|
|
|
|
c.runOnClient(m->check(m.level.getBlockState(TARGET).getShape(m.level,TARGET).bounds().getYsize()==1/16d,"Changed block entity payload invalidates cached shape"));
|
|
|
|
|
server.runOnServer(s->{var l=player(s).level();place(l,TARGET,ARCH,Direction.SOUTH,SculptureAnchor.CENTER);l.setBlockAndUpdate(TARGET.north(),Blocks.AIR.defaultBlockState());
|
|
|
|
|
for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)place(l,new BlockPos(x,y,z),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
|
|
|
|
for(int x=16;x<=20;x++)for(int z=0;z<=4;z++)l.setBlockAndUpdate(new BlockPos(x,3,z),Blocks.STONE.defaultBlockState());
|
|
|
|
|
l.setBlockAndUpdate(new BlockPos(-8,2,0),Blocks.GLOWSTONE.defaultBlockState());place(l,new BlockPos(-7,2,0),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);
|
|
|
|
|
});
|
|
|
|
|
server.waitFor(s->{var l=player(s).level();return l.getBrightness(LightLayer.SKY,SKY)==15&&l.getBrightness(LightLayer.SKY,CONTROL)<15&&l.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13;},200);
|
|
|
|
|
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15&&m.level.getBrightness(LightLayer.SKY,CONTROL)<15&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-6,2,0))==13,200);
|
|
|
|
|
server.runOnServer(s->{var l=player(s).level();for(int x=8;x<=12;x++)for(int z=0;z<=4;z++)for(int y=3;y<=5;y++)check(l.getBrightness(LightLayer.SKY,new BlockPos(x,y,z))==15,"All 75 sculptures keep full native skylight");});
|
|
|
|
|
c.runOnClient(m->{var e=(StatuaryEntity)m.level.getBlockEntity(new BlockPos(10,4,2));var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(e,state,0,Vec3.ZERO,null);check(net.minecraft.util.LightCoordsUtil.sky(state.lightCoords)==15,"Actual renderer receives full skylight inside a group");});
|
|
|
|
|
// Visual crowd at ground level: no whole-block AO squares between the models.
|
|
|
|
|
server.runOnServer(s->{var p=player(s);for(int x=-3;x<=3;x++)for(int z=-5;z<=-2;z++)place(p.level(),new BlockPos(x,1,z),ARCH,Direction.from2DDataValue(Math.floorMod(x+z,4)),SculptureAnchor.CENTER);p.teleportTo(.5,3,5.5);p.getAbilities().flying=true;p.onUpdateAbilities();});
|
|
|
|
|
c.waitFor(m->m.player.distanceToSqr(.5,3,5.5)<.1,100);c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(.5,1,-3)));c.waitTicks(12);c.takeScreenshot("sculpture122-crowd-light");
|
|
|
|
|
// Transparent-to-light is not emissive: in a closed room sculpture faces stay dark.
|
|
|
|
|
server.runOnServer(s->{var l=player(s).level();for(int x=-13;x<=-9;x++)for(int y=1;y<=5;y++)for(int z=5;z<=9;z++)l.setBlockAndUpdate(new BlockPos(x,y,z),x==-13||x==-9||y==1||y==5||z==5||z==9?Blocks.STONE.defaultBlockState():Blocks.AIR.defaultBlockState());place(l,new BlockPos(-11,2,7),ARCH,Direction.SOUTH,SculptureAnchor.CENTER);});
|
|
|
|
|
c.waitFor(m->m.level.getBlockEntity(new BlockPos(-11,2,7)) instanceof StatuaryEntity&&m.level.getBrightness(LightLayer.SKY,new BlockPos(-11,2,7))==0&&m.level.getBrightness(LightLayer.BLOCK,new BlockPos(-11,2,7))==0,200);
|
|
|
|
|
c.runOnClient(m->{var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState((StatuaryEntity)m.level.getBlockEntity(new BlockPos(-11,2,7)),state,0,Vec3.ZERO,null);check(state.lightCoords==0,"No fullbright workaround: a dark room produces dark faces");});
|
|
|
|
|
System.out.println("SCULPTURE122_LIGHT_PASS: 75 stacked models sky=15; opaque control shadow; block light=13 across sculpture; dark room=0; shade brightness=1");
|
|
|
|
|
}
|
|
|
|
|
try(var world=saved.open()){
|
|
|
|
|
c.waitFor(m->m.player!=null&&m.gui.screen()==null&&m.level.getBlockEntity(TARGET) instanceof StatuaryEntity e&&ARCH.equals(e.sculpture()),1200);
|
|
|
|
|
world.getServer().runOnServer(s->physical(player(s).level(),TARGET));c.runOnClient(m->physical(m.level,TARGET));
|
|
|
|
|
c.waitFor(m->m.level.getBrightness(LightLayer.SKY,SKY)==15,200);
|
|
|
|
|
System.out.println("SCULPTURE122_PASS: native placement, voxel outline and picking through holes, authoritative collisions, shared render geometry, cache refresh, light propagation and save/reconnect");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private static void placementOverlap(ServerPlayer player){
|
|
|
|
|
var pos=new BlockPos(3,1,0);var level=player.level();player.teleportTo(3.75,1,.5);player.setYRot(0);
|
|
|
|
|
var filled=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)filled.add(new VoxelSculpture.Cell(x,y,z,-1));
|
|
|
|
|
var dense=new VoxelSculpture(MODEL,"Solid",16,16,16,filled);var stack=StatuaryEntity.item(dense);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
|
|
|
|
var hit=new BlockHitResult(new Vec3(3.01,1,.5),Direction.UP,pos.below(),false);
|
|
|
|
|
var blocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
|
|
|
|
check(!blocked.consumesAction()&&level.getBlockState(pos).isAir()&&stack.getCount()==1,"Native placement refuses occupied voxels inside player without consuming sculpture");
|
|
|
|
|
var postCells=new ArrayList<VoxelSculpture.Cell>();for(int y=0;y<16;y++)for(int x=0;x<2;x++)for(int z=0;z<2;z++)postCells.add(new VoxelSculpture.Cell(x,y,z,-1));
|
|
|
|
|
var post=new VoxelSculpture(MODEL,"Post",2,16,2,postCells);stack=StatuaryEntity.item(post);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack);
|
|
|
|
|
var allowed=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit));
|
|
|
|
|
check(allowed.consumesAction()&&level.getBlockState(pos).is(Statuary.BLOCK),"Native placement allows empty part of block beside player");
|
|
|
|
|
check(level.noCollision(player,player.getBoundingBox()),"Accepted sculpture does not intersect player");
|
|
|
|
|
level.setBlockAndUpdate(pos,Blocks.AIR.defaultBlockState());player.teleportTo(.5,1,3.2);player.getInventory().setItem(player.getInventory().getSelectedSlot(),StatuaryEntity.item(ARCH));player.inventoryMenu.broadcastFullState();
|
|
|
|
|
System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied voxels, allows empty space, preserves stack on rejection");
|
|
|
|
|
}
|
|
|
|
|
private static void physical(Level level,BlockPos pos){
|
|
|
|
|
var state=level.getBlockState(pos);var shape=state.getShape(level,pos);var collision=state.getCollisionShape(level,pos);
|
|
|
|
|
check(shape==collision,"Selection and native collision share cached shape");check(shape.bounds().getZsize()==.25,"No invisible full-block depth");
|
|
|
|
|
check(level.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.OUTLINE,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.MISS,"Outline ray passes through empty arch center");
|
|
|
|
|
check(level.clip(new ClipContext(new Vec3(.0625,1.5,2),new Vec3(.0625,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Collision ray hits pillar");
|
|
|
|
|
check(level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query leaves hole empty");
|
|
|
|
|
check(!level.noCollision(new AABB(.02,1.2,.4,.1,1.7,.6)),"Native collision query blocks occupied pillar");
|
|
|
|
|
check(!state.isCollisionShapeFullBlock(level,pos)&&!state.isSolidRender()&&!state.canOcclude(),"Sculpture is not a solid occluding cube");
|
|
|
|
|
check(state.getOcclusionShape().isEmpty()&&state.propagatesSkylightDown()&&state.getLightDampening()==0&&state.getShadeBrightness(level,pos)==1,"Light metadata never reintroduces full-block occlusion");
|
|
|
|
|
check(!state.isSuffocating(level,pos)&&state.getLightEmission()==0,"Decorative shape neither suffocates nor emits light");
|
|
|
|
|
}
|
|
|
|
|
private static void geometry(Minecraft m){
|
|
|
|
|
var padded=new VoxelSculpture(MODEL,"Padded",13,5,15,List.of(new VoxelSculpture.Cell(2,2,3,-1),new VoxelSculpture.Cell(6,4,10,-1)));
|
|
|
|
|
var dense=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<16;z++)dense.add(new VoxelSculpture.Cell(x,y,z,-1));
|
|
|
|
|
var models=List.of(ARCH,padded,new VoxelSculpture(MODEL,"Dense",16,16,16,dense),new VoxelSculpture(MODEL,"One voxel",1,1,1,List.of(new VoxelSculpture.Cell(0,0,0,-1))));
|
|
|
|
|
int cases=0;for(var model:models)for(var direction:Direction.Plane.HORIZONTAL)for(var anchor:SculptureAnchor.values()){
|
|
|
|
|
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,direction).setValue(Statuary.ANCHOR,anchor);var entity=new StatuaryEntity(BlockPos.ZERO,state);entity.setLevel(m.level);load(entity,model,m.level);
|
|
|
|
|
var shape=entity.shape(state);check(shape==entity.shape(state),"Repeated geometry queries reuse the same shape");var boxes=shape.toAabbs();double volume=boxes.stream().mapToDouble(b->b.getXsize()*b.getYsize()*b.getZsize()).sum();check(volume==model.cells().size()/4096d,"Shape contains exactly the occupied volume");
|
|
|
|
|
var renderer=new StatuaryRenderer();var render=renderer.createRenderState();renderer.extractRenderState(entity,render,0,Vec3.ZERO,null);
|
|
|
|
|
for(var q:render.mesh){var center=new Vec3(q.x()+1/32d,q.y()+1/32d,q.z()+1/32d);check(boxes.stream().anyMatch(b->b.contains(center)),"Every rendered cell is physically present");
|
|
|
|
|
var normal=switch(q.side()){case 0->Direction.NORTH;case 1->Direction.SOUTH;case 2->Direction.WEST;case 3->Direction.EAST;case 4->Direction.UP;default->Direction.DOWN;};var neighbor=center.add(normal.getStepX()/16d,normal.getStepY()/16d,normal.getStepZ()/16d);check(boxes.stream().noneMatch(b->b.contains(neighbor)),"Rendered exposed face is also exposed physically");}
|
|
|
|
|
cases++;
|
|
|
|
|
}
|
|
|
|
|
System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native voxel shapes agree with emitted meshes, including holes, padding, dense 4096 cells, single voxel, 4 directions and 5 anchors");
|
|
|
|
|
}
|
|
|
|
|
private static void place(ServerLevel level,BlockPos pos,VoxelSculpture model,Direction facing,SculptureAnchor anchor){
|
|
|
|
|
var state=Statuary.BLOCK.defaultBlockState().setValue(Statuary.FACING,facing).setValue(Statuary.ANCHOR,anchor);level.setBlockAndUpdate(pos,state);var entity=(StatuaryEntity)level.getBlockEntity(pos);load(entity,model,level);entity.setChanged();level.sendBlockUpdated(pos,state,state,3);
|
|
|
|
|
}
|
|
|
|
|
private static void load(StatuaryEntity entity,VoxelSculpture model,Level level){var tag=new CompoundTag();tag.put("sculpture",StatuaryEntity.encode(model));entity.loadWithComponents(TagValueInput.create(ProblemReporter.DISCARDING,level.registryAccess(),tag));}
|
|
|
|
|
private static VoxelSculpture arch(){var cells=new ArrayList<VoxelSculpture.Cell>();for(int x=0;x<16;x++)for(int y=0;y<16;y++)for(int z=0;z<4;z++)if(x<2||x>=14||y>=14)cells.add(new VoxelSculpture.Cell(x,y,z,0xffc99679));return new VoxelSculpture(MODEL,"Arch",16,16,4,cells);}
|
|
|
|
|
private static ServerPlayer player(MinecraftServer server){return server.getPlayerList().getPlayers().getFirst();}
|
|
|
|
|
private static void check(boolean value,String reason){if(!value)throw new AssertionError(reason);}
|
|
|
|
|
}
|