Шаг 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:
sasheg dev
2026-10-21 17:43:30 +03:00
parent 56f0341373
commit 8488a34c80
15 changed files with 1324 additions and 1 deletions
@@ -82,6 +82,12 @@ public final class ProtocolTest {
run("every packet has a name", new Runnable() {
public void run() { testPacketNames(); }
});
run("the items match the original game", new Runnable() {
public void run() { testItems(); }
});
run("an inventory survives the wire", new Runnable() {
public void run() { testInventory(); }
});
run("a lying length cannot stall the heap", new Runnable() {
public void run() { testLyingLength(); }
});
@@ -550,6 +556,98 @@ public final class ProtocolTest {
Integer.valueOf(names.size()));
}
private static void testItems() {
// The numbers the game is built on. If these move, the game stops
// matching its own rules, so they are pinned here.
ItemType rock = ItemRegistry.require(ItemRegistry.ROCK);
eq("the rock is called a rock", "Rock", rock.name);
eq("the rock hits the head for twenty", Integer.valueOf(20),
Integer.valueOf(rock.damage(ItemType.HEAD)));
eq("and the torso for ten", Integer.valueOf(10),
Integer.valueOf(rock.damage(ItemType.TORSO)));
eq("the rock takes ten off a tree", Integer.valueOf(10),
Integer.valueOf(rock.gather(ResourceClass.TREE).damage));
eq("and five off an ore", Integer.valueOf(5),
Integer.valueOf(rock.gather(ResourceClass.METAL).damage));
eq("the rock is known from the start", Boolean.TRUE,
Boolean.valueOf(rock.knownByDefault));
eq("the rock needs no workbench", Integer.valueOf(0),
Integer.valueOf(rock.bench));
yes("the rock has a recipe", rock.recipe != null);
eq("the rock is made of ten stones", Integer.valueOf(10),
Integer.valueOf(rock.recipe.needs(ItemRegistry.STONE)));
ItemType hatchet = ItemRegistry.require(ItemRegistry.STONE_HATCHET);
eq("the stone hatchet is an axe", ToolClass.AXE, hatchet.tool);
eq("it takes twenty off a tree", Integer.valueOf(20),
Integer.valueOf(hatchet.gather(ResourceClass.TREE).damage));
ItemType pickaxe = ItemRegistry.require(ItemRegistry.STONE_PICKAXE);
eq("the stone pickaxe is a pickaxe", ToolClass.PICKAXE, pickaxe.tool);
eq("it takes sixteen off an ore", Integer.valueOf(16),
Integer.valueOf(pickaxe.gather(ResourceClass.METAL).damage));
yes("and it does not cut trees", pickaxe.gather(ResourceClass.TREE) == null);
ItemType sword = ItemRegistry.require(ItemRegistry.SALVAGED_SWORD);
eq("the sword is a sword", ToolClass.SWORD, sword.tool);
yes("the sword gathers nothing but flesh", sword.gather(ResourceClass.TREE) == null
&& sword.gather(ResourceClass.METAL) == null
&& sword.gather(ResourceClass.FLESH) != null);
ItemType workbench2 = ItemRegistry.require(ItemRegistry.WORKBENCH_2);
eq("the second workbench needs the first", Integer.valueOf(1),
Integer.valueOf(workbench2.bench));
ItemType workbench3 = ItemRegistry.require(ItemRegistry.WORKBENCH_3);
eq("the third needs the second", Integer.valueOf(2),
Integer.valueOf(workbench3.bench));
// the resources are gathered, not made
ItemType wood = ItemRegistry.require(ItemRegistry.WOOD);
yes("wood has no recipe", wood.recipe == null);
yes("every id is different", ItemRegistry.all().size() > 20);
}
private static void testInventory() {
Packets.Slot[] slots = new Packets.Slot[6];
slots[0] = new Packets.Slot(ItemRegistry.ROCK, 1, 100);
slots[1] = new Packets.Slot(ItemRegistry.WOOD, 12, -1);
slots[2] = new Packets.Slot(ItemRegistry.NOTHING, 0, -1);
Packets.Inventory inventory = new Packets.Inventory(slots, 0);
Packets.Inventory back =
Packets.Inventory.read(new Buf(inventory.write().toByteArray()));
eq("the slots survive", Integer.valueOf(6),
Integer.valueOf(back.slots.length));
eq("the rock is in the first slot", Integer.valueOf(ItemRegistry.ROCK),
Integer.valueOf(back.slots[0].item));
eq("with one of it", Integer.valueOf(1),
Integer.valueOf(back.slots[0].count));
eq("and its life", Integer.valueOf(100),
Integer.valueOf(back.slots[0].condition));
eq("wood is in the second", Integer.valueOf(12),
Integer.valueOf(back.slots[1].count));
eq("an empty slot stays empty", Integer.valueOf(ItemRegistry.NOTHING),
Integer.valueOf(back.slots[2].item));
eq("the hand survives", Integer.valueOf(0), Integer.valueOf(back.held));
Packets.SlotChange change = new Packets.SlotChange(3, ItemRegistry.SCRAP, 7, -1);
Packets.SlotChange changeBack =
Packets.SlotChange.read(new Buf(change.write().toByteArray()));
eq("the slot number survives", Integer.valueOf(3),
Integer.valueOf(changeBack.slot));
eq("the item survives", Integer.valueOf(ItemRegistry.SCRAP),
Integer.valueOf(changeBack.item));
eq("the count survives", Integer.valueOf(7),
Integer.valueOf(changeBack.count));
// a stack carries the data that belongs to it
ItemStack stack = ItemStack.of(ItemRegistry.ROCK, 1);
stack.set("charged", "yes");
eq("the data travels", "yes", stack.copy().get("charged"));
eq("the copy is separate", Boolean.FALSE,
Boolean.valueOf(stack == stack.copy()));
}
private static void testLyingLength() {
// announces a large but legal body, sends nothing, and keeps quiet.
// The decoder must hold it without growing and without pretending the