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