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 для запуска клиента
103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Show compile errors next to the source line that caused them.
|
|
|
|
javac prints file:line: error: message, which is not enough to work from: the
|
|
interesting part is usually the statement, and hunting for it in a 3000 line
|
|
decompiled file by hand is what turns a 160 error job into an afternoon.
|
|
|
|
python show_errors.py <errors.txt> [<errors.txt> ...] [--top N] [--file NAME]
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import os
|
|
import re
|
|
|
|
LINE = re.compile(r'^(?P<file>[A-Za-z]:.*?\.java):(?P<line>\d+): error: (?P<msg>.*)$')
|
|
DETAIL = re.compile(r'^ {2}(?P<key>symbol|location|where|required|found|reason):\s*(?P<rest>.*)$')
|
|
|
|
|
|
def load(paths):
|
|
"""Returns [(file, line, message, [detail lines])].
|
|
|
|
javac puts the useful part after the message: which symbol was missing, and
|
|
which types were expected. Those continuation lines are indented and carry
|
|
no filename, so they are attached to the error above them.
|
|
"""
|
|
found = []
|
|
for path in paths:
|
|
pending = None
|
|
with open(path, encoding='utf-8', errors='replace') as handle:
|
|
for raw in handle:
|
|
line = raw.rstrip('\n').rstrip('\r')
|
|
m = LINE.match(line)
|
|
if m:
|
|
if pending:
|
|
found.append(pending)
|
|
pending = (m.group('file'), int(m.group('line')),
|
|
m.group('msg'), [])
|
|
continue
|
|
if pending is not None:
|
|
# The quoted source line and its caret sit between the error
|
|
# and the symbol block, so anything that is not a detail line
|
|
# is skipped rather than treated as the end of the error.
|
|
d = DETAIL.match(line)
|
|
if d:
|
|
text = d.group('rest').strip()
|
|
pending[3].append('%s: %s' % (d.group('key'), text)
|
|
if text else d.group('key'))
|
|
if pending:
|
|
found.append(pending)
|
|
return found
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('errors', nargs='+')
|
|
parser.add_argument('--top', type=int, default=0, help='only the N worst files')
|
|
parser.add_argument('--file', help='show only errors in this file (substring match)')
|
|
parser.add_argument('--context', type=int, default=0, help='lines around the error')
|
|
args = parser.parse_args()
|
|
|
|
found = load(args.errors)
|
|
if not found:
|
|
print('no errors')
|
|
return 0
|
|
|
|
counts = collections.Counter(f for f, _, _, _ in found)
|
|
if args.file:
|
|
targets = [f for f in counts if args.file in f]
|
|
elif args.top:
|
|
targets = [f for f, _ in counts.most_common(args.top)]
|
|
else:
|
|
targets = sorted(counts)
|
|
|
|
shown = 0
|
|
for path in targets:
|
|
rows = [(ln, msg, det) for f, ln, msg, det in found if f == path]
|
|
print('%s (%d errors)' % (path, len(rows)))
|
|
cache = {}
|
|
for ln, msg, details in rows:
|
|
if path not in cache:
|
|
try:
|
|
with open(path, encoding='utf-8', errors='replace') as handle:
|
|
cache[path] = handle.readlines()
|
|
except OSError:
|
|
cache[path] = []
|
|
body = cache[path]
|
|
print(' %d: %s' % (ln, msg))
|
|
for extra in details:
|
|
print(' %s' % extra)
|
|
lo = max(0, ln - 1 - args.context)
|
|
hi = min(len(body), ln + args.context)
|
|
for i in range(lo, hi):
|
|
mark = '>' if i == ln - 1 else ' '
|
|
print(' %s %5d | %s' % (mark, i + 1, body[i].rstrip()))
|
|
shown += 1
|
|
print()
|
|
print('---- %d error(s) across %d file(s)' % (len(found), len(counts)))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main()) |