diff --git a/README.md b/README.md index 52afaaf..3cd1dc7 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,12 @@ La [vision complète](docs/vision.md) conserve les intentions ; le L’économie, les claims et les dimensions décrits dans la vision restent à implémenter. -La version **beta.122** adapte la sélection et les collisions des sculptures -à leurs voxels réels et supprime leurs ombres de blocs pleins. Les modèles -restent alignés au centre ou au bord choisi, dans les quatre orientations. -Le [ticket de livraison](docs/sculpture-shape-light-beta122.md) détaille la -forme physique, les règles de lumière et les vérifications réussies. La release -est publiée et l’instance **Sanctuary Beta** est synchronisée, sauvegardes et -réglages conservés. +La version **beta.123** limite la clé dorée aux orientations natives et confie +l'édition libre à la **Clé à molette de Steve**, utilisable aussi en survie si +elle est récupérée, avec sa texture bleue fournie par le créateur. +Le [ticket de livraison](docs/wrench-safety-beta123.md) détaille les protections, +les commandes et les vérifications client et archives réussies. La publication +et la synchronisation de l’instance sont préparées. 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**. Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées. @@ -37,6 +36,13 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après l’étude de Xaero et des alternatives libres. +## beta.123 — Clé dorée et clé de Steve + +Orientations cohérentes, inventaires et eau préservés ; clé de Steve pour +l’édition libre, utilisable aussi en survie. [Contrat et vérifications](docs/wrench-safety-beta123.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-beta.123.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.123/Sanctuary-Test-beta.123.mrpack). + ## beta.122 — Sculptures : forme et lumière Collisions et sélection sur les voxels réels, creux traversables, lumière sans diff --git a/docs/wrench-safety-beta123.md b/docs/wrench-safety-beta123.md new file mode 100644 index 0000000..7afa630 --- /dev/null +++ b/docs/wrench-safety-beta123.md @@ -0,0 +1,113 @@ +# TOOL-02 — Clé dorée et clé de Steve — beta.123 + +Contrat du 17 septembre 2026, branche `codex/wrench-safety-beta123`. +Ce ticket remplace le contrat de la clé dorée de beta.120 : l'édition libre +reste disponible dans un objet distinct, la **Clé à molette de Steve**. +Le créateur précise que la clé de Steve reste utilisable en survie dès qu’un +joueur la récupère, sans condition opérateur. Elle n’a pas de recette. + +## Clé dorée : orientations natives + +L'identifiant `sanctuary:golden_wrench`, sa recette et sa texture restent stables. +Clic droit parcourt l'orientation native ; Maj parcourt en sens inverse. +Clic gauche rappelle les gestes, sans casser le bloc ni sélectionner un état. +Les familles de propriétés autorisées sont les orientations horizontales, +les six directions, les trois axes, la rotation native des panneaux et +l'orientation native des blocs qui la proposent. La règle porte sur les +propriétés Minecraft, sans catalogue à maintenir pour chaque type de bloc. + +L'eau, la moitié d'un escalier, les inventaires, les états de croissance, +les quantités, la puissance et les autres propriétés ne sont jamais proposées. +Aucune transformation indépendante des textures n'est créée par la clé dorée. +Les textures suivent seulement le modèle orienté par Minecraft. +Les connexions et formes sont recalculées à partir des voisins réels, avec les +mises à jour natives actives. Une orientation privée de support est refusée +sans casser le bloc. L'eau déjà contenue dans un bloc est conservée. + +Les coffres doubles, portes, lits, étagères reliées, pistons sortis et leurs +pièces mobiles ne peuvent pas être tournés individuellement. Ils restent dans +leur état courant et une indication explique le refus. Il faut replacer les +éléments concernés ou rétracter le piston ; ce ticket ne déplace pas un +ensemble lié. Les coffres simples conservent leur entité et leur inventaire. + +L'assemblage, l'ouverture et la dissociation des Métablis, Fûts, Fourneaux et +super pistons restent prioritaires avec les deux clés. Les parties techniques +d'un multibloc Sanctuary assemblé ne sont pas éditées individuellement. + +## Clé de Steve + +Nouvel identifiant `sanctuary:steve_wrench`, dans la section créative des outils, +sans recette de survie. La possession de cet objet donne accès à ses pouvoirs, +y compris en survie sans commandes et sans statut opérateur, dans les deux +mains. Les restrictions natives de construction, aventure, spectateur, portée, +protection et verrouillage restent appliquées côté serveur. + +Les gestes de beta.120 sont conservés : clic gauche choisit la propriété, +clic droit la modifie, Maj inverse le parcours. Tous les états natifs et la +rotation indépendante des textures y restent volontairement accessibles. +La clé de Steve est donc volontairement capable de produire des états non +naturels, y compris pour le joueur qui la récupère en survie. + +La texture finale est le PNG 16 × 16 bleu fourni par le créateur dans ce ticket, +copié sans transformation dans `assets/sanctuary/textures/item/steve_wrench.png`. +SHA-256 : `0084c41e1f46958903075e21866facd97d85e03fb8e06742e25b2ab254956fa3`. +Le modèle reste un objet Minecraft 2D tenu en main, avec rareté épique et reflet. +La proposition imagegen intermédiaire n'est pas livrée. Les noms, messages et +indications des deux outils existent en français et en anglais. + +## Sauvegardes et ressources + +Aucune migration, modification de génération ni parcours des mondes existants. +Les transformations de texture précédemment sauvegardées sont conservées ; +la clé de Steve permet de les modifier. Les états non naturels déjà présents +ne sont pas automatiquement réécrits. Les clés dorées existantes reçoivent +immédiatement les nouvelles restrictions par leur identifiant inchangé. +Le template personnel inclut le nouvel objet à sa prochaine création ; une +copie personnelle existante n'est jamais écrasée. + +## Vérifications + +Le parcours `Wrench123ClientChecks` passe en **1 min 12 s**, client macOS et +serveur intégré, Minecraft 26.3, dans des mondes plats jetables de graines +123 et 120. Il reprend le parcours avancé de beta.120 sur la clé de Steve : + +- Vrais clics dorés répétés sur escalier, coffre et terre : pas de nouvelle eau, + pas de sélection libre, pas de faux coffre double ni de rotation de texture. +- Rotation inverse, eau existante conservée, formes recalculées selon les voisins, + trois axes de bûches et six orientations des pistons isolés. +- Coffre double avec ses deux inventaires intacts ; portes, lits, pistons sortis + et pièces mobiles protégés ; torche murale sans support laissée intacte. +- Vrais clics Steve en survie sans commandes : états et textures accessibles, + dans les deux mains. Créatif accessible aussi sans commandes ; restrictions + aventure/spectateur/portée conservées. +- **2 522 propriétés natives** toujours réversibles pour Steve, UV réellement + tournés sans déplacer la géométrie, rechargement des ressources, inventaires + et sauvegarde/reconnexion conservés. +- Assemblage et dissociation des quatre familles avec la clé dorée par vrais + clics ; le PNG de Steve apparaît en main et dans la barre rapide. + +Marqueurs `WRENCH123_SAFE_PASS`, `WRENCH123_PASS`, `WRENCH120_PASS` dans +`build/wrench123-client.log`. Captures relues dans `build/wrench123-evidence/`. +Pas de validation Windows, de client distant ou de shader tiers. + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +passe en **2 min 19 s**, 126 tâches (103 exécutées, 23 à jour). +Les sources des JAR correspondent aux sources du dépôt. La comparaison avec +beta.122 limite les dix entrées de production modifiées aux classes des clés, +aux libellés et aux ressources du nouvel objet. Les autres ressources, +notamment les sculptures et la texture dorée, sont identiques. +Les packs normal/Test contiennent le même JAR Sanctuary, sans sauvegarde ni +fixture de test. Le template ZIP correspond exactement aux ressources du JAR +et à ses ressources embarquées, dont le PNG bleu du créateur. + +- Pack normal : 10 270 954 octets, SHA-256 + `a7401dbef1bd1b888da2686368943eddc82d0d43e20449330c4849b4dff93e5f`. +- Pack Test : 10 289 877 octets, SHA-256 + `c57cc6fc47f887b90850473e51407b610a6c5639c35fc2b78dc9ddb74f795b80`. +- JAR Sanctuary : SHA-256 + `23e1a81c793a048c6375a0da07eb0bbe1a18f833a9ceb61a3c1dc5f1a3e65a30`. + +Reçu local : `build/wrench123-artifact.json`. + +Le GameTest dédié reste exclu conformément au refus antérieur de son EULA. +Aucun monde personnel n'est ouvert par les essais. diff --git a/gradle.properties b/gradle.properties index 73d739a..595e6e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,8 +9,8 @@ loom_version=1.17.20 fabric_api_version=0.160.5+26.3 # Release counter: beta.001, .002, .003, ... (see docs/versioning.md). -mod_version=beta.122 -pack_version=beta.122 +mod_version=beta.123 +pack_version=beta.123 resource_pack_version=beta.121 maven_group=fr.koka.sanctuary jei_version=30.32.0-sanctuary.3 diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index 51378ab..72261a7 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -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') } diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench120ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench120ClientChecks.java index 60a6b55..0729e8d 100644 --- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench120ClientChecks.java +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench120ClientChecks.java @@ -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)); diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench123ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench123ClientChecks.java new file mode 100644 index 0000000..2f65af8 --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Wrench123ClientChecks.java @@ -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);} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/GoldenWrench.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/GoldenWrench.java index e2982b6..80fa384 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/GoldenWrench.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/GoldenWrench.java @@ -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> 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 { diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/Multiblocks.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/Multiblocks.java index 458fb03..3c89869 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/Multiblocks.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/Multiblocks.java @@ -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 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); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/WrenchRotation.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/WrenchRotation.java new file mode 100644 index 0000000..8751743 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/multiblock/WrenchRotation.java @@ -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> 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(){} +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/items/steve_wrench.json b/mods/sanctuary/src/main/resources/assets/sanctuary/items/steve_wrench.json new file mode 100644 index 0000000..4317af3 --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/items/steve_wrench.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "sanctuary:item/steve_wrench" + } +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index bda1824..4b6a4e3 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -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" } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index 705a2f9..51895ac 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -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" } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/models/item/steve_wrench.json b/mods/sanctuary/src/main/resources/assets/sanctuary/models/item/steve_wrench.json new file mode 100644 index 0000000..097e014 --- /dev/null +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/models/item/steve_wrench.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:item/handheld", + "textures": {"layer0": "sanctuary:item/steve_wrench"} +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/steve_wrench.png b/mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/steve_wrench.png new file mode 100644 index 0000000..6f24fe4 Binary files /dev/null and b/mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/steve_wrench.png differ diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 7819422..71fc9a5 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.122" +version = "beta.123" pack-format = "packwiz:1.1.0" [index]