#!/usr/bin/env python3 """LAN 최대 인원 믹스인을 실제 Fabric Loader + 바닐라 클라이언트 jar 위에서 확인한다. python3 tools/lan-limit-probe/probe.py [--loader 0.19.5] [--java java] 1.21.11 26.3 ... 바닐라 jar 와 라이브러리를 PROBE_HOME(기본 ~/.cache/mcp2p-lan-limit-probe)에 내려받는다. 버전당 수십~백 MB 다. 모드 jar 는 build/libs 에서 가져오므로 먼저 ./gradlew build 를 돌린다. 게임 창은 뜨지 않는다 — preLaunch 에서 확인하고 바로 끝난다. 1.20.1 이하를 구버전 로더(0.14.x)로 돌릴 때는 --java 로 JDK 17 을 지정한다 (구버전 로더의 ASM 이 최신 JDK 클래스 파일을 못 읽는다). """ import argparse, glob, json, os, re, shutil, subprocess, sys, urllib.request HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(os.path.dirname(HERE)) HOME = os.environ.get('PROBE_HOME', os.path.expanduser('~/.cache/mcp2p-lan-limit-probe')) def fetch(url, path): if not os.path.exists(path): os.makedirs(os.path.dirname(path), exist_ok=True) urllib.request.urlretrieve(url, path + '.part') os.replace(path + '.part', path) return path def classpath(mc, loader): manifest = json.load(urllib.request.urlopen('https://piston-meta.mojang.com/mc/game/version_manifest_v2.json')) meta = json.load(urllib.request.urlopen(next(v['url'] for v in manifest['versions'] if v['id'] == mc))) cp, seen = [], set() profile = json.load(urllib.request.urlopen( f'https://meta.fabricmc.net/v2/versions/loader/{mc}/{loader}/profile/json')) for lib in profile['libraries']: g, a, v = lib['name'].split(':') seen.add((g, a)) rel = f"{g.replace('.', '/')}/{a}/{v}/{a}-{v}.jar" cp.append(fetch(lib['url'].rstrip('/') + '/' + rel, f'{HOME}/libs/{rel}')) for lib in meta['libraries']: art = lib.get('downloads', {}).get('artifact') # 로더가 이미 주는 것(ASM 등)은 빼야 한다. 중복이면 Knot 이 시작을 거부한다. if not art or 'natives' in art['path'] or tuple(lib['name'].split(':')[:2]) in seen: continue cp.append(fetch(art['url'], f"{HOME}/libs/{art['path']}")) cp.append(fetch(meta['downloads']['client']['url'], f'{HOME}/{mc}/client.jar')) return cp def build_probe(loader_jar, javac): out = f'{HOME}/probe-build' shutil.rmtree(out, ignore_errors=True) os.makedirs(out) subprocess.run([javac, '--release', '17', '-nowarn', '-cp', loader_jar, '-d', out, f'{HERE}/src/probe/Probe.java'], check=True) with open(f'{out}/fabric.mod.json', 'w') as f: json.dump({'schemaVersion': 1, 'id': 'mcp2p_probe', 'version': '1', 'entrypoints': {'preLaunch': ['probe.Probe']}}, f) jar = f'{HOME}/probe.jar' subprocess.run(['jar', 'cf', jar, '-C', out, '.'], check=True) return jar def main(): ap = argparse.ArgumentParser() ap.add_argument('versions', nargs='+') ap.add_argument('--loader', default='0.19.5') ap.add_argument('--java', default='java') ap.add_argument('--mod', help='검사할 모드 jar (기본: build/libs 의 산출물)') args = ap.parse_args() mods = [args.mod] if args.mod else \ [p for p in glob.glob(f'{REPO}/build/libs/mc_p2p_mod-*.jar') if 'rendezvous' not in p] if not mods: sys.exit('build/libs 에 모드 jar 가 없다 — ./gradlew build 먼저') javac = os.path.join(os.path.dirname(shutil.which(args.java) or args.java), 'javac') failed = False for mc in args.versions: cp = classpath(mc, args.loader) probe = build_probe(next(p for p in cp if '/fabric-loader/' in p), javac) game = f'{HOME}/run/{mc}-{args.loader}' shutil.rmtree(f'{game}/mods', ignore_errors=True) os.makedirs(f'{game}/mods') shutil.copy(mods[0], f'{game}/mods/') shutil.copy(probe, f'{game}/mods/') log = subprocess.run( [args.java, '-Xmx2g', '-cp', os.pathsep.join(cp), 'net.fabricmc.loader.impl.launch.knot.KnotClient', '--gameDir', game, '--version', mc, '--accessToken', '0'], cwd=game, capture_output=True, text=True, timeout=600).stdout with open(f'{game}/log.txt', 'w') as f: f.write(log) lines = [re.sub(r'^.*?PROBE ', '', l) for l in log.splitlines() if 'PROBE ' in l] values = [int(m.group(1)) for l in lines for m in [re.search(r'(?:getMaxPlayers\(\)|maxPlayers) = (\d+)', l)] if m] ok = 'raised=true' in lines and all(v == 30 for v in values) failed |= not ok print(f"{'PASS' if ok else 'FAIL'} {mc} (loader {args.loader})") for l in lines: print(' ' + l) if not lines: print(f' PROBE 출력 없음 — {game}/log.txt 확인') sys.exit(1 if failed else 0) if __name__ == '__main__': main()