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 для запуска клиента
53 lines
1.7 KiB
Python
53 lines
1.7 KiB
Python
"""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()) |