Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - prepare-game.ps1 пересобирает клиент целиком: ванильный jar, SRG и MCP, SpecialSource, FernFlower, патчи, автофиксеры, javac. Идемпотентен. - Свой код игры лежит в src-overlay, потому что src пересоздаётся при каждой сборке и всё написанное вручную там погибает. - 116 патчей в patches/decompiler-fixes.txt. Каждый сверялся с оригинальным байткодом через javap: компиляция не ловит ошибки декомпиляции. - struct_diff.py сравнивает исходный байткод с нашей сборкой по классам, интерфейсам, суперклассам, методам и полям, и отдельно проверяет, что пропавшие классы действительно никому не нужны. Загрузчик - Собирается и тестируется без игры: build.ps1 не использует classpath вообще, поэтому SelfTest из 128 проверок проходит без запущенного форка. - Клиентский API не содержит ни одного игрового типа, реализацию даёт игра. - Политика: мод меняет то, что клиент показывает, но никогда то, что истинно в мире. Мир, предметы, игроки и сеть принадлежат серверу. - Capability приведены в соответствие с этой политикой: осталось шесть, и за каждым стоит реальная проверка. Прежние WORLD, PLAYER, NETWORK, TEXT, INPUT и COMMAND выдавались, но ничего не проверяли. - Команды модов имеют область CLIENT, SERVER или BOTH. Серверные не выполняются на клиенте никогда. - Свой jar загрузчика лежит в classpath игры и сборки форка. Инструменты - add_patch.py, чтобы каждое исправление переживало передекомпиляцию - find_self_comparisons.py, find_remove_index_bugs.py, find_callers.py - stage_run.py и fetch_missing.py для запуска клиента
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package net.rustnotmc.client;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import net.rustnotmc.api.client.ClientCommand;
|
||||
import net.rustnotmc.api.client.CommandArgs;
|
||||
import net.rustnotmc.api.client.CommandScope;
|
||||
import net.rustnotmc.api.client.Commands;
|
||||
import net.rustnotmc.loader.Log;
|
||||
import net.rustnotmc.loader.ModLoader;
|
||||
|
||||
/**
|
||||
* The mod command table.
|
||||
*
|
||||
* Only a CLIENT scoped command is carried out here. SERVER and BOTH are not
|
||||
* consumed by the client at all: the line is left for the server to take, which
|
||||
* is the whole point of the scope. The client never decides an effect the server
|
||||
* owns.
|
||||
*
|
||||
* The table is shared by every mod, so a name can only be claimed once. A second
|
||||
* mod asking for a taken name is refused loudly rather than silently shadowing
|
||||
* the first, which would be miserable to debug.
|
||||
*
|
||||
* Lives in game-fork/src-overlay; src is regenerated on every build.
|
||||
*/
|
||||
final class RsxCommands implements Commands {
|
||||
|
||||
private static final class Entry {
|
||||
final String owner;
|
||||
final ClientCommand handler;
|
||||
final CommandScope scope;
|
||||
|
||||
Entry(String owner, ClientCommand handler, CommandScope scope) {
|
||||
this.owner = owner;
|
||||
this.handler = handler;
|
||||
this.scope = scope;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, Entry> entries = new LinkedHashMap<String, Entry>();
|
||||
|
||||
@Override
|
||||
public boolean register(String name, ClientCommand handler, CommandScope scope) {
|
||||
if (name == null || handler == null || scope == null) {
|
||||
return false;
|
||||
}
|
||||
String key = normalise(name);
|
||||
if (key.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
String owner = registering();
|
||||
Entry clash = this.entries.get(key);
|
||||
if (clash != null) {
|
||||
if (clash.owner.equals(owner) && clash.handler == handler) {
|
||||
return true;
|
||||
}
|
||||
Log.warn("command /" + key + " is already registered by " + clash.owner
|
||||
+ "; the second registration was refused");
|
||||
return false;
|
||||
}
|
||||
this.entries.put(key, new Entry(owner, handler, scope));
|
||||
Log.info("registered command /" + key + " scope=" + scope.name().toLowerCase());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean register(String name, ClientCommand handler) {
|
||||
return register(name, handler, CommandScope.CLIENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegistered(String name) {
|
||||
return name != null && this.entries.containsKey(normalise(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandScope scopeOf(String name) {
|
||||
Entry e = name == null ? null : this.entries.get(normalise(name));
|
||||
return e == null ? null : e.scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count() {
|
||||
return this.entries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Offers a typed line to the mod commands.
|
||||
*
|
||||
* @return true when the client carried it out and the line must not be sent
|
||||
* on. False means the line is not a client command, which is the
|
||||
* answer for SERVER and BOTH: the server has to see those.
|
||||
*/
|
||||
boolean dispatch(String line, String sender) {
|
||||
if (line == null || !line.startsWith("/")) {
|
||||
return false;
|
||||
}
|
||||
String body = line.substring(1).trim();
|
||||
int space = body.indexOf(' ');
|
||||
String name = space < 0 ? body : body.substring(0, space);
|
||||
Entry entry = this.entries.get(normalise(name));
|
||||
if (entry == null || entry.scope != CommandScope.CLIENT) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
entry.handler.run(CommandArgs.parse(body, sender));
|
||||
} catch (Throwable t) {
|
||||
Log.error("command /" + name + " failed", t);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String normalise(String name) {
|
||||
String v = name.trim();
|
||||
if (v.startsWith("/")) {
|
||||
v = v.substring(1);
|
||||
}
|
||||
return v.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Who is registering right now.
|
||||
*
|
||||
* The loader knows this because it is the one calling onConstruct, and the
|
||||
* game can read it without the loader ever depending on the game.
|
||||
*/
|
||||
private static String registering() {
|
||||
String who = ModLoader.REGISTERING.get();
|
||||
return who == null ? "unknown" : who;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user