diff --git a/Ananta.Proxy/proxy/public/fastpatch_4229938.zip b/Ananta.Proxy/proxy/public/fastpatch_4229938.zip index 61ef222..bd1503a 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 1f1f5b0..cb09b8a 100644 --- a/Ananta.Proxy/proxy/tools/generate_client_config_patch.js +++ b/Ananta.Proxy/proxy/tools/generate_client_config_patch.js @@ -718,6 +718,7 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` end)\n` + ` elseif string.sub(cmd, 1, 14) == "PLAY_TIMELINE:" then\n` + ` local arg = string.sub(cmd, 15)\n` + +` local tlName = string.lower(tostring(arg or ""))\n` + ` pcall(function()\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` + @@ -731,31 +732,46 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") + ` 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` + +` if tlData then\n` + +` if myPos then tlData.pos = myPos end\n` + +` if myRot then tlData.rot = myRot end\n` + +` pcall(function() tlData.ignoreLoadingState = true end)\n` + +` pcall(function() tlData.owner = unit end)\n` + +` pcall(function() tlData.loadFromLocal = true end)\n` + ` end\n` + ` end\n` + ` if gTimelineManager.Timeline_LoadAndPlay then\n` + -` pcall(function() gTimelineManager:Timeline_LoadAndPlay(arg, tlData) played = true end)\n` + +` pcall(function() gTimelineManager:Timeline_LoadAndPlay(tlName, tlData) played = true end)\n` + +` if not played and tlName ~= arg then\n` + +` pcall(function() gTimelineManager:Timeline_LoadAndPlay(arg, tlData) played = true end)\n` + +` end\n` + ` end\n` + ` if not played and gTimelineManager.PlayTimeline then\n` + -` pcall(function() gTimelineManager:PlayTimeline(arg) played = true end)\n` + +` pcall(function() gTimelineManager:PlayTimeline(tlName) played = true end)\n` + +` if not played and tlName ~= arg then\n` + +` pcall(function() gTimelineManager:PlayTimeline(arg) played = true end)\n` + +` 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` + +` if tlData then\n` + +` if myPos then tlData.pos = myPos end\n` + +` if myRot then tlData.rot = myRot end\n` + +` pcall(function() tlData.ignoreLoadingState = true end)\n` + +` pcall(function() tlData.owner = unit end)\n` + +` pcall(function() tlData.loadFromLocal = true end)\n` + +` end\n` + +` pcall(function() cm:LoadAndPlay(tlName, tlData) played = true end)\n` + +` if not played and tlName ~= arg then\n` + +` pcall(function() cm:LoadAndPlay(arg, tlData) played = true end)\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` + +` pcall(function() gCS.LuaUtils.PlayTimeline(tlName) end)\n` + ` end\n` + -` local tip = "Playing Timeline: " .. tostring(arg)\n` + +` local tip = "Playing Timeline: " .. tostring(tlName)\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 fa4ca6a..3e90b87 100644 --- a/Ananta.Server/Ananta.App/DebugApiServer.cs +++ b/Ananta.Server/Ananta.App/DebugApiServer.cs @@ -1229,69 +1229,47 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub 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"), + new("tl_seymour01", 1288u, "Seymour Story Chapter 1 (赛默 - 计划外响应 01)", "story"), + new("tl_seymour02", 1289u, "Seymour Story Chapter 2 (赛默 - 计划外响应 02)", "story"), + new("tl_seymour03", 1290u, "Seymour Story Chapter 3 (赛默 - 计划外响应 03)", "story"), + new("tl_seym_010_q010_s01", 1270u, "Seymour Story Quest S01 (面具之下,监视之中)", "story", 3418.28f, 295.23f, 1415.67f), + new("tl_taffy01", 1353u, "Taffy Story: High-Speed Moment 01 (塔菲 - 极速时刻 01)", "story"), + new("tl_taffy02", 1354u, "Taffy Story: High-Speed Moment 02 (塔菲 - 极速时刻 02)", "story"), + new("tl_richiet01", 1208u, "Richie Story: Holiday Syndrome 01 (里希 - 假期综合征 01)", "story"), + new("ananta_openning", 3u, "Ananta Main Opening Cinematic (ฉากเปิดตัว Ananta โลกใหม่)", "story"), + new("alienfight", 2u, "Alien Combat Cinematic (ฉากต่อสู้เอเลี่ยนสุดอลังการ)", "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), + 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_22", 113u, "Lykaia Peeling Apple in Mansion (ไลคาเอีย ปอกแอปเปิ้ลในคฤหาสน์หรู)", "character", 3204.52f, 104.46f, 959.45f), + 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), - // 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"), + // Urban Life & Transitions + 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_10", 28u, "Cyberpunk Nightclub Dancing 1 (แดนซ์ในผับไซเบอร์พังก์ 1)", "urban", 2875.39f, 0.02f, 2203.92f), + 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_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_character_1", 88u, "Buying Drink at Vending Machine (หยอดเหรียญกดตู้เครื่องดื่ม)", "urban", 3010.08f, 0f, 1656.21f), ]; private async Task PlayTimelineAsync(string json, CancellationToken token) @@ -1300,15 +1278,15 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub if (session is null) return new { ok = false, error = "no live game session (is the client in the world?)" }; - string timeline = "TL_SEYM_010_Q010_S01"; - uint explicitConfigId = 0; + string timeline = "tl_seymour01"; + bool teleport = false; 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(); + if (root.TryGetProperty("teleport", out var pt)) teleport = pt.GetBoolean(); } catch (Exception ex) { @@ -1316,27 +1294,31 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub } var match = Array.Find(AllTimelines, t => t.id.Equals(timeline, StringComparison.OrdinalIgnoreCase)); - uint configId = explicitConfigId != 0 ? explicitConfigId : (match?.configId ?? 0); + if (match is not null) + timeline = match.id; - // 1. If configured in SwitchSpiritConfig, dispatch native SwitchSpiritConfigId RPC - if (configId != 0) + // If explicit teleport requested and coordinates exist: + if (teleport && match is not null && (match.x != 0 || match.y != 0 || match.z != 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}"); + ulong unitId = Profile.InitialUnitId; + if (session.Items.TryGetValue(GameRouter.WorldStateKey, out var raw) && raw is WorldEntryState state) + { + lock (state.SyncRoot) + { + if (state.ActiveSpiritUnitId != 0) + unitId = state.ActiveSpiritUnitId; + } + } + await SendPlayerTeleportAsync(session, unitId, match.x, match.y, match.z, 0, "timeline-teleport", token); + await Task.Delay(500, token); } - // 2. Dispatch Lua Fastpatch notice + // Dispatch Lua Fastpatch notice to play timeline directly var cmd = $"CMD:PLAY_TIMELINE:{timeline}"; await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); - session.Log.Info($"[DEBUG-API] play realtime timeline={timeline} configId={configId}"); - return new { ok = true, timeline, configId }; + session.Log.Info($"[DEBUG-API] play timeline={timeline}"); + return new { ok = true, timeline }; } private async Task ToggleClothesAsync(CancellationToken token) @@ -1361,6 +1343,8 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub }; } + private ulong _activeInstrumentEntityId = 0; + private async Task LaunchMinigameAsync(string json, CancellationToken token) { var session = hub.Current; @@ -1377,6 +1361,33 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub } catch { } + // Spawn instrument prop if musical minigame + if (game.Equals("PIANO", StringComparison.OrdinalIgnoreCase)) + { + if (_activeInstrumentEntityId != 0) + { + await GameRouter.DestroyDirectAsync(session, _activeInstrumentEntityId, "instrument-replace"); + _activeInstrumentEntityId = 0; + } + + var (hasPos, px, py, pz, pyaw, _) = TrafficAndCrowdDriver.Instance.GetPlayerPosition(session); + if (hasPos) + { + var yawRad = pyaw * MathF.PI / 180f; + var spawnX = px + MathF.Sin(yawRad) * 1.6f; + var spawnZ = pz + MathF.Cos(yawRad) * 1.6f; + var facingOpposite = (pyaw + 180f) % 360f; + + var res = await GameRouter.SpawnDirectAsync( + session, 80000370, new Vec3(spawnX, py, spawnZ), facingOpposite, rightOffset: 0f, sourceType: 2, reason: "piano-minigame"); + if (res.Ok) + { + _activeInstrumentEntityId = res.EntityId; + session.Log.Info($"[MINIGAME] spawned grand piano prop entity={_activeInstrumentEntityId} in front of player"); + } + } + } + var cmd = $"CMD:LAUNCH_MINIGAME:{game.ToUpperInvariant()}"; await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); @@ -1390,6 +1401,13 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub if (session is null) return new { ok = false, error = "no live game session (is the client in the world?)" }; + if (_activeInstrumentEntityId != 0) + { + await GameRouter.DestroyDirectAsync(session, _activeInstrumentEntityId, "instrument-close"); + _activeInstrumentEntityId = 0; + session.Log.Info("[MINIGAME] removed spawned instrument prop"); + } + var cmd = "CMD:CLOSE_MINIGAME"; await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); @@ -1406,7 +1424,6 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub { new { id = "KOF97", name = "🕹️ Arcade: The King of Fighters '97 (ตู้เกม KOF '97)", category = "arcade" }, new { id = "METALSLUG", name = "🕹️ Arcade: Metal Slug (ตู้เกม Metal Slug ลุยด่าน)", category = "arcade" }, - new { id = "FIGHTER", name = "🥊 3D Arcade Fighter (มินิเกมต่อสู้ 3D)", category = "arcade" }, new { id = "PIANO", name = "🎹 Grand Piano Interactive (เล่นเปียโนสด)", category = "music" }, new { id = "DRUMKIT", name = "🥁 Drumkit Interactive (ตีกลองชุดสด)", category = "music" }, new { id = "BOWLING", name = "🎳 Bowling Alley (โยนโบว์ลิ่ง)", category = "sports" }, diff --git a/Ananta.Server/Ananta.App/DebugPanel/index.html b/Ananta.Server/Ananta.App/DebugPanel/index.html index 7d39f98..ec90e42 100644 --- a/Ananta.Server/Ananta.App/DebugPanel/index.html +++ b/Ananta.Server/Ananta.App/DebugPanel/index.html @@ -233,30 +233,64 @@ -
-

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

-
- - กดปุ่ม F8 ในเกมได้ +
+

🚇 สถานีรถไฟ & รถไฟใต้ดิน (Subway & Metro Fast Travel)

+
🏙️ Nova City Metro (12 สถานี):
+
+ + + + + + + + + + + +
-
สลับการแสดงผลเสื้อผ้าและ Dynamic Bone ของชุดชั้นนอกอย่างหมดจด โดยยังคงโมเดลบอดี้เดิมไว้ 100% ไร้ชิ้นส่วนลอยค้าง
+
🏮 Longqi Metro (13 สถานี):
+
+ + + + + + + + + + + + + +
+
วาร์ปไปยังสถานีรถไฟใต้ดิน (Subway / Metro Stations) ทั่วทั้ง Nova City และ Longqi ทันที

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

@@ -270,29 +304,28 @@ - - + - +
- +
-
เปิดตู้เกมอาร์เคด (KOF '97, Metal Slug) หรือมินิเกมดนตรี/กีฬาในโลกได้ทันที พร้อมปุ่มควบคุมแบบเต็มหน้าจอ
+
เปิดตู้เกมอาร์เคด (KOF '97, Metal Slug), มินิเกมดนตรีสด (เสกโมเดลเปียโนตรงหน้า), หรือวาร์ปไปลานโบว์ลิ่งจริงใน Nova City
@@ -854,6 +887,21 @@ async function launchArcadeMinigame() { await launchSelectedMinigame(); } +async function warpSubway(x, y, z, facing) { + const r = await api('/api/player/teleport', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ x: parseFloat(x), y: parseFloat(y), z: parseFloat(z), facing: parseFloat(facing || 0) }) + }); + log(`🚇 Warp to Subway (${x}, ${y}, ${z}): ` + JSON.stringify(r)); + refresh(); +} + +async function teleportToBowling() { + await warpSubway(1090.8, 6.5, 2083.7, 0); + log('🎳 Warped to Bowling Alley at (1090.8, 6.5, 2083.7)'); +} + async function setTimePreset(hour, minute) { const slider = document.getElementById('timeslider'); if (slider) { diff --git a/Ananta.Server/Ananta.Gameplay/Crowd/UrbanCrowdEngine.cs b/Ananta.Server/Ananta.Gameplay/Crowd/UrbanCrowdEngine.cs index f819599..0cc8c5a 100644 --- a/Ananta.Server/Ananta.Gameplay/Crowd/UrbanCrowdEngine.cs +++ b/Ananta.Server/Ananta.Gameplay/Crowd/UrbanCrowdEngine.cs @@ -97,12 +97,23 @@ public sealed class UrbanCrowdEngine public int TargetPedestrianDensity { get; set; } = 15; public bool PopulateShops { get; set; } = true; - // Verified civilian NPCs from Camp 2 with models + // Verified authentic civilian, shopkeeper, barista, and bar patron NPCs private static readonly uint[] CitizenFormworkPool = [ - 40120883, 40120884, 40120885, 40120886, 40120887, - 40120888, 40120889, 40120890, 40120891, 40120892, - 40120893, 40120894, 40120895, 40120896, 40120897 + 40130101, // Emily Sato - Lively female shopkeeper + 40130102, // Sato Saori - Gentle female shopkeeper + 40130117, // Emma - Store greeter + 40130118, // Owen King - Store manager + 40130121, // Hunter Davis - Store clerk + 40130122, // Linda Wilson - Professional female clerk + 40130128, // Eiko Anderson - Boutique clerk + 40131542, // Yang Zhiyuan - Cafe Barista + 40131591, // Cheng Mosheng - Kiosk clerk + 40130852, // Mizuki Mei - Bar & Nightclub patron + 40650080, // Mizuki Mei - Bar patron + 40651232, // Eugene Price - Cafe patron/customer + 40969501, // Daphne Shelby - Fashion blogger pedestrian + 40968790, // Kaneko Mei - Pedestrian ]; // Common idle / chatter / phone POI action IDs @@ -234,6 +245,10 @@ public sealed class UrbanCrowdEngine var sy = shop.Pos[1]; var sz = shop.Pos[2]; + // Ground elevation clamping: align indoor floor Y with player elevation to prevent falling into void + if (MathF.Abs(sy - py) > 2.5f) + sy = py; + var dx = sx - px; var dz = sz - pz; var distSq = dx * dx + dz * dz; @@ -313,12 +328,14 @@ public sealed class UrbanCrowdEngine if (candidateWaypoints.Count > 0) { var wp = candidateWaypoints[_rng.Next(candidateWaypoints.Count)]; + // Ground elevation clamping: ensure sidewalk pedestrian Y is on mesh floor + var spawnY = MathF.Abs(wp.Y - py) > 2.5f ? py : wp.Y; var formworkId = CitizenFormworkPool[_rng.Next(CitizenFormworkPool.Length)]; var poiAction = AmbientPoiActions[_rng.Next(AmbientPoiActions.Length)]; var facing = _rng.Next(0, 360); var (ok, ids) = await _driver.SpawnCrowdPedestriansAsync( - session, formworkId, poiAction, wp.X, wp.Y, wp.Z, facing); + session, formworkId, poiAction, wp.X, spawnY, wp.Z, facing); if (ok && ids.Length > 0) { @@ -327,7 +344,7 @@ public sealed class UrbanCrowdEngine EntityId = ids[0], FormworkId = formworkId, X = wp.X, - Y = wp.Y, + Y = spawnY, Z = wp.Z, SpawnedAt = DateTime.UtcNow }; diff --git a/Ananta.Server/Ananta.Gameplay/Traffic/CityTrafficEngine.cs b/Ananta.Server/Ananta.Gameplay/Traffic/CityTrafficEngine.cs index f096e7f..6158799 100644 --- a/Ananta.Server/Ananta.Gameplay/Traffic/CityTrafficEngine.cs +++ b/Ananta.Server/Ananta.Gameplay/Traffic/CityTrafficEngine.cs @@ -26,19 +26,28 @@ public sealed class CityTrafficEngine public float MaxSpawnDistance { get; set; } = 160f; public float DespawnDistance { get; set; } = 220f; - // Pool of verified vehicle configs with full models and multi-seat support + // Pool of verified vehicle configs with full models and multi-seat support (no damaged/wrecked models) private static readonly uint[] TrafficVehiclePool = [ 81001001, // Sunset Skywing (Sedan) 81001002, // Korou RV6 (SUV) 81001003, // Kazama Voyage (Sedan) 81000007, // Sunset GT-X Specter (Sports Car) - 81000002, // Korou Transporter M (Truck) - 81000005, // Erebos Black Box (Van) + 81004001, // Kazama CRN6 (Taxi) + 81004027, // Kazama Sandstorm (Police Cruiser) ]; private readonly Random _rng = new(); + public void NotifyVehicleBoarding(ulong entityId) + { + if (_simulatedVehicles.TryRemove(entityId, out var v)) + { + v.HijackedByPlayer = true; + Console.WriteLine($"[TRAFFIC] Vehicle entity={entityId} boarded by player. Detached from traffic loop."); + } + } + public sealed class SimulatedVehicle { public ulong EntityId { get; set; } diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.VehicleStory.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.VehicleStory.cs index a375f6c..2cc466a 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.VehicleStory.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.VehicleStory.cs @@ -487,7 +487,6 @@ internal sealed partial class GameRouter new SceneMethods.SyncChangeVehicleController { VehicleInstanceId = vehicleId, ControllerPid = pid }), ct); await session.NotifyAsync(MethodId.SyncPlayerFinishEnterOrExitVehic, UxSerializer.Serialize(drive), ct); await SendStoryAsync(session, StoryS011Codec4229938.BuildEnterChildBootstrap(VehicleStoryEnterNid, vehicleId, seat)); - await SendStoryAsync(session, StoryS011Codec4229938.BuildDeleteNode(VehicleStoryEnterNid)); session.Log.Info($"[VEHICLE-STORY] force enter vehicle={vehicleId} seat={seat} unit={unitId} status=4"); return (true, $"seated in vehicle {vehicleId} (seat 0) — drive with normal keys, exit with F or the Exit button"); } diff --git a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Vehicles.cs b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Vehicles.cs index 982d6ac..b1a5f47 100644 --- a/Ananta.Server/Ananta.Handlers/Game/GameRouter.Vehicles.cs +++ b/Ananta.Server/Ananta.Handlers/Game/GameRouter.Vehicles.cs @@ -379,6 +379,7 @@ internal sealed partial class GameRouter conn.Log.Info($"[VEHICLE] AskPlayerStartEnterOrExitVehicle vehicle={drive.VehicleEntityId} enter={drive.EnterOrLeave} seat={drive.SeatIndex}"); if (drive.EnterOrLeave) { + Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.NotifyVehicleBoarding(drive.VehicleEntityId); await ForceEnterVehicleAsync(conn.Session, drive.VehicleEntityId); } else diff --git a/Ananta.Server/Ananta.Handlers/Game/TrafficAndCrowdDriver.cs b/Ananta.Server/Ananta.Handlers/Game/TrafficAndCrowdDriver.cs index 537e1d3..bcad87e 100644 --- a/Ananta.Server/Ananta.Handlers/Game/TrafficAndCrowdDriver.cs +++ b/Ananta.Server/Ananta.Handlers/Game/TrafficAndCrowdDriver.cs @@ -25,9 +25,12 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd await GameRouter.DestroyDirectAsync(session, entityId, "traffic-despawn"); } + private readonly System.Collections.Concurrent.ConcurrentDictionary _moveTokens = new(); + public async Task SendVehicleMoveAsync( TcpSession session, ulong entityId, float x, float y, float z, float yaw, float vx, float vz) { + var token = _moveTokens.AddOrUpdate(entityId, 1, (_, cur) => unchecked(cur + 1)); var moveData = new SceneMethods.RaidVehicleSyncData { Id = entityId, @@ -35,8 +38,8 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd FacingDirection = yaw, EulerAngles = new SceneMethods.UxVector3(0f, yaw, 0f), Velocity = new SceneMethods.UxVector3(vx, 0f, vz), - Bits = [], - MoveToken = 0, + Bits = [1, 0, 0, 0], + MoveToken = token, }; await session.NotifyAsync(MethodId.SyncVehicleMove, UxSerializer.Serialize(moveData), CancellationToken.None); GameRouter.TrackVehicleMove(entityId, x, y, z, yaw);