Update GaCy-GO
This commit is contained in:
1 parent
da8a32f60d
commit
0cbe460e49
345 files changed
+98202
-77559
No files matched your search
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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'});
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'});
|
||||
@@ -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});
|
||||
|
||||
@@ -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});
|
||||
@@ -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'});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)}))
|
||||
@@ -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
Reference in new issue
Block a user