Files
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

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())