From 00179e30ef0b45ef98378bc164a7810552dd1bdd Mon Sep 17 00:00:00 2001 From: koka Date: Thu, 17 Sep 2026 16:00:52 +0200 Subject: [PATCH] Use one cached bounding box per sculpture (beta.124) --- README.md | 20 +++-- docs/sculpture-bounds-beta124.md | 79 +++++++++++++++++++ gradle.properties | 4 +- .../client/Sculpture122ClientChecks.java | 37 ++++++--- .../sanctuary/plans/SculptureGeometry.java | 20 ++--- packwiz/pack.toml | 2 +- 6 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 docs/sculpture-bounds-beta124.md diff --git a/README.md b/README.md index 68f005a..06e52e0 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.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 release est -publiée et l’instance **Sanctuary Beta** est synchronisée, sauvegardes et réglages -conservés. +La version **beta.124** utilise une seule boîte ajustée au volume de chaque +sculpture, commune à la sélection et aux collisions. Les détails ne créent plus +de petites marches ; l'orientation, l'ancrage et la lumière restent cohérents. +Le [ticket de livraison](docs/sculpture-bounds-beta124.md) détaille le calcul +mis en cache et les vérifications en jeu 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.124 — Une boîte par sculpture + +Sélection et collisions simplifiées, dimensions ajustées et lumière conservée. +[Contrat et vérifications](docs/sculpture-bounds-beta124.md) · +[Pack normal](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.124/Sanctuary-beta.124.mrpack) · +[Pack de test](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.124/Sanctuary-Test-beta.124.mrpack). + ## beta.123 — Clé dorée et clé de Steve Orientations cohérentes, inventaires et eau préservés ; clé de Steve pour diff --git a/docs/sculpture-bounds-beta124.md b/docs/sculpture-bounds-beta124.md new file mode 100644 index 0000000..a67da90 --- /dev/null +++ b/docs/sculpture-bounds-beta124.md @@ -0,0 +1,79 @@ +# SCULPT-10 — Une boîte par sculpture — beta.124 + +Contrat du 17 septembre 2026, branche `codex/sculpture-bounds-beta124`. +Le créateur choisit **une seule boîte pour la sélection et les collisions**. +Ce ticket remplace la forme physique détaillée de beta.122. + +La boîte englobe exactement les dimensions occupées par la sculpture, après +orientation et ancrage au centre ou au bord. Elle suit toujours la grille 1/16, +y compris les tailles impaires et les modèles comportant des marges vides. +Les trous et petits détails restent visibles mais n'ont plus leurs propres +surfaces de collision : on ne grimpe plus sur leurs petites marches. Une arche +se sélectionne et bloque désormais sur tout son volume englobant. + +La pose utilise la même boîte avant la création de l'entité de bloc : elle +refuse une intersection avec le joueur, y compris dans les trous visuels. +Les petites sculptures laissent libre le reste du bloc autour de leur boîte. +La forme reste mise en cache par modèle et état ; elle ne se recalcule qu'après +un changement de modèle, d'orientation ou d'ancrage. Les requêtes physiques et +le contour partagent cette même forme native. + +Le calcul parcourt les cellules une seule fois, sans créer de copies tournées +ni de grille de collisions 16³. Le contour émet toujours douze arêtes. +Il s'agit d'une simplification du travail géométrique ; aucun gain de FPS global +n'est annoncé sans mesure sur une scène représentative. + +Le rendu des sculptures et les règles de lumière de beta.122 restent inchangés : +pas d'occlusion de cube plein, pas d'émission artificielle de lumière. Aucun +identifiant, format d'item ou de sauvegarde ne change. Les anciennes sculptures +utilisent la nouvelle forme à partir de leurs données existantes, sans réécriture +des mondes ni régénération de chunks. L'atelier et les clés restent inchangés. + +## Vérifications + +Le parcours `Sculpture122ClientChecks`, adapté au nouveau contrat, passe en +**39 s** sur Minecraft 26.3, client macOS et serveur intégré jetable de graine 122. + +- Sélection native de tout le volume, y compris le trou visuel d'une arche ; + contour rectangulaire relu dans la capture. +- Même boîte physique et de sélection sur client et serveur ; intérieur rempli, + dimensions exactes sans profondeur artificielle de bloc entier. +- **80 cas** : boîte unique comparée aux limites du maillage réellement émis, + douze arêtes exactement, même objet réutilisé lors des requêtes répétées. + Arche, marges vides, cube de 4 096 cellules et voxel unique, dans les quatre + orientations et cinq ancrages. +- **5 120 placements** de rendu toujours alignés sur la grille entière. +- Pose refusée dans le joueur, y compris entre deux piliers séparés ; petite + sculpture autorisée à côté du joueur, hors de sa boîte physique. +- Cache renouvelé après changement d'orientation/ancrage ou de modèle ; + conservation après sauvegarde/reconnexion. +- **75 sculptures superposées** : ciel à 15 ; toit de pierre témoin sombre ; + lumière de bloc transmise avec atténuation normale ; pièce close à 0. + +Marqueurs dans `build/sculpture124-client.log`, notamment +`SCULPTURE124_BOUNDS_PASS 80`, plus les contrôles de régression de beta.122. +Captures relues dans `build/sculpture124-evidence/`. Aucun essai Windows, +client distant ou shader tiers ; le coût des boîtes est simplifié sans benchmark +FPS global. + +`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest` +passe en **2 min 9 s**, 126 tâches (110 exécutées, 16 à jour). +Les sources des JAR correspondent aux sources du dépôt. La comparaison avec +beta.123 limite les différences de production aux trois classes de +`SculptureGeometry` ; ressources, clés, interfaces, données et éclairage restent +identiques. Les packs normal/Test contiennent le même JAR, sans sauvegarde +ni fixture. Le template personnel conserve exactement les ressources de beta.123. + +- Pack normal : 10 271 059 octets, SHA-256 + `2075e4ac7a7e7e2effcd82586d46769607ca1a67e8d9110b000fa378d1bc5506`. +- Pack Test : 10 289 985 octets, SHA-256 + `88263e6896590ea9694b18084666e7234e934064e8d7d596d34866a68cc5ea23`. +- JAR Sanctuary : SHA-256 + `b3969b1a3738b8a5d332b2083db0dd722900239f73035c335b738f70328d43f1`. + +Reçu local : `build/sculpture124-artifact.json`. Les packs normal/Test et le +template sont aussi copiés, avec vérification octet pour octet, dans le dossier +habituel `sanctuary-beta/build/`. + +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 595e6e0..793edeb 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.123 -pack_version=beta.123 +mod_version=beta.124 +pack_version=beta.124 resource_pack_version=beta.121 maven_group=fr.koka.sanctuary jei_version=30.32.0-sanctuary.3 diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Sculpture122ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Sculpture122ClientChecks.java index 09dd992..5959334 100644 --- a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Sculpture122ClientChecks.java +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Sculpture122ClientChecks.java @@ -29,7 +29,7 @@ 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. */ +/** Single-box selection/collision bounds and light propagation compared with the real 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); @@ -60,12 +60,12 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest { 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"); + c.takeScreenshot("sculpture124-single-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"); + c.runOnClient(m->check(m.hitResult instanceof BlockHitResult hit&&hit.getBlockPos().equals(TARGET),"Native cursor selects the sculpture bounds even through its visual hole")); + c.takeScreenshot("sculpture124-select-volume"); // 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); @@ -97,7 +97,7 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest { 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"); + System.out.println("SCULPTURE122_PASS: native placement, one-box outline including holes, single-box physical collisions, shared render geometry, cache refresh, light propagation and save/reconnect"); } } private static void placementOverlap(ServerPlayer player){ @@ -107,20 +107,25 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest { 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"); + player.teleportTo(3.5,1,.5);var pillars=new ArrayList();for(int y=0;y<16;y++)for(int x:new int[]{0,15})pillars.add(new VoxelSculpture.Cell(x,y,0,-1)); + var hollow=new VoxelSculpture(MODEL,"Two pillars",16,16,1,pillars);stack=StatuaryEntity.item(hollow);player.getInventory().setItem(player.getInventory().getSelectedSlot(),stack); + var hollowBlocked=((BlockItem)Statuary.ITEM).place(new BlockPlaceContext(player,InteractionHand.MAIN_HAND,stack,hit)); + check(!hollowBlocked.consumesAction()&&level.getBlockState(pos).isAir(),"Placement also rejects a player inside the enclosing box between separated pillars");player.teleportTo(3.75,1,.5); var postCells=new ArrayList();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"); + System.out.println("SCULPTURE122_PLACEMENT_PASS native item rejects occupied bounds including visual holes, allows space outside the box, 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(shape==collision&&shape.toAabbs().size()==1,"Selection and collision share one cached bounding box");check(shape==state.getShape(level,pos),"Selection queries reuse the cached box");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.BLOCK,"Outline ray selects the whole sculpture volume"); 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.clip(new ClipContext(new Vec3(.5,1.5,2),new Vec3(.5,1.5,-.1),ClipContext.Block.COLLIDER,ClipContext.Fluid.NONE,CollisionContext.empty())).getType()==HitResult.Type.BLOCK,"Physical collision uses the enclosing volume, including visual holes"); + check(!level.noCollision(new AABB(.35,1.2,.35,.65,1.7,.65)),"Native collision query fills the inner volume instead of following tiny details"); 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"); @@ -132,13 +137,19 @@ public final class Sculpture122ClientChecks implements FabricClientGameTest { 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 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(boxes.size()==1&&volume>=model.cells().size()/4096d,"One box encloses all occupied cells"); + var outline=entity.shape(state);check(outline==entity.shape(state),"Repeated outline queries reuse the cached box"); + check(outline.toAabbs().size()==1&&outline.bounds().equals(shape.bounds()),"One outline box matches exact rendered extents for every orientation and anchor"); + int[] edges={0};outline.forAllEdges((x1,y1,z1,x2,y2,z2)->edges[0]++);check(edges[0]==12,"Native outline emits only the 12 enclosing edges, independent of voxel count"); 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");} + double minX=1,minY=1,minZ=1,maxX=0,maxY=0,maxZ=0; + for(var q:render.mesh){minX=Math.min(minX,q.x());minY=Math.min(minY,q.y());minZ=Math.min(minZ,q.z());maxX=Math.max(maxX,q.x()+1/16d);maxY=Math.max(maxY,q.y()+1/16d);maxZ=Math.max(maxZ,q.z()+1/16d);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"); + } + check(shape.bounds().equals(new AABB(minX,minY,minZ,maxX,maxY,maxZ)),"Single collision box exactly matches visible mesh extents"); 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"); + System.out.println("SCULPTURE124_BOUNDS_PASS "+cases+" cached single boxes agree with exact renderer extents; 12 outline edges each"); + System.out.println("SCULPTURE122_GEOMETRY_PASS "+cases+" native bounding boxes agree with emitted mesh extents, plus cached 12-edge selection bounds for 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); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/plans/SculptureGeometry.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/plans/SculptureGeometry.java index 7e946f6..d9de4c8 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/plans/SculptureGeometry.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/plans/SculptureGeometry.java @@ -1,11 +1,10 @@ package fr.koka.sanctuary.plans; import net.minecraft.core.Direction; -import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; -import net.minecraft.world.phys.shapes.CubeVoxelShape; +import net.minecraft.world.level.block.Block; import net.minecraft.world.phys.shapes.VoxelShape; -/** One integer placement shared by rendering, picking and server-side collisions. */ +/** Integer placement shared by rendering and tight, single-box selection/collision bounds. */ public final class SculptureGeometry { public record Placement(VoxelSculpture sculpture, int offsetX, int offsetZ) {} @@ -23,13 +22,16 @@ public final class SculptureGeometry { anchor.originZ(maxZ - minZ) - minZ); } + /** Tight integer bounds, without rotating/copying cells or constructing a voxel grid. */ public static VoxelShape shape(VoxelSculpture sculpture, Direction facing, SculptureAnchor anchor) { - var placed = place(sculpture, facing, anchor); - var grid = new BitSetDiscreteVoxelShape(16, 16, 16); - for (var cell : placed.sculpture().cells()) { - grid.fill(cell.x() + placed.offsetX(), cell.y(), cell.z() + placed.offsetZ()); + int minX=16,minY=16,minZ=16,maxX=0,maxY=0,maxZ=0; + for(var cell:sculpture.cells()) { + minX=Math.min(minX,cell.x()); minY=Math.min(minY,cell.y()); minZ=Math.min(minZ,cell.z()); + maxX=Math.max(maxX,cell.x()+1); maxY=Math.max(maxY,cell.y()+1); maxZ=Math.max(maxZ,cell.z()+1); } - // Build the bounded native voxel grid directly, without thousands of box unions. - return new CubeVoxelShape(grid); + int width=maxX-minX,depth=maxZ-minZ; + if(facing.getAxis()==Direction.Axis.X){int swap=width;width=depth;depth=swap;} + int x=anchor.originX(width),z=anchor.originZ(depth); + return Block.box(x,minY,z,x+width,maxY,z+depth); } } diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 71fc9a5..a86b392 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.123" +version = "beta.124" pack-format = "packwiz:1.1.0" [index]