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

Протокол:
- 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
+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)]));
}
}