From cd6d8445f2a90819eb386108b4e469a23c188821 Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Thu, 29 Oct 2026 00:22:58 +0700 Subject: [PATCH 1/5] Track active character with FaceCam --- scripts/facecam_probe_temp.lua | 20 ------- scripts/freecam.lua | 96 ++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 31 deletions(-) delete mode 100644 scripts/facecam_probe_temp.lua diff --git a/scripts/facecam_probe_temp.lua b/scripts/facecam_probe_temp.lua deleted file mode 100644 index a29cd57..0000000 --- a/scripts/facecam_probe_temp.lua +++ /dev/null @@ -1,20 +0,0 @@ -if not _G.__GAC_FACECAM_PROBE_BATTLE then - local ok,err=pcall(function() - local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60) - local manager=CS.RPG.Client.GlobalVars.s_GamePhaseManager - local phase=typeof(CS.RPG.Client.GamePhaseManager):GetField('_CurrentPhase',flags):GetValue(manager) - if not phase or not typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then return end - _G.__GAC_FACECAM_PROBE_BATTLE=true - local t=phase:GetType() - CS.UnityEngine.Debug.Log('[FaceCam battle] phase='..tostring(t)) - for _,members in ipairs({t:GetFields(flags),t:GetProperties(flags)}) do - for i=0,math.min(members.Length-1,199) do - local m=members[i];local n=tostring(m.Name):lower() - if n:find('actor') or n:find('entity') or n:find('avatar') or n:find('turn') or n:find('battle') or n:find('stage') then - CS.UnityEngine.Debug.Log('[FaceCam battle] member='..tostring(m.Name)..' detail='..tostring(m)) - end - end - end - end) - if not ok then _G.__GAC_FACECAM_PROBE_BATTLE=true;CS.UnityEngine.Debug.Log('[FaceCam battle] error='..tostring(err)) end -end diff --git a/scripts/freecam.lua b/scripts/freecam.lua index 33e4592..dc4fd37 100644 --- a/scripts/freecam.lua +++ b/scripts/freecam.lua @@ -33,6 +33,9 @@ local function rainbow_watermark(now) 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 @@ -62,11 +65,52 @@ local function get_active_cam() return nil end --- Resolve the character through the active camera's own follow target. A --- humanoid head bone gives a model-relative point as the character moves. --- Do not guess a world position if the Client has no usable target. +-- 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 + if typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then + local instance = phase.BattleInstanceRef + local 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 + if _G.__FC_FACE_ENTITY == entity.RuntimeID and _G.__FC_FACE_HEAD and _G.__FC_FACE_ROOT then + return _G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT, entity.RuntimeID, true + end + local component = entity:GetComponent(typeof(CS.RPG.GameCore.CharacterModelComponent)) + local model = component and component:GetModelGO() + if not model then return nil, nil, nil, true end + local nodes = model:GetComponentsInChildren(typeof(CS.UnityEngine.Transform), true) + for i = 0, nodes.Length - 1 do + if nodes[i].name == 'Head_M' 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 @@ -84,15 +128,37 @@ local function facecam_target(cam) 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 if not head or not root or not head.gameObject.activeInHierarchy then return false end - local distance = tonumber(settings.distance) or 2 + 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 local target = head.position - local direction = root.forward - local position = target + direction * distance - cam.transform.position = position - cam.transform.rotation = CS.UnityEngine.Quaternion.LookRotation(target - position, Vector3.up) + 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 + -- Capture the character's forward direction in the head's local + -- space: this respects the model's bone axes and follows head turns. + _G.__FC_FACE_LOCAL_FORWARD = CS.UnityEngine.Quaternion.Inverse(head.rotation) * root.forward + end + local direction = head.rotation * _G.__FC_FACE_LOCAL_FORWARD + 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 @@ -153,7 +219,8 @@ local function update_mouse_gate() local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager local phase = manager and gate.phaseField:GetValue(manager) local inBattle = phase and typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) - local wanted = _G.__FREECAM_ENABLED and inBattle + 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 @@ -233,6 +300,7 @@ function _G.__FREECAM_TICK() 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 @@ -258,12 +326,15 @@ function _G.__FREECAM_TICK() 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 + if _G.__FC_FACE_ANCHOR then CS.UnityEngine.Object.Destroy(_G.__FC_FACE_ANCHOR); _G.__FC_FACE_ANCHOR = nil end show_ui(rainbow_watermark(Time.unscaledTime or 0)) end end - if not _G.__FREECAM_ENABLED then - local now = Time.unscaledTime or 0 + local now = Time.unscaledTime or 0 + if not _G.__FREECAM_ENABLED or 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)) @@ -285,6 +356,9 @@ function _G.__FREECAM_TICK() 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 + 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 -- 2.47.3 From 85946a39961e5dbe2fd7528f1c6c3d551a1cb70f Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Thu, 29 Oct 2026 00:24:53 +0700 Subject: [PATCH 2/5] Refine FaceCam controls and status indicator --- scripts/freecam.lua | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/freecam.lua b/scripts/freecam.lua index dc4fd37..6c46f1d 100644 --- a/scripts/freecam.lua +++ b/scripts/freecam.lua @@ -143,6 +143,9 @@ local function update_facecam(cam, settings) 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 @@ -322,6 +325,7 @@ function _G.__FREECAM_TICK() show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Up/Down | RMB: Look") end else + local settings = _G.__GAC_CAMERA_SETTINGS or {mode="freecam"} if cam then set_cinemachine_enabled(cam, true) end @@ -329,12 +333,13 @@ function _G.__FREECAM_TICK() _G.__FC_FACE_ENTITY = nil _G.__FC_FACE_DISTANCE, _G.__FC_FACE_LOCAL_FORWARD = nil, nil if _G.__FC_FACE_ANCHOR then CS.UnityEngine.Object.Destroy(_G.__FC_FACE_ANCHOR); _G.__FC_FACE_ANCHOR = nil end - show_ui(rainbow_watermark(Time.unscaledTime or 0)) + local label = settings.mode == 'facecam' and 'FaceCam' or 'FreeCam' + show_ui('[' .. label .. ': OFF]') end end local now = Time.unscaledTime or 0 - if not _G.__FREECAM_ENABLED or now >= (_G.__FC_STATUS_UNTIL or 0) then + 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)) @@ -387,9 +392,11 @@ function _G.__FREECAM_TICK() -- 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 @@ -426,6 +433,11 @@ function _G.__FREECAM_TICK() 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 @@ -452,7 +464,7 @@ if not _G.__FREECAM_REGISTERED then pcall(function() CS.UnityEngine.Application.onBeforeRender = CS.UnityEngine.Application.onBeforeRender + function() _G.__FREECAM_TICK() end end) end -if not _G.__FREECAM_ENABLED then +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 -- 2.47.3 From 3fbb70a30c80dd5e0dafd81e27ea7cf9b2a719cf Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Thu, 29 Oct 2026 00:32:03 +0700 Subject: [PATCH 3/5] Align FaceCam with animated head direction --- scripts/freecam.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/freecam.lua b/scripts/freecam.lua index 6c46f1d..5bb5cd4 100644 --- a/scripts/freecam.lua +++ b/scripts/freecam.lua @@ -153,11 +153,10 @@ local function update_facecam(cam, settings) anchor = GameObject('GaC_FaceCamAnchor') anchor.transform:SetParent(head, false) _G.__FC_FACE_ANCHOR = anchor - -- Capture the character's forward direction in the head's local - -- space: this respects the model's bone axes and follows head turns. - _G.__FC_FACE_LOCAL_FORWARD = CS.UnityEngine.Quaternion.Inverse(head.rotation) * root.forward end - local direction = head.rotation * _G.__FC_FACE_LOCAL_FORWARD + -- 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 -- 2.47.3 From 448ea61a68c8ead3bb937000afb7c0383f8db778 Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Thu, 29 Oct 2026 00:47:38 +0700 Subject: [PATCH 4/5] Follow transformed battle characters in FaceCam --- scripts/freecam.lua | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/scripts/freecam.lua b/scripts/freecam.lua index 5bb5cd4..15c8ba4 100644 --- a/scripts/freecam.lua +++ b/scripts/freecam.lua @@ -74,10 +74,10 @@ local function current_facecam_target() 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 + local entity, mode if typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then local instance = phase.BattleInstanceRef - local mode = instance and instance.TurnBasedGameModeRef + 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. @@ -91,15 +91,35 @@ local function current_facecam_target() return nil, nil, nil, false end if not entity then return nil, nil, nil, true end - if _G.__FC_FACE_ENTITY == entity.RuntimeID and _G.__FC_FACE_HEAD and _G.__FC_FACE_ROOT then + 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 component = entity:GetComponent(typeof(CS.RPG.GameCore.CharacterModelComponent)) - local model = component and component:GetModelGO() - if not model then return nil, nil, nil, true end local nodes = model:GetComponentsInChildren(typeof(CS.UnityEngine.Transform), true) for i = 0, nodes.Length - 1 do - if nodes[i].name == 'Head_M' then return nodes[i], model.transform, entity.RuntimeID, true end + 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 @@ -137,7 +157,14 @@ local function update_facecam(cam, settings) end end local head, root = _G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT - if not head or not root or not head.gameObject.activeInHierarchy then return false end + 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 @@ -331,6 +358,7 @@ function _G.__FREECAM_TICK() _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('[' .. label .. ': OFF]') @@ -362,6 +390,7 @@ function _G.__FREECAM_TICK() _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("FaceCam: character unavailable") end -- 2.47.3 From 1fa9504176e70e618154f13cf872c76009cb414a Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Thu, 29 Oct 2026 00:51:34 +0700 Subject: [PATCH 5/5] Document Freecam and FaceCam controls --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d99af99..57fff87 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,8 @@ Hide UI ใช้ปุ่ม Backslash เพื่อซ่อน HUD รว Ping colors อยู่ใน `scripts/ping_rainbow.lua` เปลี่ยนสีทั้งแท่งสัญญาณและตัวเลขโดยใช้ค่า ping จริง ภาพหลอดใช้ alpha จาก sprite เดิมและสีพื้นขาวเพื่อไม่ให้สีเพี้ยน ส่วนตัวเลขลงสีที่ glyph โดยไม่เขียนข้อความทับ จึงไม่กะพริบเมื่อค่าเปลี่ยน ผู้ใช้ยืนยันผลหน้าเดินแล้ว รายละเอียดและขอบเขตอยู่ใน `docs/ping-colors.md` +Freecam และ FaceCam เลือกได้จากหน้า **สวิตช์ระบบพิเศษของ GaC** ใน [GaCy Srtool](https://gacy-srtool.vercel.app/) โดยตั้งระยะเริ่มต้นของ FaceCam แล้วกด Tab ในเกมเพื่อเปิดหรือปิด Freecam ใช้ปุ่มลูกศรและ Page Up / Page Down เคลื่อนกล้อง พร้อมคลิกขวาค้างเพื่อหมุน ส่วน FaceCam ยึดกล้องกับหัวตัวละครที่ควบคุมหรือกำลังแอ็กชันใน Battle ลูกกลิ้งปรับระยะชั่วคราว และเมื่อปิด–เปิดใหม่จะกลับเป็นระยะที่ตั้งไว้ ข้อความสถานะกล้องแสดงเมื่อปรับและหายหลังหยุดปรับ 3 วินาที ผู้ใช้ยืนยันมุม Battle และร่าง Khaslana แล้ว + `resources/PlayerCosmetics.json` ใช้รายการ PlayerOutfit และ AvatarSkin ที่อ่านจาก Client การปลดของแต่งผู้บุกเบิกส่งไอเท็มเป็นเจ้าของครั้งละหนึ่งชิ้นใน GetBag ส่วนชุดที่เลือกผ่าน SetPlayerOutfit เก็บใน `persistent.player_outfits` และส่งกลับเมื่อ login ใหม่ ค่า Outfit ID ที่สวมรับจาก selector ของ Client โดยตรงและ sync ตามโครงสร้าง PearlSR; ยังไม่ได้ export ตารางจับคู่ OutfitID/ItemID จึงไม่แปลงเลขด้วยสูตร ข้อมูลฉากที่ปรับรอบนี้จำกัดที่ Starfroster กับ Moment of Joy ใน `resources/SceneMetadata.json`: อัปเดตกลุ่ม/prop ที่ยังมีอยู่และเป็นของ Server พร้อม SavedValues จาก `RuntimeDimension.DefaultValue` จึงไม่ใช้ MaxValue เป็นค่าเริ่มต้นของสองแมพนี้ กลไกรถไฟเป็น prop ฝั่ง Client และมีตัวแปร FSV_Coaster ในตารางปัจจุบัน ผลการแสดงฉากและปุ่มรถไฟยังต้องยืนยันในเกม ส่วนแมพอื่นยังใช้ข้อมูลเดิม ไม่ได้อ้างว่าอัปเดตทั้ง resource แล้ว -- 2.47.3