fix(world): resolve traffic freeze, player vehicle kick, mob npcs, void falls, subway fast-travel, and timeline playback

This commit is contained in:
CAtlas0784 committed 2026-09-18 16:11:34 +07:00
1 parent 67bdbae24b
commit 8470c3d413
9 files changed
+233 -123

No files matched your search

Binary file not shown.
@@ -718,6 +718,7 @@ gameSwitchDefaults.map((name) => `M.${name} = true\n`).join("") +
` end)\n` + ` end)\n` +
` elseif string.sub(cmd, 1, 14) == "PLAY_TIMELINE:" then\n` + ` elseif string.sub(cmd, 1, 14) == "PLAY_TIMELINE:" then\n` +
` local arg = string.sub(cmd, 15)\n` + ` local arg = string.sub(cmd, 15)\n` +
` local tlName = string.lower(tostring(arg or ""))\n` +
` pcall(function()\n` + ` pcall(function()\n` +
` local unit = gCS and gCS.MyPlayerManager and gCS.MyPlayerManager.PlayerUnit\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 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` + ` local tlData = nil\n` +
` if gTimelineManager.Timeline_CreateTimelineData then\n` + ` if gTimelineManager.Timeline_CreateTimelineData then\n` +
` tlData = gTimelineManager:Timeline_CreateTimelineData()\n` + ` tlData = gTimelineManager:Timeline_CreateTimelineData()\n` +
` if tlData and myPos then\n` + ` if tlData then\n` +
` tlData.pos = myPos\n` + ` if myPos then tlData.pos = myPos end\n` +
` tlData.rot = myRot\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` +
` end\n` + ` end\n` +
` if gTimelineManager.Timeline_LoadAndPlay then\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` + ` end\n` +
` if not played and gTimelineManager.PlayTimeline then\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` +
` 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` + ` 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 cm = CS.LX6.TimelineScript.CutsceneManager.Instance\n` +
` local tlData = cm:CreateTimelineData()\n` + ` local tlData = cm:CreateTimelineData()\n` +
` if tlData and myPos then\n` + ` if tlData then\n` +
` tlData.pos = myPos\n` + ` if myPos then tlData.pos = myPos end\n` +
` tlData.rot = myRot\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` + ` end\n` +
` pcall(function() cm:LoadAndPlay(arg, tlData) played = true end)\n` +
` end\n` + ` end\n` +
` if not played and gCS and gCS.LuaUtils and gCS.LuaUtils.PlayTimeline then\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` + ` end\n` +
` local tip = "Playing Timeline: " .. tostring(arg)\n` + ` local tip = "Playing Timeline: " .. tostring(tlName)\n` +
` if gDisplayMessageMgr and gDisplayMessageMgr.ShowMessageContent then\n` + ` if gDisplayMessageMgr and gDisplayMessageMgr.ShowMessageContent then\n` +
` gDisplayMessageMgr:ShowMessageContent(tip)\n` + ` gDisplayMessageMgr:ShowMessageContent(tip)\n` +
` end\n` + ` end\n` +
+95 -78
View File
@@ -1229,69 +1229,47 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
private static readonly TimelineItem[] AllTimelines = private static readonly TimelineItem[] AllTimelines =
[ [
// Story & Quests // Story & Quests
new("TL_SEYM_010_Q010_S01", 2030u, "Seymour Story Quest: Chapter 1 S01 (黑客与发布会 - แฮกเกอร์กับงานแถลงข่าว)", "story", 3418.28f, 295.23f, 1415.67f), new("tl_seymour01", 1288u, "Seymour Story Chapter 1 (赛默 - 计划外响应 01)", "story"),
new("TL_TAF_010_Q010_S04", 0u, "Taffy Story Quest: Chapter 1 S04 (เควสต์เนื้อเรื่องทาฟี่)", "story"), new("tl_seymour02", 1289u, "Seymour Story Chapter 2 (赛默 - 计划外响应 02)", "story"),
new("TL_HEISTJ", 0u, "Bank Heist Cinematics Part J (ฉากภารกิจปล้นธนาคาร J)", "story"), new("tl_seymour03", 1290u, "Seymour Story Chapter 3 (赛默 - 计划外响应 03)", "story"),
new("TL_HEISTZ", 0u, "Bank Heist Cinematics Part Z (ฉากภารกิจปล้นธนาคาร Z)", "story"), new("tl_seym_010_q010_s01", 1270u, "Seymour Story Quest S01 (面具之下,监视之中)", "story", 3418.28f, 295.23f, 1415.67f),
new("TL_TGS_MQ_S02", 0u, "Tokyo Game Show Main Quest S02 (ฉากเปิดตัวงาน TGS)", "story"), new("tl_taffy01", 1353u, "Taffy Story: High-Speed Moment 01 (塔菲 - 极速时刻 01)", "story"),
new("TL_SQ_020_Q005_S02", 0u, "Side Quest Special Cinematic (เควสต์ย่อย 020 S02)", "story"), new("tl_taffy02", 1354u, "Taffy Story: High-Speed Moment 02 (塔菲 - 极速时刻 02)", "story"),
new("TL_Bernard", 0u, "Bernard Character Cinematic (คัทซีนเบอร์นาร์ด)", "story"), new("tl_richiet01", 1208u, "Richie Story: Holiday Syndrome 01 (里希 - 假期综合征 01)", "story"),
new("TL_Erin_mm_hunhun", 0u, "Erin vs Thugs Street Scene (เอรินปะทะกลุ่มอันธพาล)", "story"), new("ananta_openning", 3u, "Ananta Main Opening Cinematic (ฉากเปิดตัว Ananta โลกใหม่)", "story"),
new("xsb_phase3_part3", 0u, "Boss Encounter Phase 3 Part 3 (คัทซีนบอสเฟส 3)", "story"), new("alienfight", 2u, "Alien Combat Cinematic (ฉากต่อสู้เอเลี่ยนสุดอลังการ)", "story"),
// Character Entrances & Customs // Character Entrances & Customs
new("SwitchChar_tafei_01", 48u, "Taffy Skyscraper Apartment (ทาฟี่ โดดจากตึกอพาร์ตเมนต์)", "character", 1613.81f, 21.96f, 1565.65f), 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_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_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_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_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_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_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_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_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_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_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_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_22", 113u, "Lykaia Peeling Apple in Mansion (ไลคาเอีย ปอกแอปเปิ้ลในคฤหาสน์หรู)", "character", 3204.52f, 104.46f, 959.45f),
new("SwitchChar_character_16", 90u, "MC Receiving Street Flyer (พระเอก รับใบปลิวจากคนแจก)", "character", 2597.6f, 0.14f, 1156.3f), new("switchchar_common_01", 1u, "Cherry Blossom Ave Passenger Drop (ไลคาเอีย ขับรถมาส่งที่ถนนซากุระ)", "character", 3008.4f, -0.1f, 1614.91f),
new("SwitchChar_character_22", 113u, "Lykaia Peeling Apple in Mansion (ไลคาเอีย ปอกแอปเปิ้ลในคฤหาสน์หรู)", "character", 3204.52f, 104.46f, 959.45f), new("switchchar_common_07", 44u, "Taffy Driving Sports Car (ทาฟี่ ขับรถซิ่งบนถนนใหญ่)", "character", 3008.4f, -0.1f, 1614.91f),
new("SwitchChar_character_23", 114u, "Baijing High-Speed Overtake (ไป๋จิ้ง ซิ่งรถแซงโค้งความเร็วสูง)", "character", 3008.4f, -0.1f, 1614.91f), new("switchchar_common_09", 49u, "Exiting City Taxi (เปิดประตูก้าวลงจากรถแท็กซี่)", "character", 3008.3f, 0f, 1616.5f),
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 // Urban Life & Transitions
new("SwitchChar_common_04", 4u, "Exiting Mahjong Parlor (เดินออกจากร้านไพ่นกกระจอก)", "urban", 1031.7f, 0f, 1888.7f), 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_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_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_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_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_11", 31u, "Wild Cyberpunk Nightclub Dancing 2 (แดนซ์ในผับไซเบอร์พังก์ 2)", "urban", 558.19f, -21.14f, 1933.61f), new("switchchar_common_15", 55u, "Walking the Dog (พาสุนัขเดินเล่นรอบเมือง)", "urban", 2809.91f, 0.07f, 1940.67f),
new("SwitchChar_common_14", 54u, "Petting Street Cat (นั่งยองๆ ลูบหัวแมวจรจัด)", "urban", 2832.1f, 0.05f, 1880.63f), new("switchchar_common_16", 57u, "Taking Landscape Photos (ยกกล้องถ่ายรูปวิวทิวทัศน์เมือง)", "urban", 589f, 0f, 1937.8f),
new("SwitchChar_common_15", 55u, "Walking the Dog (พาสุนัขเดินเล่นรอบเมือง)", "urban", 2809.91f, 0.07f, 1940.67f), new("switchchar_common_18", 107u, "Photo with Citizen Fan (ถ่ายรูปร่วมกับแฟนคลับชาวเมือง)", "urban", 1229.34f, 0f, 1237.58f),
new("SwitchChar_common_16", 57u, "Taking Landscape Photos (ยกกล้องถ่ายรูปวิวทิวทัศน์เมือง)", "urban", 589f, 0f, 1937.8f), new("switchchar_common_26", 105u, "Polite Photo with NPC (ถ่ายรูปคู่กับ NPC อย่างสุภาพ)", "urban", 1889.75f, 0f, 2211.68f),
new("SwitchChar_common_17", 60u, "Street Smartphone Selfie (หยิบมือถือขึ้นมาถ่ายรูปเซลฟี่)", "urban", 706.56f, 3.41f, 2097.07f), new("switchchar_common_19", 56u, "Tossing Can in Trash Bin (โยนกระป๋องลงถังขยะลงเป๊ะ)", "urban", 1717.25f, 0.08f, 2271.25f),
new("SwitchChar_common_18", 107u, "Photo with Citizen Fan (ถ่ายรูปร่วมกับแฟนคลับชาวเมือง)", "urban", 1229.34f, 0f, 1237.58f), new("switchchar_character_1", 88u, "Buying Drink at Vending Machine (หยอดเหรียญกดตู้เครื่องดื่ม)", "urban", 3010.08f, 0f, 1656.21f),
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) private async Task<object> PlayTimelineAsync(string json, CancellationToken token)
@@ -1300,15 +1278,15 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
if (session is null) if (session is null)
return new { ok = false, error = "no live game session (is the client in the world?)" }; return new { ok = false, error = "no live game session (is the client in the world?)" };
string timeline = "TL_SEYM_010_Q010_S01"; string timeline = "tl_seymour01";
uint explicitConfigId = 0; bool teleport = false;
try try
{ {
using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json); using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(json) ? "{}" : json);
var root = doc.RootElement; var root = doc.RootElement;
if (root.TryGetProperty("name", out var pn)) timeline = pn.GetString() ?? pn.GetRawText(); 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(); 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) 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)); 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 explicit teleport requested and coordinates exist:
if (configId != 0) if (teleport && match is not null && (match.x != 0 || match.y != 0 || match.z != 0))
{ {
var state = GameRouter.GetStateIfExists(session); ulong unitId = Profile.InitialUnitId;
var pos = (match is not null && (match.x != 0 || match.y != 0 || match.z != 0)) if (session.Items.TryGetValue(GameRouter.WorldStateKey, out var raw) && raw is WorldEntryState state)
? new Vec3(match.x, match.y, match.z) {
: (state is not null && state.HasLastReportedPlayerTransform ? state.LastReportedPlayerPosition : new Vec3(0, 0, 0)); lock (state.SyncRoot)
{
var payload = WorldCodec.SwitchSpiritConfigId(configId, pos); if (state.ActiveSpiritUnitId != 0)
await session.NotifyAsync(MethodId.SyncSwitchSpiritConfigId, UxSerializer.Serialize(payload), token); unitId = state.ActiveSpiritUnitId;
session.Log.Info($"[DEBUG-API] sent native SwitchSpiritConfigId configId={configId} timeline={timeline}"); }
}
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}"; var cmd = $"CMD:PLAY_TIMELINE:{timeline}";
await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token);
session.Log.Info($"[DEBUG-API] play realtime timeline={timeline} configId={configId}"); session.Log.Info($"[DEBUG-API] play timeline={timeline}");
return new { ok = true, timeline, configId }; return new { ok = true, timeline };
} }
private async Task<object> ToggleClothesAsync(CancellationToken token) private async Task<object> ToggleClothesAsync(CancellationToken token)
@@ -1361,6 +1343,8 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
}; };
} }
private ulong _activeInstrumentEntityId = 0;
private async Task<object> LaunchMinigameAsync(string json, CancellationToken token) private async Task<object> LaunchMinigameAsync(string json, CancellationToken token)
{ {
var session = hub.Current; var session = hub.Current;
@@ -1377,6 +1361,33 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
} }
catch { } 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()}"; var cmd = $"CMD:LAUNCH_MINIGAME:{game.ToUpperInvariant()}";
await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token);
@@ -1390,6 +1401,13 @@ internal sealed class DebugApiServer(PrivateServerConfig config, GameSessionHub
if (session is null) if (session is null)
return new { ok = false, error = "no live game session (is the client in the world?)" }; 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"; var cmd = "CMD:CLOSE_MINIGAME";
await session.NotifyAsync(MethodId.SyncNotice, UxSerializer.Serialize(cmd), token); 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 = "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 = "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 = "PIANO", name = "🎹 Grand Piano Interactive (เล่นเปียโนสด)", category = "music" },
new { id = "DRUMKIT", name = "🥁 Drumkit Interactive (ตีกลองชุดสด)", category = "music" }, new { id = "DRUMKIT", name = "🥁 Drumkit Interactive (ตีกลองชุดสด)", category = "music" },
new { id = "BOWLING", name = "🎳 Bowling Alley (โยนโบว์ลิ่ง)", category = "sports" }, new { id = "BOWLING", name = "🎳 Bowling Alley (โยนโบว์ลิ่ง)", category = "sports" },
+70 -22
View File
@@ -233,30 +233,64 @@
</div> </div>
</div> </div>
<div class="card" style="border-color:#ab47bc"> <div class="card" style="border-color:#3b82f6">
<h2 style="color:#ce93d8">👗 Outfit / Appearance (สลับชุด / ซ่อนเสื้อผ้า)</h2> <h2 style="color:#60a5fa">🚇 สถานีรถไฟ & รถไฟใต้ดิน (Subway & Metro Fast Travel)</h2>
<div style="display:flex;gap:8px;align-items:center"> <div style="font-weight:bold;margin-bottom:6px;color:#93c5fd">🏙️ Nova City Metro (12 สถานี):</div>
<button style="background:#8e24aa;color:white" onclick="toggleClothes()">Toggle Clothes / ซ่อน-แสดงชุด (F8)</button> <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px">
<span class="badge" style="background:#4a148c;color:#e1bee7">กดปุ่ม F8 ในเกมได้</span> <button onclick="warpSubway(2376.0, -1.1, 2729.6, 104.5)">Sunny Bay</button>
<button onclick="warpSubway(898.4, -1.1, 1558.2, 51)">Freshville</button>
<button onclick="warpSubway(1878.8, -1.1, 2250.5, 0)">Glimmergold</button>
<button onclick="warpSubway(2748.3, -1.1, 2046.6, 0)">Memorial Park</button>
<button onclick="warpSubway(3159.0, -1.1, 2643.0, 0)">Azure North</button>
<button onclick="warpSubway(3661.7, -1.0, 1286.9, 0)">Future Port</button>
<button onclick="warpSubway(2243.6, -1.1, 1616.9, 0)">Coastal Station</button>
<button onclick="warpSubway(3941.7, -1.1, 1974.1, 0)">Vista Station</button>
<button onclick="warpSubway(1232.9, -1.1, 2213.1, 0)">Veluxity Station</button>
<button onclick="warpSubway(620.2, -1.0, 2072.1, 0)">Paradox Plaza</button>
<button onclick="warpSubway(1383.1, -1.1, 1267.7, 0)">Marine MechWorks</button>
<button onclick="warpSubway(2947.4, -1.1, 1030.3, 0)">Rixxy Wonderland</button>
</div> </div>
<div class="muted" style="margin-top:6px">สลับการแสดงผลเสื้อผ้าและ Dynamic Bone ของชุดชั้นนอกอย่างหมดจด โดยยังคงโมเดลบอดี้เดิมไว้ 100% ไร้ชิ้นส่วนลอยค้าง</div> <div style="font-weight:bold;margin-bottom:6px;color:#a78bfa">🏮 Longqi Metro (13 สถานี):</div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<button class="ghost" onclick="warpSubway(-1638.0, 18.0, 1540.5, 0)">Dinglan North</button>
<button class="ghost" onclick="warpSubway(-1492.5, 17.6, 1107.9, 0)">Zhaohua Road</button>
<button class="ghost" onclick="warpSubway(-1431.7, 18.0, 565.0, 0)">Dinglan Road</button>
<button class="ghost" onclick="warpSubway(-1725.5, 17.5, 12.1, 0)">Lakeside Pedestrian</button>
<button class="ghost" onclick="warpSubway(-1002.5, 17.7, 115.3, 0)">Zenith Center</button>
<button class="ghost" onclick="warpSubway(-1788.4, 17.5, -574.6, 0)">Lakeside South</button>
<button class="ghost" onclick="warpSubway(-844.0, 17.5, 1133.7, 0)">Great Ferry Port</button>
<button class="ghost" onclick="warpSubway(-624.4, 17.6, 660.4, 0)">Xinfeng Road</button>
<button class="ghost" onclick="warpSubway(61.6, 17.5, 778.3, 0)">Yaojin Road</button>
<button class="ghost" onclick="warpSubway(375.8, 17.5, 98.4, 0)">Yaojin Pedestrian</button>
<button class="ghost" onclick="warpSubway(-427.6, 17.5, 354.0, 0)">Mingchun North</button>
<button class="ghost" onclick="warpSubway(-2442.7, 17.5, 1015.6, 0)">Lakefront Institute</button>
<button class="ghost" onclick="warpSubway(-1138.2, 17.7, -602.3, 0)">Cloudstream West</button>
</div>
<div class="muted" style="margin-top:6px">วาร์ปไปยังสถานีรถไฟใต้ดิน (Subway / Metro Stations) ทั่วทั้ง Nova City และ Longqi ทันที</div>
</div> </div>
<div class="card" style="border-color:#00bcd4"> <div class="card" style="border-color:#00bcd4">
<h2 style="color:#4dd0e1">🎬 3D Cinematics & Story Timelines (ฉากคัทซีนเนื้อเรื่อง)</h2> <h2 style="color:#4dd0e1">🎬 3D Cinematics & Story Timelines (ฉากคัทซีนเนื้อเรื่อง)</h2>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"> <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<select id="cutsceneSelect" style="flex:1;min-width:260px;background:#263238;color:#eee;border:1px solid #37474f;border-radius:4px;padding:6px"> <select id="cutsceneSelect" style="flex:1;min-width:260px;background:#263238;color:#eee;border:1px solid #37474f;border-radius:4px;padding:6px">
<option value="TL_SEYM_010_Q010_S01">Seymour Story Quest: Chapter 1 S01 (เนื้อเรื่องหลัก)</option> <option value="tl_seymour01">Seymour Story Chapter 1 (赛默 - 计划外响应 01)</option>
<option value="SwitchChar_tafei_01">Taffy Cinematic Arrival (ทาฟี่ โดดจากตึก)</option> <option value="tl_seymour02">Seymour Story Chapter 2 (赛默 - 计划外响应 02)</option>
<option value="SwitchChar_dila_03">Dila Combat Cinematic (ดิลา แอกชันสตรีท)</option> <option value="tl_seymour03">Seymour Story Chapter 3 (赛默 - 计划外响应 03)</option>
<option value="SwitchChar_saimo_05">Seymour Street Arrival (เซย์มัวร์ สไตล์คูล)</option> <option value="tl_seym_010_q010_s01">Seymour Story Quest S01 (面具之下,监视之中)</option>
<option value="SwitchChar_lixi_04">Richie Motorcycle Rush (ริชชี่ ซิ่งมอเตอร์ไซค์)</option> <option value="tl_taffy01">Taffy Story: High-Speed Moment 01 (塔菲 - 极速时刻 01)</option>
<option value="SwitchChar_nanzhujue_02">Male MC Arrival A (พระเอก สลับตัว A)</option> <option value="tl_taffy02">Taffy Story: High-Speed Moment 02 (塔菲 - 极速时刻 02)</option>
<option value="SwitchChar_nanzhujue_03">Male MC Arrival B (พระเอก สลับตัว B)</option> <option value="tl_richiet01">Richie Story: Holiday Syndrome 01 (里希 - 假期综合征 01)</option>
<option value="SwitchChar_common_01">Metropolis Skyline Overview (มุมกล้องเมือง 1)</option> <option value="switchchar_tafei_01">Taffy Cinematic Arrival (ทาฟี่ โดดจากตึก)</option>
<option value="SwitchChar_common_04">Street Parkour Traverse (มุมกล้องฟรีรันนิง 4)</option> <option value="switchchar_dila_03">Dila Combat Cinematic (ดิลา แอกชันสตรีท)</option>
<option value="SwitchChar_common_10">Night Neon Lights (มุมกล้องวิวกลางคืน 10)</option> <option value="switchchar_saimo_05">Seymour Street Arrival (เซย์มัวร์ สไตล์คูล)</option>
<option value="SwitchChar_common_27">Dynamic Roof Drop (แอกชันดรอป 27)</option> <option value="switchchar_lixi_04">Richie Motorcycle Rush (ริชชี่ ซิ่งมอเตอร์ไซค์)</option>
<option value="switchchar_nanzhujue_02">Male MC Arrival A (พระเอก สลับตัว A)</option>
<option value="switchchar_nanzhujue_03">Male MC Arrival B (พระเอก สลับตัว B)</option>
<option value="switchchar_common_01">Metropolis Skyline Overview (มุมกล้องเมือง 1)</option>
<option value="switchchar_common_04">Street Parkour Traverse (มุมกล้องฟรีรันนิง 4)</option>
<option value="switchchar_common_10">Night Neon Lights (มุมกล้องวิวกลางคืน 10)</option>
<option value="ananta_openning">Ananta Main Opening Cinematic (ฉากเปิดตัว Ananta โลกใหม่)</option>
<option value="alienfight">Alien Combat Cinematic (ฉากต่อสู้เอเลี่ยนสุดอลังการ)</option>
</select> </select>
<button style="background:#00838f;color:white" onclick="playSelectedCutscene()">Play Cinematic / เล่นคัทซีน</button> <button style="background:#00838f;color:white" onclick="playSelectedCutscene()">Play Cinematic / เล่นคัทซีน</button>
</div> </div>
@@ -270,29 +304,28 @@
<optgroup label="🕹️ ตู้เกมอาร์เคด (Arcade Cabinets)"> <optgroup label="🕹️ ตู้เกมอาร์เคด (Arcade Cabinets)">
<option value="KOF97" selected>🕹️ Arcade: The King of Fighters '97 (ตู้เกม KOF '97)</option> <option value="KOF97" selected>🕹️ Arcade: The King of Fighters '97 (ตู้เกม KOF '97)</option>
<option value="METALSLUG">🕹️ Arcade: Metal Slug (ตู้เกม Metal Slug ลุยด่าน)</option> <option value="METALSLUG">🕹️ Arcade: Metal Slug (ตู้เกม Metal Slug ลุยด่าน)</option>
<option value="FIGHTER">🥊 3D Arcade Fighter (มินิเกมต่อสู้ 3D)</option>
</optgroup> </optgroup>
<optgroup label="🎵 ดนตรีสด (Live Musical Instruments)"> <optgroup label="🎵 ดนตรีสด (Live Musical Instruments)">
<option value="PIANO">🎹 Grand Piano (เล่นเปียโนสด)</option> <option value="PIANO">🎹 Grand Piano (เล่นเปียโนสด - เสกตู้เปียโนตรงหน้า)</option>
<option value="DRUMKIT">🥁 Drumkit Set (ตีกลองชุดสด)</option> <option value="DRUMKIT">🥁 Drumkit Set (ตีกลองชุดสด)</option>
</optgroup> </optgroup>
<optgroup label="🏆 กิจกรรมกีฬา (Sports & Leisure)"> <optgroup label="🏆 กิจกรรมกีฬา (Sports & Leisure)">
<option value="BOWLING">🎳 Bowling Alley (โยนโบว์ลิ่ง)</option>
<option value="BASKETBALL">🏀 Street Basketball (ชูตบาสเกตบอล)</option> <option value="BASKETBALL">🏀 Street Basketball (ชูตบาสเกตบอล)</option>
</optgroup> </optgroup>
</select> </select>
<button style="background:#e65100;color:white;font-weight:600;padding:8px 16px" onclick="launchSelectedMinigame()">Launch / เปิดมินิเกม</button> <button style="background:#e65100;color:white;font-weight:600;padding:8px 16px" onclick="launchSelectedMinigame()">Launch / เปิดมินิเกม</button>
<button style="background:#4a5568;color:#cbd5e0;padding:8px 14px" onclick="closeActiveMinigame()">Close / ปิดมินิเกม</button> <button style="background:#4a5568;color:#cbd5e0;padding:8px 14px" onclick="closeActiveMinigame()">Close / ปิดมินิเกม</button>
<button style="background:#059669;color:white;font-weight:600;padding:8px 14px" onclick="teleportToBowling()">🎳 Warp to Bowling Alley (วาร์ปไปลานโบว์ลิ่ง)</button>
</div> </div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px"> <div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
<button style="background:#d97706;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('KOF97')">🕹️ KOF '97</button> <button style="background:#d97706;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('KOF97')">🕹️ KOF '97</button>
<button style="background:#d97706;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('METALSLUG')">🕹️ Metal Slug</button> <button style="background:#d97706;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('METALSLUG')">🕹️ Metal Slug</button>
<button style="background:#2563eb;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('PIANO')">🎹 Piano</button> <button style="background:#2563eb;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('PIANO')">🎹 Piano</button>
<button style="background:#2563eb;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('DRUMKIT')">🥁 Drumkit</button> <button style="background:#2563eb;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('DRUMKIT')">🥁 Drumkit</button>
<button style="background:#059669;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('BOWLING')">🎳 Bowling</button> <button style="background:#059669;color:white;font-size:12px;padding:4px 8px" onclick="teleportToBowling()">🎳 Bowling Alley</button>
<button style="background:#059669;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('BASKETBALL')">🏀 Basketball</button> <button style="background:#059669;color:white;font-size:12px;padding:4px 8px" onclick="launchMinigameQuick('BASKETBALL')">🏀 Basketball</button>
</div> </div>
<div class="muted" style="margin-top:6px">เปิดตู้เกมอาร์เคด (KOF '97, Metal Slug) หรือมินิเกมดนตรี/กีฬาในโลกได้ทันที พร้อมปุ่มควบคุมแบบเต็มหน้าจอ</div> <div class="muted" style="margin-top:6px">เปิดตู้เกมอาร์เคด (KOF '97, Metal Slug), มินิเกมดนตรีสด (เสกโมเดลเปียโนตรงหน้า), หรือวาร์ปไปลานโบว์ลิ่งจริงใน Nova City</div>
</div> </div>
<div class="card" style="border-color:#10b981"> <div class="card" style="border-color:#10b981">
@@ -854,6 +887,21 @@ async function launchArcadeMinigame() {
await launchSelectedMinigame(); 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) { async function setTimePreset(hour, minute) {
const slider = document.getElementById('timeslider'); const slider = document.getElementById('timeslider');
if (slider) { if (slider) {
@@ -97,12 +97,23 @@ public sealed class UrbanCrowdEngine
public int TargetPedestrianDensity { get; set; } = 15; public int TargetPedestrianDensity { get; set; } = 15;
public bool PopulateShops { get; set; } = true; 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 = private static readonly uint[] CitizenFormworkPool =
[ [
40120883, 40120884, 40120885, 40120886, 40120887, 40130101, // Emily Sato - Lively female shopkeeper
40120888, 40120889, 40120890, 40120891, 40120892, 40130102, // Sato Saori - Gentle female shopkeeper
40120893, 40120894, 40120895, 40120896, 40120897 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 // Common idle / chatter / phone POI action IDs
@@ -234,6 +245,10 @@ public sealed class UrbanCrowdEngine
var sy = shop.Pos[1]; var sy = shop.Pos[1];
var sz = shop.Pos[2]; 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 dx = sx - px;
var dz = sz - pz; var dz = sz - pz;
var distSq = dx * dx + dz * dz; var distSq = dx * dx + dz * dz;
@@ -313,12 +328,14 @@ public sealed class UrbanCrowdEngine
if (candidateWaypoints.Count > 0) if (candidateWaypoints.Count > 0)
{ {
var wp = candidateWaypoints[_rng.Next(candidateWaypoints.Count)]; 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 formworkId = CitizenFormworkPool[_rng.Next(CitizenFormworkPool.Length)];
var poiAction = AmbientPoiActions[_rng.Next(AmbientPoiActions.Length)]; var poiAction = AmbientPoiActions[_rng.Next(AmbientPoiActions.Length)];
var facing = _rng.Next(0, 360); var facing = _rng.Next(0, 360);
var (ok, ids) = await _driver.SpawnCrowdPedestriansAsync( 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) if (ok && ids.Length > 0)
{ {
@@ -327,7 +344,7 @@ public sealed class UrbanCrowdEngine
EntityId = ids[0], EntityId = ids[0],
FormworkId = formworkId, FormworkId = formworkId,
X = wp.X, X = wp.X,
Y = wp.Y, Y = spawnY,
Z = wp.Z, Z = wp.Z,
SpawnedAt = DateTime.UtcNow SpawnedAt = DateTime.UtcNow
}; };
@@ -26,19 +26,28 @@ public sealed class CityTrafficEngine
public float MaxSpawnDistance { get; set; } = 160f; public float MaxSpawnDistance { get; set; } = 160f;
public float DespawnDistance { get; set; } = 220f; 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 = private static readonly uint[] TrafficVehiclePool =
[ [
81001001, // Sunset Skywing (Sedan) 81001001, // Sunset Skywing (Sedan)
81001002, // Korou RV6 (SUV) 81001002, // Korou RV6 (SUV)
81001003, // Kazama Voyage (Sedan) 81001003, // Kazama Voyage (Sedan)
81000007, // Sunset GT-X Specter (Sports Car) 81000007, // Sunset GT-X Specter (Sports Car)
81000002, // Korou Transporter M (Truck) 81004001, // Kazama CRN6 (Taxi)
81000005, // Erebos Black Box (Van) 81004027, // Kazama Sandstorm (Police Cruiser)
]; ];
private readonly Random _rng = new(); 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 sealed class SimulatedVehicle
{ {
public ulong EntityId { get; set; } public ulong EntityId { get; set; }
@@ -487,7 +487,6 @@ internal sealed partial class GameRouter
new SceneMethods.SyncChangeVehicleController { VehicleInstanceId = vehicleId, ControllerPid = pid }), ct); new SceneMethods.SyncChangeVehicleController { VehicleInstanceId = vehicleId, ControllerPid = pid }), ct);
await session.NotifyAsync(MethodId.SyncPlayerFinishEnterOrExitVehic, UxSerializer.Serialize(drive), ct); await session.NotifyAsync(MethodId.SyncPlayerFinishEnterOrExitVehic, UxSerializer.Serialize(drive), ct);
await SendStoryAsync(session, StoryS011Codec4229938.BuildEnterChildBootstrap(VehicleStoryEnterNid, vehicleId, seat)); 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"); 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"); return (true, $"seated in vehicle {vehicleId} (seat 0) — drive with normal keys, exit with F or the Exit button");
} }
@@ -379,6 +379,7 @@ internal sealed partial class GameRouter
conn.Log.Info($"[VEHICLE] AskPlayerStartEnterOrExitVehicle vehicle={drive.VehicleEntityId} enter={drive.EnterOrLeave} seat={drive.SeatIndex}"); conn.Log.Info($"[VEHICLE] AskPlayerStartEnterOrExitVehicle vehicle={drive.VehicleEntityId} enter={drive.EnterOrLeave} seat={drive.SeatIndex}");
if (drive.EnterOrLeave) if (drive.EnterOrLeave)
{ {
Ananta.Server.Gameplay.Traffic.CityTrafficEngine.Instance.NotifyVehicleBoarding(drive.VehicleEntityId);
await ForceEnterVehicleAsync(conn.Session, drive.VehicleEntityId); await ForceEnterVehicleAsync(conn.Session, drive.VehicleEntityId);
} }
else else
@@ -25,9 +25,12 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd
await GameRouter.DestroyDirectAsync(session, entityId, "traffic-despawn"); await GameRouter.DestroyDirectAsync(session, entityId, "traffic-despawn");
} }
private readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, int> _moveTokens = new();
public async Task SendVehicleMoveAsync( public async Task SendVehicleMoveAsync(
TcpSession session, ulong entityId, float x, float y, float z, float yaw, float vx, float vz) 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 var moveData = new SceneMethods.RaidVehicleSyncData
{ {
Id = entityId, Id = entityId,
@@ -35,8 +38,8 @@ internal sealed class TrafficAndCrowdDriver : ITrafficVehicleDriver, IUrbanCrowd
FacingDirection = yaw, FacingDirection = yaw,
EulerAngles = new SceneMethods.UxVector3(0f, yaw, 0f), EulerAngles = new SceneMethods.UxVector3(0f, yaw, 0f),
Velocity = new SceneMethods.UxVector3(vx, 0f, vz), Velocity = new SceneMethods.UxVector3(vx, 0f, vz),
Bits = [], Bits = [1, 0, 0, 0],
MoveToken = 0, MoveToken = token,
}; };
await session.NotifyAsync(MethodId.SyncVehicleMove, UxSerializer.Serialize(moveData), CancellationToken.None); await session.NotifyAsync(MethodId.SyncVehicleMove, UxSerializer.Serialize(moveData), CancellationToken.None);
GameRouter.TrackVehicleMove(entityId, x, y, z, yaw); GameRouter.TrackVehicleMove(entityId, x, y, z, yaw);