Режим игры решает сервер: клиент больше не ломает блоки сам
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user