248 lines
14 KiB
Lua
248 lines
14 KiB
Lua
-- Extend the Client's existing M list. Game IDs/labels arrive from the
|
|
-- server-validated NavigationProfiles resource; this script owns no map IDs.
|
|
_G.__GAC_NAVIGATION_TICK = function()
|
|
local now = CS.UnityEngine.Time.unscaledTime
|
|
local state = _G.__GAC_NAVIGATION_STATE
|
|
if not state then
|
|
state = { next_check = 0, panels = setmetatable({}, { __mode = 'k' }) }
|
|
_G.__GAC_NAVIGATION_STATE = state
|
|
end
|
|
if now >= state.next_check and _G.__GAC_NAVIGATION_PROFILES then
|
|
state.next_check = now + 0.25
|
|
local ok, err = pcall(function()
|
|
local page = CS.RPG.Client.GlobalVars.s_UIManager:GetPageByName('SwitchAreaPage')
|
|
if not page or not page.Trans or not page.Trans.gameObject.activeInHierarchy then return end
|
|
local regional = page.LuaTable._regional_detail_panel
|
|
local list = regional and regional._map_area_list_panel
|
|
if not list or not list._tabs or not list._binder then return end
|
|
local installed = state.panels[list]
|
|
if not installed then installed = {}; state.panels[list] = installed end
|
|
state.step = 'reflection'
|
|
if not state.reflection then
|
|
-- System.Reflection.BindingFlags: Instance|Static|Public|NonPublic.
|
|
local all_members = 60
|
|
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), all_members)
|
|
state.reflection = {
|
|
flags = flags,
|
|
clone_method = typeof(CS.System.Object):GetMethod('MemberwiseClone', flags),
|
|
empty_args = CS.System.Array.CreateInstance(typeof(CS.System.Object), 0),
|
|
invoke_flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 0),
|
|
}
|
|
end
|
|
local flags = state.reflection.flags
|
|
local clone_method = state.reflection.clone_method
|
|
local empty_args = state.reflection.empty_args
|
|
local invoke_flags = state.reflection.invoke_flags
|
|
local function clone(value) return clone_method:Invoke(value, invoke_flags, nil, empty_args, nil) end
|
|
local function set_entry(sub, id)
|
|
-- Copy through a typed array so reflection receives a boxed
|
|
-- UInt32 rather than Lua's numeric type. The setter is private
|
|
-- and is not included in the generated Lua property wrapper.
|
|
local typed = CS.System.Array.CreateInstance(typeof(CS.System.UInt32), 1)
|
|
typed[0] = id
|
|
local args = CS.System.Array.CreateInstance(typeof(CS.System.Object), 1)
|
|
CS.System.Array.Copy(typed, args, 1)
|
|
sub:GetType():GetMethod('set_EntryID', flags):Invoke(sub, invoke_flags, nil, args, nil)
|
|
if sub.EntryID ~= id then error('EntryID backing field was not updated') end
|
|
end
|
|
local function apply_teleport_entry()
|
|
local sub = list.__gac_selected_variant or page.LuaTable._context.cur_sub_tab
|
|
local detail = regional._symbol_detail_panel
|
|
local teleport = detail and detail._map_teleport_detail_panel
|
|
if not sub or not teleport or teleport.entrance_id == nil then return end
|
|
for _, profile in ipairs(_G.__GAC_NAVIGATION_PROFILES) do
|
|
if sub.FloorID == profile.floor_id and
|
|
(sub.EntryID == profile.town_entry_id or sub.EntryID == profile.story_entry_id) then
|
|
if teleport.entrance_id ~= sub.EntryID then
|
|
CS.UnityEngine.Debug.Log('[GaC Navigation] Teleport entry '..teleport.entrance_id..' -> '..sub.EntryID)
|
|
teleport.entrance_id = sub.EntryID
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
local function select_profile(tab)
|
|
if not tab then return end
|
|
local sub = tab:GetSubTabByIndex(0)
|
|
if not sub then return end
|
|
for _, profile in ipairs(_G.__GAC_NAVIGATION_PROFILES) do
|
|
if sub.FloorID == profile.floor_id then
|
|
local id = tab.ID == profile.story_entry_id and profile.story_entry_id or profile.town_entry_id
|
|
set_entry(sub, id)
|
|
-- Native selection normalizes tabs by FloorID. Restore
|
|
-- the selected variant after that callback, so the
|
|
-- symbol/teleport panel receives its real EntryID.
|
|
page.LuaTable._context.cur_sub_tab = sub
|
|
regional._context.cur_sub_tab = sub
|
|
regional._cur_sub_tab = sub
|
|
list._selected_sub_tab = sub
|
|
list.__gac_selected_variant = sub
|
|
apply_teleport_entry()
|
|
CS.UnityEngine.Debug.Log('[GaC Navigation] Selected entry='..id..' floor='..sub.FloorID)
|
|
return
|
|
end
|
|
end
|
|
list.__gac_selected_variant = nil
|
|
end
|
|
for _, profile in ipairs(_G.__GAC_NAVIGATION_PROFILES) do
|
|
local current = installed[profile.floor_id]
|
|
if current then
|
|
current.id = profile.story_entry_id
|
|
current.label = profile.story_label
|
|
local found = false
|
|
for _, tab in ipairs(list._tabs) do
|
|
if tab.ID == profile.story_entry_id then found = true; break end
|
|
end
|
|
if not found then installed[profile.floor_id] = nil end
|
|
end
|
|
if not installed[profile.floor_id] then
|
|
for index, tab in ipairs(list._tabs) do
|
|
if not tab.IsComposedTab and tab.ID ~= profile.story_entry_id then
|
|
local sub = tab:GetSubTabByIndex(0)
|
|
if sub and sub.FloorID == profile.floor_id then
|
|
local row_field = tab:GetType():GetField('_Row', flags)
|
|
if not row_field then return end
|
|
state.step = 'clone tab'
|
|
local story = clone(tab)
|
|
state.step = 'clone subtab'
|
|
local story_sub = clone(sub)
|
|
state.step = 'clone row'
|
|
local story_row = clone(row_field:GetValue(tab))
|
|
-- Distinct local list identity, derived from the real entrance.
|
|
-- This does not register a new game floor or entrance.
|
|
state.step = 'set row'
|
|
story_row.ID = profile.story_entry_id
|
|
row_field:SetValue(story, story_row)
|
|
set_entry(story_sub, profile.story_entry_id)
|
|
story_sub:GetType():GetField('<Parent>k__BackingField', flags):SetValue(story_sub, story)
|
|
story:GetType():GetField('SubTabData', flags):SetValue(story, story_sub)
|
|
set_entry(sub, profile.town_entry_id)
|
|
table.insert(list._tabs, index + 1, story)
|
|
if regional._all_tabs ~= list._tabs then
|
|
for i, candidate in ipairs(regional._all_tabs or {}) do
|
|
if candidate == tab then table.insert(regional._all_tabs, i + 1, story); break end
|
|
end
|
|
end
|
|
installed[profile.floor_id] = { tab = story, id = profile.story_entry_id, label = profile.story_label }
|
|
state.step = 'refresh list'
|
|
list:refresh_map_area_list()
|
|
CS.UnityEngine.Debug.Log('[GaC Navigation] Added Story option for floor ' .. profile.floor_id)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local reordered = false
|
|
for _, profile in ipairs(_G.__GAC_NAVIGATION_PROFILES) do
|
|
local town_index, story_index
|
|
for i, tab in ipairs(list._tabs) do
|
|
local sub = tab:GetSubTabByIndex(0)
|
|
if sub and sub.FloorID == profile.floor_id then
|
|
if tab.ID == profile.story_entry_id then story_index = i else town_index = i end
|
|
end
|
|
end
|
|
if town_index and story_index and story_index ~= town_index + 1 then
|
|
local story = table.remove(list._tabs, story_index)
|
|
if story_index < town_index then town_index = town_index - 1 end
|
|
table.insert(list._tabs, town_index + 1, story)
|
|
reordered = true
|
|
end
|
|
end
|
|
if reordered then list:refresh_map_area_list() end
|
|
local selected_variant = list.__gac_selected_variant
|
|
local current_sub = page.LuaTable._context.cur_sub_tab
|
|
if selected_variant and current_sub and current_sub.FloorID == selected_variant.FloorID then
|
|
page.LuaTable._context.cur_sub_tab = selected_variant
|
|
regional._context.cur_sub_tab = selected_variant
|
|
regional._cur_sub_tab = selected_variant
|
|
list._selected_sub_tab = selected_variant
|
|
end
|
|
local detail = regional._symbol_detail_panel
|
|
local teleport = detail and detail._map_teleport_detail_panel
|
|
if teleport and not teleport.__gac_navigation_transfer_hooked then
|
|
teleport.__gac_navigation_transfer_hooked = true
|
|
-- The native icon stores its own entrance_id. Synchronize it
|
|
-- after view setup and immediately before either transfer path;
|
|
-- changing the selected SubTabData alone does not update it.
|
|
for _, method in ipairs({'_setup_teleport_view', '_on_btn_transfer_click', '_on_teleport_btn_transfer_click'}) do
|
|
local original = teleport[method]
|
|
if type(original) == 'function' then
|
|
if method == '_setup_teleport_view' then
|
|
teleport[method] = function(...)
|
|
local result = original(...)
|
|
apply_teleport_entry()
|
|
return result
|
|
end
|
|
else
|
|
teleport[method] = function(...)
|
|
apply_teleport_entry()
|
|
return original(...)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
apply_teleport_entry()
|
|
-- LoopListView recycles rows. Set the label only on the row bound to
|
|
-- our Story object, after the Client has refreshed its localized title.
|
|
for _, child in ipairs(list._children or {}) do
|
|
if child._callback and (child._callback ~= child.__gac_navigation_callback or child.__gac_navigation_schema ~= 3) then
|
|
local row = child
|
|
if row._callback ~= row.__gac_navigation_callback then row.__gac_navigation_original = row._callback end
|
|
row.__gac_navigation_schema = 3
|
|
row.__gac_navigation_callback = function(...)
|
|
local clicked_tab = row._tab
|
|
local result = row.__gac_navigation_original(...)
|
|
local selected, failure = pcall(select_profile, clicked_tab)
|
|
if not selected then CS.UnityEngine.Debug.Log('[GaC Navigation] Selection failed: '..tostring(failure)) end
|
|
return result
|
|
end
|
|
row._callback = row.__gac_navigation_callback
|
|
end
|
|
for _, item in pairs(installed) do
|
|
if child._tab and child._tab.ID == item.id and child._binder and not child._binder.is_destroyed then
|
|
if child._binder.text_title then
|
|
child._binder.text_title:SetSimpleText(item.label)
|
|
end
|
|
if child._binder.text_item_title then
|
|
child._binder.text_item_title:SetSimpleText(item.label)
|
|
end
|
|
end
|
|
end
|
|
if child._tab and child._binder and not child._binder.is_destroyed and _G.__GAC_NAVIGATION_CURRENT_ENTRY then
|
|
local sub = child._tab:GetSubTabByIndex(0)
|
|
if sub then
|
|
for _, profile in ipairs(_G.__GAC_NAVIGATION_PROFILES) do
|
|
if sub.FloorID == profile.floor_id and child._binder.node_player_mark then
|
|
local id = child._tab.ID == profile.story_entry_id and profile.story_entry_id or profile.town_entry_id
|
|
child._binder.node_player_mark.gameObject:SetActive(id == _G.__GAC_NAVIGATION_CURRENT_ENTRY)
|
|
if id == profile.town_entry_id then
|
|
if child._binder.text_title then child._binder.text_title:SetTextID(child._tab.Name) end
|
|
if child._binder.text_item_title then child._binder.text_item_title:SetTextID(child._tab.Name) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
local failure = tostring(state.step) .. ': ' .. tostring(err)
|
|
if not ok and state.last_error ~= failure then
|
|
state.last_error = failure
|
|
CS.UnityEngine.Debug.Log('[GaC Navigation] ' .. failure)
|
|
end
|
|
if not ok then state.next_check = now + 5 end
|
|
end
|
|
end
|
|
|
|
if not _G.__GAC_NAVIGATION_REGISTERED then
|
|
local ok = pcall(function()
|
|
CS.UnityEngine.Application.onBeforeRender('+', function()
|
|
if _G.__GAC_NAVIGATION_TICK then _G.__GAC_NAVIGATION_TICK() end
|
|
end)
|
|
end)
|
|
_G.__GAC_NAVIGATION_REGISTERED = ok
|
|
end
|
|
_G.__GAC_NAVIGATION_TICK()
|