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 manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60) local field = typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase', flags) local phase = manager and field and field: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, true 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, true 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, true 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 then _G.__FC_FACE_ENTITY = id if battleHead and battleRoot then return battleHead, battleRoot end 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 not animator or not animator.isHuman then return nil, nil end local bone = animator:GetBoneTransform(CS.UnityEngine.HumanBodyBones.Head) return bone, animator.transform end) if ok 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 then if 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 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 -- Character transformations or ultimate cutscenes can briefly unload/hide a model. -- Hold last valid camera transform only within a short grace period (3s). -- If character remains unavailable (e.g. battle finished or scene changed), -- return false so FaceCam automatically deactivates and restores Cinemachine. _G.__FC_FACE_MISSING_SINCE = _G.__FC_FACE_MISSING_SINCE or now if now - _G.__FC_FACE_MISSING_SINCE > 3.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 false 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 -- Target: Center of the face/eyes (~0.08m above neck pivot Head_M) local eyeOffset = 0.08 local faceCenter = head.position + Vector3(0, eyeOffset, 0) -- In HSR rigs, head.up is aligned with the facial forward axis. -- Project onto horizontal plane (Y=0) so camera remains level and doesn't tilt/pitch with nodding. local fwdX = head.up.x local fwdZ = head.up.z local fwdMagSq = fwdX * fwdX + fwdZ * fwdZ local fwd if fwdMagSq > 0.001 then local invMag = 1.0 / math.sqrt(fwdMagSq) fwd = Vector3(fwdX * invMag, 0, fwdZ * invMag) elseif root then local rX = root.forward.x local rZ = root.forward.z local rMagSq = rX * rX + rZ * rZ if rMagSq > 0.001 then local invR = 1.0 / math.sqrt(rMagSq) fwd = Vector3(rX * invR, 0, rZ * invR) else fwd = Vector3(0, 0, 1) end else fwd = Vector3(0, 0, 1) end local desiredPos = faceCenter + fwd * distance local lookDir = faceCenter - desiredPos local desiredRot = CS.UnityEngine.Quaternion.LookRotation(lookDir, Vector3.up) -- Smooth interpolation to remove micro-jitter while tracking local dt = Time.unscaledDeltaTime or 0.016 if dt <= 0 or dt > 0.1 then dt = 0.016 end local smoothSpeed = 16.0 local factor = math.min(1.0, dt * smoothSpeed) if _G.__FC_LAST_TRACKED_ENTITY ~= _G.__FC_FACE_ENTITY or not _G.__FC_POS then _G.__FC_LAST_TRACKED_ENTITY = _G.__FC_FACE_ENTITY cam.transform.position = desiredPos cam.transform.rotation = desiredRot else cam.transform.position = Vector3.Lerp(cam.transform.position, desiredPos, factor) cam.transform.rotation = CS.UnityEngine.Quaternion.Slerp(cam.transform.rotation, desiredRot, factor) end _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() 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 if Input.GetKeyDown(KeyCode.F8) or Input.GetKeyDown(KeyCode.Insert) then _G.__FREECAM_ENABLED = not _G.__FREECAM_ENABLED _G.__FC_LAST_TIME = nil local cam = get_active_cam() if _G.__FREECAM_ENABLED then local settings = _G.__GAC_CAMERA_SETTINGS or {mode="freecam", distance=2} 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] | F8/Insert: Close") else show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Up/Down | RMB: Look | F8/Insert: Close") end else local settings = _G.__GAC_CAMERA_SETTINGS or {mode="freecam"} 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 manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager local phaseField = _G.__FC_PHASE_FIELD if not phaseField then local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60) phaseField = typeof(CS.RPG.Client.GamePhaseManager):GetField("_CurrentPhase", flags) _G.__FC_PHASE_FIELD = phaseField end 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) if _G.__FREECAM_ENABLED then local cam = get_active_cam() if cam then local settings = _G.__GAC_CAMERA_SETTINGS or {mode="freecam", 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 (+ / -) 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: Up/Down | RMB: Look | F8/Insert: 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: Up/Down | RMB: Look | F8/Insert: Close") end -- Raw Unity mouse input remains available to the camera while -- InControl battle clicks are gated separately below. local cameraAdjusted = false if Input.GetMouseButton(1) then local rotX = Input.GetAxis("Mouse X") or 0 local rotY = Input.GetAxis("Mouse Y") or 0 cameraAdjusted = rotX ~= 0 or rotY ~= 0 _G.__FC_EULER = Vector3(_G.__FC_EULER.x - rotY * 2.5, _G.__FC_EULER.y + rotX * 2.5, 0) end -- Compute 3D direction vectors from camera Euler angles local radY = _G.__FC_EULER.y * (math.pi / 180.0) local radX = _G.__FC_EULER.x * (math.pi / 180.0) local cosX = math.cos(radX) local fwd = Vector3(math.sin(radY) * cosX, -math.sin(radX), math.cos(radY) * cosX) 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) 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