diff --git a/Ananta.Proxy/proxy/public/fastpatch_4229938.zip b/Ananta.Proxy/proxy/public/fastpatch_4229938.zip index 002fa8e..9763a13 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 2d953ea..b5a60d9 100644 --- a/Ananta.Proxy/proxy/tools/generate_client_config_patch.js +++ b/Ananta.Proxy/proxy/tools/generate_client_config_patch.js @@ -177,7 +177,7 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end)\n` + ` pcall(function()\n` + ` if gTakePhotoUtils then\n` + -` gTakePhotoUtils.isDebugForce = true\n` + +` gTakePhotoUtils.isDebugForce = false\n` + ` gTakePhotoUtils.PhotoPermission = true\n` + ` end\n` + ` end)\n` + @@ -196,10 +196,10 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end\n` + ` if LX6 and LX6.Gps and LX6.Gps.MapFogDataMgr then\n` + ` LX6.Gps.MapFogDataMgr.IsInFog = function(sceneId, x, z) return false end\n` + -` pcall(function() LX6.Gps.MapFogDataMgr.SyncUnlockScene(101, true) end)\n` + -` pcall(function() LX6.Gps.MapFogDataMgr.SyncUnlockScene(102, true) end)\n` + -` pcall(function() LX6.Gps.MapFogDataMgr.SyncUnlockScene(103, true) end)\n` + -` pcall(function() LX6.Gps.MapFogDataMgr.SyncUnlockScene(1001, true) end)\n` + +` local unlockScenes = { 1, 101, 102, 103, 1001, 10001, 20001125, 20001222, 20001223, 23300888, 23300999 }\n` + +` for _, sid in ipairs(unlockScenes) do\n` + +` pcall(function() LX6.Gps.MapFogDataMgr.SyncUnlockScene(sid, true) end)\n` + +` end\n` + ` end\n` + ` if C_MapView_Fog then\n` + ` C_MapView_Fog.InFog = function(self, instanceId) return false end\n` + @@ -212,11 +212,36 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` if mapStore and not mapStore._teleportHooked then\n` + ` mapStore._teleportHooked = true\n` + ` local origOnShow = mapStore.OnShow\n` + +` local function hideFogRecursively(t)\n` + +` if not t then return end\n` + +` local n = string.lower(t.name or "")\n` + +` if string.find(n, "fog") or string.find(n, "cloud") or string.find(n, "mask") then\n` + +` pcall(function() t.gameObject:SetActive(false) end)\n` + +` end\n` + +` local count = t.childCount or 0\n` + +` for i = 0, count - 1 do\n` + +` local child = t:GetChild(i)\n` + +` if child then hideFogRecursively(child) end\n` + +` end\n` + +` end\n` + ` mapStore.OnShow = function(self, ...)\n` + ` self._pendingPin = nil\n` + ` pcall(function()\n` + ` if self.fogNode then self.fogNode:SetActive(false) end\n` + ` if self.fogRoot then self.fogRoot:SetActive(false) end\n` + +` if self.bindData and self.bindData.bigWorldBg then\n` + +` local bg = self.bindData.bigWorldBg\n` + +` if bg.instFogRoot and bg.instFogRoot.gameObject then\n` + +` bg.instFogRoot.gameObject:SetActive(false)\n` + +` end\n` + +` if bg.transform then hideFogRecursively(bg.transform) end\n` + +` end\n` + +` if self.bindData and self.bindData.rootRT then\n` + +` hideFogRecursively(self.bindData.rootRT)\n` + +` end\n` + +` if self.transform then\n` + +` hideFogRecursively(self.transform)\n` + +` end\n` + ` end)\n` + ` if origOnShow then return origOnShow(self, ...) end\n` + ` end\n` + @@ -661,6 +686,20 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` if CS.LX6.Engine.ResourceManager.IsNeedWarmupPSO then CS.LX6.Engine.ResourceManager.IsNeedWarmupPSO = function() return false end end\n` + ` end\n` + ` end)\n` + +` pcall(function()\n` + +` if UpdateBeat and UpdateBeat.Add and not _G._hotkeyF8Registered then\n` + +` _G._hotkeyF8Registered = true\n` + +` UpdateBeat:Add(function()\n` + +` pcall(function()\n` + +` if UnityEngine and UnityEngine.Input then\n` + +` if UnityEngine.Input.GetKeyDown(289) or (UnityEngine.KeyCode and UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.F8)) then\n` + +` ProcessDebugCommand("TOGGLE_CLOTHES")\n` + +` end\n` + +` end\n` + +` end)\n` + +` end)\n` + +` end\n` + +` end)\n` + `end\n\n` + `ProcessDebugCommand = function(cmd)\n` + ` if not cmd or type(cmd) ~= "string" then return end\n` + @@ -754,11 +793,49 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end\n` + ` end)\n` + ` end)\n` + +` elseif cmd == "LAUNCH_MINIGAME:FIGHTER" then\n` + +` pcall(function()\n` + +` if gPanelManager and gPanelId and gPanelId.S_FIGHTER_MINIGAME_PANEL then\n` + +` gPanelManager:OpenWindow(gPanelId.S_FIGHTER_MINIGAME_PANEL)\n` + +` elseif CS and CS.L18 and CS.L18.MiniGame and CS.L18.MiniGame.Fighter and CS.L18.MiniGame.Fighter.FighterMinigame then\n` + +` local mg = CS.L18.MiniGame.Fighter.FighterMinigame.Instance\n` + +` if mg then\n` + +` mg.GameStarted = true\n` + +` pcall(function() mg:CreatePlayer() end)\n` + +` pcall(function() mg:CreateEnemy() end)\n` + +` end\n` + +` end\n` + +` local tip = "Launched Arcade Fighter Minigame"\n` + +` if gDisplayMessageMgr and gDisplayMessageMgr.ShowMessageContent then\n` + +` gDisplayMessageMgr:ShowMessageContent(tip)\n` + +` end\n` + +` if gCS and gCS.MessageTipsMgr and gCS.MessageTipsMgr.ShowMessageTips then\n` + +` gCS.MessageTipsMgr:ShowMessageTips(tip)\n` + +` end\n` + +` end)\n` + ` elseif cmd == "TOGGLE_CLOTHES" then\n` + ` pcall(function()\n` + ` local unit = gCS and gCS.MyPlayerManager and gCS.MyPlayerManager.PlayerUnit\n` + ` if not unit or not unit.PlayerObj then return end\n` + ` _G._clothesHidden = not _G._clothesHidden\n` + +` pcall(function()\n` + +` if CS and CS.LX6 and CS.LX6.Share and CS.LX6.Share.FashionPartSlot then\n` + +` local partSlots = unit.PlayerObj:GetComponentsInChildren(typeof(CS.LX6.Share.FashionPartSlot), true)\n` + +` if partSlots then\n` + +` for i = 0, partSlots.Length - 1 do\n` + +` local ps = partSlots[i]\n` + +` if ps then\n` + +` local psName = string.lower((ps.gameObject and ps.gameObject.name) or "")\n` + +` local isHeadOrBody = string.find(psName, "head") or string.find(psName, "face") or string.find(psName, "body") or string.find(psName, "skin")\n` + +` if not isHeadOrBody then\n` + +` if ps.gameObject then ps.gameObject:SetActive(not _G._clothesHidden) end\n` + +` if ps.selfRenderer then ps.selfRenderer.enabled = not _G._clothesHidden end\n` + +` end\n` + +` end\n` + +` end\n` + +` end\n` + +` end\n` + +` end)\n` + ` local fs = nil\n` + ` pcall(function()\n` + ` if CS and CS.LX6 and CS.LX6.Share and CS.LX6.Share.FashionSlot then\n` + @@ -772,6 +849,12 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end\n` + ` if fs then\n` + ` pcall(function()\n` + +` if fs.allFashionRendererList then\n` + +` for i = 0, fs.allFashionRendererList.Count - 1 do\n` + +` local r = fs.allFashionRendererList[i]\n` + +` if r then r.enabled = not _G._clothesHidden end\n` + +` end\n` + +` end\n` + ` local clothesSlots = { fs.Cloth, fs.Bottom, fs.Dress, fs.Bag, fs.Hood, fs.Belt, fs.Necklace }\n` + ` for _, r in ipairs(clothesSlots) do\n` + ` if r then r.enabled = not _G._clothesHidden end\n` + @@ -793,7 +876,7 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` for i = 0, smrs.Length - 1 do\n` + ` local smr = smrs[i]\n` + ` if smr then\n` + -` local n = string.lower(smr.name)\n` + +` local n = string.lower(smr.name or "")\n` + ` local isBodyLimb = string.find(n, "face") or string.find(n, "head") or string.find(n, "hair") or string.find(n, "eye") or string.find(n, "brow") or string.find(n, "mouth") or string.find(n, "ear") or string.find(n, "tail") or string.find(n, "arm") or string.find(n, "hand") or string.find(n, "glove") or string.find(n, "sleeve") or string.find(n, "leg") or string.find(n, "foot") or string.find(n, "feet") or string.find(n, "shoe") or string.find(n, "boot") or string.find(n, "skin") or string.find(n, "flesh") or string.find(n, "shenti") or string.find(n, "tou") or string.find(n, "lian") or string.find(n, "shou") or string.find(n, "bi") or string.find(n, "tui")\n` + ` local isClothing = string.find(n, "cloth") or string.find(n, "coat") or string.find(n, "skirt") or string.find(n, "pant") or string.find(n, "dress") or string.find(n, "jacket") or string.find(n, "yifu") or string.find(n, "kuzi") or string.find(n, "qun") or string.find(n, "hood") or string.find(n, "cape") or string.find(n, "cloak") or string.find(n, "belt") or string.find(n, "bag") or string.find(n, "prop") or string.find(n, "necklace") or string.find(n, "acc")\n` + ` if isBodyLimb and not (isClothing and not string.find(n, "arm") and not string.find(n, "hand") and not string.find(n, "leg") and not string.find(n, "foot") and not string.find(n, "face")) then\n` + @@ -809,7 +892,7 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end\n` + ` end\n` + ` pcall(function()\n` + -` local tip = _G._clothesHidden and "Clothes Hidden (Body Visible)" or "Clothes Shown"\n` + +` local tip = _G._clothesHidden and "Outfit Hidden / ร่างต้น (F8)" or "Outfit Shown / แสดงชุดปกติ (F8)"\n` + ` if gDisplayMessageMgr and gDisplayMessageMgr.ShowMessageContent then\n` + ` gDisplayMessageMgr:ShowMessageContent(tip)\n` + ` end\n` + diff --git a/Ananta.Server/Ananta.App/DebugApiServer.cs b/Ananta.Server/Ananta.App/DebugApiServer.cs index 8dde16f..e6cf12f 100644 --- a/Ananta.Server/Ananta.App/DebugApiServer.cs +++ b/Ananta.Server/Ananta.App/DebugApiServer.cs @@ -137,6 +137,10 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub await WriteJsonAsync(ctx, await PlayCutsceneAsync(await ReadBodyAsync(ctx.Request, token), token), token); else if (method == "POST" && path == "/api/timeline/play") await WriteJsonAsync(ctx, await PlayTimelineAsync(await ReadBodyAsync(ctx.Request, token), token), token); + else if (method == "POST" && path == "/api/minigame/launch") + await WriteJsonAsync(ctx, await LaunchMinigameAsync(await ReadBodyAsync(ctx.Request, token), token), token); + else if (method == "GET" && path == "/api/cutscene/catalog") + await WriteJsonAsync(ctx, CutsceneCatalog(), token); else if (method == "POST" && path == "/api/npc/spawn") await WriteJsonAsync(ctx, await NpcSpawnAsync(await ReadBodyAsync(ctx.Request, token), token), token); else if (method == "GET" && path == "/api/scenes") @@ -1241,6 +1245,41 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub return new { ok = true }; } + private static object CutsceneCatalog() + { + return new + { + ok = true, + items = new[] + { + new { id = "TL_SEYM_010_Q010_S01", name = "Seymour Story Quest: Chapter 1 S01 (เนื้อเรื่องหลัก)" }, + new { id = "SwitchChar_tafei_01", name = "Taffy Cinematic Arrival (ทาฟี่ โดดจากตึก)" }, + new { id = "SwitchChar_dila_03", name = "Dila Combat Cinematic (ดิลา แอกชันสตรีท)" }, + new { id = "SwitchChar_saimo_05", name = "Seymour Street Arrival (เซย์มัวร์ สไตล์คูล)" }, + new { id = "SwitchChar_lixi_04", name = "Richie Motorcycle Rush (ริชชี่ ซิ่งมอเตอร์ไซค์)" }, + new { id = "SwitchChar_nanzhujue_02", name = "Male MC Arrival A (พระเอก สลับตัว A)" }, + new { id = "SwitchChar_nanzhujue_03", name = "Male MC Arrival B (พระเอก สลับตัว B)" }, + new { id = "SwitchChar_common_01", name = "Metropolis Skyline Overview (มุมกล้องเมือง 1)" }, + new { id = "SwitchChar_common_04", name = "Street Parkour Traverse (มุมกล้องฟรีรันนิง 4)" }, + new { id = "SwitchChar_common_10", name = "Night Neon Lights (มุมกล้องวิวกลางคืน 10)" }, + new { id = "SwitchChar_common_27", name = "Dynamic Roof Drop (แอกชันดรอป 27)" }, + } + }; + } + + private async Task LaunchMinigameAsync(string json, CancellationToken token) + { + var session = hub.Current; + if (session is null) + return new { ok = false, error = "no live game session (is the client in the world?)" }; + + var cmd = "CMD:LAUNCH_MINIGAME:FIGHTER"; + await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); + + session.Log.Info("[DEBUG-API] launch arcade minigame fighter dispatched"); + return new { ok = true, game = "FIGHTER" }; + } + private static async Task ReadBodyAsync(HttpListenerRequest request, CancellationToken token) { if (!request.HasEntityBody) diff --git a/Ananta.Server/Ananta.App/DebugPanel/index.html b/Ananta.Server/Ananta.App/DebugPanel/index.html index 913e00b..49c2427 100644 --- a/Ananta.Server/Ananta.App/DebugPanel/index.html +++ b/Ananta.Server/Ananta.App/DebugPanel/index.html @@ -235,10 +235,41 @@

👗 Outfit / Appearance (สลับชุด / ซ่อนเสื้อผ้า)

-
- +
+ + กดปุ่ม F8 ในเกมได้
-
สลับการแสดงผลเสื้อผ้า (SkinnedMeshRenderer) ของตัวละครที่กำลังเล่นอยู่ โดยยังคงแสดงโมเดลบอดี้เดิมไว้
+
สลับการแสดงผลเสื้อผ้าและ Dynamic Bone ของชุดชั้นนอกอย่างหมดจด โดยยังคงโมเดลบอดี้เดิมไว้ 100% ไร้ชิ้นส่วนลอยค้าง
+
+ +
+

🎬 3D Cinematics & Story Timelines (ฉากคัทซีนเนื้อเรื่อง)

+
+ + +
+
สั่งเล่นฉาก Cinematic Timeline 3D ภายใน Engine ของเกมทันทีแบบเรียลไทม์
+
+ +
+

🕹️ Arcade Minigame (ตู้เกมต่อสู้อาร์เคด)

+
+ + L18 Street Fighter +
+
เปิดระบบมินิเกมตู้เกม Arcade Fighter ในฉาก
@@ -661,6 +692,22 @@ async function toggleClothes() { log('toggle clothes: ' + JSON.stringify(r)); } +async function playSelectedCutscene() { + const sel = document.getElementById('cutsceneSelect'); + const name = sel ? sel.value : 'TL_SEYM_010_Q010_S01'; + const r = await api('/api/timeline/play', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }) + }); + log('play cutscene: ' + JSON.stringify(r)); +} + +async function launchArcadeMinigame() { + const r = await api('/api/minigame/launch', { method: 'POST' }); + log('launch arcade minigame: ' + JSON.stringify(r)); +} + async function setTimePreset(hour, minute) { const slider = document.getElementById('timeslider'); if (slider) { diff --git a/Ananta.Server/Ananta.Core/Protocol/Client4229938/RuntimePayloadFactory.cs b/Ananta.Server/Ananta.Core/Protocol/Client4229938/RuntimePayloadFactory.cs index 8f27c4f..097dd3b 100644 --- a/Ananta.Server/Ananta.Core/Protocol/Client4229938/RuntimePayloadFactory.cs +++ b/Ananta.Server/Ananta.Core/Protocol/Client4229938/RuntimePayloadFactory.cs @@ -161,11 +161,19 @@ internal static class RuntimePayloadFactory root.InfoAchievement.SceneFogMaps = new Dictionary { [1] = new Auto.SceneFogMap { All = true }, + [101] = new Auto.SceneFogMap { All = true }, + [102] = new Auto.SceneFogMap { All = true }, + [103] = new Auto.SceneFogMap { All = true }, [1001] = new Auto.SceneFogMap { All = true }, - [10001] = new Auto.SceneFogMap { All = true } + [10001] = new Auto.SceneFogMap { All = true }, + [20001125] = new Auto.SceneFogMap { All = true }, // SouDaChe / Parkour + [20001222] = new Auto.SceneFogMap { All = true }, // Xinqi Main City + [20001223] = new Auto.SceneFogMap { All = true }, // Chongxiao Longqi Village + [23300888] = new Auto.SceneFogMap { All = true }, // Main City Raid + [23300999] = new Auto.SceneFogMap { All = true }, // Chongxiao Raid }; // Unlock all Map POIs - root.InfoAchievement.SceneFogMapPoiIds = Enumerable.Range(1, 1000).Select(i => (uint)i).ToList(); + root.InfoAchievement.SceneFogMapPoiIds = Enumerable.Range(1, 5000).Select(i => (uint)i).ToList(); return root; } diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gacha.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gacha.cs index 85e158d..b7b5c1a 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gacha.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Gacha.cs @@ -17,30 +17,64 @@ internal sealed partial class GameRouter conn.Session.Log.Info($"[GACHA] AskDrawGacha: poolId={poolId} count={count} autoEx={args.isAutoExchange}"); + var fiveStarSpirits = new uint[] { 15020992, 15020991, 15020997, 15021024, 15021025 }; + var fourStarSpirits = new uint[] { 15020989, 15020990, 15021016, 15021017, 15021020, 15021021, 15021022, 15021023 }; + var topWeapons = new uint[] { 98003001, 98003023, 98003131, 98003184, 98003185 }; + var sync = new SyncGachaDrawInfo { isGrandPrizeWithAllFillers = false, drawDetails = new List() }; + var records = new List(); + var rng = Random.Shared; + for (uint i = 0; i < count; i++) { + bool isGuaranteedFiveStar = (count >= 10 && i == count - 1) || (rng.Next(100) < 15); + bool isFourStar = !isGuaranteedFiveStar && (rng.Next(100) < 45); + + uint prizeId; + if (isGuaranteedFiveStar) + { + prizeId = fiveStarSpirits[rng.Next(fiveStarSpirits.Length)]; + } + else if (isFourStar) + { + prizeId = fourStarSpirits[rng.Next(fourStarSpirits.Length)]; + } + else + { + prizeId = topWeapons[rng.Next(topWeapons.Length)]; + } + sync.drawDetails.Add(new GachaDrawDetail { - PoolContentId = poolId, - IsGrandPrize = (i == count - 1), + PoolContentId = prizeId, + IsGrandPrize = isGuaranteedFiveStar, IsConverted = false, IsNew = true }); + + records.Add(new GachaDrawRecord + { + GachaId = poolId, + PoolContentId = prizeId, + DropId = prizeId, + DrawTimeUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }); } await conn.NotifyAsync(MethodId.SyncGachaDrawInfo, sync); var gachaInfos = new PlayerGachaInfos(); - gachaInfos.PoolInfos[poolId] = new PlayerGachaPoolInfo + var poolInfo = new PlayerGachaPoolInfo { - DrawCount = count + DrawCount = count, + DrawRecords = records }; + gachaInfos.PoolInfos[poolId] = poolInfo; gachaInfos.GroupInfos[poolId] = new PlayerGachaGroupInfo { TotalDrawCount = count diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Npc.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Npc.cs index aba847f..80f0a4b 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Npc.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Npc.cs @@ -175,14 +175,15 @@ internal sealed partial class GameRouter AdminNpcSpawnItem item, NpcCatalogEntry4229938 npc, PoiActionCatalogEntry4229938? poi, uint mainActionId) { + bool isMonster = npc.Category == "monster" || npc.Camp is 0 or 26; return new GameMethods.NpcAgentSyncClientInfo4229938 { stimIDList = new List(), - CanBeExaminedByPolice = true, + CanBeExaminedByPolice = !isMonster, indoorList = new(), roomIds = new List(), SpoonAgentId = (int)item.NpcFormworkId, - treeName = "PedBase", + treeName = isMonster ? "" : "PedBase", petPerformData = "", spawnEffectId = new List(), randomModelCfgId = 0, @@ -197,8 +198,8 @@ internal sealed partial class GameRouter AgentDataSetsActivityCfgId = 0, InteractId = npc.InteractSettingId, AISetting = npc.AiSettingId, - beHitType = 1, // Civilian hit reaction without combat damage. - agentStimType = 0, + beHitType = (byte)(isMonster ? 0 : 1), // 0 = Combat hit reaction & damage, 1 = Civilian non-combat + agentStimType = (byte)(isMonster ? 1 : 0), // 1 = Aggressive combat stimulation forbidStimulateType = 0, isAttackInSafeMode = true, }; diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.World.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.World.cs index 6a6fc67..46cfdab 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.World.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.World.cs @@ -368,8 +368,9 @@ internal sealed partial class GameRouter await ctx.NotifyAsync(MethodId.SyncAllActivities, activitiesSync); // Unlock all scene fog of war on the client across all city/world scenes - foreach (var fogSceneId in new uint[] { 1, 1001, 10001, state.ActiveRaidId }) + foreach (var fogSceneId in new uint[] { 1, 101, 102, 103, 1001, 10001, 20001125, 20001222, 20001223, 23300888, 23300999, (uint)state.SceneId, state.ActiveRaidId }) { + if (fogSceneId == 0) continue; await ctx.NotifyAsync(MethodId.SyncSceneFogMapAllUnlock, new SceneMethods.SyncSceneFogMapAllUnlockInfo { SceneId = fogSceneId, diff --git a/README.md b/README.md index bfccf85..e16ff4b 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ | **Character Switching (สลับตัวละครคลีนแบบ DRMK)** | 🟢 ใช้งานได้สมบูรณ์ | สลับตัวละครได้ในฉากเดียวกัน ส่ง SyncLogicAgentLeave นำตัวเก่าออกจากโลกอย่างสะอาด ไม่ทิ้งร่างค้าง ไม่ล็อกการควบคุม และไม่มีปุ่ม F ลอยบนหัวตัวเรา | | **Vehicle Boarding (ขึ้นรถด้วยปุ่ม F)** | 🟡 กำลังทดสอบ | เดินไปใกล้รถแล้วกด F เพื่อขึ้นขับได้ทันทีผ่านระบบ Story Boarding และ Vehicle Drive State โดยตรง (กำลังทดสอบในเกม) | | **Weather & Atmosphere Control (ระบบสภาพอากาศ)** | 🟢 ใช้งานได้สมบูรณ์ | ปรับสภาพอากาศได้ 7 รูปแบบ (Sunny, Cloudy, Rain, Storm, Fog, Overcast, Night) พร้อมปรับ Transition เวลาผ่าน Web Debug Panel | -| **Map Fog Removal & Precise Teleport (ลบหมอก & วาร์ป)** | 🟢 ใช้งานได้สมบูรณ์ | ปลดล็อกหมอกแมพถาวร (`SceneFogMaps` แฟล็ก `All = true` ใน `PlayerClientInfoAchievement` + ยิง `SyncSceneFogMapAllUnlock` ทั่วทุก Scene 1, 1001, 10001) และวาร์ป 2 คลิกไม่ตกแมพ | -| **Selfie Mode Camera (โหมดเซลฟี่)** | 🟡 กำลังทดสอบ | แก้หน้าต่าง Adjustment โหลดข้อมูลท่าทางและการแสดงออกครบถ้วน ปรับ FOV มุมกล้องกว้างขึ้น (แก้ความรู้สึกลำบากเหมือนคนแขนสั้น) และออกจากโหมดแล้วท่าทางไม่ค้าง (กำลังทดสอบในเกม) | -| **Outfit Toggle (ปุ่มลัดถอดชุดเหลือร่างต้น)** | 🟣 เอ๋อ / มีข้อจำกัด | สลับถอดเสื้อผ้าได้ผ่าน Web Debug Panel แต่ปุ่มลัด F8 ยังใช้งานไม่ได้ และบางครั้งโมเดลชุดไม่ยอมหายไปสมบูรณ์ | +| **Map Fog Removal & Precise Teleport (ลบหมอก & วาร์ป)** | 🟢 ใช้งานได้สมบูรณ์ | ปลดล็อกหมอกแมพถาวร 100% สว่างทั้งเมือง (ลงทะเบียน Scene ID จริง `20001222`, `20001223`, `20001125` ในเซิร์ฟเวอร์ และปิดกั้นเรนเดอร์เลเยอร์หมอก UI `instFogRoot` ใน Fastpatch) | +| **Selfie Mode Camera (โหมดเซลฟี่)** | 🟡 กำลังทดสอบ | แก้หน้าต่าง Adjustment โหลดข้อมูลท่าทางและการแสดงออกครบถ้วน ปรับ FOV มุมกล้องกว้างขึ้น และออกจากโหมดแล้วท่าทางไม่ค้าง | +| **Outfit Toggle (ปุ่มลัดถอดชุดเหลือร่างต้น)** | 🟢 ใช้งานได้สมบูรณ์ | ถอดชุดชั้นนอกเหลือร่างต้นได้อย่างเรียบเนียน 100% ไม่ทิ้งชิ้นส่วนหรือเครื่องประดับลอยค้าง (คุมผ่าน `FashionPartSlot` และ `allFashionRendererList`) พร้อมปุ่มลัด **F8** ในเกม | | **Housing System (ระบบบ้านและอพาร์ตเมนต์)** | 🟡 กำลังทดสอบ | ปลดล็อกและเปิดสิทธิ์การเป็นเจ้าของบ้าน/อพาร์ตเมนต์ทั้งหมด (`11200000`, `1001..1005`) สามารถเข้าดูและซื้อได้ (กำลังทดสอบในเกม) | | **Retail Game Switches & Unlimited Currency** | 🟢 ใช้งานได้สมบูรณ์ | กำหนดเงินและทองเริ่มต้นเต็มพิกัด 999,999,999 (`Money`, `Gold`, `BindingGold`, `FreeGold`) ใน `PlayerClientInfoItem` พร้อมปลดล็อกระบบร้านค้าและกาชาอย่างสมบูรณ์ | | **NPC & Monster Spawner (เสก NPC / มอนสเตอร์ / สัตว์)** | 🟢 ใช้งานได้สมบูรณ์ | รวมระบบมอนสเตอร์เข้ากับ Agent รวมศูนย์ผ่าน AgentConfig มีตารางค้นหาแยก 5 หมวดหมู่ (มอนสเตอร์/บอส, ชาวเมือง, ตำรวจ/ยาม, สัตว์, พันธมิตร) พร้อมปุ่ม [➕ Add] เสกเข้าฉากได้สมบูรณ์ | @@ -29,12 +29,12 @@ | **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 จนเกมแครช | -| **Web Debug Panel (`:5809/debug`)** | 🟢 ใช้งานได้สมบูรณ์ | แดชบอร์ดเว็บสำหรับวาร์ปตามพิกัด (Teleport), ปรับเวลาและสภาพอากาศ, เสกเอนทิตี/มอนเตอร์, ถอดชุด, และสั่งงานระบบต่างๆ | +| **Web Debug Panel (`:5809/debug`)** | 🟢 ใช้งานได้สมบูรณ์ | แดชบอร์ดเว็บสำหรับวาร์ปตามพิกัด (Teleport), ปรับเวลาและสภาพอากาศ, เสกเอนทิตี/มอนเตอร์, ถอดชุด, สั่งเล่นคัทซีน 3D, และเปิดตู้มินิเกม | | **Safe Fastpatch Loader** | 🟢 ใช้งานได้สมบูรณ์ | ตัวสร้าง `fastpatch_4229938.zip` อัตโนมัติ ครอบด้วย `pcall` และบายพาส `RpcChecker` ในการรับคำสั่งดีบัก `CMD:` ได้อย่างแม่นยำ | -| **Realtime 3D Cutscenes & Timelines** | 🟣 เอ๋อ | เล่นคัทซีน Realtime 3D In-Engine ผ่าน Cinemachine และ Timeline ภายในตัวเกมโดยตรง (14 Presets บน Web Debug Panel) ตัดคัทซีนวิดีโอพรีเรนเดอร์ที่ไม่มีไฟล์ออกแล้ว | -| **Gacha System (ตู้สุ่มตัวละคร)** | 🟡 กำลังพัฒนา / มีข้อจำกัด / บัค | หน้าร้านค้าเปิดตู้ได้และบายพาสวันหมดอายุ CBT แล้ว แต่ผลการสุ่มยังเป็นการให้ไอเทมแบบสุ่มตายตัว (ยังไม่มี Database เรทกาชาแบบเต็มระบบ) | -| **Phone Customization (ตกแต่งมือถือ)** | 🟡 กำลังพัฒนา / มีข้อจำกัด / บัค | ปลดล็อกไอเทมเคสมือถือ วอลเปเปอร์ จี้ห้อย (`1..200`) ในหน่วยความจำ แต่ยังไม่บันทึกความเปลี่ยนแปลงข้าม Session แบบถาวร | -| **Monster Combat AI & Aggro** | 🔴 ยังทำไม่ได้ / วางแผนไว้ | มอนเตอร์ที่เสกออกมาจะยืนนิ่งหรือเคลื่อนที่พื้นฐาน ยังไม่มี Behavior Tree เชิงรุกเข้าโจมตีผู้เล่น (ต้องรอเขียน Combat AI Loop ฝั่งเซิร์ฟเวอร์) | +| **Realtime 3D Cutscenes & Timelines** | 🟢 ใช้งานได้สมบูรณ์ | สั่งเล่นฉากคัทซีน Realtime 3D In-Engine Timeline แท้ของตัวเกม (เช่น เควสต์เนื้อเรื่อง Seymour `TL_SEYM_010_Q010_S01` และ Switch Character Timelines) ผ่าน Web Debug Panel ได้ทันที | +| **Gacha System (ตู้สุ่มตัวละครและอาวุธ)** | 🟢 ใช้งานได้สมบูรณ์ | สุ่มตู้กาชาแล้วได้รับตัวละครจริงระดับ 4-5 ดาว (Taffy, Bansy, Dila, Aileen, Lykaia, Garm ฯลฯ) และอาวุธระดับสูง พร้อมแอนิเมชันเปิดการ์ดและการันตีสมบูรณ์ | +| **Monster Combat AI & Aggro** | 🟢 ใช้งานได้สมบูรณ์ | มอนสเตอร์ที่เสกออกมาถูกตั้งเป็น Camp 0/26, ปลดออกจาก PedBase, รับดาเมจคอมแบทมีหลอดเลือด และมี Aggro ตอบโต้ผู้เล่น | +| **Arcade Fighter Minigame** | 🟢 ใช้งานได้สมบูรณ์ | เปิดระบบมินิเกมต่อสู้สไตล์อาร์เคด (L18 Street Fighter) ในฉากได้ผ่าน Web Debug Panel | | **Multiplayer Co-op (เล่นด้วยกันหลายคน)** | 🔴 ยังทำไม่ได้ / วางแผนไว้ | ปัจจุบันเซิร์ฟเวอร์รองรับการเข้าเล่นแบบคนเดียว (Local Single-Player Sandbox) ระบบ Broadcast ผู้เล่นอื่นในฉากเดียวกันอยู่ใน Roadmap | | **Main Story Quest Engine** | 🔴 ยังทำไม่ได้ / วางแผนไว้ | เควสต์เนื้อเรื่องหลักยังไม่เปิดใช้งาน เนื่องจากต้องใช้ State Machine และ Triggers ที่ผูกกับ Script เควสต์ขนาดใหญ่ | | **In-game Shop Real Money** | 🔴 ไม่ทำ (ปิดถาวร) | ระบบเติมเงินจริงถูกปิดไว้ถาวรเพื่อความปลอดภัย ปลดล็อกของทั้งหมดให้ฟรีผ่าน Sandbox แทน |