fix(combat): hydrate skills on spawn/switch, fix grapple anti-cheat dc, monster spawn & cutscenes, and update README feature matrix

This commit is contained in:
CAtlas0784 committed 2026-09-15 19:47:53 +07:00
1 parent 5d141e617f
commit 5e25d474c3
8 files changed
+158 -77

No files matched your search

Binary file not shown.
@@ -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}`);
}
@@ -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)
@@ -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);
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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,
+8 -6
View File
@@ -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 แบบถาวร |