Files
EJClaw 16a44ec4bc
Some checks failed
build / build (push) Has been cancelled
feat: LAN 최대 인원 8 → 30 (단일 jar, 확장 불가 버전은 8 유지)
바닐라 "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>
2026-10-05 04:07:06 +09:00

106 lines
4.8 KiB
Python
Executable File

#!/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()