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
@@ -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())