Files
RustNotMC/rsx-client/tools/merge_srg_mcp.py
T
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

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