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:
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,
|
||||
|
||||
@@ -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 แบบถาวร |
|
||||
|
||||
Reference in new issue
Block a user