fix: 装备属性面板避让逻辑 + 多项功能更新

- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
This commit is contained in:
shen
2026-09-21 16:38:59 -07:00
parent 1522a8a1a1
commit c93894313a
5498 changed files with 4558004 additions and 1442 deletions
@@ -15,6 +15,7 @@ The audit ledger is the source of truth:
- `audit/history.jsonl`: append-only state-change history.
- `audit/contracts/`: detailed evidence for individual contracts.
- `audit/reports/`: generated summaries; never treat these as the source of truth.
- `audit/remediation-roadmap.md`: current repair order, anti-repeat rules, and the next primary contract.
Before auditing, fixing, or claiming parity:
@@ -23,8 +24,32 @@ Before auditing, fixing, or claiming parity:
3. Read the relevant existing contract and tests. Do not repeat a still-valid `TEST_VERIFIED` audit unless the user explicitly requests revalidation.
4. If no contract exists, add one with a stable behavior ID before marking work complete.
Before selecting the next repair target, read `audit/remediation-roadmap.md` and the
latest relevant history entries. Select by the documented priority and end-to-end
user flow, not by whichever local difference is easiest to patch. A contract may not
be used to repeat an already completed issue or branch. Every round must select an
explicit issue/branch ID using the roadmap's stable format and compare it with
`audit/history.jsonl` before editing. Older contract-level history entries do not
prove that every branch in that contract is complete; create the branch-level ID
when first selecting it.
The same issue/branch ID may be revisited only for a new regression, a substantive
implementation change, or new evidence that invalidates the earlier conclusion.
The same contract may be selected in a later round when a different `remaining`
branch is selected; contract IDs group behavior and are not deduplication keys. A
`PARTIAL` contract is not permission to repeat already verified work.
Read [references/audit-schema.md](references/audit-schema.md) whenever creating or changing ledger entries. Read [references/project-map.md](references/project-map.md) when locating reference code, current implementation, existing gap documents, or test runners.
## Full-client inventory gate
This skill covers the complete reachable 40250 client behavior surface, not only the first few reported bugs. Before claiming the audit is broadly complete:
1. Inventory active 40250 runtime areas across `UserInterface`, `GameLib`, `EffectLib`, `EterLib`, `EterGrnLib`, `PRTerrainLib`, `MilesLib`, `EterPack`, and `EterLocale`, then map each externally meaningful behavior to `project/`, `extension/`, `formats/`, resources, and tests.
2. Keep one stable contract ID per externally meaningful behavior, not one per source file. Group helper files under the behavior they implement, but do not omit a reachable behavior because its implementation is spread across several libraries.
3. Put every mapped behavior in `audit/manifest.json`, including behaviors not yet checked. Use `UNMAPPED` when no current implementation or reference mapping is established, `MAPPED` when both entry points are located, and `PARTIAL` when a material gap is known.
4. Record current-client-only features as `EXCLUDED` only with an explicit reason that they have no 40250 counterpart and are outside the parity target. Never silently omit them from the inventory.
5. The inventory is incomplete while any active runtime area has no contract entry. A clean test suite or a high percentage of mapped files does not waive this gate.
## Audit method
For each behavior:
@@ -39,6 +64,13 @@ For each behavior:
8. Run the narrow test first, then related subsystem tests, then `git diff --check`.
9. Update the contract's implementation-equivalence matrix, manifest, fingerprints, and history in the same change. Never mark `STATIC_VERIFIED` or `TEST_VERIFIED` without complete equivalence evidence.
For classic network phase audits, run `scripts/audit_packet_registry.py` together
with `scripts/audit_phase_dispatch.py`. The former compares registered wire
header values and static/dynamic kinds; the latter compares Login/Select/Loading/
Game phase branches by numeric value while explicitly reporting transport-control
and observer-stream exclusions. A PASS from either script is structural evidence,
not proof that handler side effects are equivalent.
## Implementation-equivalence gate
Source text and engine-facing APIs do not need to be identical, but the implementation must be semantically unified with 40250. Before verification, prove all of these independently:
@@ -75,3 +107,9 @@ Permitted differences are limited to documented platform adapters such as C++ co
## Completion report
Report the contract IDs changed, reference and implementation call chains, discrepancies fixed, tests run, remaining unverified branches, and resulting ledger status. A subsystem is complete only when its in-scope contracts have no unexplained `UNMAPPED`, `PARTIAL`, `STALE`, or `REGRESSION` entries.
Also report whether the round followed `audit/remediation-roadmap.md`, the selected
issue/branch ID, why it was not already completed in `audit/history.jsonl`, and the
next priority candidate. Do not claim that a single subsystem is complete merely
because its focused tests pass while its ledger contract still has material
`remaining` branches.
@@ -177,6 +177,20 @@ def command_report(root: Path, manifest: dict, args: argparse.Namespace) -> int:
(item for item in contracts if item["status"] in PENDING),
key=lambda item: (item["priority"], item["status"], item["id"]),
)
pending_by_priority = Counter()
pending_items_total = 0
for item in pending:
remaining = item.get("remaining", [])
if not isinstance(remaining, list):
continue
count = len(remaining)
pending_items_total += count
pending_by_priority[item["priority"]] += count
lines.extend(["", "## Pending branch items", ""])
lines.append(f"- Total: {pending_items_total}")
for priority in sorted(PRIORITIES):
lines.append(f"- {priority}: {pending_by_priority[priority]}")
lines.append("- Routing: see `audit/remediation-roadmap.md`")
lines.extend(["", "## Pending", ""])
lines.extend(f"- [{item['priority']}] {item['id']}: {item['status']}" for item in pending)
if not pending:
@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Compare the 40250 GC header registry with the classic wire adapter.
This is intentionally a static audit, not a replacement for wire tests. It
keeps the reference-side registry count and dynamic/static classification
machine-checkable so a new packet alias or compatibility branch cannot be
silently treated as 40250 parity.
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
REFERENCE_DYNAMIC = {
"HEADER_GC_QUEST_INFO",
"HEADER_GC_DUEL_START",
"HEADER_GC_CHAT",
"HEADER_GC_SYNC_POSITION",
"HEADER_GC_SHOP",
"HEADER_GC_SCRIPT",
"HEADER_GC_MESSENGER",
"HEADER_GC_GUILD",
"HEADER_GC_DUNGEON",
"HEADER_GC_NPC_POSITION",
"HEADER_GC_LAND_LIST",
"HEADER_GC_HYBRIDCRYPT_KEYS",
"HEADER_GC_HYBRIDCRYPT_SDB",
}
# These are documented current-client adaptations. They remain differences:
# allowing them here makes the report useful without pretending they are in the
# reference CMainPacketHeaderMap.
KNOWN_CURRENT_DYNAMIC_ADAPTATIONS = {
"HDR_GC_WHISPER",
"HDR_GC_SYMBOL_DATA",
}
def active_lines(text: str, *, gaiden: bool = False) -> list[str]:
"""Keep the 40250 branch used by the checked-in Win32 project.
The snapshot contains both GAIDEN and non-GAIDEN enum aliases in Packet.h.
The UserInterface project does not define GAIDEN, so evaluating that one
branch is necessary before comparing numeric header identities. Other
optional blocks are intentionally retained: the registry source itself
uses the improved-encryption symbols supplied by ServiceDefs.h.
"""
out: list[str] = []
stack: list[bool] = []
enabled = True
for line in text.splitlines():
stripped = line.strip()
if stripped in {"#if defined(GAIDEN)", "#ifdef GAIDEN"}:
stack.append(enabled)
enabled = enabled and gaiden
continue
if stripped == "#ifndef GAIDEN":
stack.append(enabled)
enabled = enabled and not gaiden
continue
if stripped == "#else" and stack:
parent = stack[-1]
enabled = parent and not enabled
continue
if stripped.startswith("#endif") and stack:
enabled = stack.pop()
continue
if enabled and not stripped.startswith("//"):
out.append(line)
return out
def reference_registry(path: Path) -> list[dict[str, str]]:
row = re.compile(
r"\bSet\((HEADER_GC_[A-Z0-9_]+),\s*"
r"CNetworkPacketHeaderMap::TPacketType\(sizeof\(([^)]+)\),\s*"
r"(STATIC_SIZE_PACKET|DYNAMIC_SIZE_PACKET)\)"
)
out: list[dict[str, str]] = []
for line in active_lines(path.read_text(errors="replace")):
match = row.search(line)
if match:
out.append({"header": match.group(1), "struct": match.group(2), "kind": match.group(3)})
return out
def function_body(text: str, name: str, return_type: str) -> str:
match = re.search(
rf"constexpr\s+{re.escape(return_type)}\s+{re.escape(name)}\s*\([^)]*\)\s*\{{(?P<body>.*?)\n\}}",
text,
re.S,
)
if not match:
raise ValueError(f"cannot find {name} in wire header")
return match.group("body")
def current_wire(path: Path) -> dict[str, list[str]]:
text = path.read_text(errors="replace")
size_body = function_body(text, "packet_size_gc", "int")
dynamic_body = function_body(text, "is_dynamic_gc", "bool")
case_re = re.compile(r"\bcase\s+(HDR_[A-Z0-9_]+)\s*:")
return {
"size_cases": case_re.findall(size_body),
"dynamic_cases": case_re.findall(dynamic_body),
}
def header_values(path: Path, prefix: str) -> dict[str, int]:
values: dict[str, int] = {}
pattern = re.compile(rf"\b({re.escape(prefix)}[A-Z0-9_]+)\s*=\s*(0x[0-9A-Fa-f]+|\d+)")
for line in active_lines(path.read_text(errors="replace"), gaiden=False):
match = pattern.search(line)
if match:
values.setdefault(match.group(1), int(match.group(2), 0))
return values
def audit(reference_path: Path, wire_path: Path) -> dict:
ref = reference_registry(reference_path)
wire = current_wire(wire_path)
reference_values = header_values(reference_path.parent / "Packet.h", "HEADER_GC_")
current_values = header_values(wire_path, "HDR_")
ref_dynamic = {row["header"].replace("HEADER_", "HDR_") for row in ref if row["kind"] == "DYNAMIC_SIZE_PACKET"}
current_dynamic = set(wire["dynamic_cases"])
extras = sorted(current_dynamic - ref_dynamic)
missing = sorted(ref_dynamic - current_dynamic)
unexpected_extras = sorted(set(extras) - KNOWN_CURRENT_DYNAMIC_ADAPTATIONS)
reference_by_value: dict[int, list[dict[str, str]]] = {}
for row in ref:
value = reference_values.get(row["header"])
if value is not None:
reference_by_value.setdefault(value, []).append(row)
current_registered_names = set(wire["size_cases"]) | current_dynamic
current_by_value: dict[int, list[str]] = {}
for name in current_registered_names:
value = current_values.get(name)
if value is not None:
current_by_value.setdefault(value, []).append(name)
reference_values_set = set(reference_by_value)
current_values_set = set(current_by_value)
numeric_missing = sorted(reference_values_set - current_values_set)
numeric_extras = sorted(current_values_set - reference_values_set)
kind_mismatches: list[dict[str, object]] = []
for value, rows in sorted(reference_by_value.items()):
expected = {row["kind"] for row in rows}
actual = {
"DYNAMIC_SIZE_PACKET" if name in current_dynamic else "STATIC_SIZE_PACKET"
for name in current_by_value.get(value, [])
}
if actual and not actual.intersection(expected):
kind_mismatches.append({"value": value, "reference": rows, "current": current_by_value[value]})
result = {
"reference": {
"registered": len(ref),
"static": sum(row["kind"] == "STATIC_SIZE_PACKET" for row in ref),
"dynamic": sum(row["kind"] == "DYNAMIC_SIZE_PACKET" for row in ref),
"dynamic_headers": sorted(ref_dynamic),
"numeric_values": len(reference_values_set),
},
"current": {
"packet_size_gc_cases": len(set(wire["size_cases"])),
"is_dynamic_gc_cases": len(current_dynamic),
"dynamic_headers": sorted(current_dynamic),
"numeric_values": len(current_values_set),
},
"difference": {
"missing_reference_dynamic": missing,
"current_dynamic_extras": extras,
"unapproved_current_dynamic_extras": unexpected_extras,
"known_adaptations": sorted(set(extras) & KNOWN_CURRENT_DYNAMIC_ADAPTATIONS),
"missing_reference_numeric_values": numeric_missing,
"current_extra_numeric_values": numeric_extras,
"kind_mismatches": kind_mismatches,
},
"status": "PASS"
if not missing and not extras and not numeric_missing and not numeric_extras and not kind_mismatches
else "PARTIAL",
}
return result
def main() -> int:
parser = argparse.ArgumentParser()
default_root = Path(__file__).resolve().parents[4]
parser.add_argument("--repo-root", type=Path, default=default_root)
parser.add_argument("--strict", action="store_true", help="return non-zero when the registry is not exact")
args = parser.parse_args()
root = args.repo_root.resolve()
reference = root.parent / "40250/Server Client TMP4/ClientVS22/source/UserInterface/PythonNetworkStream.cpp"
wire = root / "extension/src/net/classic/wire_classic.h"
try:
result = audit(reference, wire)
except (OSError, ValueError) as exc:
print(json.dumps({"status": "ERROR", "error": str(exc)}, indent=2, sort_keys=True))
return 2
print(json.dumps(result, indent=2, sort_keys=True))
return 1 if args.strict and result["status"] != "PASS" else 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Compare 40250 phase switch values with the classic session gates.
The Win32 source has three kinds of cases that must not be confused: phase
payload handlers, transport/control packets consumed by ClassicStream, and
observer-only cases that are not in the main packet registry. Compare numeric
wire values so enum aliases cannot hide a missing branch.
"""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
CONTROL_VALUES = {44, 152, 153, 250, 251, 252, 253, 254, 255}
OBSERVER_VALUES = {96, 97, 98}
def active_lines(text: str, *, gaiden: bool = False) -> list[str]:
out: list[str] = []
stack: list[bool] = []
enabled = True
for line in text.splitlines():
stripped = line.strip()
if stripped in {"#if defined(GAIDEN)", "#ifdef GAIDEN"}:
stack.append(enabled)
enabled = enabled and gaiden
continue
if stripped == "#ifndef GAIDEN":
stack.append(enabled)
enabled = enabled and not gaiden
continue
if stripped == "#else" and stack:
parent = stack[-1]
enabled = parent and not enabled
continue
if stripped.startswith("#endif") and stack:
enabled = stack.pop()
continue
if enabled and not stripped.startswith("//"):
out.append(line)
return out
def values(path: Path, prefix: str) -> dict[str, int]:
result: dict[str, int] = {}
pattern = re.compile(rf"\b({re.escape(prefix)}[A-Z0-9_]+)\s*=\s*(0x[0-9A-Fa-f]+|\d+)")
for line in active_lines(path.read_text(errors="replace")):
match = pattern.search(line)
if match:
result.setdefault(match.group(1), int(match.group(2), 0))
return result
def function_body(text: str, name: str) -> str:
match = re.search(rf"\bbool\s+{re.escape(name)}\s*\([^)]*\)\s*\{{", text)
if not match:
raise ValueError(f"cannot find {name}")
start = match.end()
depth = 1
index = start
while index < len(text) and depth:
if text[index] == "{":
depth += 1
elif text[index] == "}":
depth -= 1
index += 1
if depth:
raise ValueError(f"unterminated {name}")
return text[start:index - 1]
def case_values(body: str, prefix: str, table: dict[str, int]) -> set[int]:
result: set[int] = set()
for name in re.findall(rf"\b({re.escape(prefix)}[A-Z0-9_]+)\s*:", body):
if name in table:
result.add(table[name])
return result
def reference_phase_values(root: Path, filename: str) -> set[int]:
text = (root / "UserInterface" / filename).read_text(errors="replace")
enum_values = values(root / "UserInterface" / "Packet.h", "HEADER_GC_")
return case_values(text, "HEADER_GC_", enum_values)
def audit(repo_root: Path) -> dict:
reference_root = repo_root.parent / "40250/Server Client TMP4/ClientVS22/source"
wire = repo_root / "extension/src/net/classic/wire_classic.h"
session_path = repo_root / "extension/src/net/classic/classic_session.cpp"
reference_phases = {
"login": reference_phase_values(reference_root, "PythonNetworkStreamPhaseLogin.cpp"),
"select": reference_phase_values(reference_root, "PythonNetworkStreamPhaseSelect.cpp"),
"loading": reference_phase_values(reference_root, "PythonNetworkStreamPhaseLoading.cpp"),
"game": reference_phase_values(reference_root, "PythonNetworkStreamPhaseGame.cpp"),
}
current_values = values(wire, "HDR_GC_")
session_text = session_path.read_text(errors="replace")
current_phases = {
"login": case_values(function_body(session_text, "login_phase_packet"), "HDR_GC_", current_values),
"select": case_values(function_body(session_text, "select_phase_packet"), "HDR_GC_", current_values),
"loading": case_values(function_body(session_text, "loading_phase_packet"), "HDR_GC_", current_values),
"game": case_values(function_body(session_text, "game_phase_packet"), "HDR_GC_", current_values),
}
differences: dict[str, dict[str, list[int]]] = {}
for phase, reference in reference_phases.items():
ignored = CONTROL_VALUES | (OBSERVER_VALUES if phase == "game" else set())
expected = reference - ignored
actual = current_phases[phase] - ignored
differences[phase] = {
"reference_payload_not_current": sorted(expected - actual),
"current_not_reference_payload": sorted(actual - expected),
}
status = "PASS" if all(not values for row in differences.values() for values in row.values()) else "PARTIAL"
return {
"status": status,
"ignored_transport_control_values": sorted(CONTROL_VALUES),
"ignored_observer_values": sorted(OBSERVER_VALUES),
"reference_case_counts": {phase: len(items) for phase, items in reference_phases.items()},
"current_case_counts": {phase: len(items) for phase, items in current_phases.items()},
"differences": differences,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[4])
parser.add_argument("--strict", action="store_true")
args = parser.parse_args()
try:
result = audit(args.repo_root.resolve())
except (OSError, ValueError) as exc:
print(json.dumps({"status": "ERROR", "error": str(exc)}, indent=2, sort_keys=True))
return 2
print(json.dumps(result, indent=2, sort_keys=True))
return 1 if args.strict and result["status"] != "PASS" else 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Verify the classic sequence table against the 40250 Windows source.
The client-side table is protocol data, not a generated random table. Compare
the complete byte array and report the digest so a copied prefix or reordered
tail cannot be mistaken for parity.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from pathlib import Path
def read_array(path: Path, symbol: str) -> bytes:
text = path.read_text(errors="replace")
match = re.search(
rf"{re.escape(symbol)}\s*(?:\[[^\]]+\])?\s*=\s*\{{(?P<body>.*?)\}}\s*;",
text,
re.S,
)
if not match:
raise ValueError(f"cannot find {symbol} in {path}")
values = [int(token, 0) for token in re.findall(r"0x[0-9a-fA-F]+|\b\d+\b", match.group("body"))]
if any(value < 0 or value > 255 for value in values):
raise ValueError(f"out-of-range byte in {path}")
return bytes(values)
def digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def main() -> int:
parser = argparse.ArgumentParser()
default_root = Path(__file__).resolve().parents[4]
parser.add_argument("--repo-root", type=Path, default=default_root)
args = parser.parse_args()
root = args.repo_root.resolve()
reference = root.parent / "40250/Server Client TMP4/ClientVS22/source/EterLib/NetStream.cpp"
current = root / "extension/src/net/classic/sequence_table.h"
try:
ref = read_array(reference, "s_bSequenceTable")
cur = read_array(current, "SEQUENCE_TABLE")
except (OSError, ValueError) as exc:
print(json.dumps({"status": "ERROR", "error": str(exc)}, indent=2, sort_keys=True))
return 2
first_difference = next((i for i, pair in enumerate(zip(ref, cur)) if pair[0] != pair[1]), None)
equal = len(ref) == len(cur) and first_difference is None
result = {
"status": "PASS" if equal else "FAIL",
"reference": {"length": len(ref), "sha256": digest(ref)},
"current": {"length": len(cur), "sha256": digest(cur)},
"first_difference": first_difference,
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if equal else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,348 @@
#!/usr/bin/env python3
"""Audit active 40250 project sources against parity contract ownership.
This is a coverage gate, not a parity verifier. A file inferred to a contract
still requires that contract's branch-by-branch review; the report deliberately
labels inferred ownership separately from exact manifest references.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import re
import xml.etree.ElementTree as ET
from collections import Counter, defaultdict
REFERENCE_DIRS = (
"UserInterface",
"GameLib",
"EffectLib",
"EterLib",
"EterGrnLib",
"PRTerrainLib",
"MilesLib",
"EterPack",
"EterLocale",
)
SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".inl"}
IGNORED_BASENAMES = {"stdafx.cpp", "stdafx.h", "stdafx.hxx", "stdafx.hpp"}
REVIEWED_STATIC_GROUPS = {
"EffectLib", "EterGrnLib", "MilesLib",
"GameLib", "PRTerrainLib", "EterPack", "EterLocale", "EterLib",
}
REVIEWED_STATIC_FILES = {
"UserInterface/PythonNetworkStreamCommand.cpp",
"EterLib/Camera.cpp", "EterLib/Camera.h",
"EterLib/IME.cpp", "EterLib/IME.h", "EterLib/Input.h", "EterLib/msctf.h",
"EterLib/Resource.cpp", "EterLib/Resource.h", "EterLib/ResourceManager.cpp", "EterLib/ResourceManager.h",
"EterLib/FileLoaderThread.cpp", "EterLib/FileLoaderThread.h",
"EterLib/TextFileLoader.cpp", "EterLib/TextFileLoader.h", "EterLib/parser.cpp", "EterLib/parser.h",
"EterLib/PathStack.cpp", "EterLib/PathStack.h",
"EterLib/SkyBox.cpp", "EterLib/SkyBox.h", "EterLib/ScreenFilter.cpp", "EterLib/ScreenFilter.h",
"EterLib/LensFlare.cpp", "EterLib/LensFlare.h", "EterLib/ColorTransitionHelper.cpp", "EterLib/ColorTransitionHelper.h",
"EterLib/AttributeData.cpp", "EterLib/AttributeData.h", "EterLib/AttributeInstance.h",
"EterLib/CollisionData.cpp", "EterLib/CollisionData.h", "EterLib/Decal.cpp", "EterLib/Decal.h",
"EterLib/CullingManager.cpp", "EterLib/CullingManager.h",
"EterLib/GrpText.cpp", "EterLib/GrpText.h", "EterLib/GrpTextInstance.cpp", "EterLib/GrpTextInstance.h",
"EterLib/TextBar.cpp", "EterLib/TextBar.h", "EterLib/TextTag.cpp", "EterLib/TextTag.h",
"EterLib/GrpFontTexture.cpp", "EterLib/GrpFontTexture.h", "EterLib/GrpMarkInstance.cpp", "EterLib/GrpMarkInstance.h",
"EterLib/GrpDevice.cpp", "EterLib/GrpDevice.h", "EterLib/GrpScreen.cpp", "EterLib/GrpScreen.h",
"EterLib/GrpShadowTexture.cpp", "EterLib/GrpShadowTexture.h",
"UserInterface/AbstractApplication.h", "UserInterface/AbstractSingleton.h",
"UserInterface/CheckLatestFiles.h",
"UserInterface/ProcessCRC.cpp", "UserInterface/ProcessCRC.h",
"UserInterface/ProcessScanner.cpp", "UserInterface/ProcessScanner.h",
"UserInterface/HackShield.cpp", "UserInterface/Hackshield.h",
"UserInterface/HackshieldLicense.h", "UserInterface/HackshieldLicense_metin2.h",
"UserInterface/HackshieldLicense_metin2client.h",
"UserInterface/NProtectGameGuard.cpp", "UserInterface/NProtectGameGuard.h",
"UserInterface/PythonApplicationLogo.cpp", "UserInterface/PythonApplicationModule.cpp",
"UserInterface/PythonApplicationWebPage.cpp",
"UserInterface/PythonExceptionSender.cpp", "UserInterface/PythonExceptionSender.h",
"UserInterface/PythonProfilerModule.cpp",
"UserInterface/PythonSystem.cpp", "UserInterface/PythonSystem.h", "UserInterface/PythonSystemModule.cpp",
"UserInterface/MovieMan.cpp", "UserInterface/MovieMan.h",
"UserInterface/UserInterface.cpp", "UserInterface/Test.h", "UserInterface/resource.h",
"UserInterface/WiseLogicXTrap.cpp", "UserInterface/WiseLogicXTrap.h",
"UserInterface/AbstractCharacterManager.h", "UserInterface/AbstractChat.h", "UserInterface/AbstractPlayer.h",
"UserInterface/AccountConnector.h", "UserInterface/AffectFlagContainer.h",
"UserInterface/CameraProcedure.cpp", "UserInterface/Discord.h", "UserInterface/GameType.cpp",
"UserInterface/InstanceBaseEvent.cpp", "UserInterface/InstanceBaseTransform.cpp",
"UserInterface/NetworkActorManager.h",
"UserInterface/PythonBackground.h", "UserInterface/PythonBackgroundModule.cpp",
"UserInterface/PythonCharacterManager.h", "UserInterface/PythonCharacterManagerModule.cpp",
"UserInterface/PythonCharacterModule.cpp", "UserInterface/PythonNonPlayer.h",
"UserInterface/PythonNonPlayerModule.cpp", "UserInterface/PythonPlayerEventHandler.h",
"UserInterface/GuildMarkDownloader.h", "UserInterface/GuildMarkUploader.h",
"UserInterface/MarkImage.cpp", "UserInterface/MarkImage.h", "UserInterface/MarkManager.h",
"UserInterface/PythonGuild.h", "UserInterface/PythonMessenger.h",
"UserInterface/InsultChecker.cpp", "UserInterface/InsultChecker.h",
"UserInterface/PythonChatModule.cpp", "UserInterface/PythonTextTailModule.cpp",
"UserInterface/PythonEventManagerMoudle.cpp", "UserInterface/PythonGameEventManagerModule.cpp",
"UserInterface/PythonQuest.h",
"UserInterface/PythonExchange.h", "UserInterface/PythonExchangeModule.cpp",
"UserInterface/PythonSafeBox.h", "UserInterface/PythonShop.h",
"UserInterface/PythonIME.h", "UserInterface/PythonIMEModule.cpp",
"UserInterface/PythonMiniMapModule.cpp",
"UserInterface/PythonNetworkDatagram.cpp", "UserInterface/PythonNetworkDatagram.h",
"UserInterface/PythonNetworkDatagramModule.cpp", "UserInterface/PythonNetworkStream.h",
"UserInterface/PythonNetworkStreamPhaseOffline.cpp",
"UserInterface/ServerStateChecker.h", "UserInterface/ServerStateCheckerModule.cpp",
"UserInterface/PythonSkill.h",
"UserInterface/Locale.cpp", "UserInterface/Locale.h",
"UserInterface/Locale_inc_BR.h", "UserInterface/Locale_inc_CA.h",
"UserInterface/Locale_inc_EU.h", "UserInterface/Locale_inc_EU2.h",
"UserInterface/Locale_inc_GB.h", "UserInterface/Locale_inc_HK.h",
"UserInterface/Locale_inc_JP.h", "UserInterface/Locale_inc_KO.h",
"UserInterface/Locale_inc_KO_DEV.h", "UserInterface/Locale_inc_NEWCIBN.h",
"UserInterface/Locale_inc_SG.h", "UserInterface/Locale_inc_VI.h",
"UserInterface/Locale_inc_dev.h",
}
def normalize(path: pathlib.Path, root: pathlib.Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def manifest_data(repo: pathlib.Path) -> tuple[set[str], dict[str, list[str]]]:
data = json.loads((repo / "audit/manifest.json").read_text(encoding="utf-8"))
listed: set[str] = set()
owners: dict[str, list[str]] = defaultdict(list)
for contract in data["contracts"]:
cid = contract["id"]
for raw in contract.get("reference", {}).get("files", []):
path = raw.replace("\\", "/")
listed.add(path)
owners[path].append(cid)
return listed, owners
def active_project_sources(reference_root: pathlib.Path) -> set[str]:
active: set[str] = set()
for project in sorted((reference_root.parent / "vs_files").glob("*/*.vcxproj")):
try:
tree = ET.parse(project)
except ET.ParseError:
continue
for node in tree.iter():
if not (node.tag.endswith("ClCompile") or node.tag.endswith("ClInclude")):
continue
include = node.attrib.get("Include", "")
if not include:
continue
candidate = (project.parent / pathlib.Path(include.replace("\\", "/"))).resolve()
try:
relative = candidate.relative_to(reference_root.resolve())
except ValueError:
continue
if relative.parts and relative.parts[0] in REFERENCE_DIRS:
if relative.suffix.lower() in SOURCE_SUFFIXES:
active.add(relative.as_posix())
return active
def infer_owners(path: str) -> list[str]:
"""Infer behavior ownership for a source not explicitly listed in manifest.
These rules are intentionally broad. They identify review queues, never
upgrade a contract status or claim semantic equivalence.
"""
lower = path.lower()
area, _, name = lower.partition("/")
stem = pathlib.PurePosixPath(name).stem
owners: list[str] = []
def add(*ids: str) -> None:
for cid in ids:
if cid not in owners:
owners.append(cid)
if area == "effectlib":
add("render.effect_particle_motion")
elif area == "etergrnlib":
add("render.model_attachment")
elif area == "mileslib":
add("audio.bgm_ambience_3d")
elif area == "eterpack" or area == "eterlocale":
add("resource.pack_locale_proto")
elif area == "prterrainlib":
add("world.terrain_water_collision")
elif area == "eterlib":
if any(x in stem for x in ("net", "packet")):
add("network.packet_dispatch_protocol")
if any(x in stem for x in ("camera", "input", "ime")):
add("ui.input_camera_mobile")
if any(x in stem for x in ("attribute", "collision", "ray", "intersect", "decal")):
add("world.terrain_water_collision", "movement.click_collision", "combat.flying_projectile")
if any(x in stem for x in ("sky", "lensflare", "screenfilter", "colortransition")):
add("world.environment_weather")
if any(x in stem for x in ("text", "font", "tag")):
add("ui.hud_target_minimap_status", "social.chat_text_tail")
if "mark" in stem:
add("social.party_guild_messenger")
if any(x in stem for x in ("resource", "path", "fileloader", "parser", "image", "targa", "jpeg")):
add("resource.pack_locale_proto")
if any(x in stem for x in ("device", "state", "grp", "culling", "shadow", "blocktexture")):
add("render.model_attachment", "lifecycle.platform_release")
if any(x in stem for x in ("msapplication", "mswindow", "mutex", "thread", "referenceobject", "profiler")):
add("lifecycle.platform_release")
if any(x in stem for x in ("dimm", "dynamic", "event", "funcobject", "pool", "ref", "util")):
add("lifecycle.platform_release")
if "dibbar" in stem:
add("resource.pack_locale_proto", "ui.hud_target_minimap_status")
if "msctf" in stem:
add("ui.input_camera_mobile")
elif area == "gamelib":
if any(x in stem for x in ("fly", "flying")):
add("combat.flying_projectile")
if any(x in stem for x in ("weapontrace", "battle")):
add("combat.attack_combo_damage")
if "actorinstance" in stem or any(x in stem for x in ("motion", "blend", "rotation", "position")):
add("movement.motion_resource_events", "render.model_attachment", "skill.cast.effect_timing")
if any(x in stem for x in ("map", "terrain", "physics")):
add("world.terrain_water_collision", "world.map_load_transition")
if "dungeon" in stem:
add("world.dungeon_activity")
if "snow" in stem:
add("world.environment_weather")
if any(x in stem for x in ("property", "monsterarea")):
add("world.map_load_transition", "world.npc_drop_actor_spawn")
if any(x in stem for x in ("race", "item")):
add("resource.pack_locale_proto", "render.model_attachment")
if not owners:
add("world.map_load_transition")
elif area == "userinterface":
if any(x in stem for x in ("chat", "texttail", "insult")):
add("social.chat_text_tail")
if any(x in stem for x in ("character", "character_manager")):
add("network.character_select.create_delete", "social.party_guild_messenger")
if any(x in stem for x in ("eventmanager", "quest")):
add("quest.quest_event_navigation")
if any(x in stem for x in ("ime", "input", "cursor")):
add("ui.input_camera_mobile")
if "serverstate" in stem or "account" in stem:
add("network.error_timeout_server_state", "network.login.phase_flow")
if any(x in stem for x in ("locale", "language")):
add("resource.pack_locale_proto")
if any(x in stem for x in ("mark", "guild", "messenger")):
add("social.party_guild_messenger")
if any(x in stem for x in ("exchange", "safebox", "shop")):
add("social.exchange_safebox", "npc.dialog_shop")
if any(x in stem for x in ("application", "exception", "crc", "process", "movie", "system")):
add("lifecycle.bootstrap_phase_host", "lifecycle.platform_release")
if "background" in stem:
add("world.map_load_transition", "world.environment_weather")
if "minimap" in stem:
add("ui.hud_target_minimap_status")
if "nonplayer" in stem:
add("world.npc_drop_actor_spawn")
if any(x in stem for x in ("networkstream", "networkdatagram")):
add("network.packet_dispatch_protocol", "network.reconnect_warp")
if "player" in stem or "instancebase" in stem:
add("movement.keyboard.motion", "combat.attack_combo_damage", "world.entity_spawn_state_sync")
if "skill" in stem:
add("skill.player_ui_cooldown", "skill.cast.effect_timing")
if "item" in stem:
add("item.inventory_equipment_state", "item.consumable_action", "item.refine_socket_costume")
if "pythontexttail" in stem:
add("ui.hud_target_minimap_status")
if "profiler" in stem:
add("lifecycle.platform_release")
if not owners:
add("lifecycle.bootstrap_phase_host")
return owners
def build(repo: pathlib.Path) -> dict:
reference_root = (repo / "../40250/Server Client TMP4/ClientVS22/source").resolve()
listed, exact_owners = manifest_data(repo)
active = active_project_sources(reference_root)
rows = []
for path in sorted(active):
if pathlib.PurePosixPath(path).name.lower() in IGNORED_BASENAMES:
kind = "build_support"
owners = []
elif path in exact_owners:
kind = "manifest_exact"
owners = exact_owners[path]
else:
kind = "inferred_review_queue"
owners = infer_owners(path)
if path in REVIEWED_STATIC_FILES or pathlib.PurePosixPath(path).parts[0] in REVIEWED_STATIC_GROUPS:
kind = "reviewed_static_group"
rows.append({"path": path, "kind": kind, "owners": owners})
counts = Counter(row["kind"] for row in rows)
unowned = [row["path"] for row in rows if not row["owners"] and row["kind"] != "build_support"]
by_contract: dict[str, dict[str, int]] = defaultdict(lambda: Counter())
for row in rows:
for owner in row["owners"]:
by_contract[owner][row["kind"]] += 1
return {
"reference_root": "../40250/Server Client TMP4/ClientVS22/source",
"active_source_count": len(rows),
"manifest_exact_count": counts["manifest_exact"],
"inferred_review_queue_count": counts["inferred_review_queue"],
"reviewed_static_group_count": counts["reviewed_static_group"],
"build_support_count": counts["build_support"],
"unowned_behavior_candidates": unowned,
"by_contract": {k: dict(v) for k, v in sorted(by_contract.items())},
"files": rows,
}
def markdown(data: dict) -> str:
lines = [
"# Active 40250 source coverage",
"",
"This report is a coverage gate, not proof of parity. `manifest_exact` means the source is explicitly listed by a contract; `inferred_review_queue` means it is assigned by a broad area rule and still requires semantic review.",
"",
f"- Active Visual Studio source/include files: **{data['active_source_count']}**",
f"- Explicitly listed by manifest: **{data['manifest_exact_count']}**",
f"- Static-reviewed groups (still PARTIAL for parity): **{data['reviewed_static_group_count']}**",
f"- Inferred review queue: **{data['inferred_review_queue_count']}**",
f"- Build support (`StdAfx`): **{data['build_support_count']}**",
f"- Unowned behavior candidates: **{len(data['unowned_behavior_candidates'])}**",
"",
"## Review queue by contract",
"",
"| Contract | Exact manifest files | Static-reviewed files | Inferred files |",
"|---|---:|---:|---:|",
]
for cid, counts in data["by_contract"].items():
lines.append(f"| `{cid}` | {counts.get('manifest_exact', 0)} | {counts.get('reviewed_static_group', 0)} | {counts.get('inferred_review_queue', 0)} |")
lines.extend(["", "## Unowned behavior candidates", ""])
if data["unowned_behavior_candidates"]:
lines.extend(f"- `{p}`" for p in data["unowned_behavior_candidates"])
else:
lines.append("None")
lines.extend([
"",
"## Required interpretation",
"",
"The inferred queue is not considered audited merely because a rule assigned it to a contract. Each queue must be reviewed through the contract's reference call chain and either added to exact evidence, explicitly classified as support/disabled/vendor code, or recorded as a remaining gap. Static-reviewed groups have had their active symbols and lifecycle call chain checked in the current round, but their contracts remain PARTIAL until implementation equivalence and tests close the documented gaps.",
"",
])
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--write", action="store_true")
args = parser.parse_args()
repo = pathlib.Path(__file__).resolve().parents[4]
data = build(repo)
if args.write:
(repo / "audit/source-coverage.json").write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(repo / "audit/reports/source-coverage.md").write_text(markdown(data), encoding="utf-8")
else:
print(markdown(data))
return 0 if not data["unowned_behavior_candidates"] else 2
if __name__ == "__main__":
raise SystemExit(main())