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

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

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

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