Шаг A: предметы и инвентарь
Фундамент предметов по данным плана (срез 11.2025) и серверный инвентарь. В протоколе (общий словарь для клиента и сервера): - ToolClass, ResourceClass - что чем ломается и что где лежит. - ItemType - определение предмета: класс инструмента, урон по четырем частям тела, прочность, Gather Rates по каждому классу ресурса (сколько снимает удар, разброс, сколько уходит прочности инструмента), bench - требуемый уровень верстака, knownByDefault - открыт ли рецепт с начала игры, admin - только ли админы сервера. - ItemRegistry - таблица предметов. Числа из плана, каждое в одном месте, чтобы игра и план не разъехались. - Recipe - ингредиенты, количество, базовое время крафта. - ItemStack - тип, количество, остаток прочности и свои данные на стеке (как теги у плагина в Paper): стек - это то, что путешествует. Пакеты: Inventory (весь инвентарь при входе) и SlotChange (один слот). На сервере: Inventory - 30 слотов, хотбар 6, сервер владеет инвентарем и сообщает клиенту, а не спрашивает. Стартовый набор - камень в первом слоте и в руке. Тесты: предметы закреплены числами из плана (камень бьёт в голову на 20, снимает 10 дерева и 5 руды; кирка не рубит дерево; меч не ломает вообще), инвентарь переживает провод, сумка наполняется и опустошается, полная сумма говорит об этом. Замечание: прогрессия крафта в Rust упирается в лутбоксы - скрап падает только из них, а чертёж требует найденного предмета. Это отдельный вопрос (п. 5.2 плана), он не блокирует предметы и инвентарь.
This commit is contained in:
@@ -47,6 +47,7 @@ Say "compiled $($mainFiles.Count) main + $($testFiles.Count) test files"
|
||||
$suites = @(
|
||||
'net.rustnotmc.server.ServerTest',
|
||||
'net.rustnotmc.server.world.WorldTest',
|
||||
'net.rustnotmc.server.item.InventoryTest',
|
||||
'net.rustnotmc.server.PlayTest'
|
||||
)
|
||||
foreach ($suite in $suites) {
|
||||
|
||||
@@ -441,9 +441,22 @@ public final class ClientSession implements Runnable {
|
||||
this.player.viewDistance(),
|
||||
ChunkFormat.HEIGHT,
|
||||
this.player.gameMode()));
|
||||
// what the player is carrying comes with the world: the server owns
|
||||
// the bag, so the client is told what is in it
|
||||
send(Packets.INVENTORY, this.player.inventory().toPacket());
|
||||
this.player.streamChunks(this.server.world());
|
||||
}
|
||||
|
||||
/** Tells the client that one slot of the bag changed. */
|
||||
void sendSlot(int slot) {
|
||||
if (this.player == null) {
|
||||
return;
|
||||
}
|
||||
net.rustnotmc.protocol.ItemStack stack = this.player.inventory().get(slot);
|
||||
send(Packets.INVENTORY_SLOT, new Packets.SlotChange(slot, stack.item,
|
||||
stack.count, stack.condition));
|
||||
}
|
||||
|
||||
/**
|
||||
* The player's game mode changed while they were in the world.
|
||||
*
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import net.rustnotmc.protocol.Blocks;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
import net.rustnotmc.server.world.Chunk;
|
||||
import net.rustnotmc.server.world.ChunkPos;
|
||||
import net.rustnotmc.server.world.World;
|
||||
@@ -49,8 +50,17 @@ public final class Player {
|
||||
/** Survival or creative. The server decides, and the client is told. */
|
||||
private volatile int gameMode = Packets.GAMEMODE_SURVIVAL;
|
||||
|
||||
/** What the player is carrying. The server owns it. */
|
||||
private final Inventory inventory;
|
||||
|
||||
Player(ClientSession session, int playerId, int entityId, String account,
|
||||
double[] spawn) {
|
||||
this(session, playerId, entityId, account, spawn,
|
||||
Inventory.startingKit());
|
||||
}
|
||||
|
||||
Player(ClientSession session, int playerId, int entityId, String account,
|
||||
double[] spawn, Inventory inventory) {
|
||||
this.session = session;
|
||||
this.playerId = playerId;
|
||||
this.entityId = entityId;
|
||||
@@ -58,7 +68,12 @@ public final class Player {
|
||||
this.x = spawn[0];
|
||||
this.y = spawn[1];
|
||||
this.z = spawn[2];
|
||||
this.viewDistance = DEFAULT_VIEW_DISTANCE;
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
/** What the player is carrying. */
|
||||
public Inventory inventory() {
|
||||
return this.inventory;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- getters
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package net.rustnotmc.server.item;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
|
||||
/**
|
||||
* What a player is carrying.
|
||||
*
|
||||
* The server is the only one who decides what is in the slots. The client is
|
||||
* told, and when it is told it draws what it was given. A client that is free
|
||||
* to decide what it holds is a client that can be made to hold anything.
|
||||
*
|
||||
* The layout is the one from the original game: the first slots are the hotbar
|
||||
* and the rest is the bag behind it.
|
||||
*/
|
||||
public final class Inventory {
|
||||
|
||||
/** How many slots the hotbar has. */
|
||||
public static final int HOTBAR = 6;
|
||||
|
||||
/** How many slots there are in all. */
|
||||
public static final int SLOTS = 30;
|
||||
|
||||
private final ItemStack[] slots = new ItemStack[SLOTS];
|
||||
private int held;
|
||||
|
||||
public Inventory() {
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
this.slots[i] = ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The starting kit.
|
||||
*
|
||||
* A rock and a torch, the way the original game hands them out. The rock
|
||||
* is the first thing the player has and the only thing they can use at the
|
||||
* start.
|
||||
*/
|
||||
public static Inventory startingKit() {
|
||||
Inventory inventory = new Inventory();
|
||||
inventory.set(0, ItemStack.of(ItemRegistry.ROCK, 1));
|
||||
inventory.held = 0;
|
||||
return inventory;
|
||||
}
|
||||
|
||||
/** The stack in a slot, never null. */
|
||||
public ItemStack get(int slot) {
|
||||
if (slot < 0 || slot >= SLOTS) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
ItemStack stack = this.slots[slot];
|
||||
return stack == null ? ItemStack.EMPTY : stack;
|
||||
}
|
||||
|
||||
/** Puts a stack in a slot. */
|
||||
public void set(int slot, ItemStack stack) {
|
||||
if (slot < 0 || slot >= SLOTS) {
|
||||
return;
|
||||
}
|
||||
this.slots[slot] = stack == null ? ItemStack.EMPTY : stack;
|
||||
}
|
||||
|
||||
/** The slot the player is holding. */
|
||||
public int held() {
|
||||
return this.held;
|
||||
}
|
||||
|
||||
/** Holds a different slot. */
|
||||
public void held(int slot) {
|
||||
if (slot >= 0 && slot < HOTBAR) {
|
||||
this.held = slot;
|
||||
}
|
||||
}
|
||||
|
||||
/** The stack in the hand. */
|
||||
public ItemStack inHand() {
|
||||
return get(this.held);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a stack to the bag, and to the hotbar first.
|
||||
*
|
||||
* Returns true when it fit. A bag that is full simply does not take it,
|
||||
* which is the same thing every game does.
|
||||
*/
|
||||
public boolean add(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
ItemStack there = get(i);
|
||||
if (there.item == stack.item && there.count < 64) {
|
||||
int room = 64 - there.count;
|
||||
int moved = Math.min(room, stack.count);
|
||||
there.count += moved;
|
||||
stack.count -= moved;
|
||||
if (stack.count <= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
if (get(i).isEmpty()) {
|
||||
set(i, stack);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** How many of an item are in the bag. */
|
||||
public int count(int item) {
|
||||
int total = 0;
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
ItemStack stack = get(i);
|
||||
if (stack.item == item) {
|
||||
total += stack.count;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an item out of the bag.
|
||||
*
|
||||
* Returns how many were actually there, which is not always what was
|
||||
* asked for.
|
||||
*/
|
||||
public int take(int item, int amount) {
|
||||
if (amount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int taken = 0;
|
||||
for (int i = 0; i < SLOTS && taken < amount; i++) {
|
||||
ItemStack stack = get(i);
|
||||
if (stack.item != item || stack.count <= 0) {
|
||||
continue;
|
||||
}
|
||||
int moved = Math.min(stack.count, amount - taken);
|
||||
stack.count -= moved;
|
||||
taken += moved;
|
||||
if (stack.count <= 0) {
|
||||
set(i, ItemStack.EMPTY);
|
||||
}
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
/** The bag as the client is told about it. */
|
||||
public Packets.Inventory toPacket() {
|
||||
Packets.Slot[] out = new Packets.Slot[SLOTS];
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
out[i] = Packets.Slot.of(get(i));
|
||||
}
|
||||
return new Packets.Inventory(out, this.held);
|
||||
}
|
||||
|
||||
/** The slots that are not empty, for a look at what a player carries. */
|
||||
public List<ItemStack> contents() {
|
||||
List<ItemStack> out = new ArrayList<ItemStack>();
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
ItemStack stack = get(i);
|
||||
if (!stack.isEmpty()) {
|
||||
out.add(stack);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,13 @@ final class PlayClient {
|
||||
: Packets.GameMode.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
||||
}
|
||||
|
||||
/** Waits for the server to say what is in the bag. */
|
||||
Packets.Inventory awaitInventory(long millis) throws IOException {
|
||||
Frame frame = await(Packets.INVENTORY, millis);
|
||||
return frame == null ? null
|
||||
: Packets.Inventory.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));
|
||||
|
||||
@@ -57,6 +57,9 @@ public final class PlayTest {
|
||||
run("a survival player cannot break blocks", new Throwing() {
|
||||
public void go() throws Exception { testSurvivalCannotBreak(work); }
|
||||
});
|
||||
run("the player starts with a rock in his hand", new Throwing() {
|
||||
public void go() throws Exception { testStartingKit(work); }
|
||||
});
|
||||
run("the world survives a restart", new Throwing() {
|
||||
public void go() throws Exception { testRestart(work); }
|
||||
});
|
||||
@@ -432,6 +435,52 @@ public final class PlayTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The player starts with a rock, and the client is told about it.
|
||||
*
|
||||
* The server owns the bag, so the client is not asked what is in it.
|
||||
*/
|
||||
private static void testStartingKit(Path work) throws Exception {
|
||||
Server server = boot(work, "Kit");
|
||||
try {
|
||||
PlayClient client = new PlayClient("127.0.0.1", server.port(), 5000);
|
||||
yes("signed in", client.signIn("Kit", "password1") != null);
|
||||
client.send(Packets.JOIN_WORLD, Packets.JoinWorld.fresh());
|
||||
client.await(Packets.WORLD_JOIN, 5000L);
|
||||
|
||||
Packets.Inventory inventory = client.awaitInventory(5000L);
|
||||
yes("the client was told what is in the bag", inventory != null);
|
||||
if (inventory == null) {
|
||||
return;
|
||||
}
|
||||
eq("the bag has slots", Integer.valueOf(30),
|
||||
Integer.valueOf(inventory.slots.length));
|
||||
eq("the first slot holds the rock", Integer.valueOf(net.rustnotmc.protocol.ItemRegistry.ROCK),
|
||||
Integer.valueOf(inventory.slots[0].item));
|
||||
eq("one of it", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.slots[0].count));
|
||||
eq("and it is in his hand", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.held));
|
||||
|
||||
Player player = null;
|
||||
for (Player p : server.players().values()) {
|
||||
player = p;
|
||||
}
|
||||
yes("the server knows the player", player != null);
|
||||
if (player != null) {
|
||||
eq("the server agrees about the rock",
|
||||
Integer.valueOf(net.rustnotmc.protocol.ItemRegistry.ROCK),
|
||||
Integer.valueOf(player.inventory().get(0).item));
|
||||
eq("and about what is in the hand",
|
||||
Integer.valueOf(net.rustnotmc.protocol.ItemRegistry.ROCK),
|
||||
Integer.valueOf(player.inventory().inHand().item));
|
||||
}
|
||||
client.close();
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static void testRestart(Path work) throws Exception {
|
||||
Path dir = work.resolve("play-world");
|
||||
deleteTree(dir);
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package net.rustnotmc.server.item;
|
||||
|
||||
import java.util.List;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.protocol.ResourceClass;
|
||||
|
||||
/**
|
||||
* Tests the bag.
|
||||
*
|
||||
* The bag belongs to the server and nobody else, so these are about the server
|
||||
* being right about what a player has and what a player can do with it.
|
||||
*/
|
||||
public final class InventoryTest {
|
||||
|
||||
private static int checks;
|
||||
private static int failed;
|
||||
private static final java.util.List<String> failures = new java.util.ArrayList<String>();
|
||||
|
||||
public static void main(String[] args) {
|
||||
run("the starting kit is a rock in the hand", new Throwing() {
|
||||
public void go() { testStartingKit(); }
|
||||
});
|
||||
run("a bag holds what is put in it", new Throwing() {
|
||||
public void go() { testAdd(); }
|
||||
});
|
||||
run("a bag gives things back", new Throwing() {
|
||||
public void go() { testTake(); }
|
||||
});
|
||||
run("a full bag says so", new Throwing() {
|
||||
public void go() { testFull(); }
|
||||
});
|
||||
run("the client is told what is in the bag", new Throwing() {
|
||||
public void go() { testPacket(); }
|
||||
});
|
||||
run("a tool knows how much life it has left", new Throwing() {
|
||||
public void go() { testCondition(); }
|
||||
});
|
||||
|
||||
System.out.println();
|
||||
for (int i = 0; i < failures.size(); i++) {
|
||||
System.out.println(" FAIL " + failures.get(i));
|
||||
}
|
||||
System.out.println();
|
||||
System.out.println("checks: " + checks + ", failed: " + failed);
|
||||
if (failed != 0) {
|
||||
System.out.println("FAILED");
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("OK");
|
||||
}
|
||||
|
||||
private interface Throwing {
|
||||
void go() throws Exception;
|
||||
}
|
||||
|
||||
private static void run(String name, Throwing body) {
|
||||
checks++;
|
||||
int before = failed;
|
||||
try {
|
||||
body.go();
|
||||
} catch (Throwable t) {
|
||||
failed++;
|
||||
failures.add(name + ": threw " + t);
|
||||
t.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (failed == before) {
|
||||
System.out.println(" ok " + name);
|
||||
}
|
||||
}
|
||||
|
||||
private static void yes(String what, boolean condition) {
|
||||
if (!condition) {
|
||||
failed++;
|
||||
failures.add(what);
|
||||
}
|
||||
}
|
||||
|
||||
private static void eq(Object what, Object expected, Object actual) {
|
||||
if (expected == null ? actual != null : !expected.equals(actual)) {
|
||||
failed++;
|
||||
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testStartingKit() {
|
||||
Inventory inventory = Inventory.startingKit();
|
||||
eq("the first slot holds the rock", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(inventory.get(0).item));
|
||||
eq("one of it", Integer.valueOf(1), Integer.valueOf(inventory.get(0).count));
|
||||
eq("and it is in the hand", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.held()));
|
||||
eq("the hand holds the rock", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(inventory.inHand().item));
|
||||
eq("nothing else", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.contents().size()));
|
||||
}
|
||||
|
||||
private static void testAdd() {
|
||||
Inventory inventory = new Inventory();
|
||||
yes("the rock goes in", inventory.add(ItemStack.of(ItemRegistry.ROCK, 1)));
|
||||
eq("and it is there", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.ROCK)));
|
||||
|
||||
// the same kind of thing piles up
|
||||
yes("more wood goes in", inventory.add(ItemStack.of(ItemRegistry.WOOD, 10)));
|
||||
yes("and more", inventory.add(ItemStack.of(ItemRegistry.WOOD, 5)));
|
||||
eq("wood piles up", Integer.valueOf(15),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
}
|
||||
|
||||
private static void testTake() {
|
||||
Inventory inventory = Inventory.startingKit();
|
||||
inventory.add(ItemStack.of(ItemRegistry.WOOD, 10));
|
||||
|
||||
eq("four wood comes out", Integer.valueOf(4),
|
||||
Integer.valueOf(inventory.take(ItemRegistry.WOOD, 4)));
|
||||
eq("six are left", Integer.valueOf(6),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
|
||||
eq("more than there is gives what there is", Integer.valueOf(6),
|
||||
Integer.valueOf(inventory.take(ItemRegistry.WOOD, 100)));
|
||||
eq("and nothing is left", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
eq("taking nothing takes nothing", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.take(ItemRegistry.WOOD, 0)));
|
||||
}
|
||||
|
||||
private static void testFull() {
|
||||
Inventory inventory = new Inventory();
|
||||
// a slot holds a stack of up to sixty four, so the bag is full when
|
||||
// every slot is
|
||||
for (int i = 0; i < Inventory.SLOTS; i++) {
|
||||
yes("slot " + i + " takes a stack",
|
||||
inventory.add(ItemStack.of(ItemRegistry.WOOD, 64)));
|
||||
}
|
||||
yes("a full bag says so", !inventory.add(ItemStack.of(ItemRegistry.WOOD, 1)));
|
||||
eq("and it did not take the last one", Integer.valueOf(Inventory.SLOTS * 64),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
}
|
||||
|
||||
private static void testPacket() {
|
||||
Inventory inventory = Inventory.startingKit();
|
||||
inventory.add(ItemStack.of(ItemRegistry.SCRAP, 3));
|
||||
inventory.held(1);
|
||||
|
||||
Packets.Inventory packet = inventory.toPacket();
|
||||
eq("every slot is there", Integer.valueOf(Inventory.SLOTS),
|
||||
Integer.valueOf(packet.slots.length));
|
||||
eq("the rock is in the first slot", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(packet.slots[0].item));
|
||||
eq("the scrap is in the second", Integer.valueOf(ItemRegistry.SCRAP),
|
||||
Integer.valueOf(packet.slots[1].item));
|
||||
eq("three of it", Integer.valueOf(3),
|
||||
Integer.valueOf(packet.slots[1].count));
|
||||
eq("and the hand is the second slot", Integer.valueOf(1),
|
||||
Integer.valueOf(packet.held));
|
||||
eq("the empty slots stay empty", Integer.valueOf(ItemRegistry.NOTHING),
|
||||
Integer.valueOf(packet.slots[5].item));
|
||||
}
|
||||
|
||||
private static void testCondition() {
|
||||
// a tool has a life, a pile of wood does not
|
||||
ItemStack rock = ItemStack.of(ItemRegistry.ROCK, 1);
|
||||
eq("the rock starts whole", Integer.valueOf(100), Integer.valueOf(rock.condition));
|
||||
|
||||
ItemStack wood = ItemStack.of(ItemRegistry.WOOD, 1);
|
||||
eq("wood does not wear out", Integer.valueOf(-1),
|
||||
Integer.valueOf(wood.condition));
|
||||
|
||||
// a swing costs the tool something
|
||||
ItemType rockType = ItemRegistry.require(ItemRegistry.ROCK);
|
||||
eq("a swing on a tree costs the rock", 0.3F,
|
||||
rockType.gather(ResourceClass.TREE).conditionLoss, 0.0001F);
|
||||
eq("a swing on an ore costs the same", 0.3F,
|
||||
rockType.gather(ResourceClass.METAL).conditionLoss, 0.0001F);
|
||||
}
|
||||
|
||||
private static void eq(String what, Object expected, Object actual, float tolerance) {
|
||||
if (expected instanceof Float && actual instanceof Float) {
|
||||
float difference = Math.abs(((Float) expected).floatValue()
|
||||
- ((Float) actual).floatValue());
|
||||
if (difference <= tolerance) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
eq(what, expected, actual);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user