Files
RustNotMC/game-fork/src-overlay/net/rustnotmc/client/RsxCommands.java
T
sasheg dev 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 для запуска клиента
2026-10-18 21:31:27 +03:00

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;
}
}