Update GaCy-GO

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

No files matched your search

+651
View File
@@ -0,0 +1,651 @@
-- 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('<Row>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('<PrizeItemDatas>k__BackingField',r.flags)
local detail_field=base:GetField('<DetailItemDatas>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('<GachaPoolDataCache>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 now<state.next_tick then return end
state.next_tick=now+0.1
local ok,err=pcall(function()
local r=native_reflection()
local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase=manager and typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',r.flags):GetValue(manager)
if not phase or typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then return end
if not data or not data.enabled then remove_when_closed();state.next_tick=now+2;return end
local sidebar_ok,sidebar_error=pcall(update_sidebars)
if not sidebar_ok then
local message=tostring(sidebar_error)
if state.sidebar_last_error~=message then log('SIDEBAR_UPDATE_ERROR '..message);state.sidebar_last_error=message end
else state.sidebar_last_error=nil end
local pity=data.pity or {since_five_star=0,since_four_star=0}
if data.rules then
for tab,label in pairs(state.pity_labels or {})do
pcall(function()label.text='5-star: '..pity.since_five_star..'/'..data.rules.five_star_pity..' 4-star: '..pity.since_four_star..'/'..data.rules.four_star_pity end)
end
end
local opened=warp_open()
if not opened then state.page=nil;state.page_failures=nil end
-- A policy edit cannot replace pools owned by the open page. The same
-- digest may add missing independent pools and their tabs incrementally.
if opened and state.digest and state.digest~=data.digest then state.next_tick=now+0.25;return end
local module=CS.RPG.Client.GlobalVars.s_ModuleManager.GachaModule
if not module then return end
local template=module:GetGachaPoolData(data.template_id)
if not template then return end
if template:GetType().FullName~='RPG.Client.StandardGachaPoolData' or template.PrefabPath~=data.template_prefab then
error('captured initialized Standard template identity mismatch')
end
if not CS.RPG.Client.GlobalVars.s_AssetMgr:Exists(data.template_prefab) then
error('verified Standard template is no longer installed')
end
if state.digest~=data.digest then
-- A new server payload may arrive while a page still owns old tabs.
-- Keep its established projection until that page closes; tearing down
-- or mixing two digests here can change an existing selected pool.
if state.digest and warp_open() then
if before_page_bind then restore_before_binding(module) end
return
end
remove_when_closed()
state.by_id={};state.failures={};state.cursor=1
for _,banner in ipairs(data.banners) do state.by_id[banner.gacha_id]=banner end
state.digest=data.digest
end
ensure_hooks()
if before_page_bind then restore_before_binding(module)end
-- Prewarm two entries per frame. Page binding uses the same bounded work;
-- it must never create all clones in one frame and stall the game.
-- There is no assembly-wide reflection; each pass visits only configured
-- banner rows, and a failed registration never mutates another pool.
local count=#data.banners
local budget=math.min(2,count)
local processed=0
local completed_pass=false
while processed<budget and count>0 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