port 2A step 2: copy the 40250 header closure into the port mirror

76 closure headers (EterBase, EterLocale, EterPack, EterLib, GameLib, MilesLib,
EffectLib, SphereLib, EterImageLib) copied by the new port_copy.py (CP949->UTF-8,
LF, include-path case only); platform-class headers go to the mirror path too, as
the interface platform/ implements. Six manual `// PORT:` edits (Random, Singleton,
Pool, Stl, Utils, FlyTarget).

- common/D3D8Types.h: D3D8 value types with SDK values/layout, opaque COM interfaces
- common/shim/sdk (d3d8/d3dx8/mss) on every platform, common/shim/win32 off Windows
- Win32Types/Win32Crt: HRESULT, GUID, LARGE_INTEGER, LOGFONT/TEXTMETRIC, _atoi64,
  CRITICAL_SECTION on std::recursive_mutex
- header gate prepends each library's StdAfx.h (40250's precompiled header)

port_gate: macOS, Android, iOS, Windows (mingw) PASS; Linux BLOCKED (no toolchain).
ScriptLib/EterPythonLib/UserInterface wait for 2P (their StdAfx includes Python).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-22 20:27:57 +09:00
co-authored by Claude Opus 5
parent 0f2eee0b24
commit fc47582017
98 changed files with 7645 additions and 41 deletions
@@ -148,6 +148,9 @@ behavior verified from code inspection alone.
— function inventory (active `.vcxproj` sources + the 40250 Python root), progress per library and
per layer, and tag/port-map/layout consistency. Run `check` before every commit that touches
`audit/port-map/`.
- `scripts/port_copy.py copy <Lib/File>... | diff [<Lib/File>...]` — copy 40250 files into the
`extension/src/port/` mirror with mechanical edits only (CP949→UTF-8, LF, include-path case); `diff`
lists the manual `// PORT:` edits.
- `scripts/port_deps.py closure <file> | slices [--write] | order [--write]` — `#include` graph with the
implicit edges each library's `StdAfx.h` supplies; writes the 2V0-2V3 unit lists and the batch-2
topological order to `audit/slices/`. Regenerate after changing a slice definition.
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Copy 40250 sources into the port mirror (extension/src/port/<Lib>/<File>) with mechanical edits only.
port_copy.py copy <rel> [<rel>...] [--force] write the mechanical copy (refuses to overwrite without --force)
port_copy.py diff [<rel>...] show the manual edits: port file vs its mechanical copy
(no args: every mirrored 40250 file that differs)
`<rel>` is a reference path such as `GameLib/ActorInstance.h`, matched case-insensitively.
Mechanical edits, and nothing else:
- CP949 is decoded and written as UTF-8 (comments are Korean). A non-ASCII string or character
literal would change bytes at runtime, so such a file is refused and must be handled by hand.
- CRLF becomes LF.
- `#include "..."` paths get the on-disk case of the file they name (40250 builds on a
case-insensitive file system and writes `../eterLib/StdAfx.h`, `Parser.h` for `parser.h`, ...),
keeping the relative form, so the original include resolves the same way on every platform.
Everything else in a port file that differs from this copy is a manual edit, marked `// PORT:`.
"""
from __future__ import annotations
import argparse
import difflib
import re
import sys
from pathlib import Path
import refroot # script directory is on sys.path when run directly
REPO = refroot.repo_root()
PORT = REPO / "extension" / "src" / "port"
INCLUDE = re.compile(r'^(\s*#\s*include\s*")([^"]+)(")', re.M)
LITERAL = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'')
COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
def ref_index(ref: Path) -> dict[str, Path]:
return {p.relative_to(ref).as_posix().lower(): p for p in ref.rglob("*") if p.is_file()}
def canonical(ref: Path, index: dict[str, Path], path: Path) -> str:
return index[path.relative_to(ref).as_posix().lower()].relative_to(ref).as_posix()
def resolve(ref: Path, index: dict[str, Path], rel: str) -> Path:
hit = index.get(rel.replace("\\", "/").lower())
if not hit:
raise SystemExit(f"{rel}: not in the 40250 reference tree")
return hit
def fix_include(ref: Path, index: dict[str, Path], src: Path, target: str) -> str:
base = src.parent.relative_to(ref).as_posix()
parts = [] if base == "." else base.split("/")
for piece in target.replace("\\", "/").split("/"):
if piece == "..":
if not parts:
return target
parts.pop()
elif piece != ".":
parts.append(piece)
hit = index.get("/".join(parts).lower())
if not hit:
return target # unresolved (commented-out or absent in 40250): leave untouched
real = hit.relative_to(ref).as_posix().split("/")
# Keep the original shape: same number of `..` and segments, only the case changes.
kept = [p for p in target.replace("\\", "/").split("/") if p in ("..", ".")]
tail = real[len(real) - (len(target.replace("\\", "/").split("/")) - len(kept)):]
return "/".join(kept + tail)
def mechanical(ref: Path, index: dict[str, Path], src: Path) -> str:
raw = src.read_bytes()
try:
text = raw.decode("cp949")
except UnicodeDecodeError as e:
raise SystemExit(f"{src.relative_to(ref)}: not CP949 ({e})")
text = text.replace("\r\n", "\n").lstrip("")
code = COMMENT.sub(lambda m: " " * len(m.group(0)), text)
bad = [m.group(0) for m in LITERAL.finditer(code) if not m.group(0).isascii()]
if bad:
raise SystemExit(f"{src.relative_to(ref)}: non-ASCII literal(s), copy by hand: {bad[:3]}")
return INCLUDE.sub(lambda m: m.group(1) + fix_include(ref, index, src, m.group(2)) + m.group(3), text)
def cmd_copy(args) -> int:
ref = refroot.reference_root()
index = ref_index(ref)
for rel in args.rel:
src = resolve(ref, index, rel)
dst = PORT / canonical(ref, index, src)
if dst.exists() and not args.force:
print(f"skip {dst.relative_to(REPO)} (exists; --force overwrites manual edits)")
continue
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_text(mechanical(ref, index, src), encoding="utf-8")
print(f"copy {dst.relative_to(REPO)}")
return 0
def cmd_diff(args) -> int:
ref = refroot.reference_root()
index = ref_index(ref)
if args.rel:
pairs = [(resolve(ref, index, r), PORT / canonical(ref, index, resolve(ref, index, r))) for r in args.rel]
else:
pairs = []
for dst in sorted(PORT.rglob("*")):
key = dst.relative_to(PORT).as_posix().lower()
if dst.is_file() and key in index and not key.startswith("common/"):
pairs.append((index[key], dst))
changed = 0
for src, dst in pairs:
if not dst.exists():
print(f"missing {dst.relative_to(REPO)}")
continue
want = mechanical(ref, index, src).splitlines(keepends=True)
have = dst.read_text(encoding="utf-8").splitlines(keepends=True)
diff = list(difflib.unified_diff(want, have, f"40250/{src.relative_to(ref).as_posix()}",
dst.relative_to(REPO).as_posix()))
if diff:
changed += 1
sys.stdout.writelines(diff)
print(f"{changed} of {len(pairs)} file(s) with manual edits", file=sys.stderr)
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("copy")
c.add_argument("rel", nargs="+")
c.add_argument("--force", action="store_true")
d = sub.add_parser("diff")
d.add_argument("rel", nargs="*")
args = ap.parse_args()
return {"copy": cmd_copy, "diff": cmd_diff}[args.cmd](args)
if __name__ == "__main__":
sys.exit(main())
@@ -49,7 +49,7 @@ LAYER_PATTERNS = [
"EterLib/LensFlare.cpp", "EterLib/ScreenFilter.cpp", "EterLib/Decal.cpp", "EterLib/EnvironmentMap.cpp",
"EterLib/JpegFile.cpp", "EterLib/TargaResource.cpp", "EterLib/DibBar.cpp", "EterLib/BlockTexture.cpp",
"EterLib/CullingManager.cpp", "EterLib/Thread.cpp", "EterLib/Mutex.cpp", "EterLib/FileLoaderThread.cpp",
"EterLib/TextBar.cpp", "EterBase/*", "GameLib/MapOutdoorRender*.cpp", "GameLib/MapOutdoorWater.cpp",
"EterLib/TextBar.cpp", "EterLib/Dimm.h", "EterBase/*", "GameLib/MapOutdoorRender*.cpp", "GameLib/MapOutdoorWater.cpp",
"GameLib/SnowParticle.cpp", "GameLib/SnowEnvironment.cpp", "GameLib/TerrainDecal.cpp",
"GameLib/TerrainPatch.cpp", "UserInterface/PythonApplicationLogo.cpp", "UserInterface/MovieMan.cpp",
"UserInterface/HackShield.cpp", "UserInterface/NProtectGameGuard.cpp", "UserInterface/ProcessCRC.cpp",