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 для запуска клиента
139 lines
5.3 KiB
Python
139 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Merge a vanilla SRG with the MCP CSVs into a single obf -> MCP SRG.
|
|
|
|
Why this exists
|
|
---------------
|
|
The vanilla mapping for 1.12.2 arrives as `joined.srg`, whose right hand side is
|
|
in SRG names: classes already carry their MCP names, but fields and methods are
|
|
still `field_96303_A` / `func_175744_a`. The MCP CSVs are the table that turns
|
|
those into `fancyStyling` / `isFancyStyling`.
|
|
|
|
SpecialSource can then apply the merged file in one pass, which avoids two
|
|
things that a tiny v2 pipeline would require: reading every class file to learn
|
|
member descriptors, and chaining two remappers.
|
|
|
|
python merge_srg_mcp.py joined.srg mcp-stable out.srg
|
|
|
|
Any member missing from the CSVs keeps its SRG name. That is deliberate and
|
|
matches how a normal development jar looks: mapped where MCP knows the name,
|
|
`field_XXXX_` where it does not.
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import sys
|
|
|
|
|
|
def read_csv(path):
|
|
"""searge name -> MCP name."""
|
|
table = {}
|
|
try:
|
|
with open(path, newline='', encoding='utf-8') as handle:
|
|
reader = csv.reader(handle)
|
|
header = next(reader, None)
|
|
if header is None:
|
|
return table
|
|
# the first column is the SRG name, the second the MCP name
|
|
for row in reader:
|
|
if len(row) < 2:
|
|
continue
|
|
searge = row[0].strip()
|
|
named = row[1].strip()
|
|
if searge and named:
|
|
table[searge] = named
|
|
except OSError as exc:
|
|
print('cannot read %s: %s' % (path, exc), file=sys.stderr)
|
|
return table
|
|
|
|
|
|
def merge_member_name(token, table):
|
|
"""`owner/srgName` -> `owner/mcpName`, keeping the owner untouched."""
|
|
if '/' not in token:
|
|
return token
|
|
owner, _, name = token.rpartition('/')
|
|
return owner + '/' + table.get(name, name)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('srg', help='vanilla joined.srg')
|
|
parser.add_argument('mcp_dir', help='directory holding fields.csv and methods.csv')
|
|
parser.add_argument('out', help='merged SRG to write')
|
|
args = parser.parse_args()
|
|
|
|
fields = read_csv(args.mcp_dir + '/fields.csv')
|
|
methods = read_csv(args.mcp_dir + '/methods.csv')
|
|
print('csv: %d fields, %d methods' % (len(fields), len(methods)))
|
|
|
|
stats = {'CL': 0, 'FD': 0, 'MD': 0, 'PK': 0, 'renamed_f': 0, 'renamed_m': 0}
|
|
skipped = 0
|
|
|
|
with open(args.srg, 'r', encoding='utf-8', errors='replace') as src, \
|
|
open(args.out, 'w', encoding='utf-8', newline='\n') as dst:
|
|
|
|
for line in src:
|
|
line = line.rstrip('\r\n')
|
|
if not line.strip():
|
|
continue
|
|
|
|
if line.startswith('CL: '):
|
|
# class names in the SRG already are the MCP names
|
|
stats['CL'] += 1
|
|
dst.write(line + '\n')
|
|
continue
|
|
|
|
if line.startswith('FD: '):
|
|
parts = line.split(' ')
|
|
if len(parts) < 3:
|
|
skipped += 1
|
|
continue
|
|
before = parts[2].rpartition('/')[2]
|
|
after = merge_member_name(parts[2], fields)
|
|
if after.rpartition('/')[2] != before:
|
|
stats['renamed_f'] += 1
|
|
stats['FD'] += 1
|
|
dst.write('FD: %s %s\n' % (parts[1], after))
|
|
continue
|
|
|
|
if line.startswith('MD: '):
|
|
# MD: <obfOwner>/<obfMethod> <obfDesc> <srgOwner>/<srgMethod> <srgDesc>;
|
|
#
|
|
# The trailing semicolon belongs to the descriptor and is absent
|
|
# for primitive and void returns: ()I, ()Z, ()V carry none, while
|
|
# ()Ljava/lang/String; does. Appending one unconditionally turns
|
|
# ()I into the invalid ()I; and the remapper rejects it with
|
|
# "Unrecognized type in method descriptor: ;". Keep it verbatim.
|
|
trail = ';' if line.rstrip().endswith(';') else ''
|
|
parts = line.rstrip().rstrip(';').split(' ')
|
|
if len(parts) < 5:
|
|
skipped += 1
|
|
continue
|
|
before = parts[3].rpartition('/')[2]
|
|
after = merge_member_name(parts[3], methods)
|
|
if after.rpartition('/')[2] != before:
|
|
stats['renamed_m'] += 1
|
|
stats['MD'] += 1
|
|
dst.write('MD: %s %s %s %s%s\n'
|
|
% (parts[1], parts[2], after, parts[4], trail))
|
|
continue
|
|
|
|
if line.startswith('PK: '):
|
|
# Package lines exist for MCP's source-jar layout: `PK: . net/minecraft/src`
|
|
# moves the whole root package under net/minecraft/src. The CL lines
|
|
# already place every class at its final path, so applying this on top
|
|
# would double-prefix them, and SpecialSource's parser rejects the
|
|
# combination. Not needed for remapping a client jar.
|
|
stats['PK'] += 1
|
|
continue
|
|
|
|
skipped += 1
|
|
|
|
print('wrote %s' % args.out)
|
|
print(' CL=%(CL)d FD=%(FD)d MD=%(MD)d PK=%(PK)d' % stats)
|
|
print(' renamed %d fields, %d methods' % (stats['renamed_f'], stats['renamed_m']))
|
|
if skipped:
|
|
print(' skipped %d unrecognised lines' % skipped)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |