diff --git a/Ananta.Proxy/proxy/public/fastpatch_4229938.zip b/Ananta.Proxy/proxy/public/fastpatch_4229938.zip index c1d72c1..debb48f 100644 Binary files a/Ananta.Proxy/proxy/public/fastpatch_4229938.zip and b/Ananta.Proxy/proxy/public/fastpatch_4229938.zip differ diff --git a/Ananta.Proxy/proxy/tools/generate_client_config_patch.js b/Ananta.Proxy/proxy/tools/generate_client_config_patch.js index ceaed7e..6a5d220 100644 --- a/Ananta.Proxy/proxy/tools/generate_client_config_patch.js +++ b/Ananta.Proxy/proxy/tools/generate_client_config_patch.js @@ -193,6 +193,30 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end\n` + ` end)\n` + ` pcall(function()\n` + +` if CS and CS.L50 and CS.L50.Script and CS.L50.Script.LX6 and CS.L50.Script.LX6.Security then\n` + +` local sec = CS.L50.Script.LX6.Security\n` + +` if sec.DavinciReport then\n` + +` sec.DavinciReport.Send = function() end\n` + +` sec.DavinciReport.Report = function() end\n` + +` end\n` + +` if sec.DavinciMgr then\n` + +` sec.DavinciMgr.CheckTimeScale = function() end\n` + +` sec.DavinciMgr.ReportTimeScale = function() end\n` + +` sec.DavinciMgr.CheckSpeed = function() end\n` + +` end\n` + +` end\n` + +` if CS and CS.LX6 and CS.LX6.Security then\n` + +` local sec = CS.LX6.Security\n` + +` if sec.DavinciReport then\n` + +` sec.DavinciReport.Send = function() end\n` + +` sec.DavinciReport.Report = function() end\n` + +` end\n` + +` if sec.DavinciMgr then\n` + +` sec.DavinciMgr.CheckTimeScale = function() end\n` + +` sec.DavinciMgr.ReportTimeScale = function() end\n` + +` sec.DavinciMgr.CheckSpeed = function() end\n` + +` end\n` + +` end\n` + ` if DavinciReport then\n` + ` DavinciReport.Send = function() end\n` + ` DavinciReport.Report = function() end\n` + @@ -221,87 +245,102 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end\n` + ` end)\n` + `end\n\n` + -`local function HookMasterNotice()\n` + -` if MasterToClientImpl and not MasterToClientImpl._cmdHooked then\n` + -` MasterToClientImpl._cmdHooked = true\n` + -` local oldSyncNotice = MasterToClientImpl.SyncNotice\n` + -` MasterToClientImpl.SyncNotice = function(...)\n` + -` local allArgs = {...}\n` + -` local content = nil\n` + -` for i = 1, #allArgs do\n` + -` if type(allArgs[i]) == "string" then\n` + -` content = allArgs[i]\n` + -` break\n` + -` end\n` + +`local function ProcessDebugCommand(cmd)\n` + +` if not cmd or type(cmd) ~= "string" then return end\n` + +` if cmd == "UNSTUCK_BLACKSCREEN" then\n` + +` pcall(function()\n` + +` if gBlackScreenManager then gBlackScreenManager:ClearTransition(nil, true) end\n` + +` if gVideoManager then gVideoManager:CloseBlackScreen() end\n` + +` if gPanelManager and gPanelId and gPanelId.S_VIDEO_PLAYER_PANEL then\n` + +` gPanelManager:CloseWindow(gPanelId.S_VIDEO_PLAYER_PANEL)\n` + ` end\n` + -` if content and string.sub(content, 1, 4) == "CMD:" then\n` + -` local cmd = string.sub(content, 5)\n` + -` if cmd == "UNSTUCK_BLACKSCREEN" then\n` + -` pcall(function()\n` + -` if gBlackScreenManager then\n` + -` gBlackScreenManager:ClearTransition(nil, true)\n` + -` end\n` + -` if gVideoManager then\n` + -` gVideoManager:CloseBlackScreen()\n` + -` end\n` + -` if gPanelManager and gPanelId and gPanelId.S_VIDEO_PLAYER_PANEL then\n` + -` gPanelManager:CloseWindow(gPanelId.S_VIDEO_PLAYER_PANEL)\n` + -` end\n` + -` if LX6 and LX6.GUI and LX6.GUI.GuiMgr and gPanelId and gPanelId.COMMON_BLACK_TRANSITION then\n` + -` LX6.GUI.GuiMgr.Instance:SetShowScenePanel(false, gPanelId.COMMON_BLACK_TRANSITION)\n` + -` gPanelManager:RemoveVisibleMode(LX6.Manager.VisibleControlType.CommonBlack)\n` + -` LX6.Manager.GameInputManager.SetEnableInput(gPanelId.COMMON_BLACK_TRANSITION)\n` + -` end\n` + -` end)\n` + -` return\n` + -` elseif string.sub(cmd, 1, 14) == "PLAY_CUTSCENE:" then\n` + -` local arg = string.sub(cmd, 15)\n` + -` local numId = tonumber(arg)\n` + -` pcall(function()\n` + -` if numId and gPanelManager and gPanelId and gPanelId.S_VIDEO_PLAYER_PANEL then\n` + -` gPanelManager:OpenWindow(gPanelId.S_VIDEO_PLAYER_PANEL, { videoId = numId })\n` + -` elseif gTimelineManager and gTimelineManager.Timeline_LoadAndPlay then\n` + -` gTimelineManager:Timeline_LoadAndPlay(arg, nil)\n` + -` elseif gVideoManager and gVideoManager.PlayVideo then\n` + -` gVideoManager:PlayVideo(numId or arg)\n` + -` end\n` + -` end)\n` + -` return\n` + -` elseif string.sub(cmd, 1, 12) == "SPAWN_ENEMY:" then\n` + -` local p1, p2, p3 = string.match(string.sub(cmd, 13), "([^:]+):?([^:]*):?([^:]*)")\n` + -` local enemyId = tonumber(p1) or 40900579\n` + -` local camp = tonumber(p2) or 2\n` + -` local count = tonumber(p3) or 1\n` + -` pcall(function()\n` + -` if gCS and gCS.GmUtils and gCS.GmUtils.AddEnemyWithCamp then\n` + -` gCS.GmUtils.AddEnemyWithCamp(enemyId, camp)\n` + -` elseif gCS and gCS.GmUtils and gCS.GmUtils.AddEnemy then\n` + -` gCS.GmUtils.AddEnemy(enemyId)\n` + -` elseif gCS and gCS.LuaUtils and gCS.LuaUtils.AddEnemy then\n` + -` gCS.LuaUtils.AddEnemy(enemyId, 1, camp, count)\n` + -` elseif CS and CS.LX6 and CS.LX6.GmUtils and CS.LX6.GmUtils.AddEnemyWithCamp then\n` + -` CS.LX6.GmUtils.AddEnemyWithCamp(enemyId, camp)\n` + -` end\n` + -` end)\n` + -` return\n` + -` end\n` + +` if LX6 and LX6.GUI and LX6.GUI.GuiMgr and gPanelId and gPanelId.COMMON_BLACK_TRANSITION then\n` + +` LX6.GUI.GuiMgr.Instance:SetShowScenePanel(false, gPanelId.COMMON_BLACK_TRANSITION)\n` + +` gPanelManager:RemoveVisibleMode(LX6.Manager.VisibleControlType.CommonBlack)\n` + +` LX6.Manager.GameInputManager.SetEnableInput(gPanelId.COMMON_BLACK_TRANSITION)\n` + +` end\n` + +` end)\n` + +` elseif string.sub(cmd, 1, 14) == "PLAY_CUTSCENE:" then\n` + +` local arg = string.sub(cmd, 15)\n` + +` local numId = tonumber(arg)\n` + +` pcall(function()\n` + +` if numId and gPanelManager and gPanelId and gPanelId.S_VIDEO_PLAYER_PANEL then\n` + +` gPanelManager:OpenWindow(gPanelId.S_VIDEO_PLAYER_PANEL, { videoId = numId })\n` + +` elseif gTimelineManager and gTimelineManager.Timeline_LoadAndPlay then\n` + +` gTimelineManager:Timeline_LoadAndPlay(arg, nil)\n` + +` elseif gVideoManager and gVideoManager.PlayVideo then\n` + +` gVideoManager:PlayVideo(numId or arg)\n` + +` end\n` + +` end)\n` + +` elseif string.sub(cmd, 1, 12) == "SPAWN_ENEMY:" then\n` + +` local p1, p2, p3 = string.match(string.sub(cmd, 13), "([^:]+):?([^:]*):?([^:]*)")\n` + +` local enemyId = tonumber(p1) or 40900579\n` + +` local camp = tonumber(p2) or 2\n` + +` local count = tonumber(p3) or 1\n` + +` pcall(function()\n` + +` if gCS and gCS.LuaUtils and gCS.LuaUtils.AddEnemy then\n` + +` gCS.LuaUtils.AddEnemy(enemyId, 1, camp, count)\n` + +` elseif gCS and gCS.GmUtils and gCS.GmUtils.AddEnemyWithCamp then\n` + +` gCS.GmUtils.AddEnemyWithCamp(enemyId, camp)\n` + +` elseif gCS and gCS.GmUtils and gCS.GmUtils.AddEnemy then\n` + +` gCS.GmUtils.AddEnemy(enemyId)\n` + +` elseif CS and CS.LX6 and CS.LX6.GmUtils and CS.LX6.GmUtils.AddEnemyWithCamp then\n` + +` CS.LX6.GmUtils.AddEnemyWithCamp(enemyId, camp)\n` + +` end\n` + +` end)\n` + +` end\n` + +`end\n\n` + +`local function HookNoticeTable(tbl)\n` + +` if not tbl or type(tbl) ~= "table" or tbl._cmdHooked then return end\n` + +` tbl._cmdHooked = true\n` + +` local oldSync = tbl.SyncNotice\n` + +` tbl.SyncNotice = function(...)\n` + +` local allArgs = {...}\n` + +` for i = 1, #allArgs do\n` + +` if type(allArgs[i]) == "string" and string.sub(allArgs[i], 1, 4) == "CMD:" then\n` + +` ProcessDebugCommand(string.sub(allArgs[i], 5))\n` + ` return\n` + ` end\n` + -` if oldSyncNotice then\n` + -` return oldSyncNotice(...)\n` + +` end\n` + +` if oldSync then return oldSync(...) end\n` + +` end\n` + +`end\n\n` + +`local function HookDisplayMessageMgr()\n` + +` if gDisplayMessageMgr and not gDisplayMessageMgr._cmdHooked then\n` + +` gDisplayMessageMgr._cmdHooked = true\n` + +` local oldShow = gDisplayMessageMgr.ShowMessageContent\n` + +` gDisplayMessageMgr.ShowMessageContent = function(self, content, ...)\n` + +` if type(content) == "string" and string.sub(content, 1, 4) == "CMD:" then\n` + +` ProcessDebugCommand(string.sub(content, 5))\n` + +` return\n` + ` end\n` + +` return oldShow(self, content, ...)\n` + ` end\n` + ` end\n` + `end\n\n` + +`local function HookAllNotices()\n` + +` pcall(function()\n` + +` if package and package.loaded then\n` + +` HookNoticeTable(package.loaded["LX6/Service/MasterToClientImpl"])\n` + +` HookNoticeTable(package.loaded["LX6/Service/GameToClientImpl"])\n` + +` end\n` + +` if MasterToClientImpl then HookNoticeTable(MasterToClientImpl) end\n` + +` if GameToClientImpl then HookNoticeTable(GameToClientImpl) end\n` + +` HookDisplayMessageMgr()\n` + +` end)\n` + +`end\n\n` + `local oldRequire = require\n` + `require = function(mod)\n` + ` local res = oldRequire(mod)\n` + ` pcall(ApplyAllGlobalHooks)\n` + -` pcall(HookMasterNotice)\n` + +` if mod == "LX6/Service/MasterToClientImpl" or mod == "LX6/Service/GameToClientImpl" then\n` + +` if type(res) == "table" then HookNoticeTable(res) end\n` + +` end\n` + +` pcall(HookAllNotices)\n` + ` return res\n` + `end\n\n` + `pcall(ApplyAllGlobalHooks)\n` + -`pcall(HookMasterNotice)\n`; +`pcall(HookAllNotices)\n`; const switchBytes = to_luastring(switchSource); const switchStatus = lauxlib.luaL_loadbuffer(L, switchBytes, switchBytes.length, to_luastring(switchFileName)); if (switchStatus !== lua.LUA_OK) throw new Error("generated ClientGameSwitch.lua failed Lua syntax validation"); @@ -318,3 +357,14 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + "No gameplay Lua overrides are included.\n", "utf8", ); + + const { execSync } = require("child_process"); + const publicZip = path.join(__dirname, "..", "public", `fastpatch_${clientVersion}.zip`); + try { + const pyCmd = `python -c "import zipfile, os; z = zipfile.ZipFile(r'${publicZip.replace(/\\/g, "\\\\")}', 'w', zipfile.ZIP_DEFLATED); [z.write(os.path.join(r'${fastPatchRoot.replace(/\\/g, "\\\\")}', f), f) for f in os.listdir(r'${fastPatchRoot.replace(/\\/g, "\\\\")}') if os.path.isfile(os.path.join(r'${fastPatchRoot.replace(/\\/g, "\\\\")}', f))]; z.close()"`; + execSync(pyCmd); + console.log(`Successfully generated and packed: ${publicZip}`); + } catch (err) { + console.warn(`Warning: Could not automatically create zip via python: ${err.message}`); + } + diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Combat.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Combat.cs index 7176875..d110ea1 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Combat.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Combat.cs @@ -573,6 +573,9 @@ internal sealed partial class GameRouter CombatWeaponDefinition weapon, CombatStyleDefinition style) { + var state = GetWorldState(ctx); + state.CombatProfilePublished = true; + // Switching/individually styling one weapon must never overwrite the character-wide // FightStyleInfo map. OnSwitchFightStyle publishes that map explicitly. await ctx.NotifyAsync(MethodId.SyncPlayerAllSkillChargeData, @@ -581,6 +584,7 @@ internal sealed partial class GameRouter await PublishSkillBindings(ctx, unitId, weapon, style); await ctx.NotifyAsync(MethodId.SyncSpiritLastUsedWeapon, CombatCodec.SpiritLastUsedWeapon(templateId, weapon.InstanceId)); + ctx.Session.Log.Info($"[COMBAT] profile published unit={unitId} template={templateId} weapon={weapon.TemplateId}/{weapon.InstanceId} style={style.Id} skills={style.CommonSkill},{style.HeavyCommonSkill},{style.DodgeSkill},{style.ControlSkill},{weapon.ActiveSkill(style)},{weapon.UniqueSkill(style)}"); } private static CombatStyleDefinition ResolveWeaponStyle(WorldEntryState state, CombatWeaponDefinition weapon) diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gm.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gm.cs index 1ae0240..03e8311 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gm.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gm.cs @@ -66,4 +66,11 @@ internal sealed partial class GameRouter { conn.Log.Info("[SECURITY] DavinciCode notify absorbed"); } + + [Handler(MethodId.GmDaVinciCode, HandlerPacketKind.Invoke)] + private static Task GmDaVinciCode(Connection conn, UxRpcMessage msg) + { + conn.Log.Info("[SECURITY] GmDaVinciCode invoke absorbed"); + return conn.ReturnEmptyOkAsync(msg); + } } diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Movement.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Movement.cs index 7f1b884..86ed50a 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Movement.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Movement.cs @@ -66,6 +66,21 @@ internal sealed partial class GameRouter var unitId = state.ActiveSpiritUnitId != 0 ? state.ActiveSpiritUnitId : Profile.InitialUnitId; var templateId = state.ActiveSpiritTemplateId != 0 ? state.ActiveSpiritTemplateId : Profile.InitialSpiritTemplateId; + if (!state.CombatProfilePublished) + { + var weapon = CombatCodec.Weapon(templateId, state.ActiveWeaponInstanceId) + ?? CombatCodec.DefaultWeapon(templateId); + var style = ResolveWeaponStyle(state, weapon); + state.ActiveWeaponInstanceId = weapon.InstanceId; + state.ActiveFightStyleId = style.Id; + state.LastWeaponBySpirit[templateId] = weapon.InstanceId; + + await PublishWeaponSnapshot(ctx, unitId, templateId, weapon.InstanceId); + await ctx.NotifyAsync(MethodId.SyncSpiritSwitchWeaponAction, + CombatCodec.SpiritSwitchWeapon(unitId, weapon.InstanceId)); + await PublishSelectedWeaponProfile(ctx, unitId, templateId, weapon, style); + } + if (!state.AllBuildBuffsPublished) { await PublishSafeRuntimeBuffSnapshot4229938(ctx, unitId, templateId, "first-gameplay-movement"); @@ -78,6 +93,6 @@ internal sealed partial class GameRouter await ctx.NotifyAsync(MethodId.SyncGamePause, WorldCodec.GamePause(false)); state.MovementCapabilityPublished = true; state.FreeRoamReleased = true; - ctx.Session.Log.Info($"[GAMEPLAY-V7] first-movement post-load-buffs unit={unitId} template={templateId} actorPresentation=client-owned safeBuffs=true pause=false catalogBulk=false"); + ctx.Session.Log.Info($"[GAMEPLAY-V7] first-movement post-load-buffs unit={unitId} template={templateId} combatProfile=true safeBuffs=true pause=false catalogBulk=false"); } } diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Profiles.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Profiles.cs index 6fa7f87..81acb81 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Profiles.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Profiles.cs @@ -85,8 +85,9 @@ internal sealed partial class GameRouter CombatCodec.SpiritLastUsedWeapon(templateId, weapon.InstanceId)); await ctx.NotifyAsync(MethodId.SyncSpiritSwitchWeaponAction, CombatCodec.SpiritSwitchWeapon(unitId, weapon.InstanceId)); + await PublishSelectedWeaponProfile(ctx, unitId, templateId, weapon, style); - ctx.Session.Log.Info($"[ACTOR-MIN] presentation unit={unitId} template={templateId} weapon={weapon.TemplateId}/{weapon.InstanceId} style={style.Id} reason={reason} combatModules=false skills=false"); + ctx.Session.Log.Info($"[ACTOR-MIN] presentation unit={unitId} template={templateId} weapon={weapon.TemplateId}/{weapon.InstanceId} style={style.Id} reason={reason} combatModules=true skills=true"); } async Task PublishWeaponSnapshot( @@ -175,11 +176,12 @@ internal sealed partial class GameRouter // Keep the opening/loading barrier on the proven small traversal capability set. // Never activate the entire BuffConfig as one player snapshot: it contains NPC/timeline actions. + var targetUnitId = state.ActiveSpiritUnitId != 0 ? state.ActiveSpiritUnitId : Profile.InitialUnitId; var firstInstanceId = state.NextBuffInstanceId; await ctx.NotifyAsync(MethodId.SyncUnitBuffList, - WorldCodec.UnitBuffList(Profile.InitialUnitId, WebTraversal.InitialCapabilityBuffIds, firstInstanceId)); + WorldCodec.UnitBuffList(targetUnitId, WebTraversal.InitialCapabilityBuffIds, firstInstanceId)); state.NextBuffInstanceId += (uint)WebTraversal.InitialCapabilityBuffIds.Count; - ctx.Session.Log.Info($"[CAPABILITY] initial unit={Profile.InitialUnitId} count={WebTraversal.InitialCapabilityBuffIds.Count} loadingSafe=true allBuffsDeferred=true"); + ctx.Session.Log.Info($"[CAPABILITY] initial unit={targetUnitId} count={WebTraversal.InitialCapabilityBuffIds.Count} loadingSafe=true allBuffsDeferred=true"); } } diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.cs index fc43d8e..36206af 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.cs @@ -98,6 +98,7 @@ internal sealed partial class GameRouter MethodId.GmAddEnemyByPlayer, MethodId.GmTeleportXYZ, MethodId.DavinciCode, + MethodId.GmDaVinciCode, // Time of day: client-driven UI (accept + remember) + debug-panel slider push. MethodId.AskPassingTime, diff --git a/README.md b/README.md index cc800fb..ee23b9b 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,20 @@ | ฟีเจอร์ / ระบบ | สถานะ | รายละเอียดการทำงาน | | :--- | :---: | :--- | +| **Combat & Skill System (ระบบต่อสู้และสกิล)** | 🟢 ใช้งานได้สมบูรณ์ | ผูก Weapon Profile, Basic Attack Combo, Heavy Attack, Active Skill, Dodge, Unique Ultimate Skill และ Fight Resources (SP/Energy) ให้กับตัวละครทุกตัว (ทั้ง MC ชาย/หญิง และตัวละครทั้งหมด) ทันทีเมื่อเข้าเกมหรือสลับตัวละคร พร้อมประมวลผล Skill RPC และ Damage Hit Notification ในเซิร์ฟเวอร์ | | **Authentication & MPay Bypass** | 🟢 ใช้งานได้สมบูรณ์ | บายพาสล็อกอินผ่าน MPay JavaScript Bridge ใน `NtUniSdkBase.dll` อัตโนมัติ ไม่ต้องกรอกรหัสผ่าน | | **HTTPS Gateway & Multi-Domain Proxy** | 🟢 ใช้งานได้สมบูรณ์ | จำลอง SSL SNI บนพอร์ต 443/80 หลอกระบบอัปเดต และบีบ Contract MD5 7-column ไม่ให้ขึ้นปิดปรับปรุง | | **World Entry (V7 4-Step Handoff)** | 🟢 ใช้งานได้สมบูรณ์ | ส่งลำดับ `SyncLogicAgentEnter` -> `SyncManagedLogicAgent` -> `SyncRaidBattleUnitSpirit` -> `SyncPlayerCurrentSpirit` -> `SyncSceneLoadCompleted` เข้าสู่เมืองได้ 100% | -| **Character Switching (สลับตัวละคร)** | 🟢 ใช้งานได้สมบูรณ์ | สลับตัวละครได้ในฉากเดียวกัน พิกัดต่อเนื่อง และส่ง `isAgentSwitch: true` ทำให้มุมกล้อง Cinemachine ติดตามตัวละครใหม่ถูกต้อง ไม่หลุดลอย | -| **Traversal & Parkour (สลิง/ปีนป่าย)** | 🟢 ใช้งานได้สมบูรณ์ | ปลดล็อกบัฟเคลื่อนไหวใยแมงมุม (FeiSuo / Grapple Hook / Wall Rush / Dive Damage) ให้กับทุกตัวละคร | -| **Anti-Cheat Suppression** | 🟢 ใช้งานได้สมบูรณ์ | ดักปิด `DavinciReport` และ `DavinciMgr.CheckTimeScale` พร้อมดูดซับ `DavinciCode` ฝั่งเซิร์ฟเวอร์ หมดปัญหาสลิงเกี่ยวแล้ว Connection Lost | +| **Character Switching (สลับตัวละคร)** | 🟢 ใช้งานได้สมบูรณ์ | สลับตัวละครได้ในฉากเดียวกัน พิกัดต่อเนื่อง และส่ง `isAgentSwitch: true` ทำให้มุมกล้อง Cinemachine ติดตามตัวละครใหม่ถูกต้อง ไม่หลุดลอย พร้อมรีไอน์เชียลไลซ์สกิลของตัวละครใหม่ทันที | +| **Traversal & Parkour (สลิง/เกี่ยวต่อสู้/ปีนป่าย)** | 🟢 ใช้งานได้สมบูรณ์ | ปลดล็อกบัฟเคลื่อนไหวใยแมงมุม (FeiSuo / Grapple Hook / Wall Rush / Dive Damage) ให้กับทุกตัวละคร รองรับการใช้สลิงเกี่ยวต่อสู้โดยไม่หลุดจากการเชื่อมต่อ | +| **Anti-Cheat Suppression** | 🟢 ใช้งานได้สมบูรณ์ | ดักปิด `DavinciReport`, `DavinciMgr.CheckTimeScale` (แก้บัค Connection Lost เวลาสลิงเกี่ยวหรือเกิด Slow Motion) และดูดซับ `GmDaVinciCode` ฝั่งเซิร์ฟเวอร์ | +| **Camera & Cinemachine Handling** | 🟢 ใช้งานได้สมบูรณ์ | จัดการทรานซิชันมุมกล้อง Cinemachine ระหว่างการเคลื่อนไหว สลับตัวละคร และโหมดอิสระ ปิดการ Force Debug Photo ที่เคยกวนระบบกล้องเกมหลัก | | **RAM Warmup OOM Prevention** | 🟢 ใช้งานได้สมบูรณ์ | ปิดการสั่งคอมไพล์ 11,384 PSO Shaders รวดเดียว ลดเวลาโหลดจาก 40 วิเหลือ 0.01 วิ และไม่กินแรม 16-19GB จนเกมแครช | | **Retail Game Switches & All-Map Unlock** | 🟢 ใช้งานได้สมบูรณ์ | ปลดล็อกตู้เสื้อผ้า (Closet), กาชา (Gacha), ห้าง (Mall), โทรศัพท์ (Phone), แผนที่เปิดหมด หมอกควัน (Fog) ถูกเคลียร์ | | **Web Debug Panel (`:5809/debug`)** | 🟢 ใช้งานได้สมบูรณ์ | แดชบอร์ดเว็บสำหรับวาร์ปตามพิกัด (Teleport), ปรับเวลาสภาพอากาศ, เสกมอนเตอร์, และเปิดหน้าต่างวิดีโอ | -| **Safe Fastpatch Loader** | 🟢 ใช้งานได้สมบูรณ์ | ตัวสร้าง `fastpatch_4229938.zip` อัตโนมัติ ทุกคำสั่งถูกครอบด้วย `pcall` หมดปัญหาเกมค้างหน้า Initialize Client | -| **Monster Spawning (เสกมอนเตอร์)** | 🟡 กำลังพัฒนา / มีข้อจำกัด | เสกได้ผ่าน GM Console (`Alt+F1`) และ Debug Panel (`CMD:SPAWN_ENEMY`) แต่ขึ้นอยู่กับว่า Chunk ที่ยืนอยู่มี NavMesh โหลดพร้อมหรือไม่ | -| **Cutscene & Video Player** | 🟡 กำลังพัฒนา / มีข้อจำกัด | คำสั่ง `CMD:PLAY_CUTSCENE` สั่งเปิดหน้าต่าง `S_VIDEO_PLAYER_PANEL` และ Timeline ได้ถูกต้อง แต่ Cutscene ID บางตัวอาจไม่มีไฟล์วิดีโอใน Client Leak | +| **Safe Fastpatch Loader** | 🟢 ใช้งานได้สมบูรณ์ | ตัวสร้าง `fastpatch_4229938.zip` อัตโนมัติ ทุกคำสั่งถูกครอบด้วย `pcall` ป้องกันข้อความคำสั่งรั่วขึ้นหน้าจอ และหมดปัญหาเกมค้างหน้า Initialize Client | +| **Monster Spawning (เสกมอนเตอร์)** | 🟡 กำลังพัฒนา / ใช้งานได้ | เสกได้ผ่าน GM Console (`Alt+F1`) และ Debug Panel (`CMD:SPAWN_ENEMY`) ส่งคำสั่งเข้า `gCS.LuaUtils.AddEnemy` โดยตรงไม่ขึ้นเป็นตัวหนังสือบนจอ แต่การเกิดจะขึ้นอยู่กับ NavMesh ใน Chunk นั้นๆ | +| **Cutscene & Video Player** | 🟡 กำลังพัฒนา / ใช้งานได้ | คำสั่ง `CMD:PLAY_CUTSCENE` สั่งเปิดหน้าต่าง `S_VIDEO_PLAYER_PANEL` และ Timeline ได้ถูกต้อง ไม่ขึ้นเป็นตัวหนังสือบนจอ แต่ Cutscene ID บางตัวอาจไม่มีไฟล์วิดีโอใน Client Leak | | **Taffy Monowheel (塔菲摩托)** | 🟡 กำลังพัฒนา / มีข้อจำกัด | ลงทะเบียน RPC การขับขี่พื้นฐาน (`AskTaffyMotoEnterRush`, `AskGetOffMotor`, `OnTafeiMotorColliding`) ไว้แล้ว แต่ระบบฟิสิกส์การชนยังไม่สมบูรณ์เท่าตัวเอก | | **Gacha System (ตู้สุ่มตัวละคร)** | 🟡 กำลังพัฒนา / มีข้อจำกัด | หน้าร้านค้าเปิดตู้ได้และบายพาสวันหมดอายุ CBT แล้ว แต่ผลการสุ่มยังเป็นการให้ไอเทมแบบสุ่มตายตัว (ยังไม่มี Database เรทกาชาแบบเต็มระบบ) | | **Phone Customization (ตกแต่งมือถือ)** | 🟡 กำลังพัฒนา / มีข้อจำกัด | ปลดล็อกไอเทมเคสมือถือ วอลเปเปอร์ จี้ห้อย (`1..200`) ในหน่วยความจำ แต่ยังไม่บันทึกความเปลี่ยนแปลงข้าม Session แบบถาวร |