-- Resource-fed per-character Warp tabs. This file is embedded behind the -- native_presentation policy; it is deliberately outside heartbeat scripts/. -- It touches only cloned Gacha models and the content of their own Warp tab. do local state=_G.__GAC_GACHA_PRESENTATION_STATE if not state then state={installed={},failures={},originals={},next_tick=0} _G.__GAC_GACHA_PRESENTATION_STATE=state end local function log(message) CS.UnityEngine.Debug.Log('[GaC Character Warp] '..tostring(message)) end local function config() return _G.__GAC_GACHA_PRESENTATION end local function entry(pool) if not pool then return nil end local installed=state.installed[pool.GachaID] if not installed or installed.pool~=pool then return nil end return state.by_id and state.by_id[pool.GachaID] end local function warp_open() local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GachaMainPage') return page and page.Trans and page.Trans.gameObject.activeInHierarchy end local function native_reflection() if state.reflection then return state.reflection end local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60) state.reflection={ flags=flags, invoke=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),0), clone=typeof(CS.System.Object):GetMethod('MemberwiseClone',flags), empty=CS.System.Array.CreateInstance(typeof(CS.System.Object),0) } return state.reflection end local function clone(value) local r=native_reflection() return r.clone:Invoke(value,r.invoke,nil,r.empty,nil) end local function stretch(rect,minx,miny,maxx,maxy) rect.anchorMin=CS.UnityEngine.Vector2(minx,miny) rect.anchorMax=CS.UnityEngine.Vector2(maxx,maxy) rect.offsetMin=CS.UnityEngine.Vector2.zero rect.offsetMax=CS.UnityEngine.Vector2.zero rect.localScale=CS.UnityEngine.Vector3.one end local function image_object(name,parent) -- The generated xLua constructor unwraps params Type[] from separate Lua -- arguments. Passing System.Array as one argument fails overload binding. local obj=CS.UnityEngine.GameObject(name,typeof(CS.UnityEngine.RectTransform),typeof(CS.UnityEngine.UI.Image)) obj.transform:SetParent(parent,false) local image=obj:GetComponent(typeof(CS.UnityEngine.UI.Image)) return obj,image,obj:GetComponent(typeof(CS.UnityEngine.RectTransform)) end local function text_object(source,parent,name,minx,miny,maxx,maxy) local obj=CS.UnityEngine.Object.Instantiate(source.gameObject) obj.name=name obj.transform:SetParent(parent,false) stretch(obj:GetComponent(typeof(CS.UnityEngine.RectTransform)),minx,miny,maxx,maxy) local text=obj:GetComponent(source:GetType()) text.raycastTarget=false text.color=CS.UnityEngine.Color.white obj:SetActive(true) return text end local function render(tab,banner) if tab.__gac_character_banner and tab.__gac_layout_revision==5 and tab.__gac_banner_id==banner.gacha_id then if not tab.__gac_sidebar_sprites then state.sidebar_jobs=state.sidebar_jobs or setmetatable({},{__mode='k'}) state.sidebar_jobs[tab]=true end return end if tab.__gac_character_banner then CS.UnityEngine.Object.Destroy(tab.__gac_character_banner) tab.__gac_character_banner=nil for _,obj in ipairs(tab.__gac_owned_texts or {})do CS.UnityEngine.Object.Destroy(obj)end tab.__gac_owned_texts=nil for _,item in ipairs(tab.__gac_replaced_visuals or {})do pcall(function()item.object:SetActive(item.active)end)end tab.__gac_replaced_visuals=nil for _,sprite in ipairs(tab.__gac_sidebar_sprites or {})do CS.UnityEngine.Object.Destroy(sprite)end tab.__gac_sidebar_sprites=nil if state.sidebar_jobs then state.sidebar_jobs[tab]=nil end if state.pity_labels then state.pity_labels[tab]=nil end tab.__gac_banner_id=nil end local content=tab._binder and tab._binder.node_content local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GachaMainPage') local binder=page and page.LuaTable and page.LuaTable._binder local source=binder and binder.txt_pool_name if not content or not source then error('known Warp content/name binder is unavailable') end local avatar=CS.RPG.GameCore.AvatarExcelTable.GetData(banner.avatar_id) if not avatar then error('mapped avatar row vanished') end local avatar_panel=content.transform:Find('AvatarPanel') local title_panel=content.transform:Find('TitlePanel') local title_slot=content.transform:Find('TitlePanel/Content/CardPoolNamePanel/CardPoolName') local subtitle=content.transform:Find('TitlePanel/SubTitlePanel/TextPanel/SubTitleText') if not avatar_panel or not title_panel or not title_slot or not subtitle then error('captured native banner layout is unavailable') end local root,background,rect=image_object('GaCCharacterBanner',avatar_panel) local texts={} local replaced={} local function hide(path) local node=content.transform:Find(path) if node then replaced[#replaced+1]={object=node.gameObject,active=node.gameObject.activeSelf};node.gameObject:SetActive(false)end end -- Rendering is transactional. An exception after the opaque root exists -- must not leave an untracked overlay covering the tab or add one on every -- subsequent refresh. local ok,err=pcall(function() stretch(rect,0,0,1,1) -- Reuse the native white title panel/frame. Replace the template's actual -- illustration nodes, so no opaque rectangle sits over a second banner. background.color=CS.UnityEngine.Color(1,1,1,0) background.raycastTarget=false -- CutinBg is a different illustration and can repeat the character art. -- Leave the native frame visible rather than layering that asset here. local art,image,art_rect=image_object('FeaturedAvatar',root.transform) stretch(art_rect,0.32,0.02,0.99,0.98) image.color=CS.UnityEngine.Color.white image.preserveAspect=true image.raycastTarget=false local title=text_object(source,title_slot,'FeaturedName',0,0,1,1) texts[#texts+1]=title.gameObject title:SafeSetTextID(avatar.AvatarName) title.fontSize=48;title.color=CS.UnityEngine.Color(0.1,0.1,0.1,1) local description=text_object(source,title_panel,'FeaturedDescription',0.08,0.51,0.92,0.73) texts[#texts+1]=description.gameObject local rules=config().rules if rules then description.text='Every '..rules.four_star_pity..' Warps guarantees a 4-star or higher.\nFeatured 5-star guaranteed within '..rules.five_star_pity..' Warps.' else description.text='Character Warp' end description.fontSize=23;description.color=CS.UnityEngine.Color(0.1,0.1,0.1,1) local four_stars=banner.four_stars or {} if #four_stars>0 then local label=text_object(source,title_panel,'FeaturedFourStarLabel',0.08,0.44,0.92,0.50) texts[#texts+1]=label.gameObject label.text='Featured 4-star characters';label.fontSize=20;label.color=CS.UnityEngine.Color(0.3,0.3,0.3,1) local cards,card_bg,card_rect=image_object('FeaturedFourStarCards',title_panel) texts[#texts+1]=cards;card_bg.color=CS.UnityEngine.Color(1,1,1,0);card_bg.raycastTarget=false stretch(card_rect,0.08,0.14,0.92,0.43) for index,four in ipairs(four_stars)do local panel,panel_image,panel_rect=image_object('FourStarCard'..index,cards.transform) stretch(panel_rect,(index-1)/#four_stars+0.01,0,index/#four_stars-0.01,1) panel_image.color=CS.UnityEngine.Color(0.57,0.49,0.72,0.12);panel_image.raycastTarget=false local portrait,portrait_image,portrait_rect=image_object('Portrait',panel.transform) stretch(portrait_rect,0,0.16,1,1);portrait_image.preserveAspect=true;portrait_image.raycastTarget=false if CS.RPG.Client.GlobalVars.s_AssetMgr:Exists(four.portrait_path)then tab:_async_load_sprite_to(portrait_image,four.portrait_path) else -- One unavailable supporting portrait must not erase the whole featured -- character card; keep the text and the other portraits usable. portrait:SetActive(false) log('FOUR_STAR_PORTRAIT_MISSING banner='..banner.gacha_id..' avatar='..four.avatar_id) end local four_row=CS.RPG.GameCore.AvatarExcelTable.GetData(four.avatar_id) local name=text_object(source,panel.transform,'AvatarName',0,0,1,0.16) name:SafeSetTextID(four_row.AvatarName);name.fontSize=16;name.color=CS.UnityEngine.Color(0.15,0.15,0.15,1) end end local pity_label=text_object(source,title_panel,'WarpGuaranteeProgress',0.08,0.035,0.92,0.12) texts[#texts+1]=pity_label.gameObject;pity_label.fontSize=18;pity_label.color=CS.UnityEngine.Color(0.3,0.3,0.3,1) local initial_pity=config().pity or {since_five_star=0,since_four_star=0} if rules then pity_label.text='5-star: '..initial_pity.since_five_star..'/'..rules.five_star_pity..' 4-star: '..initial_pity.since_four_star..'/'..rules.four_star_pity else pity_label.text='' end state.pity_labels=state.pity_labels or setmetatable({},{__mode='k'}) state.pity_labels[tab]=pity_label tab:_async_load_sprite_to(image,banner.art_path) root.transform:SetAsLastSibling() hide('AvatarPanel/MianAvatarPanel') hide('AvatarPanel/MianAvatarPanelFront') hide('AvatarPanel/SubLightConePanel') hide('DrawCardInfo') hide('BtnInvitation') hide('TitlePanel/Content/ScrollView') for i=0,title_slot.childCount-1 do local child=title_slot:GetChild(i) if child.gameObject~=title.gameObject then replaced[#replaced+1]={object=child.gameObject,active=child.gameObject.activeSelf};child.gameObject:SetActive(false)end end local subtitle_text=subtitle.gameObject:GetComponent(typeof(CS.RPG.Client.LocalizedText)) if subtitle_text then subtitle_text.text='Character Event Warp' end end) if not ok then CS.UnityEngine.Object.Destroy(root) for _,obj in ipairs(texts)do CS.UnityEngine.Object.Destroy(obj)end for _,item in ipairs(replaced)do item.object:SetActive(item.active)end error(err) end tab.__gac_character_banner=root tab.__gac_layout_revision=5;tab.__gac_banner_id=banner.gacha_id;tab.__gac_owned_texts=texts;tab.__gac_replaced_visuals=replaced state.sidebar_jobs=state.sidebar_jobs or setmetatable({},{__mode='k'}) state.sidebar_jobs[tab]=true log('ART_REQUESTED banner='..banner.gacha_id..' avatar='..banner.avatar_id) end state.render_current=render local function update_sidebars() local processed=0 for tab in pairs(state.sidebar_jobs or {})do if processed>=2 then break end;processed=processed+1 local root=tab.__gac_character_banner local source=root and root.transform:Find('FeaturedAvatar') local source_image=source and source.gameObject:GetComponent(typeof(CS.UnityEngine.UI.Image)) local sprite=source_image and source_image.sprite local button=tab._tab_button_panel and tab._tab_button_panel._binder if sprite and button then local rect=sprite.textureRect local owned={} local ok,err=pcall(function() for _,name in ipairs({'img_selected_icon','img_unselected_icon'})do local image=button[name] -- The template's selected image is taller than its tab slot. A full -- character splash has transparent edges unlike the native banner icon, -- so keep each custom image within its own measured parent bounds. local transform=image.rectTransform local parent=transform.parent local bounds=parent and parent.rect if bounds and transform.rect.height>bounds.height and bounds.height>0 then local size=transform.sizeDelta transform.sizeDelta=CS.UnityEngine.Vector2(size.x,size.y-(transform.rect.height-bounds.height)) end local target=image.rectTransform.rect local ratio=target.width/target.height local width=math.min(rect.width,rect.height*ratio) local height=width/ratio local crop=CS.UnityEngine.Rect(rect.x+(rect.width-width)*0.5,rect.y+(rect.height-height)*0.68,width,height) local cropped=CS.UnityEngine.Sprite.Create(sprite.texture,crop,CS.UnityEngine.Vector2(0.5,0.5),sprite.pixelsPerUnit) owned[#owned+1]=cropped image.sprite=cropped;image.preserveAspect=false end end) if ok then tab.__gac_sidebar_sprites=owned state.sidebar_jobs[tab]=nil log('SIDEBAR_READY banner='..tab._gacha_pool_data.GachaID) else for _,item in ipairs(owned)do CS.UnityEngine.Object.Destroy(item)end state.sidebar_jobs[tab]=nil;log('SIDEBAR_ERROR '..tostring(err)) end end end end local function ensure_hooks() if state.hooked then return end -- These named modules define controllers; requiring them does not open a UI -- page or execute a scene/task. Hook before any new pool is made visible. local class=require('Ui.Gacha.GachaPool.DefaultGachaPoolTabItem') if type(class)~='table' or type(class._refresh)~='function' then error('named native Gacha tab controller is unavailable') end local original_refresh=class._refresh local original_dispose=class._on_dispose state.originals.refresh=original_refresh state.originals.dispose=original_dispose local refresh_hook=function(self,...) local banner=entry(self._gacha_pool_data) if not banner then return original_refresh(self,...) end -- Do not bind Standard's multi-character list or 300-draw reward panel to -- the custom card. Its native draw controls continue to use the clone ID. self._item_panels=self._item_panels or {} self._up_item_panels=self._up_item_panels or {} local ok,err=pcall(state.render_current,self,banner) if not ok then log('ART_ERROR banner='..banner.gacha_id..' '..tostring(err)) -- Preserve the known working page lifecycle even when a binding differs. -- No world camera, page Canvas or global UI state is disabled here. end end local dispose_hook=function(self,...) if self.__gac_character_banner then CS.UnityEngine.Object.Destroy(self.__gac_character_banner) end self.__gac_character_banner=nil;self.__gac_banner_id=nil for _,obj in ipairs(self.__gac_owned_texts or {})do CS.UnityEngine.Object.Destroy(obj)end self.__gac_owned_texts=nil for _,sprite in ipairs(self.__gac_sidebar_sprites or {})do CS.UnityEngine.Object.Destroy(sprite)end self.__gac_sidebar_sprites=nil if state.sidebar_jobs then state.sidebar_jobs[self]=nil end if state.pity_labels then state.pity_labels[self]=nil end for _,item in ipairs(self.__gac_replaced_visuals or {})do pcall(function()item.object:SetActive(item.active)end)end self.__gac_replaced_visuals=nil if original_dispose then return original_dispose(self,...) end end class._refresh=refresh_hook class._on_dispose=dispose_hook state.hook_class=class state.refresh_hook=refresh_hook state.dispose_hook=dispose_hook -- GetInfo refresh replaces the module's lists immediately before this -- callback. Reconcile the projection before native tabs take ownership, -- rather than racing the already-open page on a later render tick. local main=require('Ui.Gacha.GachaMainPage') local original_create=main._create_pools_panel local create_hook=function(self,...) if _G.__GAC_GACHA_PRESENTATION_TICK then state.next_tick=0 _G.__GAC_GACHA_PRESENTATION_TICK(true) end return original_create(self,...) end main._create_pools_panel=create_hook state.main_class=main;state.original_create=original_create;state.create_hook=create_hook state.hooked=true end local remove_installed local function register(module,template,banner) local asset_mgr=CS.RPG.Client.GlobalVars.s_AssetMgr if not asset_mgr:Exists(banner.art_path) or not asset_mgr:Exists(banner.icon_path) then log('ASSET_MISSING banner='..banner.gacha_id..' avatar='..banner.avatar_id) return false end local avatar=CS.RPG.GameCore.AvatarExcelTable.GetData(banner.avatar_id) if not avatar then log('ROW_MISSING avatar='..banner.avatar_id);return false end -- A native GetInfo refresh can drop a clone from lookup while its old group -- remains cached. Remove only that previously owned reference before retry. local previous=state.installed[banner.gacha_id] if previous then remove_installed(previous);state.installed[banner.gacha_id]=nil end local r=native_reflection() local pool=clone(template) local row=clone(template.Row) row.GachaID=banner.gacha_id row.SortID=template.Row.SortID+banner.sort_order row.PoolName=avatar.AvatarName row.PoolLabelIcon=banner.icon_path row.PoolLabelIconSelected=banner.icon_path local base=template:GetType().BaseType base:GetField('k__BackingField',r.flags):SetValue(pool,row) -- ItemHelper.GetItem is the same named API used by the captured native -- GachaMainPage obtain dialog. Keep detail/prize metadata aligned with the -- featured avatar, rather than inheriting the Standard pool's item array. local prize_field=base:GetField('k__BackingField',r.flags) local detail_field=base:GetField('k__BackingField',r.flags) if not prize_field or not detail_field then error('native Gacha item array fields are unavailable') end local item_type=prize_field.FieldType:GetElementType() local helper=typeof(CS.RPG.Client.ItemHelper) local parameters=CS.System.Array.CreateInstance(typeof(CS.System.Type),1) parameters[0]=typeof(CS.System.UInt32) local get_item=helper:GetMethod('GetItem',r.flags,nil,parameters,nil) -- Inspect the exact overload before invoking it. A similarly named helper -- returning an ItemConfig/tuple cannot be inserted into an ItemData array. if not get_item or not get_item.IsStatic or not item_type:IsAssignableFrom(get_item.ReturnType) then error('ItemHelper.GetItem(UInt32) return type is not the captured ItemData array element') end local native_id=CS.System.Array.CreateInstance(typeof(CS.System.UInt32),1) native_id[0]=banner.avatar_id local item_args=CS.System.Array.CreateInstance(typeof(CS.System.Object),1) CS.System.Array.Copy(native_id,item_args,1) local item=get_item:Invoke(nil,r.invoke,nil,item_args,nil) if not item or not item_type:IsInstanceOfType(item) or item.ConfigID~=banner.avatar_id then error('mapped avatar item metadata is unavailable') end local prizes=CS.System.Array.CreateInstance(item_type,1);prizes:SetValue(item,0);prize_field:SetValue(pool,prizes) local four_stars=banner.four_stars or {} local details=CS.System.Array.CreateInstance(detail_field.FieldType:GetElementType(),1+#four_stars) details:SetValue(item,0) for index,four in ipairs(four_stars)do native_id[0]=four.avatar_id;CS.System.Array.Copy(native_id,item_args,1) local four_item=get_item:Invoke(nil,r.invoke,nil,item_args,nil) if not four_item or not item_type:IsInstanceOfType(four_item) or four_item.ConfigID~=four.avatar_id then error('four-star item metadata is unavailable')end details:SetValue(four_item,index) end detail_field:SetValue(pool,details) pool.SingleDrawData=clone(template.SingleDrawData) pool.MultiDrawData=clone(template.MultiDrawData) -- The mutable draw records are distinct; the original Standard pool is -- never renamed, modified or used as the custom pool's identity. local t=module:GetType() local list=t:GetField('_GachaPoolDatasThisVersion',r.flags):GetValue(module) local groups=t:GetField('_GachaGroupDataDict',r.flags):GetValue(module) local before={} local it=groups.Keys:GetEnumerator() while it:MoveNext() do before[tostring(it.Current)]=true end it:Dispose() local ok,err=pcall(function() list:Add(pool) local args=CS.System.Array.CreateInstance(typeof(CS.System.Object),1) args[0]=pool t:GetMethod('_AddGachaPoolDataToGroupDict',r.flags):Invoke(module,r.invoke,nil,args,nil) if module:GetGachaPoolData(banner.gacha_id)~=pool then error('registered clone lookup mismatch') end end) if not ok then list:Remove(pool) local remove={} local iter=groups.Keys:GetEnumerator() while iter:MoveNext() do if not before[tostring(iter.Current)] then remove[#remove+1]=iter.Current end end iter:Dispose() local cached_field=t:GetField('_GachaGroupDatasThisVersion',r.flags) local cached=cached_field and cached_field:GetValue(module) for _,id in ipairs(remove) do local group=groups[id] if cached then cached:Remove(group) end groups:Remove(id) end error('native registration rolled back: '..tostring(err)) end local new_groups={} local retained_groups={} local iter=groups.Keys:GetEnumerator() while iter:MoveNext() do if not before[tostring(iter.Current)] then new_groups[#new_groups+1]=iter.Current retained_groups[#retained_groups+1]={id=iter.Current,value=groups[iter.Current]} end end iter:Dispose() state.installed[banner.gacha_id]={pool=pool,module=module,group_ids=new_groups,groups=retained_groups} log('REGISTERED banner='..banner.gacha_id..' avatar='..banner.avatar_id) return true end remove_installed=function(installed) local r=native_reflection() local module=installed.module local t=module:GetType() local list=t:GetField('_GachaPoolDatasThisVersion',r.flags):GetValue(module) local groups=t:GetField('_GachaGroupDataDict',r.flags):GetValue(module) local cached_groups_field=t:GetField('_GachaGroupDatasThisVersion',r.flags) local cached_groups=cached_groups_field and cached_groups_field:GetValue(module) list:Remove(installed.pool) -- Track the actual keys created by native registration; do not derive -- group IDs from banner IDs or enum values. for _,group_id in ipairs(installed.group_ids) do if groups:ContainsKey(group_id) then local group=groups[group_id] if group.IsSinglePoolGachaGroup and group.DefaultGachaPoolData==installed.pool then groups:Remove(group_id) if cached_groups then cached_groups:Remove(group) end end end end local cache_field=t:GetField('k__BackingField',r.flags) if cache_field and cache_field:GetValue(module)==installed.pool then cache_field:SetValue(module,nil) end end local function remove_when_closed() if warp_open() then return false end for id,installed in pairs(state.installed) do remove_installed(installed) state.installed[id]=nil end if state.hook_class then -- Do not overwrite a newer hook installed by another extension. if state.hook_class._refresh==state.refresh_hook then state.hook_class._refresh=state.originals.refresh end if state.hook_class._on_dispose==state.dispose_hook then state.hook_class._on_dispose=state.originals.dispose end end if state.main_class and state.main_class._create_pools_panel==state.create_hook then state.main_class._create_pools_panel=state.original_create end state.main_class=nil;state.original_create=nil;state.create_hook=nil state.hooked=false;state.hook_class=nil;state.refresh_hook=nil;state.dispose_hook=nil state.by_id=nil;state.digest=nil;state.cursor=nil return true end local function restore_before_binding(module) -- Reuse fully initialized objects prepared in the bounded render batches. -- A GetInfo refresh clears module collections; reconstructing every prefab -- or reflected model synchronously here would delay the page unnecessarily. local r=native_reflection() local t=module:GetType() local list=t:GetField('_GachaPoolDatasThisVersion',r.flags):GetValue(module) local groups=t:GetField('_GachaGroupDataDict',r.flags):GetValue(module) local cached_field=t:GetField('_GachaGroupDatasThisVersion',r.flags) local cached=cached_field and cached_field:GetValue(module) local restored=0 for id,owned in pairs(state.installed)do if owned.module==module and state.by_id[id] and owned.groups and not module:GetGachaPoolData(id) then local safe=true for _,group in ipairs(owned.groups)do if groups:ContainsKey(group.id) and groups[group.id]~=group.value then safe=false;break end end if safe then if not list:Contains(owned.pool)then list:Add(owned.pool)end for _,group in ipairs(owned.groups)do if not groups:ContainsKey(group.id)then groups:Add(group.id,group.value)end if cached and not cached:Contains(group.value)then cached:Add(group.value)end end restored=restored+1 end end end if restored>0 then log('RESTORED_BEFORE_PAGE count='..restored)end end -- Reproduce only the native single-pool branch of GachaMainPage's captured -- _create_pools_panel. The existing tab controller keeps its current selection; -- neither _refresh nor click_item_by_uid is called. One prefab/tab per tick. local function append_open_page(data) local page=CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('GachaMainPage') local ui=page and page.LuaTable if not ui or not ui._binder or not ui._tab_control or type(ui._gacha_group_datas)~='table' or type(ui._gacha_tab_items)~='table' then return end if state.page~=ui then state.page=ui;state.page_cursor=1;state.page_failures={} end local count=#data.banners for n=1,math.min(8,count)do if state.page_cursor>count then state.page_cursor=1 end local banner=data.banners[state.page_cursor];state.page_cursor=state.page_cursor+1 local installed=state.installed[banner.gacha_id] if installed and not state.page_failures[banner.gacha_id] then local present=false for _,group in ipairs(ui._gacha_group_datas)do if group and group.DefaultGachaPoolData==installed.pool then present=true;break end end if not present then local group for _,ref in ipairs(installed.groups or {})do if ref.value and ref.value.IsSinglePoolGachaGroup and ref.value:IsSinglePoolGachaGroup() and ref.value.DefaultGachaPoolData==installed.pool then group=ref.value;break end end if group then local prefab,tab,added,owned_root local ok,err=pcall(function() local types=ui._gachaType2tabItem[group.GachaType] if not types or not types[1] or not types[2] then error('native single-pool tab type unavailable')end local container=ui._binder.node_gacha_panel_container local old_children=container.childCount prefab=ui:sync_load_prefab(installed.pool.PrefabPath,container) if container.childCount~=old_children+1 then error('native prefab did not add exactly one panel')end owned_root=container:GetChild(old_children).gameObject tab=ui:create_panel(types[1],types[2]) tab:set_tab_data(installed.pool) tab.button_prefab_index=0 tab:bind(prefab) -- A new panel must not cover the user's selected one while it is added. owned_root:SetActive(false) tab:set_time_over_callback(ui._on_gacha_pool_time_out,ui) -- Track before add_item so page disposal owns the panel even if a -- later native callback throws; it must not become an orphan object. table.insert(ui._gacha_group_datas,group) table.insert(ui._gacha_tab_items,tab) added=true ui._tab_control:add_item(tab) tab:setup_group_tab_item() ui._tab_control:update_layout() end) if not ok then state.page_failures[banner.gacha_id]=true if not added then if tab and tab.dispose then pcall(function()tab:dispose()end)end if owned_root then pcall(function()CS.UnityEngine.Object.Destroy(owned_root)end)end end log('PAGE_APPEND_ERROR banner='..banner.gacha_id..' '..tostring(err)..' tracked='..tostring(added)) else log('PAGE_APPENDED banner='..banner.gacha_id)end return end end end end end _G.__GAC_GACHA_PRESENTATION_TICK=function(before_page_bind) local data=config() local now=CS.UnityEngine.Time.unscaledTime if now0 do if state.cursor>count then state.cursor=1;completed_pass=true end local banner=data.banners[state.cursor] state.cursor=state.cursor+1;processed=processed+1 local failure=state.failures[banner.gacha_id] if not module:GetGachaPoolData(banner.gacha_id) and (not opened or not state.installed[banner.gacha_id]) and (not failure or now>=failure.retry_at) then local installed,reason=pcall(register,module,template,banner) if not installed or not reason then local message=installed and 'asset or avatar row unavailable' or tostring(reason) if not failure or failure.message~=message then log('REGISTER_ERROR banner='..banner.gacha_id..' '..message)end -- Item/asset providers may not be ready on the first post-login tick. -- Retry on a later bounded pass instead of permanently losing the tab. state.failures[banner.gacha_id]={retry_at=now+5,message=message} else state.failures[banner.gacha_id]=nil end end end -- GetGachaInfo can refresh native module lists. Reconcile from the real -- lookup on a new pass instead of assuming a stale pool remains installed. if state.cursor>count then state.cursor=1;completed_pass=true end if completed_pass and not before_page_bind then state.next_tick=now+2 end if opened and not before_page_bind then append_open_page(data) end if before_page_bind then local ready=0 for _,banner in ipairs(data.banners)do if module:GetGachaPoolData(banner.gacha_id)then ready=ready+1 end end log('PAGE_BIND ready='..ready..' configured='..count) end end) if not ok then local message=tostring(err) if message~=state.last_error then log('ERROR '..message);state.last_error=message end state.next_tick=now+5 end end if not state.callback_registered then local ok,err=pcall(function() CS.UnityEngine.Application.onBeforeRender('+',function() if _G.__GAC_GACHA_PRESENTATION_TICK then _G.__GAC_GACHA_PRESENTATION_TICK() end end) end) state.callback_registered=ok if not ok then log('CALLBACK_ERROR '..tostring(err)) end end _G.__GAC_GACHA_PRESENTATION_TICK() end