Данные игрока сохраняются
- PlayerData: файл на игрока в world/players/<аккаунт>.json. Позиция, рыскание и тангаж, 30 слотов инвентаря, 7 мест брони по два слоя, здоровье, режим игры, выученные рецепты и открытые узлы тех-дерева. - Запись: при выходе игрока, раз в 30 секунд и при остановке сервера. Чтение: при входе, так что игра продолжается с того места, где игрок остановился. - В Json добавлен писатель: quote, number, numbers, texts. - Файл пишется через временный файл и подменяется, так что оборванная запись не портит прошлую. Тесты: сервер 92 (было 88), добавлен PlayerDataTest.
This commit is contained in:
@@ -52,6 +52,7 @@ $suites = @(
|
||||
'net.rustnotmc.server.item.InventoryTest',
|
||||
'net.rustnotmc.server.craft.CraftTest',
|
||||
'net.rustnotmc.server.hitbox.HitboxTest',
|
||||
'net.rustnotmc.server.player.PlayerDataTest',
|
||||
'net.rustnotmc.server.combat.DamageTest',
|
||||
'net.rustnotmc.server.PlayTest'
|
||||
)
|
||||
|
||||
@@ -83,6 +83,24 @@ public final class Player {
|
||||
return this.health;
|
||||
}
|
||||
|
||||
public float pitch() {
|
||||
return this.pitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the player where they left off.
|
||||
*
|
||||
* The server reads this back from the file of the player, so that a game
|
||||
* continues where it stopped instead of starting over in the spawn.
|
||||
*/
|
||||
public void placeAt(double x, double y, double z, float yaw, float pitch) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hurts the player.
|
||||
*
|
||||
@@ -214,11 +232,6 @@ public final class Player {
|
||||
public float yaw() {
|
||||
return this.yaw;
|
||||
}
|
||||
|
||||
public float pitch() {
|
||||
return this.pitch;
|
||||
}
|
||||
|
||||
public boolean onGround() {
|
||||
return this.onGround;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ public final class Server {
|
||||
private volatile TokenAuth tokenAuth;
|
||||
private volatile boolean localAccounts = true;
|
||||
private volatile AdminRegistry admins = new AdminRegistry(null);
|
||||
private volatile net.rustnotmc.server.player.PlayerData playerData =
|
||||
new net.rustnotmc.server.player.PlayerData(null);
|
||||
private volatile HitboxRegistry hitboxes = new HitboxRegistry(null);
|
||||
|
||||
private volatile ServerSocket listener;
|
||||
@@ -176,18 +178,89 @@ public final class Server {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells everybody whether they are an administrator.
|
||||
* Writes down what a player leaves behind.
|
||||
*
|
||||
* The client needs to know: the things that only an administrator may do
|
||||
* are not offered to anybody else, and a right that is taken away while
|
||||
* the player is looking has to disappear at once.
|
||||
* The position, what they carry, what they know and how much life they
|
||||
* have left. A file that cannot be written is a reason to say so, not a
|
||||
* reason to stop the server.
|
||||
*/
|
||||
public void sendAdminStates() {
|
||||
for (Player player : this.players.values()) {
|
||||
player.session().sendAdminState(player.isAdmin());
|
||||
public void savePlayer(Player player) {
|
||||
try {
|
||||
net.rustnotmc.server.player.PlayerData.PlayerState state =
|
||||
new net.rustnotmc.server.player.PlayerData.PlayerState();
|
||||
state.x = player.x();
|
||||
state.y = player.y();
|
||||
state.z = player.z();
|
||||
state.yaw = player.yaw();
|
||||
state.pitch = player.pitch();
|
||||
state.health = player.health();
|
||||
state.gameMode = player.gameMode();
|
||||
for (int i = 0; i < state.inventory.length; i++) {
|
||||
state.inventory[i][0] = player.inventory().get(i);
|
||||
}
|
||||
for (int i = 0; i < state.armor.length; i++) {
|
||||
state.armor[i][0] = player.inventory().armor(i, 0);
|
||||
state.armor[i][1] = player.inventory().armor(i, 1);
|
||||
}
|
||||
for (Integer id : player.knowledge().learnedRecipes()) {
|
||||
state.recipes.add(String.valueOf(id));
|
||||
}
|
||||
for (Integer id : player.knowledge().openedNodes()) {
|
||||
state.tree.add(String.valueOf(id));
|
||||
}
|
||||
this.playerData.save(player.account(), state);
|
||||
} catch (IOException e) {
|
||||
log("could not write the data of " + player.account() + ": "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads back what a player left behind.
|
||||
*
|
||||
* A player who never played starts in the spawn with a rock in the hand.
|
||||
*/
|
||||
private void loadPlayer(Player player) {
|
||||
try {
|
||||
net.rustnotmc.server.player.PlayerData.PlayerState state =
|
||||
this.playerData.load(player.account());
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
player.placeAt(state.x, state.y, state.z, state.yaw, state.pitch);
|
||||
player.heal(state.health - player.health());
|
||||
for (int i = 0; i < state.inventory.length; i++) {
|
||||
player.inventory().set(i, state.inventory[i][0]);
|
||||
}
|
||||
for (int i = 0; i < state.armor.length; i++) {
|
||||
player.inventory().setArmor(i, 0, state.armor[i][0]);
|
||||
player.inventory().setArmor(i, 1, state.armor[i][1]);
|
||||
}
|
||||
List<Integer> recipes = new ArrayList<Integer>();
|
||||
for (String id : state.recipes) {
|
||||
recipes.add(Integer.valueOf(id));
|
||||
}
|
||||
List<Integer> tree = new ArrayList<Integer>();
|
||||
for (String id : state.tree) {
|
||||
tree.add(Integer.valueOf(id));
|
||||
}
|
||||
player.knowledge().restore(recipes, tree);
|
||||
} catch (IOException e) {
|
||||
log("could not read the data of " + player.account() + ": "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Where the data of the players is kept. */
|
||||
public net.rustnotmc.server.player.PlayerData playerData() {
|
||||
return this.playerData;
|
||||
}
|
||||
|
||||
public void playerData(net.rustnotmc.server.player.PlayerData playerData) {
|
||||
this.playerData = playerData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether the server's own accounts may be used.
|
||||
*
|
||||
@@ -333,6 +406,10 @@ public final class Server {
|
||||
for (ClientSession session : new ArrayList<ClientSession>(this.sessions.values())) {
|
||||
session.close();
|
||||
}
|
||||
// everybody who is still here gets their things written down
|
||||
for (Player player : this.players.values()) {
|
||||
savePlayer(player);
|
||||
}
|
||||
log("stopped after " + this.ticks.get() + " ticks");
|
||||
}
|
||||
|
||||
@@ -436,6 +513,11 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.ticks.get() % 600L == 0L) {
|
||||
for (Player player : this.players.values()) {
|
||||
savePlayer(player);
|
||||
}
|
||||
}
|
||||
if ((this.ticks.get() & 0x3FFFL) == 0L) {
|
||||
this.tokens.sweep();
|
||||
for (ClientSession session : this.sessions.values()) {
|
||||
@@ -456,6 +538,7 @@ public final class Server {
|
||||
session.account(), this.world.spawn());
|
||||
player.viewDistance(this.viewDistance);
|
||||
player.gameMode(this.gameMode);
|
||||
loadPlayer(player);
|
||||
this.players.put(Integer.valueOf(player.playerId()), player);
|
||||
session.player(player);
|
||||
|
||||
@@ -518,6 +601,7 @@ public final class Server {
|
||||
this.sessions.remove(Long.valueOf(session.id()));
|
||||
Player player = session.player();
|
||||
if (player != null) {
|
||||
savePlayer(player);
|
||||
this.players.remove(Integer.valueOf(player.playerId()));
|
||||
for (Player other : this.players.values()) {
|
||||
other.session().send(Packets.ENTITY_REMOVE,
|
||||
|
||||
@@ -129,6 +129,9 @@ public final class ServerMain {
|
||||
// The administrators are the server's own list.
|
||||
Path adminFile = adminsFile != null ? adminsFile : worldDir.resolve("admins.txt");
|
||||
server.admins(new net.rustnotmc.server.admin.AdminRegistry(adminFile));
|
||||
// where the players leave their things
|
||||
server.playerData(new net.rustnotmc.server.player.PlayerData(
|
||||
worldDir.resolve("players")));
|
||||
server.loadAccounts(accounts);
|
||||
final Path accountFile = accounts.toAbsolutePath();
|
||||
|
||||
|
||||
@@ -128,6 +128,36 @@ public final class Knowledge {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The recipes the player has learned, by the id of the item. */
|
||||
public List<Integer> learnedRecipes() {
|
||||
List<Integer> out = new ArrayList<Integer>(this.learned);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The nodes the player has opened, by the id of the item. */
|
||||
public List<Integer> openedNodes() {
|
||||
List<Integer> out = new ArrayList<Integer>(this.unlocked);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Forgets everything, so that a player can start over. */
|
||||
public void clear() {
|
||||
this.learned.clear();
|
||||
this.unlocked.clear();
|
||||
}
|
||||
|
||||
/** Puts back what a player knew before. */
|
||||
public void restore(List<Integer> recipes, List<Integer> tree) {
|
||||
this.learned.clear();
|
||||
this.unlocked.clear();
|
||||
for (Integer id : recipes) {
|
||||
this.learned.add(id);
|
||||
}
|
||||
for (Integer id : tree) {
|
||||
this.unlocked.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** The number of recipes the player knows. */
|
||||
public int knownCount() {
|
||||
return knownRecipes().length;
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package net.rustnotmc.server.player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
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.server.item.Inventory;
|
||||
import net.rustnotmc.server.util.Json;
|
||||
|
||||
/**
|
||||
* What a player leaves behind when they go.
|
||||
*
|
||||
* The position, what they carry, what they know and how much life they have
|
||||
* left. It is written when the player goes and every half minute while they
|
||||
* play, and it is read when they come back.
|
||||
*
|
||||
* One file per player, in the world folder, so that a person can look at it
|
||||
* and see what the server thinks of them.
|
||||
*/
|
||||
public final class PlayerData {
|
||||
|
||||
/** The folder the files are in. */
|
||||
private final Path folder;
|
||||
|
||||
public PlayerData(Path folder) {
|
||||
this.folder = folder;
|
||||
}
|
||||
|
||||
/** The file of one player. */
|
||||
public Path fileOf(String account) {
|
||||
if (this.folder == null) {
|
||||
return null;
|
||||
}
|
||||
return this.folder.resolve(safe(account) + ".json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes what the player has.
|
||||
*
|
||||
* A file that cannot be written is not a reason to stop the server: the
|
||||
* caller logs it and carries on.
|
||||
*/
|
||||
public boolean save(String account, PlayerState state) throws IOException {
|
||||
if (this.folder == null) {
|
||||
return false;
|
||||
}
|
||||
Files.createDirectories(this.folder);
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append("{\n");
|
||||
out.append(" \"account\": ").append(Json.quote(account)).append(",\n");
|
||||
out.append(" \"version\": 1,\n");
|
||||
out.append(" \"position\": ").append(Json.numbers(
|
||||
state.x, state.y, state.z, state.yaw, state.pitch)).append(",\n");
|
||||
out.append(" \"health\": ").append(Json.number(state.health)).append(",\n");
|
||||
out.append(" \"gameMode\": ").append(state.gameMode).append(",\n");
|
||||
out.append(" \"inventory\": ").append(slots(state.inventory)).append(",\n");
|
||||
out.append(" \"armor\": ").append(slots(state.armor)).append(",\n");
|
||||
out.append(" \"recipes\": ").append(Json.texts(state.recipes)).append(",\n");
|
||||
out.append(" \"tree\": ").append(Json.texts(state.tree)).append("\n");
|
||||
out.append("}\n");
|
||||
Path file = fileOf(account);
|
||||
Path temp = file.resolveSibling(file.getFileName() + ".tmp");
|
||||
Files.write(temp, out.toString().getBytes(StandardCharsets.UTF_8));
|
||||
Files.move(temp, file, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads what the player left behind.
|
||||
*
|
||||
* Returns null when there is nothing to read: a player who never played
|
||||
* gets the world spawn and a rock in the hand.
|
||||
*/
|
||||
public PlayerState load(String account) throws IOException {
|
||||
if (this.folder == null) {
|
||||
return null;
|
||||
}
|
||||
Path file = fileOf(account);
|
||||
if (file == null || !Files.isRegularFile(file)) {
|
||||
return null;
|
||||
}
|
||||
Object root = Json.readFile(file);
|
||||
PlayerState state = new PlayerState();
|
||||
double[] position = numbers(root, "position");
|
||||
if (position.length >= 5) {
|
||||
state.x = position[0];
|
||||
state.y = position[1];
|
||||
state.z = position[2];
|
||||
state.yaw = (float) position[3];
|
||||
state.pitch = (float) position[4];
|
||||
}
|
||||
state.health = (float) Json.number(root, "health", 100.0D);
|
||||
state.gameMode = (int) Json.number(root, "gameMode", 0.0D);
|
||||
readSlots(Json.get(root, "inventory"), state.inventory);
|
||||
readSlots(Json.get(root, "armor"), state.armor);
|
||||
state.recipes.addAll(texts(root, "recipes"));
|
||||
state.tree.addAll(texts(root, "tree"));
|
||||
return state;
|
||||
}
|
||||
|
||||
private static String slots(ItemStack[][] stacks) {
|
||||
StringBuilder out = new StringBuilder("[");
|
||||
for (int i = 0; i < stacks.length; i++) {
|
||||
if (i > 0) {
|
||||
out.append(',');
|
||||
}
|
||||
out.append('[');
|
||||
for (int layer = 0; layer < stacks[i].length; layer++) {
|
||||
if (layer > 0) {
|
||||
out.append(',');
|
||||
}
|
||||
out.append(slot(stacks[i][layer]));
|
||||
}
|
||||
out.append(']');
|
||||
}
|
||||
return out.append(']').toString();
|
||||
}
|
||||
|
||||
private static String slot(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return "0";
|
||||
}
|
||||
StringBuilder out = new StringBuilder("{");
|
||||
out.append("\"item\":").append(stack.item);
|
||||
out.append(",\"count\":").append(stack.count);
|
||||
if (stack.condition >= 0) {
|
||||
out.append(",\"condition\":").append(stack.condition);
|
||||
}
|
||||
if (!stack.data().isEmpty()) {
|
||||
out.append(",\"data\":{");
|
||||
boolean first = true;
|
||||
for (java.util.Map.Entry<String, String> entry : stack.data().entrySet()) {
|
||||
if (!first) {
|
||||
out.append(',');
|
||||
}
|
||||
first = false;
|
||||
out.append(Json.quote(entry.getKey())).append(':')
|
||||
.append(Json.quote(entry.getValue()));
|
||||
}
|
||||
out.append('}');
|
||||
}
|
||||
return out.append('}').toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void readSlots(Object value, ItemStack[][] into) {
|
||||
if (!(value instanceof List)) {
|
||||
return;
|
||||
}
|
||||
List<Object> rows = (List<Object>) value;
|
||||
for (int i = 0; i < into.length && i < rows.size(); i++) {
|
||||
Object row = rows.get(i);
|
||||
if (!(row instanceof List)) {
|
||||
continue;
|
||||
}
|
||||
List<Object> layers = (List<Object>) row;
|
||||
for (int layer = 0; layer < into[i].length && layer < layers.size(); layer++) {
|
||||
into[i][layer] = readSlot(layers.get(layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ItemStack readSlot(Object value) {
|
||||
if (!(value instanceof java.util.Map)) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
java.util.Map<String, Object> map = (java.util.Map<String, Object>) value;
|
||||
int item = (int) Json.number(map, "item", 0.0D);
|
||||
if (item == ItemRegistry.NOTHING) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
int count = (int) Json.number(map, "count", 1.0D);
|
||||
int condition = (int) Json.number(map, "condition", -1.0D);
|
||||
ItemStack stack = new ItemStack(item, count, condition);
|
||||
Object data = Json.get(map, "data");
|
||||
if (data instanceof java.util.Map) {
|
||||
for (java.util.Map.Entry<String, Object> entry
|
||||
: ((java.util.Map<String, Object>) data).entrySet()) {
|
||||
stack.set(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
private static double[] numbers(Object root, String key) {
|
||||
Object got = Json.get(root, key);
|
||||
if (!(got instanceof List)) {
|
||||
return new double[0];
|
||||
}
|
||||
List<Object> list = (List<Object>) got;
|
||||
double[] out = new double[list.size()];
|
||||
for (int i = 0; i < out.length; i++) {
|
||||
Object value = list.get(i);
|
||||
out[i] = value instanceof Number ? ((Number) value).doubleValue() : 0.0D;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> texts(Object root, String key) {
|
||||
List<String> out = new ArrayList<String>();
|
||||
Object got = Json.get(root, key);
|
||||
if (got instanceof List) {
|
||||
for (Object value : (List<Object>) got) {
|
||||
out.add(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A name that is safe to use as a file name. */
|
||||
public static String safe(String account) {
|
||||
if (account == null) {
|
||||
return "unknown";
|
||||
}
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (int i = 0; i < account.length(); i++) {
|
||||
char c = account.charAt(i);
|
||||
boolean plain = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||
|| (c >= '0' && c <= '9') || c == '-' || c == '_';
|
||||
out.append(plain ? c : '_');
|
||||
}
|
||||
String name = out.toString();
|
||||
return name.isEmpty() ? "unknown" : name;
|
||||
}
|
||||
|
||||
/** Everything about a player that is written down. */
|
||||
public static final class PlayerState {
|
||||
|
||||
public double x;
|
||||
public double y;
|
||||
public double z;
|
||||
public float yaw;
|
||||
public float pitch;
|
||||
public float health = 100.0F;
|
||||
public int gameMode;
|
||||
public final ItemStack[][] inventory = new ItemStack[Inventory.SLOTS][1];
|
||||
public final ItemStack[][] armor =
|
||||
new ItemStack[ItemType.ARMOR_SLOTS][2];
|
||||
public final List<String> recipes = new ArrayList<String>();
|
||||
public final List<String> tree = new ArrayList<String>();
|
||||
|
||||
public PlayerState() {
|
||||
for (int i = 0; i < this.inventory.length; i++) {
|
||||
this.inventory[i][0] = ItemStack.EMPTY;
|
||||
}
|
||||
for (int i = 0; i < this.armor.length; i++) {
|
||||
this.armor[i][0] = ItemStack.EMPTY;
|
||||
this.armor[i][1] = ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,4 +275,68 @@ public final class Json {
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- writing
|
||||
|
||||
/** A string in JSON, with the quotes and the backslashes put right. */
|
||||
public static String quote(String value) {
|
||||
if (value == null) {
|
||||
return "\"\"";
|
||||
}
|
||||
StringBuilder out = new StringBuilder(value.length() + 2);
|
||||
out.append('"');
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '"' || c == '\\') {
|
||||
out.append('\\');
|
||||
}
|
||||
if (c == '\n') {
|
||||
out.append("\\n");
|
||||
continue;
|
||||
}
|
||||
if (c == '\r') {
|
||||
out.append("\\r");
|
||||
continue;
|
||||
}
|
||||
if (c == '\t') {
|
||||
out.append("\\t");
|
||||
continue;
|
||||
}
|
||||
out.append(c);
|
||||
}
|
||||
out.append('"');
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/** A number in JSON, without a decimal tail when it is whole. */
|
||||
public static String number(double value) {
|
||||
if (value == Math.rint(value) && !Double.isInfinite(value)) {
|
||||
return Long.toString((long) value);
|
||||
}
|
||||
return Double.toString(value);
|
||||
}
|
||||
|
||||
/** A list of numbers in JSON. */
|
||||
public static String numbers(double... values) {
|
||||
StringBuilder out = new StringBuilder("[");
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (i > 0) {
|
||||
out.append(',');
|
||||
}
|
||||
out.append(number(values[i]));
|
||||
}
|
||||
return out.append(']').toString();
|
||||
}
|
||||
|
||||
/** A list of strings in JSON. */
|
||||
public static String texts(java.util.List<String> values) {
|
||||
StringBuilder out = new StringBuilder("[");
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
if (i > 0) {
|
||||
out.append(',');
|
||||
}
|
||||
out.append(quote(values.get(i)));
|
||||
}
|
||||
return out.append(']').toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package net.rustnotmc.server.player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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 leaves behind.
|
||||
*
|
||||
* The data of a player is the one thing that has to survive a restart: the
|
||||
* position, the bag, the clothes and what the player knows.
|
||||
*/
|
||||
public final class PlayerDataTest {
|
||||
|
||||
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) throws Exception {
|
||||
Path work = Path.of(args.length > 0 ? args[0] : "build/selftest");
|
||||
Files.createDirectories(work);
|
||||
|
||||
run("what a player leaves is written down", new Throwing() {
|
||||
public void go() throws Exception { testRoundTrip(work); }
|
||||
});
|
||||
run("a player who never played has nothing to read", new Throwing() {
|
||||
public void go() throws Exception { testMissing(work); }
|
||||
});
|
||||
run("the file is a name that is safe", new Throwing() {
|
||||
public void go() throws Exception { testSafeName(work); }
|
||||
});
|
||||
run("the clothes keep their layers", new Throwing() {
|
||||
public void go() throws Exception { testArmor(work); }
|
||||
});
|
||||
|
||||
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 testRoundTrip(Path work) throws Exception {
|
||||
Path folder = work.resolve("players-test");
|
||||
PlayerData data = new PlayerData(folder);
|
||||
|
||||
PlayerData.PlayerState state = new PlayerData.PlayerState();
|
||||
state.x = 120.5D;
|
||||
state.y = 64.0D;
|
||||
state.z = -33.25D;
|
||||
state.yaw = 1.5F;
|
||||
state.pitch = -0.25F;
|
||||
state.health = 73.5F;
|
||||
state.gameMode = 1;
|
||||
state.inventory[0][0] = ItemStack.of(ItemRegistry.ROCK, 1);
|
||||
state.inventory[3][0] = new ItemStack(ItemRegistry.SCRAP, 17, -1);
|
||||
state.recipes.add("16");
|
||||
state.tree.add("18");
|
||||
|
||||
yes("it is written", data.save("Player", state));
|
||||
yes("the file is there", Files.isRegularFile(data.fileOf("Player")));
|
||||
|
||||
PlayerData.PlayerState back = data.load("Player");
|
||||
yes("it is read back", back != null);
|
||||
if (back == null) {
|
||||
return;
|
||||
}
|
||||
near("the position", 120.5D, back.x);
|
||||
near("the height", 64.0D, back.y);
|
||||
near("the other side", -33.25D, back.z);
|
||||
near("the life", 73.5F, back.health);
|
||||
eq("the mode", Integer.valueOf(1), Integer.valueOf(back.gameMode));
|
||||
eq("the rock is in the first slot", Integer.valueOf(ItemRegistry.ROCK),
|
||||
Integer.valueOf(back.inventory[0][0].item));
|
||||
eq("and the scrap in the fourth", Integer.valueOf(17),
|
||||
Integer.valueOf(back.inventory[3][0].count));
|
||||
eq("the recipes are remembered", Integer.valueOf(1),
|
||||
Integer.valueOf(back.recipes.size()));
|
||||
eq("and the tree too", Integer.valueOf(1), Integer.valueOf(back.tree.size()));
|
||||
}
|
||||
|
||||
private static void testMissing(Path work) throws Exception {
|
||||
PlayerData data = new PlayerData(work.resolve("players-missing"));
|
||||
yes("there is nothing to read", data.load("Nobody") == null);
|
||||
}
|
||||
|
||||
private static void testSafeName(Path work) throws Exception {
|
||||
PlayerData data = new PlayerData(work.resolve("players-safe"));
|
||||
String name = data.fileOf("bad/name:with*chars").getFileName().toString();
|
||||
yes("the name is safe", !name.contains("/") && !name.contains(":")
|
||||
&& !name.contains("*"));
|
||||
}
|
||||
|
||||
private static void testArmor(Path work) throws Exception {
|
||||
Path folder = work.resolve("players-armor");
|
||||
PlayerData data = new PlayerData(folder);
|
||||
PlayerData.PlayerState state = new PlayerData.PlayerState();
|
||||
// the chest has two layers: the clothes under and the armour over
|
||||
state.armor[ItemType.ARMOR_CHEST - 1][0] = ItemStack.of(ItemRegistry.T_SHIRT, 1);
|
||||
state.armor[ItemType.ARMOR_CHEST - 1][1] = ItemStack.of(ItemRegistry.ROADSIGN_JACKET, 1);
|
||||
data.save("Dressed", state);
|
||||
|
||||
PlayerData.PlayerState back = data.load("Dressed");
|
||||
yes("it is read back", back != null);
|
||||
if (back == null) {
|
||||
return;
|
||||
}
|
||||
eq("the clothes are under", Integer.valueOf(ItemRegistry.T_SHIRT),
|
||||
Integer.valueOf(back.armor[ItemType.ARMOR_CHEST - 1][0].item));
|
||||
eq("the armour is over", Integer.valueOf(ItemRegistry.ROADSIGN_JACKET),
|
||||
Integer.valueOf(back.armor[ItemType.ARMOR_CHEST - 1][1].item));
|
||||
}
|
||||
|
||||
private static void near(Object what, double expected, double actual) {
|
||||
if (Math.abs(expected - actual) > 0.001D) {
|
||||
failed++;
|
||||
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user