Режим игры решает сервер: клиент больше не ломает блоки сам

This commit is contained in:
sasheg dev
2026-10-20 23:03:54 +03:00
parent 1c93d9ae9a
commit aee8b61128
14 changed files with 367 additions and 33 deletions
@@ -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.
*
@@ -46,6 +46,9 @@ public final class Player {
private final Set<Long> unreadableChunks = new HashSet<Long>();
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;
}
@@ -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);
@@ -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
@@ -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;
}
@@ -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;
}
/**
@@ -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));
@@ -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;