From aee8b6112831c8058534737af1c94bee58dca06f Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:03:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B6=D0=B8=D0=BC=20=D0=B8=D0=B3?= =?UTF-8?q?=D1=80=D1=8B=20=D1=80=D0=B5=D1=88=D0=B0=D0=B5=D1=82=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80:=20=D0=BA=D0=BB=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BB=D0=BE=D0=BC=D0=B0=D0=B5=D1=82=20=D0=B1=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B8=20=D1=81=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game-fork/patches/decompiler-fixes.txt | 55 ++++++++++++++ .../rustnotmc/client/net/RsxClientMode.java | 64 ++++++++++++++++ .../client/net/RsxNetworkManager.java | 41 ++++++---- .../rustnotmc/client/net/RsxWorldBridge.java | 10 ++- .../java/net/rustnotmc/protocol/Packets.java | 51 ++++++++++++- .../net/rustnotmc/protocol/ProtocolTest.java | 14 +++- .../net/rustnotmc/server/ClientSession.java | 20 ++++- .../java/net/rustnotmc/server/Player.java | 26 +++++++ .../java/net/rustnotmc/server/Server.java | 20 +++++ .../java/net/rustnotmc/server/ServerMain.java | 8 +- .../net/rustnotmc/server/world/Chunk.java | 6 +- .../net/rustnotmc/server/world/World.java | 4 +- .../java/net/rustnotmc/server/PlayClient.java | 7 ++ .../java/net/rustnotmc/server/PlayTest.java | 74 ++++++++++++++++--- 14 files changed, 367 insertions(+), 33 deletions(-) create mode 100644 game-fork/src-overlay/net/rustnotmc/client/net/RsxClientMode.java diff --git a/game-fork/patches/decompiler-fixes.txt b/game-fork/patches/decompiler-fixes.txt index 1804c9e..4797c20 100644 --- a/game-fork/patches/decompiler-fixes.txt +++ b/game-fork/patches/decompiler-fixes.txt @@ -1192,3 +1192,58 @@ import org.lwjgl.opengl.ContextCapabilities; net.rustnotmc.client.RsxBootstrap.logGl(); + +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE + this.sendClickBlockToController(this.currentScreen == null && this.gameSettings.keyBindAttack.isKeyDown() && this.inGameHasFocus); +--- AFTER + // Breaking happens on the press, in clickMouse, and not on every frame + // the button is held. The game sends a dig packet for each of those + // frames, so a held button used to break a whole line of blocks across + // the world instead of the one under the crosshair. + this.sendClickBlockToController(false); + +=== FILE net/minecraft/client/Minecraft.java +--- BEFORE + private void clickMouse() { + if (this.leftClickCounter <= 0) { + if (this.objectMouseOver == null) { +--- AFTER + private void clickMouse() { + if (this.leftClickCounter <= 0) { + net.rustnotmc.client.net.RsxClientMode.onLeftClick(this); + if (this.objectMouseOver == null) { + +=== FILE net/minecraft/client/multiplayer/PlayerControllerMP.java +--- BEFORE + public boolean clickBlock(BlockPos blockpos, EnumFacing enumfacing) { + if (this.currentGameType.hasLimitedInteractions()) { +--- AFTER + public boolean clickBlock(BlockPos blockpos, EnumFacing enumfacing) { + // in a survival world the player breaks nothing, so the game is not + // allowed to start breaking either + if (!net.rustnotmc.client.net.RsxClientMode.canBreakBlocks()) { + return false; + } + + if (this.currentGameType.hasLimitedInteractions()) { + +=== FILE net/minecraft/client/renderer/EntityRenderer.java +--- BEFORE + private boolean isDrawBlockOutline() { + if (!this.drawBlockOutline) { + return false; + } else { + Entity entity = this.mc.getRenderViewEntity(); +--- AFTER + private boolean isDrawBlockOutline() { + if (!this.drawBlockOutline) { + return false; + } else { + // a block that cannot be broken is not pointed out either: the + // highlight says "here" and then nothing happens + if (!net.rustnotmc.client.net.RsxClientMode.canBreakBlocks()) { + return false; + } + + Entity entity = this.mc.getRenderViewEntity(); diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxClientMode.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxClientMode.java new file mode 100644 index 0000000..1f28fa7 --- /dev/null +++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxClientMode.java @@ -0,0 +1,64 @@ +package net.rustnotmc.client.net; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.RayTraceResult; +import net.rustnotmc.protocol.Packets; + +/** + * What the player is allowed to do to the world, and how the game is told. + * + * In survival the player breaks nothing. Not the blocks, and not the outline + * around the block they are looking at either: a highlight that leads nowhere + * is worse than no highlight, because it says "here" and then nothing happens. + * + * In creative the player breaks the one block they are looking at, and only on + * a deliberate click. The game sends a packet for every frame the button is + * held, which is how a held button ends up breaking a line of blocks across + * the world instead of the one under the crosshair. + */ +public final class RsxClientMode { + + private static volatile int mode = Packets.GAMEMODE_SURVIVAL; + + private RsxClientMode() { + } + + /** The mode the server says the player is in. */ + public static int mode() { + return mode; + } + + /** The server said the mode changed. */ + public static void set(int newMode) { + mode = newMode; + } + + /** True when the player may break blocks. */ + public static boolean canBreakBlocks() { + return mode == Packets.GAMEMODE_CREATIVE; + } + + /** + * The player clicked, and wants the block they are looking at broken. + * + * The game has already worked out what the crosshair is on, so that is what + * gets broken. The server checks that the player was close enough to it. + */ + public static void onLeftClick(Minecraft minecraft) { + if (!canBreakBlocks()) { + return; + } + RayTraceResult hit = minecraft.objectMouseOver; + if (hit == null || hit.typeOfHit != RayTraceResult.Type.BLOCK) { + return; + } + RsxNetworkManager manager = RsxNetworkManager.active(); + if (manager == null) { + return; + } + BlockPos pos = hit.getBlockPos(); + manager.sendBlockChange(pos.getX(), pos.getY(), pos.getZ(), + net.rustnotmc.protocol.Blocks.AIR); + } +} diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java index 385e19f..6158337 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java +++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java @@ -19,7 +19,6 @@ import net.minecraft.network.INetHandler; import net.minecraft.network.NetworkManager; import net.minecraft.network.Packet; import net.minecraft.network.play.INetHandlerPlayClient;import net.minecraft.network.play.client.CPacketPlayer; -import net.minecraft.network.play.client.CPacketPlayerDigging; import net.minecraft.util.text.ITextComponent; import net.minecraft.util.text.TextComponentTranslation; import net.rustnotmc.protocol.Buf; @@ -245,6 +244,11 @@ public final class RsxNetworkManager extends NetworkManager { case Packets.WORLD_JOIN: onWorldJoin(frame); return; + case Packets.SET_GAME_MODE: { + Packets.GameMode gameMode = Packets.GameMode.read(new Buf(frame.body())); + RsxClientMode.set(gameMode.mode); + return; + } default: onPlay(frame); } @@ -295,8 +299,9 @@ public final class RsxNetworkManager extends NetworkManager { if (this.play == null) { return; } + RsxClientMode.set(world.gameMode); List> packets = RsxWorldBridge.join(world.entityId, 200, - world.spawnX, world.spawnY, world.spawnZ); + world.spawnX, world.spawnY, world.spawnZ, world.gameMode); for (int i = 0; i < packets.size(); i++) { packets.get(i).processPacket(this.play); } @@ -364,31 +369,37 @@ public final class RsxNetworkManager extends NetworkManager { * The game is sending something; the server does not speak the game's * language, so it is translated. * - * Most of what the game sends has no counterpart on the server and is - * dropped. The two that matter are where the player is and what the - * player is breaking. + * The dig packets are the one thing that is deliberately not translated. + * The game sends one for every frame the button is held, so a held button + * becomes a stream of block changes and a line of blocks disappears across + * the world instead of the one under the crosshair. The client breaks + * blocks on its own click instead, with the game's own raycast. */ @Override public void sendPacket(Packet packet) { - if (packet instanceof CPacketPlayer) { + if (packet instanceof CPacketPlayer.Position + || packet instanceof CPacketPlayer.Rotation + || packet instanceof CPacketPlayer.PositionRotation) { CPacketPlayer move = (CPacketPlayer) packet; send(Packets.PLAYER_MOVE, new Packets.PlayerMove( move.getX(0.0D), move.getY(0.0D), move.getZ(0.0D), move.getYaw(0.0F), move.getPitch(0.0F), move.isOnGround())); return; } - if (packet instanceof CPacketPlayerDigging) { - CPacketPlayerDigging dig = (CPacketPlayerDigging) packet; - if (dig.getAction() == CPacketPlayerDigging.Action.START_DESTROY_BLOCK) { - net.minecraft.util.math.BlockPos pos = dig.getPosition(); - send(Packets.BLOCK_CHANGE, new Packets.BlockChange(pos.getX(), pos.getY(), - pos.getZ(), net.rustnotmc.protocol.Blocks.AIR)); - } - return; - } // everything else the game sends has no place in our protocol yet } + /** + * Asks the server to break a block. + * + * The server decides whether it happens: the player has to be close + * enough to it, and in a survival world the player does not break blocks + * at all. + */ + public void sendBlockChange(int x, int y, int z, byte id) { + send(Packets.BLOCK_CHANGE, new Packets.BlockChange(x, y, z, id)); + } + // ------------------------------------------------------------- teardown private void lost(String reason) { diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java index e6ae7c8..5b503af 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java +++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java @@ -55,10 +55,16 @@ public final class RsxWorldBridge { * world exists, because the player belongs to the world. */ public static List> join(int entityId, int maxPlayers, - double spawnX, double spawnY, double spawnZ) { + double spawnX, double spawnY, double spawnZ, + int gameMode) { List> packets = new ArrayList>(); - packets.add(new SPacketJoinGame(entityId, GameType.SURVIVAL, false, 0, + // the game type in the join packet is what makes the game apply its own + // rules: in creative the player may break blocks, in survival it may + // not, and the outline follows + GameType type = gameMode == net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE + ? GameType.CREATIVE : GameType.SURVIVAL; + packets.add(new SPacketJoinGame(entityId, type, false, 0, EnumDifficulty.NORMAL, maxPlayers, WorldType.DEFAULT, false)); packets.add(new SPacketSpawnPosition( new BlockPos((int) Math.floor(spawnX), (int) Math.floor(spawnY), diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java index f63a054..6d51a31 100644 --- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java +++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java @@ -102,6 +102,22 @@ public final class Packets { */ public static final int LAUNCHER_AUTH = 0x1A; + /** + * The player's game mode, changed while the player is in the world. + * + * The mode arrives with the world join and can be changed afterwards, the + * way a server changes it. What a player may do about blocks follows from + * it: in survival the player breaks nothing, in creative the player + * breaks what they look at. + */ + public static final int SET_GAME_MODE = 0x1B; + + /** Survival: the player cannot break blocks at all. */ + public static final int GAMEMODE_SURVIVAL = 0; + + /** Creative: the player breaks the block they are looking at. */ + public static final int GAMEMODE_CREATIVE = 1; + public static final int DISCONNECT = 0x7F; /** The client introducing itself, and naming the account it wants. */ @@ -465,8 +481,11 @@ public final class Packets { public final int viewDistance; public final int worldHeight; + /** The game mode the player starts in. */ + public final int gameMode; + public WorldJoin(int entityId, long seed, double spawnX, double spawnY, - double spawnZ, int viewDistance, int worldHeight) { + double spawnZ, int viewDistance, int worldHeight, int gameMode) { this.entityId = entityId; this.seed = seed; this.spawnX = spawnX; @@ -474,6 +493,7 @@ public final class Packets { this.spawnZ = spawnZ; this.viewDistance = viewDistance; this.worldHeight = worldHeight; + this.gameMode = gameMode; } public Buf write() { @@ -483,6 +503,7 @@ public final class Packets { buf.writePosition(this.spawnX, this.spawnY, this.spawnZ); buf.writeVarInt(this.viewDistance); buf.writeVarInt(this.worldHeight); + buf.writeVarInt(this.gameMode); return buf; } @@ -491,7 +512,7 @@ public final class Packets { long seed = in.readVarLong(); double[] spawn = in.readPosition(); return new WorldJoin(entityId, seed, spawn[0], spawn[1], spawn[2], - in.readVarInt(), in.readVarInt()); + in.readVarInt(), in.readVarInt(), in.readVarInt()); } } @@ -750,6 +771,31 @@ public final class Packets { } } + /** The game mode of one player. */ + public static final class GameMode implements Packet { + + public final int mode; + + public GameMode(int mode) { + this.mode = mode; + } + + /** True when the player may break blocks. */ + public boolean creative() { + return this.mode == GAMEMODE_CREATIVE; + } + + public Buf write() { + Buf buf = new Buf(8); + buf.writeVarInt(this.mode); + return buf; + } + + public static GameMode read(Buf in) { + return new GameMode(in.readVarInt()); + } + } + /** * The launcher's session, as the game server sees it. * @@ -807,6 +853,7 @@ public final class Packets { map.put(Integer.valueOf(ENTITY_SPAWN), "EntitySpawn"); map.put(Integer.valueOf(ENTITY_REMOVE), "EntityRemove"); map.put(Integer.valueOf(LAUNCHER_AUTH), "LauncherAuth"); + map.put(Integer.valueOf(SET_GAME_MODE), "GameMode"); map.put(Integer.valueOf(DISCONNECT), "Disconnect"); return map; } diff --git a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java index 819c9eb..8f14dae 100644 --- a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java +++ b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java @@ -417,7 +417,8 @@ public final class ProtocolTest { yes("a fresh join carries no position", !freshBack.hasPosition); Packets.WorldJoin world = - new Packets.WorldJoin(7, 1234567890123L, 0.5D, 64.0D, 0.5D, 8, 256); + new Packets.WorldJoin(7, 1234567890123L, 0.5D, 64.0D, 0.5D, 8, 256, + Packets.GAMEMODE_CREATIVE); Packets.WorldJoin worldBack = Packets.WorldJoin.read(new Buf(world.write().toByteArray())); eq("entity id", Integer.valueOf(7), Integer.valueOf(worldBack.entityId)); @@ -425,6 +426,16 @@ public final class ProtocolTest { eq("view distance", Integer.valueOf(8), Integer.valueOf(worldBack.viewDistance)); eq("world height", Integer.valueOf(256), Integer.valueOf(worldBack.worldHeight)); eq("spawn x", Double.valueOf(0.5D), Double.valueOf(worldBack.spawnX)); + eq("game mode", Integer.valueOf(Packets.GAMEMODE_CREATIVE), + Integer.valueOf(worldBack.gameMode)); + + Packets.GameMode mode = new Packets.GameMode(Packets.GAMEMODE_SURVIVAL); + Packets.GameMode modeBack = Packets.GameMode.read(new Buf(mode.write().toByteArray())); + eq("survival comes back", Integer.valueOf(Packets.GAMEMODE_SURVIVAL), + Integer.valueOf(modeBack.mode)); + yes("survival cannot break blocks", !modeBack.creative()); + yes("creative can break blocks", + new Packets.GameMode(Packets.GAMEMODE_CREATIVE).creative()); Packets.BlockChange change = new Packets.BlockChange(-100, 70, 250, Blocks.STONE); Packets.BlockChange changeBack = @@ -528,6 +539,7 @@ public final class ProtocolTest { Packets.WORLD_JOIN, Packets.CHUNK_DATA, Packets.BLOCK_CHANGE, Packets.PLAYER_MOVE, Packets.ENTITY_MOVE, Packets.PING, Packets.PONG, Packets.ENTITY_SPAWN, Packets.ENTITY_REMOVE, Packets.LAUNCHER_AUTH, + Packets.SET_GAME_MODE, Packets.DISCONNECT, }; for (int i = 0; i < ids.length; i++) { 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 312787a..3e622ad 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 @@ -439,10 +439,28 @@ public final class ClientSession implements Runnable { this.server.world().seed(), spawn[0], spawn[1], spawn[2], this.player.viewDistance(), - ChunkFormat.HEIGHT)); + ChunkFormat.HEIGHT, + this.player.gameMode())); this.player.streamChunks(this.server.world()); } + /** + * The player's game mode changed while they were in the world. + * + * The server decides. The client is told, because a client that thinks it + * may break blocks in a survival world will keep trying, and a client that + * thinks it may not will not try in a creative one. + */ + void onGameMode(int mode) { + if (this.player == null) { + return; + } + this.player.gameMode(mode); + send(Packets.SET_GAME_MODE, new Packets.GameMode(mode)); + this.server.log("game mode for " + this.account + ": " + + (mode == Packets.GAMEMODE_CREATIVE ? "creative" : "survival")); + } + /** * A block the player wants to change. * diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java b/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java index 23f358e..086dde0 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java @@ -46,6 +46,9 @@ public final class Player { private final Set unreadableChunks = new HashSet(); private int chunksSent; + /** Survival or creative. The server decides, and the client is told. */ + private volatile int gameMode = Packets.GAMEMODE_SURVIVAL; + Player(ClientSession session, int playerId, int entityId, String account, double[] spawn) { this.session = session; @@ -214,6 +217,26 @@ public final class Player { // -------------------------------------------------------------- building + /** + * The player's game mode. + * + * In survival the player breaks nothing. That is the server's decision and + * the client is told about it, because a client that thinks it may break + * blocks will keep trying and the world will keep saying no. + */ + public int gameMode() { + return this.gameMode; + } + + public void gameMode(int mode) { + this.gameMode = mode; + } + + /** True when the player may break blocks. */ + public boolean mayBreakBlocks() { + return this.gameMode == Packets.GAMEMODE_CREATIVE; + } + /** * Changes a block, if the player is allowed to. * @@ -222,6 +245,9 @@ public final class Player { * second, a client can rewrite the world from the other side of it. */ public boolean changeBlock(World world, int x, int y, int z, byte id) { + if (!mayBreakBlocks()) { + return false; + } if (!Blocks.isValid(id)) { return false; } diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java index 9df65c1..3cc305b 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java @@ -64,6 +64,7 @@ public final class Server { private volatile long tickNanos; private volatile String tickBacklog = ""; private volatile int viewDistance = Player.DEFAULT_VIEW_DISTANCE; + private volatile int gameMode = Packets.GAMEMODE_SURVIVAL; public Server(String name, String motd, int capacity, int port) { this(name, motd, capacity, port, new World(0L, ChunkStorage.memoryOnly())); @@ -134,6 +135,24 @@ public final class Server { this.viewDistance = chunks; } + /** + * The game mode new players start in. + * + * Survival means the player breaks nothing: no blocks, and no highlight on + * the block they are looking at either. Creative means the player breaks + * the block they are looking at. + */ + public int gameMode() { + return this.gameMode; + } + + public void gameMode(int mode) { + this.gameMode = mode; + for (Player player : this.players.values()) { + player.session().onGameMode(mode); + } + } + public AccountStore accounts() { return this.accounts; } @@ -331,6 +350,7 @@ public final class Server { Player player = new Player(session, session.playerId(), session.entityId(), session.account(), this.world.spawn()); player.viewDistance(this.viewDistance); + player.gameMode(this.gameMode); this.players.put(Integer.valueOf(player.playerId()), player); session.player(player); diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java b/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java index da7f2b1..9c05060 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java @@ -14,7 +14,7 @@ import net.rustnotmc.server.world.World; * * run-server.ps1 [--port N] [--accounts path] [--name text] [--motd text] * [--world path] [--seed N] [--launcher-api url] - * [--allow-local-accounts] + * [--allow-local-accounts] [--gamemode survival|creative] * run-server.ps1 --register Player [--password text] * * Registration is an operator action and not something a client can do over the @@ -42,6 +42,7 @@ public final class ServerMain { long seed = 0L; String launcherApi = null; boolean allowLocalAccounts = false; + String gameMode = "survival"; String register = null; String password = null; @@ -62,6 +63,8 @@ public final class ServerMain { launcherApi = value; } else if ("--allow-local-accounts".equals(flag)) { allowLocalAccounts = true; + } else if ("--gamemode".equals(flag)) { + gameMode = value; } else if ("--name".equals(flag)) { name = value; } else if ("--motd".equals(flag)) { @@ -75,6 +78,9 @@ public final class ServerMain { World world = new World(seed, new ChunkStorage(worldDir)); Server server = new Server(name, motd, capacity, port, world); + server.gameMode("creative".equalsIgnoreCase(gameMode) + ? net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE + : net.rustnotmc.protocol.Packets.GAMEMODE_SURVIVAL); if (launcherApi != null) { // the launcher is where accounts live, so it is the way in. The diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/Chunk.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/Chunk.java index 1fc289a..d3191dd 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/world/Chunk.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/Chunk.java @@ -46,7 +46,10 @@ public final class Chunk { public Chunk(ChunkPos pos) { this.pos = pos; this.blocks = new byte[VOLUME]; - this.lastUsed = System.currentTimeMillis(); + // a tick count, like every other use of it. The wall clock here would + // make a chunk that nobody touched look infinitely old, and it would + // never be dropped + this.lastUsed = 0L; } public ChunkPos pos() { @@ -96,6 +99,7 @@ public final class Chunk { this.dirty = false; } + /** The tick this chunk was last used in. */ public long lastUsed() { return this.lastUsed; } diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/World.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/World.java index 451c33e..8fac476 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/world/World.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/World.java @@ -119,7 +119,9 @@ public final class World { } public byte blockAt(int x, int y, int z) throws IOException { - return chunkAt(ChunkPos.ofBlock(x, z)).block(x & 15, y, z & 15); + Chunk chunk = chunkAt(ChunkPos.ofBlock(x, z)); + byte value = chunk.block(x & 15, y, z & 15); + return value; } /** diff --git a/rsx-server-core/src/test/java/net/rustnotmc/server/PlayClient.java b/rsx-server-core/src/test/java/net/rustnotmc/server/PlayClient.java index 5adb538..7be0281 100644 --- a/rsx-server-core/src/test/java/net/rustnotmc/server/PlayClient.java +++ b/rsx-server-core/src/test/java/net/rustnotmc/server/PlayClient.java @@ -147,6 +147,13 @@ final class PlayClient { : Packets.AuthAck.read(new net.rustnotmc.protocol.Buf(frame.body())); } + /** Waits for the server to say the game mode changed. */ + Packets.GameMode awaitGameMode(long millis) throws IOException { + Frame frame = await(Packets.SET_GAME_MODE, millis); + return frame == null ? null + : Packets.GameMode.read(new net.rustnotmc.protocol.Buf(frame.body())); + } + /** The launcher's session, offered instead of a password. */ void launcherAuth(String account, String uuid, String token) throws IOException { send(Packets.LAUNCHER_AUTH, new Packets.LauncherAuth(account, uuid, token)); diff --git a/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java b/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java index 02e6d84..bfbcc7a 100644 --- a/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java +++ b/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java @@ -54,10 +54,13 @@ public final class PlayTest { run("a ping is answered", new Throwing() { public void go() throws Exception { testPing(work); } }); + run("a survival player cannot break blocks", new Throwing() { + public void go() throws Exception { testSurvivalCannotBreak(work); } + }); run("the world survives a restart", new Throwing() { public void go() throws Exception { testRestart(work); } }); - run("the launcher decides who gets in", new Throwing() { + run("the launcher's session decides who gets in", new Throwing() { public void go() throws Exception { testLauncherAuth(work); } }); run("a launcher session that is not ours is refused", new Throwing() { @@ -117,11 +120,16 @@ public final class PlayTest { // ------------------------------------------------------------- the tests private static Server boot(Path work, String account) throws IOException { + return boot(work, account, net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE); + } + + private static Server boot(Path work, String account, int gameMode) throws IOException { Server server = new Server("RustNotMC play", "hello", 20, 0, new World(1234L, ChunkStorage.memoryOnly())); // a small view, so the tests do not spend their time generating a // hundred chunks to prove that a dozen arrive server.viewDistance(2); + server.gameMode(gameMode); Path accounts = work.resolve("play-accounts-" + account + ".txt"); Files.deleteIfExists(accounts); server.loadAccounts(accounts); @@ -370,6 +378,60 @@ public final class PlayTest { * same folder and the same seed has to find the broken block still broken * and the rest of the terrain untouched. */ + /** + * A survival player breaks nothing. + * + * The server decides, not the client: a client that says it may break + * blocks is not believed. + */ + /** A block, or a byte that is not a block if it cannot be read. */ + private static byte blockAt(World world, int x, int y, int z) { + try { + return world.blockAt(x, y, z); + } catch (IOException unreadable) { + return (byte) -1; + } + } + + private static void testSurvivalCannotBreak(Path work) throws Exception { + Server server = boot(work, "Survivor", + net.rustnotmc.protocol.Packets.GAMEMODE_SURVIVAL); + try { + PlayClient client = new PlayClient("127.0.0.1", server.port(), 5000); + client.signIn("Survivor", "password1"); + client.send(Packets.JOIN_WORLD, Packets.JoinWorld.fresh()); + client.await(Packets.WORLD_JOIN, 5000L); + client.awaitMany(Packets.CHUNK_DATA, 25, 15000L); + + double[] spawn = server.world().spawn(); + int under = (int) spawn[1] - 1; + byte before = server.world().blockAt(0, under, 0); + yes("there is something to break", before != net.rustnotmc.protocol.Blocks.AIR); + + client.send(Packets.BLOCK_CHANGE, + new Packets.BlockChange(0, under, 0, net.rustnotmc.protocol.Blocks.AIR)); + Thread.sleep(300L); + eq("the block is still there", Byte.valueOf(before), + Byte.valueOf(server.world().blockAt(0, under, 0))); + + // and the mode can be changed while the player is in the world + server.gameMode(net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE); + Packets.GameMode mode = client.awaitGameMode(3000L); + yes("the client was told", mode != null + && mode.mode == net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE); + + client.send(Packets.BLOCK_CHANGE, + new Packets.BlockChange(0, under, 0, net.rustnotmc.protocol.Blocks.AIR)); + waitFor(() -> blockAt(server.world(), 0, under, 0) + == net.rustnotmc.protocol.Blocks.AIR, 3000L); + eq("now the block is gone", Byte.valueOf(net.rustnotmc.protocol.Blocks.AIR), + Byte.valueOf(server.world().blockAt(0, under, 0))); + client.close(); + } finally { + server.stop(); + } + } + private static void testRestart(Path work) throws Exception { Path dir = work.resolve("play-world"); deleteTree(dir); @@ -378,6 +440,8 @@ public final class PlayTest { Server server = new Server("RustNotMC play", "hello", 20, 0, new World(1234L, new ChunkStorage(dir))); server.viewDistance(2); + // the player breaks a block here, and a survival player breaks nothing + server.gameMode(net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE); Path accounts = work.resolve("play-accounts-restart.txt"); Files.deleteIfExists(accounts); server.loadAccounts(accounts); @@ -548,14 +612,6 @@ public final class PlayTest { return server; } - /** A block, or a byte that is not a block if it cannot be read. */ - private static byte blockAt(World world, int x, int y, int z) { - try { - return world.blockAt(x, y, z); - } catch (IOException unreadable) { - return (byte) -1; - } - } private static void waitFor(java.util.function.BooleanSupplier condition, long millis) throws InterruptedException { long end = System.currentTimeMillis() + millis;