The 2V0 stub modules now carry the 40250 module constants (copied from each module's init function by stub_constants.py) and return zero values shaped like each reference function's Py_BuildValue, so interfaceModule.MakeInterface runs through (uiSafebox no longer divides by a zero SAFEBOX_SLOT_Y_COUNT). UserInterface/StdAfx.h includes Locale.h before GameType.h as the 40250 PCH does; the reversed order dropped GameType.h's ENABLE_NEW_EQUIPMENT_SYSTEM branches. port.login_flow's fake server checks CG_ENTERGAME; port.login_live reaches the GameWindow on 192.168.21.203. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
217 lines
8.7 KiB
Python
217 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Write the module constants and return shapes of the 2V0 native stub modules from the 40250 sources.
|
|
|
|
stub_constants.py [--check]
|
|
|
|
The stub modules in extension/src/platform/ScriptLib/GameplayModules.cpp only export names. Their
|
|
constants are data the root scripts compute with (uiSafebox divides by safebox.SAFEBOX_SLOT_Y_COUNT),
|
|
so each stub module gets its 40250 init function's `PyModule_Add{Int,String}Constant` lines verbatim,
|
|
with the preprocessor lines around them and the source file's includes, in
|
|
extension/src/platform/ScriptLib/GameplayModuleConstants.cpp as `AddStubConstants_<module>(poModule)`.
|
|
A module that is ported for real drops out of the stub list and out of this file.
|
|
|
|
The stub functions themselves (`stub_<module>_<Name>` in GameplayModules.cpp) return a zero value of
|
|
the shape their 40250 function returns on its last `return`: the `Py_BuildValue` format with every
|
|
number 0, every string "" and every object None, or None for `Py_BuildNone()`. uiTaskBar unpacks
|
|
`player.GetLocalQuickSlot` as a pair, so an `int` 0 stops the script where the real module would not.
|
|
Functions whose last return is not a build call (lists built by hand, ...) keep returning 0.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
import refroot
|
|
|
|
REPO = refroot.repo_root()
|
|
STUBS = REPO / "extension/src/platform/ScriptLib/GameplayModules.cpp"
|
|
OUT = REPO / "extension/src/platform/ScriptLib/GameplayModuleConstants.cpp"
|
|
INIT = re.compile(r'Py_InitModule\("([A-Za-z_]+)"')
|
|
KEEP = re.compile(r'^\s*(PyModule_Add(Int|String)Constant\s*\(|#\s*(if|ifdef|ifndef|elif|else|endif)\b)')
|
|
INCLUDE = re.compile(r'^\s*#\s*include\s*"([^"]+)"')
|
|
|
|
|
|
def read(path) -> list[str]:
|
|
return path.read_bytes().decode("cp949", "replace").replace("\r", "").split("\n")
|
|
|
|
|
|
def init_body(lines: list[str], module: str) -> list[str]:
|
|
start = next(i for i, l in enumerate(lines) if f'Py_InitModule("{module}"' in l and not l.strip().startswith("//"))
|
|
body = []
|
|
for line in lines[start + 1:]:
|
|
if line.startswith("}"):
|
|
break
|
|
if KEEP.match(line):
|
|
body.append(line.strip() if line.lstrip().startswith("#") else "\t" + line.strip())
|
|
# Preprocessor pairs with nothing between them carry no constant.
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for i in range(len(body) - 1):
|
|
if re.match(r"#\s*if", body[i]) and re.match(r"#\s*endif", body[i + 1]):
|
|
del body[i:i + 2]
|
|
changed = True
|
|
break
|
|
return body
|
|
|
|
|
|
def enums(lines: list[str]) -> list[str]:
|
|
"""The file's constant definitions and enum blocks (file scope or inside the init function) that constants name."""
|
|
out, inside = [], False
|
|
for line in lines:
|
|
if re.match(r"extern const \w+\s+\w+\s*=", line):
|
|
out.append(line)
|
|
continue
|
|
if re.match(r"\s*enum\b", line):
|
|
inside = True
|
|
if inside:
|
|
out.append(line[1:] if line.startswith("\t") and out and out[0].startswith("\t") else line)
|
|
if "};" in line:
|
|
inside = False
|
|
out.append("")
|
|
return out
|
|
|
|
|
|
ZERO = {"i": "0", "b": "0", "h": "0", "B": "0", "H": "0", "I": "0u", "c": "0", "l": "0L", "k": "0UL",
|
|
"L": "0LL", "K": "0ULL", "n": "(Py_ssize_t) 0", "f": "0.0", "d": "0.0", "s": '""', "z": '""',
|
|
"O": "Py_None", "S": "Py_None", "N": "Py_None"}
|
|
|
|
|
|
def zero_value(fmt: str) -> str | None:
|
|
"""A C expression building `fmt` with zeros, or None when the format has a converter."""
|
|
out_fmt, args, i = "", [], 0
|
|
while i < len(fmt):
|
|
ch = fmt[i]
|
|
if ch in "()[]{}:, \t":
|
|
out_fmt += ch
|
|
elif ch in ZERO:
|
|
out_fmt += "O" if ch in "SN" else ch
|
|
args.append(ZERO[ch])
|
|
if i + 1 < len(fmt) and fmt[i + 1] == "#":
|
|
out_fmt += "#"
|
|
args.append("0")
|
|
i += 1
|
|
elif ch == "O" and i + 1 < len(fmt) and fmt[i + 1] in "&!":
|
|
return None
|
|
else:
|
|
return None
|
|
i += 1
|
|
return "Py_BuildValue(" + ", ".join([f'"{out_fmt}"'] + args) + ")"
|
|
|
|
|
|
def function_bodies(ref) -> dict[str, str]:
|
|
bodies = {}
|
|
for path in ref.glob("*.cpp"):
|
|
text = "\n".join(read(path))
|
|
for m in re.finditer(r"PyObject\s*\*\s*(\w+)\s*\(\s*PyObject\s*\*\s*\w*\s*,\s*PyObject\s*\*\s*\w*\s*\)\s*\{", text):
|
|
depth, j = 1, m.end()
|
|
while depth and j < len(text):
|
|
depth += {"{": 1, "}": -1}.get(text[j], 0)
|
|
j += 1
|
|
bodies.setdefault(m.group(1), text[m.end():j - 1])
|
|
return bodies
|
|
|
|
|
|
def return_shape(body: str) -> str | None:
|
|
body = re.sub(r"//[^\n]*|/\*.*?\*/", "", body, flags=re.S)
|
|
returns = [r.strip() for r in re.findall(r"\breturn\b([^;]*);", body)]
|
|
returns = [r for r in returns if not r.startswith("Py_BuildException")]
|
|
if not returns:
|
|
return None
|
|
last = returns[-1]
|
|
if last.startswith("Py_BuildNone"):
|
|
return "Py_BuildNone()"
|
|
m = re.match(r'Py_BuildValue\s*\(\s*"([^"]*)"', last)
|
|
return zero_value(m.group(1)) if m else None
|
|
|
|
|
|
def shapes() -> tuple[str, int]:
|
|
"""GameplayModules.cpp with every stub returning its 40250 shape; also the count left at 0."""
|
|
ref = refroot.reference_root() / "UserInterface"
|
|
bodies = function_bodies(ref)
|
|
tables = {}
|
|
for path in ref.glob("*.cpp"):
|
|
text = "\n".join(read(path))
|
|
m = re.search(r'Py_InitModule\("(\w+)"', text)
|
|
if not m:
|
|
continue
|
|
for name, func in re.findall(r'\{\s*"(\w+)"\s*,\s*(\w+)\s*,\s*METH_VARARGS', text):
|
|
tables.setdefault(m.group(1), {}).setdefault(name, func)
|
|
left = 0
|
|
|
|
def fix(m):
|
|
nonlocal left
|
|
module, name = m.group(2).split(".", 1)
|
|
shape = return_shape(bodies.get(tables.get(module, {}).get(name, ""), ""))
|
|
if not shape:
|
|
left += 1
|
|
return f'{m.group(1)}"{m.group(2)}"); }}'
|
|
return f'{m.group(1)}"{m.group(2)}", {shape}); }}'
|
|
|
|
text = re.sub(r'(\{ return stub_call_named\()"([\w.]+)"(?:, .*)?\); \}$', fix, STUBS.read_text(), flags=re.M)
|
|
return text, left
|
|
|
|
|
|
def generate() -> str:
|
|
ref = refroot.reference_root() / "UserInterface"
|
|
modules = INIT.findall(STUBS.read_text())
|
|
sources = {}
|
|
for path in sorted(ref.glob("*.cpp")):
|
|
for line in read(path):
|
|
m = INIT.search(line)
|
|
if m and not line.strip().startswith("//"):
|
|
sources[m.group(1)] = path
|
|
out = [
|
|
"// Generated by .agents/skills/metin2-40250-parity-audit/scripts/stub_constants.py; do not edit.",
|
|
"// The module constants of the 2V0 stub modules (GameplayModules.cpp), copied from each 40250",
|
|
"// module's init function with the preprocessor lines around them.",
|
|
'#include "UserInterface/StdAfx.h"',
|
|
]
|
|
root = REPO / "extension/src/port"
|
|
port = {p.relative_to(root).as_posix().lower(): p.relative_to(root).as_posix() for p in root.rglob("*.h")}
|
|
includes, bodies = [], []
|
|
for module in modules:
|
|
path = sources[module]
|
|
body = init_body(read(path), module)
|
|
for line in read(path):
|
|
m = INCLUDE.match(line)
|
|
if m and m.group(1).lower() != "stdafx.h":
|
|
rel = m.group(1)[3:] if m.group(1).startswith("../") else "UserInterface/" + m.group(1)
|
|
if rel.lower() not in port:
|
|
continue # not mirrored (e.g. the UDP datagram unit); its constants need none of it
|
|
inc = f'#include "{port[rel.lower()]}"'
|
|
if inc not in includes:
|
|
includes.append(inc)
|
|
blocks = enums(read(path))
|
|
if blocks:
|
|
bodies.append(f"\n// 40250 UserInterface/{path.name}")
|
|
bodies.extend(l for l in blocks[:-1])
|
|
bodies.append(f"\n// 40250 UserInterface/{path.name}\nvoid AddStubConstants_{module}(PyObject* poModule)\n{{")
|
|
bodies.append("\t(void) poModule;")
|
|
bodies.extend(body)
|
|
bodies.append("}")
|
|
return "\n".join(out + includes + bodies) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--check", action="store_true", help="fail if the file is out of date")
|
|
args = ap.parse_args()
|
|
text = generate()
|
|
stubs, left = shapes()
|
|
if args.check:
|
|
stale = [p for p, t in ((OUT, text), (STUBS, stubs)) if not p.exists() or p.read_text() != t]
|
|
for p in stale:
|
|
print(f"{p.relative_to(REPO)} is out of date; run stub_constants.py", file=sys.stderr)
|
|
return 1 if stale else 0
|
|
OUT.write_text(text)
|
|
STUBS.write_text(stubs)
|
|
print(f"wrote {OUT.relative_to(REPO)} and {STUBS.relative_to(REPO)} ({left} stubs keep returning 0)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|