fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user