From cda7198c764d776285e4763965c310bbcbd7459d Mon Sep 17 00:00:00 2001
From: sasheg dev <152582704+SashegDev@users.noreply.github.com>
Date: Wed, 21 Oct 2026 19:54:22 +0300
Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D0=BF?=
=?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=BC=D0=B5=D1=82=D0=BE=D0=B2=20=D0=BF=D0=BE?=
=?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D0=B5=20=D1=80=D0=B5=D0=BC=D0=B5=D0=B9?=
=?UTF-8?q?=D0=BA=D0=B0:=20=D1=83=D0=B7=D0=BB=D1=8B,=20=D0=B4=D0=B5=D1=80?=
=?UTF-8?q?=D0=B5=D0=B2=D1=8C=D1=8F,=20=D0=B7=D0=BD=D0=B0=D0=BD=D0=B8?=
=?UTF-8?q?=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Протокол:
- 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). Форк собирается чисто.
---
PLAN.md | 16 +-
rsx-protocol/scripts/build.ps1 | 9 +
rsx-protocol/scripts/test.ps1 | 13 +
.../java/net/rustnotmc/protocol/Blocks.java | 6 +-
.../java/net/rustnotmc/protocol/ItemData.java | 173 +++++++
.../net/rustnotmc/protocol/ItemRegistry.java | 435 +++++++++++++----
.../java/net/rustnotmc/protocol/ItemType.java | 118 ++++-
.../java/net/rustnotmc/protocol/NodeType.java | 133 ++++++
.../java/net/rustnotmc/protocol/Packets.java | 438 ++++++++++++++++++
.../java/net/rustnotmc/protocol/TechTree.java | 133 ++++++
.../java/net/rustnotmc/protocol/ItemTest.java | 353 ++++++++++++++
.../net/rustnotmc/protocol/ProtocolTest.java | 15 +-
rsx-server-core/scripts/test.ps1 | 2 +
.../net/rustnotmc/server/ClientSession.java | 177 ++++++-
.../java/net/rustnotmc/server/Player.java | 75 ++-
.../java/net/rustnotmc/server/Server.java | 21 +-
.../net/rustnotmc/server/craft/Crafter.java | 111 +++++
.../net/rustnotmc/server/craft/Knowledge.java | 135 ++++++
.../rustnotmc/server/craft/ResearchTable.java | 138 ++++++
.../net/rustnotmc/server/world/JunkPile.java | 294 ++++++++++++
.../rustnotmc/server/world/ResourceNode.java | 98 ++++
.../rustnotmc/server/world/RoadNetwork.java | 114 +++++
.../server/world/WorldGenerator.java | 48 +-
.../net/rustnotmc/server/craft/CraftTest.java | 240 ++++++++++
.../net/rustnotmc/server/world/NodeTest.java | 222 +++++++++
25 files changed, 3409 insertions(+), 108 deletions(-)
create mode 100644 rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemData.java
create mode 100644 rsx-protocol/src/main/java/net/rustnotmc/protocol/NodeType.java
create mode 100644 rsx-protocol/src/main/java/net/rustnotmc/protocol/TechTree.java
create mode 100644 rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/craft/Crafter.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/craft/Knowledge.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/craft/ResearchTable.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/world/JunkPile.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/world/ResourceNode.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/world/RoadNetwork.java
create mode 100644 rsx-server-core/src/test/java/net/rustnotmc/server/craft/CraftTest.java
create mode 100644 rsx-server-core/src/test/java/net/rustnotmc/server/world/NodeTest.java
diff --git a/PLAN.md b/PLAN.md
index 5ada27a..828cae8 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -971,12 +971,16 @@ rsx-client\scripts\run-game.ps1 # клиент
### 6.3 Что не сделано (по порядку)
-1. **Шаг A** — добить: клиентский рендер своих предметов (хотбар и рука) и иконки
- из атласа; поля исследования в `ItemType`; `NodeType`; `TechTree`.
-2. **Шаг B** — `Dig`/`BreakProgress` в протоколе, состояние ломания на сервере,
- правила инструмента, тег `admin`, 4 клиентских патча.
-3. **Шаг C** — узлы (дерево/камень/металл/сера) с метками, дороги, мусорка,
- крафт и исследование (стол + тех-дерево), постройка по сокетам.
+1. **Шаг A** — сделано: поля исследования и дерева в `ItemType`, числа из таблицы
+ ремейка (`ItemData`), `NodeType`, `TechTree`, 218 предметов в реестре.
+ Не сделано: **клиентский рендер своих предметов** (хотбар и рука) и иконки из атласа.
+2. **Шаг B** — сделано: `Dig`/`BreakProgress` в протоколе, проверка досягаемости
+ на сервере, тег `admin` в типах предметов. Не сделано: узлы в мире (шаг C),
+ трещины на клиенте, 4 клиентских патча.
+3. **Шаг C** — сделано: `ResourceNode` (HP, запас, метка), `JunkPile` (бутылки,
+ ящик с едой, таймер исчезновения), `RoadNetwork` + дороги в генераторе
+ (сетка и кольцо, ровная по ширине). Не сделано: мусорка в мире (спавн и
+ респавн у дороги), станции крафта (верстак в мире), постройка по сокетам.
4. **Шаг D** — конфиг, мир 4000×4000 один раз, барьер.
5. **Шаг E** — транспорт на NIO.
6. **Шаг F** — DistantView.
diff --git a/rsx-protocol/scripts/build.ps1 b/rsx-protocol/scripts/build.ps1
index c34f6d7..c0e971b 100644
--- a/rsx-protocol/scripts/build.ps1
+++ b/rsx-protocol/scripts/build.ps1
@@ -42,6 +42,15 @@ if ($LASTEXITCODE -ne 0) {
}
Say "protocol compiled ($($mainFiles.Count) files)"
+# The resources travel with the jar: the item table and the sound registry
+# live in src\main\resources, and both ends of the wire read them.
+$resources = Join-Path $Repo 'src\main\resources'
+if (Test-Path -LiteralPath $resources) {
+ Copy-Item -Path (Join-Path $resources '*') -Destination $mainOut -Recurse -Force
+ $resourceFiles = @(Get-ChildItem $resources -Recurse -File)
+ Say "resources copied ($($resourceFiles.Count) files)"
+}
+
$manifest = Join-Path $Out 'manifest.txt'
@"
Manifest-Version: 1.0
diff --git a/rsx-protocol/scripts/test.ps1 b/rsx-protocol/scripts/test.ps1
index eb01e50..37deda6 100644
--- a/rsx-protocol/scripts/test.ps1
+++ b/rsx-protocol/scripts/test.ps1
@@ -21,6 +21,12 @@ $mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter
& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -d $mainOut $mainFiles
if ($LASTEXITCODE -ne 0) { Write-Host ' Main failed.' -ForegroundColor Red; exit 1 }
+# The tests read the item table, so it has to be on the class path as well.
+$resources = Join-Path $Repo 'src\main\resources'
+if (Test-Path -LiteralPath $resources) {
+ Copy-Item -Path (Join-Path $resources '*') -Destination $mainOut -Recurse -Force
+}
+
$testOut = Join-Path $Out 'test-classes'
Remove-Item $testOut -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $testOut | Out-Null
@@ -38,5 +44,12 @@ if ($LASTEXITCODE -ne 0) {
exit 1
}
+& java -cp "$mainOut;$testOut" net.rustnotmc.protocol.ItemTest
+if ($LASTEXITCODE -ne 0) {
+ Write-Host ''
+ Write-Host ' ITEM TESTS FAILED' -ForegroundColor Red
+ exit 1
+}
+
Write-Host ''
Write-Host ' Protocol tests passed.' -ForegroundColor Green
\ No newline at end of file
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java
index fc1d56b..6ed17f4 100644
--- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Blocks.java
@@ -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 + ")";
}
}
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemData.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemData.java
new file mode 100644
index 0000000..fb453f3
--- /dev/null
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemData.java
@@ -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 is: 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 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 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 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 all() {
+ return ROWS;
+ }
+
+ /** How many items the table knows. */
+ public static int size() {
+ return ROWS.size();
+ }
+
+ private static Map load() {
+ Map rows = new HashMap();
+ 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 map = new HashMap();
+ 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 out = new java.util.ArrayList();
+ 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()]);
+ }
+}
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java
index 40fac4f..4e123af 100644
--- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemRegistry.java
@@ -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 TYPES = new LinkedHashMap();
@@ -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) {
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemType.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemType.java
index 83a5682..6953a5a 100644
--- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemType.java
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/ItemType.java
@@ -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 + "]";
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/NodeType.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/NodeType.java
new file mode 100644
index 0000000..4166a49
--- /dev/null
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/NodeType.java
@@ -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 pool 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 mark: 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 TYPES = new java.util.ArrayList();
+
+ 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 all() {
+ return java.util.Collections.unmodifiableList(TYPES);
+ }
+
+ @Override
+ public String toString() {
+ return "NodeType[" + this.id + " " + this.name + " " + this.pool + "]";
+ }
+}
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java
index 6c9a45c..d49b420 100644
--- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java
@@ -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;
}
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/TechTree.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/TechTree.java
new file mode 100644
index 0000000..6520223
--- /dev/null
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/TechTree.java
@@ -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 NODES = new ArrayList();
+
+ 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 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 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;
+ }
+}
diff --git a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java
new file mode 100644
index 0000000..13abf96
--- /dev/null
+++ b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java
@@ -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 failures = new ArrayList();
+
+ 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 nothing = new HashSet();
+ 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 withHammer = new HashSet();
+ 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 withHatchet = new HashSet(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);
+ }
+ }
+}
diff --git a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java
index a9078bd..dc79f72 100644
--- a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java
+++ b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java
@@ -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);
diff --git a/rsx-server-core/scripts/test.ps1 b/rsx-server-core/scripts/test.ps1
index d60b550..fc05bc0 100644
--- a/rsx-server-core/scripts/test.ps1
+++ b/rsx-server-core/scripts/test.ps1
@@ -47,7 +47,9 @@ Say "compiled $($mainFiles.Count) main + $($testFiles.Count) test files"
$suites = @(
'net.rustnotmc.server.ServerTest',
'net.rustnotmc.server.world.WorldTest',
+ 'net.rustnotmc.server.world.NodeTest',
'net.rustnotmc.server.item.InventoryTest',
+ 'net.rustnotmc.server.craft.CraftTest',
'net.rustnotmc.server.PlayTest'
)
foreach ($suite in $suites) {
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java
index 921c9c7..1fdd424 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java
@@ -9,12 +9,19 @@ import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import net.rustnotmc.protocol.Buf;
+import net.rustnotmc.protocol.Blocks;
import net.rustnotmc.protocol.ChunkFormat;
import net.rustnotmc.protocol.Frame;
import net.rustnotmc.protocol.FrameDecoder;
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemType;
import net.rustnotmc.protocol.Packet;
import net.rustnotmc.protocol.Packets;
import net.rustnotmc.protocol.ProtocolException;
+import net.rustnotmc.protocol.TechTree;
+import net.rustnotmc.server.craft.Crafter;
+import net.rustnotmc.server.craft.Knowledge;
+import net.rustnotmc.server.craft.ResearchTable;
/**
* One connection, and the handshake that has to finish before it means
@@ -207,6 +214,21 @@ public final class ClientSession implements Runnable {
case Packets.BLOCK_CHANGE:
onBlockChange(frame);
return true;
+ case Packets.DIG:
+ onDig(frame);
+ return true;
+ case Packets.CRAFT_START:
+ onCraftStart(frame);
+ return true;
+ case Packets.RESEARCH_START:
+ onResearchStart(frame);
+ return true;
+ case Packets.TECH_TREE_UNLOCK:
+ onTechTreeUnlock(frame);
+ return true;
+ case Packets.LEARN_BLUEPRINT:
+ onLearnBlueprint(frame);
+ return true;
case Packets.PLAYER_MOVE:
onPlayerMove(frame);
return true;
@@ -444,6 +466,12 @@ public final class ClientSession implements Runnable {
// what the player is carrying comes with the world: the server owns
// the bag, so the client is told what is in it
send(Packets.INVENTORY, this.player.inventory().toPacket());
+ // and what the player knows, so that the client can grey out the
+ // recipes that are not known yet
+ send(Packets.KNOWN_RECIPES,
+ new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
+ send(Packets.TECH_TREE_STATE,
+ new Packets.TechTreeState(this.player.knowledge().unlockedNodes()));
this.player.streamChunks(this.server.world());
}
@@ -494,11 +522,156 @@ public final class ClientSession implements Runnable {
}
}
- /** Where the player is now. */
- private void onPlayerMove(Frame frame) {
+ /**
+ * The player is digging.
+ *
+ * The client says where it is looking and whether the button is down. The
+ * server decides what happens there: whether the player is close enough,
+ * whether it is allowed to break that, and how far the work has come.
+ *
+ * The nodes of the world are not blocks, so digging at one of them is
+ * answered by the node and not by the world. That comes with the nodes
+ * themselves; until then a dig at a block breaks it in the creative mode
+ * and does nothing in the survival one.
+ */
+ private void onDig(Frame frame) {
if (this.state != SessionState.PLAYING || this.player == null) {
return;
}
+ Packets.Dig dig = Packets.Dig.read(new Buf(frame.body()));
+
+ if (!dig.start()) {
+ // the button came up: the cracks go away
+ send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1));
+ return;
+ }
+ if (!this.player.canReach(dig.x, dig.y, dig.z)) {
+ send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1));
+ return;
+ }
+ if (this.player.mayBreakBlocks()) {
+ if (this.server.world().setBlock(dig.x, dig.y, dig.z, Blocks.AIR)) {
+ this.server.broadcastBlockChange(dig.x, dig.y, dig.z, Blocks.AIR);
+ }
+ }
+ send(Packets.BREAK_PROGRESS, new Packets.BreakProgress(dig.x, dig.y, dig.z, -1));
+ }
+
+ /**
+ * The player wants to make something.
+ *
+ * The server checks the recipe, the workbench and the ingredients, and
+ * says no otherwise.
+ */
+ private void onCraftStart(Frame frame) {
+ if (this.state != SessionState.PLAYING || this.player == null) {
+ return;
+ }
+ Packets.CraftStart start = Packets.CraftStart.read(new Buf(frame.body()));
+ ItemType type = ItemRegistry.byId(start.item);
+ int bench = this.player.benchLevel();
+ String problem = Crafter.whyNot(type, this.player.knowledge(), bench,
+ this.player.inventory());
+ if (problem != null) {
+ send(Packets.CRAFT_UPDATE, new Packets.CraftUpdate(start.item, 0, 0, problem));
+ return;
+ }
+ int seconds = Crafter.seconds(type, bench);
+ this.player.startCraft(start.item, seconds);
+ send(Packets.CRAFT_UPDATE, new Packets.CraftUpdate(start.item, start.count,
+ seconds, ""));
+ }
+
+ /**
+ * The player wants to research an item.
+ *
+ * The table takes the scrap at once and the item when the work is done.
+ */
+ private void onResearchStart(Frame frame) {
+ if (this.state != SessionState.PLAYING || this.player == null) {
+ return;
+ }
+ Packets.ResearchStart start = Packets.ResearchStart.read(new Buf(frame.body()));
+ ResearchTable table = this.player.research();
+ String problem = table.start(start.item, this.player.inventory());
+ send(Packets.RESEARCH_UPDATE, new Packets.ResearchUpdate(start.item,
+ table.secondsLeft(), problem == null ? "" : problem));
+ if (problem == null) {
+ send(Packets.INVENTORY, this.player.inventory().toPacket());
+ }
+ }
+
+ /**
+ * The player wants to open a node of the tree of a workbench.
+ *
+ * The branch has to be open and there has to be enough scrap. The scrap
+ * is taken and the node opens, and the client is told what the player
+ * knows now.
+ */
+ private void onTechTreeUnlock(Frame frame) {
+ if (this.state != SessionState.PLAYING || this.player == null) {
+ return;
+ }
+ Packets.TechTreeUnlock unlock = Packets.TechTreeUnlock.read(new Buf(frame.body()));
+ Knowledge knowledge = this.player.knowledge();
+ String problem = knowledge.reasonCannotUnlock(unlock.item);
+ if (problem == null) {
+ int cost = TechTree.costOf(unlock.item);
+ if (this.player.inventory().count(ItemRegistry.SCRAP) < cost) {
+ problem = "you are short of scrap";
+ } else {
+ this.player.inventory().take(ItemRegistry.SCRAP, cost);
+ knowledge.unlock(unlock.item);
+ }
+ }
+ if (problem != null) {
+ this.server.log("tree: " + problem + " (" + this.account + ")");
+ }
+ send(Packets.TECH_TREE_STATE, new Packets.TechTreeState(knowledge.unlockedNodes()));
+ send(Packets.KNOWN_RECIPES, new Packets.KnownRecipes(knowledge.knownRecipes()));
+ send(Packets.INVENTORY, this.player.inventory().toPacket());
+ }
+
+ /**
+ * The player learned a blueprint.
+ *
+ * The blueprint is used up and the recipe opens for good.
+ */
+ private void onLearnBlueprint(Frame frame) {
+ if (this.state != SessionState.PLAYING || this.player == null) {
+ return;
+ }
+ Packets.LearnBlueprint learn = Packets.LearnBlueprint.read(new Buf(frame.body()));
+ if (!ResearchTable.learn(this.player.inventory(), this.player.knowledge(),
+ learn.item)) {
+ return;
+ }
+ send(Packets.INVENTORY, this.player.inventory().toPacket());
+ send(Packets.KNOWN_RECIPES,
+ new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
+ }
+
+ /**
+ * How the research of a player is going.
+ *
+ * The work goes on even when the player walks away from the table.
+ */
+ void onResearchProgress(int item, int secondsLeft, boolean done) {
+ if (this.player == null) {
+ return;
+ }
+ send(Packets.RESEARCH_UPDATE, new Packets.ResearchUpdate(item, secondsLeft, ""));
+ if (done) {
+ send(Packets.INVENTORY, this.player.inventory().toPacket());
+ send(Packets.KNOWN_RECIPES,
+ new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
+ }
+ }
+
+ /** Where the player is now. */
+ private void onPlayerMove(Frame frame) { if (this.state != SessionState.PLAYING || this.player == null) {
+ return;
+ }
Packets.PlayerMove move = Packets.PlayerMove.read(new Buf(frame.body()));
// a position that is not a number would spread through the whole
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java b/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java
index 6c0fa0e..5191e33 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/Player.java
@@ -5,7 +5,10 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import net.rustnotmc.protocol.Blocks;
+import net.rustnotmc.protocol.ItemRegistry;
import net.rustnotmc.protocol.Packets;
+import net.rustnotmc.server.craft.Knowledge;
+import net.rustnotmc.server.craft.ResearchTable;
import net.rustnotmc.server.item.Inventory;
import net.rustnotmc.server.world.Chunk;
import net.rustnotmc.server.world.ChunkPos;
@@ -53,6 +56,16 @@ public final class Player {
/** What the player is carrying. The server owns it. */
private final Inventory inventory;
+ /** What the player knows: the recipes and the nodes of the trees. */
+ private final Knowledge knowledge;
+
+ /** The research table the player is working at, if any. */
+ private final ResearchTable research;
+
+ /** What the player is making, and how much of it is left to do. */
+ private int craftItem = ItemRegistry.NOTHING;
+ private int craftSecondsLeft;
+
Player(ClientSession session, int playerId, int entityId, String account,
double[] spawn) {
this(session, playerId, entityId, account, spawn,
@@ -69,6 +82,8 @@ public final class Player {
this.y = spawn[1];
this.z = spawn[2];
this.inventory = inventory;
+ this.knowledge = new Knowledge();
+ this.research = new ResearchTable();
}
/** What the player is carrying. */
@@ -76,6 +91,38 @@ public final class Player {
return this.inventory;
}
+ /** What the player knows. */
+ public Knowledge knowledge() {
+ return this.knowledge;
+ }
+
+ /** The research table the player is working at. */
+ public ResearchTable research() {
+ return this.research;
+ }
+
+ /** What the player is making, or nothing. */
+ public int craftItem() {
+ return this.craftItem;
+ }
+
+ /** How many seconds are left of the work, or zero. */
+ public int craftSecondsLeft() {
+ return this.craftSecondsLeft;
+ }
+
+ /** Starts a craft. */
+ public void startCraft(int item, int seconds) {
+ this.craftItem = item;
+ this.craftSecondsLeft = seconds;
+ }
+
+ /** Stops the craft that is going on. */
+ public void stopCraft() {
+ this.craftItem = ItemRegistry.NOTHING;
+ this.craftSecondsLeft = 0;
+ }
+
// -------------------------------------------------------------- getters
public ClientSession session() {
@@ -266,15 +313,35 @@ public final class Player {
if (!Blocks.isValid(id)) {
return false;
}
- double dx = (x + 0.5D) - this.x;
- double dy = (y + 0.5D) - this.y;
- double dz = (z + 0.5D) - this.z;
- if (dx * dx + dy * dy + dz * dz > REACH * REACH) {
+ if (!canReach(x, y, z)) {
return false;
}
return world.setBlock(x, y, z, id);
}
+ /**
+ * Whether the player is close enough to have plausibly touched a block.
+ *
+ * Without this, a client can rewrite the world from the other side of it.
+ */
+ public boolean canReach(int x, int y, int z) {
+ double dx = (x + 0.5D) - this.x;
+ double dy = (y + 0.5D) - this.y;
+ double dz = (z + 0.5D) - this.z;
+ return dx * dx + dy * dy + dz * dz <= REACH * REACH;
+ }
+
+ /**
+ * The level of the workbench the player is standing next to.
+ *
+ * Zero means there is none, and then only the things that need no
+ * workbench can be made. The workbenches that are put down in the world
+ * come with the places of the world; until then this is always zero.
+ */
+ public int benchLevel() {
+ return 0;
+ }
+
/** Floor division that works for negative numbers, unlike a cast. */
private static int floor(double value) {
int truncated = (int) value;
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java
index 3cc305b..aba7687 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java
@@ -321,8 +321,22 @@ public final class Server {
}
}
- /** One server tick. The place the world will live. */
- private void tickOnce() {
+ /**
+ * Lets the research of a player go on.
+ *
+ * The work is measured in seconds, so it is looked at once a second.
+ */
+ private void tickResearch(Player player) {
+ net.rustnotmc.server.craft.ResearchTable table = player.research();
+ if (!table.busy()) {
+ return;
+ }
+ int item = table.item();
+ boolean done = table.tick(player.inventory());
+ player.session().onResearchProgress(item, table.secondsLeft(), done);
+ }
+
+ /** One server tick. The place the world will live. */ private void tickOnce() {
this.ticks.incrementAndGet();
this.world.tick();
// chunks nobody has walked near are dropped once a second; the limit
@@ -330,6 +344,9 @@ public final class Server {
// notice, not often enough to be right
if (this.ticks.get() % 20L == 0L) {
this.world.evictIdle();
+ for (Player player : this.players.values()) {
+ tickResearch(player);
+ }
}
if ((this.ticks.get() & 0x3FFFL) == 0L) {
this.tokens.sweep();
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/craft/Crafter.java b/rsx-server-core/src/main/java/net/rustnotmc/server/craft/Crafter.java
new file mode 100644
index 0000000..c36bef7
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/craft/Crafter.java
@@ -0,0 +1,111 @@
+package net.rustnotmc.server.craft;
+
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemType;
+import net.rustnotmc.protocol.Recipe;
+import net.rustnotmc.server.item.Inventory;
+
+/**
+ * Making things.
+ *
+ * Three things have to be true before a thing is made: the player knows the
+ * recipe, there is a workbench of the right level within reach, and the
+ * ingredients are in the bag. The server checks all three and says no
+ * otherwise, because a client that decides for itself can be made to make
+ * anything.
+ */
+public final class Crafter {
+
+ private Crafter() {
+ }
+
+ /**
+ * Why the item cannot be made, or null when it can.
+ *
+ * The reasons are words a player can read.
+ */
+ public static String whyNot(ItemType type, Knowledge knowledge, int benchLevel,
+ Inventory inventory) {
+ if (type == null) {
+ return "there is no such thing";
+ }
+ if (type.recipe == null) {
+ return "it is not made, it is found";
+ }
+ if (type.admin) {
+ return "only the administrators of the server may have that";
+ }
+ if (!knowledge.knows(type.id)) {
+ return "you do not know how to make that";
+ }
+ if (type.bench > benchLevel) {
+ return "you need a workbench of level " + type.bench;
+ }
+ for (int i = 0; i < type.recipe.items.length; i++) {
+ int needed = type.recipe.counts[i];
+ if (inventory.count(type.recipe.items[i]) < needed) {
+ return "you are short of " + nameOf(type.recipe.items[i]);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Makes the item, if it can be made.
+ *
+ * The ingredients are taken out of the bag and the result is put in. When
+ * the bag is full the work is not done: the ingredients stay where they
+ * are.
+ */
+ public static boolean craft(ItemType type, int count, Knowledge knowledge,
+ int benchLevel, Inventory inventory) {
+ if (count <= 0) {
+ return false;
+ }
+ if (whyNot(type, knowledge, benchLevel, inventory) != null) {
+ return false;
+ }
+ Recipe recipe = type.recipe;
+ for (int i = 0; i < recipe.items.length; i++) {
+ inventory.take(recipe.items[i], recipe.counts[i] * count);
+ }
+ if (!inventory.add(net.rustnotmc.protocol.ItemStack.of(type.id, count))) {
+ // The bag is full: give the ingredients back rather than eat them.
+ for (int i = 0; i < recipe.items.length; i++) {
+ inventory.add(net.rustnotmc.protocol.ItemStack.of(recipe.items[i],
+ recipe.counts[i] * count));
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * How long the work takes, in seconds.
+ *
+ * A workbench above the one the recipe asks for makes it faster: half as
+ * long for the first level above, three quarters off for the second, and
+ * no more after that.
+ */
+ public static int seconds(ItemType type, int benchLevel) {
+ if (type == null || type.recipe == null) {
+ return 0;
+ }
+ int required = type.bench;
+ int levels = benchLevel - required;
+ if (levels < 0) {
+ levels = 0;
+ }
+ if (levels > 2) {
+ levels = 2;
+ }
+ double factor = Math.pow(0.5D, levels);
+ int seconds = (int) Math.floor(type.recipe.seconds * factor);
+ return seconds < 1 ? 1 : seconds;
+ }
+
+ private static String nameOf(int item) {
+ ItemType type = ItemRegistry.byId(item);
+ return type == null ? ("item " + item) : type.name;
+ }
+}
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/craft/Knowledge.java b/rsx-server-core/src/main/java/net/rustnotmc/server/craft/Knowledge.java
new file mode 100644
index 0000000..5c8dcc4
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/craft/Knowledge.java
@@ -0,0 +1,135 @@
+package net.rustnotmc.server.craft;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemType;
+import net.rustnotmc.protocol.TechTree;
+
+/**
+ * What a player knows.
+ *
+ * There are two kinds of knowledge, and they are kept apart on purpose:
+ *
+ *
+ * - the recipes the player can make. A recipe that is known by
+ * default is known from the start; anything else has to be researched,
+ * either at the research table or by opening a node in the tree of a
+ * workbench.
+ *
- the nodes the player has opened in the trees. This is what the
+ * tree of a workbench is: a set of things that were paid for with scrap.
+ *
+ *
+ * The knowledge belongs to the player and not to the item, so two players can
+ * know different things at the same moment.
+ */
+public final class Knowledge {
+
+ private final Set learned = new HashSet();
+ private final Set unlocked = new HashSet();
+
+ /**
+ * Whether the player can make the item.
+ *
+ * An item that is known by default needs nothing. Everything else has to
+ * have been researched, and the research is what the table or the tree
+ * gives.
+ */
+ public boolean knows(int item) {
+ ItemType type = ItemRegistry.byId(item);
+ if (type == null) {
+ return false;
+ }
+ if (type.knownByDefault) {
+ return true;
+ }
+ if (this.unlocked.contains(Integer.valueOf(item))) {
+ return true;
+ }
+ return this.learned.contains(Integer.valueOf(item));
+ }
+
+ /** The player learned a recipe, from a blueprint. */
+ public void learn(int item) {
+ this.learned.add(Integer.valueOf(item));
+ }
+
+ /** Whether the player has opened this node of a tree. */
+ public boolean isUnlocked(int item) {
+ return this.unlocked.contains(Integer.valueOf(item));
+ }
+
+ /** The player opened a node of a tree. */
+ public void unlock(int item) {
+ this.unlocked.add(Integer.valueOf(item));
+ }
+
+ /** What a blueprint of this item would open. */
+ public void learnFromBlueprint(int item) {
+ learn(item);
+ }
+
+ /**
+ * Whether the node can be opened, and what it costs.
+ *
+ * The cost is zero when the node cannot be opened at all: there is no
+ * node for an item that is in no tree, and a node whose parent is still
+ * closed is not for sale either.
+ */
+ public boolean canUnlock(int item) {
+ return reasonCannotUnlock(item) == null;
+ }
+
+ /**
+ * Why the node cannot be opened, or null when it can.
+ *
+ * The reasons are words a player can read: "no such node", "the branch is
+ * not open yet".
+ */
+ public String reasonCannotUnlock(int item) {
+ TechTree.Node node = TechTree.forItem(item);
+ if (node == null) {
+ return "there is no such node";
+ }
+ if (isUnlocked(item)) {
+ return "it is already open";
+ }
+ if (!TechTree.canOpen(item, this.unlocked)) {
+ return "the branch is not open yet";
+ }
+ return null;
+ }
+
+ /** The recipes the player knows, for the client to draw. */
+ public int[] knownRecipes() {
+ List out = new ArrayList();
+ for (ItemType type : ItemRegistry.all()) {
+ if (knows(type.id)) {
+ out.add(Integer.valueOf(type.id));
+ }
+ }
+ int[] ids = new int[out.size()];
+ for (int i = 0; i < ids.length; i++) {
+ ids[i] = out.get(i).intValue();
+ }
+ return ids;
+ }
+
+ /** The nodes the player has opened, for the client to draw. */
+ public int[] unlockedNodes() {
+ int[] ids = new int[this.unlocked.size()];
+ int i = 0;
+ for (Integer id : this.unlocked) {
+ ids[i++] = id.intValue();
+ }
+ return ids;
+ }
+
+ /** The number of recipes the player knows. */
+ public int knownCount() {
+ return knownRecipes().length;
+ }
+}
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/craft/ResearchTable.java b/rsx-server-core/src/main/java/net/rustnotmc/server/craft/ResearchTable.java
new file mode 100644
index 0000000..169888a
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/craft/ResearchTable.java
@@ -0,0 +1,138 @@
+package net.rustnotmc.server.craft;
+
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemStack;
+import net.rustnotmc.protocol.ItemType;
+import net.rustnotmc.server.item.Inventory;
+
+/**
+ * The research table.
+ *
+ * Researching an item means taking it apart to find out what it is made of,
+ * so the player has to have the item and the scrap to pay for the table. The
+ * item does not survive: it turns into a blueprint, which is the same item
+ * with a mark on it, and a blueprint can be learned, or given away.
+ *
+ * The work takes ten seconds, and it goes on even when the player walks away
+ * from the table.
+ */
+public final class ResearchTable {
+
+ /** How long the work takes, in seconds. */
+ public static final int SECONDS = 10;
+
+ /** The mark that turns a stack into a blueprint. */
+ public static final String BLUEPRINT = "blueprint";
+
+ private int item = ItemRegistry.NOTHING;
+ private int secondsLeft;
+
+ /** What is being researched, or nothing. */
+ public int item() {
+ return this.item;
+ }
+
+ /** How much is left of the work, in seconds. */
+ public int secondsLeft() {
+ return this.secondsLeft;
+ }
+
+ /** True when something is being researched. */
+ public boolean busy() {
+ return this.item != ItemRegistry.NOTHING;
+ }
+
+ /**
+ * Starts the work.
+ *
+ * The player has to have the item and the scrap. The scrap is taken at
+ * once; the item is taken when the work is done, so that a player who
+ * walks away does not lose the item for nothing.
+ *
+ * Returns the reason it did not start, or null when it did.
+ */
+ public String start(int item, Inventory inventory) {
+ if (busy()) {
+ return "the table is busy";
+ }
+ ItemType type = ItemRegistry.byId(item);
+ if (type == null || type.recipe == null) {
+ return "there is nothing to find out about that";
+ }
+ if (type.knownByDefault) {
+ return "you already know how to make that";
+ }
+ if (type.researchScrap <= 0) {
+ return "that cannot be researched";
+ }
+ if (inventory.count(item) <= 0) {
+ return "you do not have that";
+ }
+ if (inventory.count(ItemRegistry.SCRAP) < type.researchScrap) {
+ return "you are short of scrap";
+ }
+ inventory.take(ItemRegistry.SCRAP, type.researchScrap);
+ this.item = item;
+ this.secondsLeft = SECONDS;
+ return null;
+ }
+
+ /**
+ * Lets the work go on.
+ *
+ * Returns true when the research finished, which is when the item turns
+ * into a blueprint in the bag.
+ */
+ public boolean tick(Inventory inventory) {
+ if (!busy()) {
+ return false;
+ }
+ this.secondsLeft--;
+ if (this.secondsLeft > 0) {
+ return false;
+ }
+ int done = this.item;
+ this.item = ItemRegistry.NOTHING;
+ this.secondsLeft = 0;
+ if (inventory.count(done) <= 0) {
+ return false;
+ }
+ inventory.take(done, 1);
+ ItemStack blueprint = ItemStack.of(done, 1);
+ blueprint.set(BLUEPRINT, "1");
+ inventory.add(blueprint);
+ return true;
+ }
+
+ /**
+ * Whether a stack is a blueprint.
+ *
+ * A blueprint is the item with a mark on it, so the client draws the
+ * blueprint texture and the item on top of it.
+ */
+ public static boolean isBlueprint(ItemStack stack) {
+ return stack != null && "1".equals(stack.get(BLUEPRINT));
+ }
+
+ /**
+ * Learns a blueprint: the recipe opens and the blueprint is used up.
+ *
+ * Returns false when there was no blueprint of that item.
+ */
+ public static boolean learn(Inventory inventory, Knowledge knowledge, int item) {
+ for (int i = 0; i < Inventory.SLOTS; i++) {
+ ItemStack stack = inventory.get(i);
+ if (stack.item != item || !isBlueprint(stack)) {
+ continue;
+ }
+ if (stack.count <= 1) {
+ inventory.set(i, ItemStack.EMPTY);
+ } else {
+ stack.count--;
+ }
+ knowledge.learn(item);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/JunkPile.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/JunkPile.java
new file mode 100644
index 0000000..080e831
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/JunkPile.java
@@ -0,0 +1,294 @@
+package net.rustnotmc.server.world;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemStack;
+
+/**
+ * A heap of rubbish at the side of the road.
+ *
+ * It is the first place where scrap comes from, and the only one that is
+ * there before the monuments are. A heap is two to four barrels and, now and
+ * then, a box of food. The barrels are broken, the box is looked into, and
+ * when there is nothing left in it the heap goes away after a while and shows
+ * up again somewhere else along the road.
+ *
+ * The barrels are the ones of the original game, with the names of the colours
+ * they are painted in:
+ *
+ *
+ * - the grey one holds scrap and one piece of rubbish;
+ *
- the blue one holds a part;
+ *
- the red one is a barrel of fuel: crude oil and low grade fuel.
+ *
+ */
+public final class JunkPile {
+
+ /** A barrel with scrap and rubbish in it. */
+ public static final int GREY = 0;
+
+ /** A barrel with parts in it. */
+ public static final int BLUE = 1;
+
+ /** A barrel of fuel. */
+ public static final int RED = 2;
+
+ /** How long the heap stays after it is empty, at least. */
+ public static final int MIN_LIFE = 60;
+
+ /** How long the heap stays after it is empty, at most. */
+ public static final int MAX_LIFE = 180;
+
+ /** The rubbish a grey barrel can give. */
+ private static final int[] RUBBISH = {
+ ItemRegistry.ROAD_SIGNS, ItemRegistry.TARP, ItemRegistry.ROPE,
+ ItemRegistry.EMPTY_PROPANE_TANK, ItemRegistry.SEWING_KIT,
+ ItemRegistry.BANDANA_MASK, ItemRegistry.TANK_TOP, ItemRegistry.SHORTS,
+ ItemRegistry.T_SHIRT, ItemRegistry.PANTS, ItemRegistry.LEATHER_GLOVES,
+ ItemRegistry.BASEBALL_CAP, ItemRegistry.BOONIE_HAT, ItemRegistry.HIDE_PANTS,
+ ItemRegistry.HIDE_BOOTS, ItemRegistry.HIDE_SKIRT, ItemRegistry.HIDE_CHEST,
+ ItemRegistry.HIDE_PONCHO, ItemRegistry.ROADSIGN_JACKET, ItemRegistry.ROADSIGN_KILT,
+ ItemRegistry.ROADSIGN_GLOVES,
+ };
+
+ /** The parts a blue barrel can give. */
+ private static final int[] PARTS = {
+ ItemRegistry.METAL_BLADE, ItemRegistry.METAL_PIPE, ItemRegistry.GEARS,
+ ItemRegistry.METAL_SPRING, ItemRegistry.SHEET_METAL, ItemRegistry.ROAD_SIGNS,
+ ItemRegistry.ELECTRIC_FUSE, ItemRegistry.ROPE, ItemRegistry.TARP,
+ ItemRegistry.SEWING_KIT, ItemRegistry.EMPTY_PROPANE_TANK,
+ ItemRegistry.SEMI_AUTO_BODY, ItemRegistry.TECH_TRASH,
+ };
+
+ /** What a box of food can hold. */
+ private static final int[] FOOD = {
+ ItemRegistry.APPLE, ItemRegistry.BLUEBERRIES, ItemRegistry.BLACK_RASPBERRIES,
+ ItemRegistry.POTATO, ItemRegistry.CHOCOLATE_BAR, ItemRegistry.CAN_OF_BEANS,
+ ItemRegistry.CAN_OF_TUNA, ItemRegistry.GRANOLA_BAR, ItemRegistry.CORN,
+ ItemRegistry.PUMPKIN, ItemRegistry.MUSHROOM,
+ };
+
+ /** What a box of food can hold instead of food. */
+ private static final int[] DRINK = {
+ ItemRegistry.SMALL_WATER_BOTTLE, ItemRegistry.WATER_JUG,
+ };
+
+ /** One barrel of the heap. */
+ public static final class Barrel {
+
+ public final int kind;
+ public final int x;
+ public final int y;
+ public final int z;
+ private boolean broken;
+
+ Barrel(int kind, int x, int y, int z) {
+ this.kind = kind;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ public boolean isBroken() {
+ return this.broken;
+ }
+
+ void broken() {
+ this.broken = true;
+ }
+ }
+
+ /** One box of food of the heap. */
+ public static final class FoodBox {
+
+ public final int x;
+ public final int y;
+ public final int z;
+ private final List content = new ArrayList();
+
+ FoodBox(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ /** What is in the box. */
+ public List content() {
+ return Collections.unmodifiableList(this.content);
+ }
+
+ boolean empty() {
+ return this.content.isEmpty();
+ }
+ }
+
+ private final int x;
+ private final int y;
+ private final int z;
+ private final List barrels = new ArrayList();
+ private final List boxes = new ArrayList();
+ private int despawnIn = -1;
+
+ /**
+ * Makes a heap.
+ *
+ * Two to four barrels and, from time to time, a box of food. The barrels
+ * stand in a row along the road, and the box is next to them.
+ */
+ public JunkPile(int x, int y, int z, Random random) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+
+ int count = 2 + random.nextInt(3);
+ for (int i = 0; i < count; i++) {
+ int kind = random.nextInt(3);
+ this.barrels.add(new Barrel(kind, x + i * 2, y, z));
+ }
+ if (random.nextInt(3) == 0) {
+ FoodBox box = new FoodBox(x, y, z + 2);
+ int slots = 1 + random.nextInt(2);
+ for (int i = 0; i < slots; i++) {
+ int item;
+ if (random.nextInt(4) == 0) {
+ item = DRINK[random.nextInt(DRINK.length)];
+ } else {
+ item = FOOD[random.nextInt(FOOD.length)];
+ }
+ box.content.add(ItemStack.of(item, 1));
+ }
+ this.boxes.add(box);
+ }
+ }
+
+ public int x() {
+ return this.x;
+ }
+
+ public int y() {
+ return this.y;
+ }
+
+ public int z() {
+ return this.z;
+ }
+
+ /** The barrels of the heap. */
+ public List barrels() {
+ return Collections.unmodifiableList(this.barrels);
+ }
+
+ /** The boxes of food of the heap. */
+ public List boxes() {
+ return Collections.unmodifiableList(this.boxes);
+ }
+
+ /** True when there is nothing left in the heap. */
+ public boolean isEmpty() {
+ for (int i = 0; i < this.barrels.size(); i++) {
+ if (!this.barrels.get(i).isBroken()) {
+ return false;
+ }
+ }
+ for (int i = 0; i < this.boxes.size(); i++) {
+ if (!this.boxes.get(i).empty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Breaks a barrel and gives what is in it.
+ *
+ * Returns an empty list when the barrel is already broken or is not one
+ * of this heap.
+ */
+ public List breakBarrel(Barrel barrel, Random random) {
+ if (barrel == null || barrel.isBroken()) {
+ return Collections.emptyList();
+ }
+ boolean mine = false;
+ for (int i = 0; i < this.barrels.size(); i++) {
+ if (this.barrels.get(i) == barrel) {
+ mine = true;
+ break;
+ }
+ }
+ if (!mine) {
+ return Collections.emptyList();
+ }
+ barrel.broken();
+ List loot = new ArrayList();
+ if (barrel.kind == GREY) {
+ int scrap = 1 + random.nextInt(15);
+ loot.add(ItemStack.of(ItemRegistry.SCRAP, scrap));
+ loot.add(ItemStack.of(RUBBISH[random.nextInt(RUBBISH.length)], 1));
+ } else if (barrel.kind == BLUE) {
+ loot.add(ItemStack.of(PARTS[random.nextInt(PARTS.length)], 1));
+ } else {
+ loot.add(ItemStack.of(ItemRegistry.CRUDE_OIL, 10 + random.nextInt(11)));
+ loot.add(ItemStack.of(ItemRegistry.LOW_GRADE_FUEL, 5 + random.nextInt(11)));
+ }
+ if (isEmpty()) {
+ scheduleDespawn(random);
+ }
+ return loot;
+ }
+
+ /**
+ * Takes something out of a box of food.
+ *
+ * Returns null when the slot is empty or the box is not one of this heap.
+ */
+ public ItemStack takeFromBox(FoodBox box, int slot) {
+ return takeFromBox(box, slot, new Random());
+ }
+
+ /**
+ * Takes something out of a box of food.
+ *
+ * The random is only used to say when the heap goes away once it is empty.
+ */
+ public ItemStack takeFromBox(FoodBox box, int slot, Random random) {
+ if (box == null || slot < 0 || slot >= box.content.size()) {
+ return null;
+ }
+ ItemStack taken = box.content.remove(slot);
+ if (isEmpty()) {
+ scheduleDespawn(random);
+ }
+ return taken;
+ }
+
+ /**
+ * Says when the heap goes away.
+ *
+ * It does not go at once: the heap stays a little while, and then it is
+ * gone and another one shows up somewhere else.
+ */
+ public void scheduleDespawn(Random random) {
+ this.despawnIn = MIN_LIFE + random.nextInt(MAX_LIFE - MIN_LIFE + 1);
+ }
+
+ /** How many seconds are left before the heap goes, or -1. */
+ public int despawnIn() {
+ return this.despawnIn;
+ }
+
+ /**
+ * Lets the heap go.
+ *
+ * Returns true when the heap is gone.
+ */
+ public boolean tick() {
+ if (this.despawnIn < 0) {
+ return false;
+ }
+ this.despawnIn--;
+ return this.despawnIn <= 0;
+ }
+}
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/ResourceNode.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/ResourceNode.java
new file mode 100644
index 0000000..e1a60a5
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/ResourceNode.java
@@ -0,0 +1,98 @@
+package net.rustnotmc.server.world;
+
+import net.rustnotmc.protocol.ItemType;
+import net.rustnotmc.protocol.NodeType;
+import net.rustnotmc.protocol.ResourceClass;
+
+/**
+ * A thing in the world that can be worn down.
+ *
+ * A node has a pool of resource in it, and the pool is also its health. A
+ * swing of the right tool takes some of it out; the last swing gives what is
+ * left, so a node always gives exactly its pool.
+ *
+ * A node can have a mark: 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. A swing that misses resets the bonus.
+ */
+public final class ResourceNode {
+
+ private final NodeType type;
+ private final int x;
+ private final int y;
+ private final int z;
+ private int pool;
+ private int markStreak;
+
+ public ResourceNode(NodeType type, int x, int y, int z) {
+ this.type = type;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.pool = type.pool;
+ }
+
+ public NodeType type() {
+ return this.type;
+ }
+
+ public int x() {
+ return this.x;
+ }
+
+ public int y() {
+ return this.y;
+ }
+
+ public int z() {
+ return this.z;
+ }
+
+ /** How much is left in it. */
+ public int pool() {
+ return this.pool;
+ }
+
+ /** True when there is nothing left in it. */
+ public boolean isEmpty() {
+ return this.pool <= 0;
+ }
+
+ /** How many marked swings in a row there were. */
+ public int markStreak() {
+ return this.markStreak;
+ }
+
+ /**
+ * A swing of a tool.
+ *
+ * Returns how much was taken out, or zero when the tool cannot take
+ * anything out of this node: a pickaxe does not cut a tree and a sword
+ * does not mine.
+ */
+ public int hit(ItemType tool, boolean marked) {
+ if (tool == null || isEmpty()) {
+ return 0;
+ }
+ ItemType.Gather gather = tool.gather(this.type.resource);
+ if (gather == null || gather.damage <= 0) {
+ return 0;
+ }
+ int damage = gather.damage;
+ if (marked && this.type.hasMarker()) {
+ this.markStreak++;
+ damage = this.type.swingDamage(damage, this.markStreak);
+ } else {
+ this.markStreak = 0;
+ }
+ int taken = damage < this.pool ? damage : this.pool;
+ this.pool -= taken;
+ return taken;
+ }
+
+ @Override
+ public String toString() {
+ return "ResourceNode[" + this.type.name + " at " + this.x + ", " + this.y + ", "
+ + this.z + " " + this.pool + "]";
+ }
+}
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/RoadNetwork.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/RoadNetwork.java
new file mode 100644
index 0000000..d1803c0
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/RoadNetwork.java
@@ -0,0 +1,114 @@
+package net.rustnotmc.server.world;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The roads of the world.
+ *
+ * A grid of straight roads crosses the island and a ring goes around it. The
+ * roads are where the junk piles stand, so the network also knows the points
+ * where a heap may appear.
+ *
+ * Nothing here depends on a random number: the roads are the same in every
+ * world of the same size, because a road that moved between two runs would
+ * leave the save files full of holes.
+ */
+public final class RoadNetwork {
+
+ /** How many blocks to the side of the middle of a road. */
+ public static final int HALF_WIDTH = 3;
+
+ /** How far apart the straight roads are. */
+ private final int spacing;
+
+ /** The radius of the ring around the island. */
+ private final int ringRadius;
+
+ /** The size of the world. */
+ private final int size;
+
+ public RoadNetwork(int size) {
+ this.size = size;
+ this.spacing = Math.max(256, size / 4);
+ this.ringRadius = Math.max(128, size / 3);
+ }
+
+ /** How far apart the straight roads are. */
+ public int spacing() {
+ return this.spacing;
+ }
+
+ /** The radius of the ring. */
+ public int ringRadius() {
+ return this.ringRadius;
+ }
+
+ /** The middle of the world. */
+ public int center() {
+ return this.size / 2;
+ }
+
+ /** The middle of the straight road that is nearest to the column. */
+ public int centerLine(int value) {
+ return (int) Math.round(value / (double) this.spacing) * this.spacing;
+ }
+
+ /** True when the column is on a straight road. */
+ public boolean onStraightRoad(int x, int z) {
+ return Math.abs(x - centerLine(x)) <= HALF_WIDTH
+ || Math.abs(z - centerLine(z)) <= HALF_WIDTH;
+ }
+
+ /**
+ * The point of the ring that is nearest to the column, or null when the
+ * column is not on the ring.
+ */
+ public int[] onRing(int x, int z) {
+ int cx = center();
+ int cz = center();
+ double dx = x - cx;
+ double dz = z - cz;
+ double distance = Math.sqrt(dx * dx + dz * dz);
+ if (distance < 0.001D) {
+ return null;
+ }
+ if (Math.abs(distance - this.ringRadius) > HALF_WIDTH) {
+ return null;
+ }
+ double angle = Math.atan2(dz, dx);
+ int px = cx + (int) Math.round(Math.cos(angle) * this.ringRadius);
+ int pz = cz + (int) Math.round(Math.sin(angle) * this.ringRadius);
+ return new int[]{px, pz};
+ }
+
+ /** True when the column is on any road. */
+ public boolean onRoad(int x, int z) {
+ return onStraightRoad(x, z) || onRing(x, z) != null;
+ }
+
+ /**
+ * The points along the roads where a junk pile may stand.
+ *
+ * The points are given as columns; the height of the ground there is the
+ * business of the generator.
+ */
+ public List spawnPoints() {
+ List points = new ArrayList();
+ int half = this.size / 2;
+ int step = 64;
+ for (int v = -half; v <= half; v += step) {
+ points.add(new int[]{v, center()});
+ points.add(new int[]{center(), v});
+ }
+ // and a ring of points along the ring road
+ int around = Math.max(8, this.ringRadius * 2 / step);
+ for (int i = 0; i < around; i++) {
+ double angle = 2.0D * Math.PI * i / around;
+ points.add(new int[]{center() + (int) Math.round(Math.cos(angle) * this.ringRadius),
+ center() + (int) Math.round(Math.sin(angle) * this.ringRadius)});
+ }
+ return Collections.unmodifiableList(points);
+ }
+}
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java b/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java
index 520789e..01d4d88 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/world/WorldGenerator.java
@@ -19,15 +19,26 @@ import net.rustnotmc.protocol.ChunkFormat;
public final class WorldGenerator {
private final long seed;
+ private final RoadNetwork roads;
public WorldGenerator(long seed) {
+ this(seed, null);
+ }
+
+ public WorldGenerator(long seed, RoadNetwork roads) {
this.seed = seed;
+ this.roads = roads;
}
public long seed() {
return this.seed;
}
+ /** The roads of this world, or null when it has none. */
+ public RoadNetwork roads() {
+ return this.roads;
+ }
+
/**
* A hash of one coordinate, and nothing else.
*
@@ -89,6 +100,40 @@ public final class WorldGenerator {
/** The height of solid ground at a block column. */
public int heightAt(int blockX, int blockZ) {
+ if (this.roads != null) {
+ int roadHeight = roadHeightAt(blockX, blockZ);
+ if (roadHeight >= 0) {
+ return roadHeight;
+ }
+ }
+ return terrainHeightAt(blockX, blockZ);
+ }
+
+ /**
+ * The height of a road at a column, or -1 when the column is not on one.
+ *
+ * A road is level along its length: the ground under it is the ground of
+ * the middle of the road, so that a road does not roll up and down with
+ * every bump of the land.
+ */
+ private int roadHeightAt(int blockX, int blockZ) {
+ int middleX = this.roads.centerLine(blockX);
+ int middleZ = this.roads.centerLine(blockZ);
+ if (Math.abs(blockX - middleX) <= RoadNetwork.HALF_WIDTH) {
+ return terrainHeightAt(middleX, blockZ);
+ }
+ if (Math.abs(blockZ - middleZ) <= RoadNetwork.HALF_WIDTH) {
+ return terrainHeightAt(blockX, middleZ);
+ }
+ int[] ring = this.roads.onRing(blockX, blockZ);
+ if (ring != null) {
+ return terrainHeightAt(ring[0], ring[1]);
+ }
+ return -1;
+ }
+
+ /** The height of the land itself, with no road on it. */
+ private int terrainHeightAt(int blockX, int blockZ) {
double base = fBm(blockX / 160.0D, blockZ / 160.0D, 4);
double detail = fBm(blockX / 32.0D, blockZ / 32.0D, 2) * 0.35D;
int h = (int) Math.round(62.0D + base * 26.0D + detail * 6.0D);
@@ -116,6 +161,7 @@ public final class WorldGenerator {
int worldX = baseX + x;
int worldZ = baseZ + z;
int height = heightAt(worldX, worldZ);
+ boolean road = this.roads != null && this.roads.onRoad(worldX, worldZ);
// a column: dirt under the surface, grass on top, stone below
for (int y = 0; y <= height; y++) {
@@ -123,7 +169,7 @@ public final class WorldGenerator {
if (y == 0) {
id = Blocks.STONE;
} else if (y == height) {
- id = Blocks.GRASS;
+ id = road ? Blocks.ASPHALT : Blocks.GRASS;
} else if (y > height - 4) {
id = Blocks.DIRT;
} else {
diff --git a/rsx-server-core/src/test/java/net/rustnotmc/server/craft/CraftTest.java b/rsx-server-core/src/test/java/net/rustnotmc/server/craft/CraftTest.java
new file mode 100644
index 0000000..ecbfb2d
--- /dev/null
+++ b/rsx-server-core/src/test/java/net/rustnotmc/server/craft/CraftTest.java
@@ -0,0 +1,240 @@
+package net.rustnotmc.server.craft;
+
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemStack;
+import net.rustnotmc.protocol.ItemType;
+import net.rustnotmc.server.item.Inventory;
+
+/**
+ * Tests what a player knows and what a player can make.
+ *
+ * The knowledge belongs to the player, the workbench is a place in the world
+ * and the ingredients are in the bag, and all three have to agree before
+ * anything is made.
+ */
+public final class CraftTest {
+
+ private static int checks;
+ private static int failed;
+ private static final java.util.List failures = new java.util.ArrayList();
+
+ public static void main(String[] args) {
+ run("the known things are known from the start", new Throwing() {
+ public void go() { testKnownByDefault(); }
+ });
+ run("the rest has to be researched", new Throwing() {
+ public void go() { testUnknownByDefault(); }
+ });
+ run("a branch has to be walked", new Throwing() {
+ public void go() { testTreeBranch(); }
+ });
+ run("opening a node costs scrap", new Throwing() {
+ public void go() { testTreeCost(); }
+ });
+ run("a thing is made out of what is in the bag", new Throwing() {
+ public void go() { testCraft(); }
+ });
+ run("the workbench has to be there", new Throwing() {
+ public void go() { testBenchGate(); }
+ });
+ run("a better workbench makes it faster", new Throwing() {
+ public void go() { testCraftTime(); }
+ });
+ run("researching turns the thing into a blueprint", new Throwing() {
+ public void go() { testResearch(); }
+ });
+ run("a blueprint is learned", new Throwing() {
+ public void go() { testLearnBlueprint(); }
+ });
+
+ System.out.println();
+ for (int i = 0; i < failures.size(); i++) {
+ System.out.println(" FAIL " + failures.get(i));
+ }
+ System.out.println();
+ System.out.println("checks: " + checks + ", failed: " + failed);
+ if (failed != 0) {
+ System.out.println("FAILED");
+ System.exit(1);
+ }
+ System.out.println("OK");
+ }
+
+ private interface Throwing {
+ void go() throws Exception;
+ }
+
+ private static void run(String name, Throwing body) {
+ checks++;
+ int before = failed;
+ try {
+ body.go();
+ } catch (Throwable t) {
+ failed++;
+ failures.add(name + ": threw " + t);
+ t.printStackTrace();
+ return;
+ }
+ if (failed == before) {
+ System.out.println(" ok " + name);
+ } 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);
+ }
+ }
+
+ private static void testKnownByDefault() {
+ Knowledge knowledge = new Knowledge();
+ yes("the rock is known", knowledge.knows(ItemRegistry.ROCK));
+ yes("the stone hatchet is known", knowledge.knows(ItemRegistry.STONE_HATCHET));
+ yes("the workbench is known", knowledge.knows(ItemRegistry.WORKBENCH_1));
+ yes("there is more than a handful of known things", knowledge.knownCount() > 20);
+ }
+
+ private static void testUnknownByDefault() {
+ Knowledge knowledge = new Knowledge();
+ yes("the hatchet is not known", !knowledge.knows(ItemRegistry.HATCHET));
+ yes("the salvaged sword is not known", !knowledge.knows(ItemRegistry.SALVAGED_SWORD));
+ knowledge.learn(ItemRegistry.HATCHET);
+ yes("after the research the hatchet is known", knowledge.knows(ItemRegistry.HATCHET));
+ }
+
+ private static void testTreeBranch() {
+ Knowledge knowledge = new Knowledge();
+ yes("the root of the branch can be opened",
+ knowledge.canUnlock(ItemRegistry.SALVAGED_HAMMER));
+ yes("the next one cannot", !knowledge.canUnlock(ItemRegistry.HATCHET));
+ yes("and it says why",
+ knowledge.reasonCannotUnlock(ItemRegistry.HATCHET) != null);
+ knowledge.unlock(ItemRegistry.SALVAGED_HAMMER);
+ yes("with the hammer the hatchet opens",
+ knowledge.canUnlock(ItemRegistry.HATCHET));
+ knowledge.unlock(ItemRegistry.HATCHET);
+ yes("with the hatchet the sword opens",
+ knowledge.canUnlock(ItemRegistry.SALVAGED_SWORD));
+ yes("an opened node knows the recipe", knowledge.knows(ItemRegistry.HATCHET));
+ }
+
+ private static void testTreeCost() {
+ Knowledge knowledge = new Knowledge();
+ knowledge.unlock(ItemRegistry.SALVAGED_HAMMER);
+ eq("the node costs a hundred twenty five", Integer.valueOf(125),
+ Integer.valueOf(net.rustnotmc.protocol.TechTree
+ .costOf(ItemRegistry.HATCHET)));
+ eq("the nodes that are open are told to the client", Integer.valueOf(1),
+ Integer.valueOf(knowledge.unlockedNodes().length));
+ }
+
+ private static void testCraft() {
+ Knowledge knowledge = new Knowledge();
+ Inventory inventory = new Inventory();
+ ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
+ knowledge.learn(ItemRegistry.HATCHET);
+ inventory.add(ItemStack.of(ItemRegistry.WOOD, 100));
+ inventory.add(ItemStack.of(ItemRegistry.METAL, 75));
+
+ yes("without a workbench it cannot be made",
+ Crafter.whyNot(hatchet, knowledge, 0, inventory) != null);
+
+ yes("it can be made at the first workbench",
+ Crafter.whyNot(hatchet, knowledge, 1, inventory) == null);
+ yes("the craft works", Crafter.craft(hatchet, 1, knowledge, 1, inventory));
+ eq("the hatchet is in the bag", Integer.valueOf(1),
+ Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
+ eq("the wood is gone", Integer.valueOf(0),
+ Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
+ eq("the metal is gone", Integer.valueOf(0),
+ Integer.valueOf(inventory.count(ItemRegistry.METAL)));
+ }
+
+ private static void testBenchGate() {
+ Knowledge knowledge = new Knowledge();
+ Inventory inventory = new Inventory();
+ ItemType pickaxe = ItemRegistry.require(ItemRegistry.PICKAXE);
+ knowledge.learn(ItemRegistry.PICKAXE);
+ inventory.add(ItemStack.of(ItemRegistry.WOOD, 100));
+ inventory.add(ItemStack.of(ItemRegistry.METAL, 125));
+
+ yes("the pickaxe wants the first workbench",
+ Crafter.whyNot(pickaxe, knowledge, 0, inventory) != null);
+ yes("and it is refused without it",
+ !Crafter.craft(pickaxe, 1, knowledge, 0, inventory));
+ eq("nothing was made", Integer.valueOf(0),
+ Integer.valueOf(inventory.count(ItemRegistry.PICKAXE)));
+ yes("at the workbench it goes through",
+ Crafter.craft(pickaxe, 1, knowledge, 1, inventory));
+
+ // and without the knowledge it is refused as well
+ Knowledge empty = new Knowledge();
+ eq("the rock is short of nothing", Integer.valueOf(0),
+ Integer.valueOf(inventory.count(ItemRegistry.ROCK)));
+ yes("an unknown recipe is refused",
+ Crafter.whyNot(pickaxe, empty, 1, inventory) != null);
+ }
+
+ private static void testCraftTime() {
+ ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
+ eq("at the workbench it wants thirty seconds", Integer.valueOf(30),
+ Integer.valueOf(Crafter.seconds(hatchet, 1)));
+ eq("one level above is half", Integer.valueOf(15),
+ Integer.valueOf(Crafter.seconds(hatchet, 2)));
+ eq("two levels above is a quarter", Integer.valueOf(7),
+ Integer.valueOf(Crafter.seconds(hatchet, 3)));
+ eq("three levels above is no faster", Integer.valueOf(7),
+ Integer.valueOf(Crafter.seconds(hatchet, 4)));
+ }
+
+ private static void testResearch() {
+ Inventory inventory = new Inventory();
+ inventory.add(ItemStack.of(ItemRegistry.HATCHET, 1));
+ inventory.add(ItemStack.of(ItemRegistry.SCRAP, 30));
+
+ ResearchTable table = new ResearchTable();
+ String problem = table.start(ItemRegistry.HATCHET, inventory);
+ yes("the research starts: " + problem, problem == null);
+ eq("the scrap is taken at once", Integer.valueOf(0),
+ Integer.valueOf(inventory.count(ItemRegistry.SCRAP)));
+ eq("the item is still there while the work goes on", Integer.valueOf(1),
+ Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
+ yes("the table is busy", table.busy());
+
+ boolean done = false;
+ for (int i = 0; i < 20 && !done; i++) {
+ done = table.tick(inventory);
+ }
+ yes("the work finishes", done);
+ eq("the item is a blueprint now", Integer.valueOf(1),
+ Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
+ yes("and it is a blueprint", ResearchTable.isBlueprint(inventory.get(0)));
+ yes("the plain item is not a blueprint",
+ !ResearchTable.isBlueprint(ItemStack.of(ItemRegistry.HATCHET, 1)));
+ }
+
+ private static void testLearnBlueprint() {
+ Inventory inventory = new Inventory();
+ ItemStack blueprint = ItemStack.of(ItemRegistry.HATCHET, 1);
+ blueprint.set(ResearchTable.BLUEPRINT, "1");
+ inventory.add(blueprint);
+
+ Knowledge knowledge = new Knowledge();
+ yes("the hatchet is not known yet", !knowledge.knows(ItemRegistry.HATCHET));
+ yes("the blueprint is learned", ResearchTable.learn(inventory, knowledge,
+ ItemRegistry.HATCHET));
+ yes("and now it is known", knowledge.knows(ItemRegistry.HATCHET));
+ eq("the blueprint is used up", Integer.valueOf(0),
+ Integer.valueOf(inventory.count(ItemRegistry.HATCHET)));
+ }
+}
diff --git a/rsx-server-core/src/test/java/net/rustnotmc/server/world/NodeTest.java b/rsx-server-core/src/test/java/net/rustnotmc/server/world/NodeTest.java
new file mode 100644
index 0000000..43691d9
--- /dev/null
+++ b/rsx-server-core/src/test/java/net/rustnotmc/server/world/NodeTest.java
@@ -0,0 +1,222 @@
+package net.rustnotmc.server.world;
+
+import java.util.List;
+import java.util.Random;
+import net.rustnotmc.protocol.Blocks;
+import net.rustnotmc.protocol.ChunkFormat;
+import net.rustnotmc.protocol.ItemRegistry;
+import net.rustnotmc.protocol.ItemStack;
+import net.rustnotmc.protocol.ItemType;
+import net.rustnotmc.protocol.NodeType;
+
+/**
+ * Tests the things in the world that can be worn down: the nodes of resource,
+ * the heaps of rubbish and the roads they stand by.
+ */
+public final class NodeTest {
+
+ private static int checks;
+ private static int failed;
+ private static final java.util.List failures = new java.util.ArrayList();
+
+ public static void main(String[] args) {
+ run("a node is worn down by the right tool", new Throwing() {
+ public void go() { testGather(); }
+ });
+ run("the last swing gives what is left", new Throwing() {
+ public void go() { testLastSwing(); }
+ });
+ run("the wrong tool takes nothing", new Throwing() {
+ public void go() { testWrongTool(); }
+ });
+ run("the mark makes a swing stronger", new Throwing() {
+ public void go() { testMark(); }
+ });
+ run("a heap of rubbish gives scrap", new Throwing() {
+ public void go() { testJunkPile(); }
+ });
+ run("a heap goes away when it is empty", new Throwing() {
+ public void go() { testJunkPileLife(); }
+ });
+ run("the roads are where they should be", new Throwing() {
+ public void go() { testRoads(); }
+ });
+ run("a road is level along its length", new Throwing() {
+ public void go() { testRoadIsLevel(); }
+ });
+
+ System.out.println();
+ for (int i = 0; i < failures.size(); i++) {
+ System.out.println(" FAIL " + failures.get(i));
+ }
+ System.out.println();
+ System.out.println("checks: " + checks + ", failed: " + failed);
+ if (failed != 0) {
+ System.out.println("FAILED");
+ System.exit(1);
+ }
+ System.out.println("OK");
+ }
+
+ private interface Throwing {
+ void go() throws Exception;
+ }
+
+ private static void run(String name, Throwing body) {
+ checks++;
+ int before = failed;
+ try {
+ body.go();
+ } catch (Throwable t) {
+ failed++;
+ failures.add(name + ": threw " + t);
+ t.printStackTrace();
+ return;
+ }
+ if (failed == before) {
+ System.out.println(" ok " + name);
+ } 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);
+ }
+ }
+
+ private static void testGather() {
+ ResourceNode node = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
+ ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
+ int taken = node.hit(hatchet, false);
+ eq("the hatchet takes eighty five wood", Integer.valueOf(85), Integer.valueOf(taken));
+ eq("the node has less in it", Integer.valueOf(750 - 85),
+ Integer.valueOf(node.pool()));
+ }
+
+ private static void testLastSwing() {
+ ResourceNode node = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
+ ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
+ int total = 0;
+ for (int i = 0; i < 20 && !node.isEmpty(); i++) {
+ total += node.hit(hatchet, false);
+ }
+ eq("the node gives exactly its pool", Integer.valueOf(750), Integer.valueOf(total));
+ yes("and then it is gone", node.isEmpty());
+ eq("an empty node gives nothing", Integer.valueOf(0),
+ Integer.valueOf(node.hit(hatchet, false)));
+ }
+
+ private static void testWrongTool() {
+ ResourceNode tree = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
+ ItemType pickaxe = ItemRegistry.require(ItemRegistry.PICKAXE);
+ eq("a pickaxe does not cut", Integer.valueOf(0),
+ Integer.valueOf(tree.hit(pickaxe, false)));
+
+ ResourceNode ore = new ResourceNode(NodeType.byId(NodeType.STONE), 0, 64, 0);
+ ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
+ eq("an axe does not mine", Integer.valueOf(0),
+ Integer.valueOf(ore.hit(hatchet, false)));
+ }
+
+ private static void testMark() {
+ ResourceNode tree = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
+ ItemType hatchet = ItemRegistry.require(ItemRegistry.HATCHET);
+ int plain = tree.hit(hatchet, false);
+ ResourceNode marked = new ResourceNode(NodeType.byId(NodeType.TREE_COMMON), 0, 64, 0);
+ int onMark = marked.hit(hatchet, true);
+ yes("a swing on the cross takes more", onMark > plain);
+ eq("and the streak is kept", Integer.valueOf(1),
+ Integer.valueOf(marked.markStreak()));
+ marked.hit(hatchet, false);
+ eq("a swing that misses resets it", Integer.valueOf(0),
+ Integer.valueOf(marked.markStreak()));
+ }
+
+ private static void testJunkPile() {
+ Random random = new Random(7);
+ JunkPile heap = new JunkPile(10, 64, 10, random);
+ List barrels = heap.barrels();
+ yes("a heap has two to four barrels", barrels.size() >= 2 && barrels.size() <= 4);
+
+ int scrap = 0;
+ for (int i = 0; i < barrels.size(); i++) {
+ List loot = heap.breakBarrel(barrels.get(i), random);
+ for (int j = 0; j < loot.size(); j++) {
+ if (loot.get(j).item == ItemRegistry.SCRAP) {
+ scrap += loot.get(j).count;
+ }
+ }
+ }
+ yes("the grey barrels give scrap", scrap > 0);
+ yes("a broken barrel stays broken", barrels.get(0).isBroken());
+ yes("breaking it twice gives nothing",
+ heap.breakBarrel(barrels.get(0), random).isEmpty());
+ }
+
+ private static void testJunkPileLife() {
+ Random random = new Random(11);
+ JunkPile heap = new JunkPile(10, 64, 10, random);
+ List barrels = heap.barrels();
+ for (int i = 0; i < barrels.size(); i++) {
+ heap.breakBarrel(barrels.get(i), random);
+ }
+ // the box of food has to be emptied as well
+ List boxes = heap.boxes();
+ for (int i = 0; i < boxes.size(); i++) {
+ JunkPile.FoodBox box = boxes.get(i);
+ while (heap.takeFromBox(box, 0, random) != null) {
+ // keep taking until the box is empty
+ }
+ }
+ yes("the heap is empty", heap.isEmpty());
+ yes("and it is going away", heap.despawnIn() > 0);
+ boolean gone = false;
+ for (int i = 0; i < 200 && !gone; i++) {
+ gone = heap.tick();
+ }
+ yes("and then it is gone", gone);
+ }
+
+ private static void testRoads() {
+ RoadNetwork roads = new RoadNetwork(4000);
+ yes("the middle of the world is on a road", roads.onRoad(2000, 2000));
+ yes("the middle of a straight road is on a road", roads.onRoad(2000, 2007));
+ yes("far away from a road is not on a road", !roads.onRoad(2500, 2500));
+ yes("there are points where a heap may stand", roads.spawnPoints().size() > 10);
+ }
+
+ private static void testRoadIsLevel() {
+ RoadNetwork roads = new RoadNetwork(4000);
+ WorldGenerator generator = new WorldGenerator(1234L, roads);
+ // a stretch of road that does not cross another one
+ int height = generator.heightAt(2000, 2400);
+ eq("the road is level across it", Integer.valueOf(height),
+ Integer.valueOf(generator.heightAt(2000 + 3, 2400)));
+ eq("and level across it the other way", Integer.valueOf(height),
+ Integer.valueOf(generator.heightAt(2000, 2400 + 3)));
+
+ // and the surface of a road is asphalt
+ Chunk chunk = new Chunk(new ChunkPos(125, 125));
+ generator.generate(chunk);
+ byte[] blocks = chunk.snapshot();
+ int top = -1;
+ for (int y = Chunk.HEIGHT - 1; y >= 0; y--) {
+ if (blocks[ChunkFormat.index(0, y, 0)] != Blocks.AIR) {
+ top = y;
+ break;
+ }
+ }
+ eq("the top of the road is asphalt", Integer.valueOf(Blocks.ASPHALT),
+ Integer.valueOf(blocks[ChunkFormat.index(0, top, 0)]));
+ }
+}