feat: LAN 최대 인원 8 → 30 (단일 jar, 확장 불가 버전은 8 유지)
Some checks failed
build / build (push) Has been cancelled
Some checks failed
build / build (push) Has been cancelled
바닐라 "LAN 에 공개" 의 상수 8 을 믹스인 1개로 30 으로 바꾼다. 대상과 메서드를 문자열로만 적고 핸들러를 int -> int 로 만들어 마인크래프트 타입 참조 0 을 유지한다. 8 이 있는 자리 (바닐라 jar 바이트코드 실측, 버전마다 정확히 한 곳): - 1.17 ~ 1.21.8 class_1130.<init> (super 인자 → PlayerList.maxPlayers) - 1.21.9 ~ 1.21.11 class_1132.method_3802 (return 8) - 26.1 ~ 26.3 IntegratedServer.getMaxPlayers (공식 이름) @Pseudo + require=0 이라 자리를 못 찾는 버전은 크래시 없이 바닐라 8 로 돈다. 적용 여부는 웹 콘솔 "최대 인원" 과 로그로 보인다. tools/lan-limit-probe: 바닐라 jar + Fabric Loader 로 모드를 실제 로드해 확인하는 도구. 1.18.2 ~ 26.3 (11개 버전, 로더 0.19.5 / 0.14.25) 전부 30, 믹스인을 뺀 jar 는 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
105
tools/lan-limit-probe/probe.py
Executable file
105
tools/lan-limit-probe/probe.py
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user