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