bdf0ae866a
Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - 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 для запуска клиента
132 lines
4.2 KiB
Java
132 lines
4.2 KiB
Java
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;
|
|
}
|
|
} |