Модель предметов по базе ремейка: узлы, деревья, знание

Протокол:
- ItemData читает таблицу items/items.csv (218 предметов ремейка) и отдает
  числа: урон, дальность, темп, прочность, сколько ресурса за удар.
- ItemType: поля rid, researchScrap, techTreeBench, techTreeScrap,
  techTreeParent, admin. Урон по частям тела делится как в ремейке: голова
  вдвое, руки и ноги четыре пятых. Число в исходнике главнее таблицы.
- ItemRegistry: 218 предметов с рецептами, ценой исследования и веткой
  дерева. Новые вещи: salvage-тесак, бронедвери, гаражи, взрывчатка,
  компоненты, еда, мусор, верстаки и печи.
- NodeType: узлы с запасом (дерево 500/750/1000, камень 750, металл 500,
  сера 200) и меткой: крестик на дереве до двух, горячая точка на руде до
  трех.
- TechTree: ветки верстаков с предками, 125/250/500 скрапа.
- Новые пакеты: Dig, BreakProgress, CraftStart, CraftUpdate, ResearchStart,
  ResearchUpdate, TechTreeUnlock, KnownRecipes, TechTreeState, ContainerOpen,
  ContainerTake, LearnBlueprint.

Сервер:
- Knowledge: что игрок знает - рецепты и открытые узлы деревьев.
- Crafter: проверка рецепта, уровня верстака и ингредиентов; время крафта
  по формуле плана.
- ResearchTable: предмет плюс скрап, десять секунд, предмет превращается в
  чертёж; чертёж изучается и открывает рецепт.
- ResourceNode: запас, метка, последний удар отдает остаток.
- JunkPile: две-четыре бочки (серая - скрап и мусор, синяя - компонент,
  красная - нефть и топливо) и ящик с едой; пустая мусорка исчезает через
  60-180 секунд.
- RoadNetwork: сетка дорог и кольцо, точки спавна мусорки; генератор кладет
  асфальт и ровняет дорогу по ширине.
- Проверка досягаемости вынесена из changeBlock, игрок получил benchLevel.
- Сессия отвечает на новые пакеты, при входе шлет известные рецепты и
  открытые узлы.

Тесты: протокол 24 + 10, сервер 74 (было 57). Форк собирается чисто.
This commit is contained in:
sasheg dev
2026-10-21 19:54:22 +03:00
parent 27047e8071
commit cda7198c76
25 changed files with 3409 additions and 108 deletions
@@ -0,0 +1,353 @@
package net.rustnotmc.protocol;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Tests for the item table, the registry, the trees and the nodes.
*
* The numbers here come from the table of the remake. A test that pins them
* down is what stops a change in the table from quietly changing the game:
* when the numbers move, the test says so.
*/
public final class ItemTest {
private static int checks;
private static int failed;
private static final List<String> failures = new ArrayList<String>();
public static void main(String[] args) {
run("the table is read", new Runnable() {
public void run() { testTableLoads(); }
});
run("the table holds the numbers of the remake", new Runnable() {
public void run() { testTableNumbers(); }
});
run("every item of the registry is in the table", new Runnable() {
public void run() { testRegistryMatchesTable(); }
});
run("the tool takes the numbers of the table", new Runnable() {
public void run() { testToolNumbers(); }
});
run("the damage is split over the body", new Runnable() {
public void run() { testDamageSplit(); }
});
run("the trees cost what the plan says", new Runnable() {
public void run() { testTreePrices(); }
});
run("a branch has to be walked", new Runnable() {
public void run() { testTreeParents(); }
});
run("the nodes hold what the plan says", new Runnable() {
public void run() { testNodePools(); }
});
run("the mark of a node grows and stops", new Runnable() {
public void run() { testNodeMarkers(); }
});
run("the new packets survive the round trip", new Runnable() {
public void run() { testPackets(); }
});
System.out.println("");
if (failed == 0) {
System.out.println(" " + checks + " checks passed.");
return;
}
System.out.println(" " + failed + " of " + checks + " checks FAILED:");
for (int i = 0; i < failures.size(); i++) {
System.out.println(" " + failures.get(i));
}
System.exit(1);
}
private static void testTableLoads() {
yes("the table is not empty", ItemData.size() > 200);
ItemData.Row rock = ItemData.byRid("rock");
yes("the rock is in the table", rock != null);
if (rock != null) {
eq("the rock hurts ten", Integer.valueOf(10), Integer.valueOf(rock.damage));
eq("the rock takes fifty wood", Integer.valueOf(50), Integer.valueOf(rock.wood));
eq("the rock takes thirty five ore", Integer.valueOf(35), Integer.valueOf(rock.ore));
eq("the rock lasts a hundred", Integer.valueOf(100), Integer.valueOf(rock.durability));
eq("the rock swings every two seconds", Integer.valueOf(40),
Integer.valueOf(rock.cooldown));
}
ItemData.Row hatchet = ItemData.byRid("ironaxe");
yes("the hatchet is in the table", hatchet != null);
if (hatchet != null) {
eq("the hatchet takes eighty five wood", Integer.valueOf(85),
Integer.valueOf(hatchet.wood));
eq("the hatchet hurts twenty five", Integer.valueOf(25),
Integer.valueOf(hatchet.damage));
eq("the hatchet lasts three hundred", Integer.valueOf(300),
Integer.valueOf(hatchet.durability));
}
}
private static void testTableNumbers() {
// The numbers the plan writes down have to be the numbers the table
// holds, otherwise the two drift apart.
eq("the tree of a poor one is five hundred", Integer.valueOf(500),
Integer.valueOf(NodeType.byId(NodeType.TREE_POOR).pool));
eq("the tree of an ordinary one is seven hundred fifty", Integer.valueOf(750),
Integer.valueOf(NodeType.byId(NodeType.TREE_COMMON).pool));
eq("a dead tree is a thousand", Integer.valueOf(1000),
Integer.valueOf(NodeType.byId(NodeType.TREE_DEAD).pool));
eq("a stone outcrop is seven hundred fifty", Integer.valueOf(750),
Integer.valueOf(NodeType.byId(NodeType.STONE).pool));
eq("a metal deposit is five hundred", Integer.valueOf(500),
Integer.valueOf(NodeType.byId(NodeType.METAL).pool));
eq("a sulfur deposit is two hundred", Integer.valueOf(200),
Integer.valueOf(NodeType.byId(NodeType.SULFUR).pool));
}
private static void testRegistryMatchesTable() {
int withRid = 0;
for (ItemType type : ItemRegistry.all()) {
if (type.rid == null) {
continue;
}
withRid++;
yes("the table knows " + type.rid, ItemData.byRid(type.rid) != null);
}
yes("most items come from the table", withRid > 90);
// And the other way round: the table may know more than we do, but
// everything it knows that we also know has to be the same item.
for (ItemType type : ItemRegistry.all()) {
if (type.rid == null) {
continue;
}
ItemData.Row row = ItemData.byRid(type.rid);
if (row == null) {
continue;
}
if (row.damage > 0) {
eq("the damage of " + type.rid, Integer.valueOf(row.damage),
Integer.valueOf(type.damage(ItemType.TORSO)));
}
if (row.durability > 0) {
eq("the durability of " + type.rid, Integer.valueOf(row.durability),
Integer.valueOf(type.durability));
}
}
}
private static void testToolNumbers() {
ItemType rock = ItemRegistry.require(ItemRegistry.ROCK);
yes("the rock gathers wood", rock.gather(ResourceClass.TREE) != null);
eq("the rock takes fifty wood a swing", Integer.valueOf(50),
Integer.valueOf(rock.gather(ResourceClass.TREE).damage));
eq("the rock takes thirty five ore a swing", Integer.valueOf(35),
Integer.valueOf(rock.gather(ResourceClass.STONE).damage));
yes("the rock has no pickaxe", rock.gather(ResourceClass.TREE) != null);
ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
eq("the hatchet takes eighty five wood", Integer.valueOf(85),
Integer.valueOf(hatchet.gather(ResourceClass.TREE).damage));
yes("the hatchet does not mine", hatchet.gather(ResourceClass.STONE) == null);
ItemType pickaxe = ItemRegistry.require(ItemRegistry.PICKAXE);
eq("the pickaxe takes a hundred ore", Integer.valueOf(100),
Integer.valueOf(pickaxe.gather(ResourceClass.STONE).damage));
yes("the pickaxe does not cut", pickaxe.gather(ResourceClass.TREE) == null);
}
private static void testDamageSplit() {
ItemType rock = ItemRegistry.require(ItemRegistry.ROCK);
eq("the head takes twice", Integer.valueOf(20),
Integer.valueOf(rock.damage(ItemType.HEAD)));
eq("the torso takes the number", Integer.valueOf(10),
Integer.valueOf(rock.damage(ItemType.TORSO)));
eq("the arms take four fifths", Integer.valueOf(8),
Integer.valueOf(rock.damage(ItemType.ARMS)));
eq("the legs take four fifths", Integer.valueOf(8),
Integer.valueOf(rock.damage(ItemType.LEGS)));
}
private static void testTreePrices() {
eq("the tree of the first workbench costs a hundred twenty five",
Integer.valueOf(125), Integer.valueOf(TechTree.PRICE_L1));
eq("the tree of the second workbench costs two hundred fifty",
Integer.valueOf(250), Integer.valueOf(TechTree.PRICE_L2));
eq("the tree of the third workbench costs five hundred",
Integer.valueOf(500), Integer.valueOf(TechTree.PRICE_L3));
eq("the hatchet costs a hundred twenty five", Integer.valueOf(125),
Integer.valueOf(TechTree.costOf(ItemRegistry.HATCHET)));
eq("the hatchet sits in the tree of the first workbench", Integer.valueOf(1),
Integer.valueOf(TechTree.forItem(ItemRegistry.HATCHET).bench));
eq("the salvaged axe sits in the tree of the second workbench", Integer.valueOf(2),
Integer.valueOf(TechTree.forItem(ItemRegistry.SALVAGED_AXE).bench));
eq("the armored door sits in the tree of the third workbench", Integer.valueOf(3),
Integer.valueOf(TechTree.forItem(ItemRegistry.ARMORED_DOOR).bench));
eq("the rock is in no tree", Integer.valueOf(0),
Integer.valueOf(TechTree.costOf(ItemRegistry.ROCK)));
}
private static void testTreeParents() {
Set<Integer> nothing = new HashSet<Integer>();
yes("the root of a branch can be opened",
TechTree.canOpen(ItemRegistry.SALVAGED_HAMMER, nothing));
yes("the next one cannot",
!TechTree.canOpen(ItemRegistry.HATCHET, nothing));
Set<Integer> withHammer = new HashSet<Integer>();
withHammer.add(Integer.valueOf(ItemRegistry.SALVAGED_HAMMER));
yes("with the hammer the hatchet opens",
TechTree.canOpen(ItemRegistry.HATCHET, withHammer));
yes("with the hammer the sword does not",
!TechTree.canOpen(ItemRegistry.SALVAGED_SWORD, withHammer));
Set<Integer> withHatchet = new HashSet<Integer>(withHammer);
withHatchet.add(Integer.valueOf(ItemRegistry.HATCHET));
yes("with the hatchet the sword opens",
TechTree.canOpen(ItemRegistry.SALVAGED_SWORD, withHatchet));
}
private static void testNodePools() {
eq("the ordinary tree holds seven hundred fifty", Integer.valueOf(750),
Integer.valueOf(NodeType.byId(NodeType.TREE_COMMON).pool));
eq("the tree gives wood", Integer.valueOf(ItemRegistry.WOOD),
Integer.valueOf(NodeType.byId(NodeType.TREE_COMMON).dropItem));
eq("the outcrop gives stones", Integer.valueOf(ItemRegistry.STONE),
Integer.valueOf(NodeType.byId(NodeType.STONE).dropItem));
eq("the deposit gives metal", Integer.valueOf(ItemRegistry.METAL),
Integer.valueOf(NodeType.byId(NodeType.METAL).dropItem));
eq("the sulfur deposit gives sulfur", Integer.valueOf(ItemRegistry.SULFUR),
Integer.valueOf(NodeType.byId(NodeType.SULFUR).dropItem));
yes("a tree has a cross", NodeType.byId(NodeType.TREE_COMMON).hasMarker());
yes("an outcrop has a hot spot", NodeType.byId(NodeType.STONE).hasMarker());
}
private static void testNodeMarkers() {
NodeType tree = NodeType.byId(NodeType.TREE_COMMON);
eq("a swing without the cross is the plain one", Integer.valueOf(85),
Integer.valueOf(tree.swingDamage(85, 0)));
eq("the first cross adds an eighth", Integer.valueOf(96),
Integer.valueOf(tree.swingDamage(85, 1)));
eq("the cross stops at twice", Integer.valueOf(170),
Integer.valueOf(tree.swingDamage(85, 100)));
NodeType ore = NodeType.byId(NodeType.STONE);
eq("the first hot spot adds half", Integer.valueOf(75),
Integer.valueOf(ore.swingDamage(50, 1)));
eq("the hot spot stops at three times", Integer.valueOf(150),
Integer.valueOf(ore.swingDamage(50, 100)));
}
private static void testPackets() {
Packets.Dig dig = new Packets.Dig(Packets.Dig.START, 10, -60, 30, 2);
Packets.Dig digBack = Packets.Dig.read(new Buf(dig.write().toByteArray()));
eq("the action survives", Integer.valueOf(Packets.Dig.START),
Integer.valueOf(digBack.action));
eq("the place survives", Integer.valueOf(10), Integer.valueOf(digBack.x));
eq("the face survives", Integer.valueOf(2), Integer.valueOf(digBack.face));
yes("a start is a start", digBack.start());
Packets.BreakProgress progress = new Packets.BreakProgress(1, 2, 3, 7);
Packets.BreakProgress progressBack =
Packets.BreakProgress.read(new Buf(progress.write().toByteArray()));
eq("the stage survives", Integer.valueOf(7), Integer.valueOf(progressBack.stage));
eq("the place survives", Integer.valueOf(3), Integer.valueOf(progressBack.z));
Packets.CraftUpdate craft = new Packets.CraftUpdate(ItemRegistry.HATCHET, 1, 12, "");
Packets.CraftUpdate craftBack =
Packets.CraftUpdate.read(new Buf(craft.write().toByteArray()));
eq("the item survives", Integer.valueOf(ItemRegistry.HATCHET),
Integer.valueOf(craftBack.item));
eq("the seconds survive", Integer.valueOf(12), Integer.valueOf(craftBack.secondsLeft));
yes("an empty reason means fine", craftBack.ok());
Packets.CraftUpdate refused = new Packets.CraftUpdate(0, 0, 0, "no scrap");
Packets.CraftUpdate refusedBack =
Packets.CraftUpdate.read(new Buf(refused.write().toByteArray()));
yes("a reason means no", !refusedBack.ok());
eq("the reason survives", "no scrap", refusedBack.reason);
Packets.ResearchUpdate research =
new Packets.ResearchUpdate(ItemRegistry.PICKAXE, 10, "");
Packets.ResearchUpdate researchBack =
Packets.ResearchUpdate.read(new Buf(research.write().toByteArray()));
eq("the researched item survives", Integer.valueOf(ItemRegistry.PICKAXE),
Integer.valueOf(researchBack.item));
yes("a research that works has no reason", researchBack.ok());
Packets.TechTreeUnlock unlock = new Packets.TechTreeUnlock(ItemRegistry.HATCHET);
Packets.TechTreeUnlock unlockBack =
Packets.TechTreeUnlock.read(new Buf(unlock.write().toByteArray()));
eq("the node survives", Integer.valueOf(ItemRegistry.HATCHET),
Integer.valueOf(unlockBack.item));
Packets.KnownRecipes known = new Packets.KnownRecipes(new int[]{1, 2, 3});
Packets.KnownRecipes knownBack =
Packets.KnownRecipes.read(new Buf(known.write().toByteArray()));
eq("the recipes survive", Integer.valueOf(3),
Integer.valueOf(knownBack.items.length));
eq("the last recipe survives", Integer.valueOf(3),
Integer.valueOf(knownBack.items[2]));
Packets.TechTreeState state = new Packets.TechTreeState(new int[]{18, 16});
Packets.TechTreeState stateBack =
Packets.TechTreeState.read(new Buf(state.write().toByteArray()));
eq("the opened nodes survive", Integer.valueOf(2),
Integer.valueOf(stateBack.items.length));
Packets.Slot[] slots = new Packets.Slot[]{
new Packets.Slot(ItemRegistry.SCRAP, 4, -1),
Packets.Slot.of(null)};
Packets.ContainerOpen open = new Packets.ContainerOpen(9, "Food", slots);
Packets.ContainerOpen openBack =
Packets.ContainerOpen.read(new Buf(open.write().toByteArray()));
eq("the title survives", "Food", openBack.title);
eq("the slots survive", Integer.valueOf(2), Integer.valueOf(openBack.slots.length));
eq("what is in the slot survives", Integer.valueOf(ItemRegistry.SCRAP),
Integer.valueOf(openBack.slots[0].item));
eq("an empty slot is nothing", Integer.valueOf(ItemRegistry.NOTHING),
Integer.valueOf(openBack.slots[1].item));
Packets.ContainerTake take = new Packets.ContainerTake(9, 0);
Packets.ContainerTake takeBack =
Packets.ContainerTake.read(new Buf(take.write().toByteArray()));
eq("the container survives", Integer.valueOf(9), Integer.valueOf(takeBack.id));
eq("the slot survives", Integer.valueOf(0), Integer.valueOf(takeBack.slot));
Packets.LearnBlueprint learn = new Packets.LearnBlueprint(ItemRegistry.PICKAXE);
Packets.LearnBlueprint learnBack =
Packets.LearnBlueprint.read(new Buf(learn.write().toByteArray()));
eq("the blueprint survives", Integer.valueOf(ItemRegistry.PICKAXE),
Integer.valueOf(learnBack.item));
}
private static void run(String name, Runnable body) {
checks++;
int before = failed;
try {
body.run();
} catch (Throwable t) {
failed++;
failures.add(name + ": threw " + t);
t.printStackTrace();
return;
}
if (failed == before) {
System.out.println(" ok " + name);
} else {
System.out.println(" FAIL " + name);
}
}
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 yes(String what, boolean condition) {
if (!condition) {
failed++;
failures.add(what);
}
}
}
@@ -545,7 +545,12 @@ 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.SET_GAME_MODE, Packets.INVENTORY, Packets.INVENTORY_SLOT,
Packets.DIG, Packets.BREAK_PROGRESS, Packets.CRAFT_START, Packets.CRAFT_UPDATE,
Packets.RESEARCH_START, Packets.RESEARCH_UPDATE, Packets.TECH_TREE_UNLOCK,
Packets.KNOWN_RECIPES, Packets.TECH_TREE_STATE,
Packets.CONTAINER_OPEN, Packets.CONTAINER_TAKE,
Packets.LEARN_BLUEPRINT,
Packets.DISCONNECT,
};
for (int i = 0; i < ids.length; i++) {
@@ -565,9 +570,9 @@ public final class ProtocolTest {
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),
eq("the rock takes fifty off a tree", Integer.valueOf(50),
Integer.valueOf(rock.gather(ResourceClass.TREE).damage));
eq("and five off an ore", Integer.valueOf(5),
eq("and thirty five off an ore", Integer.valueOf(35),
Integer.valueOf(rock.gather(ResourceClass.METAL).damage));
eq("the rock is known from the start", Boolean.TRUE,
Boolean.valueOf(rock.knownByDefault));
@@ -579,12 +584,12 @@ public final class ProtocolTest {
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),
eq("it takes eighty off a tree", Integer.valueOf(80),
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),
eq("it takes eighty off an ore", Integer.valueOf(80),
Integer.valueOf(pickaxe.gather(ResourceClass.METAL).damage));
yes("and it does not cut trees", pickaxe.gather(ResourceClass.TREE) == null);