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 для запуска клиента
147 lines
5.3 KiB
Python
147 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Repair FernFlower's stale local variable references, scoped to the method.
|
|
|
|
The decompiler renames conflicting locals only sometimes: a declaration ends up
|
|
as `variantlist` while its uses stay `variantlist1`, and javac reports
|
|
"cannot find symbol ... variable variantlist1".
|
|
|
|
Renaming is confined to the enclosing method. That matters because the suffixed
|
|
name is often a real, correctly declared variable in a *different* method of the
|
|
same class, and a whole-file rename would corrupt it.
|
|
|
|
The method is found by its declaration line and the closing brace at the same
|
|
indentation, which is reliable for well-formed decompiled output.
|
|
|
|
python fix_stale_locals.py <errors.txt> [--apply]
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import os
|
|
import re
|
|
|
|
ERROR = re.compile(r'^(?P<file>[A-Za-z]:.*?\.java):(?P<line>\d+): error: (?P<msg>.*)$')
|
|
SYMBOL = re.compile(r'^ {2}symbol:\s*variable (?P<name>\w+)\s*$')
|
|
DECL = re.compile(r'^[ \t]*(?:final\s+)?[A-Za-z_$][\w$.<>,\[\] ]*\s+(?P<name>[A-Za-z_$]\w*)\s*(?==|;|,|\))')
|
|
METHOD = re.compile(r'^[ \t]{0,8}(?:(?:public|private|protected|static|final|abstract|synchronized|native|default|strictfp)\s+)*'
|
|
r'[\w$.<>,\[\] ]+\s+\w+\s*\([^;]*\)\s*(?:throws\s+[\w., ]+)?\s*\{\s*$')
|
|
|
|
|
|
def method_regions(lines):
|
|
"""Returns [(start_index, end_index_exclusive, indent)] for each method."""
|
|
regions = []
|
|
for i, line in enumerate(lines):
|
|
if not METHOD.match(line):
|
|
continue
|
|
indent = len(line) - len(line.lstrip())
|
|
close = ' ' * indent + '}'
|
|
for j in range(i + 1, len(lines)):
|
|
if lines[j].rstrip() == close:
|
|
regions.append((i, j + 1, indent))
|
|
break
|
|
return regions
|
|
|
|
|
|
def region_of(regions, index):
|
|
for start, end, _ in regions:
|
|
if start <= index < end:
|
|
return start, end
|
|
return None
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('errors')
|
|
parser.add_argument('--apply', action='store_true')
|
|
args = parser.parse_args()
|
|
|
|
missing = collections.defaultdict(set)
|
|
current = None
|
|
with open(args.errors, encoding='utf-8', errors='replace') as handle:
|
|
for raw in handle:
|
|
line = raw.rstrip('\n').rstrip('\r')
|
|
m = ERROR.match(line)
|
|
if m:
|
|
current = m.group('file')
|
|
continue
|
|
s = SYMBOL.match(line)
|
|
if s and current:
|
|
missing[current].add(s.group('name'))
|
|
|
|
files_changed = 0
|
|
renames = 0
|
|
skipped = []
|
|
|
|
for path in sorted(missing):
|
|
if not os.path.isfile(path):
|
|
continue
|
|
with open(path, encoding='utf-8', errors='replace') as handle:
|
|
text = handle.read()
|
|
lines = text.split('\n')
|
|
regions = method_regions(lines)
|
|
|
|
# for each stale name, which regions hold the clean declaration
|
|
edits = collections.defaultdict(list) # old name -> list of (start, end)
|
|
for name in sorted(missing[path]):
|
|
pattern = re.compile(r'\b%s\b' % re.escape(name))
|
|
longer = re.compile(r'\b%s\d+\b' % re.escape(name))
|
|
shorter = name.rstrip('0123456789')
|
|
shorter_pattern = re.compile(r'\b%s\b' % re.escape(shorter)) if shorter != name else None
|
|
|
|
hits = 0
|
|
for start, end, _indent in regions:
|
|
chunk = '\n'.join(lines[start:end])
|
|
if not pattern.search(chunk):
|
|
continue
|
|
# Either direction may be the stale one: sometimes the use kept
|
|
# the short name while the declaration gained a suffix.
|
|
target = None
|
|
if shorter_pattern is not None and shorter_pattern.search(chunk):
|
|
target = shorter
|
|
else:
|
|
seen = {m.group(0) for m in longer.finditer(chunk)}
|
|
if len(seen) == 1:
|
|
target = seen.pop()
|
|
if target is None:
|
|
continue
|
|
edits[name].append((start, end, target))
|
|
hits += 1
|
|
|
|
if not hits:
|
|
skipped.append('%s: nothing in scope matches %s'
|
|
% (os.path.basename(path), name))
|
|
else:
|
|
renames += hits
|
|
|
|
if not edits:
|
|
continue
|
|
|
|
# rebuild from the last region so earlier edits keep their positions
|
|
replacements = []
|
|
for name, spans in edits.items():
|
|
for start, end, target in spans:
|
|
chunk = '\n'.join(lines[start:end])
|
|
chunk = re.sub(r'\b%s\b' % re.escape(name), target, chunk)
|
|
replacements.append((start, end, chunk))
|
|
|
|
replacements.sort(key=lambda t: t[0], reverse=True)
|
|
for start, end, chunk in replacements:
|
|
lines[start:end] = chunk.split('\n')
|
|
|
|
files_changed += 1
|
|
if args.apply:
|
|
with open(path, 'w', encoding='utf-8', newline='\n') as handle:
|
|
handle.write('\n'.join(lines))
|
|
|
|
print('methods fixed: %d across %d files' % (renames, files_changed))
|
|
if skipped:
|
|
print('unresolved %d:' % len(skipped))
|
|
for line in sorted(set(skipped)):
|
|
print(' %s' % line)
|
|
if not args.apply:
|
|
print('dry run, pass --apply to write')
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main()) |