Files

506 lines
23 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 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 entity, mode
if typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then
local instance = phase.BattleInstanceRef
mode = instance and instance.TurnBasedGameModeRef
entity = mode and mode.CurrentTurnOwnerEntity
-- Inserted attacks can act outside the turn owner's turn. Prefer the
-- most recently attacking entity while the Client reports one.
if mode then
local attackers = mode:GetAttackingEntityList()
if attackers and attackers.Count > 0 then entity = attackers[attackers.Count - 1] 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 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 model = model_for(entity)
-- In a transformed turn the Client can report a model-less turn owner;
-- the live battle's MVP entity points to the visible transformed actor.
if not model and mode then
local fallback = mode.CurrentMVPEntity
local fallbackModel = model_for(fallback)
if 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 nodes = model:GetComponentsInChildren(typeof(CS.UnityEngine.Transform), true)
for i = 0, nodes.Length - 1 do
if nodes[i].name == 'Head_M' and nodes[i].gameObject.activeInHierarchy then
return nodes[i], model.transform, entity.RuntimeID, true
end
end
return nil, nil, nil, true
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
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 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.15
local ok, head, root, id, tracked = pcall(current_facecam_target)
if ok and tracked and head and root then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT, _G.__FC_FACE_ENTITY = head, root, id
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 can briefly unload a model while keeping
-- the same entity ID. Give the replacement time to become available.
_G.__FC_FACE_MISSING_SINCE = _G.__FC_FACE_MISSING_SINCE or now
return now - _G.__FC_FACE_MISSING_SINCE < 2
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 CS.UnityEngine.Object.Destroy(anchor) end
anchor = GameObject('GaC_FaceCamAnchor')
anchor.transform:SetParent(head, false)
_G.__FC_FACE_ANCHOR = anchor
end
-- The observed game rig uses Head_M's local up axis as its facial
-- direction; model-root forward does not follow the animated pose.
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
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 = state.allowed 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
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
module.allowMouseInput = false
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.Tab) 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 not head or not root then
_G.__FREECAM_ENABLED = false
show_ui("<color=#FFAA44>FaceCam: character head unavailable</color>")
CS.UnityEngine.Debug.Log("[FaceCam] active camera follow target has no humanoid head")
return
end
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT = head, root
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</color>")
else
show_ui("<color=#00FF88><b>[FreeCam: ON]</b> Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Up/Down | RMB: Look</color>")
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_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
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_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 (+ / -)
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: Up/Down | RMB: Look</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: Up/Down | RMB: Look</color>")
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('<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)
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