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

Протокол:
- 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
@@ -27,9 +27,12 @@ public final class Blocks {
public static final byte WOOD = 6;
public static final byte LEAVES = 7;
/** The surface of a road. */
public static final byte ASPHALT = 8;
/** Lowest and highest legal block id. */
public static final byte MIN = AIR;
public static final byte MAX = LEAVES;
public static final byte MAX = ASPHALT;
private Blocks() {
}
@@ -44,6 +47,7 @@ public final class Blocks {
case WATER: return "water";
case WOOD: return "wood";
case LEAVES: return "leaves";
case ASPHALT: return "asphalt";
default: return "unknown(" + id + ")";
}
}
@@ -0,0 +1,173 @@
package net.rustnotmc.protocol;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* The numbers of every item, read from the table.
*
* The table is {@code items/items.csv} in the resources of this module. It was
* unpacked from the item database of the remake, which keeps the damage, the
* range, the tempo, the durability and the harvest of every item it knows.
* Keeping it in a file rather than in the source means one place to look when
* a number has to change, and one place to compare with the plan.
*
* The registry still decides what an item <i>is</i>: the id, the class of
* tool, the recipe, what it costs to research. This class only fills in the
* numbers.
*/
public final class ItemData {
/** One row of the table. */
public static final class Row {
public final String rid;
public final String name;
public final String category;
public final String material;
public final int damage;
public final int range;
public final int cooldown;
public final int reach;
public final int durability;
public final int wood;
public final int ore;
public final int ammo;
public final int weaponId;
Row(Map<String, String> cells) {
this.rid = cells.get("rid");
this.name = cells.get("name");
this.category = cells.get("category");
this.material = cells.get("material");
this.damage = number(cells, "damage");
this.range = number(cells, "range");
this.cooldown = number(cells, "cooldown");
this.reach = number(cells, "reach");
this.durability = number(cells, "durability");
this.wood = number(cells, "wood");
this.ore = number(cells, "ore");
this.ammo = number(cells, "ammo");
this.weaponId = number(cells, "weapon_id");
}
private static int number(Map<String, String> cells, String key) {
String value = cells.get(key);
if (value == null || value.isEmpty()) {
return 0;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException ignored) {
return 0;
}
}
/** True when the row says nothing about this number. */
public boolean isEmpty() {
return this.damage == 0 && this.cooldown == 0 && this.durability == 0
&& this.wood == 0 && this.ore == 0;
}
}
private static final Map<String, Row> ROWS = load();
private ItemData() {
}
/** The row of an item, or null when the table does not know it. */
public static Row byRid(String rid) {
if (rid == null) {
return null;
}
return ROWS.get(rid);
}
/** Every row, by the Rust id of the item. */
public static Map<String, Row> all() {
return ROWS;
}
/** How many items the table knows. */
public static int size() {
return ROWS.size();
}
private static Map<String, Row> load() {
Map<String, Row> rows = new HashMap<String, Row>();
InputStream in = ItemData.class.getResourceAsStream("/items/items.csv");
if (in == null) {
return Collections.unmodifiableMap(rows);
}
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(in, StandardCharsets.UTF_8));
try {
String header = reader.readLine();
if (header == null) {
return Collections.unmodifiableMap(rows);
}
String[] names = splitCsv(header);
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
String[] cells = splitCsv(line);
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < names.length && i < cells.length; i++) {
map.put(names[i], cells[i]);
}
Row row = new Row(map);
if (row.rid != null && !row.rid.isEmpty()) {
rows.put(row.rid, row);
}
}
} finally {
reader.close();
}
} catch (IOException ignored) {
// A table that cannot be read is an empty table: the registry
// still works, it just has to carry the numbers itself.
}
return Collections.unmodifiableMap(rows);
}
/** Splits a line of the table, honouring quotes. */
private static String[] splitCsv(String line) {
java.util.List<String> out = new java.util.ArrayList<String>();
StringBuilder cell = new StringBuilder();
boolean quoted = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (quoted) {
if (c == '"') {
if (i + 1 < line.length() && line.charAt(i + 1) == '"') {
cell.append('"');
i++;
} else {
quoted = false;
}
} else {
cell.append(c);
}
} else if (c == '"') {
quoted = true;
} else if (c == ',') {
out.add(cell.toString());
cell.setLength(0);
} else {
cell.append(c);
}
}
out.add(cell.toString());
return out.toArray(new String[out.size()]);
}
}
@@ -8,13 +8,11 @@ import java.util.Map;
/**
* Every item the game knows.
*
* The numbers are the ones from the original game at the state of November
* 2025, and they are written here once so that the game, the plan and the
* wiki cannot drift apart. A table copied by hand into two places drifts, and
* then the game stops matching its own rules.
*
* The ids are ours and they are stable: they go into save files and into the
* wire, so an id that ever changes is a bug.
* wire, so an id that ever changes is a bug. The numbers come from the table
* of {@link ItemData}, which was unpacked from the item database of the
* remake, and the plan keeps the same numbers. A table copied by hand into
* two places drifts, and then the game stops matching its own rules.
*/
public final class ItemRegistry {
@@ -47,6 +45,93 @@ public final class ItemRegistry {
public static final int METAL_PIPE = 26;
public static final int METAL_BLADE = 27;
public static final int GEARS = 28;
public static final int SALVAGED_CLEAVER = 29;
public static final int GARAGE_DOOR = 30;
public static final int ARMORED_DOOR = 31;
public static final int ARMORED_DOUBLE_DOOR = 32;
public static final int EXPLOSIVES = 33;
public static final int TIMED_CHARGE = 34;
// The resources and the parts the junk pile gives out.
public static final int CRUDE_OIL = 35;
public static final int LOW_GRADE_FUEL = 36;
public static final int METAL_SPRING = 37;
public static final int SHEET_METAL = 38;
public static final int ROAD_SIGNS = 39;
public static final int ELECTRIC_FUSE = 40;
public static final int ROPE = 41;
public static final int TARP = 42;
public static final int SEWING_KIT = 43;
public static final int EMPTY_PROPANE_TANK = 44;
public static final int SEMI_AUTO_BODY = 45;
public static final int TECH_TRASH = 46;
public static final int GUNPOWDER = 47;
public static final int CHARCOAL = 48;
public static final int CLOTH = 49;
public static final int LEATHER = 50;
public static final int PLANT_FIBER = 51;
public static final int ANIMAL_FAT = 52;
// The food.
public static final int APPLE = 53;
public static final int BLUEBERRIES = 54;
public static final int BLACK_RASPBERRIES = 55;
public static final int POTATO = 56;
public static final int CHOCOLATE_BAR = 57;
public static final int CAN_OF_BEANS = 58;
public static final int CAN_OF_TUNA = 59;
public static final int GRANOLA_BAR = 60;
public static final int CORN = 61;
public static final int SMALL_WATER_BOTTLE = 62;
public static final int WATER_JUG = 63;
public static final int ANTI_RAD = 64;
public static final int BANDAGE = 65;
public static final int MEDICAL_SYRINGE = 66;
public static final int LARGE_MEDKIT = 67;
public static final int PUMPKIN = 68;
public static final int MUSHROOM = 69;
// The rubbish of a junk pile: worn clothes and old rags.
public static final int BANDANA_MASK = 70;
public static final int TANK_TOP = 71;
public static final int SHORTS = 72;
public static final int T_SHIRT = 73;
public static final int PANTS = 74;
public static final int LEATHER_GLOVES = 75;
public static final int BASEBALL_CAP = 76;
public static final int BOONIE_HAT = 77;
public static final int HIDE_PANTS = 78;
public static final int HIDE_BOOTS = 79;
public static final int HIDE_SKIRT = 80;
public static final int HIDE_CHEST = 81;
public static final int HIDE_PONCHO = 82;
public static final int ROADSIGN_JACKET = 83;
public static final int ROADSIGN_KILT = 84;
public static final int ROADSIGN_GLOVES = 85;
// The places and the things in them.
public static final int CAMPFIRE = 86;
public static final int FURNACE = 87;
public static final int SLEEPING_BAG = 88;
public static final int RECYCLER = 89;
public static final int WOOD_STORAGE_BOX = 90;
public static final int LARGE_WOOD_BOX = 91;
public static final int TOOL_CUPBOARD = 92;
public static final int KEY_LOCK = 93;
public static final int WOODEN_LADDER = 94;
public static final int FLOOR_GRILL = 95;
public static final int METAL_WINDOW_BARS = 96;
public static final int WOODEN_WINDOW_BARS = 97;
public static final int STRENGTHENED_GLASS_WINDOW = 98;
public static final int REINFORCED_GLASS_WINDOW = 99;
public static final int WOOD_DOUBLE_DOOR = 100;
public static final int SHEETMETAL_DOOR = 101;
public static final int SHEETMETAL_DOUBLE_DOOR = 102;
public static final int HOMEMADE_LANDMINE = 103;
public static final int SHOTGUN_TRAP = 104;
public static final int SNAP_TRAP = 105;
public static final int WOODEN_FLOOR_SPIKES = 106;
public static final int TORCH = 107;
private static final Map<Integer, ItemType> TYPES = new LinkedHashMap<Integer, ItemType>();
@@ -57,144 +142,338 @@ public final class ItemRegistry {
// The resources. They are not made, they are gathered, so they have no
// recipe at all.
define(ItemType.of(NOTHING, "Nothing").tool(ToolClass.NONE));
define(ItemType.of(WOOD, "Wood"));
define(ItemType.of(STONE, "Stones"));
define(ItemType.of(METAL, "Metal Fragments"));
define(ItemType.of(HIGH_QUALITY_METAL, "High Quality Metal"));
define(ItemType.of(SULFUR, "Sulfur Ore"));
define(ItemType.of(SCRAP, "Scrap"));
define(ItemType.of(WOOD, "Wood").rid("wood"));
define(ItemType.of(STONE, "Stones").rid("stone"));
define(ItemType.of(METAL, "Metal Fragments").rid("metalfragment"));
define(ItemType.of(HIGH_QUALITY_METAL, "High Quality Metal").rid("hqm"));
define(ItemType.of(SULFUR, "Sulfur Ore").rid("sulfur_ore"));
define(ItemType.of(SCRAP, "Scrap").rid("scrap"));
// The fragments of the blueprints. The remake does not know them: they
// come out of the loot crates, and the crates are still to come.
define(ItemType.of(BASIC_FRAGMENT, "Basic Blueprint Fragment"));
define(ItemType.of(ADVANCED_FRAGMENT, "Advanced Blueprint Fragment"));
define(ItemType.of(METAL_PIPE, "Metal Pipe"));
define(ItemType.of(METAL_BLADE, "Metal Blade"));
define(ItemType.of(GEARS, "Gears"));
define(ItemType.of(METAL_PIPE, "Metal Pipe").rid("metalpipe"));
define(ItemType.of(METAL_BLADE, "Metal Blade").rid("metalblade"));
define(ItemType.of(GEARS, "Gears").rid("gears"));
define(ItemType.of(CRUDE_OIL, "Crude Oil").rid("crudeoil"));
define(ItemType.of(LOW_GRADE_FUEL, "Low Grade Fuel").rid("lowgradefuel"));
define(ItemType.of(METAL_SPRING, "Metal Spring").rid("metalspring"));
define(ItemType.of(SHEET_METAL, "Sheet Metal").rid("sheetmetal"));
define(ItemType.of(ROAD_SIGNS, "Road Signs").rid("roadsigns"));
define(ItemType.of(ELECTRIC_FUSE, "Electric Fuse").rid("electricfuse"));
define(ItemType.of(ROPE, "Rope").rid("rope"));
define(ItemType.of(TARP, "Tarp").rid("tarp"));
define(ItemType.of(SEWING_KIT, "Sewing Kit").rid("sewingkit"));
define(ItemType.of(EMPTY_PROPANE_TANK, "Empty Propane Tank").rid("emptypropanetank"));
define(ItemType.of(SEMI_AUTO_BODY, "Semi Automatic Body").rid("semiautomaticbody"));
define(ItemType.of(TECH_TRASH, "Tech Trash").rid("techtrash"));
define(ItemType.of(GUNPOWDER, "Gunpowder").rid("gunpowder"));
define(ItemType.of(CHARCOAL, "Charcoal").rid("charcoal"));
define(ItemType.of(CLOTH, "Cloth").rid("cloth"));
define(ItemType.of(LEATHER, "Leather").rid("leather"));
define(ItemType.of(PLANT_FIBER, "Plant Fiber").rid("plantfiber"));
define(ItemType.of(ANIMAL_FAT, "Animal Fat").rid("animalfat"));
// The tools. Gather Damage, Bonus and Condition Loss are from the
// official Gather Rates table; the damage is to the head, the torso,
// the arms and the legs.
// The food and the medicine.
define(ItemType.of(APPLE, "Apple").rid("apple"));
define(ItemType.of(BLUEBERRIES, "Blueberries").rid("blueberries"));
define(ItemType.of(BLACK_RASPBERRIES, "Black Raspberries").rid("blackraspberries"));
define(ItemType.of(POTATO, "Potato").rid("potato"));
define(ItemType.of(CHOCOLATE_BAR, "Chocolate Bar").rid("chocolatebar"));
define(ItemType.of(CAN_OF_BEANS, "Can of Beans").rid("canofbeans"));
define(ItemType.of(CAN_OF_TUNA, "Can of Tuna").rid("canoftuna"));
define(ItemType.of(GRANOLA_BAR, "Granola Bar").rid("granolabar"));
define(ItemType.of(CORN, "Corn").rid("corn"));
define(ItemType.of(SMALL_WATER_BOTTLE, "Small Water Bottle").rid("smallwaterbottle"));
define(ItemType.of(WATER_JUG, "Water Jug").rid("waterjug"));
define(ItemType.of(ANTI_RAD, "Anti Rad Pills").rid("antirad"));
define(ItemType.of(BANDAGE, "Bandage").rid("bandage"));
define(ItemType.of(MEDICAL_SYRINGE, "Medical Syringe").rid("medicalsyringe"));
define(ItemType.of(LARGE_MEDKIT, "Large Medkit").rid("largemedkit"));
define(ItemType.of(PUMPKIN, "Pumpkin").rid("pumpkin"));
define(ItemType.of(MUSHROOM, "Mushroom").rid("mushroom"));
// The rubbish of a junk pile.
define(ItemType.of(BANDANA_MASK, "Bandana Mask").rid("maskbandana"));
define(ItemType.of(TANK_TOP, "Tank Top").rid("tanktop"));
define(ItemType.of(SHORTS, "Shorts").rid("shorts"));
define(ItemType.of(T_SHIRT, "T-Shirt").rid("tshirt"));
define(ItemType.of(PANTS, "Pants").rid("pants"));
define(ItemType.of(LEATHER_GLOVES, "Leather Gloves").rid("leathergloves"));
define(ItemType.of(BASEBALL_CAP, "Baseball Cap").rid("baseballcap"));
define(ItemType.of(BOONIE_HAT, "Boonie Hat").rid("booniehat"));
define(ItemType.of(HIDE_PANTS, "Hide Pants").rid("hidepants"));
define(ItemType.of(HIDE_BOOTS, "Hide Boots").rid("hideboots"));
define(ItemType.of(HIDE_SKIRT, "Hide Skirt").rid("hideskirt"));
define(ItemType.of(HIDE_CHEST, "Hide Chest").rid("hidechest"));
define(ItemType.of(HIDE_PONCHO, "Hide Poncho").rid("hideponcho"));
define(ItemType.of(ROADSIGN_JACKET, "Road Sign Jacket").rid("roadsignjacket"));
define(ItemType.of(ROADSIGN_KILT, "Road Sign Kilt").rid("roadsignkilt"));
define(ItemType.of(ROADSIGN_GLOVES, "Road Sign Gloves").rid("roadsigngloves"));
// The tools. The numbers of the gather and the damage are from the
// table of the remake; what a tool is for is ours.
define(ItemType.of(ROCK, "Rock")
.rid("rock")
.tool(ToolClass.NONE)
.durability(100)
.damage(20, 10, 8, 8)
.damage(10)
.bench(0)
.known(true)
.recipe(new Recipe(new int[]{STONE}, new int[]{10}, 5))
.gather(ResourceClass.TREE, 10, 1.0F, 0.3F)
.gather(ResourceClass.STONE, 5, 1.0F, 0.3F)
.gather(ResourceClass.METAL, 5, 1.0F, 0.3F)
.gather(ResourceClass.SULFUR, 5, 1.0F, 0.3F)
.gather(ResourceClass.FLESH, 10, 1.0F, 0.0F));
define(ItemType.of(STONE_HATCHET, "Stone Hatchet")
.rid("stoneaxe")
.tool(ToolClass.AXE)
.durability(100)
.damage(27, 15, 12, 12)
.durability(160)
.damage(15)
.recipe(new Recipe(new int[]{WOOD, STONE}, new int[]{200, 100}, 15))
.gather(ResourceClass.TREE, 20, 2.0F, 0.3F)
.gather(ResourceClass.FLESH, 10, 1.0F, 0.3F));
define(ItemType.of(STONE_PICKAXE, "Stone Pick Axe")
define(ItemType.of(STONE_PICKAXE, "Stone Pickaxe")
.rid("stonepickaxe")
.tool(ToolClass.PICKAXE)
.durability(100)
.damage(35, 17, 13, 13)
.durability(160)
.damage(17)
.recipe(new Recipe(new int[]{WOOD, STONE}, new int[]{200, 100}, 15))
.gather(ResourceClass.STONE, 16, 3.2F, 0.3F)
.gather(ResourceClass.METAL, 16, 3.2F, 0.3F)
.gather(ResourceClass.SULFUR, 16, 3.2F, 0.3F)
.gather(ResourceClass.FLESH, 10, 1.0F, 1.0F));
define(ItemType.of(HAMMER, "Hammer")
.rid("hammer")
.tool(ToolClass.HAMMER)
.durability(100)
.damage(0, 0, 0, 0)
.durability(300)
.damage(0)
.recipe(new Recipe(new int[]{WOOD}, new int[]{100}, 15)));
define(ItemType.of(TORCH, "Torch")
.rid("torch")
.recipe(new Recipe(new int[]{WOOD}, new int[]{10}, 5)));
// The tools that have to be researched. The price is the one of the
// research table, the tree is the one of the workbench.
define(ItemType.of(HATCHET, "Hatchet")
.rid("ironaxe")
.tool(ToolClass.AXE)
.durability(100)
.damage(25, 25, 25, 25)
.durability(300)
.damage(25)
.bench(1)
.known(false)
.research(30)
.tree(1, TechTree.PRICE_L1, SALVAGED_HAMMER)
.recipe(new Recipe(new int[]{WOOD, METAL}, new int[]{100, 75}, 30))
.gather(ResourceClass.TREE, 30, 3.0F, 0.3F)
.gather(ResourceClass.FLESH, 12, 1.2F, 1.0F));
.gather(ResourceClass.FLESH, 12, 1.0F, 1.0F));
define(ItemType.of(PICKAXE, "Pickaxe")
.rid("ironpickaxe")
.tool(ToolClass.PICKAXE)
.durability(100)
.damage(30, 30, 30, 30)
.durability(300)
.damage(30)
.bench(1)
.known(false)
.research(30)
.tree(1, TechTree.PRICE_L1, SALVAGED_HAMMER)
.recipe(new Recipe(new int[]{WOOD, METAL}, new int[]{100, 125}, 30))
.gather(ResourceClass.STONE, 30, 6.0F, 1.0F)
.gather(ResourceClass.METAL, 30, 6.0F, 1.0F)
.gather(ResourceClass.SULFUR, 30, 6.0F, 1.0F)
.gather(ResourceClass.FLESH, 5, 0.5F, 1.0F));
.gather(ResourceClass.FLESH, 5, 1.0F, 1.0F));
define(ItemType.of(SALVAGED_HAMMER, "Salvaged Hammer")
.rid("salvagedhammer")
.tool(ToolClass.HAMMER)
.durability(100)
.damage(40, 40, 40, 40)
.durability(400)
.damage(30)
.bench(1)
.known(false)
.recipe(new Recipe(new int[]{METAL_PIPE, METAL}, new int[]{1, 50}, 30))
.gather(ResourceClass.TREE, 20, 2.0F, 0.5F)
.gather(ResourceClass.STONE, 20, 4.0F, 0.5F)
.gather(ResourceClass.METAL, 20, 4.0F, 0.5F)
.gather(ResourceClass.FLESH, 10, 1.0F, 0.5F));
define(ItemType.of(SALVAGED_AXE, "Salvaged Axe")
.tool(ToolClass.AXE)
.durability(100)
.damage(40, 40, 40, 40)
.bench(2)
.known(false)
.recipe(new Recipe(new int[]{METAL_PIPE, METAL_BLADE}, new int[]{1, 5}, 30))
.gather(ResourceClass.TREE, 50, 5.0F, 0.3F)
.gather(ResourceClass.FLESH, 15, 1.5F, 1.0F));
define(ItemType.of(SALVAGED_ICEPICK, "Salvaged Icepick")
.tool(ToolClass.PICKAXE)
.durability(100)
.damage(40, 40, 40, 40)
.bench(2)
.known(false)
.recipe(new Recipe(new int[]{METAL_PIPE, METAL_BLADE}, new int[]{1, 5}, 30))
.gather(ResourceClass.STONE, 50, 10.0F, 0.3F)
.gather(ResourceClass.METAL, 50, 10.0F, 0.3F)
.gather(ResourceClass.SULFUR, 50, 10.0F, 0.3F)
.gather(ResourceClass.FLESH, 5, 0.5F, 0.5F));
.research(30)
.tree(1, TechTree.PRICE_L1, NOTHING)
.recipe(new Recipe(new int[]{METAL_PIPE, METAL}, new int[]{1, 50}, 30)));
define(ItemType.of(SALVAGED_SWORD, "Salvaged Sword")
.rid("salvagedsword")
.tool(ToolClass.SWORD)
.durability(100)
.damage(50, 50, 50, 50)
.durability(300)
.damage(50)
.bench(1)
.known(false)
.research(15)
.tree(1, TechTree.PRICE_L1, HATCHET)
.recipe(new Recipe(new int[]{METAL_BLADE, METAL}, new int[]{1, 15}, 30))
.gather(ResourceClass.FLESH, 10, 1.0F, 0.5F));
// The deployables and the furniture. They are not tools, so they
// gather nothing, and they are the things the players build with.
define(ItemType.of(SALVAGED_CLEAVER, "Salvaged Cleaver")
.rid("salvagedcleaver")
.tool(ToolClass.SWORD)
.durability(400)
.damage(60)
.bench(1)
.known(false)
.research(30)
.tree(1, TechTree.PRICE_L1, HATCHET)
.recipe(new Recipe(new int[]{ROAD_SIGNS, METAL}, new int[]{1, 50}, 30))
.gather(ResourceClass.FLESH, 10, 1.0F, 0.5F));
define(ItemType.of(SALVAGED_AXE, "Salvaged Axe")
.rid("salvagedaxe")
.tool(ToolClass.AXE)
.durability(1500)
.damage(40)
.bench(2)
.known(false)
.research(60)
.tree(2, TechTree.PRICE_L2, SALVAGED_HAMMER)
.recipe(new Recipe(new int[]{METAL_PIPE, METAL_BLADE}, new int[]{1, 5}, 30)));
define(ItemType.of(SALVAGED_ICEPICK, "Salvaged Icepick")
.rid("salvagedicepick")
.tool(ToolClass.PICKAXE)
.durability(1500)
.damage(40)
.bench(2)
.known(false)
.research(60)
.tree(2, TechTree.PRICE_L2, SALVAGED_HAMMER)
.recipe(new Recipe(new int[]{METAL_PIPE, METAL_BLADE}, new int[]{1, 5}, 30)));
// The places and the things in them.
define(ItemType.of(BUILDING_PLAN, "Building Plan")
.rid("building_plan")
.recipe(new Recipe(new int[]{WOOD}, new int[]{20}, 15)));
define(ItemType.of(WORKBENCH_1, "Work Bench Level 1")
.rid("workbenchlevel1")
.recipe(new Recipe(new int[]{WOOD, METAL}, new int[]{500, 100}, 30)));
define(ItemType.of(WORKBENCH_2, "Work Bench Level 2")
.rid("workbenchlevel2")
.bench(1)
.recipe(new Recipe(new int[]{METAL, HIGH_QUALITY_METAL, BASIC_FRAGMENT},
new int[]{500, 20, 5}, 45)));
define(ItemType.of(WORKBENCH_3, "Work Bench Level 3")
.rid("workbenchlevel3")
.bench(2)
.recipe(new Recipe(new int[]{METAL, HIGH_QUALITY_METAL, ADVANCED_FRAGMENT},
new int[]{1000, 100, 5}, 60)));
define(ItemType.of(RESEARCH_TABLE, "Research Table")
.rid("researchtable")
.bench(1)
.recipe(new Recipe(new int[]{METAL, SCRAP}, new int[]{200, 20}, 30)));
define(ItemType.of(WOODEN_DOOR, "Wooden Door")
define(ItemType.of(TOOL_CUPBOARD, "Tool Cupboard")
.rid("toolcupboard")
.recipe(new Recipe(new int[]{WOOD}, new int[]{1000}, 30)));
define(ItemType.of(CAMPFIRE, "Campfire")
.rid("campfire")
.recipe(new Recipe(new int[]{WOOD}, new int[]{100}, 15)));
define(ItemType.of(FURNACE, "Large Furnace")
.rid("furnace")
.bench(2)
.known(false)
.research(60)
.tree(2, TechTree.PRICE_L2, METAL_BLADE)
.recipe(new Recipe(new int[]{WOOD, STONE, LOW_GRADE_FUEL},
new int[]{600, 500, 75}, 40)));
define(ItemType.of(RECYCLER, "Recycler")
.rid("recycler")
.bench(2)
.known(false)
.research(60)
.tree(2, TechTree.PRICE_L2, METAL_BLADE)
.recipe(new Recipe(new int[]{METAL, METAL_PIPE, HIGH_QUALITY_METAL},
new int[]{150, 20, 10}, 30)));
define(ItemType.of(SLEEPING_BAG, "Sleeping Bag")
.rid("sleepingbag")
.recipe(new Recipe(new int[]{CLOTH, PLANT_FIBER}, new int[]{20, 20}, 15)));
define(ItemType.of(WOOD_STORAGE_BOX, "Wood Storage Box")
.rid("woodstoragebox")
.recipe(new Recipe(new int[]{WOOD}, new int[]{300}, 30)));
define(ItemType.of(LARGE_WOOD_BOX, "Large Wood Box")
.rid("largewoodbox")
.recipe(new Recipe(new int[]{WOOD}, new int[]{300}, 30)));
define(ItemType.of(WOODEN_DOOR, "Wooden Door")
.rid("woodendoor")
.recipe(new Recipe(new int[]{WOOD}, new int[]{300}, 30)));
define(ItemType.of(WOOD_DOUBLE_DOOR, "Double Wooden Door")
.rid("wooddoubledoor")
.recipe(new Recipe(new int[]{WOOD}, new int[]{350}, 30)));
define(ItemType.of(SHEETMETAL_DOOR, "Sheet Metal Door")
.rid("sheetmetaldoor")
.recipe(new Recipe(new int[]{METAL}, new int[]{150}, 30)));
define(ItemType.of(SHEETMETAL_DOUBLE_DOOR, "Double Sheet Metal Door")
.rid("sheetmetaldoubledoor")
.recipe(new Recipe(new int[]{METAL}, new int[]{200}, 30)));
define(ItemType.of(ARMORED_DOOR, "Armored Door")
.rid("armoreddoor")
.bench(3)
.known(false)
.research(120)
.tree(3, TechTree.PRICE_L3, METAL_PIPE)
.recipe(new Recipe(new int[]{HIGH_QUALITY_METAL, GEARS}, new int[]{20, 5}, 30)));
define(ItemType.of(ARMORED_DOUBLE_DOOR, "Double Armored Door")
.rid("armoreddoubledoor")
.bench(3)
.known(false)
.research(120)
.tree(3, TechTree.PRICE_L3, METAL_PIPE)
.recipe(new Recipe(new int[]{HIGH_QUALITY_METAL, GEARS}, new int[]{25, 5}, 30)));
define(ItemType.of(GARAGE_DOOR, "Garage Door")
.rid("garagedoor")
.bench(2)
.known(false)
.research(30)
.tree(2, TechTree.PRICE_L2, GEARS)
.recipe(new Recipe(new int[]{METAL, GEARS}, new int[]{300, 2}, 30)));
define(ItemType.of(CODE_LOCK, "Code Lock")
.rid("codelock")
.recipe(new Recipe(new int[]{METAL}, new int[]{100}, 30)));
define(ItemType.of(KEY_LOCK, "Key Lock")
.rid("keylock")
.recipe(new Recipe(new int[]{WOOD}, new int[]{75}, 15)));
define(ItemType.of(WOODEN_LADDER, "Wooden Ladder")
.rid("woodenladder")
.recipe(new Recipe(new int[]{WOOD}, new int[]{100}, 15)));
define(ItemType.of(FLOOR_GRILL, "Floor Grilling")
.rid("floorgrill")
.recipe(new Recipe(new int[]{METAL}, new int[]{100}, 30)));
define(ItemType.of(METAL_WINDOW_BARS, "Metal Window Bars")
.rid("metalwindowbars")
.recipe(new Recipe(new int[]{METAL}, new int[]{100}, 30)));
define(ItemType.of(WOODEN_WINDOW_BARS, "Wooden Window Bars")
.rid("woodenwindowbars")
.recipe(new Recipe(new int[]{WOOD}, new int[]{100}, 15)));
define(ItemType.of(STRENGTHENED_GLASS_WINDOW, "Strengthened Glass Window")
.rid("strengthenedglasswindow")
.recipe(new Recipe(new int[]{METAL}, new int[]{50}, 15)));
define(ItemType.of(REINFORCED_GLASS_WINDOW, "Reinforced Glass Window")
.rid("reinforcedglasswindow")
.recipe(new Recipe(new int[]{METAL}, new int[]{100}, 15)));
// The traps.
define(ItemType.of(HOMEMADE_LANDMINE, "Homemade Landmine")
.rid("homemadelandmine")
.recipe(new Recipe(new int[]{METAL, SCRAP}, new int[]{50, 10}, 15)));
define(ItemType.of(SHOTGUN_TRAP, "Shotgun Trap")
.rid("shotguntrap")
.known(false)
.research(60)
.recipe(new Recipe(new int[]{METAL, GEARS}, new int[]{200, 10}, 30)));
define(ItemType.of(SNAP_TRAP, "Snap Trap")
.rid("snaptrap")
.recipe(new Recipe(new int[]{WOOD, CLOTH}, new int[]{100, 10}, 15)));
define(ItemType.of(WOODEN_FLOOR_SPIKES, "Wooden Floor Spikes")
.rid("woodenfloorspikes")
.recipe(new Recipe(new int[]{WOOD, METAL}, new int[]{200, 10}, 15)));
// The things that only an administrator may use.
define(ItemType.of(EXPLOSIVES, "Explosives")
.rid("explosives")
.bench(3)
.known(false)
.research(120)
.tree(3, TechTree.PRICE_L3, METAL_BLADE)
.admin(true));
define(ItemType.of(TIMED_CHARGE, "Timed Explosive Charge")
.rid("timedexplosivecharge")
.bench(3)
.known(false)
.research(120)
.tree(3, TechTree.PRICE_L3, EXPLOSIVES)
.admin(true));
}
private static void define(ItemType.Builder builder) {
@@ -5,11 +5,13 @@ package net.rustnotmc.protocol;
*
* Everything about an item that the game needs to know and that never changes
* is here: what class of tool it is, how much it hurts, how fast it gathers,
* how long it lasts, and what it takes to make one.
* how long it lasts, what it takes to make one and how it can be researched.
*
* The numbers are the ones from the original game, at the state of November
* 2025, and they live in one place on purpose: a table that is copied by hand
* into two places drifts, and then the game stops matching its own rules.
* The numbers come from the table of {@link ItemData}, which was unpacked from
* the item database of the remake. The registry decides what an item is; the
* table says how much it does. A number that is written in the source wins
* over the table, so a change in the table never silently changes a hand
* written item.
*/
public final class ItemType {
@@ -38,6 +40,9 @@ public final class ItemType {
public static final int ARMS = 2;
public static final int LEGS = 3;
/** The id of the item in the remake, or null when it has none. */
public final String rid;
public final int id;
public final String name;
public final ToolClass tool;
@@ -60,17 +65,36 @@ public final class ItemType {
/** True when only the administrators of the server may use it. */
public final boolean admin;
/**
* The scrap the research table wants for the blueprint of this item.
* Zero means the item is not researched at all.
*/
public final int researchScrap;
/**
* The workbench whose tree holds this item. Zero means it is in no tree.
*/
public final int techTreeBench;
/** The scrap the node of this item in the tree costs. */
public final int techTreeScrap;
/** The item that has to be unlocked before this one. */
public final int techTreeParent;
/** How to make one, or null when the item is not made but gathered. */
public final Recipe recipe;
private final Gather[] gathers;
public ItemType(int id, String name, ToolClass tool, int harvestLevel, int durability,
int[] damage, int bench, boolean knownByDefault, boolean admin,
Recipe recipe, Gather[] gathers) {
public ItemType(String rid, int id, String name, ToolClass tool, int harvestLevel,
int durability, int[] damage, int bench, boolean knownByDefault,
boolean admin, int researchScrap, int techTreeBench, int techTreeScrap,
int techTreeParent, Recipe recipe, Gather[] gathers) {
if (damage == null || damage.length != 4) {
throw new IllegalArgumentException("the damage needs four body parts");
}
this.rid = rid;
this.id = id;
this.name = name;
this.tool = tool;
@@ -80,6 +104,10 @@ public final class ItemType {
this.bench = bench;
this.knownByDefault = knownByDefault;
this.admin = admin;
this.researchScrap = researchScrap;
this.techTreeBench = techTreeBench;
this.techTreeScrap = techTreeScrap;
this.techTreeParent = techTreeParent;
this.recipe = recipe;
this.gathers = gathers == null ? new Gather[ResourceClass.values().length] : gathers;
}
@@ -100,6 +128,7 @@ public final class ItemType {
private final int id;
private final String name;
private String rid;
private ToolClass tool = ToolClass.NONE;
private int harvestLevel;
private int durability;
@@ -107,6 +136,10 @@ public final class ItemType {
private int bench;
private boolean knownByDefault = true;
private boolean admin;
private int researchScrap;
private int techTreeBench;
private int techTreeScrap;
private int techTreeParent;
private Recipe recipe;
private final Gather[] gathers = new Gather[ResourceClass.values().length];
@@ -136,6 +169,17 @@ public final class ItemType {
return this;
}
/**
* The damage, split over the body the way the remake splits it: the
* head takes twice as much as the torso, the arms and the legs four
* fifths of it.
*/
public Builder damage(int torso) {
this.damage = new int[]{torso * 2, torso, Math.round(torso * 0.8f),
Math.round(torso * 0.8f)};
return this;
}
public Builder bench(int bench) {
this.bench = bench;
return this;
@@ -153,6 +197,20 @@ public final class ItemType {
return this;
}
/** The scrap the research table wants for this item. */
public Builder research(int scrap) {
this.researchScrap = scrap;
return this;
}
/** The node of this item in the tree of a workbench. */
public Builder tree(int bench, int scrap, int parent) {
this.techTreeBench = bench;
this.techTreeScrap = scrap;
this.techTreeParent = parent;
return this;
}
public Builder recipe(Recipe recipe) {
this.recipe = recipe;
return this;
@@ -165,10 +223,42 @@ public final class ItemType {
return this;
}
/**
* Fills the numbers of the item from the table.
*
* The id of the remake is the key: the table has one row per item, and
* the row says how much the item hurts, how long it lasts and how much
* it takes out of a tree or out of a node. Numbers that were written
* in the source are kept.
*/
public Builder rid(String rid) {
this.rid = rid;
ItemData.Row row = ItemData.byRid(rid);
if (row == null) {
throw new IllegalArgumentException("the table has no item " + rid);
}
if (this.damage[HEAD] == 0 && this.damage[TORSO] == 0 && row.damage > 0) {
damage(row.damage);
}
if (this.durability == 0 && row.durability > 0) {
this.durability = row.durability;
}
if (row.wood > 0 && this.gathers[ResourceClass.TREE.ordinal()] == null) {
gather(ResourceClass.TREE, row.wood, 1.0F, 0.3F);
}
if (row.ore > 0 && this.gathers[ResourceClass.STONE.ordinal()] == null) {
gather(ResourceClass.STONE, row.ore, 1.0F, 0.3F);
gather(ResourceClass.METAL, row.ore, 1.0F, 0.3F);
gather(ResourceClass.SULFUR, row.ore, 1.0F, 0.3F);
}
return this;
}
public ItemType build() {
return new ItemType(this.id, this.name, this.tool, this.harvestLevel,
return new ItemType(this.rid, this.id, this.name, this.tool, this.harvestLevel,
this.durability, this.damage, this.bench, this.knownByDefault,
this.admin, this.recipe, this.gathers);
this.admin, this.researchScrap, this.techTreeBench, this.techTreeScrap,
this.techTreeParent, this.recipe, this.gathers);
}
}
@@ -192,6 +282,16 @@ public final class ItemType {
return this.damage[bodyPart];
}
/** True when the item has to be researched before it can be made. */
public boolean needsResearch() {
return !this.knownByDefault && this.researchScrap > 0;
}
/** True when the item sits in a tree of a workbench. */
public boolean inTechTree() {
return this.techTreeBench > 0;
}
@Override
public String toString() {
return "ItemType[" + this.id + " " + this.name + "]";
@@ -0,0 +1,133 @@
package net.rustnotmc.protocol;
/**
* A thing in the world that can be worn down: a tree, a stone outcrop, a
* metal deposit.
*
* A node is not a block. It has a <b>pool</b> of resource in it, and the pool
* is also its health: every swing takes some of it out, and when the pool is
* empty the node is gone. The last swing gives whatever is left, so a node
* always gives exactly its pool.
*
* A node can also have a <b>mark</b>: a cross on a tree, a hot spot on an
* outcrop. A swing that hits the mark does more, and every swing that follows
* it does a little more still, up to a limit. Missing the mark resets the
* bonus.
*/
public final class NodeType {
/** The id of nothing. */
public static final int NOTHING = 0;
/** A poor tree: 500 wood. */
public static final int TREE_POOR = 1;
/** An ordinary tree: 750 wood. */
public static final int TREE_COMMON = 2;
/** A dead tree: 1000 wood. */
public static final int TREE_DEAD = 3;
/** A stone outcrop: 750 stones. */
public static final int STONE = 4;
/** A metal deposit: 500 metal. */
public static final int METAL = 5;
/** A sulfur deposit: 200 sulfur. */
public static final int SULFUR = 6;
public final int id;
public final String name;
public final ResourceClass resource;
/** What the node gives when it is worn out. */
public final int dropItem;
/** How much is in it. It is also the number of points of health. */
public final int pool;
/** How much a marked swing adds to the damage, again and again. */
public final float markerStep;
/** The limit of the mark. One means the node has no mark at all. */
public final float markerMax;
public NodeType(int id, String name, ResourceClass resource, int dropItem, int pool,
float markerStep, float markerMax) {
this.id = id;
this.name = name;
this.resource = resource;
this.dropItem = dropItem;
this.pool = pool;
this.markerStep = markerStep;
this.markerMax = markerMax;
}
/** True when the node has a mark that a swing can hit. */
public boolean hasMarker() {
return this.markerMax > 1.0F;
}
/**
* The damage of a swing, with the mark taken into account.
*
* @param damage what the tool takes without a mark
* @param marked how many marked swings in a row there were
*/
public int swingDamage(int damage, int marked) {
if (!hasMarker() || marked <= 0) {
return damage;
}
float multiplier = 1.0F + this.markerStep * marked;
if (multiplier > this.markerMax) {
multiplier = this.markerMax;
}
return Math.round(damage * multiplier);
}
private static final java.util.List<NodeType> TYPES = new java.util.ArrayList<NodeType>();
static {
add(new NodeType(NOTHING, "Nothing", ResourceClass.NONE, ItemRegistry.NOTHING, 0,
1.0F, 1.0F));
// A tree: the mark is a cross on the trunk, and it gives at most twice.
add(new NodeType(TREE_POOR, "Poor Tree", ResourceClass.TREE, ItemRegistry.WOOD, 500,
0.125F, 2.0F));
add(new NodeType(TREE_COMMON, "Tree", ResourceClass.TREE, ItemRegistry.WOOD, 750,
0.125F, 2.0F));
add(new NodeType(TREE_DEAD, "Dead Tree", ResourceClass.TREE, ItemRegistry.WOOD, 1000,
0.125F, 2.0F));
// An outcrop: the mark is a hot spot, and it gives at most three times.
add(new NodeType(STONE, "Stone Outcrop", ResourceClass.STONE, ItemRegistry.STONE, 750,
0.5F, 3.0F));
add(new NodeType(METAL, "Metal Deposit", ResourceClass.METAL, ItemRegistry.METAL, 500,
0.5F, 3.0F));
add(new NodeType(SULFUR, "Sulfur Deposit", ResourceClass.SULFUR, ItemRegistry.SULFUR, 200,
0.5F, 3.0F));
}
private static void add(NodeType type) {
TYPES.add(type);
}
/** The node with that id, or null. */
public static NodeType byId(int id) {
for (int i = 0; i < TYPES.size(); i++) {
if (TYPES.get(i).id == id) {
return TYPES.get(i);
}
}
return null;
}
/** Every node type. */
public static java.util.List<NodeType> all() {
return java.util.Collections.unmodifiableList(TYPES);
}
@Override
public String toString() {
return "NodeType[" + this.id + " " + this.name + " " + this.pool + "]";
}
}
@@ -113,6 +113,42 @@ public final class Packets {
/** One slot of the inventory, when it changes. */
public static final int INVENTORY_SLOT = 0x1D;
/** The client starts or stops digging. */
public static final int DIG = 0x1E;
/** How far the digging has come, for the cracks. */
public static final int BREAK_PROGRESS = 0x1F;
/** The client asks to make something. */
public static final int CRAFT_START = 0x20;
/** What the server says about a craft. */
public static final int CRAFT_UPDATE = 0x21;
/** The client asks to research an item at the research table. */
public static final int RESEARCH_START = 0x22;
/** What the server says about a research. */
public static final int RESEARCH_UPDATE = 0x23;
/** The client asks to open a node of the tree of a workbench. */
public static final int TECH_TREE_UNLOCK = 0x24;
/** The recipes the player knows. */
public static final int KNOWN_RECIPES = 0x25;
/** The nodes of the trees the player has opened. */
public static final int TECH_TREE_STATE = 0x26;
/** A container the player may look into. */
public static final int CONTAINER_OPEN = 0x27;
/** The client takes something out of a container. */
public static final int CONTAINER_TAKE = 0x28;
/** The client learns a blueprint. */
public static final int LEARN_BLUEPRINT = 0x29;
/**
* One slot of the inventory and what is in it now.
*
@@ -948,6 +984,394 @@ public final class Packets {
}
}
/**
* The client starts or stops digging.
*
* The client does not decide anything: it says where it is looking and
* whether the button is down. The server works out what happens there,
* how long it takes and who is allowed to do it.
*/
public static final class Dig implements Packet {
/** The button went down. */
public static final int START = 0;
/** The button came up. */
public static final int STOP = 1;
public final int action;
public final int x;
public final int y;
public final int z;
public final int face;
public Dig(int action, int x, int y, int z, int face) {
this.action = action;
this.x = x;
this.y = y;
this.z = z;
this.face = face;
}
public boolean start() {
return this.action == START;
}
public Buf write() {
Buf buf = new Buf(24);
buf.writeVarInt(this.action);
buf.writeVarInt(this.x);
buf.writeVarInt(this.y);
buf.writeVarInt(this.z);
buf.writeVarInt(this.face);
return buf;
}
public static Dig read(Buf in) {
return new Dig(in.readVarInt(), in.readVarInt(), in.readVarInt(),
in.readVarInt(), in.readVarInt());
}
}
/**
* How far the digging has come, so that everybody sees the cracks.
*
* The stage goes from zero to nine. A stage of minus one means that
* nothing is being dug at that place.
*/
public static final class BreakProgress implements Packet {
/** The stage of the cracks, from zero to nine. */
public static final int STAGES = 10;
public final int x;
public final int y;
public final int z;
public final int stage;
public BreakProgress(int x, int y, int z, int stage) {
this.x = x;
this.y = y;
this.z = z;
this.stage = stage;
}
public Buf write() {
Buf buf = new Buf(24);
buf.writeVarInt(this.x);
buf.writeVarInt(this.y);
buf.writeVarInt(this.z);
buf.writeVarInt(this.stage);
return buf;
}
public static BreakProgress read(Buf in) {
return new BreakProgress(in.readVarInt(), in.readVarInt(), in.readVarInt(),
in.readVarInt());
}
}
/**
* The client asks to make something.
*
* The server looks the recipe up, checks the workbench and the knowledge
* of the player, and answers with a {@link CraftUpdate}.
*/
public static final class CraftStart implements Packet {
public final int item;
public final int count;
public CraftStart(int item, int count) {
this.item = item;
this.count = count;
}
public Buf write() {
Buf buf = new Buf(8);
buf.writeVarInt(this.item);
buf.writeVarInt(this.count);
return buf;
}
public static CraftStart read(Buf in) {
return new CraftStart(in.readVarInt(), in.readVarInt());
}
}
/**
* What the server says about a craft.
*
* The seconds left go down while the work goes on. When it reaches zero
* the item is in the inventory. A reason that is not empty means that
* nothing was made, and says why.
*/
public static final class CraftUpdate implements Packet {
public final int item;
public final int count;
public final int secondsLeft;
public final String reason;
public CraftUpdate(int item, int count, int secondsLeft, String reason) {
this.item = item;
this.count = count;
this.secondsLeft = secondsLeft;
this.reason = reason == null ? "" : reason;
}
public boolean ok() {
return this.reason.isEmpty();
}
public Buf write() {
Buf buf = new Buf(64);
buf.writeVarInt(this.item);
buf.writeVarInt(this.count);
buf.writeVarInt(this.secondsLeft);
buf.writeString(this.reason);
return buf;
}
public static CraftUpdate read(Buf in) {
return new CraftUpdate(in.readVarInt(), in.readVarInt(), in.readVarInt(),
in.readString(MAX_TEXT));
}
}
/**
* The client asks to research an item at the research table.
*
* The server checks that the player has the item and the scrap, takes
* both and answers with a {@link ResearchUpdate}.
*/
public static final class ResearchStart implements Packet {
public final int item;
public ResearchStart(int item) {
this.item = item;
}
public Buf write() {
Buf buf = new Buf(8);
buf.writeVarInt(this.item);
return buf;
}
public static ResearchStart read(Buf in) {
return new ResearchStart(in.readVarInt());
}
}
/** What the server says about a research. */
public static final class ResearchUpdate implements Packet {
public final int item;
public final int secondsLeft;
public final String reason;
public ResearchUpdate(int item, int secondsLeft, String reason) {
this.item = item;
this.secondsLeft = secondsLeft;
this.reason = reason == null ? "" : reason;
}
public boolean ok() {
return this.reason.isEmpty();
}
public Buf write() {
Buf buf = new Buf(64);
buf.writeVarInt(this.item);
buf.writeVarInt(this.secondsLeft);
buf.writeString(this.reason);
return buf;
}
public static ResearchUpdate read(Buf in) {
return new ResearchUpdate(in.readVarInt(), in.readVarInt(), in.readString(MAX_TEXT));
}
}
/**
* The client asks to open a node of the tree of a workbench.
*
* The server checks that the branch is open and that there is enough
* scrap, and takes the scrap.
*/
public static final class TechTreeUnlock implements Packet {
public final int item;
public TechTreeUnlock(int item) {
this.item = item;
}
public Buf write() {
Buf buf = new Buf(8);
buf.writeVarInt(this.item);
return buf;
}
public static TechTreeUnlock read(Buf in) {
return new TechTreeUnlock(in.readVarInt());
}
}
/** The recipes the player knows. */
public static final class KnownRecipes implements Packet {
public final int[] items;
public KnownRecipes(int[] items) {
this.items = items;
}
public Buf write() {
Buf buf = new Buf(8 + this.items.length * 4);
buf.writeVarInt(this.items.length);
for (int i = 0; i < this.items.length; i++) {
buf.writeVarInt(this.items[i]);
}
return buf;
}
public static KnownRecipes read(Buf in) {
int count = in.readVarInt();
if (count < 0 || count > 4096) {
throw new ProtocolException("that many recipes is not real");
}
int[] items = new int[count];
for (int i = 0; i < count; i++) {
items[i] = in.readVarInt();
}
return new KnownRecipes(items);
}
}
/** The nodes of the trees the player has opened. */
public static final class TechTreeState implements Packet {
public final int[] items;
public TechTreeState(int[] items) {
this.items = items;
}
public Buf write() {
Buf buf = new Buf(8 + this.items.length * 4);
buf.writeVarInt(this.items.length);
for (int i = 0; i < this.items.length; i++) {
buf.writeVarInt(this.items[i]);
}
return buf;
}
public static TechTreeState read(Buf in) {
int count = in.readVarInt();
if (count < 0 || count > 4096) {
throw new ProtocolException("that many nodes is not real");
}
int[] items = new int[count];
for (int i = 0; i < count; i++) {
items[i] = in.readVarInt();
}
return new TechTreeState(items);
}
}
/**
* A container the player may look into: a crate of food on a junk pile,
* a chest, a barrel.
*/
public static final class ContainerOpen implements Packet {
public final int id;
public final String title;
public final Slot[] slots;
public ContainerOpen(int id, String title, Slot[] slots) {
this.id = id;
this.title = title == null ? "" : title;
this.slots = slots;
}
public Buf write() {
Buf buf = new Buf(64 + this.slots.length * 12);
buf.writeVarInt(this.id);
buf.writeString(this.title);
buf.writeVarInt(this.slots.length);
for (int i = 0; i < this.slots.length; i++) {
Slot slot = this.slots[i] == null ? Slot.of(null) : this.slots[i];
slot.write(buf);
}
return buf;
}
public static ContainerOpen read(Buf in) {
int id = in.readVarInt();
String title = in.readString(MAX_TEXT);
int count = in.readVarInt();
if (count < 0 || count > 1024) {
throw new ProtocolException("a container of " + count + " slots is not real");
}
Slot[] slots = new Slot[count];
for (int i = 0; i < count; i++) {
slots[i] = Slot.read(in);
}
return new ContainerOpen(id, title, slots);
}
}
/** The client takes something out of a container. */
public static final class ContainerTake implements Packet {
public final int id;
public final int slot;
public ContainerTake(int id, int slot) {
this.id = id;
this.slot = slot;
}
public Buf write() {
Buf buf = new Buf(8);
buf.writeVarInt(this.id);
buf.writeVarInt(this.slot);
return buf;
}
public static ContainerTake read(Buf in) {
return new ContainerTake(in.readVarInt(), in.readVarInt());
}
}
/**
* The client asks to learn a blueprint.
*
* A blueprint is the item with a mark on it. Learning it uses it up and
* opens the recipe for good.
*/
public static final class LearnBlueprint implements Packet {
public final int item;
public LearnBlueprint(int item) {
this.item = item;
}
public Buf write() {
Buf buf = new Buf(8);
buf.writeVarInt(this.item);
return buf;
}
public static LearnBlueprint read(Buf in) {
return new LearnBlueprint(in.readVarInt());
}
}
/**
* Every packet id this build knows, and what it decodes into.
*
@@ -974,6 +1398,20 @@ public final class Packets {
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(INVENTORY), "Inventory");
map.put(Integer.valueOf(INVENTORY_SLOT), "SlotChange");
map.put(Integer.valueOf(DIG), "Dig");
map.put(Integer.valueOf(BREAK_PROGRESS), "BreakProgress");
map.put(Integer.valueOf(CRAFT_START), "CraftStart");
map.put(Integer.valueOf(CRAFT_UPDATE), "CraftUpdate");
map.put(Integer.valueOf(RESEARCH_START), "ResearchStart");
map.put(Integer.valueOf(RESEARCH_UPDATE), "ResearchUpdate");
map.put(Integer.valueOf(TECH_TREE_UNLOCK), "TechTreeUnlock");
map.put(Integer.valueOf(KNOWN_RECIPES), "KnownRecipes");
map.put(Integer.valueOf(TECH_TREE_STATE), "TechTreeState");
map.put(Integer.valueOf(CONTAINER_OPEN), "ContainerOpen");
map.put(Integer.valueOf(CONTAINER_TAKE), "ContainerTake");
map.put(Integer.valueOf(LEARN_BLUEPRINT), "LearnBlueprint");
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
return map;
}
@@ -0,0 +1,133 @@
package net.rustnotmc.protocol;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* The trees of the workbenches.
*
* Every workbench has its own tree, and a node in it costs scrap. To open a
* node the one before it has to be open: that is what makes a tree a path and
* not a list. Unlocking a node is personal, it cannot be given away, and the
* item it opens needs no blueprint afterwards.
*
* The prices come from the wiki of the original game: 125 scrap for the tree
* of the first workbench, 250 for the second, 500 for the third and 250 for
* the engineering workbench.
*/
public final class TechTree {
/** One node of a tree. */
public static final class Node {
/** The item this node opens. */
public final int item;
/** The item that has to be open first, or the id of nothing. */
public final int parent;
/** The workbench whose tree this node is in. */
public final int bench;
/** What the node costs. */
public final int scrap;
public Node(int item, int parent, int bench, int scrap) {
this.item = item;
this.parent = parent;
this.bench = bench;
this.scrap = scrap;
}
}
/** The price of a node in the tree of the first workbench. */
public static final int PRICE_L1 = 125;
/** The price of a node in the tree of the second workbench. */
public static final int PRICE_L2 = 250;
/** The price of a node in the tree of the third workbench. */
public static final int PRICE_L3 = 500;
/** The price of a node in the tree of the engineering workbench. */
public static final int PRICE_ENGINEERING = 250;
private static final List<Node> NODES = new ArrayList<Node>();
static {
// The tree of the first workbench: the tools, one after another.
add(ItemRegistry.SALVAGED_HAMMER, ItemRegistry.NOTHING, 1, PRICE_L1);
add(ItemRegistry.HATCHET, ItemRegistry.SALVAGED_HAMMER, 1, PRICE_L1);
add(ItemRegistry.PICKAXE, ItemRegistry.SALVAGED_HAMMER, 1, PRICE_L1);
add(ItemRegistry.SALVAGED_SWORD, ItemRegistry.HATCHET, 1, PRICE_L1);
add(ItemRegistry.SALVAGED_CLEAVER, ItemRegistry.HATCHET, 1, PRICE_L1);
// The tree of the second workbench: the salvage tools and the machine.
add(ItemRegistry.SALVAGED_AXE, ItemRegistry.SALVAGED_HAMMER, 2, PRICE_L2);
add(ItemRegistry.SALVAGED_ICEPICK, ItemRegistry.SALVAGED_HAMMER, 2, PRICE_L2);
add(ItemRegistry.METAL_BLADE, ItemRegistry.SALVAGED_AXE, 2, PRICE_L2);
add(ItemRegistry.GARAGE_DOOR, ItemRegistry.GEARS, 2, PRICE_L2);
// The tree of the third workbench: the heavy stuff.
add(ItemRegistry.GEARS, ItemRegistry.METAL_BLADE, 3, PRICE_L3);
add(ItemRegistry.METAL_PIPE, ItemRegistry.GEARS, 3, PRICE_L3);
add(ItemRegistry.ARMORED_DOOR, ItemRegistry.METAL_PIPE, 3, PRICE_L3);
add(ItemRegistry.EXPLOSIVES, ItemRegistry.METAL_BLADE, 3, PRICE_L3);
add(ItemRegistry.TIMED_CHARGE, ItemRegistry.EXPLOSIVES, 3, PRICE_L3);
}
private static void add(int item, int parent, int bench, int scrap) {
NODES.add(new Node(item, parent, bench, scrap));
}
private TechTree() {
}
/** Every node of every tree. */
public static List<Node> nodes() {
return Collections.unmodifiableList(NODES);
}
/** The node that opens an item, or null when the item is in no tree. */
public static Node forItem(int item) {
for (int i = 0; i < NODES.size(); i++) {
if (NODES.get(i).item == item) {
return NODES.get(i);
}
}
return null;
}
/** What it costs to open an item, or zero when the item is in no tree. */
public static int costOf(int item) {
Node node = forItem(item);
return node == null ? 0 : node.scrap;
}
/**
* Whether the item can be opened with what is open already.
*
* A node needs its parent, and the parent needs its own parent, and so on
* down to the root of the branch.
*/
public static boolean canOpen(int item, java.util.Set<Integer> unlocked) {
Node node = forItem(item);
if (node == null) {
return false;
}
if (unlocked.contains(Integer.valueOf(item))) {
return true;
}
int parent = node.parent;
while (parent != ItemRegistry.NOTHING) {
if (!unlocked.contains(Integer.valueOf(parent))) {
return false;
}
Node above = forItem(parent);
if (above == null) {
return false;
}
parent = above.parent;
}
return true;
}
}
@@ -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);