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