Files
GaCy-GO/scripts/freecam.lua
T

775 lines
34 KiB
Lua

local Input = CS.UnityEngine.Input
local KeyCode = CS.UnityEngine.KeyCode
local Vector3 = CS.UnityEngine.Vector3
local Time = CS.UnityEngine.Time
local Camera = CS.UnityEngine.Camera
local GameObject = CS.UnityEngine.GameObject
_G.__FREECAM_ENABLED = _G.__FREECAM_ENABLED or false
_G.__FREECAM_SPEED = _G.__FREECAM_SPEED or 15.0
local WATERMARK_PREFIX = "<color=#bb00ff>นี่คือเวอร์ชั่นทดสอบของเกย์ </color> "
local WATERMARK_NAME = "Honkai Star Gay"
-- A closely spaced hue gradient sweeps from right to left.
-- Keep the server name, UID, and the Freecam controls independent.
local function rainbow_watermark(now)
local parts = {WATERMARK_PREFIX}
for i = 1, #WATERMARK_NAME do
local h = ((now * 0.12 + (i - 1) * 0.035) % 1) * 6
local x = 1 - math.abs(h % 2 - 1)
local r, g, b
if h < 1 then r, g, b = 1, x, 0
elseif h < 2 then r, g, b = x, 1, 0
elseif h < 3 then r, g, b = 0, 1, x
elseif h < 4 then r, g, b = 0, x, 1
elseif h < 5 then r, g, b = x, 0, 1
else r, g, b = 1, 0, x end
parts[#parts + 1] = string.format("<color=#%02X%02X%02X>%s</color>",
math.floor(r * 255 + 0.5), math.floor(g * 255 + 0.5), math.floor(b * 255 + 0.5), WATERMARK_NAME:sub(i, i))
end
return table.concat(parts)
end
local function show_ui(msg)
if msg:find('[FreeCam:', 1, true) or msg:find('[FaceCam:', 1, true) or msg:find('FaceCam:', 1, true) then
_G.__FC_STATUS_UNTIL = (Time.unscaledTime or 0) + 3
end
pcall(function()
local obj = GameObject.Find("VersionText")
if obj then
local textComp = obj:GetComponentInChildren(typeof(CS.RPG.Client.LocalizedText))
if textComp then
textComp.text = msg
end
end
end)
end
local function get_active_cam()
local cam = Camera.main
if cam and cam.gameObject.activeInHierarchy then
return cam
end
local all = Camera.allCameras
if all and all.Length > 0 then
for i = 0, all.Length - 1 do
local c = all[i]
if c and c.gameObject.activeInHierarchy and c.name ~= "UICamera" and not string.find(c.name, "UI") then
return c
end
end
return all[0]
end
return nil
end
-- Resolve the controlled member or current battle actor through the Client's
-- entity state, and anchor to that model's observed head bone. If the current
-- phase does not expose either, do not guess a world position.
local function model_for(candidate)
local ok, result = pcall(function()
local component = candidate and candidate:GetComponent(typeof(CS.RPG.GameCore.CharacterModelComponent))
return component and component:GetModelGO()
end)
if ok then return result end
return nil
end
local function is_player_avatar(entity, model)
if not entity or not model then return false end
local name = tostring(model.name or '')
if name:find('Monster', 1, true) then return false end
if name:find('Avatar_00', 1, true) then return false end
if name:find('Avatar_', 1, true) or name:find('Player_', 1, true) then
return true
end
local ok, isHuman = pcall(function()
local anim = model:GetComponentInChildren(typeof(CS.UnityEngine.Animator))
return anim and anim.isHuman
end)
return ok and (isHuman == true)
end
local function find_head_bone(model)
if not model then return nil end
local ok, bone = pcall(function()
local animator = model:GetComponentInChildren(typeof(CS.UnityEngine.Animator))
if animator and animator.isHuman then
return animator:GetBoneTransform(CS.UnityEngine.HumanBodyBones.Head)
end
return nil
end)
if ok and bone and bone.gameObject.activeInHierarchy then
return bone
end
local nodes = model:GetComponentsInChildren(typeof(CS.UnityEngine.Transform), true)
for i = 0, nodes.Length - 1 do
local node = nodes[i]
if node.name == 'Head_M' and node.gameObject.activeInHierarchy then
return node
end
end
for i = 0, nodes.Length - 1 do
local node = nodes[i]
local lname = node.name:lower()
if (lname == 'head' or lname == 'bip001 head' or lname:find('head_m', 1, true)) and node.gameObject.activeInHierarchy then
return node
end
end
return nil
end
-- Resolve the controlled member or current battle actor through the Client's
-- entity state, and anchor to that model's observed head bone. If the current
-- phase does not expose either, do not guess a world position.
local function current_facecam_target()
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
local phaseField = _G.__FC_PHASE_FIELD or typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase', flags)
_G.__FC_PHASE_FIELD = phaseField
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase = manager and phaseField and phaseField:GetValue(manager)
if not phase then return nil, nil, nil, false end
local now = Time.unscaledTime or 0
local entity, mode
if typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then
local instance = phase.BattleInstanceRef
mode = instance and instance.TurnBasedGameModeRef
if mode then
-- 1. Attacking Avatar (skill, ultimate, basic attack)
local attackers = mode:GetAttackingEntityList()
if attackers and attackers.Count > 0 then
for i = attackers.Count - 1, 0, -1 do
local cand = attackers[i]
local candModel = model_for(cand)
if is_player_avatar(cand, candModel) then
entity = cand
_G.__FC_BATTLE_LAST_ATTACKER = cand
_G.__FC_BATTLE_ATTACK_TIME = now
break
end
end
end
-- 2. Keep tracking recent ultimate / attacking avatar during cutscenes (up to 4.5s)
if not entity and _G.__FC_BATTLE_LAST_ATTACKER then
local lastModel = model_for(_G.__FC_BATTLE_LAST_ATTACKER)
if lastModel and (now - (_G.__FC_BATTLE_ATTACK_TIME or 0)) < 4.5 then
entity = _G.__FC_BATTLE_LAST_ATTACKER
end
end
-- 3. Turn owner (only if player avatar)
if not entity then
local turnOwner = mode.CurrentTurnOwnerEntity
local turnModel = model_for(turnOwner)
if is_player_avatar(turnOwner, turnModel) then
entity = turnOwner
_G.__FC_BATTLE_LAST_AVATAR = turnOwner
end
end
-- 4. Stick to last active player avatar if turn owner is an enemy monster
if not entity and _G.__FC_BATTLE_LAST_AVATAR then
local lastModel = model_for(_G.__FC_BATTLE_LAST_AVATAR)
if lastModel then
entity = _G.__FC_BATTLE_LAST_AVATAR
end
end
-- 5. Fallback: MVP entity if it's an avatar
if not entity and mode.CurrentMVPEntity then
local mvp = mode.CurrentMVPEntity
local mvpModel = model_for(mvp)
if is_player_avatar(mvp, mvpModel) then
entity = mvp
_G.__FC_BATTLE_LAST_AVATAR = mvp
end
end
end
elseif phase:GetType().Name == 'AdventurePhase' then
entity = phase.CurrentPlayingTeamMember
else
return nil, nil, nil, false
end
if not entity then return nil, nil, nil, false end
local model = model_for(entity)
if not model and mode then
local fallback = mode.CurrentMVPEntity
local fallbackModel = model_for(fallback)
if fallbackModel and is_player_avatar(fallback, fallbackModel) then
entity, model = fallback, fallbackModel
end
end
if not model then return nil, nil, nil, false end
local cachedAlive, cachedActive = pcall(function()
return _G.__FC_FACE_HEAD and _G.__FC_FACE_ROOT
and _G.__FC_FACE_HEAD.gameObject.activeInHierarchy and _G.__FC_FACE_ROOT.gameObject.activeInHierarchy
end)
if _G.__FC_FACE_ENTITY == entity.RuntimeID and cachedAlive and cachedActive then
return _G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT, entity.RuntimeID, true
end
local head = find_head_bone(model)
if head then
return head, model.transform, entity.RuntimeID, true
end
return nil, model.transform, entity.RuntimeID, false
end
-- Log only changes, so diagnostics stay useful during long battles.
local function log_facecam_target(ok, head, root, id, tracked)
local success, detail = pcall(function()
if not ok then return 'resolve_error=' .. tostring(head) end
return 'tracked=' .. tostring(tracked) .. ' entity=' .. tostring(id)
.. ' model=' .. tostring(root and root.name)
.. ' head=' .. tostring(head and head.name)
.. ' head_parent=' .. tostring(head and head.parent and head.parent.name)
.. ' head_active=' .. tostring(head and head.gameObject.activeInHierarchy)
end)
if not success then detail = 'diagnostic_error=' .. tostring(detail) end
if _G.__FC_FACE_LAST_DIAGNOSTIC ~= detail then
_G.__FC_FACE_LAST_DIAGNOSTIC = detail
local message = '[GaC FaceCam] ' .. detail
CS.UnityEngine.Debug.Log(message)
local written, err = pcall(function()
local dir = _G.__GAC_DIAGNOSTICS_DIR
if not dir or dir == '' then error('server diagnostics directory unavailable') end
CS.System.IO.Directory.CreateDirectory(dir)
local path = CS.System.IO.Path.Combine(dir, 'facecam.log')
CS.System.IO.File.AppendAllText(path, tostring(CS.System.DateTime.Now) .. ' ' .. message .. '\n')
end)
if not written and _G.__FC_FACE_FILE_ERROR ~= tostring(err) then
_G.__FC_FACE_FILE_ERROR = tostring(err)
CS.UnityEngine.Debug.Log('[GaC FaceCam] file log error: ' .. tostring(err))
end
end
end
local function facecam_target(cam)
local ok, head, root = pcall(function()
local battleHead, battleRoot, id, tracked = current_facecam_target()
if tracked and battleHead and battleRoot then
_G.__FC_FACE_ENTITY = id
return battleHead, battleRoot
end
local brain = cam:GetComponent(typeof(CS.Cinemachine.CinemachineBrain))
local virtual = brain and brain.ActiveVirtualCamera
local follow = virtual and virtual.Follow
if not follow then return nil, nil end
local animator = follow:GetComponentInChildren(typeof(CS.UnityEngine.Animator))
if not animator and follow.root then
animator = follow.root:GetComponentInChildren(typeof(CS.UnityEngine.Animator))
end
if animator and animator.isHuman then
local bone = animator:GetBoneTransform(CS.UnityEngine.HumanBodyBones.Head)
if bone then return bone, animator.transform end
end
local nodes = follow:GetComponentsInChildren(typeof(CS.UnityEngine.Transform), true)
if nodes then
for i = 0, nodes.Length - 1 do
local node = nodes[i]
if node.name == 'Head_M' and node.gameObject.activeInHierarchy then
return node, follow.transform
end
end
end
return nil, nil
end)
if ok and head and root then return head, root end
return nil, nil
end
local function update_facecam(cam, settings)
local now = Time.unscaledTime or 0
if now >= (_G.__FC_FACE_NEXT or 0) then
_G.__FC_FACE_NEXT = now + 0.08
local ok, head, root, id, tracked = pcall(current_facecam_target)
log_facecam_target(ok, head, root, id, tracked)
if ok and tracked and head and root then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT, _G.__FC_FACE_ENTITY = head, root, id
_G.__FC_FACE_MISSING_SINCE = nil
else
-- Fall back to Cinemachine active follow target if current phase entity is not resolved
local fbHead, fbRoot = facecam_target(cam)
if fbHead and fbRoot then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = fbHead, fbRoot
_G.__FC_FACE_MISSING_SINCE = nil
end
end
end
local head, root = _G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT
local alive, active = pcall(function() return head and root and head.gameObject.activeInHierarchy end)
if not alive or not active then
-- Immediate re-acquisition: cutscenes or transformations can swap model rigs.
-- Try current target or Cinemachine follow target right now in the same frame!
local ok, nHead, nRoot, nId, tracked = pcall(current_facecam_target)
if ok and tracked and nHead and nRoot then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT, _G.__FC_FACE_ENTITY = nHead, nRoot, nId
head, root = nHead, nRoot
alive, active = true, true
_G.__FC_FACE_MISSING_SINCE = nil
else
local fbHead, fbRoot = facecam_target(cam)
if fbHead and fbRoot then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = fbHead, fbRoot
head, root = fbHead, fbRoot
alive, active = true, true
_G.__FC_FACE_MISSING_SINCE = nil
end
end
end
if not alive or not active then
-- Hold last valid camera transform within grace period (10s); do not drop FaceCam lock.
_G.__FC_FACE_MISSING_SINCE = _G.__FC_FACE_MISSING_SINCE or now
if now - _G.__FC_FACE_MISSING_SINCE > 10.0 then
return false
end
if _G.__FC_POS and _G.__FC_EULER then
cam.transform.position = _G.__FC_POS
cam.transform.eulerAngles = _G.__FC_EULER
return true
end
return true
end
_G.__FC_FACE_MISSING_SINCE = nil
local distance = _G.__FC_FACE_DISTANCE or tonumber(settings.distance) or 2
local scroll = Input.mouseScrollDelta.y
if scroll ~= 0 then distance = distance - scroll * 0.2 end
distance = math.max(0.5, math.min(5, distance))
_G.__FC_FACE_DISTANCE = distance
if scroll ~= 0 then
show_ui('<color=#00FF88><b>[FaceCam: ON]</b> Distance: ' .. string.format('%.1f', distance) .. '</color>')
end
local target = head.position
local anchor = _G.__FC_FACE_ANCHOR
if not anchor or anchor.transform.parent ~= head then
if anchor then pcall(function() CS.UnityEngine.Object.Destroy(anchor) end) end
anchor = GameObject('GaC_FaceCamAnchor')
anchor.transform:SetParent(head, false)
_G.__FC_FACE_ANCHOR = anchor
end
-- หมุนตามหัวเหมือนเดิม: ล็อคตรงหน้าหัวตัวละครแบบ 1:1 ไม่หน่วง ไม่ดีเลย์ และไม่มีการเด้งไปมา
local direction = head.up
anchor.transform.position = target + direction * distance
anchor.transform.rotation = CS.UnityEngine.Quaternion.LookRotation(target - anchor.transform.position, Vector3.up)
cam.transform.position = anchor.transform.position
cam.transform.rotation = anchor.transform.rotation
_G.__FC_POS = cam.transform.position
_G.__FC_EULER = cam.transform.eulerAngles
return true
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
pcall(function()
if cam then
local brain = cam:GetComponent(typeof(CS.Cinemachine.CinemachineBrain))
if brain then brain.enabled = true end
cam.enabled = true
end
local mainCam = CS.UnityEngine.Camera.main
if mainCam and mainCam ~= cam then
local brain = mainCam:GetComponent(typeof(CS.Cinemachine.CinemachineBrain))
if brain then brain.enabled = true end
mainCam.enabled = true
end
end)
return
end
pcall(function()
if not cam then return end
local brain = cam:GetComponent(typeof(CS.Cinemachine.CinemachineBrain))
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)
end
-- Client inspection: InControlInputModule.allowMouseInput controls UI
-- clicks. Battle shortcuts also read MouseBindingSource independently.
-- Mask only mouse button controls in memory; leave keyboard bindings intact.
-- Do not use RemoveBinding/AddBinding (which emit OnBindingsChanged).
_G.__FC_MOUSE_GATE = _G.__FC_MOUSE_GATE or {modules={}, bindings={}}
local function restore_mouse_gate()
local gate = _G.__FC_MOUSE_GATE
for _, state in ipairs(gate.modules) do
pcall(function() state.module.allowMouseInput = true end)
end
for _, state in ipairs(gate.bindings) do
pcall(function()
if state.binding.Control == CS.InControl.Mouse.None then
gate.controlField:SetValue(state.binding, state.control)
state.action:ClearInputState()
end
end)
end
pcall(function()
local eventSystem = CS.UnityEngine.EventSystems.EventSystem.current
if eventSystem then
local module = eventSystem:GetComponent(typeof(CS.InControl.InControlInputModule))
if module then module.allowMouseInput = true end
end
end)
gate.modules, gate.bindings, gate.sets = {}, {}, {}
gate.active = false
end
local function update_mouse_gate()
local gate = _G.__FC_MOUSE_GATE
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 cameraSettings = _G.__GAC_CAMERA_SETTINGS
local wanted = _G.__FREECAM_ENABLED and inBattle and (not cameraSettings or cameraSettings.mode ~= 'facecam')
-- 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
restore_mouse_gate()
end
return
end
gate.active = true
if not gate.controlField then
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
gate.controlField = typeof(CS.InControl.MouseBindingSource):GetField("<Control>k__BackingField", flags)
gate.setsField = typeof(CS.InControl.InputManager):GetField("playerActionSets", flags)
assert(gate.controlField and gate.setsField, "Client mouse input schema is unavailable")
end
local eventSystem = CS.UnityEngine.EventSystems.EventSystem.current
if eventSystem then
local module = eventSystem:GetComponent(typeof(CS.InControl.InControlInputModule))
if module then
local recorded = false
for _, state in ipairs(gate.modules) do
if state.module == module then recorded = true; break end
end
if not recorded then
gate.modules[#gate.modules + 1] = {module=module, allowed=module.allowMouseInput}
end
if Input.GetMouseButton(1) then
module.allowMouseInput = false
else
module.allowMouseInput = true
end
end
end
gate.sets = gate.sets or {}
local sets = gate.setsField:GetValue(nil)
-- Scan each newly created action set once, rather than reflecting/scanning
-- the assembly or all bindings on every render callback.
for i = 0, sets.Count - 1 do
local actionSet = sets[i]
if not gate.sets[actionSet] then
gate.sets[actionSet] = true
local actions = actionSet.Actions
for j = 0, actions.Count - 1 do
local action = actions[j]
local bindings = action.Bindings
for k = 0, bindings.Count - 1 do
local binding = bindings[k]
if typeof(CS.InControl.MouseBindingSource):IsInstanceOfType(binding) then
local control = binding.Control
if control == CS.InControl.Mouse.LeftButton or control == CS.InControl.Mouse.RightButton then
gate.bindings[#gate.bindings + 1] = {binding=binding, control=control, action=action}
gate.controlField:SetValue(binding, CS.InControl.Mouse.None)
action:ClearInputState()
end
end
end
end
end
end
end
function _G.__FREECAM_TICK()
local ok, tickErr = pcall(function()
-- Render callbacks run for several cameras/passes in one frame. Consume
-- key-down events and integrate movement only once per Unity frame.
local frame = Time.frameCount
if _G.__FC_INPUT_FRAME == frame then
if _G.__FREECAM_ENABLED then
local cam = get_active_cam()
if cam and _G.__FC_POS then
cam.transform.position = _G.__FC_POS
cam.transform.eulerAngles = _G.__FC_EULER
end
end
return
end
_G.__FC_INPUT_FRAME = frame
-- Hotkey Handling:
-- Tab: Toggle (as set by user)
if Input.GetKeyDown(KeyCode.Tab) then
_G.__FREECAM_ENABLED = not _G.__FREECAM_ENABLED
_G.__FC_LAST_TIME = nil
local cam = get_active_cam()
local settings = _G.__GAC_CAMERA_SETTINGS or {mode="facecam", distance=2}
if _G.__FREECAM_ENABLED then
if settings.mode == "facecam" then
_G.__FC_FACE_DISTANCE = tonumber(settings.distance) or 2
local head, root
if cam then head, root = facecam_target(cam) end
if head and root then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = head, root
else
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = nil, nil
end
end
if cam then
_G.__FC_POS = cam.transform.position
_G.__FC_EULER = cam.transform.eulerAngles
set_cinemachine_enabled(cam, false)
end
if settings.mode == "facecam" then
show_ui("<color=#00FF88><b>[FaceCam: ON]</b> | Tab: Close | Scroll: Distance</color>")
else
show_ui("<color=#00FF88><b>[FreeCam: ON]</b> Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Height | RMB: Look | Tab: Close</color>")
end
else
if cam then
set_cinemachine_enabled(cam, true)
end
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = nil, nil
_G.__FC_FACE_ENTITY = nil
_G.__FC_LAST_TRACKED_ENTITY = nil
_G.__FC_BATTLE_LAST_ATTACKER = nil
_G.__FC_BATTLE_LAST_AVATAR = nil
_G.__FC_FACE_DISTANCE, _G.__FC_FACE_LOCAL_FORWARD = nil, nil
_G.__FC_FACE_MISSING_SINCE = nil
if _G.__FC_FACE_ANCHOR then CS.UnityEngine.Object.Destroy(_G.__FC_FACE_ANCHOR); _G.__FC_FACE_ANCHOR = nil end
local label = (settings.mode == 'facecam') and 'FaceCam' or 'FreeCam'
show_ui('<color=#00FF88><b>[' .. label .. ': OFF]</b></color>')
end
end
local now = Time.unscaledTime or 0
if now >= (_G.__FC_STATUS_UNTIL or 0) then
if now >= (_G.__FC_WATERMARK_NEXT or 0) then
_G.__FC_WATERMARK_NEXT = now + 0.05
show_ui(rainbow_watermark(now))
end
end
local gateOK, gateError = pcall(update_mouse_gate)
if not gateOK and not _G.__FC_GATE_ERROR then
_G.__FC_GATE_ERROR = true
CS.UnityEngine.Debug.Log("[FreeCam] Mouse gate error: " .. tostring(gateError))
end
-- Detect game phase changes (e.g. Battle <-> Adventure or Scene loading transitions)
-- Immediately restore Cinemachine and deactivate FreeCam/FaceCam so the camera does not
-- get pinned in the void (black screen) when returning to overworld scenes like Dewlight Pavilion.
pcall(function()
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
local phaseField = _G.__FC_PHASE_FIELD or typeof(CS.RPG.Client.GamePhaseManager):GetField("_CurrentPhase", flags)
_G.__FC_PHASE_FIELD = phaseField
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
local currentPhase = manager and phaseField and phaseField:GetValue(manager)
local currentPhaseName = currentPhase and currentPhase:GetType().Name or ""
if _G.__FC_LAST_PHASE_NAME and _G.__FC_LAST_PHASE_NAME ~= currentPhaseName and currentPhaseName ~= "" then
local cam = get_active_cam()
set_cinemachine_enabled(cam, true)
restore_mouse_gate()
_G.__FREECAM_ENABLED = false
_G.__FC_POS = nil
_G.__FC_EULER = nil
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = nil, nil
_G.__FC_FACE_ENTITY = nil
_G.__FC_LAST_TRACKED_ENTITY = nil
_G.__FC_BATTLE_LAST_ATTACKER = nil
_G.__FC_BATTLE_LAST_AVATAR = nil
_G.__FC_FACE_DISTANCE, _G.__FC_FACE_LOCAL_FORWARD = nil, nil
_G.__FC_FACE_MISSING_SINCE = nil
if _G.__FC_FACE_ANCHOR then
CS.UnityEngine.Object.Destroy(_G.__FC_FACE_ANCHOR)
_G.__FC_FACE_ANCHOR = nil
end
end
if currentPhaseName ~= "" then
_G.__FC_LAST_PHASE_NAME = currentPhaseName
end
end)
-- Ensure Cinemachine is always active when Freecam is off to prevent black screens
if not _G.__FREECAM_ENABLED then
pcall(function()
local mainCam = CS.UnityEngine.Camera.main
if mainCam then
local brain = mainCam:GetComponent(typeof(CS.Cinemachine.CinemachineBrain))
if brain and not brain.enabled then
brain.enabled = true
end
end
end)
end
if _G.__FREECAM_ENABLED then
local cam = get_active_cam()
if cam then
local settings = _G.__GAC_CAMERA_SETTINGS or {mode="facecam", distance=2}
if settings.mode == "facecam" then
if not update_facecam(cam, settings) then
set_cinemachine_enabled(cam, true)
_G.__FREECAM_ENABLED = false
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = nil, nil
_G.__FC_FACE_ENTITY = nil
_G.__FC_LAST_TRACKED_ENTITY = nil
_G.__FC_BATTLE_LAST_ATTACKER = nil
_G.__FC_BATTLE_LAST_AVATAR = nil
_G.__FC_FACE_DISTANCE, _G.__FC_FACE_LOCAL_FORWARD = nil, nil
_G.__FC_FACE_MISSING_SINCE = nil
if _G.__FC_FACE_ANCHOR then CS.UnityEngine.Object.Destroy(_G.__FC_FACE_ANCHOR); _G.__FC_FACE_ANCHOR = nil end
show_ui("<color=#FFAA44>FaceCam: character unavailable</color>")
end
return
end
set_cinemachine_enabled(cam, false)
if not _G.__FC_POS then
_G.__FC_POS = cam.transform.position
_G.__FC_EULER = cam.transform.eulerAngles
end
local now = Time.unscaledTime or 0
local dt = now - (_G.__FC_LAST_TIME or now)
if dt <= 0 or dt > 0.1 then dt = 0.016 end
_G.__FC_LAST_TIME = now
-- Speed adjustment (+ / -)
local cameraAdjusted = false
if Input.GetKeyDown(KeyCode.Equals) or Input.GetKeyDown(KeyCode.KeypadPlus) then
_G.__FREECAM_SPEED = math.min(300.0, _G.__FREECAM_SPEED * 1.5)
show_ui("<color=#00FF88><b>[FreeCam: ON]</b> Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Height | RMB: Look | Tab: Close</color>")
end
if Input.GetKeyDown(KeyCode.Minus) or Input.GetKeyDown(KeyCode.KeypadMinus) then
_G.__FREECAM_SPEED = math.max(0.5, _G.__FREECAM_SPEED / 1.5)
show_ui("<color=#00FF88><b>[FreeCam: ON]</b> Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Height | RMB: Look | Tab: Close</color>")
end
-- Raw Unity mouse input remains available to the camera while
-- RMB is held; our InControl mouse gate blocks gameplay clicks.
if Input.GetMouseButton(1) then
local mouseX = Input.GetAxis("Mouse X") or 0
local mouseY = Input.GetAxis("Mouse Y") or 0
local rotSpeed = 3.0
local euler = _G.__FC_EULER or cam.transform.eulerAngles
local newPitch = euler.x - mouseY * rotSpeed
if newPitch > 180 then newPitch = newPitch - 360 end
newPitch = math.max(-89.0, math.min(89.0, newPitch))
local newYaw = euler.y + mouseX * rotSpeed
_G.__FC_EULER = Vector3(newPitch, newYaw, 0)
cameraAdjusted = true
end
local curEuler = _G.__FC_EULER or cam.transform.eulerAngles
local radY = curEuler.y * math.pi / 180.0
local radX = curEuler.x * math.pi / 180.0
local fwd = Vector3(math.sin(radY) * math.cos(radX), -math.sin(radX), math.cos(radY) * math.cos(radX))
local right = Vector3(math.cos(radY), 0, -math.sin(radY))
local move = Vector3(0, 0, 0)
-- Continuous flying via Arrow Keys
if Input.GetKey(KeyCode.UpArrow) then
move = move + fwd
end
if Input.GetKey(KeyCode.DownArrow) then
move = move - fwd
end
if Input.GetKey(KeyCode.RightArrow) then
move = move + right
end
if Input.GetKey(KeyCode.LeftArrow) then
move = move - right
end
if Input.GetKey(KeyCode.PageUp) then
move = move + Vector3(0, 1, 0)
end
if Input.GetKey(KeyCode.PageDown) then
move = move - Vector3(0, 1, 0)
end
if move.sqrMagnitude > 0.0001 then
_G.__FC_POS = _G.__FC_POS + move.normalized * (_G.__FREECAM_SPEED * dt)
cameraAdjusted = true
end
if cameraAdjusted and now >= (_G.__FC_INDICATOR_NEXT or 0) then
_G.__FC_INDICATOR_NEXT = now + 0.1
show_ui('<color=#00FF88><b>[FreeCam: ON]</b> Speed: ' .. string.format('%.1f', _G.__FREECAM_SPEED) .. '</color>')
end
-- Enforce locked free camera transform EVERY frame
cam.transform.position = _G.__FC_POS
cam.transform.eulerAngles = _G.__FC_EULER
end
end
end)
if not ok and _G.__FC_LAST_TICK_ERROR ~= tostring(tickErr) then
_G.__FC_LAST_TICK_ERROR = tostring(tickErr)
CS.UnityEngine.Debug.LogError("[GaC FreeCam Error] " .. tostring(tickErr))
end
end
-- Multiple hooks for complete Unity & xLua render pipeline coverage
if not _G.__FREECAM_REGISTERED then
_G.__FREECAM_REGISTERED = true
local function hook(target, evt)
pcall(function() target[evt]('+', function(c) _G.__FREECAM_TICK() end) end)
pcall(function() target[evt] = target[evt] + function(c) _G.__FREECAM_TICK() end end)
end
hook(Camera, "onPreRender")
hook(Camera, "onPreCull")
hook(Camera, "onPostRender")
pcall(function() CS.UnityEngine.Application.onBeforeRender('+', function() _G.__FREECAM_TICK() end) end)
pcall(function() CS.UnityEngine.Application.onBeforeRender = CS.UnityEngine.Application.onBeforeRender + function() _G.__FREECAM_TICK() end end)
end
if not _G.__FREECAM_ENABLED and (Time.unscaledTime or 0) >= (_G.__FC_STATUS_UNTIL or 0) then
show_ui(rainbow_watermark(Time.unscaledTime or 0))
end
-- Restore the in-memory controls before the Client tears down its settings.
if not _G.__FC_MOUSE_QUIT_REGISTERED then
local ok = pcall(function()
CS.UnityEngine.Application.quitting('+', function() restore_mouse_gate() end)
end)
_G.__FC_MOUSE_QUIT_REGISTERED = ok
end