rsxm-lib: общая библиотека для модов
Загрузчик игры даёт каждому моду свой class loader, поэтому моды не видят друг друга. Но у всех этих загрузчиков родитель — системный, а значит всё, что попало в его class path, доступно каждому моду. Агент подмешивает jar'ы из libs\ именно туда, поэтому библиотека одна на всех, и модам не нужно вшивать её в себя. Первый набор: - json.Json — разборщик и писатель JSON без внешних зависимостей; - config.Config — настройки мода в config\<mod>.json, битый файл не роняет игру, а откатывается на значения по умолчанию с записью в лог; - config.Home — где лежит модлоадер, чтобы мод не гадал; - config.Log — логи с именем мода. build.cmd собирает jar и сразу гоняет тесты библиотеки.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
*.class
|
||||
@@ -0,0 +1,39 @@
|
||||
# rsxm-lib
|
||||
|
||||
Общая библиотека для модов RustexRemake. Лежит в `libs\` рядом с `mods\`, и
|
||||
агент подмешивает её в системный class path при старте — поэтому моды видят её
|
||||
без дублирования: один jar, одна копия в памяти.
|
||||
|
||||
Загрузчик игры даёт каждому моду свой class loader, и напрямую моды друг друга
|
||||
не видят. Но у всех этих загрузчиков родитель — системный, а значит всё, что
|
||||
в нём, доступно каждому. На этом и построена библиотека.
|
||||
|
||||
## Что внутри
|
||||
|
||||
* `net.rsxm.lib.json.Json` — маленький разборщик и писатель JSON. Без внешних
|
||||
зависимостей: мод — это jar, который друг бросил в папку, тащить туда ещё и
|
||||
библиотеку незачем.
|
||||
* `net.rsxm.lib.config.Config` — настройки мода в `config\<mod>.json`.
|
||||
Значения по умолчанию задаёт сам мод, битый файл не роняет игру, а заменяется
|
||||
дефолтами с записью в лог.
|
||||
* `net.rsxm.lib.config.Home` — где вообще лежит модлоадер, чтобы мод не гадал.
|
||||
* `net.rsxm.lib.config.Log` — логирование с именем мода.
|
||||
|
||||
## Сборка
|
||||
|
||||
build.cmd
|
||||
|
||||
Нужен только JDK. Скрипт собирает `dist\rsxm-lib.jar` и сразу прогоняет тесты
|
||||
JSON и конфигов.
|
||||
|
||||
## Использование
|
||||
|
||||
Компилируйте мод против `rsx-api.jar` **и** `rsxm-lib.jar`, но не кладите
|
||||
`rsxm-lib.jar` внутрь мода — он и так на class path:
|
||||
|
||||
javac -cp "rsx-api.jar;rsxm-lib.jar" ...
|
||||
|
||||
Config cfg = Config.of("hud");
|
||||
boolean show = cfg.bool("show", true);
|
||||
cfg.set("x", 8);
|
||||
cfg.save();
|
||||
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
rem Builds rsxm-lib: the jar goes to dist\, the tests run right after.
|
||||
rem Only a JDK is needed.
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
if not exist "build\classes" mkdir "build\classes"
|
||||
if not exist "build\testclasses" mkdir "build\testclasses"
|
||||
if not exist "dist" mkdir "dist"
|
||||
|
||||
echo [1/3] compiling...
|
||||
dir /s /b src\main\java\*.java > build\sources.txt
|
||||
dir /s /b src\test\java\*.java > build\testsources.txt
|
||||
javac -encoding UTF-8 --release 8 -nowarn -cp "libs\rsx-api.jar" -d build\classes @build\sources.txt
|
||||
if errorlevel 1 goto :fail
|
||||
javac -encoding UTF-8 --release 8 -nowarn -cp "libs\rsx-api.jar;build\classes" -d build\testclasses @build\testsources.txt
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
echo [2/3] packaging rsxm-lib.jar...
|
||||
if exist "dist\rsxm-lib.jar" del "dist\rsxm-lib.jar"
|
||||
jar cf "dist\rsxm-lib.jar" -C build\classes .
|
||||
|
||||
echo [3/3] running the library tests...
|
||||
java -cp "libs\rsx-api.jar;build\classes;build\testclasses" net.rsxm.lib.test.LibTest
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
echo.
|
||||
echo Готово: dist\rsxm-lib.jar
|
||||
exit /b 0
|
||||
|
||||
:fail
|
||||
echo.
|
||||
echo Сборка не удалась.
|
||||
exit /b 1
|
||||
Binary file not shown.
@@ -0,0 +1,237 @@
|
||||
package net.rsxm.lib.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.rsxm.lib.json.Json;
|
||||
|
||||
/**
|
||||
* A mod's settings, kept in a JSON file under config\.
|
||||
*
|
||||
* A mod declares its defaults once, asks for a value by name, and never has to
|
||||
* think about where the file lives or whether it is readable: a missing or
|
||||
* broken file falls back to the defaults, is rewritten, and the reason lands in
|
||||
* the log.
|
||||
*
|
||||
* Config cfg = Config.of("hud");
|
||||
* boolean show = cfg.bool("show", true);
|
||||
* cfg.save();
|
||||
*/
|
||||
public final class Config {
|
||||
|
||||
private static final Map<String, Config> LOADED =
|
||||
new LinkedHashMap<String, Config>();
|
||||
|
||||
private final String mod;
|
||||
private final File file;
|
||||
private final Map<String, Object> values = new LinkedHashMap<String, Object>();
|
||||
private boolean dirty;
|
||||
|
||||
private Config(String mod, File file) {
|
||||
this.mod = mod;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/** The config of a mod, shared so two calls see the same object. */
|
||||
public static synchronized Config of(String mod) {
|
||||
Config c = LOADED.get(mod);
|
||||
if (c == null) {
|
||||
c = new Config(mod, new File(Home.configDir(), mod + ".json"));
|
||||
c.load();
|
||||
LOADED.put(mod, c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private void load() {
|
||||
if (!file.isFile()) {
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
InputStream in = new FileInputStream(file);
|
||||
try {
|
||||
ByteBuilder b = new ByteBuilder();
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) {
|
||||
b.add(buf, n);
|
||||
}
|
||||
Object parsed = Json.parse(b.toString("UTF-8"));
|
||||
if (parsed instanceof Map) {
|
||||
for (Map.Entry<?, ?> e : ((Map<?, ?>) parsed).entrySet()) {
|
||||
values.put(String.valueOf(e.getKey()), e.getValue());
|
||||
}
|
||||
} else {
|
||||
dirty = true;
|
||||
}
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.warn(mod + ": cannot read " + file.getName() + " (" + t
|
||||
+ "), using defaults");
|
||||
values.clear();
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Write the config back, if anything changed. */
|
||||
public synchronized void save() {
|
||||
if (!dirty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
File dir = file.getParentFile();
|
||||
if (dir != null && !dir.isDirectory() && !dir.mkdirs()) {
|
||||
Log.warn(mod + ": cannot create " + dir);
|
||||
return;
|
||||
}
|
||||
OutputStream out = new FileOutputStream(file);
|
||||
try {
|
||||
out.write(Json.write(values).getBytes("UTF-8"));
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
dirty = false;
|
||||
} catch (Throwable t) {
|
||||
Log.warn(mod + ": cannot write " + file.getName() + ": " + t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reload from disk, dropping unsaved changes. */
|
||||
public synchronized void reload() {
|
||||
values.clear();
|
||||
dirty = false;
|
||||
load();
|
||||
}
|
||||
|
||||
public File file() {
|
||||
return file;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- getters
|
||||
|
||||
public boolean bool(String key, boolean def) {
|
||||
Object v = values.get(key);
|
||||
if (v instanceof Boolean) {
|
||||
return ((Boolean) v).booleanValue();
|
||||
}
|
||||
if (v instanceof String) {
|
||||
return Boolean.parseBoolean((String) v);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
public int integer(String key, int def) {
|
||||
Object v = number(key);
|
||||
return v == null ? def : (int) ((Number) v).longValue();
|
||||
}
|
||||
|
||||
public long number(String key, long def) {
|
||||
Object v = number(key);
|
||||
return v == null ? def : ((Number) v).longValue();
|
||||
}
|
||||
|
||||
public double decimal(String key, double def) {
|
||||
Object v = number(key);
|
||||
return v == null ? def : ((Number) v).doubleValue();
|
||||
}
|
||||
|
||||
public String text(String key, String def) {
|
||||
Object v = values.get(key);
|
||||
return v == null ? def : String.valueOf(v);
|
||||
}
|
||||
|
||||
public List<String> list(String key) {
|
||||
Object v = values.get(key);
|
||||
List<String> out = new ArrayList<String>();
|
||||
if (v instanceof List) {
|
||||
for (Object o : (List<?>) v) {
|
||||
out.add(String.valueOf(o));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Object number(String key) {
|
||||
Object v = values.get(key);
|
||||
if (v instanceof Number) {
|
||||
return v;
|
||||
}
|
||||
if (v instanceof String) {
|
||||
try {
|
||||
return Double.valueOf((String) v);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- setters
|
||||
|
||||
public Config set(String key, boolean value) {
|
||||
return put(key, Boolean.valueOf(value));
|
||||
}
|
||||
|
||||
public Config set(String key, int value) {
|
||||
return put(key, Integer.valueOf(value));
|
||||
}
|
||||
|
||||
public Config set(String key, long value) {
|
||||
return put(key, Long.valueOf(value));
|
||||
}
|
||||
|
||||
public Config set(String key, double value) {
|
||||
return put(key, Double.valueOf(value));
|
||||
}
|
||||
|
||||
public Config set(String key, String value) {
|
||||
return put(key, value);
|
||||
}
|
||||
|
||||
public Config set(String key, List<String> value) {
|
||||
return put(key, new ArrayList<Object>(value));
|
||||
}
|
||||
|
||||
public Config remove(String key) {
|
||||
if (values.remove(key) != null) {
|
||||
dirty = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Config put(String key, Object value) {
|
||||
Object old = values.put(key, value);
|
||||
if (old == null || !old.equals(value)) {
|
||||
dirty = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Every key currently in the file, defaults included after a save. */
|
||||
public synchronized List<String> keys() {
|
||||
return new ArrayList<String>(values.keySet());
|
||||
}
|
||||
|
||||
private static final class ByteBuilder {
|
||||
private final java.io.ByteArrayOutputStream out =
|
||||
new java.io.ByteArrayOutputStream();
|
||||
|
||||
void add(byte[] b, int n) {
|
||||
out.write(b, 0, n);
|
||||
}
|
||||
|
||||
String toString(String enc) throws Exception {
|
||||
return out.toString(enc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.rsxm.lib.config;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Where the modloader keeps things, discovered once from the class path.
|
||||
*
|
||||
* The bridge jar sits in <home>\bridge\, so its parent is the modloader home.
|
||||
* A mod that asks "where is my config" should not have to know that, and it
|
||||
* should not have to care whether the game is running or not.
|
||||
*/
|
||||
public final class Home {
|
||||
|
||||
private static File dir;
|
||||
|
||||
private Home() {
|
||||
}
|
||||
|
||||
public static synchronized File dir() {
|
||||
if (dir == null) {
|
||||
dir = discover();
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk up from where this code lives until a directory looks like a
|
||||
* modloader home, i.e. it has bridge\ or mods\ next to it. The library is
|
||||
* normally in <home>\libs\, but the same jar on a class path during a build
|
||||
* or a test sits somewhere else entirely, so nothing here may assume a
|
||||
* fixed depth.
|
||||
*/
|
||||
private static File discover() {
|
||||
try {
|
||||
File self = new File(Home.class.getProtectionDomain()
|
||||
.getCodeSource().getLocation().toURI());
|
||||
File at = self.isDirectory() ? self : self.getParentFile();
|
||||
for (int up = 0; at != null && up < 4; up++) {
|
||||
if (isHome(at)) {
|
||||
return at;
|
||||
}
|
||||
at = at.getParentFile();
|
||||
}
|
||||
if (!self.isDirectory() && self.getParentFile() != null) {
|
||||
return self.getParentFile();
|
||||
}
|
||||
return self;
|
||||
} catch (Throwable ignored) {
|
||||
return new File(System.getProperty("user.home"), "rsx-mod");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHome(File f) {
|
||||
return new File(f, "bridge").isDirectory() || new File(f, "mods").isDirectory();
|
||||
}
|
||||
|
||||
/** <home>\config, created on demand. */
|
||||
public static File configDir() {
|
||||
File d = new File(dir(), "config");
|
||||
if (!d.isDirectory()) {
|
||||
d.mkdirs();
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/** <home>\mods */
|
||||
public static File modsDir() {
|
||||
return new File(dir(), "mods");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.rsxm.lib.config;
|
||||
|
||||
/**
|
||||
* Logging for mods, so a mod's messages land in the same bridge log as the
|
||||
* modloader's own and carry the mod's name.
|
||||
*/
|
||||
public final class Log {
|
||||
|
||||
private Log() {
|
||||
}
|
||||
|
||||
public static void info(String mod, String message) {
|
||||
net.rustexremake.api.Rsx.info("[" + mod + "] " + message);
|
||||
}
|
||||
|
||||
public static void warn(String mod, String message) {
|
||||
net.rustexremake.api.Rsx.warn("[" + mod + "] " + message);
|
||||
}
|
||||
|
||||
public static void error(String mod, String message, Throwable cause) {
|
||||
net.rustexremake.api.Rsx.error("[" + mod + "] " + message, cause);
|
||||
}
|
||||
|
||||
public static void warn(String message) {
|
||||
net.rustexremake.api.Rsx.warn("[rsxm-lib] " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package net.rsxm.lib.json;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A very small JSON reader and writer.
|
||||
*
|
||||
* Mod configs are plain files next to the mods, and a mod is a jar a friend
|
||||
* dropped into a folder. That rules out pulling a JSON library into the mod
|
||||
* classpath, so this parses the subset that actually shows up in a config:
|
||||
* objects, arrays, strings, numbers, booleans and null.
|
||||
*
|
||||
* It is deliberately forgiving: a broken file does not throw into the game, it
|
||||
* falls back to the defaults and says so once in the log.
|
||||
*/
|
||||
public final class Json {
|
||||
|
||||
private Json() {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ parse
|
||||
|
||||
public static Object parse(String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Reader r = new Reader(text);
|
||||
Object v = r.value();
|
||||
r.skip();
|
||||
return v;
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Reader {
|
||||
private final String s;
|
||||
private int i;
|
||||
|
||||
Reader(String s) {
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
void skip() {
|
||||
while (i < s.length() && Character.isWhitespace(s.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
Object value() {
|
||||
skip();
|
||||
if (i >= s.length()) {
|
||||
throw new IllegalStateException("unexpected end");
|
||||
}
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '{': return object();
|
||||
case '[': return array();
|
||||
case '"': return string();
|
||||
case 't': expect("true"); return Boolean.TRUE;
|
||||
case 'f': expect("false"); return Boolean.FALSE;
|
||||
case 'n': expect("null"); return null;
|
||||
default: return number();
|
||||
}
|
||||
}
|
||||
|
||||
private void expect(String word) {
|
||||
if (!s.startsWith(word, i)) {
|
||||
throw new IllegalStateException("expected " + word);
|
||||
}
|
||||
i += word.length();
|
||||
}
|
||||
|
||||
Map<String, Object> object() {
|
||||
Map<String, Object> out = new LinkedHashMap<String, Object>();
|
||||
i++; // {
|
||||
skip();
|
||||
if (i < s.length() && s.charAt(i) == '}') {
|
||||
i++;
|
||||
return out;
|
||||
}
|
||||
while (true) {
|
||||
skip();
|
||||
String key = string();
|
||||
skip();
|
||||
if (i >= s.length() || s.charAt(i) != ':') {
|
||||
throw new IllegalStateException("expected :");
|
||||
}
|
||||
i++;
|
||||
out.put(key, value());
|
||||
skip();
|
||||
if (i >= s.length()) {
|
||||
throw new IllegalStateException("unterminated object");
|
||||
}
|
||||
char c = s.charAt(i++);
|
||||
if (c == '}') {
|
||||
return out;
|
||||
}
|
||||
if (c != ',') {
|
||||
throw new IllegalStateException("expected , or }");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Object> array() {
|
||||
List<Object> out = new ArrayList<Object>();
|
||||
i++; // [
|
||||
skip();
|
||||
if (i < s.length() && s.charAt(i) == ']') {
|
||||
i++;
|
||||
return out;
|
||||
}
|
||||
while (true) {
|
||||
out.add(value());
|
||||
skip();
|
||||
if (i >= s.length()) {
|
||||
throw new IllegalStateException("unterminated array");
|
||||
}
|
||||
char c = s.charAt(i++);
|
||||
if (c == ']') {
|
||||
return out;
|
||||
}
|
||||
if (c != ',') {
|
||||
throw new IllegalStateException("expected , or ]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String string() {
|
||||
if (i >= s.length() || s.charAt(i) != '"') {
|
||||
throw new IllegalStateException("expected string");
|
||||
}
|
||||
i++;
|
||||
StringBuilder b = new StringBuilder();
|
||||
while (i < s.length()) {
|
||||
char c = s.charAt(i++);
|
||||
if (c == '"') {
|
||||
return b.toString();
|
||||
}
|
||||
if (c != '\\') {
|
||||
b.append(c);
|
||||
continue;
|
||||
}
|
||||
char e = s.charAt(i++);
|
||||
switch (e) {
|
||||
case 'n': b.append('\n'); break;
|
||||
case 't': b.append('\t'); break;
|
||||
case 'r': b.append('\r'); break;
|
||||
case 'b': b.append('\b'); break;
|
||||
case 'f': b.append('\f'); break;
|
||||
case 'u':
|
||||
b.append((char) Integer.parseInt(
|
||||
s.substring(i, i + 4), 16));
|
||||
i += 4;
|
||||
break;
|
||||
default: b.append(e);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("unterminated string");
|
||||
}
|
||||
|
||||
Object number() {
|
||||
int start = i;
|
||||
while (i < s.length() && "+-.eE0123456789".indexOf(s.charAt(i)) >= 0) {
|
||||
i++;
|
||||
}
|
||||
String raw = s.substring(start, i);
|
||||
if (raw.isEmpty()) {
|
||||
throw new IllegalStateException("expected a value at " + start);
|
||||
}
|
||||
if (raw.indexOf('.') < 0 && raw.indexOf('e') < 0
|
||||
&& raw.indexOf('E') < 0) {
|
||||
return Long.valueOf(raw);
|
||||
}
|
||||
return Double.valueOf(raw);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ write
|
||||
|
||||
public static String write(Object value) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
write(value, b, 0);
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
private static void write(Object v, StringBuilder b, int depth) {
|
||||
if (v == null) {
|
||||
b.append("null");
|
||||
} else if (v instanceof Map) {
|
||||
writeObject((Map<?, ?>) v, b, depth);
|
||||
} else if (v instanceof List) {
|
||||
writeArray((List<?>) v, b, depth);
|
||||
} else if (v instanceof Boolean) {
|
||||
b.append(v.toString());
|
||||
} else if (v instanceof Number) {
|
||||
writeNumber((Number) v, b);
|
||||
} else {
|
||||
quote(String.valueOf(v), b);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeNumber(Number n, StringBuilder b) {
|
||||
double d = n.doubleValue();
|
||||
if (n instanceof Double || n instanceof Float) {
|
||||
if (d == Math.rint(d) && !Double.isInfinite(d)) {
|
||||
b.append((long) d).append(".0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
b.append(n.toString());
|
||||
}
|
||||
|
||||
private static void writeObject(Map<?, ?> m, StringBuilder b, int depth) {
|
||||
if (m.isEmpty()) {
|
||||
b.append("{}");
|
||||
return;
|
||||
}
|
||||
b.append("{\n");
|
||||
int n = 0;
|
||||
for (Map.Entry<?, ?> e : m.entrySet()) {
|
||||
if (n++ > 0) {
|
||||
b.append(",\n");
|
||||
}
|
||||
indent(b, depth + 1);
|
||||
quote(String.valueOf(e.getKey()), b);
|
||||
b.append(": ");
|
||||
write(e.getValue(), b, depth + 1);
|
||||
}
|
||||
b.append('\n');
|
||||
indent(b, depth);
|
||||
b.append('}');
|
||||
}
|
||||
|
||||
private static void writeArray(List<?> l, StringBuilder b, int depth) {
|
||||
if (l.isEmpty()) {
|
||||
b.append("[]");
|
||||
return;
|
||||
}
|
||||
b.append("[\n");
|
||||
for (int i = 0; i < l.size(); i++) {
|
||||
if (i > 0) {
|
||||
b.append(",\n");
|
||||
}
|
||||
indent(b, depth + 1);
|
||||
write(l.get(i), b, depth + 1);
|
||||
}
|
||||
b.append('\n');
|
||||
indent(b, depth);
|
||||
b.append(']');
|
||||
}
|
||||
|
||||
private static void indent(StringBuilder b, int depth) {
|
||||
for (int i = 0; i < depth; i++) {
|
||||
b.append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
static void quote(String s, StringBuilder b) {
|
||||
b.append('"');
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '"': b.append("\\\""); break;
|
||||
case '\\': b.append("\\\\"); break;
|
||||
case '\n': b.append("\\n"); break;
|
||||
case '\r': b.append("\\r"); break;
|
||||
case '\t': b.append("\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
b.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
b.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
b.append('"');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package net.rsxm.lib.test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import net.rsxm.lib.config.Config;
|
||||
import net.rsxm.lib.config.Home;
|
||||
import net.rsxm.lib.json.Json;
|
||||
|
||||
/**
|
||||
* Checks the parts of the library that have nothing to do with the game: the
|
||||
* JSON codec and the config file. Run it straight from the build, it needs no
|
||||
* mods and no game.
|
||||
*/
|
||||
public final class LibTest {
|
||||
|
||||
private static int failed;
|
||||
|
||||
public static void main(String[] args) {
|
||||
jsonRoundTrip();
|
||||
jsonBroken();
|
||||
configRoundTrip();
|
||||
home();
|
||||
|
||||
System.out.println(failed == 0
|
||||
? "all library checks passed"
|
||||
: failed + " check(s) failed");
|
||||
System.exit(failed == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
private static void check(String what, boolean ok) {
|
||||
System.out.println(" " + what + " " + (ok ? "OK" : "FAIL"));
|
||||
if (!ok) {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void jsonRoundTrip() {
|
||||
System.out.println("== json ==");
|
||||
String src = "{\"a\":1,\"b\":2.5,\"c\":\"текст\",\"d\":true,"
|
||||
+ "\"e\":null,\"f\":[1,2,{\"g\":\"h\"}]}";
|
||||
Object v = Json.parse(src);
|
||||
check("parses to a map", v instanceof java.util.Map);
|
||||
java.util.Map<String, Object> m = (java.util.Map<String, Object>) v;
|
||||
check("keeps integers", Long.valueOf(1L).equals(m.get("a")));
|
||||
check("keeps decimals", Double.valueOf(2.5).equals(m.get("b")));
|
||||
check("keeps non-ascii", "текст".equals(m.get("c")));
|
||||
check("keeps booleans", Boolean.TRUE.equals(m.get("d")));
|
||||
check("keeps null", m.containsKey("e") && m.get("e") == null);
|
||||
check("keeps arrays", m.get("f") instanceof List);
|
||||
|
||||
String out = Json.write(m);
|
||||
Object again = Json.parse(out);
|
||||
check("write then parse is stable",
|
||||
again instanceof java.util.Map && m.equals(again));
|
||||
check("escapes quotes", Json.write("a\"b").equals("\"a\\\"b\""));
|
||||
}
|
||||
|
||||
private static void jsonBroken() {
|
||||
System.out.println("== json, broken input ==");
|
||||
check("garbage does not throw", Json.parse("{not json") == null);
|
||||
check("empty does not throw", Json.parse("") == null);
|
||||
check("null does not throw", Json.parse(null) == null);
|
||||
}
|
||||
|
||||
private static void configRoundTrip() {
|
||||
System.out.println("== config ==");
|
||||
Config c = Config.of("rsxm-test");
|
||||
c.set("show", true)
|
||||
.set("x", 3)
|
||||
.set("scale", 1.5)
|
||||
.set("label", "тест")
|
||||
.set("list", java.util.Arrays.asList("a", "b"));
|
||||
c.save();
|
||||
|
||||
File f = c.file();
|
||||
check("file exists", f.isFile());
|
||||
check("defaults when absent", c.bool("missing", true));
|
||||
|
||||
// read it back through a fresh object
|
||||
Config fresh = newFileConfig("rsxm-test");
|
||||
check("reads booleans", fresh.bool("show", false));
|
||||
check("reads integers", fresh.integer("x", 0) == 3);
|
||||
check("reads decimals", fresh.decimal("scale", 0d) == 1.5d);
|
||||
check("reads text", "тест".equals(fresh.text("label", "")));
|
||||
check("reads lists", fresh.list("list").size() == 2);
|
||||
f.delete();
|
||||
}
|
||||
|
||||
private static void home() {
|
||||
System.out.println("== home ==");
|
||||
File d = Home.dir();
|
||||
check("home is a directory", d.isDirectory());
|
||||
check("config dir is created", Home.configDir().isDirectory());
|
||||
System.out.println(" home: " + d);
|
||||
}
|
||||
|
||||
/** A second view of the same file, bypassing the shared instance. */
|
||||
private static Config newFileConfig(String mod) {
|
||||
try {
|
||||
java.lang.reflect.Constructor<Config> c = Config.class
|
||||
.getDeclaredConstructor(String.class, File.class);
|
||||
c.setAccessible(true);
|
||||
Config cfg = c.newInstance(mod, new File(Home.configDir(), mod + ".json"));
|
||||
java.lang.reflect.Method load = Config.class
|
||||
.getDeclaredMethod("load");
|
||||
load.setAccessible(true);
|
||||
load.invoke(cfg);
|
||||
return cfg;
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalStateException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user