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 = "นี่คือเวอร์ชั่นทดสอบของเกย์ " 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("%s", 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('[FaceCam: ON] Distance: ' .. string.format('%.1f', distance) .. '') 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("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("[FaceCam: ON] | Tab: Close | Scroll: Distance") else show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Height | RMB: Look | Tab: Close") 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('[' .. label .. ': OFF]') 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("FaceCam: character unavailable") 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("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Height | RMB: Look | Tab: Close") 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("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Height | RMB: Look | Tab: Close") 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('[FreeCam: ON] Speed: ' .. string.format('%.1f', _G.__FREECAM_SPEED) .. '') 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