Update GaCy-GO

This commit is contained in:
CAtlas0784 committed 2026-10-28 18:54:43 +07:00
1 parent da8a32f60d
commit 0cbe460e49
345 files changed
+98202 -77559

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ description: คู่มือ Honkai Star Rail Private Server (GaC-hkrpg-go
# คู่มือ HSR Private Server — บทเรียนที่พิสูจน์แล้ว (อัปเดตล่าสุด: 2026-10-07)
> **สถานะปัจจุบัน:** เซิร์ฟเวอร์หลักคือ **`C:\Users\Phitchayut\Desktop\GaC-hkrpg-go` (ภาษา Go, client 4.6.51)**
> **สถานะปัจจุบัน:** เซิร์ฟเวอร์หลักคือ **`C:\Users\Phitchayut\Desktop\GaC-hkrpg-go` (ภาษา Go, client 4.6.5)**
> โปรเจกต์ Rust `GaC-hkrpg-ps` เป็นตัวต้นทาง/reference — โค้ด Rust ล่าสุดแตกตัวกับ proto ใน repo (ดูหัวข้อ 10)
> เอกสารฉบับเต็มของตัว Go อยู่ที่ `GaC-hkrpg-go/README.md`
Binary file not shown.
Binary file not shown.
+50
View File
@@ -0,0 +1,50 @@
-- Research only: named NPC/Peak config metadata, eight members per heartbeat.
-- Does not load scene graphs, enumerate assembly types, mutate module state,
-- create monsters, send requests or call any config getter.
do
local function tick()
local state=_G.__gacAANpcConfigMetadata1 or {target=1,index=0}
_G.__gacAANpcConfigMetadata1=state
if state.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),62)
local names={
{'client','RPG.Client.NPCManager'},
{'core','RPG.GameCore.NPCMonsterDataRow'},
{'core','RPG.GameCore.NPCMonsterDataExcelTable'},
{'core','RPG.GameCore.NPCMonsterExcelTable'},
{'core','RPG.GameCore.ChallengePeakConfigRow'},
{'core','RPG.GameCore.ChallengePeakExcelTable'}
}
local function log(value)CS.UnityEngine.Debug.Log('[GAC_AA_NPC_META] '..tostring(value))end
if state.target>#names then state.done=true;log('END');return end
local descriptor=names[state.target]
if not state.type then
local assembly=descriptor[1]=='client' and typeof(CS.RPG.Client.GridFightModule).Assembly or typeof(CS.RPG.GameCore.AvatarExcelTable).Assembly
state.type=assembly:GetType(descriptor[2])
if not state.type then log('MISSING '..descriptor[2]);state.target=state.target+1;return end
log('TYPE '..state.type.FullName)
state.fields=state.type:GetFields(flags);state.methods=nil;state.index=0
end
for n=1,8 do
if not state.methods then
if state.index>=state.fields.Length then state.methods=state.type:GetMethods(flags);state.index=0
else
local field=state.fields[state.index];state.index=state.index+1
if descriptor[1]=='core' or field.Name:lower():find('monster',1,true) then
log('FIELD '..state.type.FullName..' '..field.Name..' '..field.FieldType.FullName)
end
end
else
if state.index>=state.methods.Length then
state.type=nil;state.fields=nil;state.methods=nil;state.index=0;state.target=state.target+1;return
end
local method=state.methods[state.index];state.index=state.index+1
if method.DeclaringType==state.type and (descriptor[1]=='core' or method.Name=='CreateNPCMonster')then
log('METHOD '..state.type.FullName..' '..tostring(method)..' HANDLE '..tostring(method.MethodHandle.Value))
end
end
end
end
local ok,err=pcall(tick)
if not ok then _G.__gacAANpcConfigMetadata1={done=true};CS.UnityEngine.Debug.Log('[GAC_AA_NPC_META] ERROR '..tostring(err))end
end
+15
View File
@@ -0,0 +1,15 @@
-- Exact AA module metadata; eight methods per heartbeat, no refresh calls.
do local function capture()
local s=_G.__gacAaRoundMeta1 or {index=0};_G.__gacAaRoundMeta1=s
if s.done then return end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengePeakModule
if not module then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_AA_ROUND_META] '..tostring(v))end
if not s.t then s.t=module:GetType();s.methods=s.t:GetMethods(flags);log('TYPE '..s.t.FullName)end
for n=1,8 do
if s.index>=s.methods.Length then s.done=true;log('END');return end
local m=s.methods[s.index];s.index=s.index+1
if m.DeclaringType==s.t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
end local ok,e=pcall(capture);if not ok then _G.__gacAaRoundMeta1={done=true};CS.UnityEngine.Debug.Log('[GAC_AA_ROUND_META] ERROR '..tostring(e))end end
+34
View File
@@ -0,0 +1,34 @@
-- Exact failing AdventurePhase only. Inspect metadata and nullness; no phase
-- setters, scene loads, method invocation, or loading-screen bypass.
do
local function capture()
local s=_G.__gacAdventureFinalReady1 or {offset=0,kind='fields'}
_G.__gacAdventureFinalReady1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or phase:GetType().FullName~='RPG.Client.AdventurePhase'then return end
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_ADVENTURE_FINAL_READY] '..tostring(v))end
if not s.t then s.t=phase:GetType();s.members=s.t:GetFields(flags);log('TYPE '..s.t.FullName)end
for n=1,16 do
if s.offset>=s.members.Length then
if s.kind=='fields'then
s.t=s.t.BaseType
if s.t and s.t.FullName~='System.Object'then s.members=s.t:GetFields(flags);s.offset=0;return end
s.t=phase:GetType();s.members=s.t:GetMethods(flags);s.offset=0;s.kind='methods';return
end
s.done=true;log('END');return
end
local member=s.members[s.offset];s.offset=s.offset+1
if member.DeclaringType==s.t then
if s.kind=='fields'and not member.IsStatic then
local value=member:GetValue(phase)
log('FIELD '..member.Name..' '..member.FieldType.FullName..' HANDLE '..tostring(member.FieldHandle.Value)..' NULL '..tostring(value==nil))
elseif s.kind=='methods'then log('METHOD '..member.Name..' HANDLE '..tostring(member.MethodHandle.Value))end
end
end
end
local ok,e=pcall(capture)
if not ok then _G.__gacAdventureFinalReady1={done=true};CS.UnityEngine.Debug.Log('[GAC_ADVENTURE_FINAL_READY] ERROR '..tostring(e))end
end
+29
View File
@@ -0,0 +1,29 @@
-- Read only metadata for the exact failing phase and its GlobalVars services.
-- Bounded per heartbeat; no native calls, initialization or phase flag changes.
do
local function capture()
local s=_G.__gacAdventureGlobals1 or {index=0,stage='phase'}
_G.__gacAdventureGlobals1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_ADVENTURE_GLOBALS] '..tostring(v))end
if not s.fields then
s.t=s.stage=='phase' and typeof(CS.RPG.Client.AdventurePhase) or typeof(CS.RPG.Client.GlobalVars)
s.fields=s.t:GetFields(flags)
end
for n=1,12 do
if s.index>=s.fields.Length then
if s.stage=='phase' then s.stage='globals';s.index=0;s.fields=nil;return end
s.done=true;log('END');return
end
local f=s.fields[s.index];s.index=s.index+1
if (s.stage=='globals' and f.IsStatic) or
(s.stage=='phase' and (f.Name=='<ADPInitParams>k__BackingField' or f.Name=='<InputController>k__BackingField' or f.Name=='_ReactionSys')) then
local value=f.IsStatic and f:GetValue(nil) or nil
log(s.stage..' FIELD '..f.Name..' TYPE '..f.FieldType.FullName..' HANDLE '..tostring(f.FieldHandle.Value)..' NULL '..tostring(value==nil))
end
end
end
local ok,e=pcall(capture)
if not ok then _G.__gacAdventureGlobals1={done=true};CS.UnityEngine.Debug.Log('[GAC_ADVENTURE_GLOBALS] ERROR '..tostring(e))end
end
+18
View File
@@ -0,0 +1,18 @@
"""Build an explicit compatibility resource from a completed current-Client capture."""
import json, hashlib
from pathlib import Path
root = Path(__file__).resolve().parents[1]
capture = root / 'battle_diagnostics/cw54-role-replacement.log'
raw = capture.read_text(encoding='utf-8-sig')
rows = [json.loads(line.split(' ROW ', 1)[1]) for line in raw.splitlines() if '[GAC_CW_REPLACEMENT] ROW ' in line]
if '[GAC_CW_REPLACEMENT] END' not in raw or not rows:
raise ValueError('Capture is incomplete')
old = json.loads((root / 'resources/CurrencyWarsCatalog.json').read_text(encoding='utf-8-sig'))
current = json.loads((root / 'resources/CurrencyWarsCurrentRoles.json').read_text(encoding='utf-8-sig'))
ids = {row['ID'] for row in current['roles']}
target = rows[0]['Config']
missing = [row['Config'] for row in old['active_handbook_default_roles'] if row['Config']['ID'] not in ids and row['Config']['AvatarID'] == target['AvatarID'] and row['Config']['SeasonID'] == target['SeasonID']]
if len(missing) != 1 or {row['Star'] for row in rows} != set(range(1, rows[0]['MaxStar'] + 1)):
raise ValueError('Replacement identity or star coverage is ambiguous')
resource = {'format_version': 1, 'source_assembly_sha256': old['provenance']['game_assembly_sha256'], 'target_assembly_sha256': current['client_assembly_sha256'], 'capture_sha256': hashlib.sha256(capture.read_bytes()).hexdigest(), 'source': 'Current Client GetHandBookAllRoleConfig DefaultRole, complete existing star configs; no new role ID inferred from avatar arithmetic.', 'entries': [{'previous_role_id': missing[0]['ID'], 'current_role_id': target['ID'], 'variants': rows}]}
(root / 'resources/CurrencyWarsRoleCompatibility.json').write_text(json.dumps(resource, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
+110
View File
@@ -0,0 +1,110 @@
"""Regenerate resources/GachaBannerMapping.json from the live import.
Run from the repository root after the roster changes:
python tools/build_gacha_banner_mapping.py
Stable-ID rules: existing (gacha_id -> avatar_id) pairs are preserved exactly;
only avatars missing from the current mapping receive NEW IDs (max+1). Never
re-sort or renumber, so a banner ID always means the same avatar. The mapping
is the backend half of the custom per-character banner presentation: each
banner ID (900001+) maps to one imported avatar with a configured first Eidolon
cost. Nothing here is an official Client banner row.
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
MAPPING = ROOT / "resources" / "GachaBannerMapping.json"
BANNER_ID_BASE = 900001
def main() -> int:
freesr = json.loads((ROOT / "freesr-data.json").read_text(encoding="utf-8"))
avatars = sorted(int(k) for k in freesr.get("avatars", {}).keys())
eidolon = json.loads(
(ROOT / "resources" / "EidolonConfig.json").read_text(encoding="utf-8")
).get("avatars", {})
catalog = json.loads(
(ROOT / "resources" / "PreviewGameplayCatalog.json").read_text(encoding="utf-8")
)
client_digest = catalog.get("client_assembly_sha256", "")
if not re.fullmatch(r"[0-9a-f]{64}", client_digest) or not catalog.get("evidence_id"):
raise ValueError("Current captured catalog identity/provenance is missing")
captured_ids = {row["GachaID"] for row in catalog.get("tables", {}).get("gacha", [])}
drawable = []
skipped_no_eidolon = []
for avatar in avatars:
row = eidolon.get(str(avatar))
ranks = row.get("ranks", []) if row else []
if not ranks or not ranks[0].get("cost"):
skipped_no_eidolon.append(avatar)
continue
drawable.append(avatar)
existing = {}
if MAPPING.exists():
previous = json.loads(MAPPING.read_text(encoding="utf-8"))
if previous.get("client_assembly_sha256") != client_digest:
raise ValueError("Mapping and current captured catalog belong to different Clients")
known_ids = set()
for banner in previous.get("banners", []):
avatar_id, banner_id = banner["avatar_id"], banner["gacha_id"]
if (
type(avatar_id) is not int or avatar_id <= 0
or type(banner_id) is not int or not BANNER_ID_BASE <= banner_id <= 0xFFFFFFFF
or avatar_id in existing or banner_id in known_ids or banner_id in captured_ids
):
raise ValueError("Duplicate, invalid or colliding existing banner identity")
existing[avatar_id] = banner_id
known_ids.add(banner_id)
used = set(existing.values())
next_id = max(used, default=BANNER_ID_BASE - 1) + 1
banners = []
added = []
# Retain unavailable identities as disabled reservations. Dropping them
# would allow a smaller future roster to reuse an old character's ID.
for avatar in sorted(set(drawable) | set(existing)):
gacha_id = existing.get(avatar)
if gacha_id is None:
while next_id in used or next_id in captured_ids:
next_id += 1
if next_id > 0xFFFFFFFF:
raise ValueError("Custom banner identity namespace exhausted")
gacha_id = next_id
used.add(gacha_id)
next_id += 1
added.append((gacha_id, avatar))
banners.append(
{
"gacha_id": gacha_id,
"avatar_id": avatar,
"roster_available": avatar in drawable,
"provenance": "sandbox custom banner; avatar from imported roster; not official Client banner config",
}
)
banners.sort(key=lambda b: b["gacha_id"])
out = {
"format_version": 1,
"source": "Generated from freesr-data.json avatars + resources/EidolonConfig.json by tools/build_gacha_banner_mapping.py; stable IDs: existing pairs preserved, new avatars get max+1.",
"note": "Custom per-character banner mapping for the custom gacha presentation. Banner IDs use a 900000+ sandbox namespace that cannot collide with the Client captured GachaBasicInfo rows (1001/4001/2001/2002/3001/3002). Draws grant the mapped avatar; duplicates convert at its configured first Eidolon cost; first acquisition of a not-yet-owned avatar is rejected until the avatar acquisition path exists.",
"client_assembly_sha256": client_digest,
"banners": banners,
}
MAPPING.write_text(
json.dumps(out, ensure_ascii=False, indent=1) + chr(10), encoding="utf-8"
)
print(
f"active banners: {len(drawable)} (reserved identities {len(banners)}, kept {len(existing)}, newly assigned {len(added)}, "
f"skipped without eidolon config: {skipped_no_eidolon})"
)
return 0
if __name__ == "__main__":
sys.exit(main())
+94
View File
@@ -0,0 +1,94 @@
"""Render an explicitly staged, resource-fed native Warp adapter capture.
This never modifies scripts/, policy or saves. The operator supplies an output
path, then stages that file only after the walking scene is stable. The default
is one banner. --assets-only checks Exists without creating pools or UI.
"""
import argparse
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def quote(value):
separator = ""
while "]" + separator + "]" in value:
separator += "="
return "[" + separator + "[" + value + "]" + separator + "]"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--limit", type=int, default=1)
parser.add_argument("--assets-only", action="store_true")
args = parser.parse_args()
policy = json.loads((ROOT / "resources/GachaSandboxPolicy.json").read_text(encoding="utf-8"))
catalog = json.loads((ROOT / "resources/PreviewGameplayCatalog.json").read_text(encoding="utf-8"))
mapping = json.loads((ROOT / "resources/GachaBannerMapping.json").read_text(encoding="utf-8"))
if mapping["client_assembly_sha256"] != catalog["client_assembly_sha256"]:
raise ValueError("Mapping/captured catalog identity mismatch")
template_id = policy["native_presentation"]["template_gacha_id"]
template = next(row for row in catalog["tables"]["gacha"] if row["GachaID"] == template_id)
if template["PrefabPath"] not in policy["verified_banner_prefabs"]:
raise ValueError("Template has no installed-asset verification")
avatars = {row["AvatarID"]: row for row in json.loads((ROOT / "resources/AvatarConfig.json").read_text(encoding="utf-8"))["avatar_config"]}
active = [row for row in mapping["banners"] if row.get("roster_available", True)]
roster = json.loads((ROOT / 'freesr-data.json').read_text(encoding='utf-8'))['avatars']
active = [row for row in active if str(row['avatar_id']) in roster]
four = sorted(row['avatar_id'] for row in active if avatars[row['avatar_id']]['Rarity'] == 'CombatPowerAvatarRarityType4')
banners = [row for row in active if avatars[row['avatar_id']]['Rarity'] == 'CombatPowerAvatarRarityType5']
if len(four) < 3:
raise ValueError('Need three actual imported four-star avatars')
if not 1 <= args.limit <= len(banners):
raise ValueError("Limit must select at least one existing mapped banner")
config_name = "__GAC_GACHA_ASSET_CAPTURE_CONFIG" if args.assets_only else "__GAC_GACHA_PRESENTATION"
rules = policy['limited_rules']
payload = ["_G." + config_name + "={enabled=true,template_id=" + str(template_id) + ",template_prefab=" + quote(template["PrefabPath"]) + ",rules={five_star_pity="+str(rules['five_star_pity'])+",four_star_pity="+str(rules['four_star_pity'])+"},banners={"]
for index, banner in enumerate(banners[:args.limit], 1):
row = avatars[banner["avatar_id"]]
featured = [four[(index-1+offset) % len(four)] for offset in range(3)]
cards = ','.join('{avatar_id=%d,portrait_path=%s}' % (id, quote(avatars[id]['AvatarGachaResultImgPath'])) for id in featured)
payload.append("{gacha_id=%d,avatar_id=%d,sort_order=%d,art_path=%s,icon_path=%s,four_stars={%s}}," % (banner["gacha_id"], banner["avatar_id"], index, quote(row["AvatarCutinFrontImgPath"]), quote(row["DefaultAvatarHeadIconPath"]), cards))
payload.append("}}")
adapter = "" if args.assets_only else (ROOT / "internal/gameserver/gacha_presentation.lua").read_text(encoding="utf-8")
digest = hashlib.sha256(("\n".join(payload) + adapter).encode()).hexdigest()
payload.append("_G." + config_name + ".digest=" + quote(digest))
pity = json.loads((ROOT / 'freesr-data.json').read_text(encoding='utf-8')).get('gacha_limited', {})
payload.append('_G.' + config_name + '.pity={since_five_star=%d,since_four_star=%d}' % (pity.get('since_five_star', 0), pity.get('since_four_star', 0)))
if args.assets_only:
payload.append("""
do
local data=_G.__GAC_GACHA_ASSET_CAPTURE_CONFIG
local s=_G.__GAC_GACHA_ART_ASSETS
if not s or s.digest~=data.digest then s={digest=data.digest,index=1};_G.__GAC_GACHA_ART_ASSETS=s end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if phase and not typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then
local ok,err=pcall(function()
for n=1,8 do
local row=data.banners[s.index]
if not row then
if not s.done then CS.UnityEngine.Debug.Log('[GAC_GACHA_ART_ASSET] END count='..#data.banners);s.done=true end
break
end
s.index=s.index+1
local mgr=CS.RPG.Client.GlobalVars.s_AssetMgr
CS.UnityEngine.Debug.Log('[GAC_GACHA_ART_ASSET] banner='..row.gacha_id..' avatar='..row.avatar_id..' art='..tostring(mgr:Exists(row.art_path))..' icon='..tostring(mgr:Exists(row.icon_path)))
end
end)
if not ok then s.done=true;s.index=#data.banners+1;CS.UnityEngine.Debug.Log('[GAC_GACHA_ART_ASSET] ERROR '..tostring(err)) end
end
end
""")
else:
payload.append(adapter)
args.output.write_text("\n".join(payload) + "\n", encoding="utf-8")
print(f"Rendered {args.limit} mapped banners; assets_only={args.assets_only}; output={args.output}")
if __name__ == "__main__":
main()
+494
View File
@@ -0,0 +1,494 @@
"""Package the repository's available Beta data and evidence without running it.
This is an offline handoff builder, not a Client dumper. It never reads saves,
account settings or live process memory. Source files are copied byte-for-byte;
only the separate derived indices normalize large text-hash integers to strings.
"""
import argparse
import csv
import hashlib
import io
import json
import re
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
POLICY_FILES = {
"ActivityPreviewPolicy.json", "GachaSandboxPolicy.json", "AhaHelpConfig.json",
"CurrencyWarsSpotlightPolicy.json", "TeamPresetConfig.json",
}
ROOT_DATA = (
"res.json", "teleports.json", "challenge_data.json", "mission_unlocks.json",
"cmd_ids_4_6.json",
)
DOCS = (
"GLM-BETA-DATA.md", "GLM-BETA-EVENTS.md", "GLM-GACHA-GUIDE.md",
"ZCODE-HANDOFF.md", "offline-gameplay-continuation.md",
"currency-wars-investigation.md", "currency-wars-6900-audit.md",
"currency-wars-coverage.md", "protocol-update.md", "endgame-audit.md",
)
SOURCE_FILES = (
"internal/common/activity_preview.go", "internal/common/gacha_catalog.go",
"internal/common/stella_jump.go", "internal/common/res.go",
"internal/gameserver/handlers_activity_preview.go",
"internal/gameserver/handlers_gacha.go", "internal/gameserver/handlers_stella_jump.go",
"internal/gameserver/handlers_challenge.go", "internal/gameserver/dispatch.go",
)
EVIDENCE_FILES = {
"gacha-main-page.json", "gacha-main-page.luau", "gacha-main-page.hex",
"luau-bytecode.h", "luau-lvmload.cpp", "glm-beta-targeted-metadata.log",
"glm-beta-textid-metadata.log", "glm-gacha-group-enum.log",
"glm-gacha-groups-current.json", "glm-gacha-group-assets.json",
}
# These are config/type metadata captures, not unrestricted Player.log copies.
PREVIEW_CONFIG_LOGS = {
"preview-gameboy-current.log", "preview-metadata-current.log",
"preview-minigame-current.log", "preview-module-fields-current.log",
"preview-modules-current.log",
}
TOOL_PREFIXES = (
"preview_", "gacha_", "gameplay_", "fivedim_", "glm_beta",
"cw_current_", "export_cw_",
)
TOOL_FILES = {
"build_glm_beta_handoff.py", "inspect_luau_v4.py", "check_preview_assets.py",
"build_cw_role_compatibility.py", "disassemble_pe_range.py",
"find_pe_direct_calls.py", "find_pe_rip_references.py",
"cw_config_cache_export.lua", "cw_excel_export.lua",
"sync_protocol_commands.py", "audit_protocol_handlers.py",
}
HANDLERS = {
"GetFiveDimMiniGameData", "GetActiveActivityData", "QuestState",
"GetMultipleDropInfo",
}
def read_json(path):
return json.loads(path.read_text(encoding="utf-8-sig"))
def exact_hashes(value):
"""Avoid JavaScript/CSV consumers rounding 64-bit text hashes in indices."""
if isinstance(value, dict):
return {
key: str(item) if key.lower() == "hash64" and isinstance(item, int)
else exact_hashes(item)
for key, item in value.items()
}
if isinstance(value, list):
return [exact_hashes(item) for item in value]
if isinstance(value, int) and not isinstance(value, bool) and abs(value) > 2**53 - 1:
return str(value)
return value
def declared_hashes(value, prefix=""):
result = {}
if isinstance(value, dict):
for key, item in value.items():
location = f"{prefix}.{key}" if prefix else key
if isinstance(item, str) and "sha256" in key.lower():
result[location] = item
elif isinstance(item, (dict, list)):
result.update(declared_hashes(item, location))
elif isinstance(value, list):
for index, item in enumerate(value):
if isinstance(item, (dict, list)):
result.update(declared_hashes(item, f"{prefix}[{index}]"))
return result
def collection_counts(value):
if isinstance(value, list):
return {"$": len(value)}
if not isinstance(value, dict):
return {}
result = {key: len(item) for key, item in value.items() if isinstance(item, (dict, list))}
for key in ("tables",):
if isinstance(value.get(key), dict):
result.update({f"{key}.{name}": len(rows)
for name, rows in value[key].items()
if isinstance(rows, (dict, list))})
return result
def classify(path, value, client_hash):
name = path.name
if "Compatibility" in name:
return "compatibility-overlays"
if name in POLICY_FILES or name.endswith("Policy.json"):
return "server-policies"
hashes = declared_hashes(value)
assemblies = {item.lower() for key, item in hashes.items() if "assembly" in key.lower()}
if client_hash in assemblies:
return "current-client-captures"
if assemblies:
return "legacy-client-or-reference"
if isinstance(value, dict):
source = value.get("source")
if isinstance(source, dict) and (source.get("repository") or source.get("url")):
return "external-reference-data"
return "unversioned-server-data"
def policy_eligibility(panel, policy, availability):
handler = policy.get(panel.get("UIPrefab", ""), "")
issues = []
if not handler:
issues.append("family_not_enabled_by_policy")
if availability.get(panel.get("UIPrefab", "")) is not True:
issues.append("prefab_missing_or_unobserved")
if availability.get(panel.get("TabIcon", "")) is not True:
issues.append("tab_icon_missing_or_unobserved")
if panel.get("DisplayItemList"):
issues.append("explicit_display_item_dependencies")
return handler, issues
def derive_events(catalog, policy_document, availability_document):
panels = catalog.get("tables", {}).get("panels", [])
activities = catalog.get("tables", {}).get("activities", [])
paths = availability_document.get("paths", {})
policy = {row.get("prefab", ""): row.get("data_handler", "")
for row in policy_document.get("panels", [])}
linked = defaultdict(list)
unresolved = []
known = {row["PanelID"] for row in panels}
for activity in activities:
panel_id = activity.get("ActivityPanelID", 0) or activity.get("ActivityID", 0)
if panel_id in known:
linked[panel_id].append(activity)
else:
unresolved.append({"reason": "effective_panel_not_in_captured_panel_table",
"effective_panel_id": panel_id, "activity": activity})
index, eligible, module_panels = [], {}, defaultdict(set)
for panel in sorted(panels, key=lambda row: row["PanelID"]):
panel_id = panel["PanelID"]
associations = linked[panel_id]
main = sorted({m for a in associations for m in a.get("ActivityModuleIDList", []) if m})
resident = sorted({m for a in associations for m in a.get("ResidentModuleList", []) if m})
prefab = panel.get("UIPrefab", "")
handler, issues = policy_eligibility(panel, policy, paths)
row = {
"panel_id": panel_id,
"technical_prefab_name": Path(prefab).stem,
"name_is_localized": False,
"prefab": prefab,
"tab_icon": panel.get("TabIcon", ""),
"activity_ids": sorted({a.get("ActivityID", 0) for a in associations}),
"main_modules": main,
"resident_modules": resident,
"activity_associations": associations,
"unlock_conditions": panel.get("UnlockConditions", ""),
"finish_conditions": panel.get("FinishConditions", ""),
"finish_type": panel.get("FinishType"),
"prefab_asset_exists": paths.get(prefab),
"tab_icon_asset_exists": paths.get(panel.get("TabIcon", "")),
"display_items": panel.get("DisplayItemList", []),
"text_hashes": {k: panel[k] for k in (
"TabName", "TitleName", "TagDesc", "IntroDesc", "PanelDesc", "PanelBrief") if k in panel},
"policy_family": prefab if handler else None,
"data_handler": handler or None,
"preview_panel_eligible": not issues,
"preview_exclusion_reasons": issues,
"has_activity_association": bool(associations),
"gameplay_support": "not_established_by_catalog_or_asset_presence",
}
index.append(row)
if not issues:
eligible[panel_id] = (handler, prefab)
for module_id in main + resident:
module_panels[module_id].add(panel_id)
errors = []
if not policy_document.get("source"):
errors.append("activity preview policy missing")
if not catalog.get("evidence_id"):
errors.append("activity preview catalog provenance missing")
if not availability_document.get("source") or not isinstance(paths, dict):
errors.append("event asset verification missing")
for handler in policy.values():
if handler not in HANDLERS:
errors.append(f"unsupported preview data handler {handler}")
candidates, conflicts = [], []
for module_id, panel_ids in sorted(module_panels.items()):
panel_ids = sorted(panel_ids)
signatures = {eligible[p] for p in panel_ids}
if len(signatures) != 1:
conflicts.append({"module_id": module_id, "panel_ids": panel_ids,
"signatures": [{"panel_id": p, "data_handler": eligible[p][0],
"prefab": eligible[p][1]} for p in panel_ids]})
else:
candidates.append({"module_id": module_id, "panel_id": panel_ids[0],
"data_handler": eligible[panel_ids[0]][0],
"equivalent_candidate_panel_ids": panel_ids})
if conflicts:
errors.extend(f"activity module {c['module_id']} maps to conflicting panels {c['panel_ids']}"
for c in conflicts)
if policy and not module_panels:
errors.append("no supported activity-preview modules in captured catalog")
schedules = {
"semantics": "Mirror internal/common/activity_preview.go: filter eligible panels; sort module IDs and panel IDs; require identical handler+prefab for every candidate of each module; reject the whole result on any conflict.",
"status": "error" if errors else "ready",
"errors": errors,
"conflicts": conflicts,
"schedules": [] if errors else candidates,
"compatible_candidates_for_diagnosis_only": candidates if errors else [],
"not_a_gameplay_confirmation": True,
}
return exact_hashes(index), exact_hashes(unresolved), schedules
def derive_gacha(catalog, policy, group_document, group_assets):
observations = policy.get("asset_verification", {})
verified_paths = set(policy.get("verified_banner_prefabs", []))
rows = []
for row in catalog.get("tables", {}).get("gacha", []):
prefab = row.get("PrefabPath", "")
observed = None
basename = Path(prefab).name
if basename.startswith("StandardGacha_"):
observed = observations.get("standard")
elif basename.startswith("NoviceGacha_"):
observed = observations.get("novice")
elif "/GachaPanelLimited/" in prefab:
observed = observations.get("captured_limited_character_and_lightcone_prefabs")
rows.append({"gacha_id": row.get("GachaID"), "gacha_type": row.get("GachaType"),
"prefab": prefab, "asset_verified": observed,
"browse_enabled_by_policy": prefab in verified_paths,
"original_row": row})
group_note = "No supplemental current group-table capture is present in this bundle."
if group_document is not None:
group_note = "Use the supplemental capture's declared completion/count. A completed zero-row table is observed empty, not a missing capture. Loaded generic Lua presentation classes do not establish GroupIDs."
return exact_hashes({
"captured_basic_rows": rows,
"asset_verification_source": observations,
"group_table_capture": group_document,
"generic_group_prefab_asset_observations": group_assets,
"group_capture_note": group_note,
"limitation": "Catalog rows, prefab existence and server browse policy do not establish usable drawing, presentation binding or one banner for every avatar.",
})
class Bundle:
def __init__(self, output):
self.output = output
self.entries = []
def add(self, relative, data, provenance, source_relative=None, metadata=None):
target = self.output / relative
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("xb") as stream:
stream.write(data)
entry = {"source_relative": source_relative, "bundle_relative": relative,
"sha256": hashlib.sha256(data).hexdigest(), "size_bytes": len(data),
"provenance_class": provenance}
if metadata:
entry.update(metadata)
self.entries.append(entry)
def json(self, relative, value, provenance="derived-index"):
data = (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
self.add(relative, data, provenance)
def copy(self, root, source, relative, provenance):
source = source.resolve()
source_relative = source.relative_to(root).as_posix()
data = source.read_bytes()
metadata = None
if source.suffix == ".json":
try:
value = json.loads(data.decode("utf-8-sig"))
metadata = {"declared_hashes": declared_hashes(value),
"collection_counts": collection_counts(value)}
except (ValueError, UnicodeError):
metadata = {"json_parse_status": "invalid_or_non_utf8; original bytes retained"}
self.add(relative, data, provenance, source_relative, metadata)
def build(root, output, client_hash, client_label):
root = root.resolve()
output = output.resolve()
archive = output.with_name(output.name + ".zip")
if output.exists() or archive.exists():
raise ValueError("Output directory and adjacent zip must both be new; existing paths are never overwritten.")
if not (root / "AGENTS.md").is_file() or not (root / "resources").is_dir():
raise ValueError("Root does not contain the expected repository structure.")
required = [root / "resources" / name for name in (
"PreviewGameplayCatalog.json", "ActivityPreviewPolicy.json",
"PreviewAssetAvailability.json", "GachaSandboxPolicy.json")]
# The derived indices and their copied raw inputs share the exact same bytes,
# even if another editor changes one of these files during bundle creation.
input_bytes = {path: path.read_bytes() for path in required}
catalog, policy, availability, gacha_policy = [
json.loads(input_bytes[path].decode("utf-8-sig")) for path in required]
if catalog.get("client_assembly_sha256", "").lower() != client_hash:
raise ValueError("Requested Client SHA differs from the captured preview catalog.")
if availability.get("client_assembly_sha256", "").lower() != client_hash:
raise ValueError("Requested Client SHA differs from the asset availability observations.")
output.mkdir(parents=True, exist_ok=False)
bundle = Bundle(output)
for source in sorted((root / "resources").glob("*.json")):
raw = input_bytes.get(source)
if raw is None:
raw = source.read_bytes()
value = json.loads(raw.decode("utf-8-sig"))
category = classify(source, value, client_hash)
bundle.add(f"data/{category}/resources/{source.name}", raw, category,
source.relative_to(root).as_posix(),
{"declared_hashes": declared_hashes(value), "collection_counts": collection_counts(value)})
for name in ROOT_DATA:
source = root / name
if source.is_file():
category = "protocol-reference" if name == "cmd_ids_4_6.json" else "unversioned-server-data"
bundle.copy(root, source, f"data/{category}/root/{name}", category)
proto_root = root / "protocol" / "proto"
for source in sorted(proto_root.iterdir()):
if source.suffix == ".proto" or (source.suffix == ".go" and not source.name.endswith(".pb.go")):
bundle.copy(root, source, f"protocol/{source.name}", "protocol-source-revision-not-full-client-dump")
bundle.copy(root, root / "AGENTS.md", "AGENTS.md", "repository-instructions")
for name in DOCS:
source = root / "docs" / name
if source.is_file():
bundle.copy(root, source, f"docs/{name}", "chronological-project-documentation")
for name in SOURCE_FILES:
source = root / name
if source.is_file():
bundle.copy(root, source, f"source/{name}", "selected-current-source; not gameplay verification")
tools_root = root / "tools"
for source in sorted(tools_root.iterdir()):
if (source.is_file() and source.suffix in {".py", ".lua", ".js", ".go"}
and (source.name in TOOL_FILES or source.name.startswith(TOOL_PREFIXES))
and not source.name.startswith("test_") and not source.name.endswith("_test.go")):
bundle.copy(root, source, f"tools/{source.name}", "offline-or-runtime-research-tool; not executed by bundle")
evidence_root = root / "battle_diagnostics"
if evidence_root.is_dir():
for source in sorted(evidence_root.iterdir()):
selected = (source.name in EVIDENCE_FILES or source.name in PREVIEW_CONFIG_LOGS
or (source.suffix == ".asm" and source.name.startswith(("preview-", "glm-"))))
if source.is_file() and selected:
bundle.copy(root, source, f"evidence/{source.name}", "selected-config-research-evidence")
group_file = evidence_root / "glm-gacha-groups-current.json"
group_asset_file = evidence_root / "glm-gacha-group-assets.json"
group_document = read_json(group_file) if group_file.is_file() else None
group_assets = read_json(group_asset_file) if group_asset_file.is_file() else None
event_index, unresolved, schedules = derive_events(catalog, policy, availability)
bundle.json("derived/event-index.json", event_index)
bundle.json("derived/unresolved-activities.json", unresolved)
bundle.json("derived/preview-schedules.json", schedules)
bundle.json("derived/gacha-index.json", derive_gacha(catalog, gacha_policy, group_document, group_assets))
csv_stream = io.StringIO(newline="")
columns = list(event_index[0]) if event_index else ["panel_id"]
writer = csv.DictWriter(csv_stream, fieldnames=columns)
writer.writeheader()
for row in event_index:
writer.writerow({key: json.dumps(value, ensure_ascii=False) if isinstance(value, (dict, list)) else value
for key, value in row.items()})
bundle.add("derived/event-index.csv", csv_stream.getvalue().encode("utf-8-sig"), "derived-index")
tables = catalog.get("tables", {})
classes = Counter(entry["provenance_class"] for entry in bundle.entries)
readme = f"""# Beta data handoff for GLM
Client label supplied by operator: {client_label}
Current assembly SHA-256: `{client_hash}`
## Scope
This bundle contains **all currently available resource JSON files in this checkout**, selected root data,
protocol metadata, research tools and bounded config evidence. It is **not a dump of all Beta game data**.
No game asset bundles, native executable, account settings, `persistent`, `freesr-data.json`,
`server.local.json` or unrestricted player/server logs are included. Tools are copied, never executed.
Captured current preview tables: {', '.join(f'{name}={len(rows)}' for name, rows in tables.items())}.
Event presence observations: {len(availability.get('paths', {}))} paths.
Event policy families: {len(policy.get('panels', []))}.
Event index: {len(event_index)} captured panels; unresolved activity associations: {len(unresolved)}.
Preview schedule derivation status: **{schedules['status']}**; publishable schedule count: {len(schedules['schedules'])}.
## Start here
- `derived/event-index.json` / CSV: technical prefab names, ActivityID-to-PanelID associations,
main/resident modules, raw unlock/finish conditions, item dependencies, observed assets and policy eligibility.
Technical names are **not localized event titles**. Text hashes are retained and 64-bit values are decimal strings.
- `derived/unresolved-activities.json`: ActivityConfig rows whose effective panel is absent from the captured panel table.
- `derived/preview-schedules.json`: deterministic server eligibility/conflict rules from `internal/common/activity_preview.go`.
A conflict rejects the entire Go schedule result; diagnostic candidates are not published partial success.
- `derived/gacha-index.json`: six captured basic rows when present in the source catalog, observed asset presence,
policy browsing status and supplemental current group-table capture/prefab observations when supplied.
- The current group table's explicit completed zero-row capture, if present, means **observed empty**;
generic Lua presentation classes do not supply missing GroupIDs or prove an installed generic prefab.
- `docs/ZCODE-HANDOFF.md` and other documents retain chronology. Some deployment/gameplay status may be superseded.
- `docs/GLM-BETA-DATA.md`, `docs/GLM-BETA-EVENTS.md` and `docs/GLM-GACHA-GUIDE.md` contain the current Thai handoff.
- `source/internal/` includes selected event/gacha/entry handler source for comparison; it is not the whole buildable repository.
## Provenance and limitations
- `data/current-client-captures`: an explicit matching assembly hash is present. Coverage is only the rows/tables declared.
- `data/compatibility-overlays`: cross-revision/exclusion mappings, not a recaptured full ruleset.
- `data/server-policies`: operator-defined sandbox choices, not captured official rates or gameplay guarantees.
- `data/legacy-client-or-reference`: explicit different assembly provenance; do not relabel as the current Client.
- `data/external-reference-data`: external repository/dataset sources.
- `data/unversioned-server-data`: no matching assembly proof was found. Presence or successful server use does not establish revision.
- Protocol files retain their own source revision; this bundle does not certify the full protocol against this Client.
- Raw copied files preserve original bytes and numeric JSON tokens. Derived indices stringify 64-bit hashes for readers
that would otherwise round JSON numbers. Never reserialize raw game tables through floating-point JavaScript numbers.
- Asset existence is not gameplay support. Do not expose every historical panel with empty success replies.
- Validate current process/module identity before any runtime investigation. Never reuse old session pointers.
- Keep runtime probes outside `scripts/` when inactive, bound work per frame and avoid broad scene/assembly scans.
## Manifest
`manifest.json` records source-relative path, bundle path, SHA-256, size, classification, declared hashes and collection counts.
It deliberately does not hash itself or the zip, avoiding recursive hashes. The zip contains the same bundle contents.
Files by category: {json.dumps(dict(sorted(classes.items())), ensure_ascii=False)}.
Build and Client gameplay success are separate. Follow AGENTS.md and the user's current authorization before running tests,
deploying or interacting with a live game. Creating this bundle does not run tests or change the server.
"""
bundle.add("README-GLM.md", readme.encode("utf-8"), "handoff-documentation")
manifest = {
"format_version": 1,
"created_at_utc": datetime.now(timezone.utc).isoformat(),
"client_label": client_label,
"client_assembly_sha256": client_hash,
"scope": "all currently available repository resources plus explicitly selected supporting data; not all Beta Client data",
"files": sorted(bundle.entries, key=lambda entry: entry["bundle_relative"]),
"manifest_excludes_own_hash_and_archive_hash": True,
}
with (output / "manifest.json").open("x", encoding="utf-8", newline="\n") as stream:
json.dump(manifest, stream, ensure_ascii=False, indent=2)
stream.write("\n")
with ZipFile(archive, "x", compression=ZIP_DEFLATED) as zipped:
for source in sorted(output.rglob("*")):
if source.is_file():
zipped.write(source, arcname=f"{output.name}/{source.relative_to(output).as_posix()}")
return output, archive, len(bundle.entries) + 1
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, required=True, help="New bundle directory; an adjacent .zip is also created.")
parser.add_argument("--client-sha256", required=True)
parser.add_argument("--client-label", required=True)
args = parser.parse_args()
client_hash = args.client_sha256.lower()
if not re.fullmatch(r"[0-9a-f]{64}", client_hash):
parser.error("--client-sha256 must be exactly 64 hexadecimal characters")
if not args.client_label.strip() or any(c in args.client_label for c in "\r\n"):
parser.error("--client-label must be a nonempty single line")
try:
output, archive, count = build(args.root, args.output, client_hash, args.client_label.strip())
except (OSError, ValueError, KeyError) as error:
parser.exit(1, f"Handoff build failed: {error}\nExisting paths were not deleted; inspect any newly created partial output before choosing another output.\n")
print(f"Created {count} files: {output}")
print(f"Created archive: {archive}")
print("No Client probes, tests, save writes or server changes were performed.")
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
"""Prepare a bounded, inactive Client audit from the actual outgoing ID catalog."""
import argparse
import hashlib
import json
from pathlib import Path
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--catalog', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
raw = args.catalog.read_bytes()
ids = sorted(set(json.loads(raw)['subMissionIds']))
if not ids or any(not isinstance(i, int) or not 0 < i < 2**32 for i in ids):
raise SystemExit('Invalid outgoing mission catalog')
ranges = []
for i in ids:
if ranges and ranges[-1][1] + 1 == i:
ranges[-1][1] = i
else:
ranges.append([i, i])
source = '''-- Temporary: read-only lookup of outgoing sub-missions, at most four per frame.
do
if not _G.__gacMissionAudit1 then
local s={range=1,done=false,checked=0,valid=0,missing={},frame=-1}
_G.__gacMissionAudit1=s
local ranges=__RANGES__
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_MISSION_AUDIT] '..tostring(v))end
log('BEGIN expected=__COUNT__ catalog_sha256=__HASH__')
s.tick=function()
if s.done then return end
local frame=CS.UnityEngine.Time.frameCount
if s.frame==frame then return end
s.frame=frame
local good,err=pcall(function()
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if phase and typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.MissionModule
local start=CS.UnityEngine.Time.realtimeSinceStartup
for n=1,4 do
local r=ranges[s.range]
if not r then
s.done=true
log('END checked='..s.checked..' valid='..s.valid..' missing='..table.concat(s.missing,','))
return
end
local id=s.next or r[1]
local cfg=m:GetSubMissionInfoConfig(id)
if cfg then s.valid=s.valid+1 else s.missing[#s.missing+1]=tostring(id)end
s.checked=s.checked+1
if id==r[2]then s.range=s.range+1;s.next=nil else s.next=id+1 end
if s.checked%1000==0 then log('PROGRESS checked='..s.checked..' valid='..s.valid)end
if CS.UnityEngine.Time.realtimeSinceStartup-start>0.002 then break end
end
end)
if not good then s.done=true;log('ERROR checked='..s.checked..' '..tostring(err))end
end
local ok,e=pcall(function()CS.UnityEngine.Application.onBeforeRender('+',s.tick)end)
if not ok then s.done=true;log('ERROR '..tostring(e))end
end
end
'''
source = source.replace('__RANGES__', '{' + ','.join('{%d,%d}' % tuple(r) for r in ranges) + '}')
source = source.replace('__COUNT__', str(len(ids))).replace('__HASH__', hashlib.sha256(raw).hexdigest())
args.output.write_text(source, encoding='utf-8')
print(f'Prepared {len(ids)} IDs in {len(ranges)} ranges; {len(source)} bytes')
+32
View File
@@ -0,0 +1,32 @@
// Passive capture only. Supply GAC_NAMED_BAKED_CAPTURE={output_directory, modules}
// in the caller's private prelude. Each entry is {module, file}; filenames must
// be plain basenames. This does not load or execute any Lua closure itself.
const plan = globalThis.GAC_NAMED_BAKED_CAPTURE;
if (!plan || typeof plan.output_directory !== 'string' || !Array.isArray(plan.modules) ||
plan.modules.length < 1 || plan.modules.length > 5) throw Error('Exact bounded capture plan required');
const requested = new Map();
for (const entry of plan.modules) {
if (!/^Ui\.[A-Za-z0-9_.]+$/.test(entry.module) || !/^[a-z0-9_-]+\.luau$/.test(entry.file))
throw Error('Invalid exact module or output basename');
requested.set('@BakedLua/' + entry.module.replace(/\./g, '/') + '.bytes', entry.file);
}
const module = Process.getModuleByName('xluau.dll');
const load = module.getExportByName('xluaL_loadbuffer');
const completed = new Set();
Interceptor.attach(load, {
onEnter(args) {
try {
const name = args[3].readUtf8String();
if (!requested.has(name) || completed.has(name)) return;
const size = args[2].toUInt32();
if (size < 4 || size > 131072) throw Error('Named chunk outside capture size bound');
const bytes = args[1].readByteArray(size);
const path = plan.output_directory.replace(/[\\/]$/, '') + '/' + requested.get(name);
const output = new File(path, 'wb');
try { output.write(bytes); output.flush(); } finally { output.close(); }
completed.add(name);
send({kind:'CAPTURED', module:name, path, bytes:size});
} catch (error) { send({kind:'ERROR', error:String(error)}); }
}
});
send({kind:'ATTACHED', modules:Array.from(requested.keys()), policy:'passive exact-name loadbuffer capture; no closure execution'});
+24
View File
@@ -0,0 +1,24 @@
"""Prepare a bounded, read-only presence query for captured event paths."""
import json
from pathlib import Path
root=Path(__file__).resolve().parents[1]
c=json.loads((root/'resources/PreviewGameplayCatalog.json').read_text(encoding='utf-8-sig'))
paths=sorted({p[k] for p in c['tables']['panels'] for k in ('UIPrefab','TabIcon') if p.get(k)})
source='''-- Asset manifest presence only, eight captured paths per heartbeat; no loading.
do local function capture()
local s=_G.__gacEventAssets1 or {index=1};_G.__gacEventAssets1=s
local paths=PATHS
if s.index>#paths then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
for n=1,8 do
if s.index>#paths then CS.UnityEngine.Debug.Log('[GAC_EVENT_ASSET] END');return end
local path=paths[s.index];s.index=s.index+1
CS.UnityEngine.Debug.Log('[GAC_EVENT_ASSET] '..tostring(CS.RPG.Client.GlobalVars.s_AssetMgr:Exists(path))..' '..path)
end
end;local ok,e=pcall(capture);if not ok then _G.__gacEventAssets1={index=99999};CS.UnityEngine.Debug.Log('[GAC_EVENT_ASSET] ERROR '..tostring(e))end end
'''.replace('PATHS','{'+','.join(json.dumps(p)for p in paths)+'}')
(root/'tools/event_asset_presence.lua').write_text(source,encoding='utf-8')
print('Captured paths:',len(paths))
+31
View File
@@ -0,0 +1,31 @@
-- Exact live ComicDessert services, bounded metadata only.
do
local function capture()
local s=_G.__gacComicServicesMeta1 or {index=1,offset=0,kind='fields'}
_G.__gacComicServicesMeta1=s
local names={'NetworkService','CycleConfigRepository','LevelConfigRepository','FeatureUnlockService','ProgressDomainService'}
if s.index>#names then return end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.ComicDessertModule
if not module then return end
local value=module[names[s.index]]
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_COMIC_SERVICES_META] '..tostring(v))end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
if not value then log('MISSING '..names[s.index]);s.index=s.index+1;return end
if not s.t then s.t=value:GetType();s.members=s.t:GetFields(flags);log('TYPE '..names[s.index]..' '..s.t.FullName)end
for n=1,16 do
if s.offset>=s.members.Length then
if s.kind=='fields'then s.kind='methods';s.members=s.t:GetMethods(flags);s.offset=0;return end
s.index=s.index+1;s.t=nil;s.members=nil;s.offset=0;s.kind='fields'
if s.index>#names then log('END')end
return
end
local m=s.members[s.offset];s.offset=s.offset+1
if m.DeclaringType==s.t then
if s.kind=='fields'then log('FIELD '..m.Name..' '..m.FieldType.FullName)
else log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
end
end
local ok,e=pcall(capture)
if not ok then _G.__gacComicServicesMeta1={index=99};CS.UnityEngine.Debug.Log('[GAC_COMIC_SERVICES_META] ERROR '..tostring(e))end
end
+41
View File
@@ -0,0 +1,41 @@
// Current .54 SHA 4aa08a0706f27d2cf9951b61920c179140ece9ea94f7e34422c18e46922939b6.
// Exact Row.FromBinary -> Add -> lazy GetAll chain in comic-*-current.asm.
// Only two flat config getters; no repository/scene/gameplay constructors.
const module = Process.getModuleByName('GameAssembly.dll');
const tables = [['cycle', 0x1eafb230], ['level', 0x1e2556c0]];
for (const [name,rva] of tables) {
if (module.base.add(rva).readU32()!==0x28ec8348) throw Error('Getter differs: '+name);
}
const serializer=module.base.add(0x1d46c040);
const serialize=new NativeFunction(serializer,'pointer',['pointer','pointer']);
const marker='GAC_COMIC_FLAT_TICK';
let table=0,index=0,count=0,entries=null,done=false,busy=false;
Interceptor.attach(serializer,{
onEnter(args){
this.trigger=false;if(done||busy)return;
try{this.trigger=args[0].add(0x10).readS32()===marker.length&&args[0].add(0x14).readUtf16String(marker.length)===marker;}catch(_){}
},
onLeave(){
if(!this.trigger||done||busy)return;busy=true;
try{
if(entries===null){
const get=new NativeFunction(module.base.add(tables[table][1]),'pointer',['pointer']);
const dict=get(ptr(0));if(dict.isNull())throw Error('Missing dictionary');
count=dict.add(0x20).readU32();entries=dict.add(0x18).readPointer();index=0;
if(count>256||count>0&&(entries.isNull()||entries.add(0x18).readU32()<count))throw Error('Dictionary bound');
send({kind:'BEGIN',table:tables[table][0],count});
}
const rows=[];
for(let n=0;n<4&&index<count;n++){
const entry=entries.add(0x20+24*index++);if(entry.readS32()<0)continue;
const row=entry.add(16).readPointer();if(row.isNull())continue;
const value=serialize(row,ptr(0));if(value.isNull())throw Error('Missing serialization');
const length=value.add(0x10).readS32();if(length<2||length>32768)throw Error('Row size bound');
rows.push(value.add(0x14).readUtf16String(length));
}
send({kind:'ROWS',table:tables[table][0],rows});
if(index>=count){send({kind:'TABLE_END',table:tables[table][0],count});table++;entries=null;if(table===tables.length){done=true;send({kind:'END'});}}
}catch(e){done=true;send({kind:'ERROR',error:String(e)});}finally{busy=false;}
}
});
send({kind:'ATTACHED',scope:'two exact lazy flat getters, four rows per marker heartbeat',module:module.path});
+48
View File
@@ -0,0 +1,48 @@
-- Exact existing ComicDessert services/Start response only; eight members per
-- non-battle heartbeat. No Start request, callback, constructor or scene call.
do local function capture()
local s=_G.__gacComicStartMetadata1 or {target=1,index=0};_G.__gacComicStartMetadata1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local gm=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=gm and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(gm)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.ComicDessertModule
if not module then return end
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_COMIC_START_METADATA] '..tostring(v))end
if not s.t then
if s.target==1 then
local service=module.NetworkService;s.t=service and service:GetType()
elseif s.target==2 then
local service=module.LevelStartFlowService;s.t=service and service:GetType()
if s.t then
s.nested=s.t:GetNestedTypes(flags)
if s.nested.Length>32 then error('exact start-flow nested-type bound')end
end
elseif s.target==3 then
local network=module.NetworkService
local method=network and network:GetType():GetMethod('OFHMHPKEECI',flags)
local args=method and method.ReturnType:GetGenericArguments()
s.t=args and args.Length==1 and args[0] or nil
s.properties=true
elseif s.nested and s.target-4<s.nested.Length then
s.t=s.nested[s.target-4]
else s.done=true;log('END');return end
if not s.t then log('MISSING_TARGET '..s.target);s.target=s.target+1;return end
log('TYPE '..s.target..' '..s.t.FullName)
s.members=s.properties and s.t:GetProperties(flags) or s.t:GetMethods(flags)
s.index=0
end
for n=1,8 do
if s.index>=s.members.Length then s.target=s.target+1;s.t=nil;s.members=nil;s.properties=false;return end
local member=s.members[s.index];s.index=s.index+1
if member.DeclaringType==s.t then
if s.properties then
local getter=member:GetGetMethod(true)
log('PROPERTY '..member.Name..' '..member.PropertyType.FullName..' GETTER '..tostring(getter and getter.MethodHandle.Value))
else
log('METHOD '..tostring(member)..' HANDLE '..tostring(member.MethodHandle.Value))
end
end
end
end local ok,e=pcall(capture);if not ok then _G.__gacComicStartMetadata1={done=true};CS.UnityEngine.Debug.Log('[GAC_COMIC_START_METADATA] ERROR '..tostring(e))end end
+16
View File
@@ -0,0 +1,16 @@
-- Exact constructed progress services; bounded metadata, no getters with work.
do local function capture()
local s=_G.__gacComicProgressMeta1 or {target=1,index=0};_G.__gacComicProgressMeta1=s
local names={'Progress','ProgressService','LongTailService'}
if s.target>#names then return end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.ComicDessertModule
local value=module and module[names[s.target]];if not value then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_COMIC_PROGRESS_META] '..tostring(v))end
if not s.t then s.t=value:GetType();s.members=s.t:GetMethods(flags);s.index=0;log('TYPE '..names[s.target]..' '..s.t.FullName)end
for n=1,8 do
if s.index>=s.members.Length then s.target=s.target+1;s.t=nil;s.members=nil;return end
local m=s.members[s.index];s.index=s.index+1
if m.DeclaringType==s.t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
end local ok,e=pcall(capture);if not ok then _G.__gacComicProgressMeta1={target=99};CS.UnityEngine.Debug.Log('[GAC_COMIC_PROGRESS_META] ERROR '..tostring(e))end end
+31
View File
@@ -0,0 +1,31 @@
-- Read exact repository inheritance; private base fields are not returned by
-- GetField on the concrete type. No repository initialization or constructors.
do local function capture()
local s=_G.__gacComicRepoHierarchy1 or {target=1,index=0};_G.__gacComicRepoHierarchy1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),62)
local names={'CycleConfigRepository','LevelConfigRepository'}
if s.target>#names then s.done=true;return end
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ComicDessertModule
local value=m and m[names[s.target]];if not value then return end
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_COMIC_REPO_HIERARCHY] '..tostring(v))end
if not s.t then s.t=value:GetType()end
if not s.fields then s.fields=s.t:GetFields(flags);s.index=0;log('TYPE '..names[s.target]..' '..s.t.FullName)end
for n=1,8 do
if s.index>=s.fields.Length then
s.t=s.t.BaseType;s.fields=nil
if not s.t or s.t.FullName=='System.Object'then s.target=s.target+1;s.t=nil end
return
end
local f=s.fields[s.index];s.index=s.index+1
if not f.IsStatic then
local v=f:GetValue(value)
log('FIELD '..f.Name..' TYPE '..f.FieldType.FullName..' NULL '..tostring(v==nil))
if v and f.FieldType.FullName:find('System.Collections.Generic.Dictionary',1,true) then
log('DICTIONARY '..names[s.target]..' FIELD '..f.Name..' COUNT '..v.Count)
_G.__GAC_COMIC_CAPTURE_REPOSITORIES=_G.__GAC_COMIC_CAPTURE_REPOSITORIES or {}
_G.__GAC_COMIC_CAPTURE_REPOSITORIES[names[s.target]]=v
end
end
end
end local ok,e=pcall(capture);if not ok then _G.__gacComicRepoHierarchy1={done=true};CS.UnityEngine.Debug.Log('[GAC_COMIC_REPO_HIERARCHY] ERROR '..tostring(e))end end
+80
View File
@@ -0,0 +1,80 @@
-- Offline-analysis data capture only; do not keep in the heartbeat scripts/.
-- Reads already constructed repositories. Never initializes a repository,
-- invokes a gameplay method, creates a scene, or changes live state.
do
local function capture()
local s=_G.__gacComicStellaConfig1 or {stage=1,row=0}
_G.__gacComicStellaConfig1=s
if s.stage>5 then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
if not manager then return end
local phase=typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then return end
local modules=CS.RPG.Client.GlobalVars.s_ModuleManager
local comic=modules and modules.ComicDessertModule
if not comic then return end
local log=function(v) CS.UnityEngine.Debug.Log('[GAC_COMIC_STELLA_CONFIG] '..tostring(v)) end
local function json(v)
local text=CS.Newtonsoft.Json.JsonConvert.SerializeObject(v)
if #text>32768 then error('single configuration exceeds 32768-byte output bound') end
return text
end
if s.stage==1 then
-- Exact existing named method handles, no assembly enumeration.
local t=comic:GetType()
for _,name in ipairs({'_OnGetDataSuccess','_SetUpProgress','_SetUpUpgradeTree'}) do
local method=t:GetMethod(name,flags)
if method then log('METHOD '..t.FullName..' '..tostring(method)..' HANDLE '..tostring(method.MethodHandle.Value)) end
end
local network=comic.NetworkService
local method=network and network:GetType():GetMethod('NPFCAEIAFOM',flags)
if method then log('METHOD '..network:GetType().FullName..' '..tostring(method)..' HANDLE '..tostring(method.MethodHandle.Value)) end
s.stage=2;return
end
if s.stage==2 then
-- Callback parameter supplies the true response type, avoiding xLua placeholders.
local method=comic:GetType():GetMethod('_OnGetDataSuccess',flags)
local parameters=method:GetParameters()
local t=parameters[0].ParameterType
log('RESPONSE_TYPE '..t.FullName)
s.fields=t:GetFields(flags);s.index=0;s.stage=3;return
end
if s.stage==3 then
for n=1,8 do
if s.index>=s.fields.Length then s.fields=nil;s.stage=4;return end
local f=s.fields[s.index];s.index=s.index+1
log('RESPONSE_FIELD '..f.Name..' '..f.FieldType.FullName)
end
return
end
if s.stage==4 or s.stage==5 then
local name=s.stage==4 and 'CycleConfigRepository' or 'LevelConfigRepository'
local repository=comic[name]
if not repository then log('MISSING '..name);s.stage=s.stage+1;return end
if not s.enumerator then
local t=repository:GetType()
local f=t:GetField('BDBOGJHLEIG',flags)
local dictionary=f and f:GetValue(repository)
if not dictionary then log('MISSING_DICTIONARY '..name);s.stage=s.stage+1;return end
if dictionary.Count>256 then error('repository row bound') end
log('BEGIN '..name..' count='..dictionary.Count)
s.enumerator=dictionary:GetEnumerator();s.row=0
end
if s.enumerator:MoveNext() then
local pair=s.enumerator.Current;s.row=s.row+1
log('ROW '..name..' '..json(pair.Value))
else
s.enumerator:Dispose();s.enumerator=nil
log('END_REPOSITORY '..name..' rows='..s.row);s.stage=s.stage+1
if s.stage>5 then log('END') end
end
return
end
end
local ok,e=pcall(capture)
if not ok then
_G.__gacComicStellaConfig1={stage=99}
CS.UnityEngine.Debug.Log('[GAC_COMIC_STELLA_CONFIG] ERROR '..tostring(e))
end
end
+65
View File
@@ -0,0 +1,65 @@
-- Exact named rows/protobuf properties only, four members per heartbeat.
-- No recursive JsonConvert/config serialization or gameplay calls.
do
local function capture()
local s=_G.__gacComicStellaSchema1 or {target=1,index=0,kind='fields'}
_G.__gacComicStellaSchema1=s
if s.target>4 then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
if not manager then return end
local phase=typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then return end
local modules=CS.RPG.Client.GlobalVars.s_ModuleManager
local log=function(v) CS.UnityEngine.Debug.Log('[GAC_COMIC_STELLA_SCHEMA] '..tostring(v)) end
if not s.t then
if s.target<=2 then
local name=s.target==1 and 'RPG.GameCore.ComicDessertCycleRow' or 'RPG.GameCore.ComicDessertLevelRow'
s.t=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly:GetType(name)
elseif s.target==3 then
local m=modules.ComicDessertModule:GetType():GetMethod('_OnGetDataSuccess',flags)
s.t=m:GetParameters()[0].ParameterType;s.kind='properties'
else
local five=modules.FiveDimModule
local cfg=five:GetType():GetField('_GameConfig',flags):GetValue(five)
s.value=cfg and cfg:GetType():GetField('PlayerConfig',flags):GetValue(cfg)
s.t=s.value and s.value:GetType()
end
if not s.t then log('MISSING_TARGET '..s.target);s.target=s.target+1;return end
log('TYPE '..s.t.FullName)
s.members=s.kind=='properties' and s.t:GetProperties(flags) or s.t:GetFields(flags)
s.index=0
end
for n=1,4 do
if s.index>=s.members.Length then
if s.target<=2 and s.kind=='fields' then
local method=s.t:GetMethod('FromBinary',flags)
if method then log('METHOD '..s.t.FullName..' '..tostring(method)..' HANDLE '..tostring(method.MethodHandle.Value)) end
end
log('END_TYPE '..s.t.FullName)
s.target=s.target+1;s.t=nil;s.members=nil;s.value=nil;s.index=0;s.kind='fields'
if s.target>4 then log('END') end
return
end
local member=s.members[s.index];s.index=s.index+1
if member.DeclaringType==s.t then
if s.kind=='properties' then
local getter=member:GetGetMethod(true)
log('PROPERTY '..member.Name..' '..member.PropertyType.FullName..' GETTER '..(getter and tostring(getter.MethodHandle.Value) or 'nil'))
else
local kind=member.FieldType.FullName
local text='FIELD '..member.Name..' '..kind
if s.value and (kind=='System.UInt32' or kind=='System.Int32' or kind=='System.Single' or kind=='System.Boolean' or kind=='System.String') then
text=text..' VALUE '..tostring(member:GetValue(s.value))
end
log(text)
end
end
end
end
local ok,e=pcall(capture)
if not ok then
_G.__gacComicStellaSchema1={target=99}
CS.UnityEngine.Debug.Log('[GAC_COMIC_STELLA_SCHEMA] ERROR '..tostring(e))
end
end
+18
View File
@@ -0,0 +1,18 @@
-- Named flat configuration tables only; no scene/graph loading.
do local function capture()
local s=_G.__gacComicFlatMeta1 or {target=1,index=0};_G.__gacComicFlatMeta1=s
local names={'RPG.GameCore.ComicDessertCycleExcelTable','RPG.GameCore.ComicDessertLevelExcelTable'}
if s.target>#names then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_COMIC_FLAT_META] '..tostring(v))end
if not s.t then
s.t=typeof(CS.RPG.GameCore.AvatarExcelTable).Assembly:GetType(names[s.target])
if not s.t then log('MISSING '..names[s.target]);s.target=s.target+1;return end
log('TYPE '..s.t.FullName);s.members=s.t:GetMethods(flags);s.index=0
end
for n=1,8 do
if s.index>=s.members.Length then s.t=nil;s.members=nil;s.target=s.target+1;return end
local m=s.members[s.index];s.index=s.index+1
if m.DeclaringType==s.t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
end local ok,e=pcall(capture);if not ok then _G.__gacComicFlatMeta1={target=99};CS.UnityEngine.Debug.Log('[GAC_COMIC_FLAT_META] ERROR '..tostring(e))end end
+71
View File
@@ -0,0 +1,71 @@
"""Compare reference CW descriptors with the repository's compiled descriptor set."""
import argparse
import json
import re
from pathlib import Path
from google.protobuf import descriptor_pb2 as d
def messages(files):
result = {}
def visit(rows, prefix):
for row in rows:
full = prefix+'.'+row.name
result[full] = row
visit(row.nested_type, full)
for file in files:
visit(file.message_type, '.'+file.package)
return result
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--reference', type=Path, required=True)
p.add_argument('--current', type=Path, required=True)
p.add_argument('--commands', type=Path, required=True)
p.add_argument('--output', type=Path, required=True)
args = p.parse_args()
reference = d.FileDescriptorProto.FromString(args.reference.read_bytes())
current = d.FileDescriptorSet.FromString(args.current.read_bytes())
fm, cm = messages([reference]), messages(current.file)
current_names = {int(v):k for k,v in re.findall(r'"([^"]+)":\s*(\d+)',args.commands.read_text())}
commands = [{'id':v.number,'ffgo_name':v.name[3:],'current_name':current_names.get(v.number)} for enum in reference.enum_type if enum.name == 'CmdGridFightType' for v in enum.value if v.number]
mapping, conflicts = {}, []
def match(a,b,seen):
if (a,b) in seen:
return True
seen.add((a,b))
if a not in fm or b not in cm:
return False
xf, yf = ({v.number:v for v in rows.field} for rows in (fm[a],cm[b]))
if xf.keys()!=yf.keys():
return False
for num,u in xf.items():
v=yf[num]
if u.type!=v.type or u.label!=v.label or u.HasField('oneof_index')!=v.HasField('oneof_index'):
return False
if u.type==u.TYPE_MESSAGE and not match(u.type_name,v.type_name,seen):
return False
# Preserve the partition of fields into oneofs, independent of names.
groups = lambda fields: sorted(sorted(v.number for v in fields if v.HasField('oneof_index') and v.oneof_index==i) for i in set(v.oneof_index for v in fields if v.HasField('oneof_index')))
return groups(fm[a].field)==groups(cm[b].field)
for row in commands:
row['same_name']=row['ffgo_name']==row['current_name']
a='.'+reference.package+'.'+row['ffgo_name']
candidates=[k for k in cm if k.rsplit('.',1)[-1]==row['current_name']]
seen=set()
row['wire_shape_equal']=len(candidates)==1 and match(a,candidates[0],seen)
if row['wire_shape_equal']:
for k,v in seen:
if k in mapping and mapping[k]!=v:
conflicts.append([k,mapping[k],v])
mapping[k]=v
args.output.mkdir(parents=True,exist_ok=True)
provenance={'reference_descriptor_sha256':__import__('hashlib').sha256(args.reference.read_bytes()).hexdigest(),'current_descriptor_sha256':__import__('hashlib').sha256(args.current.read_bytes()).hexdigest(),'limitations':['Equal wire shapes do not establish enum meanings, field semantics, runtime success, or permission to register numeric aliases.']}
(args.output/'cw-command-comparison.json').write_text(json.dumps({'provenance':provenance,'commands':commands},indent=2))
(args.output/'cw-message-mapping.json').write_text(json.dumps({'provenance':provenance,'messages':mapping,'conflicts':conflicts},indent=2))
print(json.dumps({'commands':len(commands),'same_names':sum(x['same_name'] for x in commands),'equal_wire_shapes':sum(x['wire_shape_equal'] for x in commands),'mapped_messages':len(mapping),'conflicts':len(conflicts),'shape_differences':[x for x in commands if not x['wire_shape_equal']]}))
if __name__=='__main__':
main()
+69
View File
@@ -0,0 +1,69 @@
-- Read-only, one-shot probe for the current Currency Wars encounter.
do
local state=_G.__gacCwActiveEncounterCapture7 or {done=false};_G.__gacCwActiveEncounterCapture7=state
if not state.done then
local function log(value)CS.UnityEngine.Debug.Log('[GAC_CW_ACTIVE_ENCOUNTER] '..tostring(value))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local session=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession
if not session then return end
local stage
local fs=session:GetType():GetFields(flags)
for i=0,math.min(fs.Length-1,63)do if fs[i].FieldType.FullName=='RPG.Client.GridFightStage'then stage=fs[i]:GetValue(session);break end end
if not stage or not stage.CurNode then return end
local node=stage.CurNode
local function field(obj,name)
if not obj then return nil end
local all=obj:GetType():GetFields(flags)
for i=0,math.min(all.Length-1,63)do if all[i].Name==name then return all[i]:GetValue(obj)end end
end
local function list(obj)
local array=field(obj,'array');local count=field(obj,'count')or 0;local out={}
if array then for i=0,math.min(count-1,31)do out[#out+1]=array[i]end end
return out
end
local function showRow(tableName,id,getter)
local success,row=pcall(function()return getter(id)end)
if not success then log('TABLE_ERROR '..tableName..' id='..id..' '..tostring(row));return end
if not row then log('TABLE_EMPTY '..tableName..' id='..id);return end
local encodedOK,encoded=pcall(function()return CS.Newtonsoft.Json.JsonConvert.SerializeObject(row)end)
if encodedOK and #tostring(encoded)<=24000 then log('TABLE '..tableName..' id='..id..' '..tostring(encoded))else log('TABLE_ERROR '..tableName..' id='..id..' '..tostring(encoded))end
end
log('PROBE_VERSION=7')
log('BEGIN route='..stage.RouteID..' chapter='..stage.ChapterID..' section='..stage.SectionID..' stage='..node.StageID..' level='..stage.CurMonsterLevel..' base_diff='..stage:GetBaseDiffAddLv()..' current_diff='..stage:GetCurrentMonsterDiffAddLv()..' all_diff='..stage:GetAllDiffAddLv())
local stageRow=CS.RPG.GameCore.StageExcelTable.GetData(node.StageID)
log('ROW StageExcelTable '..CS.Newtonsoft.Json.JsonConvert.SerializeObject(stageRow))
local battleField=node:GetType():GetField('_BattleNodeInfo',flags)
local battle=battleField and battleField:GetValue(node)
local branches=list(field(battle,'EOHMIOEPOGP'))
log('BRANCH_COUNT '..#branches)
for branchIndex=1,#branches do
local branch=branches[branchIndex]
local option=field(branch,'DDKGLOFJIHH')or 0
local penalty=field(branch,'KDCDJDFHIPP')or 0
local difficulty=field(branch,'PANBGEDCMCH')or 0
local waveObjects=list(field(branch,'EMGPLPNHBHF'))
log('BRANCH index='..branchIndex..' option='..option..' penalty='..penalty..' extra_difficulty='..difficulty..' waves='..#waveObjects)
for waveIndex=1,#waveObjects do
local wave=waveObjects[waveIndex]
local waveID=field(wave,'BLCIEPPJJCC')or 0
local monsters=list(field(wave,'ABMPKGNBMNA'))
local summary={}
for monsterIndex=1,#monsters do
local monster=monsters[monsterIndex]
summary[#summary+1]=tostring(field(monster,'OEPOBIODAJH')or 0)..':'..tostring(field(monster,'NNLJKKAEICI')or 0)
end
log('WAVE branch='..branchIndex..' id='..waveID..' count='..#monsters..' monster_id:role_star='..table.concat(summary,','))
showRow('GridFightFormationWaveExcelTable',waveID,function(id)return CS.RPG.GameCore.GridFightFormationWaveExcelTable.GetData(id)end)
showRow('GridFightFormationWaveConfigExcelTable',waveID,function(id)return CS.RPG.GameCore.GridFightFormationWaveConfigExcelTable.GetData(id)end)
showRow('GridFightEliteGroupExcelTable',waveID,function(id)return CS.RPG.GameCore.GridFightEliteGroupExcelTable.GetData(id)end)
showRow('GridFightEliteGroupConfigExcelTable',waveID,function(id)return CS.RPG.GameCore.GridFightEliteGroupConfigExcelTable.GetData(id)end)
end
end
local penaltyRow=node:GetType():GetProperty('_PenaltyRuleConfigRow',flags)
if penaltyRow then log('ROW PenaltyRule '..CS.Newtonsoft.Json.JsonConvert.SerializeObject(penaltyRow:GetValue(node,nil)))end
log('END');state.done=true
end)
if not ok then log('ERROR '..tostring(err));state.done=true end
end
end
+105
View File
@@ -0,0 +1,105 @@
-- One-shot, read-only inspection of the active Beta client's current CW node.
-- This inspects only objects reachable from the active node, with bounded depth/counts.
do
local state = _G.__gacCwActiveNodeContext2 or { done = false }
_G.__gacCwActiveNodeContext2 = state
if not state.done then
local lines = {}
local function add(s)
if #lines < 260 then lines[#lines + 1] = tostring(s) end
end
local ok, err = pcall(function()
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
local session = CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession
if not session then add('NO_SESSION'); return end
local function field(obj, name)
if not obj then return nil end
local fs = obj:GetType():GetFields(flags)
for i = 0, math.min(fs.Length - 1, 79) do
if fs[i].Name == name then return fs[i]:GetValue(obj) end
end
end
local function value(v)
if v == nil then return 'nil' end
local t = type(v)
if t == 'string' or t == 'number' or t == 'boolean' then return tostring(v) end
local success, managed = pcall(function() return v:GetType() end)
if success then
if managed.IsEnum or managed.IsPrimitive then return tostring(v) end
return '<' .. tostring(managed.FullName) .. '>'
end
return '<' .. t .. '>'
end
local function inspect(label, obj, maxFields)
if not obj then add('OBJECT ' .. label .. ' nil'); return end
local ty = obj:GetType()
add('OBJECT ' .. label .. ' ' .. tostring(ty.FullName))
local fs = ty:GetFields(flags)
local emitted = 0
for i = 0, math.min(fs.Length - 1, 95) do
local f = fs[i]
if not f.IsStatic and emitted < (maxFields or 48) then
local got, v = pcall(function() return f:GetValue(obj) end)
add('FIELD ' .. label .. '.' .. tostring(f.Name) .. ' ' .. tostring(f.FieldType.FullName) .. '=' .. (got and value(v) or '<read-error>'))
emitted = emitted + 1
end
end
end
local stage
local sessionFields = session:GetType():GetFields(flags)
for i = 0, math.min(sessionFields.Length - 1, 79) do
if sessionFields[i].FieldType.FullName == 'RPG.Client.GridFightStage' then
stage = sessionFields[i]:GetValue(session); break
end
end
if not stage or not stage.CurNode then add('NO_ACTIVE_NODE'); return end
local node = stage.CurNode
add('RUN route=' .. tostring(stage.RouteID) .. ' chapter=' .. tostring(stage.ChapterID) ..
' section=' .. tostring(stage.SectionID) .. ' stage=' .. tostring(node.StageID) ..
' level=' .. tostring(stage.CurMonsterLevel))
inspect('stage.levelInfo', field(stage, '_LevelInfo'), 24)
inspect('stage.normalMonsterData', field(stage, '<NormalMonsterStageData>k__BackingField'), 36)
inspect('stage.powerfulMonsterData', field(stage, '<PowerfulMonsterStageData>k__BackingField'), 36)
inspect('stage.route', field(stage, 'Route'), 12)
inspect('node', node, 24)
inspect('node.routeData', field(node, '_RouteNode'), 24)
local battleField = node:GetType():GetField('_BattleNodeInfo', flags)
local battle = battleField and battleField:GetValue(node)
inspect('battle', battle, 16)
local function list(obj)
local arr = field(obj, 'array') or field(obj, '_items')
local count = field(obj, 'count') or field(obj, '_size') or 0
local out = {}
if arr then for i = 0, math.min(count - 1, 31) do out[#out + 1] = arr[i] end end
return out
end
local branches = list(field(battle, 'EOHMIOEPOGP'))
add('BRANCHES ' .. tostring(#branches))
for bi = 1, math.min(#branches, 4) do
local branch = branches[bi]
add('BRANCH index=' .. bi .. ' option=' .. value(field(branch, 'DDKGLOFJIHH')) ..
' penalty=' .. value(field(branch, 'KDCDJDFHIPP')) ..
' difficulty=' .. value(field(branch, 'PANBGEDCMCH')))
inspect('branch' .. bi, branch, 16)
local waves = list(field(branch, 'EMGPLPNHBHF'))
for wi = 1, math.min(#waves, 4) do
local wave = waves[wi]
add('WAVE branch=' .. bi .. ' index=' .. wi .. ' id=' .. value(field(wave, 'BLCIEPPJJCC')))
inspect('wave' .. bi .. '_' .. wi, wave, 16)
local monsters = list(field(wave, 'ABMPKGNBMNA'))
for mi = 1, #monsters do
local monster = monsters[mi]
if (bi == 1 and mi == 10) or (bi == 2 and mi == 11) then
add('MONSTER branch=' .. bi .. ' wave=' .. wi .. ' index=' .. mi ..
' id=' .. value(field(monster, 'OEPOBIODAJH')) .. ' star=' .. value(field(monster, 'NNLJKKAEICI')))
inspect('monster' .. bi .. '_' .. wi .. '_' .. mi, monster, 16)
end
end
end
end
end)
if not ok then add('ERROR ' .. tostring(err)) end
CS.UnityEngine.Debug.Log('[GAC_CW_ACTIVE_NODE_CONTEXT]\n' .. table.concat(lines, '\n'))
state.done = true
end
end
+70
View File
@@ -0,0 +1,70 @@
-- One-shot, read-only Beta Client lookup for the active CW route/stage rows.
-- IDs passed to these lookups come from the active GridFightStage objects.
do
local state = _G.__gacCwActiveTableRows1 or { done = false }
_G.__gacCwActiveTableRows1 = state
if not state.done then
local lines = {}
local function add(s) if #lines < 24 then lines[#lines + 1] = tostring(s) end end
local function getField(obj, name, flags)
if not obj then return nil end
local fs = obj:GetType():GetFields(flags)
for i = 0, math.min(fs.Length - 1, 95) do
if fs[i].Name == name then return fs[i]:GetValue(obj) end
end
end
local function row(name, id, getter)
local ok, value = pcall(getter)
if not ok then add('ROW_ERROR ' .. name .. ' id=' .. tostring(id) .. ' ' .. tostring(value)); return end
if not value then add('ROW_EMPTY ' .. name .. ' id=' .. tostring(id)); return end
local encodedOK, encoded = pcall(function() return CS.Newtonsoft.Json.JsonConvert.SerializeObject(value) end)
if encodedOK and #tostring(encoded) <= 20000 then
add('ROW ' .. name .. ' id=' .. tostring(id) .. ' ' .. tostring(encoded))
elseif encodedOK then
add('ROW_TOO_LARGE ' .. name .. ' id=' .. tostring(id) .. ' chars=' .. tostring(#tostring(encoded)))
else
add('ROW_SERIALIZE_ERROR ' .. name .. ' id=' .. tostring(id) .. ' ' .. tostring(encoded))
end
end
local ok, err = pcall(function()
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
local session = CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession
if not session then add('NO_SESSION'); return end
local stage
local fs = session:GetType():GetFields(flags)
for i = 0, math.min(fs.Length - 1, 79) do
if fs[i].FieldType.FullName == 'RPG.Client.GridFightStage' then stage = fs[i]:GetValue(session); break end
end
if not stage or not stage.CurNode then add('NO_ACTIVE_NODE'); return end
local node = stage.CurNode
local stageID, routeID = node.StageID, stage.RouteID
local battle = getField(node, '_BattleNodeInfo', flags)
local battleAuxA = getField(battle, 'INGNOGNLFBC', flags)
local battleAuxB = getField(battle, 'LFGIGDPIGJE', flags)
add('RUN route=' .. tostring(routeID) .. ' section=' .. tostring(stage.SectionID) ..
' stage=' .. tostring(stageID) .. ' level=' .. tostring(stage.CurMonsterLevel) ..
' battle_aux=' .. tostring(battleAuxA) .. ',' .. tostring(battleAuxB))
local routeNode = getField(node, '_RouteNode', flags)
local nodeParam = getField(node, '_NodeParam', flags)
local nodeInfo = getField(node, '_NodeInfo', flags)
local displayRow = getField(node, '_DisplayRow', flags)
local routeDisplay = getField(routeNode, 'NMABHKCOLCM', flags)
local routeID = stage.RouteID
row('StageExcelTable', stageID, function() return CS.RPG.GameCore.StageExcelTable.GetData(stageID) end)
row('GridFightStageRouteConfigExcelTable', routeID, function() return CS.RPG.GameCore.GridFightStageRouteConfigExcelTable.GetData(routeID) end)
row('GridFightEnemyDifficultyLvConfigExcelTable(candidate_aux_b)', battleAuxB, function() return CS.RPG.GameCore.GridFightEnemyDifficultyLvConfigExcelTable.GetData(battleAuxB) end)
for label, obj in pairs({nodeParam = nodeParam, nodeInfo = nodeInfo, displayRow = displayRow, routeNode = routeNode, routeDisplay = routeDisplay}) do
if obj then
local encodedOK, encoded = pcall(function() return CS.Newtonsoft.Json.JsonConvert.SerializeObject(obj) end)
if encodedOK and #tostring(encoded) <= 12000 then add('OBJECT ' .. label .. ' ' .. tostring(encoded))
else add('OBJECT ' .. label .. ' TYPE=' .. tostring(obj:GetType().FullName) .. ' serialize=' .. tostring(encoded)) end
else
add('OBJECT ' .. label .. ' nil')
end
end
end)
if not ok then add('ERROR ' .. tostring(err)) end
CS.UnityEngine.Debug.Log('[GAC_CW_ACTIVE_TABLE_ROWS]\n' .. table.concat(lines, '\n'))
state.done = true
end
end
+26
View File
@@ -0,0 +1,26 @@
-- Inspect only the named CW wave/elite configuration tables and their lookup methods.
do
local state=_G.__gacCwActiveWaveTableMethods1 or {done=false};_G.__gacCwActiveWaveTableMethods1=state
if not state.done then
local lines={}
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local assembly=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly
local names={'StageConfigExcelTable','GridFightFormationWaveExcelTable','GridFightFormationWaveConfigExcelTable','GridFightEliteGroupExcelTable','GridFightEliteGroupConfigExcelTable'}
for _,name in ipairs(names)do
local t=assembly:GetType('RPG.GameCore.'..name)
lines[#lines+1]='TYPE '..name..' '..tostring(t)
if t then
local methods=t:GetMethods(flags)
for i=0,math.min(methods.Length-1,95)do
local m=methods[i]
if m.DeclaringType==t and (m.Name=='GetData'or m.Name=='GetAllData'or m.Name=='GetDataRow'or m.Name=='GetAllRows')then lines[#lines+1]='METHOD '..name..' '..tostring(m)end
end
end
end
end)
if not ok then lines[#lines+1]='ERROR '..tostring(err)end
CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_SCHEMA]\n'..table.concat(lines,'\n'))
state.done=true
end
end
+21
View File
@@ -0,0 +1,21 @@
-- Named battle data only; metadata reads pause during Battle.
do
local s=_G.__gacCwBattleAvatarSchema1 or {index=1,offset=0};_G.__gacCwBattleAvatarSchema1=s
if s.done then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_BATTLE_AVATAR] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local names={'RPG.GameCore.BattleGridFightAvatarData','RPG.GameCore.GridFightManager'}
local name=names[s.index];if not name then s.done=true;log('END');return end
local a=typeof(CS.RPG.Client.GridFightModule).Assembly;local b=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly
local t=a:GetType(name) or b:GetType(name);if not t then log('MISSING '..name);s.index=s.index+1;return end
if s.offset==0 then local fs=t:GetFields(flags);log('TYPE '..name);for i=0,math.min(fs.Length-1,95)do log('FIELD '..tostring(fs[i]))end end
local ms=t:GetMethods(flags)
for i=s.offset,math.min(s.offset+23,ms.Length-1)do local m=ms[i];if m.DeclaringType==t and (s.index==1 or m.Name:lower():find('spot') or tostring(m):find('BJGJBNJDLIK'))then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
s.offset=s.offset+24;if s.offset>=ms.Length then s.offset=0;s.index=s.index+1 end
end)
if not ok then s.done=true;log('ERROR '..tostring(err))end
end
+23
View File
@@ -0,0 +1,23 @@
-- Isolated protobuf setters/getters only; nothing sent to gameplay modules.
do local function capture()
if _G.__gacCwBattleSpotWire1 then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local mgr=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=mgr and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(mgr)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
_G.__gacCwBattleSpotWire1=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_BATTLE_SPOT_WIRE] '..tostring(x))end
local ok,err=pcall(function()
local t=typeof(CS.ABJGLPCOMMK);local methods=t:GetMethods(flags)
for i=0,math.min(methods.Length-1,31)do
local m=methods[i];local ps=m:GetParameters()
if m.DeclaringType==t and ps.Length==1 and ps[0].ParameterType==typeof(CS.System.UInt32)then
local obj=CS.System.Activator.CreateInstance(t);obj[m.Name](obj,1)
log('SET '..m.Name..' '..CS.System.BitConverter.ToString(CS.Google.Protobuf.MessageExtensions.ToByteArray(obj)))
for j=0,math.min(methods.Length-1,31)do local g=methods[j];if g.DeclaringType==t and g:GetParameters().Length==0 and g.ReturnType==typeof(CS.System.UInt32) and obj[g.Name](obj)==1 then log('GET '..g.Name..' '..tostring(g.MethodHandle.Value))end end
end
end
end)
if not ok then log('ERROR '..tostring(err))end
log('END')
end;capture()end
+16
View File
@@ -0,0 +1,16 @@
-- Inspect only three CW runtime types and current saved game-event object.
do local function capture()
local s=_G.__gacCwBonfireContext1 or {index=1};_G.__gacCwBonfireContext1=s
if s.done then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_BONFIRE_CONTEXT] '..tostring(x))end
local names={'RPG.Client.GridFightGameEventData','RPG.Client.GridFightPendingActionManager','RPG.Client.GridFightPendingSelection'}
local t=typeof(CS.RPG.Client.GridFightModule).Assembly:GetType(names[s.index])
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
if t then
log('TYPE '..t.FullName)
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,63)do log('FIELD '..tostring(fs[i]))end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,127)do local m=ms[i];if m.DeclaringType==t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
if s.index==1 then local session=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession;if session then local f=session:GetType():GetField('_GameEventData',flags);local obj=f and f:GetValue(session);if obj then log('OBJECT '..CS.Newtonsoft.Json.JsonConvert.SerializeObject(obj))end end end
else log('MISSING '..names[s.index])end
s.index=s.index+1;if s.index>#names then s.done=true;log('END')end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwBonfireContext1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_BONFIRE_CONTEXT] ERROR '..tostring(e))end end
+25
View File
@@ -0,0 +1,25 @@
do local function capture()
local s=_G.__gacBonfireDialogContract1 or {i=1};_G.__gacBonfireDialogContract1=s
local targets={
{'Ui.GridFight.Preparation.Bonfire.GridFightBonfireDialog','_on_load'},
{'Ui.GridFight.Preparation.Bonfire.GridFightBonfireDialog','_on_enter_ending_phase'},
{'Ui.GridFight.Preparation.Bonfire.GridFightBonfireEventPanel','_on_load'},
{'Ui.GridFight.Preparation.Bonfire.GridFightBonfireFinalRewardPanel','_on_btn_take_reward_click'},
}
local target=targets[s.i];if not target then return end
s.i=s.i+1
local module=package.loaded[target[1]];local fn=module and module[target[2]]
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_BONFIRE_DIALOG] '..tostring(v))end
if type(fn)~='function' then log('MISSING '..target[1]..' '..target[2]);return end
local raw=string.dump(fn);if #raw>32768 then error('function bound')end
local bytes=CS.System.Array.CreateInstance(typeof(CS.System.Byte),#raw)
for i=1,#raw do bytes[i-1]=raw:byte(i)end
log('BYTECODE '..s.i..' '..CS.System.Convert.ToBase64String(bytes))
for i=1,16 do
local key,value=debug.getupvalue(fn,i);if not key then break end
log('UPVALUE '..target[2]..' '..key..' '..type(value))
if type(value)=='table' then
local n=0;for k,v in pairs(value)do n=n+1;if n>80 then break end;if type(v)=='number'then log('CONSTANT '..key..' '..tostring(k)..' '..tostring(v))end end
end
end
end local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_BONFIRE_DIALOG] ERROR '..tostring(e))end end
+40
View File
@@ -0,0 +1,40 @@
-- Read only the active dialog's pending instance, not all runtime types.
do local function capture()
if _G.__gacBonfireLivePending1 then return end
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightBonfireDialog')
local lua=page and page.LuaTable;local action=lua and lua._pending_action
if not action then
local session=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession
local manager=session and session.PrepPendingAction
if manager then
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local f=manager:GetType():GetField('_ActionQueue',flags);local queue=f and f:GetValue(manager)
if queue and queue.Count<=16 then
for i=0,queue.Count-1 do local v=queue[i];if v and v:GetType().Name:find('Bonfire',1,true)then action=v;break end end
end
end
if manager and not _G.__gacBonfireManagerMetadata1 then
_G.__gacBonfireManagerMetadata1=true
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local fs=manager:GetType():GetFields(flags)
for i=0,math.min(fs.Length-1,31)do local f=fs[i];local v=f:GetValue(manager);CS.UnityEngine.Debug.Log('[GAC_BONFIRE_PENDING] MANAGER '..f.Name..' '..f.FieldType.FullName..' '..tostring(v))end
end
if not action then return end
end
_G.__gacBonfireLivePending1=true
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_BONFIRE_PENDING] '..tostring(v))end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local t=action:GetType();log('TYPE '..t.FullName);log('HAS_SELECTED '..tostring(action.HasSelected))
for _,name in ipairs({'get_HasSelected','Sync','TakeReward','SelectOption','HealHp'})do
local ms=t:GetMethods(flags)
for i=0,math.min(ms.Length-1,127)do local m=ms[i];if m.Name==name then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end
local fs=t:GetFields(flags)
for i=0,math.min(fs.Length-1,31)do
local f=fs[i];local v=f:GetValue(action);log('FIELD '..f.Name..' '..f.FieldType.FullName..' '..tostring(v))
if f.FieldType.IsEnum then
local names=CS.System.Enum.GetNames(f.FieldType);local values=CS.System.Enum.GetValues(f.FieldType)
for j=0,math.min(names.Length-1,15)do log('ENUM '..names[j]..' '..CS.System.Convert.ToInt32(values:GetValue(j)))end
end
end
end local ok,e=pcall(capture);if not ok then _G.__gacBonfireLivePending1=true;CS.UnityEngine.Debug.Log('[GAC_BONFIRE_PENDING] ERROR '..tostring(e))end end
+32
View File
@@ -0,0 +1,32 @@
-- Read only: bounded loaded-Lua inventory and exact bonfire enum metadata.
do local function capture()
if _G.__gacBonfirePhaseContract1 then return end
_G.__gacBonfirePhaseContract1=true
local log=function(v) CS.UnityEngine.Debug.Log('[GAC_BONFIRE_PHASE] '..tostring(v)) end
local n=0
for k,v in pairs(package.loaded) do
n=n+1;if n>8192 then break end
if type(k)=='string' and (k:find('Bonfire',1,true) or k:find('TeaBreak',1,true)) then
log('MODULE '..k..' '..type(v))
if type(v)=='table' then
local count=0
for name,fn in pairs(v) do
count=count+1;if count>128 then break end
if type(fn)=='function' then
log('METHOD '..k..' '..tostring(name))
if tostring(name):lower():find('phase',1,true) or tostring(name):lower():find('refresh',1,true) then
for i=1,16 do
local key,value=debug.getupvalue(fn,i);if not key then break end
log('UPVALUE '..tostring(name)..' '..key..' '..type(value))
if type(value)=='table' and key:lower():find('phase',1,true) then
for ek,ev in pairs(value) do if type(ev)=='number' or type(ev)=='string' then log('ENUM '..tostring(ek)..' '..tostring(ev))end end
end
end
end
end
end
end
end
end
log('END')
end local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_BONFIRE_PHASE] ERROR '..tostring(e))end end
+19
View File
@@ -0,0 +1,19 @@
-- Metadata only: inspect the named CW module in batches; no gameplay calls.
do local function capture()
local s=_G.__gacCwBonfireSchema1 or {index=0};_G.__gacCwBonfireSchema1=s
if s.done then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_BONFIRE_SCHEMA] '..tostring(x))end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local t=typeof(CS.RPG.Client.GridFightModule)
if s.index==0 then
local session=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession
if session then local fs=session:GetType():GetFields(flags);for i=0,math.min(fs.Length-1,63)do log('SESSION_FIELD '..tostring(fs[i]))end end
end
local ms=t:GetMethods(flags)
for i=s.index,math.min(s.index+31,ms.Length-1,2047)do
local m=ms[i];local name=m.Name:lower()
if m.DeclaringType==t and (name:find('bonfire')or name:find('healhp')or name:find('teabreak'))then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
s.index=s.index+32
if s.index>=ms.Length or s.index>=2048 then s.done=true;log('END inspected='..math.min(ms.Length,2048)..' total='..ms.Length)end
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_BONFIRE_SCHEMA] ERROR '..tostring(e))end end
+12
View File
@@ -0,0 +1,12 @@
do local function capture()
local s=_G.__gacBonfireSyncMetadata1 or {i=0};_G.__gacBonfireSyncMetadata1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local t=typeof(CS.RPG.Client.GridFightBonfirePendingAction)
local ms=t:GetMethods(flags)
for i=s.i,math.min(s.i+7,ms.Length-1,127)do
local m=ms[i]
if m.DeclaringType==t then CS.UnityEngine.Debug.Log('[GAC_BONFIRE_SYNC] '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
s.i=s.i+8;if s.i>=ms.Length or s.i>=128 then s.done=true end
end local ok,e=pcall(capture);if not ok then _G.__gacBonfireSyncMetadata1={done=true};CS.UnityEngine.Debug.Log('[GAC_BONFIRE_SYNC] ERROR '..tostring(e))end end
+30
View File
@@ -0,0 +1,30 @@
-- Narrow metadata/read-only scalar state from the already resolved CW panel.
-- At most eight members per heartbeat; no assembly scan or gameplay calls.
do local function capture()
local s=_G.__gacCwBookActivityMetadata1 or {index=0,target=1};_G.__gacCwBookActivityMetadata1=s
if s.done then return end
local function log(v)CS.UnityEngine.Debug.Log('[GAC_CW_BOOK_ACTIVITY] '..tostring(v))end
local ok,err=pcall(function()
if not s.objects then
local class=package.loaded['Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem']
local constants
for i=1,16 do local n,v=debug.getupvalue(class._on_click_go,i);if not n then break end;if n=='_GRID_FIGHT_CONSTANTS'then constants=v;break end end
local panel=CS.RPG.Client.GlobalVars.s_ModuleManager.ActivityModule:GetPanelData(constants.ActivityID)
if not panel or not panel.StateData then error('Resolved CW panel/state unavailable')end
s.objects={panel.StateData,panel};s.flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
end
local value=s.objects[s.target]
if not s.members then
local t=value:GetType();s.members=t:GetFields(s.flags)
log('TYPE '..t.FullName..' FIELDS '..s.members.Length)
end
for i=s.index,math.min(s.index+7,s.members.Length-1)do
local m=s.members[i];local v=m:GetValue(value)
local text=v==nil and 'nil' or (m.FieldType.IsPrimitive or m.FieldType.IsEnum or m.FieldType.FullName=='System.String') and tostring(v) or tostring(m.FieldType.FullName)
log('FIELD '..m.Name..' TYPE '..m.FieldType.FullName..' VALUE '..text)
end
s.index=s.index+8
if s.index>=s.members.Length or s.index>=64 then s.members=nil;s.index=0;s.target=s.target+1;if s.target>#s.objects then s.done=true;log('END')end end
end)
if not ok then s.done=true;log('ERROR '..tostring(err))end
end;capture()end
+9
View File
@@ -0,0 +1,9 @@
do
local s=_G.__gacCwBookBaked1 or {index=1};_G.__gacCwBookBaked1=s
local names={'Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem','Ui.DailyMission.Rogue.HandbookRogueGotoBtnBehaviour'}
if s.index<=#names then
local name=names[s.index];s.index=s.index+1
local ok,v=pcall(package.loaders[3],name)
CS.UnityEngine.Debug.Log('[GAC_CW_BOOK_BAKED] '..name..' loaded='..tostring(ok)..' closure='..type(v))
end
end
+22
View File
@@ -0,0 +1,22 @@
-- Serialize only the live CW ActivityPanelConditionRow; no condition mutation.
do
if not _G.__gacCwBookConditionRowCapture2 then
_G.__gacCwBookConditionRowCapture2=true
local function log(v)CS.UnityEngine.Debug.Log('[GAC_CW_BOOK_CONDITION] '..tostring(v))end
log('BEGIN')
local ok,err=pcall(function()
local class=package.loaded['Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem']
local constants
for i=1,16 do local n,v=debug.getupvalue(class._on_click_go,i);if not n then break end;if n=='_GRID_FIGHT_CONSTANTS'then constants=v;break end end
local panel=CS.RPG.Client.GlobalVars.s_ModuleManager.ActivityModule:GetPanelData(constants.ActivityID)
local state=panel.StateData
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local f=state:GetType():GetField('<ConditionConfig>k__BackingField',flags)
local row=f:GetValue(state)
local value=CS.Newtonsoft.Json.JsonConvert.SerializeObject(row)
if #value>16384 then error('Condition row exceeds bound')end
log('ROW '..value)
end)
if not ok then log('ERROR '..tostring(err))end
end
end
+39
View File
@@ -0,0 +1,39 @@
-- Read only already loaded, exact Handbook/CW Go callbacks. One function per
-- heartbeat, bounded 32 KiB bytecode; never calls a callback or sends a packet.
do local function capture()
local s=_G.__gacCwBookEntryCapture2 or {index=1};_G.__gacCwBookEntryCapture2=s
local targets={
{'Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem','_on_click_go'},
{'Ui.DailyMission.Rogue.HandbookRogueGotoBtnBehaviour','on_btn_go'},
{'Logic.Goto.GotoCommand.GotoGridFightEntrancePageCommand','execute'},
{'Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem','_setup_view'},
}
if s.index>#targets then return end
local target=targets[s.index];s.index=s.index+1
local function log(v)CS.UnityEngine.Debug.Log('[GAC_CW_BOOK_ENTRY] '..tostring(v))end
local phase='module'
local ok,err=pcall(function()
local class=package.loaded[target[1]]
if type(class)~='table'then log('MISSING_MODULE '..target[1]);return end
phase='function';local fn=class[target[2]]
if type(fn)~='function'then
log('MISSING_FUNCTION '..target[1]..'.'..target[2])
local count=0;for k,v in pairs(class)do if type(v)=='function'then log('METHOD '..tostring(k));count=count+1;if count>=32 then break end end end
return
end
phase='upvalues'
if type(debug.getupvalue)=='function'then
for i=1,16 do local name,value=debug.getupvalue(fn,i);if not name then break end
log('UPVALUE '..target[2]..' '..name..' '..type(value)..' '..tostring(value))
end
end
phase='dump';log('DUMP_CAPABILITY '..type(string.dump)..' '..type(debug.getupvalue))
if type(string.dump)~='function'then return end
local raw=string.dump(fn)
if #raw>32768 then error('Named callback exceeds bytecode bound')end
local bytes=CS.System.Array.CreateInstance(typeof(CS.System.Byte),#raw)
for i=1,#raw do bytes[i-1]=raw:byte(i)end
log('BYTECODE '..target[1]..'.'..target[2]..' '..CS.System.Convert.ToBase64String(bytes))
end)
if not ok then log('ERROR '..phase..' '..tostring(err))end
end;capture()end
+31
View File
@@ -0,0 +1,31 @@
-- Read exact constants upvalue and its current ActivityPanelData only.
do
if not _G.__gacCwBookPanelStateAfterGuide1 and type(package.loaded['Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem'])=='table' then
_G.__gacCwBookPanelStateAfterGuide1=true
local function log(v)CS.UnityEngine.Debug.Log('[GAC_CW_BOOK_AFTER_GUIDE] '..tostring(v))end
local ok,err=pcall(function()
local class=package.loaded['Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem']
local fn=class and class._on_click_go
if type(fn)~='function'then error('Book Go callback not loaded')end
local constants
for i=1,16 do local name,value=debug.getupvalue(fn,i);if not name then break end
if name=='_GRID_FIGHT_CONSTANTS'then constants=value;break end
end
if not constants then error('Book Go constants upvalue absent')end
for _,key in ipairs({'ActivityID','GridFightContentID'})do log('CONSTANT '..key..' '..tostring(constants[key]))end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.ActivityModule
local id=constants.ActivityID
local panel=module:GetPanelData(id)
log('PANEL id='..tostring(id)..' present='..tostring(panel~=nil)..' type='..tostring(panel and panel:GetType().FullName))
if panel then
for _,key in ipairs({'ContentID','IsSkipSwitchStoryLine','IsOpen','PanelID'})do
local good,value=pcall(function()return panel[key]end);log('PANEL_VALUE '..key..' '..tostring(good)..' '..tostring(value))
end
local state=panel.StateData
log('STATE present='..tostring(state~=nil)..' CurState='..tostring(state and state.CurState))
end
end)
if not ok then log('ERROR '..tostring(err))end
end
end
+30
View File
@@ -0,0 +1,30 @@
-- Read exact constants upvalue and its current ActivityPanelData only.
do
if not _G.__gacCwBookPanelState1 then
_G.__gacCwBookPanelState1=true
local function log(v)CS.UnityEngine.Debug.Log('[GAC_CW_BOOK_PANEL] '..tostring(v))end
local ok,err=pcall(function()
local class=package.loaded['Ui.DailyMission.Rogue.GridFight.HandbookRogueGridFightTabItem']
local fn=class and class._on_click_go
if type(fn)~='function'then error('Book Go callback not loaded')end
local constants
for i=1,16 do local name,value=debug.getupvalue(fn,i);if not name then break end
if name=='_GRID_FIGHT_CONSTANTS'then constants=value;break end
end
if not constants then error('Book Go constants upvalue absent')end
for _,key in ipairs({'ActivityID','GridFightContentID'})do log('CONSTANT '..key..' '..tostring(constants[key]))end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.ActivityModule
local id=constants.ActivityID
local panel=module:GetPanelData(id)
log('PANEL id='..tostring(id)..' present='..tostring(panel~=nil)..' type='..tostring(panel and panel:GetType().FullName))
if panel then
for _,key in ipairs({'ContentID','IsSkipSwitchStoryLine','IsOpen','PanelID'})do
local good,value=pcall(function()return panel[key]end);log('PANEL_VALUE '..key..' '..tostring(good)..' '..tostring(value))
end
local state=panel.StateData
log('STATE present='..tostring(state~=nil)..' CurState='..tostring(state and state.CurState))
end
end)
if not ok then log('ERROR '..tostring(err))end
end
end
+7
View File
@@ -0,0 +1,7 @@
do
if not _G.__gacCwCommonBaked1 then
_G.__gacCwCommonBaked1=true
local ok,v=pcall(package.loaders[3],'Ui.Activity.CommonComponents.CommonActivityGotoBtnBehaviour')
CS.UnityEngine.Debug.Log('[GAC_CW_COMMON_BAKED] loaded='..tostring(ok)..' closure='..type(v))
end
end
+25
View File
@@ -0,0 +1,25 @@
-- Cached config wrappers only, eight entries per heartbeat, paused in Battle.
do local function capture()
local s=_G.__gacCwConfigCache1 or {table=1};_G.__gacCwConfigCache1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local phase=CS.RPG.Client.GlobalVars.s_GamePhaseManager
phase=phase and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(phase)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local names={'GridFightForgeConfigs','EquipItemConfigs','ConsumableItemConfigs','NPCConfigs'}
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_CONFIG_CACHE] '..tostring(x))end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule
if not s.iterator then
local dict=module[names[s.table]]
if not dict then log('MISSING '..names[s.table]);s.table=s.table+1;return end
if dict.Count>4096 then error('cache bound exceeded')end
s.iterator=dict:GetEnumerator();log('TABLE '..names[s.table]..' count='..dict.Count)
end
for _=1,8 do
if not s.iterator:MoveNext()then s.iterator:Dispose();s.iterator=nil;s.table=s.table+1;if s.table>#names then s.done=true;log('END')end;return end
local entry=s.iterator.Current;local obj=entry.Value;local field=obj:GetType():GetField('_Row',flags)
local row=field and field:GetValue(obj)
if row then local encoded=CS.Newtonsoft.Json.JsonConvert.SerializeObject(row);if #encoded>32768 then error('row bound exceeded')end;log('ROW '..names[s.table]..' '..tostring(entry.Key)..' '..encoded)end
end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwConfigCache1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_CONFIG_CACHE] ERROR '..tostring(e))end end
+23
View File
@@ -0,0 +1,23 @@
-- Read initialized handbook defaults, four families per heartbeat, never Battle.
do local function capture()
local s=_G.__gacCwCurrentDefaults2 or {index=0};_G.__gacCwCurrentDefaults2=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_CURRENT_DEFAULTS] '..tostring(x))end
if not s.rows then
s.rows=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule:GetHandBookAllRoleConfig()
if s.rows.Count>512 then error('handbook bound')end
log('BEGIN '..s.rows.Count)
end
for n=1,4 do
if s.index>=s.rows.Count then s.done=true;log('END');return end
local family=s.rows[s.index];s.index=s.index+1
local role=family.DefaultRole
local config=role.RoleConfig
log('ROW {"AvatarID":'..config.AvatarID..',"RoleID":'..role.ID..',"Price":'..config.Price..',"Rarity":'..config.Rarity..'}')
end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwCurrentDefaults2={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_CURRENT_DEFAULTS] ERROR '..tostring(e))end end
+20
View File
@@ -0,0 +1,20 @@
-- Bounded read of the active node's already available penalty row.
do local function capture()
if _G.__gacCwCurrentPenalty1 then return end
local session=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule.CurGameSession
if not session then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local fs=session:GetType():GetFields(flags)
for i=0,math.min(fs.Length-1,63)do
if fs[i].FieldType.FullName=='RPG.Client.GridFightStage'then
local stage=fs[i]:GetValue(session)
local node=stage and stage.CurNode
if not node then return end
local property=node:GetType():GetProperty('_PenaltyRuleConfigRow',flags)
if not property then return end
local row=property:GetValue(node,nil)
if row then CS.UnityEngine.Debug.Log('[GAC_CW_CURRENT_PENALTY] '..CS.Newtonsoft.Json.JsonConvert.SerializeObject(row));_G.__gacCwCurrentPenalty1=true end
return
end
end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwCurrentPenalty1=true;CS.UnityEngine.Debug.Log('[GAC_CW_CURRENT_PENALTY] ERROR '..tostring(e))end end
+27
View File
@@ -0,0 +1,27 @@
-- Research target derived from the missing saved-role comparison, not a game rule.
-- Reads the Client-selected handbook default and its existing star configurations.
do local function capture()
local s=_G.__gacCwReplacement1 or {index=0};_G.__gacCwReplacement1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_REPLACEMENT] '..tostring(x))end
if not s.rows then s.rows=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule:GetHandBookAllRoleConfig();if s.rows.Count>512 then error('bound')end end
if s.role then
local role=s.role;local c=role.RoleConfig
local star=c:GetStarConfig(s.star)
local row=c:GetType():GetField('_Row',flags):GetValue(c)
local starRow=star:GetType():GetField('_Row',flags):GetValue(star)
local servant=star:GetType():GetField('_ServantStarRow',flags):GetValue(star)
local json=CS.Newtonsoft.Json.JsonConvert.SerializeObject
log('ROW {"Config":'..json(row)..',"Price":'..c.Price..',"MaxStar":'..c.MaxStar..',"Star":'..s.star..',"SaleQuoteKnown":false,"StarConfig":'..json(starRow)..',"ServantStarConfig":'..json(servant)..'}')
s.star=s.star+1;if s.star>c.MaxStar then s.done=true;log('END')end;return
end
for n=1,4 do
if s.index>=s.rows.Count then s.done=true;log('ERROR default not found');return end
local role=s.rows[s.index].DefaultRole;s.index=s.index+1
if role.RoleConfig.AvatarID==1511 then s.role=role;s.star=1;log('DEFAULT '..role.ID);return end
end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwReplacement1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_REPLACEMENT] ERROR '..tostring(e))end end
+33
View File
@@ -0,0 +1,33 @@
-- Metadata only on two established types. Parameter names are current protocol
-- candidates: finding a consumer does not by itself establish request pairing.
-- Keep outside scripts/. No assembly scan, packet sends, hooks or state writes.
do local function capture()
local s=_G.__gacCwResponseConsumers1 or {target=1,index=0,beats=0};_G.__gacCwResponseConsumers1=s
if s.done then return end
s.beats=s.beats+1;if s.beats>100 then s.done=true;return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local f=manager and manager:GetType():GetField('_CurrentPhase',flags)
local phase=f and f:GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_CW_RESPONSE_CONSUMER] '..tostring(v))end
if not s.methods then
if s.target==1 then s.t=typeof(CS.RPG.Client.GridFightModule)
elseif s.target==2 then s.t=typeof(CS.RPG.Client.NetworkManager)
else s.done=true;log('END');return end
s.methods=s.t:GetMethods(flags);s.index=0
log('TYPE '..s.t.FullName..' count='..s.methods.Length)
end
local start=CS.UnityEngine.Time.realtimeSinceStartup
for n=1,32 do
if s.index>=s.methods.Length then s.methods=nil;s.t=nil;s.target=s.target+1;return end
local m=s.methods[s.index];s.index=s.index+1
if m.DeclaringType==s.t then
local signature=tostring(m)
if signature:find('PNHDGCBLPGD',1,true)or signature:find('GDHFMHKFEAL',1,true)or signature:find('GridFightEnterBonfireScRsp',1,true)then
log('METHOD '..signature..' HANDLE '..tostring(m.MethodHandle.Value))
end
end
if CS.UnityEngine.Time.realtimeSinceStartup-start>0.002 then return end
end
end local ok,e=pcall(capture);if not ok then _G.__gacCwResponseConsumers1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_RESPONSE_CONSUMER] ERROR '..tostring(e))end end
+12
View File
@@ -0,0 +1,12 @@
-- Parameter metadata only for the exact equipment request helpers.
do local function capture()
if _G.__gacCwEquipmentParams1 then return end;_G.__gacCwEquipmentParams1=true
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local t=typeof(CS.RPG.Client.GridFightEquipInfo)
local ms=t:GetMethods(flags)
for i=0,math.min(ms.Length-1,127)do local m=ms[i]
if m.Name=='TryCraftEquip'or m.Name=='TryDressEquipToRole'then
local ps=m:GetParameters();for j=0,ps.Length-1 do CS.UnityEngine.Debug.Log('[GAC_CW_EQUIPMENT_PARAMS] '..m.Name..' '..j..' '..ps[j].Name..' '..tostring(ps[j].ParameterType))end
end
end
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_EQUIPMENT_PARAMS] ERROR '..tostring(e))end end
+7
View File
@@ -0,0 +1,7 @@
-- Calls only an already initialized handbook getter, for at most 12 heartbeats.
do local function capture()
local n=_G.__gacCwFormationTrigger1 or 0;if n>=12 then return end
_G.__gacCwFormationTrigger1=n+1
CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule:GetHandBookAllRoleConfig()
end;capture()end
+17
View File
@@ -0,0 +1,17 @@
-- Exact inventory consumers, one type per heartbeat; metadata only.
do local function capture()
local s=_G.__gacCwInventoryConsumers2 or {index=1};_G.__gacCwInventoryConsumers2=s
local names={'GridFightOrbServerData','GridFightConsumableInfo','GridFightForgeItemData'}
if s.index>#names then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local name=names[s.index];s.index=s.index+1
local t=typeof(CS.RPG.Client.GridFightModule).Assembly:GetType('RPG.Client.'..name)
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_INVENTORY_CONSUMERS] '..tostring(x))end
if not t then log('MISSING '..name);return end;log('TYPE '..t.FullName)
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,95)do log('FIELD '..fs[i].Name..' '..fs[i].FieldType.FullName)end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,159)do local m=ms[i];if m.DeclaringType==t and (m.Name:find('Sync')or m.Name:find('UpdateInfo'))then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
if s.index>#names then log('END')end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwInventoryConsumers2={index=99};CS.UnityEngine.Debug.Log('[GAC_CW_INVENTORY_CONSUMERS] ERROR '..tostring(e))end end
+18
View File
@@ -0,0 +1,18 @@
-- Bounded method metadata for one explicitly named inventory type per beat.
do local function capture()
local s=_G.__gacCwInventoryWire3 or {index=1,offset=0};_G.__gacCwInventoryWire3=s
local names={'RPG.Client.GridFightTeam','MMEDLCAENAL','IBPDPCOGDEP','RPG.GameCore.GridFightForgeFuncType','RPG.GameCore.GridFightEquipCategory'}
if s.index>#names then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local name=names[s.index]
local t=typeof(CS.RPG.Client.GridFightModule).Assembly:GetType(name)or typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly:GetType(name)
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_INVENTORY_WIRE] '..tostring(x))end
if not t then log('MISSING '..name);s.index=s.index+1;return end
if s.offset==0 then log('TYPE '..name);if t.IsEnum then local ns=CS.System.Enum.GetNames(t);local vs=CS.System.Enum.GetValues(t);for i=0,math.min(ns.Length-1,47)do log('ENUM '..name..' '..ns[i]..' '..CS.System.Convert.ToInt32(vs:GetValue(i)))end;s.index=s.index+1;return end end
local ms=t:GetMethods(flags)
for i=s.offset,math.min(s.offset+31,ms.Length-1)do local m=ms[i];if m.DeclaringType==t and (m.Name:find('Forge')or m.Name:find('get_'))then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
s.offset=s.offset+32;if s.offset>=ms.Length then s.offset=0;s.index=s.index+1;if s.index>#names then log('END')end end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwInventoryWire3={index=99};CS.UnityEngine.Debug.Log('[GAC_CW_INVENTORY_WIRE] ERROR '..tostring(e))end end
+20
View File
@@ -0,0 +1,20 @@
// Research only; assembly SHA256 c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144.
// Reproduce only the protobuf allocation/empty repeated-field initialization
// at e3a53ba..e3a5415. Objects are isolated and never sent or synced to a module.
const m=Process.getModuleByName('GameAssembly.dll');
const make=new NativeFunction(m.base.add(0x3f629d0),'pointer',['pointer']);
const encode=new NativeFunction(m.base.add(0x1c8ca3a0),'pointer',['pointer','pointer']);
let done=false;
Interceptor.attach(m.base.add(0xdb5f860),{onLeave(){if(done)return;done=true;try{
const cls=m.base.add(0x988a200).readPointer(),listClass=m.base.add(0x9877108).readPointer();
if(cls.isNull()||listClass.isNull()||listClass.add(0xb7).readU8()===0)throw Error('Protobuf class prerequisite is not initialized');
const statics=listClass.add(0x88).readPointer();if(statics.isNull())throw Error('Repeated-field statics are missing');
const empty=statics.add(8).readPointer();if(empty.isNull())throw Error('Repeated-field empty array is missing');
const list=make(listClass);list.add(0x10).writePointer(empty);
const obj=make(cls);obj.add(0x10).writePointer(list);
for(const offset of [0x20,0x24,0x28]){
obj.add(offset).writeU32(1);const bytes=encode(obj,ptr(0));if(bytes.isNull())throw Error('No wire bytes');const n=bytes.add(0x18).readU32();if(n>64)throw Error('Isolated wire bound');const data=Array.from(new Uint8Array(bytes.add(0x20).readByteArray(n)));send({kind:'WIRE',offset,bytes:data});obj.add(offset).writeU32(0);
}
send({kind:'END'});
}catch(e){send({kind:'ERROR',error:String(e)});}}});
send({kind:'ATTACHED',scope:'isolated craft protobufs only'});
+6
View File
@@ -0,0 +1,6 @@
-- Calls only an already initialized handbook getter, for at most 12 heartbeats.
do local function capture()
local n=_G.__gacCwMonsterTrigger1 or 0;if n>=12 then return end
_G.__gacCwMonsterTrigger1=n+1
CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule:GetHandBookAllRoleConfig()
end;capture()end
+12
View File
@@ -0,0 +1,12 @@
-- Bounded metadata for two exact native methods; no constructors/actions.
do local function capture()
if _G.__gacCwMonsterHandles1 then return end;_G.__gacCwMonsterHandles1=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_MONSTER_HANDLE] '..tostring(x))end
local ok,err=pcall(function()
local t=typeof(CS.RPG.Client.GridFightMonsterData)
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local ms=t:GetMethods(flags)
for i=0,math.min(ms.Length-1,47)do local m=ms[i];if m.DeclaringType==t and m.Name=='Create'then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end)
if not ok then log('ERROR '..tostring(err))end;log('END')
end;capture()end
+18
View File
@@ -0,0 +1,18 @@
-- Four exact type/assembly lookups, one per heartbeat, metadata only.
do local function capture()
local s=_G.__gacCwMonsterTables3 or {index=1,assembly=1};_G.__gacCwMonsterTables3=s
local names={'GridFightMonsterConfigExcelTable','GridFightMonsterExcelTable'}
if s.index>#names then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_MONSTER_TABLE] '..tostring(x))end
local ok,err=pcall(function()
local assemblies={typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly,typeof(CS.RPG.GameCore.GameCoreConfigLoader).Assembly}
local t=assemblies[s.assembly]:GetType('RPG.GameCore.'..names[s.index]);log('TYPE '..names[s.index]..' assembly='..s.assembly..' '..tostring(t))
if t then
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,47)do if ms[i].DeclaringType==t then log('METHOD '..tostring(ms[i]))end end
end
end)
if not ok then log('ERROR '..tostring(err))end
s.assembly=s.assembly+1;if s.assembly>2 then s.assembly=1;s.index=s.index+1 end
if s.index>#names then log('END')end
end;capture()end
+20
View File
@@ -0,0 +1,20 @@
// Research only, verified assembly SHA256:
// c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144.
// CraftConfigRow.FromBinary -> loader 1e0db310 -> table at GetAll 1e0dadf0.
// Getter/entry layout proven by 1e0db000 (24-byte entries, value at +16).
// Runs on the existing managed handbook callback; no gameplay actions/loaders.
const m=Process.getModuleByName('GameAssembly.dll');
const check=(rva,expected)=>{const a=Array.from(new Uint8Array(m.base.add(rva).readByteArray(expected.length)));if(a.some((x,i)=>x!==expected[i]))throw Error('Code identity differs');};
check(0x1e0dadf0,[0x48,0x83,0xec,0x28]);check(0x1d3efb60,[0x56,0x57,0x48,0x83,0xec,0x28]);
const all=new NativeFunction(m.base.add(0x1e0dadf0),'pointer',['pointer']);
const serialize=new NativeFunction(m.base.add(0x1d3efb60),'pointer',['pointer','pointer']);
let index=0,count=0,entries=null,done=false;
Interceptor.attach(m.base.add(0xdb5f860),{onLeave(){if(done)return;try{
if(entries===null){const d=all(ptr(0));if(d.isNull())throw Error('Craft table is missing');count=d.add(0x20).readU32();entries=d.add(0x18).readPointer();if(count>2048||entries.isNull()||entries.add(0x18).readU32()<count)throw Error('Craft dictionary bound');send({kind:'BEGIN',count});}
for(let batch=0;batch<24&&index<count;batch++){
const row=entries.add(0x30+24*index++).readPointer();if(row.isNull())continue;
const value=serialize(row,ptr(0));if(value.isNull())throw Error('Serialization is empty');const n=value.add(0x10).readS32();if(n<2||n>32768)throw Error('Craft row bound');const json=value.add(0x14).readUtf16String(n);const parsed=JSON.parse(json);if(!parsed.CraftID||!parsed.CraftEquipID||!Array.isArray(parsed.CostEquipList))throw Error('Craft schema differs');send({kind:'ROW',json});
}
if(index>=count){done=true;send({kind:'END',count});}
}catch(e){done=true;send({kind:'ERROR',error:String(e)});}}});
send({kind:'ATTACHED',scope:'flat craft rows only'});
+14
View File
@@ -0,0 +1,14 @@
// Research only: current assembly SHA c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144.
// Wave formation consumer f0c9d50 calls this flat row getter at RVA 1e33bb30.
// Only that getter and the verified existing serializer run; no battle is created.
const ids=globalThis.CW_FORMATION_IDS;
if(!Array.isArray(ids)||!ids.length||ids.length>128||ids.some(x=>!Number.isInteger(x)||x<=0))throw Error('Bounded formation ID plan required');
const m=Process.getModuleByName('GameAssembly.dll');
const check=(rva,expected)=>{const b=Array.from(new Uint8Array(m.base.add(rva).readByteArray(expected.length)));if(b.some((x,i)=>x!==expected[i]))throw Error('Entry identity differs');};
check(0x1e33bb30,[0x56,0x48,0x83,0xec,0x60]);check(0x1d3efb60,[0x56,0x57,0x48,0x83,0xec,0x28]);
const read=new NativeFunction(m.base.add(0x1e33bb30),'pointer',['uint32','pointer']);
const serialize=new NativeFunction(m.base.add(0x1d3efb60),'pointer',['pointer','pointer']);
let index=0,done=false;
Interceptor.attach(m.base.add(0xdb5f860),{onLeave(){if(done)return;try{for(let n=0;n<4&&index<ids.length;n++){const id=ids[index++],row=read(id,ptr(0));if(row.isNull())throw Error('Missing monster row '+id);const v=serialize(row,ptr(0));if(v.isNull())throw Error('Missing serialization');const length=v.add(0x10).readS32();if(length<2||length>65536)throw Error('Row bound');send({kind:'ROW',id,json:v.add(0x14).readUtf16String(length)});}if(index===ids.length){done=true;send({kind:'END',count:index});}}catch(e){done=true;send({kind:'ERROR',error:String(e)});}}});
send({kind:'BEGIN',count:ids.length});
+13
View File
@@ -0,0 +1,13 @@
// Research only: current assembly SHA c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144.
// Create(UInt32,...) calls this flat row getter at RVA 1dee0820 before
// allocating MonsterData. Only that getter and the existing serializer run.
const ids=globalThis.CW_MONSTER_IDS;
if(!Array.isArray(ids)||!ids.length||ids.length>128||ids.some(x=>!Number.isInteger(x)||x<=0))throw Error('Bounded monster ID plan required');
const m=Process.getModuleByName('GameAssembly.dll');
const check=(rva,expected)=>{const b=Array.from(new Uint8Array(m.base.add(rva).readByteArray(expected.length)));if(b.some((x,i)=>x!==expected[i]))throw Error('Entry identity differs');};
check(0x1dee0820,[0x56,0x48,0x83,0xec,0x60]);check(0x1d3efb60,[0x56,0x57,0x48,0x83,0xec,0x28]);
const read=new NativeFunction(m.base.add(0x1dee0820),'pointer',['uint32','pointer']);
const serialize=new NativeFunction(m.base.add(0x1d3efb60),'pointer',['pointer','pointer']);
let index=0,done=false;
Interceptor.attach(m.base.add(0xdb5f860),{onLeave(){if(done)return;try{for(let n=0;n<4&&index<ids.length;n++){const id=ids[index++],row=read(id,ptr(0));if(row.isNull())throw Error('Missing monster row '+id);const v=serialize(row,ptr(0));if(v.isNull())throw Error('Missing serialization');const length=v.add(0x10).readS32();if(length<2||length>65536)throw Error('Row bound');send({kind:'ROW',id,json:v.add(0x14).readUtf16String(length)});}if(index===ids.length){done=true;send({kind:'END',count:index});}}catch(e){done=true;send({kind:'ERROR',error:String(e)});}}});
send({kind:'BEGIN',count:ids.length});
+14
View File
@@ -0,0 +1,14 @@
// Research only: revision SHA c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144.
// FromBinary callers and their flat dictionary getters are traced in
// battle_diagnostics/cw-*-flat-*.asm. No scene/gameplay factories are invoked.
const m=Process.getModuleByName('GameAssembly.dll');
const tables=[['orb',0x1dd79470],['consumable',0x1e88ddc0],['forge',0x1ddfbd20],['item',0x1edcc120],['combination',0x1de23170]];
for(const [name,rva]of tables){const b=Array.from(new Uint8Array(m.base.add(rva).readByteArray(4)));if(b.join(',')!=='72,131,236,40')throw Error('Getter identity differs: '+name);}
const serialize=new NativeFunction(m.base.add(0x1d3efb60),'pointer',['pointer','pointer']);
let table=0,index=0,count=0,entries=null,done=false;
Interceptor.attach(m.base.add(0xdb5f860),{onLeave(){if(done)return;try{
if(entries===null){const [name,rva]=tables[table],get=new NativeFunction(m.base.add(rva),'pointer',['pointer']);const d=get(ptr(0));if(d.isNull())throw Error('Missing '+name);count=d.add(0x20).readU32();entries=d.add(0x18).readPointer();index=0;if(count>4096||entries.isNull()||entries.add(0x18).readU32()<count)throw Error('Dictionary bound '+name);send({kind:'BEGIN',table:name,count});}
for(let n=0;n<64&&index<count;n++){const entry=entries.add(0x20+24*index++);if(entry.readS32()<0)continue;const row=entry.add(16).readPointer();if(row.isNull())continue;const v=serialize(row,ptr(0));if(v.isNull())throw Error('Empty serialization');const length=v.add(0x10).readS32();if(length<2||length>32768)throw Error('Row serialization bound');send({kind:'ROW',table:tables[table][0],json:v.add(0x14).readUtf16String(length)});}
if(index>=count){send({kind:'TABLE_END',table:tables[table][0],count});table++;entries=null;if(table===tables.length){done=true;send({kind:'END'});}}
}catch(e){done=true;send({kind:'ERROR',error:String(e)});}}});
send({kind:'ATTACHED',path:m.path,scope:'five named flat reward tables; at most 64 row serializations per managed callback'});
+14
View File
@@ -0,0 +1,14 @@
-- Resolve the loaded preparation callback's named network upvalue, metadata only.
do local function capture()
if _G.__gacCwNetUpvalue1 then return end
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightPreparationPage');local obj=page and page.LuaTable
if type(obj)~='table'then return end;_G.__gacCwNetUpvalue1=true
local f=obj._enter_battle;if type(f)~='function'then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
for i=1,8 do local name,value=debug.getupvalue(f,i);if not name then break end
if name=='GridFightModule'and type(value)=='userdata'then
local t=value:GetType();CS.UnityEngine.Debug.Log('[GAC_CW_NETWORK_UPVALUE] TYPE '..tostring(t.FullName)..' VALUE '..tostring(value))
local ms=t:GetMethods(flags);for j=0,math.min(ms.Length-1,127)do local m=ms[j];if m.DeclaringType==t then CS.UnityEngine.Debug.Log('[GAC_CW_NETWORK_UPVALUE] METHOD '..tostring(m))end end
end
end
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_NETWORK_UPVALUE] ERROR '..tostring(e))end end
+34
View File
@@ -0,0 +1,34 @@
-- Two exact established CW/network types, metadata only. No request sending,
-- callbacks, scene loading, run mutation or assembly/type enumeration.
do local function capture()
local s=_G.__gacCwOrbBonfireCallbacks1 or {target=1,index=0};_G.__gacCwOrbBonfireCallbacks1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local names={'RPG.Client.GridFightModule','RPG.Client.NetworkManager'}
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_CW_ORB_BONFIRE_CALLBACKS] '..tostring(v))end
if s.target>#names then s.done=true;log('END');return end
if not s.methods then
s.t=typeof(CS.RPG.Client.GridFightModule).Assembly:GetType(names[s.target])
if not s.t then log('MISSING '..names[s.target]);s.target=s.target+1;return end
s.methods=s.t:GetMethods(flags);s.index=0
if s.methods.Length>8192 then error('exact-type method bound')end
log('TYPE '..s.t.FullName..' count='..s.methods.Length)
end
-- Name checks are cheap; limit them independently of the detailed formatting.
-- A time budget also stops the chunk if this Client's reflection is slower.
local start=CS.UnityEngine.Time.realtimeSinceStartup
local formatted=0
for n=1,64 do
if s.index>=s.methods.Length then s.target=s.target+1;s.methods=nil;s.t=nil;return end
local m=s.methods[s.index];s.index=s.index+1
local name=m.Name
if m.DeclaringType==s.t and (name:find('Orb',1,true) or name:find('Bonfire',1,true))then
log('METHOD '..s.t.FullName..' '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))
formatted=formatted+1
end
if formatted>=8 or CS.UnityEngine.Time.realtimeSinceStartup-start>0.002 then return end
end
end local ok,e=pcall(capture);if not ok then _G.__gacCwOrbBonfireCallbacks1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_ORB_BONFIRE_CALLBACKS] ERROR '..tostring(e))end end
+44
View File
@@ -0,0 +1,44 @@
-- Only nested types observed on the established Orb consumer, plus the live
-- session's Bonus object. Metadata only: never invoke a network/gameplay method.
do local function capture()
local s=_G.__gacCwOrbNested1 or {target=1,index=0,stage='fields',beats=0};_G.__gacCwOrbNested1=s
if s.done then return end
s.beats=s.beats+1;if s.beats>64 then s.done=true;return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local names={'<>c__DisplayClass11_0','<>c','OrbSummarizer'}
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_CW_ORB_NESTED] '..tostring(v))end
if s.target>4 then s.done=true;log('END');return end
if not s.t then
if s.target<=#names then
s.t=typeof(CS.RPG.Client.GridFightOrbServerData):GetNestedType(names[s.target],flags)
else
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule
local session=module and module.CurGameSession
local bonus=session and session.Bonus
s.t=bonus and bonus:GetType()
end
if not s.t then log('MISSING_TARGET '..s.target);s.target=s.target+1;return end
s.members=s.t:GetFields(flags);s.index=0;s.stage='fields';log('TYPE '..s.t.FullName)
end
for n=1,8 do
if s.index>=s.members.Length then
if s.stage=='fields'then s.members=s.t:GetMethods(flags);s.index=0;s.stage='methods';return end
s.target=s.target+1;s.t=nil;s.members=nil;return
end
local m=s.members[s.index];s.index=s.index+1
if m.DeclaringType==s.t then
if s.stage=='fields'then log('FIELD '..m.Name..' '..m.FieldType.FullName)
else
log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))
local ps=m:GetParameters()
for i=0,math.min(ps.Length-1,3)do
local t=ps[i].ParameterType
if t.Assembly.FullName:find('RPG.Network.Proto',1,true)then log('PROTO_PARAMETER '..m.Name..' '..t.FullName)end
end
end
end
end
end local ok,e=pcall(capture);if not ok then _G.__gacCwOrbNested1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_ORB_NESTED] ERROR '..tostring(e))end end
+33
View File
@@ -0,0 +1,33 @@
-- Read only the established Orb consumer, eight members per heartbeat. Keep
-- outside scripts/ until deliberately staged by the runtime investigator.
do local function capture()
local state=_G.__gacCwOrbRuntimeMetadata1 or {index=0,beats=0}
_G.__gacCwOrbRuntimeMetadata1=state
if state.done then return end
state.beats=state.beats+1
local function log(s)CS.UnityEngine.Debug.Log('[GAC_CW_ORB_RUNTIME_META] '..tostring(s))end
if state.beats>36 then state.done=true;state.members=nil;log('END beat budget');return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local f=manager and manager:GetType():GetField('_CurrentPhase',flags)
local phase=f and f:GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
if not state.members then
state.type=typeof(CS.RPG.Client.GridFightModule).Assembly:GetType('RPG.Client.GridFightOrbServerData')
if not state.type then state.done=true;log('MISSING established Orb consumer');return end
state.members=state.type:GetMembers(flags)
if state.members.Length>192 then error('Orb member bound')end
log('TYPE '..state.type.FullName..' members='..state.members.Length)
end
for _=1,8 do
if state.index>=state.members.Length then state.done=true;state.members=nil;state.type=nil;log('END');return end
local m=state.members[state.index];state.index=state.index+1
if m.DeclaringType==state.type then
if m.MemberType==CS.System.Reflection.MemberTypes.Method then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))
else log(tostring(m.MemberType)..' '..tostring(m))end
end
end
end
local ok,e=pcall(capture)
if not ok then _G.__gacCwOrbRuntimeMetadata1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_ORB_RUNTIME_META] ERROR '..tostring(e))end
end
+16
View File
@@ -0,0 +1,16 @@
-- Exact protobuf parameter types obtained from the current consumer metadata.
do local function capture()
if _G.__gacCwOrbFields1 then return end;_G.__gacCwOrbFields1=true
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_ORB_FIELDS] '..tostring(x))end
local t=typeof(CS.RPG.Client.GridFightOrbServerData)
local ms=t:GetMethods(flags)
for i=0,math.min(ms.Length-1,95)do local m=ms[i]
if m.Name=='Sync'and m:GetParameters().Length==1 then
local p=m:GetParameters()[0].ParameterType
log('TYPE '..p.FullName..' ASSEMBLY '..p.Assembly.FullName)
if p.Name=='MMEDLCAENAL'then local methods=p:GetMethods(flags);for j=0,math.min(methods.Length-1,95)do local g=methods[j];if g.DeclaringType==p and g.Name:find('get_')then log('METHOD '..g.Name..' HANDLE '..tostring(g.MethodHandle.Value))end end end
end
end
log('END')
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_ORB_FIELDS] ERROR '..tostring(e))end end
+15
View File
@@ -0,0 +1,15 @@
-- Export three loaded button functions without invoking them or changing UI.
do local function capture()
local s=_G.__gacCwPrepBytecode1 or {index=1};_G.__gacCwPrepBytecode1=s
local names={'_on_start_battle_click','_enter_battle','_check_enter_battle_by_reason'}
if s.index>#names then return end
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightPreparationPage')
local obj=page and page.LuaTable;if type(obj)~='table'then return end
local name=names[s.index];s.index=s.index+1
local f=obj[name];if type(f)~='function'then return end
local raw=string.dump(f)
if #raw>32768 then CS.UnityEngine.Debug.Log('[GAC_CW_PREP_BYTECODE] TOO_LARGE '..name);return end
local bytes=CS.System.Array.CreateInstance(typeof(CS.System.Byte),#raw)
for i=1,#raw do bytes[i-1]=raw:byte(i)end
CS.UnityEngine.Debug.Log('[GAC_CW_PREP_BYTECODE] '..name..' '..CS.System.Convert.ToBase64String(bytes))
end;local ok,e=pcall(capture);if not ok then _G.__gacCwPrepBytecode1={index=4};CS.UnityEngine.Debug.Log('[GAC_CW_PREP_BYTECODE] ERROR '..tostring(e))end end
+25
View File
@@ -0,0 +1,25 @@
-- Read-only closure metadata for the loaded preparation page's button handlers.
do local function capture()
if _G.__gacCwPreparationCallbacks1 then return end
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightPreparationPage')
local obj=page and page.LuaTable;if type(obj)~='table'then return end
_G.__gacCwPreparationCallbacks1=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_PREP_CALLBACK] '..tostring(x))end
for _,key in ipairs({'_on_start_battle_click','_enter_battle','_check_enter_battle_by_reason'})do
local f=obj[key];if type(f)=='function'then
for i=1,16 do local name,value=debug.getupvalue(f,i);if not name then break end
local shown=type(value)=='number'and tostring(value)or(type(value)=='string'and value:sub(1,160)or type(value))
log(key..' UPVALUE '..name..' '..shown)
end
end
end
local visited={}
local function walk(t,depth)
if type(t)~='table'or visited[t]or depth>4 then return end;visited[t]=true
local count=0;for k,v in pairs(t)do
count=count+1;if count>256 then break end
if tostring(k):lower():find('bonfire')or tostring(k):lower():find('tea')then log('KEY '..tostring(k)..' '..type(v))end
if k=='__index'or k=='class'or k=='super'then walk(v,depth+1)end
end;walk(getmetatable(t),depth+1)
end;walk(obj,0)
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_PREP_CALLBACK] ERROR '..tostring(e))end end
+18
View File
@@ -0,0 +1,18 @@
-- Read the current preparation page's Lua class metadata only.
do local function capture()
if _G.__gacCwPrepLuaMethods1 then return end;_G.__gacCwPrepLuaMethods1=true
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightPreparationPage')
local obj=page and page.LuaTable;if type(obj)~='table'then return end
local visited={}
local function inspect(t,depth)
if type(t)~='table'or visited[t]or depth>4 then return end;visited[t]=true
local count=0
for k,v in pairs(t)do
count=count+1;if count>192 then break end
if type(v)=='function'then local info=debug.getinfo(v,'S');CS.UnityEngine.Debug.Log('[GAC_CW_PREP_LUA] FUNCTION '..tostring(k)..' '..tostring(info and info.source)..' lines='..tostring(info and info.linedefined))
elseif (k=='__index'or k=='super'or k=='class')and type(v)=='table'then inspect(v,depth+1)end
end
inspect(getmetatable(t),depth+1)
end
inspect(obj,0)
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_PREP_LUA] ERROR '..tostring(e))end end
+7
View File
@@ -0,0 +1,7 @@
-- Exact protobuf serializer metadata; no serialization/gameplay invocation.
do local function capture()
if _G.__gacCwProtoSerializeHandle1 then return end;_G.__gacCwProtoSerializeHandle1=true
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local ms=typeof(CS.Google.Protobuf.MessageExtensions):GetMethods(flags)
for i=0,math.min(ms.Length-1,127)do local m=ms[i];if m.Name=='ToByteArray'and m:GetParameters().Length==1 then CS.UnityEngine.Debug.Log('[GAC_CW_PROTO_SERIALIZER] '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end;local ok,e=pcall(capture);if not ok then CS.UnityEngine.Debug.Log('[GAC_CW_PROTO_SERIALIZER] ERROR '..tostring(e))end end
+10
View File
@@ -0,0 +1,10 @@
-- Existing initialized handbook getter only, paused during Battle.
do local function capture()
local n=_G.__gacCwRewardTrigger1 or 0;if n>=120 then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
_G.__gacCwRewardTrigger1=n+1
CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule:GetHandBookAllRoleConfig()
end;local ok,e=pcall(capture);if not ok then _G.__gacCwRewardTrigger1=120;CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_TRIGGER] ERROR '..tostring(e))end end
+30
View File
@@ -0,0 +1,30 @@
-- Explicit CW module metadata only. Keep outside scripts/ until a live Client
-- is available. No assembly scan, config getter invocation or run mutation.
do local function capture()
local state=_G.__gacCwRewardInventorySchema1 or {index=0}
_G.__gacCwRewardInventorySchema1=state
if state.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then return end
local function log(value)CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_INVENTORY_SCHEMA] '..tostring(value))end
local function relevant(name)
return name:find('Orb') or name:find('Consumable') or name:find('Forge') or name:find('Bonus') or name:find('Reward')
end
if not state.members then
local target=typeof(CS.RPG.Client.GridFightModule)
state.members=target:GetMembers(flags)
if state.members.Length>4096 then error('named module metadata bound exceeded')end
log('TYPE '..target.FullName)
end
-- Bound metadata formatting/logging per heartbeat; never invoke members.
for _=1,32 do
if state.index>=state.members.Length then state.done=true;state.members=nil;log('END');return end
local member=state.members[state.index];state.index=state.index+1
if relevant(tostring(member.Name))then log(tostring(member.MemberType)..' '..tostring(member))end
end
end
local ok,err=pcall(capture)
if not ok then _G.__gacCwRewardInventorySchema1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_INVENTORY_SCHEMA] ERROR '..tostring(err))end
end
+30
View File
@@ -0,0 +1,30 @@
-- Exact reward row names recorded by the earlier reward capture tool. Probe
-- their presence in the current target rather than accepting reference RVAs.
do local function capture()
local s=_G.__gacCwRewardPoolCurrent1 or {target=1,index=0,stage='fields'};_G.__gacCwRewardPoolCurrent1=s
if s.done then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local gm=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=gm and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(gm)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local names={'GridFightBasicBonusPoolConfigRow','GridFightBonusPoolConfigRow','GridFightBonusPoolV2Row','GridFightBasicBonusPoolV2Row'}
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_POOL_CURRENT] '..tostring(v))end
if s.target>#names then s.done=true;log('END');return end
if not s.members then
local asm=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly
s.t=asm:GetType('RPG.GameCore.'..names[s.target])
if not s.t then log('MISSING '..names[s.target]);s.target=s.target+1;return end
s.members=s.t:GetFields(flags);s.index=0;s.stage='fields';log('TYPE '..s.t.FullName)
end
for n=1,8 do
if s.index>=s.members.Length then
if s.stage=='fields'then
local method=s.t:GetMethod('FromBinary',flags)
if method then log('BINARY '..s.t.FullName..' HANDLE '..tostring(method.MethodHandle.Value))end
end
s.target=s.target+1;s.members=nil;s.t=nil;return
end
local member=s.members[s.index];s.index=s.index+1
if member.DeclaringType==s.t then log('FIELD '..member.Name..' '..member.FieldType.FullName)end
end
end local ok,e=pcall(capture);if not ok then _G.__gacCwRewardPoolCurrent1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_POOL_CURRENT] ERROR '..tostring(e))end end
+26
View File
@@ -0,0 +1,26 @@
-- Named reward row metadata only, two exact types per heartbeat.
do local function capture()
local s=_G.__gacCwRewardRows3 or {index=1};_G.__gacCwRewardRows3=s
local names={'GridFightBasicBonusPoolConfigRow','GridFightBonusPoolConfigRow','GridFightBonusPoolV2Row','GridFightBasicBonusPoolV2Row'}
if s.index>#names then return end
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_ROWS] '..tostring(x))end
for _=1,2 do
if s.index>#names then log('END');return end
local name=names[s.index];s.index=s.index+1
local asm=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly
local t
if name=='GridFightConsumableItemConfig'or name=='GridFightForgeConfig'then
t=typeof(CS.RPG.Client[name]);local fs=t:GetFields(flags)
for i=0,math.min(fs.Length-1,63)do local f=fs[i];if f.FieldType.FullName:find('Row$')then t=f.FieldType;break end end
else t=asm:GetType('RPG.GameCore.'..name)end
log('TYPE '..name..' '..tostring(t))
if t then
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,63)do log('FIELD '..fs[i].Name..' '..fs[i].FieldType.FullName)end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,63)do local m=ms[i];if m.DeclaringType==t and m.Name=='FromBinary'then log('BINARY '..t.FullName..' HANDLE '..tostring(m.MethodHandle.Value))end end
end
end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwRewardRows3={index=99};CS.UnityEngine.Debug.Log('[GAC_CW_REWARD_ROWS] ERROR '..tostring(e))end end
+19
View File
@@ -0,0 +1,19 @@
-- Specific battle-context types only, metadata only, one type per heartbeat.
do
local s=_G.__gacCwSpotBattleSchema1 or {index=1};_G.__gacCwSpotBattleSchema1=s
if s.done then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_SPOT_BATTLE] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local names={'RPG.Client.GridFightBattleGameContext','RPG.Client.GridFightRoleEffect','RPG.Client.GridFightBattleRole','RPG.Client.GridFightBattleRoleProfile','RPG.GameCore.GridFightSpotLightComponent'}
local name=names[s.index];if not name then s.done=true;log('END');return end;s.index=s.index+1
local a=typeof(CS.RPG.Client.GridFightModule).Assembly;local b=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly
local t=a:GetType(name) or b:GetType(name);log('TYPE '..name..' '..tostring(t));if not t then return end
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,63)do log('FIELD '..tostring(fs[i]))end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,95)do local m=ms[i];if m.DeclaringType==t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end)
if not ok then s.done=true;log('ERROR '..tostring(err))end
end
+19
View File
@@ -0,0 +1,19 @@
-- Only the named manager/binding methods, no actions or assembly enumeration.
do
if _G.__gacCwBindingHandles1 then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
_G.__gacCwBindingHandles1=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_BINDING_HANDLE] '..tostring(x))end
local ok,err=pcall(function()
local t=typeof(CS.RPG.Client.GridFightSpotLightManager)
local ms=t:GetMethods(flags)
for i=0,math.min(ms.Length-1,127)do local m=ms[i];if m.Name=='AddOrUpdate' or m.Name=='GetActiveSpotLightBindingByRoleId' then log(t.FullName..'.'..m.Name..' '..tostring(m.MethodHandle.Value))end end
local t2=typeof(CS.RPG.Client.GridFightSpotLightBinding)
local cs=t2:GetConstructors(flags);for i=0,math.min(cs.Length-1,3)do log('CTOR '..tostring(cs[i])..' '..tostring(cs[i].MethodHandle.Value))end
end)
if not ok then log('ERROR '..tostring(err))end
log('END')
end
+66
View File
@@ -0,0 +1,66 @@
-- Read-only current-session capture. Keep outside scripts/ until coordinated.
-- Reads only the established Preparation page and live Stage/session metadata.
do local function capture()
local s=_G.__gacCwTeaConsumer1 or {phase=1,index=0,beats=0};_G.__gacCwTeaConsumer1=s
if s.done then return end
s.beats=s.beats+1;if s.beats>32 then s.done=true;return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local gm=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local f=gm and gm:GetType():GetField('_CurrentPhase',flags)
local phase=f and f:GetValue(gm)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local log=function(v)CS.UnityEngine.Debug.Log('[GAC_CW_TEA_CONSUMER] '..tostring(v))end
if s.phase==1 then
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightPreparationPage')
local obj=page and page.LuaTable
if type(obj)~='table'then log('NO_LOADED_PREPARATION');s.phase=2;return end
s.obj=obj;s.phase=3;s.index=1;return
end
if s.phase==3 then
local names={'_on_start_battle_click','_enter_battle','_check_enter_battle_by_reason'}
local name=names[s.index];s.index=s.index+1
if not name then s.obj=nil;s.phase=2;return end
local fn=s.obj[name]
if type(fn)=='function'then
local info=debug.getinfo(fn,'S');log('SOURCE '..name..' '..tostring(info and info.source))
for i=1,24 do
local k,v=debug.getupvalue(fn,i);if not k then break end
log('UPVALUE '..name..' '..k..' '..type(v))
end
local raw=string.dump(fn)
if #raw<=32768 then
local bytes=CS.System.Array.CreateInstance(typeof(CS.System.Byte),#raw)
for i=1,#raw do bytes[i-1]=raw:byte(i)end
log('BYTECODE '..name..' '..CS.System.Convert.ToBase64String(bytes))
else log('FUNCTION_TOO_LARGE '..name)end
else log('NO_FUNCTION '..name)end
return
end
if s.phase==2 then
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule
local session=module and module.CurGameSession
s.obj=session;s.target=1;s.phase=4;s.index=0;return
end
if s.phase==4 then
if not s.obj then log('NO_LIVE_TARGET '..tostring(s.target));s.done=true;return end
if not s.fields then
s.t=s.obj:GetType();s.fields=s.t:GetFields(flags);log('TYPE '..s.t.FullName)
end
for n=1,8 do
if s.index>=s.fields.Length then
if s.target==1 then
s.obj=s.obj.Stage;s.target=2;s.index=0;s.fields=nil;return
end
s.done=true;s.obj=nil;s.fields=nil;log('END');return
end
local field=s.fields[s.index];s.index=s.index+1
if field.DeclaringType==s.t then
log('FIELD '..field.Name..' '..field.FieldType.FullName)
if field.Name:lower():find('bonfire',1,true)then
local value=field:GetValue(s.obj)
log('BONFIRE_VALUE_TYPE '..field.Name..' '..tostring(value and value:GetType().FullName))
end
end
end
end
end local ok,e=pcall(capture);if not ok then _G.__gacCwTeaConsumer1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_TEA_CONSUMER] ERROR '..tostring(e))end end
+118
View File
@@ -0,0 +1,118 @@
-- Read-only, bounded Tea Break investigation. Keep outside scripts/ until an
-- operator deliberately stages it. Does not send packets or change UI/state.
do local function capture()
local state=_G.__gacCwTeaBreakRuntime2
if not state then
state={step=1,index=0,beats=0};_G.__gacCwTeaBreakRuntime2=state
end
if state.done then return end
state.beats=state.beats+1
local function log(s)CS.UnityEngine.Debug.Log('[GAC_CW_TEA_RUNTIME2] '..tostring(s))end
if state.beats>36 then state.done=true;log('END beat budget');return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local gm=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local current=gm and gm:GetType():GetField('_CurrentPhase',flags)
local phase=current and current:GetValue(gm)
if not phase then log('WAIT game phase');return end
if typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
if state.step==1 then
local t=typeof(CS.RPG.Client.GridFightModule)
local p=t:GetProperty('Instance',flags)
if p then local ok,obj=pcall(function()return p:GetValue(nil,nil)end);if ok then state.module=obj end end
if not state.module then
local manager=CS.RPG.Client.GlobalVars.s_ModuleManager
if manager then local ok,obj=pcall(function()return manager.GridFightModule end);if ok then state.module=obj end end
end
log('MODULE '..tostring(state.module and state.module:GetType().FullName))
state.session=state.module and state.module.CurGameSession
local sf=state.session and state.session:GetType():GetField('_GameEventData',flags)
state.event=sf and sf:GetValue(state.session)
log('EVENT_TYPE '..tostring(state.event and state.event:GetType().FullName))
local stage=state.session and state.session.Stage
local node=stage and stage.CurNode
if node then
log('NODE route='..tostring(stage.RouteID)..' chapter='..tostring(stage.ChapterID)..' section='..tostring(stage.SectionID)..' bonfire='..tostring(node.IsBonfireNode))
local ids=node.BonfireEventIds
if ids then
if ids.Count>16 then error('bonfire event bound')end
for i=0,ids.Count-1 do log('BONFIRE_EVENT '..tostring(ids[i]))end
end
local nf=node:GetType():GetField('_NodeParam',flags)
local param=nf and nf:GetValue(node)
if param then
local raw=CS.Newtonsoft.Json.JsonConvert.SerializeObject(param)
if #raw>16384 then error('node parameter bound')end
log('NODE_PARAM '..raw)
end
end
state.types={t.Assembly:GetType('RPG.Client.GridFightGameEventData'),t.Assembly:GetType('RPG.Client.GridFightStageNode')}
state.typeindex=1;state.step=2;return
end
if state.step==2 then
local t=state.types[state.typeindex]
if not t then state.typeindex=state.typeindex+1;if state.typeindex>2 then state.step=3 end;return end
if not state.methods then
log('TYPE '..t.FullName)
if state.typeindex==2 then
for _,name in ipairs({'_RefreshBonfireEventIds','get_IsBonfireNode','get_BonfireEventIds'})do
local m=t:GetMethod(name,flags)
if m then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
state.step=3;return
end
state.methods=t:GetMethods(flags);state.fields=t:GetFields(flags);state.index=0
end
for i=state.index,math.min(state.index+7,state.methods.Length-1)do
local m=state.methods[i]
if m.DeclaringType==t then
local n=m.Name:lower()
if state.typeindex==1 or n:find('bonfire')then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
end
state.index=state.index+8
if state.index>=state.methods.Length then state.step=4;state.index=0 end
return
end
if state.step==4 then
local t=state.types[state.typeindex]
for i=state.index,math.min(state.index+7,state.fields.Length-1)do
local f=state.fields[i]
if f.DeclaringType==t and not f.IsStatic then
log('FIELD '..tostring(f))
if state.typeindex==1 and state.event then
local v=f:GetValue(state.event)
if v==nil then log('VALUE '..f.Name..'=nil')
elseif f.FieldType.IsPrimitive or f.FieldType.IsEnum or f.FieldType.FullName=='System.String' then log('VALUE '..f.Name..'='..tostring(v))
else log('VALUE_TYPE '..f.Name..' '..v:GetType().FullName)end
end
end
end
state.index=state.index+8
if state.index>=state.fields.Length then
state.typeindex=state.typeindex+1;state.methods=nil;state.fields=nil;state.index=0
state.step=state.typeindex>2 and 3 or 2
end
return
end
if state.step==3 then
local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GridFightPreparationPage')
local obj=page and page.LuaTable
if type(obj)=='table'then
local names={'_on_start_battle_click','_enter_battle','_check_enter_battle_by_reason'}
for _,name in ipairs(names)do
local fn=obj[name]
if type(fn)=='function'then
log('LUA_FUNCTION '..name)
for i=1,24 do
local key,val=debug.getupvalue(fn,i);if not key then break end
log('LUA_UPVALUE '..name..' '..i..' '..key..' '..type(val))
end
end
end
else log('NO preparation page')end
state.done=true;state.module=nil;state.session=nil;state.event=nil;log('END')
end
end
local ok,e=pcall(capture)
if not ok then _G.__gacCwTeaBreakRuntime2={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_TEA_RUNTIME2] ERROR '..tostring(e))end
end
+32
View File
@@ -0,0 +1,32 @@
-- IDs are injected from the current revision's captured role configuration.
-- Read config rows only, four traits per heartbeat, never during Battle.
do local function capture()
local s=_G.__gacCwTraitRows1 or {index=1};_G.__gacCwTraitRows1=s
if s.done then return end
local ids=_G.__gacCwTraitCaptureIDs
if type(ids)~='table'or #ids>256 then error('bounded captured trait ID plan required')end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.GridFightModule
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_TRAIT_ROWS] '..tostring(x))end
local function rows(label,obj)
local fs=obj:GetType():GetFields(flags)
for i=0,math.min(fs.Length-1,47)do local f=fs[i]
if not f.IsStatic and f.FieldType.FullName:find('Row$')then local row=f:GetValue(obj);if row then local encoded=CS.Newtonsoft.Json.JsonConvert.SerializeObject(row);if #encoded>32768 then error('trait row bound')end;log(label..' '..encoded)end end
end
end
for _=1,4 do
if s.index>#ids then s.done=true;log('END');return end
local id=ids[s.index];s.index=s.index+1
local config=module:GetTraitConfig(id)
if config then
rows('BASE',config)
local f=config:GetType():GetField('_layerConfigs',flags);local layers=f and f:GetValue(config)
if layers then if layers.Count>32 then error('trait layer bound')end;for i=0,layers.Count-1 do rows('LAYER',layers[i])end end
local effects=config.TraitEffectList
if effects then for i=0,math.min(effects.Length-1,15)do local effect=module:GetTraitEffectConfig(effects[i]);if effect then rows('EFFECT',effect)end end end
else log('MISSING '..id)end
end
end;local ok,e=pcall(capture);if not ok then _G.__gacCwTraitRows1={done=true};CS.UnityEngine.Debug.Log('[GAC_CW_TRAIT_ROWS] ERROR '..tostring(e))end end
+27
View File
@@ -0,0 +1,27 @@
-- Caller supplies IDs/world level from the catalog/world resource. Flat rows
-- only; no avatar creation or level graph. Two lookups per heartbeat.
do local function capture()
local s=_G.__gacCwTrialExport1 or {index=1};_G.__gacCwTrialExport1=s
if s.done then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_TRIAL_ROW] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
local plan=_G.__GAC_CW_TRIAL_PLAN;if not plan or #plan>128 then error('bounded capture plan missing')end
if not s.begun then log('BEGIN '..#plan);s.begun=true end
for batch=1,2 do
local request=plan[s.index];if not request then s.done=true;log('END');return end
if s.world==nil then s.world=request.WorldLevel end
local row=CS.RPG.GameCore.SpecialAvatarExcelTable.GetData(request.SpecialAvatarID,s.world)
if row then
if row.SpecialAvatarID~=request.SpecialAvatarID or row.AvatarID~=request.AvatarID or row.WorldLevel~=s.world then error('trial row identity differs')end
local text=tostring(CS.Newtonsoft.Json.JsonConvert.SerializeObject(row));if #text>65536 then error('row exceeds bound')end
log('ROW '..text);s.index=s.index+1;s.world=nil
elseif s.world>0 then s.world=s.world-1
else error('trial row not found in supported world levels')end
end
end)
if not ok then s.done=true;log('ERROR '..tostring(err))end
end;capture()end
+19
View File
@@ -0,0 +1,19 @@
-- Metadata for four named flat trial tables only; no rows/loaders invoked.
do
local s=_G.__gacCwTrialSchema1 or 1
local names={'SpecialAvatarExcelTable','SpecialAvatarRow','SpecialAvatarRelicExcelTable','AvatarExcelTable'}
if s>#names then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
_G.__gacCwTrialSchema1=s+1
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_TRIAL_SCHEMA] '..tostring(x))end
local ok,err=pcall(function()
local t=typeof(CS.RPG.GameCore.GridFightRoleConfigRow).Assembly:GetType('RPG.GameCore.'..names[s]);log('TYPE '..names[s]..' '..tostring(t));if not t then return end
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,47)do log('FIELD '..tostring(fs[i]))end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,47)do if ms[i].DeclaringType==t then log('METHOD '..tostring(ms[i])..' HANDLE '..tostring(ms[i].MethodHandle.Value))end end
end)
if not ok then log('ERROR '..tostring(err))end
if s==#names then log('END')end
end
+23
View File
@@ -0,0 +1,23 @@
-- Isolated protobuf conversion only, verified native 166b4da0 / 166b5510:
-- allocates data objects and copies scalar fields; no scene/table/game action.
do local function capture()
if _G.__gacCwWaveConfigIdentity3 then return end;_G.__gacCwWaveConfigIdentity3=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_CONFIG_ID] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local p=typeof(CS.RPG.GameCore.BattleInstance):GetMethod('GetCurWaveParam',flags).ReturnType
local helper=p.Assembly:GetType('FKOEECADKFF');local ms=helper:GetMethods(flags);local convert
for i=0,math.min(ms.Length-1,63)do local m=ms[i];if m.ReturnType==p and m:GetParameters().Length==1 then convert=m;break end end
if not convert then error('Named converter missing')end
local sourceType=convert:GetParameters()[0].ParameterType;local raw=CS.System.Activator.CreateInstance(sourceType)
local setters=sourceType:GetMethods(flags);for i=0,math.min(setters.Length-1,95)do local m=setters[i];local ps=m:GetParameters();if m.DeclaringType==sourceType and not m.IsStatic and m.ReturnType==typeof(CS.System.Void) and ps.Length==1 and ps[0].ParameterType==typeof(CS.System.UInt32)then raw[m.Name](raw,1)end end
local args=CS.System.Array.CreateInstance(typeof(CS.System.Object),1);args:SetValue(raw,0)
local invokeFlags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),0)
local converted=convert:Invoke(nil,invokeFlags,nil,args,nil);local config=p:GetField('FGNHFJHBPIM',flags):GetValue(converted)
if not config then error('Isolated conversion has no config reference')end
local t=config:GetType();log('TYPE '..t.FullName..' BASE '..tostring(t.BaseType))
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,31)do log('FIELD '..tostring(fs[i]))end
local methods=t:GetMethods(flags);for i=0,math.min(methods.Length-1,31)do local m=methods[i];if m.DeclaringType==t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end)
if not ok then log('ERROR '..tostring(err))end;log('END')
end;capture()end
+29
View File
@@ -0,0 +1,29 @@
-- Metadata only for four named battle data types. No loader/gameplay method
-- is invoked. Inspect at most 32 methods per heartbeat, bounded to 256/type.
do local function capture()
local s=_G.__gacCwWaveConsumer3 or {type=1,method=0};_G.__gacCwWaveConsumer3=s
local names={'RPG.GameCore.GridFightManager','RPG.Client.BattleGamePhase'}
if s.type>#names then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_CONSUMER] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local types={typeof(CS.RPG.GameCore.GridFightManager),typeof(CS.RPG.Client.BattleGamePhase)}
local t=types[s.type]
if not t then log('MISSING '..names[s.type]);s.type=s.type+1;s.method=0;return end
if s.method==0 then
log('TYPE '..t.FullName)
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,47)do log('FIELD '..tostring(fs[i]))end
end
local ms=t:GetMethods(flags)
for i=s.method,math.min(s.method+31,ms.Length-1,255)do
local m=ms[i]
if m.DeclaringType==t and (m.Name:find('Wave')or m.Name:find('Elite')or m.Name:find('Formation')or m.Name:find('Scene')or m.Name:find('Init'))then
log('METHOD '..t.FullName..' '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))
end
end
s.method=s.method+32
if s.method>=ms.Length or s.method>=256 then log('TYPE_END '..t.FullName..' inspected='..math.min(ms.Length,256)..' total='..ms.Length);s.type=s.type+1;s.method=0 end
end)
if not ok then log('ERROR '..tostring(err));s.type=#names+1 end
if s.type>#names then log('END')end
end;capture()end
+16
View File
@@ -0,0 +1,16 @@
-- Metadata only for the observed internal wave parameter, its config field,
-- and the exact conversion helper seen in the Client stack. One type/HB.
do local function capture()
local s=_G.__gacCwWaveConversion1 or 1;if s>3 then return end;_G.__gacCwWaveConversion1=s+1
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_CONVERT] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local p=typeof(CS.RPG.GameCore.BattleInstance):GetMethod('GetCurWaveParam',flags).ReturnType
local types={p,p:GetField('FGNHFJHBPIM',flags).FieldType,p.Assembly:GetType('FKOEECADKFF')}
local t=types[s];if not t then log('MISSING '..s);return end;log('TYPE '..t.FullName)
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,47)do log('FIELD '..tostring(fs[i]))end
local cs=t:GetConstructors(flags);for i=0,math.min(cs.Length-1,7)do log('CTOR '..tostring(cs[i])..' HANDLE '..tostring(cs[i].MethodHandle.Value))end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,47)do local m=ms[i];if m.DeclaringType==t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end)
if not ok then log('ERROR '..tostring(err))end;if s==3 then log('END')end
end;capture()end
+22
View File
@@ -0,0 +1,22 @@
-- Two named wave/container types only; metadata, 32 methods per heartbeat.
do local function capture()
local s=_G.__gacCwWaveInfo1 or {kind=1,index=0};_G.__gacCwWaveInfo1=s
if s.kind>2 then return end
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_INFO] '..tostring(x))end
local ok,err=pcall(function()
local types={typeof(CS.RPG.GameCore.GridFightWaveInfo),typeof(CS.RPG.GameCore.BattleInstance)}
local t=types[s.kind];local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
if s.index==0 then
log('TYPE '..t.FullName)
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,63)do log('FIELD '..tostring(fs[i]))end
local cs=t:GetConstructors(flags);for i=0,math.min(cs.Length-1,7)do log('CTOR '..tostring(cs[i])..' HANDLE '..tostring(cs[i].MethodHandle.Value))end
end
local ms=t:GetMethods(flags)
for i=s.index,math.min(s.index+31,ms.Length-1,255)do
local m=ms[i];if m.DeclaringType==t and (s.kind==1 or m.Name:find('Wave')or m.Name:find('Monster')or m.Name:find('Init'))then log('METHOD '..t.FullName..' '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end
end
s.index=s.index+32;if s.index>=ms.Length or s.index>=256 then log('TYPE_END '..t.FullName..' total='..ms.Length);s.kind=s.kind+1;s.index=0 end
end)
if not ok then log('ERROR '..tostring(err));s.kind=3 end
if s.kind>2 then log('END')end
end;capture()end
+14
View File
@@ -0,0 +1,14 @@
-- Current wave parameter identity and named lineup metadata; no game actions.
do local function capture()
if _G.__gacCwWaveParamIdentity1 then return end;_G.__gacCwWaveParamIdentity1=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_PARAM_ID] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local m=typeof(CS.RPG.GameCore.BattleInstance):GetMethod('GetCurWaveParam',flags)
local t=m.ReturnType;log('TYPE '..t.FullName)
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,47)do local f=fs[i];if f.IsLiteral then log('CONST '..f.Name..' '..tostring(f:GetRawConstantValue()))else log('FIELD '..tostring(f))end end
local ps=t:GetProperties(flags);for i=0,math.min(ps.Length-1,31)do local p=ps[i];local g=p:GetGetMethod(true);log('PROPERTY '..tostring(p)..' HANDLE '..tostring(g and g.MethodHandle.Value))end
local l=typeof(CS.RPG.GameCore.BattleLineupData);log('TYPE '..l.FullName);local fields=l:GetFields(flags);for i=0,math.min(fields.Length-1,63)do log('FIELD '..tostring(fields[i]))end
end)
if not ok then log('ERROR '..tostring(err))end;log('END')
end;capture()end
+13
View File
@@ -0,0 +1,13 @@
-- Named config-reference type from the existing Client stack; metadata only.
do local function capture()
if _G.__gacCwWaveRef1 then return end;_G.__gacCwWaveRef1=true
local log=function(x)CS.UnityEngine.Debug.Log('[GAC_CW_WAVE_REF] '..tostring(x))end
local ok,err=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local p=typeof(CS.RPG.GameCore.BattleInstance):GetMethod('GetCurWaveParam',flags).ReturnType
local t=p.Assembly:GetType('NMBMHCNGCFG');if not t then log('MISSING');return end;log('TYPE '..t.FullName..' BASE '..tostring(t.BaseType))
local fs=t:GetFields(flags);for i=0,math.min(fs.Length-1,31)do log('FIELD '..tostring(fs[i]))end
local ms=t:GetMethods(flags);for i=0,math.min(ms.Length-1,31)do local m=ms[i];if m.DeclaringType==t then log('METHOD '..tostring(m)..' HANDLE '..tostring(m.MethodHandle.Value))end end
end)
if not ok then log('ERROR '..tostring(err))end;log('END')
end;capture()end
+70
View File
@@ -0,0 +1,70 @@
"""Read a named embedded FileDescriptorProto without executing its carrier."""
import argparse
import hashlib
import json
from pathlib import Path
from google.protobuf import descriptor_pb2, json_format
def varint(data, pos):
value = 0
for shift in range(0, 70, 7):
byte = data[pos]
pos += 1
value |= (byte & 127) << shift
if byte < 128:
return value, pos
raise ValueError('Oversized varint')
def extract(data, name):
encoded = name.encode()
occurrences = []
pos = 0
while (pos := data.find(encoded, pos)) >= 0:
if len(encoded) < 128 and data[pos-2:pos] == bytes([10, len(encoded)]):
occurrences.append(pos-2)
pos += len(encoded)
valid = []
for start in occurrences:
pos = start
while pos < min(len(data), start+16*1024*1024):
tag, pos = varint(data, pos)
if tag & 7 != 2 or tag >> 3 not in (1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 14):
break
size, pos = varint(data, pos)
end = pos+size
if end > len(data):
break
if tag >> 3 == 12 and data[pos:end] in (b'proto2', b'proto3'):
desc = descriptor_pb2.FileDescriptorProto.FromString(data[start:end])
if desc.name == name:
valid.append((start, end, desc))
break
pos = end
if len(valid) != 1:
raise ValueError(f'Expected one complete descriptor {name}, found {len(valid)}')
return valid[0]
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--binary', type=Path, required=True)
p.add_argument('--descriptor', required=True)
p.add_argument('--output', type=Path, required=True)
args = p.parse_args()
data = args.binary.read_bytes()
start, end, desc = extract(data, args.descriptor)
args.output.mkdir(parents=True, exist_ok=True)
stem = Path(args.descriptor).stem
provenance = {'artifact_sha256':hashlib.sha256(data).hexdigest(), 'descriptor':desc.name, 'offset':start, 'length':end-start, 'descriptor_sha256':hashlib.sha256(data[start:end]).hexdigest()}
(args.output/(stem+'.descriptor.pb')).write_bytes(data[start:end])
(args.output/(stem+'.descriptor.json')).write_text(json.dumps({'provenance':provenance, 'descriptor':json_format.MessageToDict(desc, preserving_proto_field_name=True)}, indent=2), encoding='utf8')
cmds = {v.name:v.number for enum in desc.enum_type if enum.name.startswith('Cmd') for v in enum.value}
if cmds:
(args.output/'cmdids.json').write_text(json.dumps({'provenance':provenance, 'commands':cmds},indent=2), encoding='utf8')
print(json.dumps({**provenance, 'enums':len(desc.enum_type), 'messages':len(desc.message_type), 'commands':len(cmds)}))
if __name__ == '__main__':
main()
+91
View File
@@ -0,0 +1,91 @@
"""Read-only Go pclntab and direct-call inventory for an explicitly selected PE.
This recovers compiled function names and machine-code call edges, not source
code or proof that a handler succeeds. CmdIDs require separate wire evidence.
"""
import argparse
import hashlib
import json
import pathlib
import struct
from capstone import Cs, CS_ARCH_X86, CS_MODE_64
from capstone.x86 import X86_OP_IMM
def dump(path, output):
data = path.read_bytes()
pe = struct.unpack_from('<I', data, 0x3c)[0]
if data[:2] != b'MZ' or data[pe:pe+4] != b'PE\0\0':
raise ValueError('Expected PE image')
count = struct.unpack_from('<H', data, pe+6)[0]
optional = pe+24
if struct.unpack_from('<H', data, optional)[0] != 0x20b:
raise ValueError('Expected PE32+')
image_base = struct.unpack_from('<Q', data, optional+24)[0]
sections = []
section_table = optional+struct.unpack_from('<H', data, pe+20)[0]
for i in range(count):
pos = section_table+i*40
name = data[pos:pos+8].rstrip(b'\0').decode('ascii')
vsize, rva, size, raw = struct.unpack_from('<IIII', data, pos+8)
sections.append((name, rva, size, raw))
def offset(va):
for _, rva, size, raw in sections:
if rva <= va-image_base < rva+size:
return raw+va-image_base-rva
raise ValueError(f'Unmapped VA {va:#x}')
candidates = []
start = 0
while (start := data.find(b'\xf1\xff\xff\xff\0\0\x01\x08', start)) >= 0:
try:
nfunc, nfiles, text, names, cu, files, pctab, pcln = struct.unpack_from('<8Q', data, start+8)
if 0 < nfunc < 1000000 and 0 < nfiles < 1000000 and start+pcln+(nfunc+1)*8 < len(data):
if text == 0:
text = image_base+next(rva for name,rva,_,_ in sections if name == '.text')
offset(text)
candidates.append((start, nfunc, text, names, pcln))
except (ValueError, struct.error):
pass
start += 8
if len(candidates) != 1:
raise ValueError(f'Expected one valid Go1.20+ pclntab, found {len(candidates)}')
header, nfunc, text, names, pcln = candidates[0]
funcs = []
for i in range(nfunc):
entry, info = struct.unpack_from('<II', data, header+pcln+i*8)
end = struct.unpack_from('<I', data, header+pcln+(i+1)*8)[0]
nameoff = struct.unpack_from('<i', data, header+pcln+info+4)[0]
pos = header+names+nameoff
name = data[pos:data.index(b'\0', pos)].decode('utf8')
funcs.append({'name': name, 'va': hex(text+entry), 'rva': hex(text+entry-image_base), 'size': end-entry})
byva = {int(f['va'], 16): f['name'] for f in funcs}
cs = Cs(CS_ARCH_X86, CS_MODE_64)
cs.detail = True
selected = [f for f in funcs if 'gridfight' in f['name'].lower() or ('firefly-go' in f['name'] and any(x in f['name'].lower() for x in ('register', 'route', 'dispatch')))]
for f in selected:
va = int(f['va'], 16)
raw = offset(va)
f['calls'] = []
asm = []
for ins in cs.disasm(data[raw:raw+f['size']], va):
target = None
if ins.mnemonic in ('call', 'jmp') and ins.operands and ins.operands[0].type == X86_OP_IMM:
target = ins.operands[0].imm
if target in byva:
f['calls'].append({'at':hex(ins.address), 'target':byva[target], 'va':hex(target)})
asm.append(f'{ins.address:#x}: {ins.mnemonic} {ins.op_str}' + (f' ; {byva[target]}' if target in byva else ''))
f['assembly'] = asm
output.mkdir(parents=True, exist_ok=True)
provenance = {'sha256':hashlib.sha256(data).hexdigest(), 'bytes':len(data), 'image_base':hex(image_base), 'pclntab_offset':header, 'function_count':nfunc, 'limitations':['Compiled symbols and direct-call edges only; no source reconstruction or runtime success assertion.']}
(output/'functions.json').write_text(json.dumps({'provenance':provenance,'functions':funcs}, indent=2), encoding='utf8')
(output/'gridfight-workflow.json').write_text(json.dumps({'provenance':provenance,'functions':selected}, indent=2), encoding='utf8')
print(json.dumps({**provenance,'selected_functions':len(selected)}))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=pathlib.Path, required=True)
parser.add_argument('--output', type=pathlib.Path, required=True)
args = parser.parse_args()
dump(args.binary, args.output)
+15
View File
@@ -0,0 +1,15 @@
-- Asset manifest presence only, eight captured paths per heartbeat; no loading.
do local function capture()
local s=_G.__gacEventAssets1 or {index=1};_G.__gacEventAssets1=s
local paths={"Assets/AsbRes/UI/Quest/Widget/QuestBlindBoxPanel.prefab","SpriteOutput/Quest/TabIcon/BtnAbyssAlternation.png","SpriteOutput/Quest/TabIcon/BtnAbyssReward_1207.png","SpriteOutput/Quest/TabIcon/BtnActivityActive.png","SpriteOutput/Quest/TabIcon/BtnActivityMarble.png","SpriteOutput/Quest/TabIcon/BtnActivityRelicBox.png","SpriteOutput/Quest/TabIcon/BtnAmphoreusCurio.png","SpriteOutput/Quest/TabIcon/BtnAnniversarySevenDayVersion0210.png","SpriteOutput/Quest/TabIcon/BtnAntiVirus.png","SpriteOutput/Quest/TabIcon/BtnBelogogSubmit.png","SpriteOutput/Quest/TabIcon/BtnBoxingClubResonance.png","SpriteOutput/Quest/TabIcon/BtnCakeCatch.png","SpriteOutput/Quest/TabIcon/BtnChallengeBossReward_1214.png","SpriteOutput/Quest/TabIcon/BtnChallengeStoryReward_1110.png","SpriteOutput/Quest/TabIcon/BtnChenLingBattle.png","SpriteOutput/Quest/TabIcon/BtnClockPark.png","SpriteOutput/Quest/TabIcon/BtnComicDessert.png","SpriteOutput/Quest/TabIcon/BtnDoubleCocoon.png","SpriteOutput/Quest/TabIcon/BtnEvolveBuild.png","SpriteOutput/Quest/TabIcon/BtnEvolveBuildSC.png","SpriteOutput/Quest/TabIcon/BtnExpeditionBattle.png","SpriteOutput/Quest/TabIcon/BtnFantasticStory.png","SpriteOutput/Quest/TabIcon/BtnFarmMultipleDrop.png","SpriteOutput/Quest/TabIcon/BtnFarmMultipleDrop2Set.png","SpriteOutput/Quest/TabIcon/BtnFinalityBattle.png","SpriteOutput/Quest/TabIcon/BtnFistClubQuestMission.png","SpriteOutput/Quest/TabIcon/BtnFourthWorldRaidCollectionGame.png","SpriteOutput/Quest/TabIcon/BtnGridFightS1.png","SpriteOutput/Quest/TabIcon/BtnGridFightS2.png","SpriteOutput/Quest/TabIcon/BtnImgSingleTask.png","SpriteOutput/Quest/TabIcon/BtnLocalLegend.png","SpriteOutput/Quest/TabIcon/BtnPhotoExhibition.png","SpriteOutput/Quest/TabIcon/BtnQuesAlley.png","SpriteOutput/Quest/TabIcon/BtnQuesHeliobus.png","SpriteOutput/Quest/TabIcon/BtnQuesPlayerReturn.png","SpriteOutput/Quest/TabIcon/BtnQuestActivityElationBattle.png","SpriteOutput/Quest/TabIcon/BtnQuestActivityStrongChallenge.png","SpriteOutput/Quest/TabIcon/BtnQuestAetherDivide.png","SpriteOutput/Quest/TabIcon/BtnQuestAmphoreusActivityTimeLimit.png","SpriteOutput/Quest/TabIcon/BtnQuestB51Racing.png","SpriteOutput/Quest/TabIcon/BtnQuestBlindBox.png","SpriteOutput/Quest/TabIcon/BtnQuestCakeRace.png","SpriteOutput/Quest/TabIcon/BtnQuestChenLingFes.png","SpriteOutput/Quest/TabIcon/BtnQuestChimera.png","SpriteOutput/Quest/TabIcon/BtnQuestChimeraDuel.png","SpriteOutput/Quest/TabIcon/BtnQuestColleague.png","SpriteOutput/Quest/TabIcon/BtnQuestDiceCombat.png","SpriteOutput/Quest/TabIcon/BtnQuestDiceCombatV2.png","SpriteOutput/Quest/TabIcon/BtnQuestDrinkMaker.png","SpriteOutput/Quest/TabIcon/BtnQuestDrinkMakerSecond.png","SpriteOutput/Quest/TabIcon/BtnQuestElfRestaurant.png","SpriteOutput/Quest/TabIcon/BtnQuestExploitMission.png","SpriteOutput/Quest/TabIcon/BtnQuestFeverTime.png","SpriteOutput/Quest/TabIcon/BtnQuestFightFest.png","SpriteOutput/Quest/TabIcon/BtnQuestGraffiti.png","SpriteOutput/Quest/TabIcon/BtnQuestGuessTheSilhouetteTask.png","SpriteOutput/Quest/TabIcon/BtnQuestHarmonyHero.png","SpriteOutput/Quest/TabIcon/BtnQuestHipplen.png","SpriteOutput/Quest/TabIcon/BtnQuestMatchThree.png","SpriteOutput/Quest/TabIcon/BtnQuestMechCraft.png","SpriteOutput/Quest/TabIcon/BtnQuestMedicineAssignment.png","SpriteOutput/Quest/TabIcon/BtnQuestMiniGame.png","SpriteOutput/Quest/TabIcon/BtnQuestMonopoly.png","SpriteOutput/Quest/TabIcon/BtnQuestMuseum.png","SpriteOutput/Quest/TabIcon/BtnQuestMusicRhythm.png","SpriteOutput/Quest/TabIcon/BtnQuestNewbieMission.png","SpriteOutput/Quest/TabIcon/BtnQuestParkour.png","SpriteOutput/Quest/TabIcon/BtnQuestPenaconyActivityTimeLimit.png","SpriteOutput/Quest/TabIcon/BtnQuestPixAir.png","SpriteOutput/Quest/TabIcon/BtnQuestPlanarcadiaActivityTimeLimit.png","SpriteOutput/Quest/TabIcon/BtnQuestPlanetFes.png","SpriteOutput/Quest/TabIcon/BtnQuestRealityShow.png","SpriteOutput/Quest/TabIcon/BtnQuestReturnRecallVer470.png","SpriteOutput/Quest/TabIcon/BtnQuestRogueNous.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial10.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial11.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial12.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial13.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial14.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial15.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial17.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial19.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial2.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial21.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial23.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial25.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial27.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial28.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial3.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial30.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial32.png","SpriteOutput/Quest/TabIcon/BtnQuestRoleTrial4.png","SpriteOutput/Quest/TabIcon/BtLine truncated
if s.index>#paths then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)then return end
for n=1,8 do
if s.index>#paths then CS.UnityEngine.Debug.Log('[GAC_EVENT_ASSET] END');return end
local path=paths[s.index];s.index=s.index+1
CS.UnityEngine.Debug.Log('[GAC_EVENT_ASSET] '..tostring(CS.RPG.Client.GlobalVars.s_AssetMgr:Exists(path))..' '..path)
end
end;local ok,e=pcall(capture);if not ok then _G.__gacEventAssets1={index=99999};CS.UnityEngine.Debug.Log('[GAC_EVENT_ASSET] ERROR '..tostring(e))end end
+49
View File
@@ -0,0 +1,49 @@
-- Temporary analysis only. Keep outside scripts/ unless deliberately dispatched.
-- Twelve members per heartbeat, exact live module types, no scene factory calls.
do
local function capture()
local s=_G.__gacEventStellaMeta1 or {module=1,index=0,kind='fields'}
_G.__gacEventStellaMeta1=s
local names={'ComicDessertModule','ChenLingBattleModule','FiveDimModule'}
if s.module>#names then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
if not manager then return end
local phase=typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then return end
local modules=CS.RPG.Client.GlobalVars.s_ModuleManager
if not modules then return end
local value=modules[names[s.module]]
local log=function(v) CS.UnityEngine.Debug.Log('[GAC_EVENT_STELLA_META] '..tostring(v)) end
if not value then log('MISSING '..names[s.module]);s.module=s.module+1;return end
if not s.target then
s.target=value:GetType();s.members=s.target:GetFields(flags)
log('TYPE '..s.target.FullName)
end
for n=1,12 do
if s.index>=s.members.Length then
if s.kind=='fields' then
s.kind='methods';s.index=0;s.members=s.target:GetMethods(flags)
return
end
log('END_TYPE '..s.target.FullName)
s.module=s.module+1;s.target=nil;s.members=nil;s.kind='fields';s.index=0
if s.module>#names then log('END') end
return
end
local member=s.members[s.index];s.index=s.index+1
if member.DeclaringType==s.target then
if s.kind=='fields' then
log('FIELD '..member.Name..' '..member.FieldType.FullName)
else
log('METHOD '..tostring(member)..' HANDLE '..tostring(member.MethodHandle.Value))
end
end
end
end
local ok,e=pcall(capture)
if not ok then
_G.__gacEventStellaMeta1={module=99}
CS.UnityEngine.Debug.Log('[GAC_EVENT_STELLA_META] ERROR '..tostring(e))
end
end
+129
View File
@@ -0,0 +1,129 @@
"""Retain complete bounded ComicDessert/FiveDim repository captures offline.
This does not unlock activities, fabricate initial progress or modify game saves.
An incomplete capture fails rather than generating a misleading catalog.
"""
import argparse
import hashlib
import json
import re
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description=__doc__)
inputs = parser.add_mutually_exclusive_group(required=True)
inputs.add_argument('--log', type=Path)
inputs.add_argument('--evidence', type=Path, help='Complete exact flat-getter Frida evidence')
parser.add_argument('--assembly-sha256', required=True)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
if not re.fullmatch(r'[0-9a-f]{64}', args.assembly_sha256):
raise SystemExit('An exact verified assembly SHA256 is required')
raw = (args.evidence or args.log).read_bytes()
if args.evidence:
evidence = json.loads(raw)
observed = evidence['normalized_result']
if observed.get('messages_truncated'):
raise SystemExit('Truncated evidence cannot produce a catalog')
tables, counts, complete, scripts = {}, {}, set(), set()
ended = False
for message in observed['messages']:
value = message.get('payload', {})
if value.get('kind') == 'ATTACHED' and value.get('scope', '').startswith('two exact lazy flat getters'):
scripts.add(message['script_id'])
if message.get('script_id') not in scripts:
continue
kind, name = value.get('kind'), value.get('table')
if kind == 'ERROR':
raise SystemExit('Flat capture failed: ' + value.get('error', 'unknown'))
if kind == 'BEGIN':
if name in tables or name not in ('cycle', 'level') or not 0 < value['count'] <= 256:
raise SystemExit('Unexpected or empty table')
tables[name], counts[name] = [], value['count']
elif kind == 'ROWS':
if name not in tables or name in complete:
raise SystemExit('Rows outside capture boundaries')
tables[name].extend(json.loads(row) for row in value['rows'])
elif kind == 'TABLE_END':
if name not in tables or len(tables[name]) != counts[name] or value['count'] != counts[name]:
raise SystemExit('Table count mismatch')
complete.add(name)
elif kind == 'END':
ended = True
if len(scripts) != 1 or not ended or complete != {'cycle', 'level'}:
raise SystemExit('Complete single flat capture required')
level_ids = {row['ID'] for row in tables['level']}
if len(level_ids) != len(tables['level']) or len({r['ID'] for r in tables['cycle']}) != len(tables['cycle']):
raise SystemExit('Duplicate configuration IDs')
if any(level not in level_ids for row in tables['cycle'] for level in row['LevelIDList']):
raise SystemExit('Cycle references missing levels')
result = {
'format_version': 1,
'client_assembly_sha256': args.assembly_sha256,
'evidence_id': evidence['evidence_id'],
'source': 'Current named ComicDessert CycleRow/LevelRow FromBinary -> dictionary Add -> lazy flat GetAll chains; no gameplay or scene factories',
'capture_sha256': hashlib.sha256(raw).hexdigest(),
'cycles': tables['cycle'],
'levels': tables['level'],
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'cycles': len(tables['cycle']), 'levels': len(tables['level']), 'output': str(args.output)}))
return
marker = '[GAC_COMIC_STELLA_CONFIG] '
repositories, expected, completed, configs = {}, {}, set(), {}
ended = False
for line in raw.decode('utf-8', errors='replace').splitlines():
if marker not in line:
continue
value = line.split(marker, 1)[1]
if value.startswith('ERROR '):
raise SystemExit('Capture stopped: ' + value)
if value.startswith('BEGIN '):
match = re.fullmatch(r'BEGIN (\w+) count=(\d+)', value)
if not match or match[1] in repositories:
raise SystemExit('Repeated/incompatible capture: use a single run')
name, count = match[1], int(match[2])
if count > 256:
raise SystemExit('Repository outside capture bound')
repositories[name], expected[name] = [], count
elif value.startswith('ROW '):
_, name, body = value.split(' ', 2)
if name not in repositories or name in completed:
raise SystemExit('Repository row outside its bounded run')
repositories[name].append(json.loads(body))
elif value.startswith('END_REPOSITORY '):
match = re.fullmatch(r'END_REPOSITORY (\w+) rows=(\d+)', value)
if not match or match[1] not in repositories:
raise SystemExit('Unexpected repository completion')
name = match[1]
if len(repositories[name]) != int(match[2]) or len(repositories[name]) != expected[name]:
raise SystemExit('Repository row count mismatch: ' + name)
completed.add(name)
elif value.startswith('FIVEDIM_CONFIG '):
_, name, body = value.split(' ', 2)
if name in configs:
raise SystemExit('Duplicate FiveDim config capture: ' + name)
configs[name] = json.loads(body)
elif value == 'END':
ended = True
required = {'CycleConfigRepository', 'LevelConfigRepository'}
if not ended or completed != required or not all(repositories.get(name) for name in required):
raise SystemExit('Complete nonempty cycle and level repositories are required')
result = {
'format_version': 1,
'client_assembly_sha256': args.assembly_sha256,
'source': 'Read-only existing ComicDessert repositories and selected FiveDim config; no gameplay method invocation',
'capture_sha256': hashlib.sha256(raw).hexdigest(),
'repositories': repositories,
'five_dim_config': configs,
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'output': str(args.output), 'rows': {k: len(v) for k, v in repositories.items()},
'five_dim_config': sorted(configs)}, ensure_ascii=False))
if __name__ == '__main__':
main()
+32
View File
@@ -0,0 +1,32 @@
"""Import bounded, identity-checked current Client monster row captures."""
import argparse
import hashlib
import json
from pathlib import Path
p = argparse.ArgumentParser(description=__doc__)
for name in ('capture', 'catalog', 'output'):
p.add_argument('--' + name, type=Path, required=True)
a = p.parse_args()
capture = json.loads(a.capture.read_text(encoding='utf-8-sig'))
catalog = json.loads(a.catalog.read_text(encoding='utf-8-sig'))
rows = {}
for item in capture['rows']:
row = item['row']
key = item['requested_id']
if key != row['MonsterID'] or key in rows or not key:
raise ValueError('Monster row identity differs/duplicated')
for star in range(1, 5):
if row['Star' + str(star) + 'EliteGroup3'] <= 0:
raise ValueError('Monster star group missing')
rows[key] = row
if len(set(capture['plan'])) != len(capture['plan']) or set(rows) != set(capture['plan']):
raise ValueError('Incomplete monster capture plan')
result = dict(format_version=1, target_client_version=catalog['client_version'],
target_assembly_sha256=catalog['provenance']['game_assembly_sha256'],
source_capture_sha256=hashlib.sha256(a.capture.read_bytes()).hexdigest(),
scope='Current saved run camps and its reference encounter camp only; not all monster rows.',
encounter_camp_policy='uniform_compatible_active_camp_same_tier',
entries=[rows[k] for k in sorted(rows)])
a.output.write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
print('Exported', len(rows), 'current monster rows')
+37
View File
@@ -0,0 +1,37 @@
"""Export completed, identity-checked native SpecialAvatar rows for CW."""
import argparse
import hashlib
import json
from pathlib import Path
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--capture', type=Path, required=True)
p.add_argument('--catalog', type=Path, required=True)
p.add_argument('--output', type=Path, required=True)
a = p.parse_args()
raw = a.capture.read_bytes()
lines = raw.decode('utf-8-sig').splitlines()
if '[GAC_CW_TRIAL_ROW] END' not in lines or any('[GAC_CW_TRIAL_ROW] ERROR' in l for l in lines):
raise ValueError('native trial capture did not complete')
rows = [json.loads(l.split(' ROW ', 1)[1]) for l in lines if '[GAC_CW_TRIAL_ROW] ROW ' in l]
c = json.loads(a.catalog.read_text(encoding='utf-8-sig'))
roles = {r['Config']['SpecialAvatarID']: r['Config']['AvatarID'] for r in c['active_handbook_default_roles']}
seen = set()
for r in rows:
key = r['SpecialAvatarID'], r['WorldLevel']
if key in seen or roles.get(key[0]) != r['AvatarID'] or not r['Level']:
raise ValueError('native trial identity invalid/duplicate')
seen.add(key)
if not rows:
raise ValueError('trial capture empty')
result = dict(format_version=1, target_client_version=c['client_version'], target_assembly_sha256=c['provenance']['game_assembly_sha256'],
source_capture_sha256=hashlib.sha256(raw).hexdigest(),
scope='Current catalog fake-avatar trials at captured world levels. SpecialAvatarExcelTable.GetData, bounded flat-row access; no scene loader. Other trial profiles remain uncaptured.', entries=rows)
a.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
print(f'Exported {len(rows)} current trial rows')
if __name__ == '__main__':
main()
+58
View File
@@ -0,0 +1,58 @@
"""Project FFGO's labelled Bonfire reference tables without copying a run.
The target identity is a compatibility gate for the existing server catalog,
not a claim that these rows were captured from that Client assembly.
"""
import argparse
import hashlib
import json
from pathlib import Path
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--excel', type=Path, required=True)
p.add_argument('--custom', type=Path, required=True)
p.add_argument('--catalog', type=Path, required=True)
p.add_argument('--source-binary-sha256', required=True)
p.add_argument('--output', type=Path, required=True)
a = p.parse_args()
if len(bytes.fromhex(a.source_binary_sha256)) != 32:
raise ValueError('Expected SHA256 identity')
excel_raw, custom_raw = a.excel.read_bytes(), a.custom.read_bytes()
excel, custom = json.loads(excel_raw), json.loads(custom_raw)
catalog = json.loads(a.catalog.read_bytes())
def rows(name, fields):
result = []
for row in excel[name]:
copy = dict(row)
for field in fields:
if field in copy:
copy[field] = int(copy[field])
result.append(copy)
return result
configs = {k:v for k,v in custom['config_files'].items()
if v.get('Engine') == 'TeaBreakOption'}
result = {
'format_version': 1,
'target_client_version': catalog['client_version'],
'target_assembly_sha256': catalog['provenance']['game_assembly_sha256'],
'source_binary_sha256': a.source_binary_sha256,
'source_kind': 'reference-implementation',
'source': 'FFGO embedded GridFight Excel rows plus FFGO declarative TeaBreakOption recipes; not original Client scripts or a native Client capture.',
'compatibility_scope': 'Event IDs must also be present on the active Client node; no reference route or saved run is imported. Only explicitly supported effects execute.',
'source_excel_sha256': hashlib.sha256(excel_raw).hexdigest(),
'source_custom_sha256': hashlib.sha256(custom_raw).hexdigest(),
'tables': {
'events': rows('grid_fight_bonfire_event', ['ID','PetPalID','DialogID']),
'options': rows('grid_fight_bonfire_event_option', ['EventID','OptionID','DialogID']),
'heal': rows('grid_fight_bonfire_heal', ['ChapterID','HealNum']),
},
'configs': configs,
}
a.output.write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n', encoding='utf8')
print(json.dumps({k:len(v) for k,v in result['tables'].items()} | {'recipes':len(configs)}))
if __name__ == '__main__':
main()
+113
View File
@@ -0,0 +1,113 @@
"""Extract reference Excel/custom data offline; never execute the reference binary.
Requires protobuf plus the sibling dump_embedded_proto.py. Output is evidence,
not an automatically trusted replacement for current Client configuration.
"""
import argparse
import hashlib
import json
import re
import zlib
from pathlib import Path
from google.protobuf import descriptor_pool, json_format, message_factory
from dump_embedded_proto import extract
def sha(data):
return hashlib.sha256(data).hexdigest()
def save_json(path, value):
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
binary = args.binary.read_bytes()
args.output.mkdir(parents=True, exist_ok=True)
pool = descriptor_pool.DescriptorPool()
descriptors = []
for name in ("gridfight_excel", "rogue_tourn_excel", "resources"):
start, end, desc = extract(binary, "proto/" + name + ".proto")
pool.Add(desc)
descriptors.append({"name": desc.name, "offset": start,
"length": end - start, "sha256": sha(binary[start:end])})
excel_type = message_factory.GetMessageClass(pool.FindMessageTypeByName("excel.ExcelData"))
manifest = {"format_version": 1, "binary_sha256": sha(binary),
"binary_size": len(binary), "descriptors": descriptors,
"resources": [], "limitations": [
"Reference binary data; no runtime execution or Client verification.",
"Custom config files are reference-authored rules, not original Client JSON.",
"int64 protobuf fields remain JSON strings to retain exact values."]}
found = set()
# Exact gzip headers, finite carrier size and 64 MiB output cap per candidate.
for match in re.finditer(b"\x1f\x8b\x08\x00", binary):
offset = match.start()
try:
decoder = zlib.decompressobj(31)
raw = decoder.decompress(binary[offset:], 64 * 1024 * 1024)
if not decoder.eof:
continue
compressed_length = len(binary) - offset - len(decoder.unused_data)
if raw.startswith(b"{"):
value = json.loads(raw)
if not isinstance(value, dict) or "config_files" not in value:
continue
stem = "gridfight-custom-data"
else:
message = excel_type.FromString(raw)
if not message.HasField("grid_fight"):
continue
value = json_format.MessageToDict(message, preserving_proto_field_name=True)
stem = "excel-data"
if stem in found:
raise RuntimeError("Ambiguous embedded resource: " + stem)
found.add(stem)
save_json(args.output / (stem + ".json"), value)
if stem == "excel-data":
save_json(args.output / "gridfight-excel-data.json", value["grid_fight"])
manifest["resources"].append({"output": stem + ".json", "offset": offset,
"compressed_length": compressed_length, "compressed_sha256": sha(binary[offset:offset+compressed_length]),
"uncompressed_length": len(raw), "uncompressed_sha256": sha(raw)})
except (zlib.error, ValueError, UnicodeError):
continue
except Exception as error:
# A random gzip-like sequence need not be a protobuf resource.
if type(error).__name__ != "DecodeError":
raise
if found != {"excel-data", "gridfight-custom-data"}:
raise RuntimeError("Expected unique Excel and CW custom resources; got " + repr(found))
# This separate JSON supplement is not part of ExcelData. Locate its named
# root rather than baking in one build's offset or any game identifiers.
supplements = []
for match in re.finditer(rb'\{\s*"challenge_peak_extra_data"\s*:', binary):
candidate = binary[match.start():match.start()+1024*1024]
try:
# Decode only through the JSON terminator; following PE bytes are
# unrelated and may not be valid UTF-8.
text = candidate.decode("utf-8", errors="surrogateescape")
value, end = json.JSONDecoder().raw_decode(text)
raw = text[:end].encode("utf-8", errors="surrogateescape")
if isinstance(value.get("challenge_peak_extra_data"), list):
supplements.append((match.start(), raw, value))
except (ValueError, UnicodeError):
continue
if len(supplements) != 1:
raise RuntimeError("Expected unique named Peak scene supplement")
offset, raw, value = supplements[0]
save_json(args.output / "challenge_peak_extra_data.json", {
"binary_sha256": sha(binary), "source_file_offset": hex(offset),
"decoded_byte_length": len(raw), "data": value})
manifest["resources"].append({"output": "challenge_peak_extra_data.json",
"offset": offset, "uncompressed_length": len(raw),
"uncompressed_sha256": sha(raw), "encoding": "utf-8-json"})
save_json(args.output / "resource-manifest.json", manifest)
print(json.dumps({"binary_sha256": manifest["binary_sha256"], "resources": manifest["resources"]}))
if __name__ == "__main__":
main()
+22
View File
@@ -0,0 +1,22 @@
"""Extract declarative Orb recipes from explicit, previously decoded reference files."""
import argparse
import hashlib
import json
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--excel',type=Path,required=True)
p.add_argument('--custom',type=Path,required=True)
p.add_argument('--binary-sha256',required=True)
p.add_argument('--output',type=Path,required=True)
a=p.parse_args()
if len(bytes.fromhex(a.binary_sha256))!=32: raise ValueError('Expected SHA256')
excel=json.loads(a.excel.read_text(encoding='utf8'))
custom=json.loads(a.custom.read_text(encoding='utf8'))
orbs=[{'id':int(x['OrbID']),'bonus_id':int(x['BonusID'])} for x in excel['grid_fight_orb']]
base=[{'id':int(x['BonusID']),'kind':x.get('BonusType',''),'amount':int(x.get('BonusTypeParam','0'))} for x in excel['grid_fight_basic_bonus']]
recipes=[v for k,v in custom['config_files'].items() if k.startswith('Orb/') and v.get('Engine')=='Orb']
equipment=[{'id':int(x['ID']),'category':x.get('EquipCategory','')} for x in excel['grid_fight_equipment']]
out={'format_version':1,'source_kind':'reference-server-authored-orb-recipes','binary_sha256':a.binary_sha256,'excel_json_sha256':hashlib.sha256(a.excel.read_bytes()).hexdigest(),'custom_json_sha256':hashlib.sha256(a.custom.read_bytes()).hexdigest(),'limitations':['Reference-authored recipes are not a claim of captured Client reward odds. Unsupported reward kinds are rejected without consuming an Orb.'], 'orbs':orbs,'basic_bonuses':base,'equipment':equipment,'recipes':recipes}
a.output.write_text(json.dumps(out,indent=2,ensure_ascii=True)+'\n',encoding='utf8')
print(json.dumps({'orbs':len(orbs),'basic_bonuses':len(base),'custom_recipes':len(recipes)}))
+31
View File
@@ -0,0 +1,31 @@
"""Export labelled reference penalty rules; never imports a saved run."""
import argparse
import hashlib
import json
from pathlib import Path
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--excel', type=Path, required=True)
p.add_argument('--catalog', type=Path, required=True)
p.add_argument('--binary-sha256', required=True)
p.add_argument('--output', type=Path, required=True)
a = p.parse_args()
if len(bytes.fromhex(a.binary_sha256)) != 32:
raise ValueError('Expected SHA256')
raw = a.excel.read_bytes()
c = json.loads(a.catalog.read_bytes())
fields = ('ID', 'ThresholdPosition', 'ThresholdFailPlayerHPPenalty',
'ThresholdPassBasicPlayerHPPenalty', 'ProgressPenaltyCoefficient')
rows = [{k: int(r.get(k, 0)) for k in fields}
for r in json.loads(raw)['grid_fight_penalty_rule']]
a.output.write_text(json.dumps({
'format_version': 1,
'target_client_version': c['client_version'],
'target_assembly_sha256': c['provenance']['game_assembly_sha256'],
'source_kind': 'reference-implementation; captured current rules take precedence',
'reference_executable_sha256': a.binary_sha256,
'excel_dump_sha256': hashlib.sha256(raw).hexdigest(),
'source_table': 'grid_fight_penalty_rule',
'penalty_rules': rows,
}, indent=2) + '\n', encoding='utf8')
print(f'Exported {len(rows)} reference penalties')
Loaded 100 of 345 files, more files were not shown because too many files have changed in this diff. Show more