Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - 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,53 @@
|
||||
"""Append a hand fix to the patch file so it survives a re-decompile.
|
||||
|
||||
Keeping every fix in one reviewed file, rather than editing generated source,
|
||||
is what makes prepare-game.ps1 reproducible: the tree is thrown away and rebuilt
|
||||
on every run, so a fix that is not recorded simply disappears.
|
||||
|
||||
python add_patch.py <patch-file> <java-file-path> <before-file> <after-file>
|
||||
|
||||
The before/after files hold the exact text to replace and must match the
|
||||
decompiled output byte for byte.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
patch_file, java_path, before_file, after_file = sys.argv[1:5]
|
||||
with io.open(before_file, encoding='utf-8') as handle:
|
||||
before = handle.read().rstrip('\n')
|
||||
with io.open(after_file, encoding='utf-8') as handle:
|
||||
after = handle.read().rstrip('\n')
|
||||
|
||||
if not before or not after:
|
||||
print('ERROR: before/after must not be empty')
|
||||
return 1
|
||||
if before == after:
|
||||
print('ERROR: before and after are identical')
|
||||
return 1
|
||||
|
||||
existing = ''
|
||||
if os.path.isfile(patch_file):
|
||||
with io.open(patch_file, encoding='utf-8') as handle:
|
||||
existing = handle.read()
|
||||
|
||||
header = '=== FILE %s\n' % java_path
|
||||
if header + '--- BEFORE\n' + before + '\n--- AFTER\n' in existing:
|
||||
print('patch already present')
|
||||
return 0
|
||||
|
||||
with io.open(patch_file, 'a', encoding='utf-8') as handle:
|
||||
if existing and not existing.endswith('\n'):
|
||||
handle.write('\n')
|
||||
handle.write('\n' + header)
|
||||
handle.write('--- BEFORE\n' + before + '\n')
|
||||
handle.write('--- AFTER\n' + after + '\n')
|
||||
print('appended patch for %s' % java_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user