From 5b81db7c395f4c0fa6bcc50628f3d2498e25d8ec Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Fri, 23 Oct 2026 07:50:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B2=D0=B0=D0=BB=D0=BA=D0=B8=20=D1=83?= =?UTF-8?q?=20=D0=B4=D0=BE=D1=80=D0=BE=D0=B3:=20=D0=B1=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B5=D1=80=D1=8B=D0=B5/=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5/=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B8=20=D1=8F=D1=89=D0=B8=D0=BA=D0=B8=20=D0=B5=D0=B4=D1=8B=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BA=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8,=20=D0=BB?= =?UTF-8?q?=D1=83=D1=82=20=D0=BF=D0=BE=20=D0=B4=D0=B5=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=BC=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=D0=BC?= =?UTF-8?q?,=20=D1=81=D0=B8=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D1=86=D0=B5=20=D0=B4=D0=B0=D1=8E=D1=82=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B4=D0=B2=D0=B8=D0=BD=D1=83=D1=82=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../net/rustnotmc/client/net/RsxBlockMap.java | 17 + .../java/net/rustnotmc/protocol/Blocks.java | 26 +- .../net/rustnotmc/protocol/ItemRegistry.java | 5 +- rsx-server-core/scripts/test.ps1 | 1 + .../net/rustnotmc/server/ClientSession.java | 29 ++ .../rustnotmc/server/world/LodGenerator.java | 8 + .../server/world/WorldGenerator.java | 173 +++++++++ .../net/rustnotmc/server/world/PilesTest.java | 346 ++++++++++++++++++ 8 files changed, 602 insertions(+), 3 deletions(-) create mode 100644 rsx-server-core/src/test/java/net/rustnotmc/server/world/PilesTest.java diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxBlockMap.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxBlockMap.java index 8c2b64a..17352c8 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/net/RsxBlockMap.java +++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxBlockMap.java @@ -48,6 +48,23 @@ public final class RsxBlockMap { net.minecraft.item.EnumDyeColor.GRAY); STATES[net.rustnotmc.protocol.Blocks.SNOW] = Blocks.SNOW.getDefaultState(); + // the junk piles of the roads: grey, blue and red barrels, and the + // wooden crate of food. A barrel is drawn as a barrel's colour clang + // of lantern hardened clay, the crate as a plank blocked square + STATES[net.rustnotmc.protocol.Blocks.BARREL] = + Blocks.STAINED_HARDENED_CLAY.getDefaultState().withProperty( + net.minecraft.block.BlockColored.COLOR, + net.minecraft.item.EnumDyeColor.SILVER); + STATES[net.rustnotmc.protocol.Blocks.BARREL_BLUE] = + Blocks.STAINED_HARDENED_CLAY.getDefaultState().withProperty( + net.minecraft.block.BlockColored.COLOR, + net.minecraft.item.EnumDyeColor.BLUE); + STATES[net.rustnotmc.protocol.Blocks.BARREL_RED] = + Blocks.STAINED_HARDENED_CLAY.getDefaultState().withProperty( + net.minecraft.block.BlockColored.COLOR, + net.minecraft.item.EnumDyeColor.RED); + STATES[net.rustnotmc.protocol.Blocks.CRATE] = + Blocks.PLANKS.getDefaultState(); } private RsxBlockMap() { diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java index 380b1fc..2fe14fc 100644 --- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java +++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java @@ -33,9 +33,17 @@ public final class Blocks { /** The surface of the high and cold places. */ public static final byte SNOW = 9; + /** A barrel that may be broken for what is in it. */ + public static final byte BARREL = 10; + public static final byte BARREL_BLUE = 11; + public static final byte BARREL_RED = 12; + + /** A wooden box of food, standing with the barrels. */ + public static final byte CRATE = 13; + /** Lowest and highest legal block id. */ public static final byte MIN = AIR; - public static final byte MAX = SNOW; + public static final byte MAX = CRATE; private Blocks() { } @@ -52,6 +60,10 @@ public final class Blocks { case LEAVES: return "leaves"; case ASPHALT: return "asphalt"; case SNOW: return "snow"; + case BARREL: return "barrel"; + case BARREL_BLUE: return "barrel_blue"; + case BARREL_RED: return "barrel_red"; + case CRATE: return "crate"; default: return "unknown(" + id + ")"; } } @@ -66,6 +78,18 @@ public final class Blocks { return id == AIR || id == WATER; } + /** + * True when breaking it gives what is inside instead of nothing. + * + * The barrels and the crates are the loot of the roads: a broken one + * empties its content into the hands of whoever broke it, whatever the + * game mode says. + */ + public static boolean isLootable(byte id) { + return id == BARREL || id == BARREL_BLUE || id == BARREL_RED + || id == CRATE; + } + public static boolean isValid(byte id) { return id >= MIN && id <= MAX; } diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java index 2fd835f..ba43ba1 100644 --- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java +++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java @@ -178,8 +178,9 @@ public final class ItemRegistry { define(ItemType.of(HIGH_QUALITY_METAL, "High Quality Metal").rid("hqm")); define(ItemType.of(SULFUR, "Sulfur Ore").rid("sulfur_ore")); define(ItemType.of(SCRAP, "Scrap").rid("scrap")); - // The fragments of the blueprints. The remake does not know them: they - // come out of the loot crates, and the crates are still to come. + // The fragments of the blueprints. They come out of the blue barrels + // of the junk piles on the roads, and the advanced ones out of the + // barrels on the ring road. define(ItemType.of(BASIC_FRAGMENT, "Basic Blueprint Fragment")); define(ItemType.of(ADVANCED_FRAGMENT, "Advanced Blueprint Fragment")); define(ItemType.of(METAL_PIPE, "Metal Pipe").rid("metalpipe")); diff --git a/rsx-server-core/scripts/test.ps1 b/rsx-server-core/scripts/test.ps1 index fec60e1..4a77337 100644 --- a/rsx-server-core/scripts/test.ps1 +++ b/rsx-server-core/scripts/test.ps1 @@ -50,6 +50,7 @@ $suites = @( 'net.rustnotmc.server.world.WorldTest', 'net.rustnotmc.server.world.NodeTest', 'net.rustnotmc.server.world.LodTest', + 'net.rustnotmc.server.world.PilesTest', 'net.rustnotmc.server.item.InventoryTest', 'net.rustnotmc.server.craft.CraftTest', 'net.rustnotmc.server.hitbox.HitboxTest', diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java index 1ae1193..3c2de81 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java @@ -14,6 +14,7 @@ import net.rustnotmc.protocol.ChunkFormat; import net.rustnotmc.protocol.Frame; import net.rustnotmc.protocol.FrameDecoder; import net.rustnotmc.protocol.ItemRegistry; +import net.rustnotmc.protocol.ItemStack; import net.rustnotmc.protocol.ItemType; import net.rustnotmc.protocol.Packet; import net.rustnotmc.protocol.Packets; @@ -797,6 +798,34 @@ public final class ClientSession implements Runnable { send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1)); return; } + byte at; + try { + at = this.server.world().blockAt(dig.x, dig.y, dig.z); + } catch (IOException lost) { + at = Blocks.AIR; // the place has no readable ground there + } + if (Blocks.isLootable(at)) { + // a barrel or a crate of the roads gives up what is in it when it + // is broken, in the survival mode as well as the creative one + if (this.server.world().setBlock(dig.x, dig.y, dig.z, Blocks.AIR)) { + this.server.broadcastBlockChange(dig.x, dig.y, dig.z, Blocks.AIR); + boolean took = false; + for (ItemStack stack : this.server.world().generator() + .pileLoot(at, dig.x, dig.y, dig.z)) { + if (this.player.inventory().add(stack)) { + took = true; + } + } + if (took) { + this.server.log("pile: " + this.account + " emptied a " + + (at == Blocks.CRATE ? "crate" : "barrel") + + " at " + dig.x + "," + dig.y + "," + dig.z); + send(Packets.INVENTORY, this.player.inventory().toPacket()); + } + } + send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1)); + return; + } if (this.player.mayBreakBlocks()) { if (this.server.world().setBlock(dig.x, dig.y, dig.z, Blocks.AIR)) { this.server.broadcastBlockChange(dig.x, dig.y, dig.z, Blocks.AIR); diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/LodGenerator.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/LodGenerator.java index 9ec5ae3..eb376cd 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/world/LodGenerator.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/LodGenerator.java @@ -156,6 +156,14 @@ public final class LodGenerator { return 0x6E6E6E; case Blocks.ASPHALT: return 0x3A3A3A; + case Blocks.BARREL: + return 0x53514C; + case Blocks.BARREL_BLUE: + return 0x3D5C85; + case Blocks.BARREL_RED: + return 0x7B3535; + case Blocks.CRATE: + return 0x8A7040; case Blocks.SNOW: return 0xE9EDEF; case Blocks.WATER: diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java index 60f092f..252d951 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java @@ -2,6 +2,11 @@ package net.rustnotmc.server.world; import net.rustnotmc.protocol.Blocks; import net.rustnotmc.protocol.ChunkFormat; +import net.rustnotmc.protocol.ItemRegistry; +import net.rustnotmc.protocol.ItemStack; + +import java.util.ArrayList; +import java.util.List; /** * Turns a seed and a position into terrain. @@ -58,6 +63,14 @@ public final class WorldGenerator { /** How many blocks of canopy a tree may reach. */ private static final int TREE_REACH = 2; + /** The salts of the junk piles: usually a point stands empty. */ + private static final int SALT_SITE = 21; + private static final int SALT_COUNT = 22; + private static final int SALT_COLOUR = 23; + private static final int SALT_CRATE = 24; + private static final int SALT_LOOT = 25; + private static final int SALT_ITEM = 26; + private final long seed; private final int size; private final int islandRadius; @@ -579,10 +592,170 @@ public final class WorldGenerator { } } } + + // the junk piles, put up on the roads where they belong + layPiles(chunk, blocks, baseX, baseZ); chunk.markGenerated(); } } + /** + * Puts the junk piles of the roads into a chunk. + * + * The heaps stand where the roads meet the network, at every point that + * the road keeps free for them: a quarter of those points carries a + * heap, and the rest stands empty so the roads do not read as one long + * wall of barrels. A heap is a short row of barrels, two to four of them, + * and every third row ends with a crate of food. + * + * Nothing here depends on a random number, the same way the roads + * themselves do not: the same world always has the same heaps, so a + * broken one stays broken and an untouched one stays untouched when the + * chunk is worked out again. + */ + private void layPiles(Chunk chunk, byte[] blocks, int baseX, int baseZ) { + ChunkPos pos = chunk.pos(); + for (int[] point : this.roads.spawnPoints()) { + int sx = point[0]; + int sz = point[1]; + if (sx < baseX - 6 || sx > baseX + ChunkFormat.WIDTH + 6 + || sz < baseZ - 6 || sz > baseZ + ChunkFormat.WIDTH + 6) { + continue; + } + if (!isRoad(sx, sz) || (hash(sx, SALT_SITE, sz) & 0xFFFF) % 3 != 0) { + continue; // this point stands empty + } + int count = 2 + (int) ((hash(sx, SALT_COUNT, sz) & 0xFFFF) % 3); + int[] dir = pileDirection(sx, sz); + for (int j = 0; j < count; j++) { + putPileBlock(blocks, baseX, baseZ, + sx + dir[0] * j, sz + dir[1] * j, barrelOf(sx, sz, j)); + } + if ((hash(sx, SALT_CRATE, sz) & 0xFFFF) % 3 == 0) { + putPileBlock(blocks, baseX, baseZ, + sx + dir[0] * count, sz + dir[1] * count, Blocks.CRATE); + } + } + } + + /** + * The way a row of barrels runs at a point of the roads. + * + * Along the road it stands on, so a heap reads as a little side of the + * road rather than a wall across it. + */ + private int[] pileDirection(int sx, int sz) { + int[] ring = this.roads.onRing(sx, sz); + if (ring == null) { + // the straight roads run along the axes, so a heap laid along the + // axis the road runs in lies along the road + if (Math.abs(sz - this.roads.centerLine(sz)) <= RoadNetwork.HALF_WIDTH + && Math.abs(sx - this.roads.centerLine(sx)) > RoadNetwork.HALF_WIDTH) { + return new int[]{1, 0}; + } + return new int[]{0, 1}; + } + // the ring: a heap lies along the way the ring goes. The tangent is a + // quarter turn from the radius, and it is snapped to the axis it + // mostly goes in + int rx = ring[0] - this.roads.center(); + int rz = ring[1] - this.roads.center(); + int tx = -rz; + int tz = rx; + if (Math.abs(tx) >= Math.abs(tz)) { + return new int[]{Integer.signum(tx), 0}; + } + return new int[]{0, Integer.signum(tz)}; + } + + /** What one barrel of a heap is, grey or blue or red. */ + private byte barrelOf(int sx, int sz, int index) { + int pick = (int) ((hash(sx, SALT_COLOUR + index, sz) & 0xFFFF) % 100); + if (pick < 60) { + return Blocks.BARREL; + } + if (pick < 85) { + return Blocks.BARREL_BLUE; + } + return Blocks.BARREL_RED; + } + + /** Lays one barrel of a heap into a chunk that holds its square. */ + private void putPileBlock(byte[] blocks, int baseX, int baseZ, + int px, int pz, byte block) { + if (!isRoad(px, pz)) { + return; // only a road carries the heaps + } + int height = heightAt(px, pz); + int y = height + 1; + if (height < SEA_LEVEL + 1 || y <= 0 || y >= Chunk.HEIGHT - 1) { + return; + } + int x = px - baseX; + int z = pz - baseZ; + if (x < 0 || x >= ChunkFormat.WIDTH || z < 0 || z >= ChunkFormat.WIDTH) { + return; // the barrel belongs to the chunk next door + } + int index = ChunkFormat.index(x, y, z); + if (blocks[index] == Blocks.AIR) { + blocks[index] = block; + } + } + + /** + * What comes out of a barrel or a crate of the roads. + * + * The loot is a pure function of the place as well, so the same barrel + * always gives the same content: the looting does not depend on who + * counted what when. A grey thing holds scrap with rags, a blue one + * parts with a fragment of a blueprint, a red one fuel, and a crate + * food and drink. The blue barrels of the ring road sometimes hold the + * advanced fragments, the rest of the world gives only the basic ones. + */ + public List pileLoot(byte block, int x, int y, int z) { + List out = new ArrayList(); + long loot = hash(x, y ^ SALT_LOOT, z); + long item = hash(x, SALT_ITEM, z ^ y); + switch (block) { + case Blocks.BARREL: { + out.add(ItemStack.of(ItemRegistry.SCRAP, + 2 + (int) ((loot & 0xFFFF) % 3))); + if ((item & 0xFF) < 64) { + out.add(ItemStack.of(ItemRegistry.BANDANA_MASK + + (int) (((item & 0xFF00) >>> 8) % 16), 1)); + } + break; + } + case Blocks.BARREL_BLUE: { + out.add(ItemStack.of(ItemRegistry.SCRAP, + 1 + (int) ((loot & 0xFFFF) % 2))); + if ((item & 0xFF) < 115) { + out.add(ItemStack.of(ItemRegistry.BASIC_FRAGMENT, 1)); + } + if (this.roads.onRing(x, z) != null && (item & 0xFF00) < 0x2000) { + out.add(ItemStack.of(ItemRegistry.ADVANCED_FRAGMENT, 1)); + } + break; + } + case Blocks.BARREL_RED: { + out.add(ItemStack.of(ItemRegistry.LOW_GRADE_FUEL, + 1 + (int) ((loot & 0xFFFF) % 2))); + if ((item & 0xFF) < 102) { + out.add(ItemStack.of(ItemRegistry.CRUDE_OIL, 1)); + } + break; + } + case Blocks.CRATE: { + out.add(ItemStack.of(ItemRegistry.APPLE + (int) ((item & 0xFFFF) % 17), + 1 + (int) ((loot & 0xFFFF) % 2))); + break; + } + default: + break; + } + return out; + } + /** * Lays the wood of one tree into a column of a chunk. * diff --git a/rsx-server-core/src/test/java/net/rustnotmc/server/world/PilesTest.java b/rsx-server-core/src/test/java/net/rustnotmc/server/world/PilesTest.java new file mode 100644 index 0000000..d0afcf2 --- /dev/null +++ b/rsx-server-core/src/test/java/net/rustnotmc/server/world/PilesTest.java @@ -0,0 +1,346 @@ +package net.rustnotmc.server.world; + +import java.util.List; +import net.rustnotmc.protocol.Blocks; +import net.rustnotmc.protocol.ItemRegistry; +import net.rustnotmc.protocol.ItemStack; + +/** + * Tests the junk piles of the roads. + * + * The heaps are what the roads are for: someone put barrels and crates out + * there, and a player on foot breaks one and carries off what it kept. The + * heaps have to be on the roads, out of the water, and the same in every + * world of the same seed, and the blue ones have to pay in the fragments of + * the blueprints the workbenches want. + */ +public final class PilesTest { + + private static int checks; + private static int failed; + private static final java.util.List failures = new java.util.ArrayList(); + + public static void main(String[] args) throws Exception { + run("the heaps stand on the roads", new Throwing() { + public void go() throws Exception { testPlacement(); } + }); + run("a world has the same heaps again", new Throwing() { + public void go() throws Exception { testDeterminism(); } + }); + run("no road keeps a barrel over the sea", new Throwing() { + public void go() throws Exception { testNoHeapOverTheSea(); } + }); + run("a grey barrel gives scrap and rags", new Throwing() { + public void go() throws Exception { testGrey(); } + }); + run("a blue barrel gives the pieces of a blueprint", new Throwing() { + public void go() throws Exception { testFragments(); } + }); + run("a red barrel gives fuel", new Throwing() { + public void go() throws Exception { testFuel(); } + }); + run("a crate gives food and drink", new Throwing() { + public void go() throws Exception { testCrate(); } + }); + run("a walk over the middle of the island meets heaps", new Throwing() { + public void go() throws Exception { testRealWorldHeaps(); } + }); + + System.out.println(); + for (int i = 0; i < failures.size(); i++) { + System.out.println(" FAIL " + failures.get(i)); + } + System.out.println(); + System.out.println("checks: " + checks + ", failed: " + failed); + if (failed != 0) { + System.out.println("FAILED"); + System.exit(1); + } + System.out.println("OK"); + } + + private interface Throwing { + void go() throws Exception; + } + + private static void run(String name, Throwing body) { + checks++; + int before = failed; + try { + body.go(); + } catch (Throwable t) { + failed++; + failures.add(name + ": threw " + t); + t.printStackTrace(); + return; + } + if (failed == before) { + System.out.println(" ok " + name); + } else { + System.out.println(" FAIL " + name); + } + } + + private static void eq(Object what, Object expected, Object actual) { + if (expected == null ? actual != null : !expected.equals(actual)) { + failed++; + failures.add(what + ": expected " + expected + " but got " + actual); + } + } + + private static void yes(String what, boolean condition) { + if (!condition) { + failed++; + failures.add(what); + } + } + + private static Chunk chunkOf(WorldGenerator generator, int cx, int cz) { + Chunk chunk = new Chunk(new ChunkPos(cx, cz)); + generator.generate(chunk); + return chunk; + } + + /** + * Walks the heaps of the chunks around the points the road network keeps + * for them, and says every barrel stands where a barrel should: on a + * road, on the ground, with nothing in the way over it. + */ + private static void testPlacement() throws Exception { + WorldGenerator generator = new WorldGenerator(1234L); + RoadNetwork roads = new RoadNetwork(generator.size()); + int seen = 0; + int heaps = 0; + java.util.Set visited = new java.util.HashSet(); + for (int[] site : roads.spawnPoints()) { + int chunkX = Math.floorDiv(site[0], 16); + int chunkZ = Math.floorDiv(site[1], 16); + long key = ((long) chunkX << 32) ^ chunkZ; + // the same chunk may hold several points, and generation costs + // real seconds + if (!visited.add(key)) { + continue; + } + Chunk chunk = chunkOf(generator, chunkX, chunkZ); + boolean heap = false; + for (int y = 0; y < Chunk.HEIGHT; y++) { + for (int z = 0; z < Chunk.WIDTH; z++) { + for (int x = 0; x < Chunk.WIDTH; x++) { + byte id = chunk.block(x, y, z); + if (!Blocks.isLootable(id)) { + continue; + } + heap = true; + seen++; + int worldX = chunkX * 16 + x; + int worldZ = chunkZ * 16 + z; + yes("a barrel on the road at " + worldX + "," + worldZ, + generator.isRoad(worldX, worldZ)); + eq("the ground under the barrel at " + worldX + + "," + worldZ, + Integer.valueOf(y - 1), + Integer.valueOf(generator.heightAt(worldX, worldZ))); + eq("nothing wedged over the barrel at " + worldX + + "," + worldZ, + Byte.valueOf(Blocks.AIR), + Byte.valueOf(chunk.block(x, y + 1, z))); + } + } + } + if (heap) { + heaps++; + } + } + yes("the roads carry heaps (" + seen + " barrels seen)", seen > 0); + yes("more heaps of one (" + heaps + " chunks with a heap)", + heaps > 3); + } + + private static void testDeterminism() throws Exception { + Chunk one = chunkOf(new WorldGenerator(1234L), 0, 0); + Chunk two = chunkOf(new WorldGenerator(1234L), 0, 0); + yes("same seed, same chunk, barrels and all", + java.util.Arrays.equals(one.raw(), two.raw())); + } + + /** No barrel stands where the sea is, whatever the network plans. */ + private static void testNoHeapOverTheSea() throws Exception { + WorldGenerator generator = new WorldGenerator(1234L); + RoadNetwork roads = new RoadNetwork(generator.size()); + for (int[] point : roads.spawnPoints()) { + if (!generator.isRoad(point[0], point[1])) { + continue; // the network does not keep a road here + } + yes("the road at " + point[0] + "," + point[1] + " is dry", + generator.heightAt(point[0], point[1]) >= WorldGenerator.SEA_LEVEL + 1); + } + } + + /** One broken barrel: what comes out of it puts a player on their way. */ + private static void greyLoot(WorldGenerator generator, int x, int y, int z) { + List loot = generator.pileLoot(Blocks.BARREL, x, y, z); + yes("a grey barrel gives something", !loot.isEmpty()); + int scrap = 0; + int others = 0; + for (ItemStack stack : loot) { + if (stack.item == ItemRegistry.SCRAP) { + scrap = stack.count; + } else { + others++; + yes("the rags of a heap are worn things (" + stack.item + ")", + stack.item >= ItemRegistry.BANDANA_MASK + && stack.item <= ItemRegistry.ROADSIGN_GLOVES); + eq("a rag is one", Integer.valueOf(1), + Integer.valueOf(stack.count)); + } + } + eq("a grey barrel keeps at most two stacks", Integer.valueOf(1), + Integer.valueOf(loot.size() <= 2 ? 1 : 0)); + yes("the scrap is " + scrap + ", between two and four", + scrap >= 2 && scrap <= 4); + yes("at most one rag with the scrap", others <= 1); + } + + private static void testGrey() throws Exception { + WorldGenerator generator = new WorldGenerator(1234L); + int rags = 0; + for (int w = 0; w < 120; w++) { + List loot = generator.pileLoot(Blocks.BARREL, + w * 37, 70, -w * 11); + for (ItemStack stack : loot) { + if (stack.item != ItemRegistry.SCRAP) { + rags++; + } + } + greyLoot(generator, w * 37, 70, -w * 11); + } + yes("now and then a rag rides with the scrap (" + rags + ")", + rags > 0 && rags < 60); + } + + /** The blue barrels pay in the fragments of the blueprints. */ + private static void testFragments() throws Exception { + WorldGenerator generator = new WorldGenerator(1234L); + RoadNetwork roads = new RoadNetwork(generator.size()); + int radius = roads.ringRadius(); + int basic = 0; + int advanced = 0; + int offRing = 0; + // the straight roads of the grid, all the way out, and the ring road + // itself: the advanced fragments only belong to the ring + for (int w = 0; w < 800; w++) { + int x = w * 7 - 2800; + int z = (w * 13) % 2200; + List loot = generator.pileLoot(Blocks.BARREL_BLUE, x, 70, z); + for (ItemStack stack : loot) { + if (stack.item == ItemRegistry.BASIC_FRAGMENT) { + basic++; + } + if (stack.item == ItemRegistry.ADVANCED_FRAGMENT) { + advanced++; + if (roads.onRing(x, z) == null) { + offRing++; + } + } + } + for (ItemStack stack : generator.pileLoot(Blocks.BARREL_BLUE, x, 70, -z)) { + if (stack.item == ItemRegistry.BASIC_FRAGMENT) { + basic++; + } + } + } + for (int i = 0; i < 720; i++) { + double angle = 2.0D * Math.PI * i / 720; + int x = (int) Math.round(Math.cos(angle) * radius); + int z = (int) Math.round(Math.sin(angle) * radius); + for (ItemStack stack : generator.pileLoot(Blocks.BARREL_BLUE, x, 70, z)) { + if (stack.item == ItemRegistry.ADVANCED_FRAGMENT) { + advanced++; + } + } + } + yes("the blue barrels give the basic fragments (" + basic + ")", + basic > 200); + yes("the advanced fragments come out now and then (" + advanced + ")", + advanced > 0); + eq("none of them off the ring", Integer.valueOf(0), + Integer.valueOf(offRing)); + } + + /** The red barrels give fuel, the crude oil with it. */ + private static void testFuel() throws Exception { + WorldGenerator generator = new WorldGenerator(1234L); + boolean fuel = false; + int oils = 0; + for (int w = 0; w < 120; w++) { + List loot = generator.pileLoot(Blocks.BARREL_RED, + -w * 19, 70, w * 29); + yes("a red barrel gives something", !loot.isEmpty()); + for (ItemStack stack : loot) { + if (stack.item == ItemRegistry.LOW_GRADE_FUEL) { + fuel = true; + yes("the fuel is " + stack.count + ", one or two", + stack.count >= 1 && stack.count <= 2); + } + if (stack.item == ItemRegistry.CRUDE_OIL) { + oils++; + eq("a jug of oil is one", Integer.valueOf(1), + Integer.valueOf(stack.count)); + } + } + yes("a red barrel keeps at most two stacks at " + w, + loot.size() <= 2); + } + yes("the red barrels give fuel", fuel); + yes("some of them give crude oil (" + oils + ")", + oils > 0 && oils < 60); + } + + /** A crate feeds a player: the thing inside is something to eat. */ + private static void testCrate() throws Exception { + WorldGenerator generator = new WorldGenerator(1234L); + int foods = 0; + boolean drink = false; + for (int w = 0; w < 170; w++) { + List loot = generator.pileLoot(Blocks.CRATE, + w * 23, 70, -w * 31); + eq("a crate gives one stack", Integer.valueOf(1), + Integer.valueOf(loot.size())); + int item = loot.get(0).item; + yes("a crate keeps food or drink (" + item + ")", + item >= ItemRegistry.APPLE && item <= ItemRegistry.MUSHROOM); + foods++; + if (item >= ItemRegistry.SMALL_WATER_BOTTLE + && item <= ItemRegistry.WATER_JUG) { + drink = true; + } + } + yes("the crates are counted (" + foods + ")", foods > 0); + yes("now and then a crate keeps drink", drink); + } + + /** + * The heaps a player meets on an actual walk: the chunks over the middle + * of the island carry roads, the roads carry heaps, and a heap carries + * barrels in a row of two to four. + */ + private static void testRealWorldHeaps() throws Exception { + int heaps = 0; + for (int cx = -2; cx <= 2; cx++) { + for (int cz = -2; cz <= 2; cz++) { + Chunk chunk = chunkOf(new WorldGenerator(1234L), cx, cz); + for (int y = 0; y < Chunk.HEIGHT; y++) { + for (int z = 0; z < Chunk.WIDTH; z++) { + for (int x = 0; x < Chunk.WIDTH; x++) { + if (Blocks.isLootable(chunk.block(x, y, z))) { + heaps++; + } + } + } + } + } + } + yes("the roads about the middle carry heaps (" + heaps + " barrels)", + heaps >= 2); + } +}