Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и загрузчик, который является его частью, а не чем-то рядом. Форк - 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,103 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user