Save endgame progress, team presets, technique support and camera controls

This commit is contained in:
CAtlas0784 committed 2026-10-17 23:44:36 +07:00
1 parent a681affdb3
commit 639cef7d19
52 files changed
+11940 -766

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
"""Audit every installed Endgame room against stage tables and Client metadata.
This reads data only. It does not establish that a room or battle plays in the
Client, nor does it validate reward/settlement callbacks.
"""
import argparse
import collections
import json
import pathlib
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=pathlib.Path, required=True)
args = parser.parse_args()
def read(name):
return json.loads((args.root / name).read_text(encoding="utf-8"))
metadata = read("resources/SceneMetadata.json")["floors"]
entrances = {row["ID"]: row for row in read("resources/MapEntrance.json")["map_entrance_config"]}
stages = {row["StageID"]: row for row in read("resources/StageConfig.json")["stage_config"]}
root_scenes = read("res.json")["levelOutputConfigs"]
supplementary = {row["entryID"]: row for row in read("resources/res.json")["scene_config"]}
checks = []
def check(mode, identifier, node, entry, group, events, config_ids):
problems = []
entrance = entrances.get(entry)
floor_id = entrance["FloorID"] if entrance else None
floor = metadata.get(str(floor_id))
if floor is None or len(floor["groups"]) != floor["group_count"]:
problems.append("missing or incomplete Client floor metadata")
else:
group_data = floor["groups"].get(str(group))
if not group_data or group_data["is_deleted"] or group_data["load_side"] == "Client":
problems.append("invalid server group")
if not floor.get("start_anchor"):
problems.append("missing Client start anchor")
placements = floor.get("monsters", {}).get(str(group), {})
for config_id in config_ids:
if str(config_id) not in placements:
problems.append(f"missing Client monster placement {config_id}")
source = root_scenes.get(str(entry), {})
if source:
monsters = [m for scene in source.values() for m in scene["scenes"].get(str(group), {}).get("monsters", [])]
else:
monsters = [m for m in supplementary.get(entry, {}).get("monsters", []) if m["groupId"] == group]
if not monsters:
problems.append("missing scene monster source used by Go loader")
for event in events:
stage = stages.get(event)
if not stage or not stage["MonsterList"] or any(not wave for wave in stage["MonsterList"]):
problems.append(f"missing stage waves {event}")
checks.append({"mode": mode, "id": identifier, "node": node, "entry": entry,
"floor": floor_id, "group": group, "events": events, "issues": problems})
maze = read("resources/ChallengeMazeConfig.json")["challenge_config"]
tierce = read("resources/ChallengeMazeTierceConfig.json")["challenge_tierce_config"]
peak = read("resources/ChallengePeakConfig.json")["challenge_peak_config"]
for row in maze:
mode = "PF" if 20000 < row["ID"] < 30000 else "AS" if row["ID"] > 30000 else "Memory/MoC"
for index in (1, 2):
suffix = str(index)
if row.get("EventIDList" + suffix):
check(mode, row["ID"], index - 1, row["MapEntranceID" + ("2" if index == 2 else "")],
row["MazeGroupID" + suffix], row["EventIDList" + suffix], row["ConfigList" + suffix])
for row in tierce:
check("Tierce", row["ID"], 2, row["MapEntranceID"], row["MazeGroupID"], row["EventIDList"], row["ConfigList"])
for row in peak:
# Peak's selected monster instance is resolved from its configured group.
check("Peak", row["ID"], 0, row["MapEntranceID"], row["MazeGroupID"], row["EventIDList"], [])
by_id = {row["ID"]: row for row in peak}
for row in read("resources/ChallengePeakBossConfig.json")["challenge_peak_boss_config"]:
base = by_id[row["ID"]]
check("Peak hard", row["ID"], 0, base["MapEntranceID"], base["MazeGroupID"], row["HardEventIDList"], [])
report = {"scope": "resource and Client metadata audit; no gameplay verification",
"counts": dict(collections.Counter(row["mode"] for row in checks)),
"checked_rooms": len(checks), "floors": len({row["floor"] for row in checks}),
"issues": [row for row in checks if row["issues"]]}
args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps({key: value for key, value in report.items() if key != "issues"}))
print(f"Issues: {len(report['issues'])}; details: {args.output}")
+74
View File
@@ -0,0 +1,74 @@
"""Export complete RuntimeFloor group captures from a Client Player.log.
The capture must include FLOOR/GROUP/END records from a read-only Client
GameCoreConfigLoader probe. Inputs and Client revision are explicit arguments.
"""
import argparse
import hashlib
import json
import pathlib
import re
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("log", type=pathlib.Path)
parser.add_argument("--assembly", type=pathlib.Path, required=True)
parser.add_argument("--client-version", required=True)
parser.add_argument("--output", type=pathlib.Path, required=True)
parser.add_argument("--merge", action="store_true", help="Preserve compatible previously captured floors in output")
args = parser.parse_args()
floors, completed = {}, set()
placements = {}
placement_completed = set()
for line in args.log.read_text(encoding="utf-8", errors="replace").splitlines():
if "[GAC_SCENE_PLACEMENT] " in line:
row = line.split("[GAC_SCENE_PLACEMENT] ", 1)[1]
end_placement = re.fullmatch(r"END (\d+)", row)
if end_placement:
placement_completed.add(end_placement[1])
position = re.fullmatch(r"(ANCHOR|MONSTER) (\d+) (\d+)(?: (\d+))? pos=([^,]+),([^,]+),([^ ]+) rot=(.+)", row)
if position:
kind, floor, first_id, inst_id, x, y, z, rot = position.groups()
# Unity metres/degrees -> protocol position/rotation units.
value = {"pos": {"x": round(float(x)*1000), "y": round(float(y)*1000), "z": round(float(z)*1000)}, "rot": {"x": 0, "y": round(float(rot)*1000), "z": 0}}
target = placements.setdefault(floor, {"monsters": {}})
if kind == "ANCHOR":
value["ID"] = int(first_id)
target["start_anchor"] = value
else:
target["monsters"].setdefault(first_id, {})[inst_id] = value
continue
if "[GAC_SCENE_META]" not in line:
continue
row = line.split("[GAC_SCENE_META] ", 1)[1]
start = re.fullmatch(r"FLOOR (\d+) (\d+) startGroup=(\d+) anchor=(\d+) count=(\d+)", row)
group = re.fullmatch(r"GROUP (\d+) (\d+) side=(\w+): \d+ hoyo=(true|false) deleted=(true|false)", row)
end = re.fullmatch(r"END (\d+)", row)
if start:
plane, floor, start_group, anchor, count = start.groups()
floors[floor] = {"plane_id": int(plane), "group_count": int(count), "groups": {}}
completed.discard(floor)
elif group:
floor, group_id, side, hoyo, deleted = group.groups()
if floor in floors:
floors[floor]["groups"][group_id] = {"load_side": side, "is_hoyo_group": hoyo == "true", "is_deleted": deleted == "true"}
elif end:
completed.add(end[1])
if not floors or any(key not in completed or len(value["groups"]) != value["group_count"] for key, value in floors.items()):
raise SystemExit("Incomplete scene metadata capture; no output written")
for key, value in placements.items():
if key in floors and key in placement_completed:
floors[key].update(value)
result = {"client_version": args.client_version, "source": {
"kind": "Client GameCoreConfigLoader.LoadRtLevelFloorInfo / GroupInstanceCommonMap",
"assembly_sha256": hashlib.file_digest(args.assembly.open("rb"), "sha256").hexdigest(),
"capture_sha256": hashlib.sha256(args.log.read_bytes()).hexdigest(),
}, "floors": floors}
if args.merge and args.output.exists():
previous = json.loads(args.output.read_text(encoding="utf-8"))
if previous["client_version"] != result["client_version"] or previous["source"]["assembly_sha256"] != result["source"]["assembly_sha256"]:
raise SystemExit("Cannot merge metadata from different Clients")
previous["floors"].update(floors)
result["floors"] = previous["floors"]
result["source"]["previous_capture_sha256"] = previous["source"].get("previous_capture_sha256", []) + [previous["source"]["capture_sha256"]]
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"Exported {len(floors)} floors to {args.output}")
+74
View File
@@ -0,0 +1,74 @@
-- Read-only template. Replace __FLOOR_INPUTS__ with Lua pairs from the
-- installed MapEntrance table, and __CAPTURE_KEY__ with a unique string.
-- Read a bounded batch per heartbeat; never enter a map or alter entities.
do
local key = '__CAPTURE_KEY__'
_G.__gacSceneExports = _G.__gacSceneExports or {}
local progress = _G.__gacSceneExports[key] or 1
local floors = __FLOOR_INPUTS__
local function log(kind, text)
CS.UnityEngine.Debug.Log('[GAC_SCENE_' .. kind .. '] ' .. tostring(text))
end
local function world(group, item)
local yaw = group.RotY * math.pi / 180
return group.PosX + item.PosX * math.cos(yaw) + item.PosZ * math.sin(yaw),
group.PosY + item.PosY,
group.PosZ - item.PosX * math.sin(yaw) + item.PosZ * math.cos(yaw),
group.RotY + item.RotY
end
local function position(kind, prefix, group, item)
local x, y, z, rot = world(group, item)
log(kind, prefix .. ' pos=' .. tostring(x) .. ',' .. tostring(y) .. ',' .. tostring(z) .. ' rot=' .. tostring(rot))
end
local batchSize = 4
for index = progress, math.min(progress + batchSize - 1, #floors) do
local pair = floors[index]
local ok, err = pcall(function()
local path = CS.RPG.Client.MapDef.GetRtFloorInfoPath(pair[1], pair[2])
local floor = CS.RPG.GameCore.GameCoreConfigLoader.LoadRtLevelFloorInfo(path)
if floor == nil then error('missing RuntimeFloor ' .. path) end
local floorID = tostring(pair[2])
log('META', 'FLOOR ' .. tostring(pair[1]) .. ' ' .. floorID
.. ' startGroup=' .. tostring(floor.StartGroupIndex) .. ' anchor=' .. tostring(floor.StartAnchorID)
.. ' count=' .. tostring(floor.GroupInstanceCommonMap.Count))
local iterator = floor.GroupInstanceCommonMap:GetEnumerator()
while iterator:MoveNext() do
local entry = iterator.Current
log('META', 'GROUP ' .. floorID .. ' ' .. tostring(entry.Key)
.. ' side=' .. tostring(entry.Value.LoadSide) .. ' hoyo=' .. tostring(entry.Value.IsHoyoGroup)
.. ' deleted=' .. tostring(entry.Value.IsDelete))
end
log('META', 'END ' .. floorID)
for i = 0, floor.GroupInstanceList.Length - 1 do
local group = floor.GroupInstanceList[i]
local common = floor.GroupInstanceCommonMap[group.ID]
if not group.IsDelete and common ~= nil and not common.IsDelete
and (i == floor.StartGroupIndex or tostring(common.LoadSide):find('Server', 1, true)) then
local data = CS.RPG.GameCore.GameCoreConfigLoader.LoadRtLevelGroupInfo(group.GroupPath)
if data == nil then error('missing RuntimeGroup ' .. tostring(group.GroupPath)) end
if i == floor.StartGroupIndex and data.AnchorList ~= nil then
for j = 0, data.AnchorList.Length - 1 do
local anchor = data.AnchorList[j]
if not anchor.IsDelete and anchor.ID == floor.StartAnchorID then
position('PLACEMENT', 'ANCHOR ' .. floorID .. ' ' .. tostring(anchor.ID), group, anchor)
end
end
end
if data.MonsterList ~= nil then
for j = 0, data.MonsterList.Length - 1 do
local monster = data.MonsterList[j]
if not monster.IsDelete and not monster.IsClientOnly then
position('PLACEMENT', 'MONSTER ' .. floorID .. ' ' .. tostring(group.ID)
.. ' ' .. tostring(monster.ID), group, monster)
end
end
end
end
end
log('PLACEMENT', 'END ' .. floorID)
end)
if not ok then log('META', 'ERROR ' .. tostring(pair[2]) .. ' ' .. tostring(err)) end
progress = index + 1
end
_G.__gacSceneExports[key] = progress
end