feat(cutscene): expand 60+ cutscene catalog and fix in-engine timeline playback via native SwitchSpiritConfigId and Lua Fastpatch
This commit is contained in:
1 parent
75a1d91df0
commit
fb429bd6db
4 files changed
+225
-44
No files matched your search
Binary file not shown.
@@ -719,14 +719,48 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") +
|
||||
` elseif string.sub(cmd, 1, 14) == "PLAY_TIMELINE:" then\n` +
|
||||
` local arg = string.sub(cmd, 15)\n` +
|
||||
` pcall(function()\n` +
|
||||
` if gTimelineManager and gTimelineManager.Timeline_LoadAndPlay then\n` +
|
||||
` gTimelineManager:Timeline_LoadAndPlay(arg, nil)\n` +
|
||||
` elseif gTimelineManager and gTimelineManager.PlayTimeline then\n` +
|
||||
` gTimelineManager:PlayTimeline(arg)\n` +
|
||||
` elseif gDramaManager and gDramaManager.PlayTimeline then\n` +
|
||||
` gDramaManager:PlayTimeline(arg)\n` +
|
||||
` elseif gCS and gCS.LuaUtils and gCS.LuaUtils.PlayTimeline then\n` +
|
||||
` gCS.LuaUtils.PlayTimeline(arg)\n` +
|
||||
` local unit = gCS and gCS.MyPlayerManager and gCS.MyPlayerManager.PlayerUnit\n` +
|
||||
` local myPos = (unit and unit.Position) or (UnityEngine and UnityEngine.Vector3 and UnityEngine.Vector3.zero)\n` +
|
||||
` local myRot = (unit and unit.Facing and UnityEngine and UnityEngine.Vector3 and UnityEngine.Vector3(0, unit.Facing, 0)) or (UnityEngine and UnityEngine.Vector3 and UnityEngine.Vector3.zero)\n` +
|
||||
` local numId = tonumber(arg)\n` +
|
||||
` if numId and CS and CS.LX6 and CS.LX6.GUI and CS.LX6.GUI.SwitchTeleportManager and CS.LX6.GUI.SwitchTeleportManager.Instance then\n` +
|
||||
` pcall(function() CS.LX6.GUI.SwitchTeleportManager.Instance:OnSyncSwitchSpiritConfigId(numId) end)\n` +
|
||||
` end\n` +
|
||||
` local played = false\n` +
|
||||
` if gTimelineManager then\n` +
|
||||
` local tlData = nil\n` +
|
||||
` if gTimelineManager.Timeline_CreateTimelineData then\n` +
|
||||
` tlData = gTimelineManager:Timeline_CreateTimelineData()\n` +
|
||||
` if tlData and myPos then\n` +
|
||||
` tlData.pos = myPos\n` +
|
||||
` tlData.rot = myRot\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` if gTimelineManager.Timeline_LoadAndPlay then\n` +
|
||||
` pcall(function() gTimelineManager:Timeline_LoadAndPlay(arg, tlData) played = true end)\n` +
|
||||
` end\n` +
|
||||
` if not played and gTimelineManager.PlayTimeline then\n` +
|
||||
` pcall(function() gTimelineManager:PlayTimeline(arg) played = true end)\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` if not played and CS and CS.LX6 and CS.LX6.TimelineScript and CS.LX6.TimelineScript.CutsceneManager and CS.LX6.TimelineScript.CutsceneManager.Instance then\n` +
|
||||
` local cm = CS.LX6.TimelineScript.CutsceneManager.Instance\n` +
|
||||
` local tlData = cm:CreateTimelineData()\n` +
|
||||
` if tlData and myPos then\n` +
|
||||
` tlData.pos = myPos\n` +
|
||||
` tlData.rot = myRot\n` +
|
||||
` end\n` +
|
||||
` pcall(function() cm:LoadAndPlay(arg, tlData) played = true end)\n` +
|
||||
` end\n` +
|
||||
` if not played and gCS and gCS.LuaUtils and gCS.LuaUtils.PlayTimeline then\n` +
|
||||
` pcall(function() gCS.LuaUtils.PlayTimeline(arg) end)\n` +
|
||||
` end\n` +
|
||||
` local tip = "Playing Timeline: " .. tostring(arg)\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 string.sub(cmd, 1, 14) == "PLAY_CUTSCENE:" then\n` +
|
||||
@@ -735,10 +769,8 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") +
|
||||
` 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` +
|
||||
` else\n` +
|
||||
` ProcessDebugCommand("PLAY_TIMELINE:" .. tostring(arg))\n` +
|
||||
` end\n` +
|
||||
` end)\n` +
|
||||
` elseif string.sub(cmd, 1, 11) == "SPAWN_ENEMY" then\n` +
|
||||
@@ -964,23 +996,35 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") +
|
||||
`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` +
|
||||
` local newSync = 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` +
|
||||
` local function makeCmdWrapper(origFunc)\n` +
|
||||
` return function(...)\n` +
|
||||
` local allArgs = {...}\n` +
|
||||
` for i = 1, #allArgs do\n` +
|
||||
` local arg = allArgs[i]\n` +
|
||||
` if type(arg) == "string" and string.sub(arg, 1, 4) == "CMD:" then\n` +
|
||||
` ProcessDebugCommand(string.sub(arg, 5))\n` +
|
||||
` return\n` +
|
||||
` elseif type(arg) == "table" then\n` +
|
||||
` for _, val in pairs(arg) do\n` +
|
||||
` if type(val) == "string" and string.sub(val, 1, 4) == "CMD:" then\n` +
|
||||
` ProcessDebugCommand(string.sub(val, 5))\n` +
|
||||
` return\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` if origFunc then return origFunc(...) end\n` +
|
||||
` end\n` +
|
||||
` if oldSync then return oldSync(...) end\n` +
|
||||
` end\n` +
|
||||
` if tbl._meta and type(tbl._meta) == "table" then\n` +
|
||||
` tbl._meta.SyncNotice = newSync\n` +
|
||||
` if tbl.SyncNotice then\n` +
|
||||
` tbl.SyncNotice = makeCmdWrapper(tbl.SyncNotice)\n` +
|
||||
` end\n` +
|
||||
` if tbl.SyncShowMessage then\n` +
|
||||
` tbl.SyncShowMessage = makeCmdWrapper(tbl.SyncShowMessage)\n` +
|
||||
` end\n` +
|
||||
` if tbl.SyncShowTipMessage then\n` +
|
||||
` tbl.SyncShowTipMessage = makeCmdWrapper(tbl.SyncShowTipMessage)\n` +
|
||||
` end\n` +
|
||||
` tbl.SyncNotice = nil\n` +
|
||||
` tbl.SyncNotice = newSync\n` +
|
||||
` rawset(tbl, "SyncNotice", newSync)\n` +
|
||||
`end\n\n` +
|
||||
`local function HookDisplayMessageMgr()\n` +
|
||||
` if gDisplayMessageMgr and not gDisplayMessageMgr._cmdHooked then\n` +
|
||||
@@ -1000,9 +1044,33 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") +
|
||||
` if package and package.loaded then\n` +
|
||||
` HookNoticeTable(package.loaded["LX6/Service/MasterToClientImpl"])\n` +
|
||||
` HookNoticeTable(package.loaded["LX6/Service/GameToClientImpl"])\n` +
|
||||
` HookNoticeTable(package.loaded["LX6/Service/GameSceneToClientImpl"])\n` +
|
||||
` local autoLoaded = package.loaded["LuaGen/AutoGen/RPCDeserializeAuto"]\n` +
|
||||
` if autoLoaded and autoLoaded.midToReader then\n` +
|
||||
` autoLoaded.midToReader[45418774] = function(reader) return reader:ReadString() end\n` +
|
||||
` autoLoaded.midToName[45418774] = "SyncNotice"\n` +
|
||||
` end\n` +
|
||||
` local baseLoaded = package.loaded["LX6/Service/RPCDeserializeBase"]\n` +
|
||||
` if baseLoaded and not baseLoaded._cmdHooked then\n` +
|
||||
` baseLoaded._cmdHooked = true\n` +
|
||||
` local oldDisp = baseLoaded.Dispatcher\n` +
|
||||
` baseLoaded.Dispatcher = function(ctx, br, mid)\n` +
|
||||
` if mid == 45418774 then\n` +
|
||||
` pcall(function()\n` +
|
||||
` local str = br:ReadString()\n` +
|
||||
` if type(str) == "string" and string.sub(str, 1, 4) == "CMD:" then\n` +
|
||||
` ProcessDebugCommand(string.sub(str, 5))\n` +
|
||||
` end\n` +
|
||||
` end)\n` +
|
||||
` return true\n` +
|
||||
` end\n` +
|
||||
` return oldDisp(ctx, br, mid)\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` end\n` +
|
||||
` if MasterToClientImpl then HookNoticeTable(MasterToClientImpl) end\n` +
|
||||
` if GameToClientImpl then HookNoticeTable(GameToClientImpl) end\n` +
|
||||
` if GameSceneToClientImpl then HookNoticeTable(GameSceneToClientImpl) end\n` +
|
||||
` HookDisplayMessageMgr()\n` +
|
||||
` end)\n` +
|
||||
`end\n\n` +
|
||||
@@ -1010,7 +1078,7 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") +
|
||||
`require = function(mod)\n` +
|
||||
` local res = oldRequire(mod)\n` +
|
||||
` pcall(ApplyAllGlobalHooks)\n` +
|
||||
` if mod == "LX6/Service/MasterToClientImpl" or mod == "LX6/Service/GameToClientImpl" then\n` +
|
||||
` if mod == "LX6/Service/MasterToClientImpl" or mod == "LX6/Service/GameToClientImpl" or mod == "LX6/Service/GameSceneToClientImpl" then\n` +
|
||||
` if type(res) == "table" then HookNoticeTable(res) end\n` +
|
||||
` end\n` +
|
||||
` pcall(HookAllNotices)\n` +
|
||||
|
||||
@@ -1206,30 +1206,119 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
|
||||
return new { ok = true, id = cutsceneId };
|
||||
}
|
||||
|
||||
private sealed record TimelineItem(string id, uint configId, string name, string category, float x = 0, float y = 0, float z = 0);
|
||||
|
||||
private static readonly TimelineItem[] AllTimelines =
|
||||
[
|
||||
// Story & Quests
|
||||
new("TL_SEYM_010_Q010_S01", 2030u, "Seymour Story Quest: Chapter 1 S01 (黑客与发布会 - แฮกเกอร์กับงานแถลงข่าว)", "story", 3418.28f, 295.23f, 1415.67f),
|
||||
new("TL_TAF_010_Q010_S04", 0u, "Taffy Story Quest: Chapter 1 S04 (เควสต์เนื้อเรื่องทาฟี่)", "story"),
|
||||
new("TL_HEISTJ", 0u, "Bank Heist Cinematics Part J (ฉากภารกิจปล้นธนาคาร J)", "story"),
|
||||
new("TL_HEISTZ", 0u, "Bank Heist Cinematics Part Z (ฉากภารกิจปล้นธนาคาร Z)", "story"),
|
||||
new("TL_TGS_MQ_S02", 0u, "Tokyo Game Show Main Quest S02 (ฉากเปิดตัวงาน TGS)", "story"),
|
||||
new("TL_SQ_020_Q005_S02", 0u, "Side Quest Special Cinematic (เควสต์ย่อย 020 S02)", "story"),
|
||||
new("TL_Bernard", 0u, "Bernard Character Cinematic (คัทซีนเบอร์นาร์ด)", "story"),
|
||||
new("TL_Erin_mm_hunhun", 0u, "Erin vs Thugs Street Scene (เอรินปะทะกลุ่มอันธพาล)", "story"),
|
||||
new("xsb_phase3_part3", 0u, "Boss Encounter Phase 3 Part 3 (คัทซีนบอสเฟส 3)", "story"),
|
||||
|
||||
// Character Entrances & Customs
|
||||
new("SwitchChar_tafei_01", 48u, "Taffy Skyscraper Apartment (ทาฟี่ โดดจากตึกอพาร์ตเมนต์)", "character", 1613.81f, 21.96f, 1565.65f),
|
||||
new("SwitchChar_dila_03", 62u, "Dila Combat Rooftop Action (ดิลา แอ็กชันคอมแบทบนดาดฟ้า)", "character", 484.28f, 15.03f, 2018.23f),
|
||||
new("SwitchChar_saimo_05", 100u, "Seymour Skyline Overview (เซย์มัวร์ ชมวิวตึกระฟ้าสไตล์คูล)", "character", 2198.68f, 92.08f, 1807.14f),
|
||||
new("SwitchChar_lixi_04", 59u, "Richie Motorcycle Battle (ริชชี่ ซิ่งมอเตอร์ไซค์สู้แก๊งสเตอร์)", "character", 1647.65f, 0f, 1578.67f),
|
||||
new("SwitchChar_nanzhujue_02", 61u, "Male MC Sports Car Arrival (พระเอก นั่งสปอร์ตคาร์มาส่ง)", "character", 3142.67f, 0f, 2522.78f),
|
||||
new("SwitchChar_nanzhujue_03", 115u, "Male MC Rolls-Royce Arrival (พระเอก ก้าวลงจาก Rolls-Royce)", "character", 2468.71f, -0.1f, 1371.11f),
|
||||
new("SwitchChar_character_2", 82u, "Taffy Park Bench Nap (ทาฟี่ นอนหลับบนม้านั่งสวนสาธารณะ)", "character", 403.61f, 0f, 2117.54f),
|
||||
new("SwitchChar_character_3", 83u, "Aileen at Maid Cafe (ไอลีน สนทนากับเมดในคาเฟ่)", "character", 2086.22f, -0.11f, 2362.25f),
|
||||
new("SwitchChar_character_4", 84u, "Bansy Street Graffiti (แบนซี พ่นสเปรย์กราฟิตี้ข้างถนน)", "character", 522.15f, 0f, 2083.76f),
|
||||
new("SwitchChar_character_6", 85u, "Dila Scrolling Phone on Bench (ดิลา นั่งเล่นโทรศัพท์มือถือ)", "character", 395.99f, 0f, 2111.99f),
|
||||
new("SwitchChar_character_7", 86u, "Enomi Drinking Milk (เอโนมิ ยืนดื่มนมกล่อง)", "character", 1606.63f, -0.1f, 1222.73f),
|
||||
new("SwitchChar_character_13", 89u, "Garm Baseball Batting (การ์ม ซ้อมหวดลูกเบสบอลสุดเท่)", "character", 2862.36f, -0.67f, 2761.5f),
|
||||
new("SwitchChar_character_14", 94u, "Richie Catching Criminal (ริชชี่ บุกจับคนร้ายคาหนังคาเขา)", "character", 3913.1f, -0.14f, 626.1f),
|
||||
new("SwitchChar_character_16", 90u, "MC Receiving Street Flyer (พระเอก รับใบปลิวจากคนแจก)", "character", 2597.6f, 0.14f, 1156.3f),
|
||||
new("SwitchChar_character_22", 113u, "Lykaia Peeling Apple in Mansion (ไลคาเอีย ปอกแอปเปิ้ลในคฤหาสน์หรู)", "character", 3204.52f, 104.46f, 959.45f),
|
||||
new("SwitchChar_character_23", 114u, "Baijing High-Speed Overtake (ไป๋จิ้ง ซิ่งรถแซงโค้งความเร็วสูง)", "character", 3008.4f, -0.1f, 1614.91f),
|
||||
new("SwitchChar_character_19", 302u, "Richie Official PV Cinematic (ริชชี่ ช็อตไฮไลท์จากเทรลเลอร์ PV)", "character", 3008.4f, -0.1f, 1614.91f),
|
||||
new("SwitchChar_character_20", 303u, "Continuous One-Shot Camera (มุมกล้อง Long-Take ช็อตเดียวจบ)", "character", 3008.4f, -0.1f, 1614.91f),
|
||||
new("SwitchChar_common_01", 1u, "Cherry Blossom Ave Passenger Drop (ไลคาเอีย ขับรถมาส่งที่ถนนซากุระ)", "character", 3008.4f, -0.1f, 1614.91f),
|
||||
new("SwitchChar_common_07", 44u, "Taffy Driving Sports Car (ทาฟี่ ขับรถซิ่งบนถนนใหญ่)", "character", 3008.4f, -0.1f, 1614.91f),
|
||||
new("SwitchChar_common_09", 49u, "Exiting City Taxi (เปิดประตูก้าวลงจากรถแท็กซี่)", "character", 3008.3f, 0f, 1616.5f),
|
||||
new("SwitchChar_common_27", 87u, "Street Basketball Game (ดังก์บาสเกตบอลสตรีท)", "character", 410.72f, 0f, 2108.59f),
|
||||
|
||||
// Urban Life & Interactive
|
||||
new("SwitchChar_common_04", 4u, "Exiting Mahjong Parlor (เดินออกจากร้านไพ่นกกระจอก)", "urban", 1031.7f, 0f, 1888.7f),
|
||||
new("SwitchChar_common_21", 63u, "Exiting Store & Stretching (เดินออกจากร้านพร้อมบิดขี้เกียจ)", "urban", 1030.2f, 0f, 1889.73f),
|
||||
new("SwitchChar_common_22", 96u, "Exiting Store Fist Pump (เดินออกจากร้านพร้อมชูกำปั้นมั่นใจ)", "urban", 2062.63f, 0f, 2459f),
|
||||
new("SwitchChar_common_23", 106u, "Exiting Restaurant Patting Belly (เดินออกจากร้านอาหารลูบท้องอิ่ม)", "urban", 3074.69f, 0f, 2185.89f),
|
||||
new("SwitchChar_common_24", 97u, "Walking & Hanging up Scam Call (คุยโทรศัพท์สายหลอกลวงแล้วส่ายหัวตัดสาย)", "urban", 1727f, 9.64f, 1968f),
|
||||
new("SwitchChar_common_10", 28u, "Cyberpunk Nightclub Dancing 1 (แดนซ์ในผับไซเบอร์พังก์ 1)", "urban", 2875.39f, 0.02f, 2203.92f),
|
||||
new("SwitchChar_common_11", 31u, "Wild Cyberpunk Nightclub Dancing 2 (แดนซ์ในผับไซเบอร์พังก์ 2)", "urban", 558.19f, -21.14f, 1933.61f),
|
||||
new("SwitchChar_common_14", 54u, "Petting Street Cat (นั่งยองๆ ลูบหัวแมวจรจัด)", "urban", 2832.1f, 0.05f, 1880.63f),
|
||||
new("SwitchChar_common_15", 55u, "Walking the Dog (พาสุนัขเดินเล่นรอบเมือง)", "urban", 2809.91f, 0.07f, 1940.67f),
|
||||
new("SwitchChar_common_16", 57u, "Taking Landscape Photos (ยกกล้องถ่ายรูปวิวทิวทัศน์เมือง)", "urban", 589f, 0f, 1937.8f),
|
||||
new("SwitchChar_common_17", 60u, "Street Smartphone Selfie (หยิบมือถือขึ้นมาถ่ายรูปเซลฟี่)", "urban", 706.56f, 3.41f, 2097.07f),
|
||||
new("SwitchChar_common_18", 107u, "Photo with Citizen Fan (ถ่ายรูปร่วมกับแฟนคลับชาวเมือง)", "urban", 1229.34f, 0f, 1237.58f),
|
||||
new("SwitchChar_common_26", 105u, "Polite Photo with NPC (ถ่ายรูปคู่กับ NPC อย่างสุภาพ)", "urban", 1889.75f, 0f, 2211.68f),
|
||||
new("SwitchChar_common_19", 56u, "Tossing Can in Trash Bin (โยนกระป๋องลงถังขยะลงเป๊ะ)", "urban", 1717.25f, 0.08f, 2271.25f),
|
||||
new("SwitchChar_common_25", 99u, "Tossing Can in Trash Bin Missed (โยนกระป๋องไม่ลงถังขยะ)", "urban", 1717.25f, 0.08f, 2271.25f),
|
||||
new("SwitchChar_character_1", 88u, "Buying Drink at Vending Machine (หยอดเหรียญกดตู้เครื่องดื่ม)", "urban", 3010.08f, 0f, 1656.21f),
|
||||
new("SwitchChar_common_20", 81u, "Defeat Gang & Escape (ถล่มแก๊งข้างถนนแล้วกระโดดหนี)", "urban", 3984.66f, -0.14f, 636.22f),
|
||||
new("SwitchChar_common_20a", 111u, "Defeat Gold Gang & Escape (ถล่มแก๊งทองดำแล้วสปีดหนี)", "urban", 3984.66f, -0.14f, 636.22f),
|
||||
new("SwitchChar_common_20b", 112u, "Defeat TV-Head Gang & Escape (ถล่มแก๊งหัวทีวีแล้วสเก็ตหนี)", "urban", 3984.66f, -0.14f, 636.22f),
|
||||
|
||||
// Transit & World Transitions
|
||||
new("loading_plane01", 0u, "Airport Airplane Takeoff Cinematic (คัทซีนเครื่องบินขึ้นจากสนามบิน)", "transit"),
|
||||
new("loading_plane02", 0u, "Airport Airplane Landing Cinematic (คัทซีนเครื่องบินร่อนลงจอด)", "transit"),
|
||||
new("loading_metro_end_1", 0u, "Subway Metro Train Arrival (คัทซีนรถไฟใต้ดินเทียบชานชาลา)", "transit"),
|
||||
new("Loading_bus_end", 0u, "Metropolis City Bus Arrival (คัทซีนรถเมล์เทศบาลเทียบป้าย)", "transit"),
|
||||
new("loading_elevator02", 0u, "Skyscraper Glass Elevator (คัทซีนลิฟต์แก้วตึกระฟ้าความเร็วสูง)", "transit"),
|
||||
new("loading_indoor_in01", 0u, "Cinematic Building Entry A (คัทซีนเดินเข้าอาคารแบบสมจริง A)", "transit"),
|
||||
new("loading_indoor_out01", 0u, "Cinematic Building Exit A (คัทซีนเดินออกจากอาคารสู่ถนนใหญ่ A)", "transit"),
|
||||
];
|
||||
|
||||
private async Task<object> PlayTimelineAsync(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?)" };
|
||||
|
||||
string timeline = "SwitchChar_tafei_01";
|
||||
string timeline = "TL_SEYM_010_Q010_S01";
|
||||
uint explicitConfigId = 0;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("name", out var pn)) timeline = pn.GetString() ?? pn.GetRawText();
|
||||
else if (root.TryGetProperty("id", out var pi)) timeline = pi.GetString() ?? pi.GetRawText();
|
||||
if (root.TryGetProperty("configId", out var pc)) explicitConfigId = pc.GetUInt32();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new { ok = false, error = $"bad request: {ex.Message}" };
|
||||
}
|
||||
|
||||
var match = Array.Find(AllTimelines, t => t.id.Equals(timeline, StringComparison.OrdinalIgnoreCase));
|
||||
uint configId = explicitConfigId != 0 ? explicitConfigId : (match?.configId ?? 0);
|
||||
|
||||
// 1. If configured in SwitchSpiritConfig, dispatch native SwitchSpiritConfigId RPC
|
||||
if (configId != 0)
|
||||
{
|
||||
var state = GameRouter.GetStateIfExists(session);
|
||||
var pos = (match is not null && (match.x != 0 || match.y != 0 || match.z != 0))
|
||||
? new Vec3(match.x, match.y, match.z)
|
||||
: (state is not null && state.HasLastReportedPlayerTransform ? state.LastReportedPlayerPosition : new Vec3(0, 0, 0));
|
||||
|
||||
var payload = WorldCodec.SwitchSpiritConfigId(configId, pos);
|
||||
await session.NotifyAsync(MethodId.SyncSwitchSpiritConfigId, UxSerializer.Serialize(payload), token);
|
||||
session.Log.Info($"[DEBUG-API] sent native SwitchSpiritConfigId configId={configId} timeline={timeline}");
|
||||
}
|
||||
|
||||
// 2. Dispatch Lua Fastpatch notice
|
||||
var cmd = $"CMD:PLAY_TIMELINE:{timeline}";
|
||||
await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token);
|
||||
|
||||
session.Log.Info($"[DEBUG-API] play realtime timeline={timeline}");
|
||||
return new { ok = true, timeline };
|
||||
session.Log.Info($"[DEBUG-API] play realtime timeline={timeline} configId={configId}");
|
||||
return new { ok = true, timeline, configId };
|
||||
}
|
||||
|
||||
private async Task<object> ToggleClothesAsync(CancellationToken token)
|
||||
@@ -1250,20 +1339,7 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
|
||||
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)" },
|
||||
}
|
||||
items = AllTimelines
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -692,13 +692,49 @@ async function toggleClothes() {
|
||||
log('toggle clothes: ' + JSON.stringify(r));
|
||||
}
|
||||
|
||||
async function loadCutsceneCatalog() {
|
||||
const sel = document.getElementById('cutsceneSelect');
|
||||
if (!sel) return;
|
||||
try {
|
||||
const res = await api('/api/cutscene/catalog');
|
||||
if (res && res.items && res.items.length > 0) {
|
||||
const groups = {
|
||||
story: { label: '🎬 เนื้อเรื่องหลัก & เควสต์ (Main Story & Quests)', items: [] },
|
||||
character: { label: '🌟 แอนิเมชันเปิดตัวตัวละคร (Character Entrances & Specials)', items: [] },
|
||||
urban: { label: '🏙️ ฉากชีวิตเมือง & กิจกรรม (Urban Life & Activities)', items: [] },
|
||||
transit: { label: '✈️ ทรานซิชันโลก & ยานพาหนะ (World & Vehicle Transitions)', items: [] }
|
||||
};
|
||||
res.items.forEach(it => {
|
||||
const cat = groups[it.category] ? it.category : 'character';
|
||||
groups[cat].items.push(it);
|
||||
});
|
||||
let html = '';
|
||||
for (const k in groups) {
|
||||
const g = groups[k];
|
||||
if (g.items.length > 0) {
|
||||
html += `<optgroup label="${g.label}">`;
|
||||
g.items.forEach(it => {
|
||||
html += `<option value="${it.id}" data-config-id="${it.configId || 0}">${it.name}</option>`;
|
||||
});
|
||||
html += `</optgroup>`;
|
||||
}
|
||||
}
|
||||
sel.innerHTML = html;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load cutscene catalog:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function playSelectedCutscene() {
|
||||
const sel = document.getElementById('cutsceneSelect');
|
||||
const opt = sel ? sel.options[sel.selectedIndex] : null;
|
||||
const name = sel ? sel.value : 'TL_SEYM_010_Q010_S01';
|
||||
const configId = opt ? parseInt(opt.getAttribute('data-config-id') || '0', 10) : 0;
|
||||
const r = await api('/api/timeline/play', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
body: JSON.stringify({ name, configId })
|
||||
});
|
||||
log('play cutscene: ' + JSON.stringify(r));
|
||||
}
|
||||
@@ -742,6 +778,7 @@ async function setCustomWeather() {
|
||||
loadFleet();
|
||||
loadScenes();
|
||||
loadNpcCatalog();
|
||||
loadCutsceneCatalog();
|
||||
refresh();
|
||||
setInterval(refresh, 2000);
|
||||
document.getElementById('timeslider').addEventListener('input', e => {
|
||||
|
||||
Reference in new issue
Block a user