Шаг A: предметы и инвентарь
Фундамент предметов по данным плана (срез 11.2025) и серверный инвентарь. В протоколе (общий словарь для клиента и сервера): - ToolClass, ResourceClass - что чем ломается и что где лежит. - ItemType - определение предмета: класс инструмента, урон по четырем частям тела, прочность, Gather Rates по каждому классу ресурса (сколько снимает удар, разброс, сколько уходит прочности инструмента), bench - требуемый уровень верстака, knownByDefault - открыт ли рецепт с начала игры, admin - только ли админы сервера. - ItemRegistry - таблица предметов. Числа из плана, каждое в одном месте, чтобы игра и план не разъехались. - Recipe - ингредиенты, количество, базовое время крафта. - ItemStack - тип, количество, остаток прочности и свои данные на стеке (как теги у плагина в Paper): стек - это то, что путешествует. Пакеты: Inventory (весь инвентарь при входе) и SlotChange (один слот). На сервере: Inventory - 30 слотов, хотбар 6, сервер владеет инвентарем и сообщает клиенту, а не спрашивает. Стартовый набор - камень в первом слоте и в руке. Тесты: предметы закреплены числами из плана (камень бьёт в голову на 20, снимает 10 дерева и 5 руды; кирка не рубит дерево; меч не ломает вообще), инвентарь переживает провод, сумка наполняется и опустошается, полная сумма говорит об этом. Замечание: прогрессия крафта в Rust упирается в лутбоксы - скрап падает только из них, а чертёж требует найденного предмета. Это отдельный вопрос (п. 5.2 плана), он не блокирует предметы и инвентарь.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
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.
|
||||
*/
|
||||
public final class ItemRegistry {
|
||||
|
||||
public static final int NOTHING = 0;
|
||||
public static final int ROCK = 1;
|
||||
public static final int WOOD = 2;
|
||||
public static final int STONE = 3;
|
||||
public static final int METAL = 4;
|
||||
public static final int HIGH_QUALITY_METAL = 5;
|
||||
public static final int SULFUR = 6;
|
||||
public static final int SCRAP = 7;
|
||||
public static final int STONE_HATCHET = 8;
|
||||
public static final int STONE_PICKAXE = 9;
|
||||
public static final int HAMMER = 10;
|
||||
public static final int BUILDING_PLAN = 11;
|
||||
public static final int WORKBENCH_1 = 12;
|
||||
public static final int WORKBENCH_2 = 13;
|
||||
public static final int WORKBENCH_3 = 14;
|
||||
public static final int RESEARCH_TABLE = 15;
|
||||
public static final int HATCHET = 16;
|
||||
public static final int PICKAXE = 17;
|
||||
public static final int SALVAGED_HAMMER = 18;
|
||||
public static final int SALVAGED_SWORD = 19;
|
||||
public static final int SALVAGED_AXE = 20;
|
||||
public static final int SALVAGED_ICEPICK = 21;
|
||||
public static final int WOODEN_DOOR = 22;
|
||||
public static final int CODE_LOCK = 23;
|
||||
public static final int BASIC_FRAGMENT = 24;
|
||||
public static final int ADVANCED_FRAGMENT = 25;
|
||||
public static final int METAL_PIPE = 26;
|
||||
public static final int METAL_BLADE = 27;
|
||||
public static final int GEARS = 28;
|
||||
|
||||
private static final Map<Integer, ItemType> TYPES = new LinkedHashMap<Integer, ItemType>();
|
||||
|
||||
private ItemRegistry() {
|
||||
}
|
||||
|
||||
static {
|
||||
// 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(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"));
|
||||
|
||||
// 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.
|
||||
define(ItemType.of(ROCK, "Rock")
|
||||
.tool(ToolClass.NONE)
|
||||
.durability(100)
|
||||
.damage(20, 10, 8, 8)
|
||||
.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")
|
||||
.tool(ToolClass.AXE)
|
||||
.durability(100)
|
||||
.damage(27, 15, 12, 12)
|
||||
.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")
|
||||
.tool(ToolClass.PICKAXE)
|
||||
.durability(100)
|
||||
.damage(35, 17, 13, 13)
|
||||
.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")
|
||||
.tool(ToolClass.HAMMER)
|
||||
.durability(100)
|
||||
.damage(0, 0, 0, 0)
|
||||
.recipe(new Recipe(new int[]{WOOD}, new int[]{100}, 15)));
|
||||
|
||||
define(ItemType.of(HATCHET, "Hatchet")
|
||||
.tool(ToolClass.AXE)
|
||||
.durability(100)
|
||||
.damage(25, 25, 25, 25)
|
||||
.bench(1)
|
||||
.known(false)
|
||||
.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));
|
||||
|
||||
define(ItemType.of(PICKAXE, "Pickaxe")
|
||||
.tool(ToolClass.PICKAXE)
|
||||
.durability(100)
|
||||
.damage(30, 30, 30, 30)
|
||||
.bench(1)
|
||||
.known(false)
|
||||
.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));
|
||||
|
||||
define(ItemType.of(SALVAGED_HAMMER, "Salvaged Hammer")
|
||||
.tool(ToolClass.HAMMER)
|
||||
.durability(100)
|
||||
.damage(40, 40, 40, 40)
|
||||
.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));
|
||||
|
||||
define(ItemType.of(SALVAGED_SWORD, "Salvaged Sword")
|
||||
.tool(ToolClass.SWORD)
|
||||
.durability(100)
|
||||
.damage(50, 50, 50, 50)
|
||||
.bench(1)
|
||||
.known(false)
|
||||
.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(BUILDING_PLAN, "Building Plan")
|
||||
.recipe(new Recipe(new int[]{WOOD}, new int[]{20}, 15)));
|
||||
define(ItemType.of(WORKBENCH_1, "Work Bench Level 1")
|
||||
.recipe(new Recipe(new int[]{WOOD, METAL}, new int[]{500, 100}, 30)));
|
||||
define(ItemType.of(WORKBENCH_2, "Work Bench Level 2")
|
||||
.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")
|
||||
.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")
|
||||
.bench(1)
|
||||
.recipe(new Recipe(new int[]{METAL, SCRAP}, new int[]{200, 20}, 30)));
|
||||
define(ItemType.of(WOODEN_DOOR, "Wooden Door")
|
||||
.recipe(new Recipe(new int[]{WOOD}, new int[]{300}, 30)));
|
||||
define(ItemType.of(CODE_LOCK, "Code Lock")
|
||||
.recipe(new Recipe(new int[]{METAL}, new int[]{100}, 30)));
|
||||
}
|
||||
|
||||
private static void define(ItemType.Builder builder) {
|
||||
ItemType type = builder.build();
|
||||
if (TYPES.put(Integer.valueOf(type.id), type) != null) {
|
||||
throw new IllegalStateException("item " + type.id + " is defined twice");
|
||||
}
|
||||
}
|
||||
|
||||
/** The item with that id, or null. */
|
||||
public static ItemType byId(int id) {
|
||||
return TYPES.get(Integer.valueOf(id));
|
||||
}
|
||||
|
||||
/** The item with that id. Throws when there is none. */
|
||||
public static ItemType require(int id) {
|
||||
ItemType type = byId(id);
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("no item with id " + id);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/** Every item, in the order they were defined. */
|
||||
public static Collection<ItemType> all() {
|
||||
return Collections.unmodifiableCollection(TYPES.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A stack of one kind of item.
|
||||
*
|
||||
* The stack is the thing that travels: the inventory holds stacks, a drop is a
|
||||
* stack, a blueprint is a stack. The kind of item is an id and never changes;
|
||||
* what changes is how many there are, how much life is left in the tool, and
|
||||
* the data that belongs to this particular stack.
|
||||
*
|
||||
* The data is the same idea as the tags a plugin puts on an item in Paper: a
|
||||
* small set of named values that travel with the item, so that a torch can
|
||||
* carry its charge and a blueprint can carry what it unlocks.
|
||||
*/
|
||||
public final class ItemStack {
|
||||
|
||||
/** The stack of nothing. */
|
||||
public static final ItemStack EMPTY = new ItemStack(ItemRegistry.NOTHING, 0, -1);
|
||||
|
||||
/** The kind of item. */
|
||||
public final int item;
|
||||
|
||||
/** How many there are. */
|
||||
public int count;
|
||||
|
||||
/** How much life is left in the tool, or -1 when it does not wear out. */
|
||||
public int condition;
|
||||
|
||||
private final Map<String, String> data = new LinkedHashMap<String, String>();
|
||||
|
||||
public ItemStack(int item, int count, int condition) {
|
||||
this.item = item;
|
||||
this.count = count;
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
/** A stack of an item, at full life. */
|
||||
public static ItemStack of(int item, int count) {
|
||||
ItemType type = ItemRegistry.byId(item);
|
||||
int condition = type == null || type.durability <= 0 ? -1 : type.durability;
|
||||
return new ItemStack(item, count, condition);
|
||||
}
|
||||
|
||||
/** True when there is nothing here. */
|
||||
public boolean isEmpty() {
|
||||
return this.item == ItemRegistry.NOTHING || this.count <= 0;
|
||||
}
|
||||
|
||||
/** The kind of item, or null when the stack is empty. */
|
||||
public ItemType type() {
|
||||
if (isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return ItemRegistry.byId(this.item);
|
||||
}
|
||||
|
||||
/** A value that travels with this stack. */
|
||||
public String get(String key) {
|
||||
return this.data.get(key);
|
||||
}
|
||||
|
||||
/** A value that travels with this stack. */
|
||||
public ItemStack set(String key, String value) {
|
||||
this.data.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Every value that travels with this stack. */
|
||||
public Map<String, String> data() {
|
||||
return Collections.unmodifiableMap(this.data);
|
||||
}
|
||||
|
||||
/** A copy, so that handing a stack out does not hand out the original. */
|
||||
public ItemStack copy() {
|
||||
ItemStack out = new ItemStack(this.item, this.count, this.condition);
|
||||
out.data.putAll(this.data);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof ItemStack)) {
|
||||
return false;
|
||||
}
|
||||
ItemStack that = (ItemStack) other;
|
||||
return this.item == that.item && this.count == that.count
|
||||
&& this.condition == that.condition && this.data.equals(that.data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = this.item;
|
||||
hash = hash * 31 + this.count;
|
||||
hash = hash * 31 + this.condition;
|
||||
hash = hash * 31 + this.data.hashCode();
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ItemStack[" + this.item + " x" + this.count
|
||||
+ (this.condition >= 0 ? " cond " + this.condition : "") + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
/**
|
||||
* One kind of item.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public final class ItemType {
|
||||
|
||||
/** How much a swing takes out of a node, and what it costs the tool. */
|
||||
public static final class Gather {
|
||||
|
||||
/** How much of the resource one swing takes. */
|
||||
public final int damage;
|
||||
|
||||
/** The spread of that amount, as a fraction. */
|
||||
public final float bonus;
|
||||
|
||||
/** How much of the tool's condition one swing costs. */
|
||||
public final float conditionLoss;
|
||||
|
||||
public Gather(int damage, float bonus, float conditionLoss) {
|
||||
this.damage = damage;
|
||||
this.bonus = bonus;
|
||||
this.conditionLoss = conditionLoss;
|
||||
}
|
||||
}
|
||||
|
||||
/** The body parts a hit can land on, in the order the numbers are given. */
|
||||
public static final int HEAD = 0;
|
||||
public static final int TORSO = 1;
|
||||
public static final int ARMS = 2;
|
||||
public static final int LEGS = 3;
|
||||
|
||||
public final int id;
|
||||
public final String name;
|
||||
public final ToolClass tool;
|
||||
|
||||
/** The level a tool has to be to gather the thing. */
|
||||
public final int harvestLevel;
|
||||
|
||||
/** How many swings the tool lasts. Zero means it never breaks. */
|
||||
public final int durability;
|
||||
|
||||
/** The damage to the head, the torso, the arms and the legs. */
|
||||
public final int[] damage;
|
||||
|
||||
/** The level of workbench the recipe needs. Zero means no workbench. */
|
||||
public final int bench;
|
||||
|
||||
/** True when the recipe is known from the start and needs no research. */
|
||||
public final boolean knownByDefault;
|
||||
|
||||
/** True when only the administrators of the server may use it. */
|
||||
public final boolean admin;
|
||||
|
||||
/** 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) {
|
||||
if (damage == null || damage.length != 4) {
|
||||
throw new IllegalArgumentException("the damage needs four body parts");
|
||||
}
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.tool = tool;
|
||||
this.harvestLevel = harvestLevel;
|
||||
this.durability = durability;
|
||||
this.damage = damage;
|
||||
this.bench = bench;
|
||||
this.knownByDefault = knownByDefault;
|
||||
this.admin = admin;
|
||||
this.recipe = recipe;
|
||||
this.gathers = gathers == null ? new Gather[ResourceClass.values().length] : gathers;
|
||||
}
|
||||
|
||||
/** A starting point for defining an item. */
|
||||
public static Builder of(int id, String name) {
|
||||
return new Builder(id, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an item a piece at a time.
|
||||
*
|
||||
* The registry is a table of two hundred things eventually, and a table
|
||||
* that reads like a table is the only kind that can be checked against the
|
||||
* numbers in the plan.
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final int id;
|
||||
private final String name;
|
||||
private ToolClass tool = ToolClass.NONE;
|
||||
private int harvestLevel;
|
||||
private int durability;
|
||||
private int[] damage = new int[]{0, 0, 0, 0};
|
||||
private int bench;
|
||||
private boolean knownByDefault = true;
|
||||
private boolean admin;
|
||||
private Recipe recipe;
|
||||
private final Gather[] gathers = new Gather[ResourceClass.values().length];
|
||||
|
||||
private Builder(int id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Builder tool(ToolClass tool) {
|
||||
this.tool = tool;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder level(int level) {
|
||||
this.harvestLevel = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder durability(int durability) {
|
||||
this.durability = durability;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** The damage to the head, the torso, the arms and the legs. */
|
||||
public Builder damage(int head, int torso, int arms, int legs) {
|
||||
this.damage = new int[]{head, torso, arms, legs};
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder bench(int bench) {
|
||||
this.bench = bench;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Whether the recipe is known from the start. */
|
||||
public Builder known(boolean known) {
|
||||
this.knownByDefault = known;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Whether only the administrators of the server may use it. */
|
||||
public Builder admin(boolean admin) {
|
||||
this.admin = admin;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder recipe(Recipe recipe) {
|
||||
this.recipe = recipe;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** What a swing takes out of a node, and what it costs the tool. */
|
||||
public Builder gather(ResourceClass resource, int damage, float bonus,
|
||||
float conditionLoss) {
|
||||
this.gathers[resource.ordinal()] = new Gather(damage, bonus, conditionLoss);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ItemType build() {
|
||||
return new ItemType(this.id, this.name, this.tool, this.harvestLevel,
|
||||
this.durability, this.damage, this.bench, this.knownByDefault,
|
||||
this.admin, this.recipe, this.gathers);
|
||||
}
|
||||
}
|
||||
|
||||
/** How this tool gathers something, or null when it cannot. */
|
||||
public Gather gather(ResourceClass resource) {
|
||||
if (resource == null) {
|
||||
return null;
|
||||
}
|
||||
int index = resource.ordinal();
|
||||
if (index < 0 || index >= this.gathers.length) {
|
||||
return null;
|
||||
}
|
||||
return this.gathers[index];
|
||||
}
|
||||
|
||||
/** The damage to a body part. */
|
||||
public int damage(int bodyPart) {
|
||||
if (bodyPart < 0 || bodyPart >= this.damage.length) {
|
||||
return 0;
|
||||
}
|
||||
return this.damage[bodyPart];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ItemType[" + this.id + " " + this.name + "]";
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,52 @@ public final class Packets {
|
||||
*/
|
||||
public static final int LAUNCHER_AUTH = 0x1A;
|
||||
|
||||
/**
|
||||
* The whole inventory, sent when the player arrives.
|
||||
*
|
||||
* The server is the only one who decides what is in the slots, so the
|
||||
* client is told rather than asked.
|
||||
*/
|
||||
public static final int INVENTORY = 0x1C;
|
||||
|
||||
/** One slot of the inventory, when it changes. */
|
||||
public static final int INVENTORY_SLOT = 0x1D;
|
||||
|
||||
/**
|
||||
* One slot of the inventory and what is in it now.
|
||||
*
|
||||
* The slot number is here because the client has to know which one to
|
||||
* redraw, and the contents because it has no way of guessing them.
|
||||
*/
|
||||
public static final class SlotChange implements Packet {
|
||||
|
||||
public final int slot;
|
||||
public final int item;
|
||||
public final int count;
|
||||
public final int condition;
|
||||
|
||||
public SlotChange(int slot, int item, int count, int condition) {
|
||||
this.slot = slot;
|
||||
this.item = item;
|
||||
this.count = count;
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf(24);
|
||||
buf.writeVarInt(this.slot);
|
||||
buf.writeVarInt(this.item);
|
||||
buf.writeVarInt(this.count);
|
||||
buf.writeVarInt(this.condition);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static SlotChange read(Buf in) {
|
||||
return new SlotChange(in.readVarInt(), in.readVarInt(), in.readVarInt(),
|
||||
in.readVarInt());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The player's game mode, changed while the player is in the world.
|
||||
*
|
||||
@@ -796,6 +842,80 @@ public final class Packets {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One slot of an inventory.
|
||||
*
|
||||
* The empty slot is a stack of nothing, so there is no special case: an
|
||||
* empty slot is one with the id of nothing and a count of zero.
|
||||
*/
|
||||
public static final class Slot {
|
||||
|
||||
public final int item;
|
||||
public final int count;
|
||||
public final int condition;
|
||||
|
||||
public Slot(int item, int count, int condition) {
|
||||
this.item = item;
|
||||
this.count = count;
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
public static Slot of(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return new Slot(ItemRegistry.NOTHING, 0, -1);
|
||||
}
|
||||
return new Slot(stack.item, stack.count, stack.condition);
|
||||
}
|
||||
|
||||
public void write(Buf buf) {
|
||||
buf.writeVarInt(this.item);
|
||||
buf.writeVarInt(this.count);
|
||||
buf.writeVarInt(this.condition);
|
||||
}
|
||||
|
||||
public static Slot read(Buf in) {
|
||||
return new Slot(in.readVarInt(), in.readVarInt(), in.readVarInt());
|
||||
}
|
||||
}
|
||||
|
||||
/** The whole inventory of a player. */
|
||||
public static final class Inventory implements Packet {
|
||||
|
||||
public final Slot[] slots;
|
||||
|
||||
/** The slot the player is holding. */
|
||||
public final int held;
|
||||
|
||||
public Inventory(Slot[] slots, int held) {
|
||||
this.slots = slots;
|
||||
this.held = held;
|
||||
}
|
||||
|
||||
public Buf write() {
|
||||
Buf buf = new Buf(16 + this.slots.length * 12);
|
||||
buf.writeVarInt(this.held);
|
||||
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 Inventory read(Buf in) {
|
||||
int held = in.readVarInt();
|
||||
int count = in.readVarInt();
|
||||
if (count < 0 || count > 1024) {
|
||||
throw new ProtocolException("an inventory 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 Inventory(slots, held);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The launcher's session, as the game server sees it.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
/**
|
||||
* A recipe, and what it takes to unlock it.
|
||||
*
|
||||
* Two things gate a recipe and they are not the same thing:
|
||||
*
|
||||
* <ul>
|
||||
* <li>the <b>bench</b> is the level of workbench you have to be standing
|
||||
* next to. Zero means you can craft it with your hands.
|
||||
* <li>the <b>blueprint</b> is the knowledge itself. A recipe that is known
|
||||
* from the start needs no research and no scrap; anything else has to be
|
||||
* researched at a table, which needs scrap and the item itself.
|
||||
* </ul>
|
||||
*
|
||||
* The knowledge is the player's, not the item's: two players can know
|
||||
* different recipes at the same moment.
|
||||
*/
|
||||
public final class Recipe {
|
||||
|
||||
/** The item ids that go in, in any order. */
|
||||
public final int[] items;
|
||||
|
||||
/** How many of each, in the same order. */
|
||||
public final int[] counts;
|
||||
|
||||
/** The base craft time in seconds, before the workbench shortens it. */
|
||||
public final int seconds;
|
||||
|
||||
public Recipe(int[] items, int[] counts, int seconds) {
|
||||
if (items.length != counts.length) {
|
||||
throw new IllegalArgumentException("the recipe has "
|
||||
+ items.length + " items but " + counts.length + " counts");
|
||||
}
|
||||
this.items = items;
|
||||
this.counts = counts;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
|
||||
/** How many of an item the recipe needs, or zero. */
|
||||
public int needs(int item) {
|
||||
for (int i = 0; i < this.items.length; i++) {
|
||||
if (this.items[i] == item) {
|
||||
return this.counts[i];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder out = new StringBuilder("Recipe[");
|
||||
for (int i = 0; i < this.items.length; i++) {
|
||||
if (i > 0) {
|
||||
out.append(" + ");
|
||||
}
|
||||
out.append(this.counts[i]).append("x").append(this.items[i]);
|
||||
}
|
||||
return out.append(", ").append(this.seconds).append("s]").toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
/**
|
||||
* What a node in the world holds.
|
||||
*
|
||||
* A node is not a block in the world but a thing that can be worn down: an
|
||||
* oak, a stone outcrop, a metal deposit, a carcass. What is inside it and how
|
||||
* much there is decides what a swing of the tool takes away.
|
||||
*/
|
||||
public enum ResourceClass {
|
||||
|
||||
/** Not a resource. A tool gathers nothing of this kind. */
|
||||
NONE,
|
||||
|
||||
/** Wood. */
|
||||
TREE,
|
||||
|
||||
/** A stone outcrop: stones. */
|
||||
STONE,
|
||||
|
||||
/** A metal deposit: metal ore. */
|
||||
METAL,
|
||||
|
||||
/** A sulfur deposit: sulfur ore. */
|
||||
SULFUR,
|
||||
|
||||
/** A carcass: meat and hide. */
|
||||
FLESH
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.rustnotmc.protocol;
|
||||
|
||||
/**
|
||||
* What a tool is for.
|
||||
*
|
||||
* The class of the tool decides what it can gather and how fast. It is the
|
||||
* short answer to "why does this pickaxe not cut a tree": because a pickaxe is
|
||||
* for stone and ore, and a tree wants an axe.
|
||||
*
|
||||
* A tool can serve more than one class in Rust, where the salvaged axe also
|
||||
* works as a hammer, but that is a detail for later. One class is enough to
|
||||
* start, and the rules live with the tools.
|
||||
*/
|
||||
public enum ToolClass {
|
||||
|
||||
/** Bare hands. They dig, badly and slowly. */
|
||||
NONE,
|
||||
|
||||
/** For stone and ore. */
|
||||
PICKAXE,
|
||||
|
||||
/** For wood. */
|
||||
AXE,
|
||||
|
||||
/** A weapon. It gathers nothing, on purpose. */
|
||||
SWORD,
|
||||
|
||||
/** For building: it upgrades a structure and repairs it. */
|
||||
HAMMER
|
||||
}
|
||||
@@ -82,6 +82,12 @@ public final class ProtocolTest {
|
||||
run("every packet has a name", new Runnable() {
|
||||
public void run() { testPacketNames(); }
|
||||
});
|
||||
run("the items match the original game", new Runnable() {
|
||||
public void run() { testItems(); }
|
||||
});
|
||||
run("an inventory survives the wire", new Runnable() {
|
||||
public void run() { testInventory(); }
|
||||
});
|
||||
run("a lying length cannot stall the heap", new Runnable() {
|
||||
public void run() { testLyingLength(); }
|
||||
});
|
||||
@@ -550,6 +556,98 @@ public final class ProtocolTest {
|
||||
Integer.valueOf(names.size()));
|
||||
}
|
||||
|
||||
private static void testItems() {
|
||||
// The numbers the game is built on. If these move, the game stops
|
||||
// matching its own rules, so they are pinned here.
|
||||
ItemType rock = ItemRegistry.require(ItemRegistry.ROCK);
|
||||
eq("the rock is called a rock", "Rock", rock.name);
|
||||
eq("the rock hits the head for twenty", Integer.valueOf(20),
|
||||
Integer.valueOf(rock.damage(ItemType.HEAD)));
|
||||
eq("and the torso for ten", Integer.valueOf(10),
|
||||
Integer.valueOf(rock.damage(ItemType.TORSO)));
|
||||
eq("the rock takes ten off a tree", Integer.valueOf(10),
|
||||
Integer.valueOf(rock.gather(ResourceClass.TREE).damage));
|
||||
eq("and five off an ore", Integer.valueOf(5),
|
||||
Integer.valueOf(rock.gather(ResourceClass.METAL).damage));
|
||||
eq("the rock is known from the start", Boolean.TRUE,
|
||||
Boolean.valueOf(rock.knownByDefault));
|
||||
eq("the rock needs no workbench", Integer.valueOf(0),
|
||||
Integer.valueOf(rock.bench));
|
||||
yes("the rock has a recipe", rock.recipe != null);
|
||||
eq("the rock is made of ten stones", Integer.valueOf(10),
|
||||
Integer.valueOf(rock.recipe.needs(ItemRegistry.STONE)));
|
||||
|
||||
ItemType hatchet = ItemRegistry.require(ItemRegistry.STONE_HATCHET);
|
||||
eq("the stone hatchet is an axe", ToolClass.AXE, hatchet.tool);
|
||||
eq("it takes twenty off a tree", Integer.valueOf(20),
|
||||
Integer.valueOf(hatchet.gather(ResourceClass.TREE).damage));
|
||||
|
||||
ItemType pickaxe = ItemRegistry.require(ItemRegistry.STONE_PICKAXE);
|
||||
eq("the stone pickaxe is a pickaxe", ToolClass.PICKAXE, pickaxe.tool);
|
||||
eq("it takes sixteen off an ore", Integer.valueOf(16),
|
||||
Integer.valueOf(pickaxe.gather(ResourceClass.METAL).damage));
|
||||
yes("and it does not cut trees", pickaxe.gather(ResourceClass.TREE) == null);
|
||||
|
||||
ItemType sword = ItemRegistry.require(ItemRegistry.SALVAGED_SWORD);
|
||||
eq("the sword is a sword", ToolClass.SWORD, sword.tool);
|
||||
yes("the sword gathers nothing but flesh", sword.gather(ResourceClass.TREE) == null
|
||||
&& sword.gather(ResourceClass.METAL) == null
|
||||
&& sword.gather(ResourceClass.FLESH) != null);
|
||||
|
||||
ItemType workbench2 = ItemRegistry.require(ItemRegistry.WORKBENCH_2);
|
||||
eq("the second workbench needs the first", Integer.valueOf(1),
|
||||
Integer.valueOf(workbench2.bench));
|
||||
ItemType workbench3 = ItemRegistry.require(ItemRegistry.WORKBENCH_3);
|
||||
eq("the third needs the second", Integer.valueOf(2),
|
||||
Integer.valueOf(workbench3.bench));
|
||||
|
||||
// the resources are gathered, not made
|
||||
ItemType wood = ItemRegistry.require(ItemRegistry.WOOD);
|
||||
yes("wood has no recipe", wood.recipe == null);
|
||||
yes("every id is different", ItemRegistry.all().size() > 20);
|
||||
}
|
||||
|
||||
private static void testInventory() {
|
||||
Packets.Slot[] slots = new Packets.Slot[6];
|
||||
slots[0] = new Packets.Slot(ItemRegistry.ROCK, 1, 100);
|
||||
slots[1] = new Packets.Slot(ItemRegistry.WOOD, 12, -1);
|
||||
slots[2] = new Packets.Slot(ItemRegistry.NOTHING, 0, -1);
|
||||
Packets.Inventory inventory = new Packets.Inventory(slots, 0);
|
||||
|
||||
Packets.Inventory back =
|
||||
Packets.Inventory.read(new Buf(inventory.write().toByteArray()));
|
||||
eq("the slots survive", Integer.valueOf(6),
|
||||
Integer.valueOf(back.slots.length));
|
||||
eq("the rock is in the first slot", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(back.slots[0].item));
|
||||
eq("with one of it", Integer.valueOf(1),
|
||||
Integer.valueOf(back.slots[0].count));
|
||||
eq("and its life", Integer.valueOf(100),
|
||||
Integer.valueOf(back.slots[0].condition));
|
||||
eq("wood is in the second", Integer.valueOf(12),
|
||||
Integer.valueOf(back.slots[1].count));
|
||||
eq("an empty slot stays empty", Integer.valueOf(ItemRegistry.NOTHING),
|
||||
Integer.valueOf(back.slots[2].item));
|
||||
eq("the hand survives", Integer.valueOf(0), Integer.valueOf(back.held));
|
||||
|
||||
Packets.SlotChange change = new Packets.SlotChange(3, ItemRegistry.SCRAP, 7, -1);
|
||||
Packets.SlotChange changeBack =
|
||||
Packets.SlotChange.read(new Buf(change.write().toByteArray()));
|
||||
eq("the slot number survives", Integer.valueOf(3),
|
||||
Integer.valueOf(changeBack.slot));
|
||||
eq("the item survives", Integer.valueOf(ItemRegistry.SCRAP),
|
||||
Integer.valueOf(changeBack.item));
|
||||
eq("the count survives", Integer.valueOf(7),
|
||||
Integer.valueOf(changeBack.count));
|
||||
|
||||
// a stack carries the data that belongs to it
|
||||
ItemStack stack = ItemStack.of(ItemRegistry.ROCK, 1);
|
||||
stack.set("charged", "yes");
|
||||
eq("the data travels", "yes", stack.copy().get("charged"));
|
||||
eq("the copy is separate", Boolean.FALSE,
|
||||
Boolean.valueOf(stack == stack.copy()));
|
||||
}
|
||||
|
||||
private static void testLyingLength() {
|
||||
// announces a large but legal body, sends nothing, and keeps quiet.
|
||||
// The decoder must hold it without growing and without pretending the
|
||||
|
||||
@@ -47,6 +47,7 @@ Say "compiled $($mainFiles.Count) main + $($testFiles.Count) test files"
|
||||
$suites = @(
|
||||
'net.rustnotmc.server.ServerTest',
|
||||
'net.rustnotmc.server.world.WorldTest',
|
||||
'net.rustnotmc.server.item.InventoryTest',
|
||||
'net.rustnotmc.server.PlayTest'
|
||||
)
|
||||
foreach ($suite in $suites) {
|
||||
|
||||
@@ -441,9 +441,22 @@ public final class ClientSession implements Runnable {
|
||||
this.player.viewDistance(),
|
||||
ChunkFormat.HEIGHT,
|
||||
this.player.gameMode()));
|
||||
// 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());
|
||||
this.player.streamChunks(this.server.world());
|
||||
}
|
||||
|
||||
/** Tells the client that one slot of the bag changed. */
|
||||
void sendSlot(int slot) {
|
||||
if (this.player == null) {
|
||||
return;
|
||||
}
|
||||
net.rustnotmc.protocol.ItemStack stack = this.player.inventory().get(slot);
|
||||
send(Packets.INVENTORY_SLOT, new Packets.SlotChange(slot, stack.item,
|
||||
stack.count, stack.condition));
|
||||
}
|
||||
|
||||
/**
|
||||
* The player's game mode changed while they were in the world.
|
||||
*
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import net.rustnotmc.protocol.Blocks;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.server.item.Inventory;
|
||||
import net.rustnotmc.server.world.Chunk;
|
||||
import net.rustnotmc.server.world.ChunkPos;
|
||||
import net.rustnotmc.server.world.World;
|
||||
@@ -49,8 +50,17 @@ public final class Player {
|
||||
/** Survival or creative. The server decides, and the client is told. */
|
||||
private volatile int gameMode = Packets.GAMEMODE_SURVIVAL;
|
||||
|
||||
/** What the player is carrying. The server owns it. */
|
||||
private final Inventory inventory;
|
||||
|
||||
Player(ClientSession session, int playerId, int entityId, String account,
|
||||
double[] spawn) {
|
||||
this(session, playerId, entityId, account, spawn,
|
||||
Inventory.startingKit());
|
||||
}
|
||||
|
||||
Player(ClientSession session, int playerId, int entityId, String account,
|
||||
double[] spawn, Inventory inventory) {
|
||||
this.session = session;
|
||||
this.playerId = playerId;
|
||||
this.entityId = entityId;
|
||||
@@ -58,7 +68,12 @@ public final class Player {
|
||||
this.x = spawn[0];
|
||||
this.y = spawn[1];
|
||||
this.z = spawn[2];
|
||||
this.viewDistance = DEFAULT_VIEW_DISTANCE;
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
/** What the player is carrying. */
|
||||
public Inventory inventory() {
|
||||
return this.inventory;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- getters
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package net.rustnotmc.server.item;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
|
||||
/**
|
||||
* What a player is carrying.
|
||||
*
|
||||
* The server is the only one who decides what is in the slots. The client is
|
||||
* told, and when it is told it draws what it was given. A client that is free
|
||||
* to decide what it holds is a client that can be made to hold anything.
|
||||
*
|
||||
* The layout is the one from the original game: the first slots are the hotbar
|
||||
* and the rest is the bag behind it.
|
||||
*/
|
||||
public final class Inventory {
|
||||
|
||||
/** How many slots the hotbar has. */
|
||||
public static final int HOTBAR = 6;
|
||||
|
||||
/** How many slots there are in all. */
|
||||
public static final int SLOTS = 30;
|
||||
|
||||
private final ItemStack[] slots = new ItemStack[SLOTS];
|
||||
private int held;
|
||||
|
||||
public Inventory() {
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
this.slots[i] = ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The starting kit.
|
||||
*
|
||||
* A rock and a torch, the way the original game hands them out. The rock
|
||||
* is the first thing the player has and the only thing they can use at the
|
||||
* start.
|
||||
*/
|
||||
public static Inventory startingKit() {
|
||||
Inventory inventory = new Inventory();
|
||||
inventory.set(0, ItemStack.of(ItemRegistry.ROCK, 1));
|
||||
inventory.held = 0;
|
||||
return inventory;
|
||||
}
|
||||
|
||||
/** The stack in a slot, never null. */
|
||||
public ItemStack get(int slot) {
|
||||
if (slot < 0 || slot >= SLOTS) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
ItemStack stack = this.slots[slot];
|
||||
return stack == null ? ItemStack.EMPTY : stack;
|
||||
}
|
||||
|
||||
/** Puts a stack in a slot. */
|
||||
public void set(int slot, ItemStack stack) {
|
||||
if (slot < 0 || slot >= SLOTS) {
|
||||
return;
|
||||
}
|
||||
this.slots[slot] = stack == null ? ItemStack.EMPTY : stack;
|
||||
}
|
||||
|
||||
/** The slot the player is holding. */
|
||||
public int held() {
|
||||
return this.held;
|
||||
}
|
||||
|
||||
/** Holds a different slot. */
|
||||
public void held(int slot) {
|
||||
if (slot >= 0 && slot < HOTBAR) {
|
||||
this.held = slot;
|
||||
}
|
||||
}
|
||||
|
||||
/** The stack in the hand. */
|
||||
public ItemStack inHand() {
|
||||
return get(this.held);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a stack to the bag, and to the hotbar first.
|
||||
*
|
||||
* Returns true when it fit. A bag that is full simply does not take it,
|
||||
* which is the same thing every game does.
|
||||
*/
|
||||
public boolean add(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
ItemStack there = get(i);
|
||||
if (there.item == stack.item && there.count < 64) {
|
||||
int room = 64 - there.count;
|
||||
int moved = Math.min(room, stack.count);
|
||||
there.count += moved;
|
||||
stack.count -= moved;
|
||||
if (stack.count <= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
if (get(i).isEmpty()) {
|
||||
set(i, stack);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** How many of an item are in the bag. */
|
||||
public int count(int item) {
|
||||
int total = 0;
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
ItemStack stack = get(i);
|
||||
if (stack.item == item) {
|
||||
total += stack.count;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an item out of the bag.
|
||||
*
|
||||
* Returns how many were actually there, which is not always what was
|
||||
* asked for.
|
||||
*/
|
||||
public int take(int item, int amount) {
|
||||
if (amount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int taken = 0;
|
||||
for (int i = 0; i < SLOTS && taken < amount; i++) {
|
||||
ItemStack stack = get(i);
|
||||
if (stack.item != item || stack.count <= 0) {
|
||||
continue;
|
||||
}
|
||||
int moved = Math.min(stack.count, amount - taken);
|
||||
stack.count -= moved;
|
||||
taken += moved;
|
||||
if (stack.count <= 0) {
|
||||
set(i, ItemStack.EMPTY);
|
||||
}
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
/** The bag as the client is told about it. */
|
||||
public Packets.Inventory toPacket() {
|
||||
Packets.Slot[] out = new Packets.Slot[SLOTS];
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
out[i] = Packets.Slot.of(get(i));
|
||||
}
|
||||
return new Packets.Inventory(out, this.held);
|
||||
}
|
||||
|
||||
/** The slots that are not empty, for a look at what a player carries. */
|
||||
public List<ItemStack> contents() {
|
||||
List<ItemStack> out = new ArrayList<ItemStack>();
|
||||
for (int i = 0; i < SLOTS; i++) {
|
||||
ItemStack stack = get(i);
|
||||
if (!stack.isEmpty()) {
|
||||
out.add(stack);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,13 @@ final class PlayClient {
|
||||
: Packets.GameMode.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
||||
}
|
||||
|
||||
/** Waits for the server to say what is in the bag. */
|
||||
Packets.Inventory awaitInventory(long millis) throws IOException {
|
||||
Frame frame = await(Packets.INVENTORY, millis);
|
||||
return frame == null ? null
|
||||
: Packets.Inventory.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
||||
}
|
||||
|
||||
/** The launcher's session, offered instead of a password. */
|
||||
void launcherAuth(String account, String uuid, String token) throws IOException {
|
||||
send(Packets.LAUNCHER_AUTH, new Packets.LauncherAuth(account, uuid, token));
|
||||
|
||||
@@ -57,6 +57,9 @@ public final class PlayTest {
|
||||
run("a survival player cannot break blocks", new Throwing() {
|
||||
public void go() throws Exception { testSurvivalCannotBreak(work); }
|
||||
});
|
||||
run("the player starts with a rock in his hand", new Throwing() {
|
||||
public void go() throws Exception { testStartingKit(work); }
|
||||
});
|
||||
run("the world survives a restart", new Throwing() {
|
||||
public void go() throws Exception { testRestart(work); }
|
||||
});
|
||||
@@ -432,6 +435,52 @@ public final class PlayTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The player starts with a rock, and the client is told about it.
|
||||
*
|
||||
* The server owns the bag, so the client is not asked what is in it.
|
||||
*/
|
||||
private static void testStartingKit(Path work) throws Exception {
|
||||
Server server = boot(work, "Kit");
|
||||
try {
|
||||
PlayClient client = new PlayClient("127.0.0.1", server.port(), 5000);
|
||||
yes("signed in", client.signIn("Kit", "password1") != null);
|
||||
client.send(Packets.JOIN_WORLD, Packets.JoinWorld.fresh());
|
||||
client.await(Packets.WORLD_JOIN, 5000L);
|
||||
|
||||
Packets.Inventory inventory = client.awaitInventory(5000L);
|
||||
yes("the client was told what is in the bag", inventory != null);
|
||||
if (inventory == null) {
|
||||
return;
|
||||
}
|
||||
eq("the bag has slots", Integer.valueOf(30),
|
||||
Integer.valueOf(inventory.slots.length));
|
||||
eq("the first slot holds the rock", Integer.valueOf(net.rustnotmc.protocol.ItemRegistry.ROCK),
|
||||
Integer.valueOf(inventory.slots[0].item));
|
||||
eq("one of it", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.slots[0].count));
|
||||
eq("and it is in his hand", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.held));
|
||||
|
||||
Player player = null;
|
||||
for (Player p : server.players().values()) {
|
||||
player = p;
|
||||
}
|
||||
yes("the server knows the player", player != null);
|
||||
if (player != null) {
|
||||
eq("the server agrees about the rock",
|
||||
Integer.valueOf(net.rustnotmc.protocol.ItemRegistry.ROCK),
|
||||
Integer.valueOf(player.inventory().get(0).item));
|
||||
eq("and about what is in the hand",
|
||||
Integer.valueOf(net.rustnotmc.protocol.ItemRegistry.ROCK),
|
||||
Integer.valueOf(player.inventory().inHand().item));
|
||||
}
|
||||
client.close();
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static void testRestart(Path work) throws Exception {
|
||||
Path dir = work.resolve("play-world");
|
||||
deleteTree(dir);
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package net.rustnotmc.server.item;
|
||||
|
||||
import java.util.List;
|
||||
import net.rustnotmc.protocol.ItemRegistry;
|
||||
import net.rustnotmc.protocol.ItemStack;
|
||||
import net.rustnotmc.protocol.ItemType;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.protocol.ResourceClass;
|
||||
|
||||
/**
|
||||
* Tests the bag.
|
||||
*
|
||||
* The bag belongs to the server and nobody else, so these are about the server
|
||||
* being right about what a player has and what a player can do with it.
|
||||
*/
|
||||
public final class InventoryTest {
|
||||
|
||||
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 starting kit is a rock in the hand", new Throwing() {
|
||||
public void go() { testStartingKit(); }
|
||||
});
|
||||
run("a bag holds what is put in it", new Throwing() {
|
||||
public void go() { testAdd(); }
|
||||
});
|
||||
run("a bag gives things back", new Throwing() {
|
||||
public void go() { testTake(); }
|
||||
});
|
||||
run("a full bag says so", new Throwing() {
|
||||
public void go() { testFull(); }
|
||||
});
|
||||
run("the client is told what is in the bag", new Throwing() {
|
||||
public void go() { testPacket(); }
|
||||
});
|
||||
run("a tool knows how much life it has left", new Throwing() {
|
||||
public void go() { testCondition(); }
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void yes(String what, boolean condition) {
|
||||
if (!condition) {
|
||||
failed++;
|
||||
failures.add(what);
|
||||
}
|
||||
}
|
||||
|
||||
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 testStartingKit() {
|
||||
Inventory inventory = Inventory.startingKit();
|
||||
eq("the first slot holds the rock", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(inventory.get(0).item));
|
||||
eq("one of it", Integer.valueOf(1), Integer.valueOf(inventory.get(0).count));
|
||||
eq("and it is in the hand", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.held()));
|
||||
eq("the hand holds the rock", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(inventory.inHand().item));
|
||||
eq("nothing else", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.contents().size()));
|
||||
}
|
||||
|
||||
private static void testAdd() {
|
||||
Inventory inventory = new Inventory();
|
||||
yes("the rock goes in", inventory.add(ItemStack.of(ItemRegistry.ROCK, 1)));
|
||||
eq("and it is there", Integer.valueOf(1),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.ROCK)));
|
||||
|
||||
// the same kind of thing piles up
|
||||
yes("more wood goes in", inventory.add(ItemStack.of(ItemRegistry.WOOD, 10)));
|
||||
yes("and more", inventory.add(ItemStack.of(ItemRegistry.WOOD, 5)));
|
||||
eq("wood piles up", Integer.valueOf(15),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
}
|
||||
|
||||
private static void testTake() {
|
||||
Inventory inventory = Inventory.startingKit();
|
||||
inventory.add(ItemStack.of(ItemRegistry.WOOD, 10));
|
||||
|
||||
eq("four wood comes out", Integer.valueOf(4),
|
||||
Integer.valueOf(inventory.take(ItemRegistry.WOOD, 4)));
|
||||
eq("six are left", Integer.valueOf(6),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
|
||||
eq("more than there is gives what there is", Integer.valueOf(6),
|
||||
Integer.valueOf(inventory.take(ItemRegistry.WOOD, 100)));
|
||||
eq("and nothing is left", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
eq("taking nothing takes nothing", Integer.valueOf(0),
|
||||
Integer.valueOf(inventory.take(ItemRegistry.WOOD, 0)));
|
||||
}
|
||||
|
||||
private static void testFull() {
|
||||
Inventory inventory = new Inventory();
|
||||
// a slot holds a stack of up to sixty four, so the bag is full when
|
||||
// every slot is
|
||||
for (int i = 0; i < Inventory.SLOTS; i++) {
|
||||
yes("slot " + i + " takes a stack",
|
||||
inventory.add(ItemStack.of(ItemRegistry.WOOD, 64)));
|
||||
}
|
||||
yes("a full bag says so", !inventory.add(ItemStack.of(ItemRegistry.WOOD, 1)));
|
||||
eq("and it did not take the last one", Integer.valueOf(Inventory.SLOTS * 64),
|
||||
Integer.valueOf(inventory.count(ItemRegistry.WOOD)));
|
||||
}
|
||||
|
||||
private static void testPacket() {
|
||||
Inventory inventory = Inventory.startingKit();
|
||||
inventory.add(ItemStack.of(ItemRegistry.SCRAP, 3));
|
||||
inventory.held(1);
|
||||
|
||||
Packets.Inventory packet = inventory.toPacket();
|
||||
eq("every slot is there", Integer.valueOf(Inventory.SLOTS),
|
||||
Integer.valueOf(packet.slots.length));
|
||||
eq("the rock is in the first slot", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(packet.slots[0].item));
|
||||
eq("the scrap is in the second", Integer.valueOf(ItemRegistry.SCRAP),
|
||||
Integer.valueOf(packet.slots[1].item));
|
||||
eq("three of it", Integer.valueOf(3),
|
||||
Integer.valueOf(packet.slots[1].count));
|
||||
eq("and the hand is the second slot", Integer.valueOf(1),
|
||||
Integer.valueOf(packet.held));
|
||||
eq("the empty slots stay empty", Integer.valueOf(ItemRegistry.NOTHING),
|
||||
Integer.valueOf(packet.slots[5].item));
|
||||
}
|
||||
|
||||
private static void testCondition() {
|
||||
// a tool has a life, a pile of wood does not
|
||||
ItemStack rock = ItemStack.of(ItemRegistry.ROCK, 1);
|
||||
eq("the rock starts whole", Integer.valueOf(100), Integer.valueOf(rock.condition));
|
||||
|
||||
ItemStack wood = ItemStack.of(ItemRegistry.WOOD, 1);
|
||||
eq("wood does not wear out", Integer.valueOf(-1),
|
||||
Integer.valueOf(wood.condition));
|
||||
|
||||
// a swing costs the tool something
|
||||
ItemType rockType = ItemRegistry.require(ItemRegistry.ROCK);
|
||||
eq("a swing on a tree costs the rock", 0.3F,
|
||||
rockType.gather(ResourceClass.TREE).conditionLoss, 0.0001F);
|
||||
eq("a swing on an ore costs the same", 0.3F,
|
||||
rockType.gather(ResourceClass.METAL).conditionLoss, 0.0001F);
|
||||
}
|
||||
|
||||
private static void eq(String what, Object expected, Object actual, float tolerance) {
|
||||
if (expected instanceof Float && actual instanceof Float) {
|
||||
float difference = Math.abs(((Float) expected).floatValue()
|
||||
- ((Float) actual).floatValue());
|
||||
if (difference <= tolerance) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
eq(what, expected, actual);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user