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

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
+55
View File
@@ -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();
@@ -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);
}
}
@@ -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<Packet<INetHandlerPlayClient>> 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) {
@@ -55,10 +55,16 @@ public final class RsxWorldBridge {
* world exists, because the player belongs to the world.
*/
public static List<Packet<INetHandlerPlayClient>> join(int entityId, int maxPlayers,
double spawnX, double spawnY, double spawnZ) {
double spawnX, double spawnY, double spawnZ,
int gameMode) {
List<Packet<INetHandlerPlayClient>> packets =
new ArrayList<Packet<INetHandlerPlayClient>>();
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),
@@ -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;
}
@@ -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++) {
@@ -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;