pre cw with probe and a bunch script to read game behavior
This commit is contained in:
1 parent
5120219ae2
commit
da8a32f60d
310 files changed
+219693
-872
No files matched your search
@@ -0,0 +1,390 @@
|
||||
-- Push current Client battle-statistic snapshots to the configured GaC SDK.
|
||||
-- Reads one known BattleModule property; no assembly or method enumeration.
|
||||
-- Keep the corrected reader isolated from earlier callbacks that may still be
|
||||
-- registered in the Client until its Lua event handlers are refreshed.
|
||||
-- Older callbacks may survive a script refresh in the same Client session.
|
||||
if _G.__GAC_DAMAGE_ANALYST_LIVE_V4 then
|
||||
_G.__GAC_DAMAGE_ANALYST_LIVE_V4.nextSample = math.huge
|
||||
end
|
||||
if _G.__GAC_DAMAGE_ANALYST_LIVE_V5 then
|
||||
_G.__GAC_DAMAGE_ANALYST_LIVE_V5.nextSample = math.huge
|
||||
end
|
||||
if _G.__GAC_DAMAGE_ANALYST_LIVE_V6 then
|
||||
_G.__GAC_DAMAGE_ANALYST_LIVE_V6.nextSample = math.huge
|
||||
end
|
||||
local state = _G.__GAC_DAMAGE_ANALYST_LIVE_V7
|
||||
if not state then
|
||||
state = {registered=false, inBattle=false, nextSample=0, pending=nil, queuedBody=nil}
|
||||
_G.__GAC_DAMAGE_ANALYST_LIVE_V7 = state
|
||||
end
|
||||
|
||||
local Time = CS.UnityEngine.Time
|
||||
local Camera = CS.UnityEngine.Camera
|
||||
if state.sourceRevision ~= 2 then
|
||||
state.battleWorld = nil
|
||||
state.sourceRevision = 2
|
||||
end
|
||||
if state.detailsRevision ~= 3 then
|
||||
state.lastFinalSignature = nil
|
||||
state.detailsRevision = 3
|
||||
end
|
||||
|
||||
local function logOnce(key, message)
|
||||
if state[key] ~= message then
|
||||
state[key] = message
|
||||
CS.UnityEngine.Debug.Log("[GaC Damage Analyst] " .. tostring(message))
|
||||
end
|
||||
end
|
||||
|
||||
local function isBattleActive()
|
||||
local ok, active = pcall(function()
|
||||
if not state.phaseField then
|
||||
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
|
||||
state.phaseField = typeof(CS.RPG.Client.GamePhaseManager):GetField("_CurrentPhase", flags)
|
||||
end
|
||||
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
|
||||
local phase = manager and state.phaseField:GetValue(manager)
|
||||
return phase ~= nil and typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)
|
||||
end)
|
||||
if not ok then
|
||||
logOnce("phaseError", active)
|
||||
return false
|
||||
end
|
||||
return active
|
||||
end
|
||||
|
||||
local function readKnownField(object, name)
|
||||
if object == nil then return nil end
|
||||
if type(object) == "table" then return object[name] end
|
||||
if not state.fieldCache then state.fieldCache = {} end
|
||||
local typeName = tostring(object:GetType().FullName)
|
||||
local key = typeName .. ":" .. name
|
||||
local field = state.fieldCache[key]
|
||||
if field == nil then
|
||||
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
|
||||
field = object:GetType():GetField(name, flags)
|
||||
state.fieldCache[key] = field or false
|
||||
end
|
||||
if field == false then return nil end
|
||||
return field:GetValue(object)
|
||||
end
|
||||
|
||||
local function breakdownJSON(character)
|
||||
local function dictionary(name)
|
||||
local value = readKnownField(character, name)
|
||||
return value and CS.Newtonsoft.Json.JsonConvert.SerializeObject(value) or '{}'
|
||||
end
|
||||
return '{"attack_type_damage":' .. dictionary("FNFFCPLHPGI") ..
|
||||
',"attack_type_break_damage":' .. dictionary("HNJNDLEFMBE") ..
|
||||
',"attack_type_max_damage":' .. dictionary("KEOOJBHFLFD") .. '}'
|
||||
end
|
||||
|
||||
local function activeStatistics()
|
||||
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
|
||||
local phase = state.phaseField:GetValue(manager)
|
||||
local instance = phase.BattleInstanceRef
|
||||
if instance == nil then return nil end
|
||||
local world = readKnownField(instance, "_GameWorld")
|
||||
if world ~= state.battleWorld then
|
||||
state.battleWorld = world
|
||||
state.statComponent = nil
|
||||
state.componentArray = nil
|
||||
state.componentIndex = 0
|
||||
state.entityIndex = 0
|
||||
state.battleInfoBase64 = nil
|
||||
local info = CS.RPG.Client.GlobalVars.s_ModuleManager.BattleModule:GetBattleInfo()
|
||||
if info ~= nil then
|
||||
state.battleInfoBase64 = CS.System.Convert.ToBase64String(CS.Google.Protobuf.MessageExtensions.ToByteArray(info))
|
||||
end
|
||||
end
|
||||
if not state.statComponent then
|
||||
if not state.componentArray then
|
||||
local entities = readKnownField(world, "_EntityList")
|
||||
for i=state.entityIndex,math.min(state.entityIndex+15,entities.Count-1) do
|
||||
local entity = entities[i]
|
||||
state.entityIndex = i+1
|
||||
if entity and (entity.RuntimeID == CS.RPG.GameCore.GameWorld.CommonLevelEntityRuntimeID
|
||||
or entity.RuntimeID == CS.RPG.GameCore.GameWorld.BattleLevelEntityRuntimeID) then
|
||||
state.componentArray = readKnownField(entity, "_ComponentArray")
|
||||
state.componentIndex = 0
|
||||
break
|
||||
end
|
||||
end
|
||||
if not state.componentArray then return nil end
|
||||
end
|
||||
for i=state.componentIndex,math.min(state.componentIndex+31,state.componentArray.Length-1) do
|
||||
state.componentIndex = i+1
|
||||
local component = state.componentArray[i]
|
||||
if component and typeof(CS.RPG.GameCore.StatisticsComponent):IsInstanceOfType(component) then
|
||||
state.statComponent = component
|
||||
break
|
||||
end
|
||||
end
|
||||
if not state.statComponent then
|
||||
if state.componentIndex >= state.componentArray.Length then state.componentArray = nil end
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local component = state.statComponent
|
||||
local characters = readKnownField(component, "_LightTeamStt")
|
||||
if characters == nil then return nil end
|
||||
local avatars, breakdowns = {}, {}
|
||||
for i=0,math.min(characters.Length-1,15) do
|
||||
local character = characters[i]
|
||||
if character ~= nil then
|
||||
avatars[#avatars+1] = character.avatarBattleInfo
|
||||
-- Native dictionaries are updated during battle; their protobuf
|
||||
-- lists are populated only by the final snapshot builder.
|
||||
breakdowns[#breakdowns+1] = breakdownJSON(character)
|
||||
end
|
||||
end
|
||||
local servants, servantBreakdowns = {}, {}
|
||||
local servantStats = readKnownField(component, "_ServantStt")
|
||||
if servantStats ~= nil then
|
||||
local iterator = servantStats:GetEnumerator()
|
||||
local scanned = 0
|
||||
while scanned < 64 and iterator:MoveNext() do
|
||||
scanned = scanned + 1
|
||||
local character = iterator.Current.Value
|
||||
local info = readKnownField(character, "LJLDPJFOKCE")
|
||||
if info ~= nil then
|
||||
servants[#servants+1] = info
|
||||
servantBreakdowns[#servantBreakdowns+1] = breakdownJSON(character)
|
||||
end
|
||||
end
|
||||
end
|
||||
return {
|
||||
servantBattleInfoList=servants, servantBreakdownJSON='[' .. table.concat(servantBreakdowns, ',') .. ']',
|
||||
lightTeamBattleInfoList=avatars, breakdownJSON='[' .. table.concat(breakdowns, ',') .. ']',
|
||||
totalBattleTurns=readKnownField(component,"_totalBattleTurns"),
|
||||
totalAutoTurns=readKnownField(component,"_totalAutoTurns"),
|
||||
totalBattleRounds=readKnownField(component,"_totalBattleRounds"),
|
||||
costTime=readKnownField(component,"_lastSimulateTime")
|
||||
}
|
||||
end
|
||||
|
||||
local function jsonNumber(value)
|
||||
local number = tonumber(value) or 0
|
||||
if number ~= number or number == math.huge or number == -math.huge then return "0" end
|
||||
return string.format("%.6f", number)
|
||||
end
|
||||
|
||||
local function jsonInteger(value)
|
||||
local number = tonumber(value) or 0
|
||||
if number ~= number or number == math.huge or number == -math.huge then return "0" end
|
||||
return string.format("%.0f", number)
|
||||
end
|
||||
|
||||
local function buildSnapshot(statistics)
|
||||
local avatars = readKnownField(statistics, "lightTeamBattleInfoList")
|
||||
local luaAvatars = type(avatars) == "table"
|
||||
local avatarCount = avatars and (luaAvatars and #avatars or avatars.Count) or 0
|
||||
if avatarCount <= 0 then
|
||||
-- Keep compatibility with protocol BattleStatistics if the Client
|
||||
-- changes the type exposed by LastBattleStatistic in a later build.
|
||||
avatars = statistics.BattleAvatarList
|
||||
avatarCount = avatars and avatars.Count or 0
|
||||
if avatarCount <= 0 then return nil end
|
||||
local bytes = CS.Google.Protobuf.MessageExtensions.ToByteArray(statistics)
|
||||
local byteCount = type(bytes) == "string" and #bytes or bytes.Length
|
||||
if byteCount > 2*1024*1024 then error("live statistics exceed the upload limit") end
|
||||
local encoded = CS.System.Convert.ToBase64String(bytes)
|
||||
return {
|
||||
signature=encoded,
|
||||
body='{"active":true,"statistics_base64":"' .. encoded .. '"}'
|
||||
}
|
||||
end
|
||||
|
||||
local encodedAvatars = {}
|
||||
local signatureParts = {}
|
||||
local totalBytes = 0
|
||||
for i=0,math.min(avatarCount-1,15) do
|
||||
local avatar = avatars[luaAvatars and i+1 or i]
|
||||
if avatar ~= nil then
|
||||
-- AvatarBattleInfo is a protobuf message even though its containing
|
||||
-- PVEGameStatistics object is a Client-only type and cannot be serialized.
|
||||
local bytes = CS.Google.Protobuf.MessageExtensions.ToByteArray(avatar)
|
||||
local byteCount = type(bytes) == "string" and #bytes or bytes.Length
|
||||
totalBytes = totalBytes + byteCount
|
||||
if totalBytes > 2*1024*1024 then error("live avatar statistics exceed the upload limit") end
|
||||
local encoded = CS.System.Convert.ToBase64String(bytes)
|
||||
encodedAvatars[#encodedAvatars+1] = '"' .. encoded .. '"'
|
||||
signatureParts[#signatureParts+1] = encoded
|
||||
end
|
||||
end
|
||||
if #encodedAvatars == 0 then return nil end
|
||||
|
||||
local servants = readKnownField(statistics, "servantBattleInfoList")
|
||||
local luaServants = type(servants) == "table"
|
||||
local servantCount = servants and (luaServants and #servants or servants.Count) or 0
|
||||
if servantCount > 64 then error("too many native servant statistics") end
|
||||
local encodedServants = {}
|
||||
for i=0,servantCount-1 do
|
||||
local info = servants[luaServants and i+1 or i]
|
||||
if info ~= nil then
|
||||
local bytes = CS.Google.Protobuf.MessageExtensions.ToByteArray(info)
|
||||
totalBytes = totalBytes + (type(bytes) == "string" and #bytes or bytes.Length)
|
||||
if totalBytes > 2*1024*1024 then error("native statistics exceed the upload limit") end
|
||||
local encoded = CS.System.Convert.ToBase64String(bytes)
|
||||
encodedServants[#encodedServants+1] = '"' .. encoded .. '"'
|
||||
signatureParts[#signatureParts+1] = encoded
|
||||
end
|
||||
end
|
||||
|
||||
local totalBattleTurns = readKnownField(statistics, "totalBattleTurns") or statistics.TotalBattleTurns or 0
|
||||
local totalAutoTurns = readKnownField(statistics, "totalAutoTurns") or statistics.TotalAutoTurns or 0
|
||||
local roundCount = readKnownField(statistics, "totalBattleRounds") or statistics.RoundCnt or 0
|
||||
local costTime = readKnownField(statistics, "costTime") or statistics.CostTime or 0
|
||||
local breakdown = type(statistics) == "table" and statistics.breakdownJSON or nil
|
||||
local servantBreakdown = type(statistics) == "table" and statistics.servantBreakdownJSON or nil
|
||||
local signature = table.concat(signatureParts, "|") .. (breakdown or "") .. (servantBreakdown or "") .. table.concat({
|
||||
tostring(totalBattleTurns), tostring(totalAutoTurns), tostring(roundCount), tostring(costTime)
|
||||
}, ":")
|
||||
local body = '{"active":true,"avatar_statistics_base64":[' .. table.concat(encodedAvatars, ",") ..
|
||||
'],"total_battle_turns":' .. jsonInteger(totalBattleTurns) ..
|
||||
',"total_auto_turns":' .. jsonInteger(totalAutoTurns) ..
|
||||
',"round_cnt":' .. jsonInteger(roundCount) ..
|
||||
',"cost_time":' .. jsonNumber(costTime) ..
|
||||
(breakdown and ',"avatar_breakdowns":' .. breakdown or '') ..
|
||||
',"servant_statistics_base64":[' .. table.concat(encodedServants, ',') .. ']' ..
|
||||
(servantBreakdown and ',"servant_breakdowns":' .. servantBreakdown or '') ..
|
||||
(state.battleInfoBase64 and ',"battle_info_base64":"' .. state.battleInfoBase64 .. '"' or '') .. '}'
|
||||
return {signature=signature, body=body}
|
||||
end
|
||||
|
||||
local function sendBody(body, finalSignature)
|
||||
if state.pending then
|
||||
state.queuedBody = body
|
||||
state.queuedFinalSignature = finalSignature
|
||||
return
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
local url = _G.__GAC_SRTOOL_LIVE_URL
|
||||
if not url or url == "" then error("SDK URL is not configured") end
|
||||
-- Some client Lua bindings do not expose UnityWebRequest.Post(string, string).
|
||||
-- Build the request explicitly so the overload resolution is unambiguous.
|
||||
local request = CS.UnityEngine.Networking.UnityWebRequest(url .. "/api/battle_report/live", "POST")
|
||||
-- XLua marshals a Lua string directly to byte[]. This body contains
|
||||
-- ASCII JSON keys/numbers and base64, so it is already UTF-8 bytes.
|
||||
request.uploadHandler = CS.UnityEngine.Networking.UploadHandlerRaw(body)
|
||||
request.downloadHandler = CS.UnityEngine.Networking.DownloadHandlerBuffer()
|
||||
request:SetRequestHeader("Content-Type", "application/json")
|
||||
state.pending = {request=request, operation=request:SendWebRequest(), finalSignature=finalSignature}
|
||||
end)
|
||||
if not ok then logOnce("sendError", err) end
|
||||
end
|
||||
|
||||
local function finishPendingRequest()
|
||||
if not state.pending then return end
|
||||
local ok, done = pcall(function() return state.pending.operation.isDone end)
|
||||
if not ok or not done then return end
|
||||
local pending = state.pending
|
||||
state.pending = nil
|
||||
pcall(function()
|
||||
local code = pending.request.responseCode
|
||||
if code < 200 or code >= 300 then
|
||||
logOnce("httpError", "SDK live endpoint returned HTTP " .. tostring(code))
|
||||
else
|
||||
state.httpError = nil
|
||||
if pending.finalSignature then state.lastFinalSignature = pending.finalSignature end
|
||||
end
|
||||
pending.request:Dispose()
|
||||
end)
|
||||
if state.queuedBody then
|
||||
local body = state.queuedBody
|
||||
local finalSignature = state.queuedFinalSignature
|
||||
state.queuedBody = nil
|
||||
state.queuedFinalSignature = nil
|
||||
sendBody(body, finalSignature)
|
||||
end
|
||||
end
|
||||
|
||||
local function sample()
|
||||
if Time.frameCount == state.lastFrame then return end
|
||||
state.lastFrame = Time.frameCount
|
||||
local now = tonumber(Time.unscaledTime)
|
||||
if not now and os and os.clock then now = os.clock() end
|
||||
if not now then return end
|
||||
local nextSample = tonumber(state.nextSample) or 0
|
||||
if now < nextSample then return end
|
||||
state.nextSample = now + 0.75
|
||||
finishPendingRequest()
|
||||
|
||||
if isBattleActive() then
|
||||
local ok, result = pcall(function()
|
||||
local statistics = activeStatistics()
|
||||
if statistics == nil then return nil end
|
||||
return {statistics=statistics, snapshot=buildSnapshot(statistics)}
|
||||
end)
|
||||
if not ok then
|
||||
logOnce("statisticsError", result)
|
||||
return
|
||||
end
|
||||
if result == nil then return end
|
||||
state.statisticsError = nil
|
||||
if result.snapshot == nil then
|
||||
logOnce("snapshotError", "Client battle statistics have no avatar snapshots")
|
||||
return
|
||||
end
|
||||
state.snapshotError = nil
|
||||
if not state.inBattle then
|
||||
-- Send the first sample immediately to replace any prior report.
|
||||
state.inBattle = true
|
||||
state.lastSignature = nil
|
||||
state.nextHeartbeat = 0
|
||||
end
|
||||
if result.snapshot.signature == state.lastSignature and now < state.nextHeartbeat then return end
|
||||
state.lastSignature = result.snapshot.signature
|
||||
state.nextHeartbeat = now + 2.5
|
||||
sendBody(result.snapshot.body)
|
||||
else
|
||||
state.inBattle = false
|
||||
state.lastSignature = nil
|
||||
state.nextHeartbeat = 0
|
||||
-- The network result intentionally omits rich per-avatar counters.
|
||||
-- Upload the completed native snapshot; SDK matches counters/team
|
||||
-- against the received result before preserving these details.
|
||||
-- Module access can temporarily be unavailable during login/logout.
|
||||
local ready, statistics = pcall(function()
|
||||
local manager = CS.RPG.Client.GlobalVars.s_ModuleManager
|
||||
local module = manager and manager.BattleModule
|
||||
return module and module.LastBattleStatistic
|
||||
end)
|
||||
if not ready then return end
|
||||
if statistics ~= nil then
|
||||
local snapshot = buildSnapshot(statistics)
|
||||
if snapshot and snapshot.signature ~= state.lastFinalSignature then
|
||||
local body = snapshot.body:gsub('"active":true', '"active":false,"final":true', 1)
|
||||
sendBody(body, snapshot.signature)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function runSampleSafely()
|
||||
local ok, err = pcall(sample)
|
||||
if not ok then logOnce("sampleError", err) end
|
||||
end
|
||||
state.sample = runSampleSafely
|
||||
-- Also sample on heartbeat, so a background window or a render pipeline that
|
||||
-- suppresses a camera event cannot stop delivery altogether.
|
||||
runSampleSafely()
|
||||
|
||||
local function addHook(target, eventName, callback)
|
||||
local ok = pcall(function() target[eventName]("+", callback) end)
|
||||
if ok then return true end
|
||||
return pcall(function() target[eventName] = target[eventName] + callback end)
|
||||
end
|
||||
|
||||
if not state.registered then
|
||||
local function tick() if state.sample then state.sample() end end
|
||||
local cameraHooked = addHook(Camera, "onPostRender", tick)
|
||||
local applicationHooked = addHook(CS.UnityEngine.Application, "onBeforeRender", tick)
|
||||
state.registered = cameraHooked or applicationHooked
|
||||
if not state.registered then
|
||||
logOnce("hookError", "Could not register a Unity render callback; will retry")
|
||||
else
|
||||
state.hookError = nil
|
||||
logOnce("hookReady", "Live battle sampling callback registered")
|
||||
end
|
||||
end
|
||||
+25
-24
@@ -41,33 +41,28 @@ local function get_active_cam()
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Detach only the active camera brain and restore its recorded state. Virtual
|
||||
-- cameras continue updating animation/effect state; do not scan/disable every
|
||||
-- virtual camera on each render frame.
|
||||
_G.__FC_BRAINS = _G.__FC_BRAINS or {}
|
||||
local function set_cinemachine_enabled(cam, enabled)
|
||||
if enabled then
|
||||
for id, state in pairs(_G.__FC_BRAINS) do
|
||||
pcall(function() if state.brain then state.brain.enabled = state.enabled end end)
|
||||
_G.__FC_BRAINS[id] = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
pcall(function()
|
||||
if not cam then return end
|
||||
local brain = cam:GetComponent(typeof(CS.Cinemachine.CinemachineBrain))
|
||||
if brain then brain.enabled = enabled end
|
||||
local brain2 = cam:GetComponent("CinemachineBrain")
|
||||
if brain2 then brain2.enabled = enabled end
|
||||
if not brain then return end
|
||||
local id = brain:GetInstanceID()
|
||||
if not _G.__FC_BRAINS[id] then
|
||||
_G.__FC_BRAINS[id] = {brain=brain, enabled=brain.enabled}
|
||||
end
|
||||
brain.enabled = false
|
||||
end)
|
||||
if not enabled then
|
||||
pcall(function()
|
||||
local vcs = GameObject.FindObjectsOfType(typeof(CS.Cinemachine.CinemachineVirtualCamera))
|
||||
if vcs and vcs.Length > 0 then
|
||||
for i = 0, vcs.Length - 1 do
|
||||
vcs[i].enabled = false
|
||||
end
|
||||
end
|
||||
end)
|
||||
else
|
||||
pcall(function()
|
||||
local vcs = GameObject.FindObjectsOfType(typeof(CS.Cinemachine.CinemachineVirtualCamera))
|
||||
if vcs and vcs.Length > 0 then
|
||||
for i = 0, vcs.Length - 1 do
|
||||
vcs[i].enabled = true
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Client inspection: InControlInputModule.allowMouseInput controls UI
|
||||
@@ -94,8 +89,14 @@ end
|
||||
|
||||
local function update_mouse_gate()
|
||||
local gate = _G.__FC_MOUSE_GATE
|
||||
local battleHUD = GameObject.Find("UIRoot/Page/BattleGamePhaseUI(Clone)")
|
||||
local wanted = _G.__FREECAM_ENABLED and battleHUD ~= nil
|
||||
if not gate.phaseField then
|
||||
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
|
||||
gate.phaseField = typeof(CS.RPG.Client.GamePhaseManager):GetField("_CurrentPhase", flags)
|
||||
end
|
||||
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
|
||||
local phase = manager and gate.phaseField:GetValue(manager)
|
||||
local inBattle = phase and typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase)
|
||||
local wanted = _G.__FREECAM_ENABLED and inBattle
|
||||
-- Releasing Freecam while holding RMB must not become a battle click.
|
||||
if not wanted then
|
||||
if gate.active and not Input.GetMouseButton(0) and not Input.GetMouseButton(1) then
|
||||
|
||||
+180
-39
@@ -10,11 +10,12 @@ local Camera = CS.UnityEngine.Camera
|
||||
-- Never target an ancestor shared by HUD and character/reward dialogs.
|
||||
local HUD_PATHS = {
|
||||
"UIRoot/Page/MazeMainPage(Clone)",
|
||||
-- Fallback for clients where the phase's private HudPage field is absent
|
||||
-- or does not include every battle overlay.
|
||||
"UIRoot/Page/BattleGamePhaseUI(Clone)",
|
||||
"UIRoot/BelowPage/AdventureWaypointsPage(Clone)",
|
||||
"UIRoot/BelowPage/MazeJumpingText(Clone)",
|
||||
"UIRoot/BelowPage/MazeMonsterWarningPage(Clone)",
|
||||
"UIRoot/AboveDialog/BetaHintDialog(Clone)",
|
||||
-- Character controls observed under AvatarMainPage/Root. Keep RankPanel
|
||||
-- and dialogs intact so Eidolon illustrations are not hidden with menus.
|
||||
"UIRoot/Page/AvatarMainPage(Clone)/Root/AvatarSubMenuPanel_Vertical",
|
||||
@@ -23,12 +24,11 @@ local HUD_PATHS = {
|
||||
"UIRoot/Page/AvatarMainPage(Clone)/Root/AvatarListPanel",
|
||||
"UIRoot/Page/AvatarMainPage(Clone)/Root/BtnClose",
|
||||
"UIRoot/Page/AvatarMainPage(Clone)/Root/TopBarArea",
|
||||
"VersionText"
|
||||
}
|
||||
|
||||
-- The old script did not record CanvasGroup state. Undo its blanket hide once
|
||||
-- on live upgrade; a fresh Client session avoids any lost legacy fade state.
|
||||
if _G.__HIDE_UI_REGISTERED and _G.__HIDE_UI_SCHEMA ~= 2 and _G.__HIDE_UI_ENABLED then
|
||||
if _G.__HIDE_UI_REGISTERED and (_G.__HIDE_UI_SCHEMA or 0) < 2 and _G.__HIDE_UI_ENABLED then
|
||||
pcall(function()
|
||||
local cams = Camera.allCameras
|
||||
for i = 0, cams.Length - 1 do
|
||||
@@ -54,7 +54,6 @@ if _G.__HIDE_UI_REGISTERED and _G.__HIDE_UI_SCHEMA ~= 2 and _G.__HIDE_UI_ENABLED
|
||||
for i = 0, canvases.Length - 1 do restore(canvases[i].gameObject) end
|
||||
end)
|
||||
end
|
||||
_G.__HIDE_UI_SCHEMA = 2
|
||||
_G.__HIDE_UI_ENABLED = _G.__HIDE_UI_ENABLED or false
|
||||
_G.__HIDE_UI_TARGETS = _G.__HIDE_UI_TARGETS or {}
|
||||
|
||||
@@ -75,26 +74,151 @@ local function restore_hud()
|
||||
end
|
||||
end
|
||||
|
||||
local function protected_name(name)
|
||||
local n = string.lower(tostring(name))
|
||||
return string.find(n, "uid", 1, true) ~= nil and string.find(n, "guide", 1, true) == nil
|
||||
or n == "versiontext" or n == "betahintdialog(clone)"
|
||||
end
|
||||
|
||||
local function hide_object(obj)
|
||||
if not obj or protected_name(obj.name) then return end
|
||||
local id = obj:GetInstanceID()
|
||||
local state = _G.__HIDE_UI_TARGETS[id]
|
||||
if not state then
|
||||
local group = obj:GetComponent(typeof(CanvasGroup))
|
||||
local added = not group
|
||||
if added then group = obj:AddComponent(typeof(CanvasGroup)) end
|
||||
state = {group=group, added=added, alpha=group.alpha,
|
||||
raycast=group.blocksRaycasts, interactable=group.interactable}
|
||||
_G.__HIDE_UI_TARGETS[id] = state
|
||||
end
|
||||
state.group.alpha = 0
|
||||
state.group.blocksRaycasts = false
|
||||
state.group.interactable = false
|
||||
end
|
||||
|
||||
-- Keep UID/branding subtrees visible even if they belong to a HUD container.
|
||||
local function hide_hud_tree(obj)
|
||||
if not obj then return end
|
||||
local protected = {}
|
||||
local ts = obj:GetComponentsInChildren(typeof(CS.UnityEngine.Transform), true)
|
||||
for i = 0, ts.Length - 1 do
|
||||
if protected_name(ts[i].name) then
|
||||
local t = ts[i]
|
||||
while t do
|
||||
protected[t:GetInstanceID()] = true
|
||||
if t == obj.transform then break end
|
||||
t = t.parent
|
||||
end
|
||||
end
|
||||
end
|
||||
local function visit(t)
|
||||
if protected_name(t.name) then return end
|
||||
if not protected[t:GetInstanceID()] then
|
||||
hide_object(t.gameObject)
|
||||
-- Battle HUD widgets have their own CanvasGroups and some ignore
|
||||
-- their parent's alpha. Hide those groups too, preserving each
|
||||
-- original state for restoration when the toggle is disabled.
|
||||
local groups = t:GetComponentsInChildren(typeof(CanvasGroup), true)
|
||||
for i = 0, groups.Length - 1 do
|
||||
hide_object(groups[i].gameObject)
|
||||
end
|
||||
return
|
||||
end
|
||||
for i = 0, t.childCount - 1 do visit(t:GetChild(i)) end
|
||||
end
|
||||
visit(obj.transform)
|
||||
end
|
||||
|
||||
-- EX/Eidolon artwork is an image. Hide interactive controls and labels,
|
||||
-- never the page's Canvas/camera or the illustration's parent container.
|
||||
local function hide_control_components(root, kind)
|
||||
local ok, controls = pcall(function()
|
||||
return root:GetComponentsInChildren(kind, true)
|
||||
end)
|
||||
if not ok or not controls then return end
|
||||
for i = 0, controls.Length - 1 do
|
||||
local component = controls[i]
|
||||
if component and component.gameObject and not protected_name(component.gameObject.name) then
|
||||
-- Hiding each control directly avoids rescanning its entire
|
||||
-- subtree for every Button/LocalizedText pair on the page.
|
||||
hide_object(component.gameObject)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function hide_rank_controls(root)
|
||||
if not root or not root.activeInHierarchy then return end
|
||||
hide_control_components(root, typeof(CS.RPG.Client.LocalizedText))
|
||||
-- Selectable includes buttons, toggles, sliders, dropdowns and scrollbars;
|
||||
-- some rank-page controls have no text or Button component of their own.
|
||||
hide_control_components(root, typeof(CS.UnityEngine.UI.Selectable))
|
||||
-- Skill icons use SkillIcon sprites; illustrations use separate portrait
|
||||
-- assets. Do not hide arbitrary images or the illustration container.
|
||||
local images = root:GetComponentsInChildren(typeof(CS.UnityEngine.UI.Image), true)
|
||||
for i = 0, images.Length - 1 do
|
||||
local sprite = images[i].sprite
|
||||
if sprite and string.lower(tostring(sprite.name)):find("skillicon",1,true) then
|
||||
hide_object(images[i].gameObject)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function hide_hud()
|
||||
for _, path in ipairs(HUD_PATHS) do
|
||||
pcall(function()
|
||||
local obj = GameObject.Find(path)
|
||||
if not obj then return end
|
||||
local id = obj:GetInstanceID()
|
||||
local state = _G.__HIDE_UI_TARGETS[id]
|
||||
if not state then
|
||||
local group = obj:GetComponent(typeof(CanvasGroup))
|
||||
local added = not group
|
||||
if added then group = obj:AddComponent(typeof(CanvasGroup)) end
|
||||
state = {group=group, added=added, alpha=group.alpha,
|
||||
raycast=group.blocksRaycasts, interactable=group.interactable}
|
||||
_G.__HIDE_UI_TARGETS[id] = state
|
||||
end
|
||||
state.group.alpha = 0
|
||||
state.group.blocksRaycasts = false
|
||||
state.group.interactable = false
|
||||
end)
|
||||
pcall(function() hide_hud_tree(GameObject.Find(path)) end)
|
||||
end
|
||||
local ok, err = pcall(function()
|
||||
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
|
||||
_G.__HIDE_UI_PHASE_FIELD = _G.__HIDE_UI_PHASE_FIELD
|
||||
or typeof(CS.RPG.Client.GamePhaseManager):GetField("_CurrentPhase", flags)
|
||||
_G.__HIDE_UI_BATTLE_HUD_FIELD = _G.__HIDE_UI_BATTLE_HUD_FIELD
|
||||
or typeof(CS.RPG.Client.BattleGamePhase):GetField("_HudPage", flags)
|
||||
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
|
||||
local phase = manager and _G.__HIDE_UI_PHASE_FIELD:GetValue(manager)
|
||||
if phase and typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then
|
||||
local hud = _G.__HIDE_UI_BATTLE_HUD_FIELD:GetValue(phase)
|
||||
if hud and hud.Trans then hide_hud_tree(hud.Trans.gameObject) end
|
||||
if hud and hud.ChildDialogs then
|
||||
local dialogs = hud.ChildDialogs
|
||||
for i = 0, dialogs.Count - 1 do
|
||||
if dialogs[i].Trans then hide_hud_tree(dialogs[i].Trans.gameObject) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
if not ok and _G.__HIDE_UI_LAST_ERROR ~= tostring(err) then
|
||||
_G.__HIDE_UI_LAST_ERROR = tostring(err)
|
||||
CS.UnityEngine.Debug.Log("[GaC HideUI] Battle HUD: " .. tostring(err))
|
||||
end
|
||||
pcall(function()
|
||||
local ui = CS.RPG.Client.GlobalVars.s_UIManager
|
||||
local pages = ui:GetAllPageNames()
|
||||
for i = 0, pages.Count - 1 do
|
||||
local name = tostring(pages[i]); local lower = string.lower(name)
|
||||
local page = ui:GetPageByName(name)
|
||||
if page and page.ChildDialogs then
|
||||
for j = 0, page.ChildDialogs.Count - 1 do
|
||||
local dialog = page.ChildDialogs[j]
|
||||
local dialogName = string.lower(tostring(dialog.Name))
|
||||
if dialog.Trans and (dialogName:find("rank",1,true) or dialogName:find("eidolon",1,true)) then
|
||||
hide_rank_controls(dialog.Trans.gameObject)
|
||||
end
|
||||
end
|
||||
end
|
||||
if lower:find("battle",1,true) then
|
||||
local page = ui:GetPageByName(name)
|
||||
if page and page.Trans and page.Trans.gameObject.activeInHierarchy then
|
||||
hide_hud_tree(page.Trans.gameObject)
|
||||
end
|
||||
end
|
||||
if lower:find("rank",1,true) or lower:find("eidolon",1,true) then
|
||||
local page = ui:GetPageByName(name)
|
||||
if page and page.Trans then hide_rank_controls(page.Trans.gameObject) end
|
||||
end
|
||||
end
|
||||
hide_rank_controls(GameObject.Find("UIRoot/Page/AvatarMainPage(Clone)/Root/AvatarRankPanel"))
|
||||
end)
|
||||
end
|
||||
|
||||
function _G.__HIDE_UI_TICK()
|
||||
@@ -107,32 +231,49 @@ function _G.__HIDE_UI_TICK()
|
||||
_G.__HIDE_UI_ENABLED = not _G.__HIDE_UI_ENABLED
|
||||
if not _G.__HIDE_UI_ENABLED then restore_hud() end
|
||||
end
|
||||
-- Only the small named HUD list is checked, four times a second.
|
||||
-- The page/child scan walks Unity UI trees, so keep it to once a second.
|
||||
-- New pages/dialogs never inherit a hidden UIRoot or UI camera.
|
||||
if _G.__HIDE_UI_ENABLED then
|
||||
local now = Time.unscaledTime
|
||||
if toggled or now >= (_G.__HIDE_UI_NEXT_SCAN or 0) then
|
||||
_G.__HIDE_UI_NEXT_SCAN = now + 0.25
|
||||
_G.__HIDE_UI_NEXT_SCAN = now + 1.0
|
||||
hide_hud()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Hook into render pipeline
|
||||
if not _G.__HIDE_UI_REGISTERED then
|
||||
_G.__HIDE_UI_REGISTERED = true
|
||||
|
||||
local function hook(target, evt)
|
||||
pcall(function() target[evt]('+', function(c) _G.__HIDE_UI_TICK() end) end)
|
||||
pcall(function() target[evt] = target[evt] + function(c) _G.__HIDE_UI_TICK() end end)
|
||||
end
|
||||
|
||||
hook(Camera, "onPreRender")
|
||||
hook(Camera, "onPreCull")
|
||||
hook(Camera, "onPostRender")
|
||||
pcall(function() CS.UnityEngine.Application.onBeforeRender('+', function() _G.__HIDE_UI_TICK() end) end)
|
||||
pcall(function() CS.UnityEngine.Application.onBeforeRender = CS.UnityEngine.Application.onBeforeRender + function() _G.__HIDE_UI_TICK() end end)
|
||||
|
||||
CS.UnityEngine.Debug.Log("<color=#FF00FF>[UI Toggle Script]</color> HUD-only HideUI loaded! Press 'ฃ' to toggle UI.")
|
||||
-- Restore targets from the previous policy (including VersionText) on upgrade.
|
||||
if _G.__HIDE_UI_SCHEMA ~= 5 then
|
||||
restore_hud()
|
||||
_G.__HIDE_UI_SCHEMA = 5
|
||||
_G.__HIDE_UI_NEXT_SCAN = 0
|
||||
end
|
||||
|
||||
-- Use one render callback. Registering all camera events plus before-render
|
||||
-- made the toggle callback run redundantly several times per frame.
|
||||
if not _G.__HIDE_UI_REGISTERED then
|
||||
local callback = function() _G.__HIDE_UI_TICK() end
|
||||
local ok = pcall(function()
|
||||
CS.UnityEngine.Application.onBeforeRender('+', callback)
|
||||
end)
|
||||
if not ok then
|
||||
ok = pcall(function()
|
||||
CS.UnityEngine.Application.onBeforeRender = CS.UnityEngine.Application.onBeforeRender + callback
|
||||
end)
|
||||
end
|
||||
if not ok then
|
||||
ok = pcall(function()
|
||||
Camera.onPostRender('+', callback)
|
||||
end)
|
||||
if not ok then
|
||||
ok = pcall(function() Camera.onPostRender = Camera.onPostRender + callback end)
|
||||
end
|
||||
end
|
||||
_G.__HIDE_UI_REGISTERED = ok
|
||||
if ok then
|
||||
CS.UnityEngine.Debug.Log("<color=#FF00FF>[UI Toggle Script]</color> HUD-only HideUI loaded! Press 'ฃ' to toggle UI.")
|
||||
else
|
||||
CS.UnityEngine.Debug.Log("[GaC HideUI] Could not register a render callback.")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,247 @@
|
||||
-- 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()
|
||||
Reference in new issue
Block a user