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