Separate safe golden rotations from Steve wrench powers (beta.123)
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def wrench123Client = providers.gradleProperty('sanctuaryWrench123ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWrench123ClientTests', wrench123Client)
|
||||
if (wrench123Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Wrench123ClientChecks') }
|
||||
|
||||
def sculpture122Client = providers.gradleProperty('sanctuarySculpture122ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySculpture122ClientTests', sculpture122Client)
|
||||
if (sculpture122Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Sculpture122ClientChecks') }
|
||||
|
||||
+6
-6
@@ -43,7 +43,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
net.fabricmc.fabric.api.client.gametest.v1.world.TestWorldSave saved;
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench120 "+UUID.randomUUID());s.setSeed("120");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();p.teleportTo(.5,1,3.2);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.STEVE_WRENCH));p.inventoryMenu.broadcastFullState();
|
||||
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
int tested=0;for(var block:BuiltInRegistries.BLOCK)for(var property:block.getStateDefinition().getProperties()){
|
||||
@@ -52,7 +52,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
check(tested>1000,"Broad block property coverage");System.out.println("WRENCH120_PROPERTIES "+tested);
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));click(c,TARGET,false,false);
|
||||
server.waitFor(s->player(s).level().getBlockState(TARGET)!=stairs,100);c.waitFor(m->m.level.getBlockState(TARGET)!=stairs,100);
|
||||
server.runOnServer(s->{var actual=player(s).level().getBlockState(TARGET);check(actual.getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING)&&actual.getValue(StairBlock.HALF)==stairs.getValue(StairBlock.HALF),"Right click changes facing only");});
|
||||
@@ -64,8 +64,8 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
c.takeScreenshot("wrench120-stairs");
|
||||
// A container rotates in place; inventory and block entity survive the full native interaction.
|
||||
click(c,CHEST,false,false);server.runOnServer(s->{var p=player(s);var chest=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(chest.getItem(0).is(Items.DIAMOND)&&chest.getItem(0).getCount()==23,"Chest contents retained");check(p.containerMenu==p.inventoryMenu,"Wrench does not open ordinary chest");check(chest.getBlockState().getValue(ChestBlock.FACING)!=Blocks.CHEST.defaultBlockState().getValue(ChestBlock.FACING),"Chest rotates");});
|
||||
// Survival uses exactly the same gestures; dirt keeps its native ID, shape and loot.
|
||||
server.runOnServer(s->player(s).setGameMode(GameType.SURVIVAL));c.waitFor(m->!m.player.isCreative(),100);
|
||||
// Ordinary survival without commands retains Steve’s full powers; dirt keeps its native ID, shape and loot.
|
||||
server.runOnServer(s->{s.getWorldData().setAllowCommands(false);player(s).setGameMode(GameType.SURVIVAL);});c.waitFor(m->!m.player.isCreative(),100);
|
||||
var original=c.computeOnClient(m->geometry(m,DIRT));c.takeScreenshot("wrench120-dirt-before");
|
||||
click(c,DIRT,false,false);server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
var rotated=c.computeOnClient(m->geometry(m,DIRT));checkUvs(original,rotated);c.takeScreenshot("wrench120-dirt-90");
|
||||
@@ -81,7 +81,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
var p=player(s);p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure cannot edit");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.SURVIVAL);check(!GoldenWrench.allowed(p,TARGET.offset(100,0,0),InteractionHand.MAIN_HAND,Direction.UP),"Out-of-range edit refused");
|
||||
});
|
||||
// A full Metabli still assembles with two normal right clicks, then separates with sneak-use.
|
||||
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
|
||||
var table=new BlockPos(6,1,0);server.runOnServer(s->{var p=player(s);p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(Multiblocks.WRENCH));p.inventoryMenu.broadcastFullState();for(var q:Metatable.members(table))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());p.teleportTo(7.5,1,4);});
|
||||
c.waitFor(m->m.player.distanceToSqr(7.5,1,4)<.1&&m.level.getBlockState(table).is(Blocks.CRAFTING_TABLE),100);
|
||||
click(c,table.offset(1,0,2),false,false);click(c,table.offset(1,0,2),false,false);
|
||||
server.waitFor(s->player(s).level().getBlockState(table).is(Metatable.BLOCK),100);c.waitFor(m->m.level.getBlockState(table).is(Metatable.BLOCK),100);
|
||||
@@ -116,7 +116,7 @@ public final class Wrench120ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
System.out.println("WRENCH120_PASS native creative/survival clicks, reverse, properties, textures and resource reload, unchanged geometry, filled chest, multiblock assembly priority, permissions, cleanup and save/reconnect");
|
||||
}
|
||||
private static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
|
||||
static void click(ClientGameTestContext c,BlockPos pos,boolean left,boolean sneak){
|
||||
if(sneak){c.getInput().holdKey(o->o.keyShift);c.waitTicks(3);}
|
||||
c.runOnClient(m->m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,new Vec3(pos.getX()+.5,pos.getY()+.3,pos.getZ()+.5)));c.waitTicks(3);
|
||||
c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(pos),"Native cursor hits target "+pos+" instead of "+m.hitResult));
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.multiblock.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.fabric.api.event.player.*;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.*;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
import net.minecraft.world.level.block.piston.PistonBaseBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Native golden-wrench clicks cannot create resources or split linked blocks. */
|
||||
public final class Wrench123ClientChecks implements FabricClientGameTest {
|
||||
private static final BlockPos TARGET=new BlockPos(0,1,0),CHEST=new BlockPos(2,1,0),DIRT=new BlockPos(-2,1,0);
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(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);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Wrench123 "+UUID.randomUUID());s.setSeed("123");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);var l=p.level();for(int x=-5;x<=8;x++)for(int z=-4;z<=6;z++)l.setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.teleportTo(.5,1,3.2);equip(p,Multiblocks.WRENCH);
|
||||
l.setBlockAndUpdate(TARGET,Blocks.OAK_STAIRS.defaultBlockState());l.setBlockAndUpdate(CHEST,Blocks.CHEST.defaultBlockState());((ChestBlockEntity)l.getBlockEntity(CHEST)).setItem(0,new ItemStack(Items.DIAMOND,23));l.setBlockAndUpdate(DIRT,Blocks.DIRT.defaultBlockState());
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Multiblocks.WRENCH)&&m.player.distanceToSqr(.5,1,3.2)<.1&&m.level.getBlockState(TARGET).is(Blocks.OAK_STAIRS),150);
|
||||
var stairs=c.computeOnClient(m->m.level.getBlockState(TARGET));
|
||||
Wrench120ClientChecks.click(c,TARGET,false,false);server.runOnServer(s->check(player(s).level().getBlockState(TARGET).getValue(StairBlock.FACING)!=stairs.getValue(StairBlock.FACING),"Golden wrench changes native stair facing"));
|
||||
Wrench120ClientChecks.click(c,TARGET,false,true);server.waitFor(s->player(s).level().getBlockState(TARGET)==stairs,100);
|
||||
for(int i=0;i<5;i++){Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);}
|
||||
server.runOnServer(s->{var state=player(s).level().getBlockState(TARGET);check(!state.getValue(StairBlock.WATERLOGGED)&&state.getValue(StairBlock.HALF)==Half.BOTTOM&&state.getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT,"Repeated native clicks never select water, half or fake stair shapes");});
|
||||
for(int i=0;i<4;i++)Wrench120ClientChecks.click(c,CHEST,false,false);
|
||||
server.runOnServer(s->{var p=player(s);var be=(ChestBlockEntity)p.level().getBlockEntity(CHEST);check(be.getItem(0).getCount()==23&&be.getBlockState()==Blocks.CHEST.defaultBlockState(),"Single chest remains single, dry and filled after four rotations");check(p.containerMenu==p.inventoryMenu,"Ordinary rotation does not open chest");});
|
||||
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
|
||||
server.runOnServer(s->{var l=player(s).level();check(WrenchTextures.rotation(l.getChunkAt(DIRT),DIRT)==0&&l.getBlockState(DIRT).is(Blocks.DIRT),"Golden wrench never adds a texture transform");regressions(player(s));});
|
||||
c.takeScreenshot("wrench123-golden-safe");
|
||||
// Finding Steve's wrench grants its full powers in ordinary survival, without operator status.
|
||||
server.runOnServer(s->{var p=player(s);p.setGameMode(GameType.SURVIVAL);s.getWorldData().setAllowCommands(false);equip(p,Multiblocks.STEVE_WRENCH);});
|
||||
c.waitFor(m->!m.player.isCreative()&&m.player.getMainHandItem().is(Multiblocks.STEVE_WRENCH),100);
|
||||
Wrench120ClientChecks.click(c,DIRT,true,false);Wrench120ClientChecks.click(c,DIRT,false,false);
|
||||
server.waitFor(s->WrenchTextures.rotation(player(s).level().getChunkAt(DIRT),DIRT)==1,100);c.waitFor(m->WrenchTextureModels.rotation(DIRT)==1,100);
|
||||
Wrench120ClientChecks.click(c,TARGET,false,false);Wrench120ClientChecks.click(c,TARGET,true,false);Wrench120ClientChecks.click(c,TARGET,false,false);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(TARGET).getValue(StairBlock.HALF)==Half.TOP,"Native Steve edit works for ordinary survival without commands");
|
||||
for(var hand:InteractionHand.values()){
|
||||
p.setItemInHand(hand,new ItemStack(Multiblocks.STEVE_WRENCH));check(GoldenWrench.allowed(p,TARGET,hand,Direction.UP),"Survival use allowed in both hands");
|
||||
GoldenWrench.change(p,DIRT,hand,Direction.UP);
|
||||
}
|
||||
check(WrenchTextures.rotation(p.level().getChunkAt(DIRT),DIRT)==3,"Both hands can freely edit textures in survival");
|
||||
p.setItemInHand(InteractionHand.OFF_HAND,ItemStack.EMPTY);p.setGameMode(GameType.CREATIVE);check(GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Creative access without commands enabled");
|
||||
p.setGameMode(GameType.ADVENTURE);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Adventure restrictions remain");p.setGameMode(GameType.SPECTATOR);check(!GoldenWrench.allowed(p,TARGET,InteractionHand.MAIN_HAND,Direction.UP),"Spectator cannot edit");p.setGameMode(GameType.CREATIVE);
|
||||
var steve=new ItemStack(Multiblocks.STEVE_WRENCH);check(Boolean.TRUE.equals(steve.get(DataComponents.ENCHANTMENT_GLINT_OVERRIDE)),"Distinct enchanted Steve item");
|
||||
});
|
||||
c.waitFor(m->m.player.isCreative(),100);c.takeScreenshot("wrench123-steve-texture");
|
||||
System.out.println("WRENCH123_SAFE_PASS native clicks, water/inventory preservation, linked blocks, real neighbour shapes, support checks, ordinary survival and creative use, adventure/spectator restrictions and both hands");
|
||||
}
|
||||
// Keep the full former debug-stick contract on Steve, including UVs/save/reload, and assembly on gold.
|
||||
new Wrench120ClientChecks().runTest(c);
|
||||
System.out.println("WRENCH123_PASS safe golden wrench and Steve wrench with native regression coverage");
|
||||
}
|
||||
private static void regressions(ServerPlayer p){
|
||||
var l=p.level();
|
||||
// Water stays present when rotating a legitimately waterlogged block.
|
||||
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.WATERLOGGED,true),18);
|
||||
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Existing water is preserved");
|
||||
l.setBlock(TARGET,Blocks.OAK_STAIRS.defaultBlockState().setValue(StairBlock.FACING,Direction.WEST).setValue(StairBlock.SHAPE,StairsShape.INNER_LEFT),18);
|
||||
use(p,TARGET);check(l.getBlockState(TARGET).getValue(StairBlock.SHAPE)==StairsShape.STRAIGHT&&!l.getBlockState(TARGET).getValue(StairBlock.WATERLOGGED),"Native shape recomputed against actual empty neighbours");
|
||||
var a=CHEST;var b=CHEST.east();var base=Blocks.CHEST.defaultBlockState().setValue(ChestBlock.FACING,Direction.NORTH);
|
||||
l.setBlock(a,base.setValue(ChestBlock.TYPE,ChestType.LEFT),18);l.setBlock(b,base.setValue(ChestBlock.TYPE,ChestType.RIGHT),18);
|
||||
var first=(ChestBlockEntity)l.getBlockEntity(a);var second=(ChestBlockEntity)l.getBlockEntity(b);first.setItem(0,new ItemStack(Items.DIAMOND,23));second.setItem(0,new ItemStack(Items.EMERALD,17));
|
||||
var left=l.getBlockState(a);var right=l.getBlockState(b);use(p,a);use(p,b);
|
||||
check(l.getBlockState(a)==left&&l.getBlockState(b)==right&&l.getBlockEntity(a)==first&&l.getBlockEntity(b)==second&&first.getItem(0).getCount()==23&&second.getItem(0).getCount()==17,"Double chest remains linked with both original inventories");
|
||||
var q=TARGET;var door=Blocks.OAK_DOOR.defaultBlockState();l.setBlock(q,door.setValue(DoorBlock.HALF,DoubleBlockHalf.LOWER),18);l.setBlock(q.above(),door.setValue(DoorBlock.HALF,DoubleBlockHalf.UPPER),18);
|
||||
var lower=l.getBlockState(q);var upper=l.getBlockState(q.above());use(p,q);use(p,q.above());check(l.getBlockState(q)==lower&&l.getBlockState(q.above())==upper,"Both door halves protected");l.setBlock(q.above(),Blocks.AIR.defaultBlockState(),18);
|
||||
var bed=Blocks.BED.red().defaultBlockState();l.setBlock(q,bed,18);use(p,q);check(l.getBlockState(q)==bed,"Bed half protected even if malformed already");
|
||||
var extended=Blocks.PISTON.defaultBlockState().setValue(PistonBaseBlock.EXTENDED,true);l.setBlock(q,extended,18);use(p,q);check(l.getBlockState(q)==extended,"Extended piston protected");
|
||||
for(var block:List.of(Blocks.PISTON_HEAD,Blocks.MOVING_PISTON)){l.setBlock(q,block.defaultBlockState(),18);use(p,q);check(l.getBlockState(q)==block.defaultBlockState(),"Attached/moving piston part protected");}
|
||||
l.setBlock(q,Blocks.OAK_LOG.defaultBlockState(),18);var axes=EnumSet.noneOf(Direction.Axis.class);for(int i=0;i<3;i++){use(p,q);axes.add(l.getBlockState(q).getValue(RotatedPillarBlock.AXIS));}check(axes.size()==3,"All three native log axes supported");
|
||||
l.setBlock(q,Blocks.PISTON.defaultBlockState(),18);var facings=EnumSet.noneOf(Direction.class);for(int i=0;i<6;i++){use(p,q);facings.add(l.getBlockState(q).getValue(PistonBaseBlock.FACING));}check(facings.size()==6,"Isolated piston supports all six native orientations");
|
||||
// Turning a wall torch toward empty air must not break it or its support.
|
||||
l.setBlock(q.north(),Blocks.STONE.defaultBlockState(),18);var torch=Blocks.WALL_TORCH.defaultBlockState().setValue(WallTorchBlock.FACING,Direction.SOUTH);l.setBlock(q,torch,18);use(p,q);check(l.getBlockState(q)==torch,"Unsupported rotation leaves wall-mounted block intact");l.setBlock(q.north(),Blocks.AIR.defaultBlockState(),18);
|
||||
l.setBlock(q,Blocks.OAK_STAIRS.defaultBlockState(),18);
|
||||
var top=GoldenWrench.cycle(l.getBlockState(q),StairBlock.HALF,false);check(top.getValue(StairBlock.HALF)==Half.TOP,"Steve still has native debug property cycle");
|
||||
// Existing visual edits are preserved; the ordinary key cannot create or reset them.
|
||||
WrenchTextures.turn(l,q,1);use(p,q);check(WrenchTextures.rotation(l.getChunkAt(q),q)==1,"Golden rotation preserves saved Steve texture data");WrenchTextures.turn(l,q,-1);
|
||||
}
|
||||
private static void use(ServerPlayer p,BlockPos pos){UseBlockCallback.EVENT.invoker().interact(p,p.level(),InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(pos),Direction.UP,pos,false));}
|
||||
private static void equip(ServerPlayer p,Item item){p.getInventory().setItem(p.getInventory().getSelectedSlot(),new ItemStack(item));p.inventoryMenu.broadcastFullState();}
|
||||
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
|
||||
/** Server-owned debug-stick gestures without changing a block's type or its inventory. */
|
||||
/** Ordinary rotations and Steve debug gestures, both owned by the server. */
|
||||
public final class GoldenWrench {
|
||||
public static final String TEXTURE="sanctuary_texture";
|
||||
private static final Map<ServerPlayer,Map<Block,String>> SELECTED=new WeakHashMap<>();
|
||||
@@ -27,14 +27,19 @@ public final class GoldenWrench {
|
||||
public static void register(){
|
||||
WrenchTextures.register();
|
||||
AttackBlockCallback.EVENT.register((player,level,hand,pos,face)->{
|
||||
if(!player.getItemInHand(hand).is(Multiblocks.WRENCH))return InteractionResult.PASS;
|
||||
if(!isWrench(player.getItemInHand(hand)))return InteractionResult.PASS;
|
||||
if(player.isSpectator())return InteractionResult.FAIL;
|
||||
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;select(p,pos);}
|
||||
if(player instanceof ServerPlayer p){if(!allowed(p,pos,hand,face))return InteractionResult.FAIL;
|
||||
if(p.getItemInHand(hand).is(Multiblocks.STEVE_WRENCH))select(p,pos);
|
||||
else p.sendSystemMessage(text("rotate_help"),true);}
|
||||
return InteractionResult.SUCCESS;
|
||||
});
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler,server)->{SELECTED.remove(handler.player);LAST_SELECTION.remove(handler.player);});
|
||||
}
|
||||
public static boolean isWrench(ItemStack stack){return stack.is(Multiblocks.WRENCH)||stack.is(Multiblocks.STEVE_WRENCH);}
|
||||
public static boolean allowed(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||
var tool=p.getItemInHand(hand);
|
||||
if(!isWrench(tool))return false;
|
||||
return p.isAlive()&&!p.isSpectator()&&p.getAbilities().mayBuild&&p.level().hasChunkAt(pos)
|
||||
&&p.isWithinBlockInteractionRange(pos,0)&&p.level().mayInteract(p,pos)
|
||||
&&p.mayUseItemAt(pos,face,p.getItemInHand(hand))
|
||||
@@ -66,8 +71,12 @@ public final class GoldenWrench {
|
||||
var property=choices.get(next);selected.put(state.getBlock(),property);
|
||||
p.sendSystemMessage(text("selected",label(property),value(p,pos,state,property)),true);
|
||||
}
|
||||
public static void change(ServerPlayer p,BlockPos pos){
|
||||
public static void change(ServerPlayer p,BlockPos pos,InteractionHand hand,Direction face){
|
||||
if(!allowed(p,pos,hand,face))return;
|
||||
var state=p.level().getBlockState(pos);if(state.isAir()||assembled(p,pos,state))return;
|
||||
if(p.getItemInHand(hand).is(Multiblocks.WRENCH)){
|
||||
p.sendSystemMessage(text(WrenchRotation.turn(p.level(),pos,p.isShiftKeyDown())),true);return;
|
||||
}
|
||||
var name=selected(p,state);SELECTED.computeIfAbsent(p,key->new HashMap<>()).put(state.getBlock(),name);
|
||||
if(name.equals(TEXTURE))WrenchTextures.turn(p.level(),pos,p.isShiftKeyDown()?-1:1);
|
||||
else {
|
||||
|
||||
@@ -23,7 +23,15 @@ import static fr.koka.sanctuary.multiblock.MultiblockData.Machine;
|
||||
|
||||
public final class Multiblocks {
|
||||
public static final Item WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("golden_wrench"),
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1)
|
||||
.component(net.minecraft.core.component.DataComponents.LORE,new net.minecraft.world.item.component.ItemLore(List.of(Component.translatable("sanctuary.wrench.rotate_help"))))
|
||||
.setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("golden_wrench")))));
|
||||
public static final Item STEVE_WRENCH=Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id("steve_wrench"),
|
||||
new GoldenWrench.Tool(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC)
|
||||
.component(net.minecraft.core.component.DataComponents.ENCHANTMENT_GLINT_OVERRIDE,true)
|
||||
.component(net.minecraft.core.component.DataComponents.LORE,new net.minecraft.world.item.component.ItemLore(List.of(
|
||||
Component.translatable("sanctuary.wrench.steve_power"),Component.translatable("sanctuary.wrench.steve_help"))))
|
||||
.setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id("steve_wrench")))));
|
||||
public static final MenuType<MultiblockMenu> MENU=Registry.register(BuiltInRegistries.MENU,SanctuaryMod.id("multiblock"),
|
||||
new ExtendedMenuType<>(MultiblockMenu::new,MultiblockMenu.View.CODEC));
|
||||
private record Preview(ServerLevel level,BlockPos origin,boolean furnace,long until){}
|
||||
@@ -34,9 +42,9 @@ public final class Multiblocks {
|
||||
GoldenWrench.register();
|
||||
SuperPistons.register();
|
||||
Metatable.register();
|
||||
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->out.accept(WRENCH));
|
||||
net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(CreativeModeTabs.TOOLS_AND_UTILITIES).register(out->{out.accept(WRENCH);out.accept(STEVE_WRENCH);});
|
||||
UseBlockCallback.EVENT.register((player,world,hand,hit)->{
|
||||
boolean wrench=player.getItemInHand(hand).is(WRENCH);
|
||||
boolean wrench=GoldenWrench.isWrench(player.getItemInHand(hand));
|
||||
if(world.isClientSide())return wrench?InteractionResult.SUCCESS:InteractionResult.PASS;
|
||||
if(!(player instanceof ServerPlayer p)||p.isSpectator())return InteractionResult.PASS;
|
||||
var level=p.level();var pos=hit.getBlockPos();var machine=MultiblockData.get(level).at(pos);
|
||||
@@ -45,7 +53,7 @@ public final class Multiblocks {
|
||||
if(!wrench&&p.isShiftKeyDown()&&(!p.getMainHandItem().isEmpty()||!p.getOffhandItem().isEmpty()))return InteractionResult.PASS;
|
||||
if(!level.mayInteract(p,pos)||!p.mayUseItemAt(pos,hit.getDirection(),p.getItemInHand(hand)))return InteractionResult.FAIL;
|
||||
if(wrench){if(!GoldenWrench.allowed(p,pos,hand,hit.getDirection()))return InteractionResult.FAIL;
|
||||
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos);
|
||||
if(!Metatable.wrench(p,pos)&&!SuperPistons.wrench(p,pos,hit.getDirection())&&!useWrench(p,pos,hit.getDirection()))GoldenWrench.change(p,pos,hand,hit.getDirection());
|
||||
return InteractionResult.SUCCESS;}
|
||||
if(ready(level,machine)&&accessible(p,machine))open(p,machine,pos,0,"",0);
|
||||
else p.sendSystemMessage(text("unavailable"),true);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.koka.sanctuary.multiblock;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.*;
|
||||
import static net.minecraft.world.level.block.state.properties.BlockStateProperties.*;
|
||||
|
||||
/** Only native orientation properties; fluid, contents and gameplay state are never selectable. */
|
||||
public final class WrenchRotation {
|
||||
private static final List<Property<?>> ORIENTATIONS=List.of(HORIZONTAL_FACING,FACING,FACING_HOPPER,AXIS,HORIZONTAL_AXIS,ROTATION_16,ORIENTATION);
|
||||
|
||||
public static String turn(ServerLevel level,BlockPos pos,boolean reverse){
|
||||
var before=level.getBlockState(pos);
|
||||
if(linked(before))return "linked";
|
||||
var property=ORIENTATIONS.stream().filter(before::hasProperty).findFirst().orElse(null);
|
||||
if(property==null)return "no_rotation";
|
||||
var oriented=GoldenWrench.cycle(before,property,reverse);
|
||||
if(!oriented.canSurvive(level,pos))return "unsupported";
|
||||
// Stairs and connections must reflect their real neighbours, not a debug-stick shape.
|
||||
var after=Block.updateFromNeighbourShapes(oriented,level,pos);
|
||||
if(!after.is(before.getBlock())||linked(after)||after.getFluidState()!=before.getFluidState()
|
||||
||!after.canSurvive(level,pos))return "unsupported";
|
||||
level.setBlock(pos,after,Block.UPDATE_ALL);
|
||||
return "rotated";
|
||||
}
|
||||
private static boolean linked(BlockState state){
|
||||
// A single-block operation cannot rotate another half, an attached head or a joined inventory.
|
||||
return state.hasProperty(DOUBLE_BLOCK_HALF)||state.hasProperty(BED_PART)||state.hasProperty(SIDE_CHAIN_PART)&&state.getValue(SIDE_CHAIN_PART).isConnected()
|
||||
||state.hasProperty(PISTON_TYPE)||state.hasProperty(EXTENDED)&&state.getValue(EXTENDED)
|
||||
||state.hasProperty(CHEST_TYPE)&&state.getValue(CHEST_TYPE)!=ChestType.SINGLE;
|
||||
}
|
||||
private WrenchRotation(){}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "sanctuary:item/steve_wrench"
|
||||
}
|
||||
}
|
||||
@@ -2233,5 +2233,13 @@
|
||||
"sanctuary.wrench.selected": "Selected property: %s = %s · right click to change",
|
||||
"sanctuary.wrench.changed": "%s = %s",
|
||||
"sanctuary.wrench.texture": "Texture rotation",
|
||||
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components."
|
||||
"sanctuary.wrench.assembled": "Assembled multiblock: right click to open, sneak-right click to separate before editing its components.",
|
||||
"item.sanctuary.steve_wrench": "Steve’s Wrench",
|
||||
"sanctuary.wrench.rotate_help": "Right click: orient · Sneak: reverse · Multiblocks: assemble/separate",
|
||||
"sanctuary.wrench.steve_help": "Left click: select property · Right click: change · Sneak: reverse",
|
||||
"sanctuary.wrench.rotated": "Orientation changed",
|
||||
"sanctuary.wrench.no_rotation": "This block has no vanilla orientation.",
|
||||
"sanctuary.wrench.linked": "This block has linked parts: separate or retract it before rotating.",
|
||||
"sanctuary.wrench.unsupported": "This orientation does not fit the neighbouring blocks or support.",
|
||||
"sanctuary.wrench.steve_power": "Free editing of block states and textures"
|
||||
}
|
||||
|
||||
@@ -2233,5 +2233,13 @@
|
||||
"sanctuary.wrench.selected": "État choisi : %s = %s · clic droit pour modifier",
|
||||
"sanctuary.wrench.changed": "%s = %s",
|
||||
"sanctuary.wrench.texture": "Rotation des textures",
|
||||
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants."
|
||||
"sanctuary.wrench.assembled": "Multibloc assemblé : clic droit pour ouvrir, Maj-clic droit pour dissocier avant de modifier ses composants.",
|
||||
"item.sanctuary.steve_wrench": "Clé à molette de Steve",
|
||||
"sanctuary.wrench.rotate_help": "Clic droit : orienter · Maj : sens inverse · Multiblocs : assembler/dissocier",
|
||||
"sanctuary.wrench.steve_help": "Clic gauche : choisir un état · Clic droit : modifier · Maj : sens inverse",
|
||||
"sanctuary.wrench.rotated": "Orientation modifiée",
|
||||
"sanctuary.wrench.no_rotation": "Ce bloc ne possède pas d’orientation vanilla.",
|
||||
"sanctuary.wrench.linked": "Bloc lié à une autre partie : dissocie-le ou rétracte-le avant de le tourner.",
|
||||
"sanctuary.wrench.unsupported": "Cette orientation ne convient pas aux blocs voisins ou au support.",
|
||||
"sanctuary.wrench.steve_power": "Édition libre des états et des textures"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"parent": "minecraft:item/handheld",
|
||||
"textures": {"layer0": "sanctuary:item/steve_wrench"}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 323 B |
Reference in New Issue
Block a user