Files

124 KiB
Raw Permalink Blame History

Devlog 2026-09-19 — Metro + City Traffic Engine (AI Handoff)

เอกสารส่งมอบงานสำหรับ AI/ผู้พัฒนาตัวต่อไป — อ่านจบแล้วควรจับงานต่อได้โดยไม่ต้องถามเรื่องบริบท

สถานะปลายวัน (TL;DR)

ระบบ สถานะ
Metro / Light Rail (4 สายวนเมือง) ✅ ใช้ได้จริง ยืนยันในเกมแล้ว (ผู้ใช้เห็นรถไฟวิ่ง)
City Traffic — client-driven ambient (MassTraffic) 🟡 โค้ดครบ build ผ่าน แต่ยังไม่ยืนยันว่า client ปล่อยรถ (รอเทส)
City Traffic — server-driven fallback (IDM + NPC คนขับ + สีรถ) 🟢 โค้ดครบ build ผ่าน (ปิดอยู่ เปิดด้วย ServerSpawnedVehicles=true)
NPC Spawner POI picker ✅ ครบ ~30 สถานะจาก UrbanDiversityPOIActionConfig
Gacha ตั๋ว ✅ ปุ่ม "เสกตั๋วกาชาทั้งหมด" (auto หา id จาก GachaPoolConfig)
WebUI คัทซีนรถไฟตอนวาร์ป ✅ ลบแล้ว (วาร์ปตรง)

Build: dotnet build AnantaPS.sln -c Debug → 0 error 0 warning (ยืนยันล่าสุด)

ไฟล์ที่แตะวันนี้

  • Ananta.Gameplay/Traffic/CityTrafficEngine.cs — เขียนใหม่ทั้งไฟล์ (IDM + ไฟแดง + Aether + จัดการ session ศพ)
  • Ananta.Gameplay/Traffic/ITrafficVehicleDriver.cs — เพิ่ม SpawnAetherVehicleAsync / SendAetherLaneDatasAsync / Spawn/RemoveAetherVehicleNpcAsync / SendAetherIntersectionUpdateAsync
  • Ananta.Handlers/Game/TrafficAndCrowdDriver.cs — implement ช่องทางใหม่ทั้งหมด (+ SyncTeleportVehicle channel เดิม)
  • Ananta.RpcTypes/.../GameScene/AetherVehicleMethods.cs — ไฟล์ใหม่: DTO ทั้งชุด (ClientVehicleInitData4229938, ClientVehicleLaneData4229938, ClientVehicleNpcInitData4229938, ฯลฯ)
  • Ananta.RpcTypes/.../GameScene/VehicleMethods.cs — AetherAIInitData แก้ให้ตรง client จริง (4 field: header + Intersections)
  • Ananta.Core/Protocol/Client4229938/MethodId.cs — เพิ่ม SyncAetherAIIntersectionUpdateData = 68819947 (ตัวอื่นมีอยู่แล้ว ค่าตรง dump)
  • Ananta.Core/ClientData/Client4229938/RailCatalog4229938.cs + VehicleSpawnCatalog4229938.cs — ไฟล์ใหม่ (catalog ตารางรถไฟ + ตาราง spawn รถ/คนขับ)
  • Ananta.Core/ClientData/Client4229938/PoiActionCatalog4229938.cs — เพิ่ม Name + All()
  • Ananta.Core/ClientData/Client4229938/EconomyConfigRepository.cs — เพิ่ม GachaTicketItemIds()
  • Ananta.Handlers/Game/GameRouter.Metro.cs — ไฟล์ใหม่: AskGetAllMetroInfos + push loop SyncRunningMetroInfos
  • Ananta.Handlers/Game/GameRouter.Vehicles.cs — ลบ stub AskGetAllMetroInfos เก่า, init Aether ใส่ intersections
  • Ananta.Handlers/Game/GameRouter.Npc.cs — ส่ง init Aether พร้อม intersections
  • Ananta.App/DebugApiServer.cs — /api/gacha/grant-tickets, config useAetherProtocol/moveReset, POI ไดนามิก
  • Ananta.App/DebugPanel/index.html — ลบคัทซีนรถไฟ, POI picker ยิงจาก API
  • Ananta.Gameplay/Traffic/RoadNetworkModels.cs — TrafficSignalFile เพิ่ม Intersections
  • ข้อมูลใหม่: ClientData/4229938/Configs/Rail{Line,Stop,Station,Train}Config.json, VehicleSpawnConfig.json

ความรู้เชิงเทคนิคที่ค้นพบ (สำคัญมาก — อย่าทำพัง)

  1. UxSerializer อ่านเฉพาะ public FIELD (ไม่ใช่ property) เรียงตามลำดับประกาศ — DTO ที่ใช้ property จะ serialize เป็น object เปล่า (บัคเดิมของ MetroClientInfo)
  2. นาฬิกา server วิ่งเร็วกว่าจริง 15 เท่า — metro ElapsedTime = เวลาตาราง(วินาทีจริง) × 15 ไม่งั้นขบวนคลานเต่า
  3. Aether lane handle = index ใน ZoneGraphStorage.Lanes (flat array) — ZoneLaneData ไม่มี field handle; mapping ของเรา: handle = lane.Id - base ของ region (city base=0 → handle = lane id) — ยังไม่ยืนยันในเกม
  4. LaneHandle/DistanceAlongLane อยู่ทั้งใน ClientVehicleInitData (spawn) และ ClientVehicleLaneData (sync ต่อเนื่อง, status flags: Normal=0, Teleport=4, StandBy=8)
  5. คนขับรถจริง = NPC form 45200091, ผู้โดยสาร = 45200093 (จาก VehicleSpawnConfig.json — มี ColorMod 9994xxxx เป็นสีรถ)
  6. MassTrafficIntersectionState: Start=0, Transition=1, Finishing=2; เฟสไฟของเรา: period 0 = กลุ่ม zebra คู่ (t∈[0,17)), period 1 = กลุ่มคี่ (t∈[17,30)) — cycle 30s, offset = |inter×7919|%30
  7. ทุก Aether packet ได้รับการยืนยันว่าออกจริง: packets log รอบทดลองมี AddData 1,594 + LaneDatas 9,034 รายการ — client ยังไม่ render = ปัญหาอยู่ฝั่ง parse/gating ของ client
  8. UxContract: complex = 0xFF marker, Inline = ไม่มี marker, struct list ใช้ ItemObjectEncoding = Struct

ปัญหาค้าง #1: รถ ambient (client-driven) — ยืนยันแล้วว่าปลุกได้ แต่ตำแหน่ง/โซนผิด

ความคืบหน้าสำคัญ (ทดลองคืนเย็นวันนี้): ส่ง init แยกงเขต 37 จุด (ZoneIndex=0 ทุกแยก) → client ปล่อยรถ ambient ออกมาเป็นครั้งแรก! แต่:

  • รถเกิดซ้อนทับกันที่จุดเดียว จนฟิสิกส์เด้งเละ (ไม่ใช่รถติดธรรมดา)
  • ไม่มี AI ขับ เลย (รถตั้งอยู่กับที่/เด้ง)

สมมติฐานเรียงตามความน่าจะเป็น:

  1. ZoneIndex=0 ที่เราส่งทุกแยกผิด — client ใช้ InstanceId+ZoneIndex ไป resolve แยกกับ zone graph ของตัวเอง; ผิด → จุด spawn เด不堪ใช้ → รถซ้อนกันที่เดียว ต้องหา ZoneIndex จริงของแต่ละแยก (จากตำแหน่งไฟใน TrafficSignals + Bounds ของ ZoneData ใน zonegraph JSON หรือจาก ZoneInfos blob)
  2. Client ต้องการตาราง period ของตัวเอง — เราส่งแค่ index 0/1 ที่สมมติขึ้น; ตารางจริงน่าจะอยู่ใน RPCSerializeAuto.lua (ยังไม่ได้เปิด!) หรือ client คำนวณเองจาก zebraConnector
  3. InstanceId อาจต้องเป็น handle อื่น (ไม่ใช่ id แยกจาก interInfos)

สวิตช์ที่เพิ่มไว้ (ปรับสดผ่าน POST /api/traffic/config):

  • ambientIntersections (default false — ค่าเริ่มต้นปลอดภัย ไม่มีรถระเบิด; ใส่ init แยกงเขตเมื่อเปิด)
  • serverVehicles (default false — รถ server-driven AddData+คนขับ+สี)
  • pushIntersectionUpdates (default false — อย่าเปิดจนกว่าจะรู้ semantics ของ period)
  • useAetherProtocol, moveReset (โหมดเก่า)

แผนเซสชันหน้า: (a) หา ZoneIndex จริงต่อแยก → ทดสอบ ambient อีกรอบ (b) เปิดดู RPCSerializeAuto.lua หา writer ของ SyncAetherAIInitDatas/IntersectionUpdateData เทียบ wire (c) probe แบบรถเดี่ยว: เปิด serverVehicles=true สแปม 1 คัน handle=0 distance=0 ดูว่าโผล่ตรงไหน → ถอด mapping จริงของ LaneHandle (d) ถ้าไหนไม่ผ่าน ถอด hex จาก logs/packets-latest.log เทียบ

ปัญหาค้าง #0 (เดิม): AddData รถ server-driven มองไม่เห็น

รอบก่อน AddData+LaneDatas ยิง 1,594/9,034 แพ็กเก็ตแต่ client ไม่ render — แต่ตอนนี้รู้แล้วว่า client render รถ Aether ได้ (ambient โผล่จริง) ดังนั้นโอกาสสูงที่ AddData ของเรา wire ผิดจุดเดียว (field order ของ ClientVehicleInitData ที่ flatten เอง หรือ LaneHandle) — หลังแก้ ZoneIndex/handle แล้วให้ทดสอบซ้ำ

ปัญหาค้าง #2: Connection lost ตอนวาร์ป/สลับซีน

สงสัย client crash ระหว่าง reload ระบบ Aether/Metro กลางแพ็กเก็ต — รอยืนยันอาการหลังลบคัทซีนรถไฟออกจาก WebUI (รอบก่อนเด้งตอนเล่นคัทซีน + วาร์ปพร้อมกัน) ถ้ายังเด้ง: ลองหยุด push traffic/metro ชั่วคราวตอน scene switching (hook ที่ GameRouter.RaidSwitch/SceneSwitch)

เส้นทางสำรองที่พร้อมใช้

  • โหมด server-driven รถ: POST /api/traffic/config {"ServerSpawnedVehicles": true} (รถ spawn เป็น Aether vehicle + คนขับ + สี) — ถ้า ambient ไม่เคยขึ้น ให้ใช้เส้นนี้เป็นหลักแล้วไล่ debug wire ต่อ
  • โหมด legacy scene-entity: {"useAetherProtocol": false} (SyncSpawnVehicle + SyncVehicleMove/SyncTeleportVehicle — รถกระตุก ใช้สำรองสุดท้าย)

แหล่งข้อมูลอ้างอิง (บนเครื่องนี้)

  • C:\Users\Phitchayut\Desktop\Anangay asset\readable_world_dump_4229938_final\ — ดัมพ์ world (06_RUNTIME_CODE = โค้ด client ที่ถอดแล้ว, 01_CONFIGS = config จริง, 00_INDEX = ดัชนี+คู่มือ NPC_TRAFFIC_GUIDE_RU.md)
  • C:\Users\Phitchayut\Desktop\Anangay asset\Assembly-CSharp-ULTIMATE\ — IL2CPP dump ใหม่ล่าสุด (ใช้ตัวนี้ก่อนเสมอ) มี dump.cs + ux_rpc/
  • C:\Users\Phitchayut\Desktop\pc_dump_new\dump.cs — dump รวม (เก่ากว่า แต่ใช้ได้)
  • C:\Users\Phitchayut\Desktop\Anangay asset\PrivateServer修改_20260918\ — เซิร์ฟเวอร์อื่น (มี Metro ที่ใช้ได้ = ต้นแบบที่พอร์ตมา; ไม่มี traffic)
  • Lua ของ client: Ananta lua server base original\LuaGen\AutoGen\RPCSerializeAuto.lua (wire order ที่ client ใช้จริง — ยังไม่ได้เปิดตรวจ!)

🎯 บัคตายตัวถูกพบ (ท้ายวัน): ลำดับ field ผิดทั้ง packet

+ บัค connection lost ตัวจริงถูกพบด้วย (จาก log 02:56): RuntimeLogs.AppendToPair throw IOException เมื่อไฟล์ packets-*.log ถูก process อื่นล็อก (viewer/proxy) → exception ลอยเข้า RPC pipeline ตอน NotifyAsync → session ตาย = "connection lost" ไม่เกี่ยวกับ traffic/metro เลย! แก้แล้ว: log write เป็น best-effort (try/catch swallow) ใน RuntimeLogs.AppendToPair — logging ตายได้ เกมต้องไม่ตาย

เปิด RPCSerializeAuto.lua ตัวจริง (line 2600) — WriteClientVehicleInitData ใส่ field ของ base class (Id/Position/Facing/EulerAngles) ท้ายสุด ไม่ใช่ต้น และ ControlType เป็น WriteByte ไม่ใช่ Int32! DTO ของเราเรียงแบบ C# inheritance (base first) → wire พังทั้ง packet = เหตุผลที่ AddData มองไม่เห็นมาตลอด

แก้แล้ว: ClientVehicleInitData4229938 เรียงใหม่ตาม Lua (derived first, base last, ControlType=byte) + เปิด ServerSpawnedVehicles=true เป็นค่าเริ่มต้น — Build ผ่าน รอเทสรถในเกม

กฎใหม่สำหรับทุก DTO ที่มี base class: ต้องเช็คลำดับจาก RPCSerializeAuto.lua เสมอ — Lua ของ client อยู่ที่ Anangay asset\Ananta lua server base original\LuaGen\AutoGen\RPCSerializeAuto.lua (writers ตระกูล Aether อยู่บรรทัด ~317 + 2600-2660; LaneData/NpcInitData/InitData ของเราตรงหมดแล้ว)

ยังเปิดค้าง: ambient MassTraffic (ยังเละ — ต้องหา ZoneIndex จริง + อ่าน logic ไฟแดงใน Lua เพิ่ม), UI translator port (ดู roadmap)

✅ ยืนยันจากไฟล์ "All lanes" (คนทำ traffic สำเร็จส่งมา) — ค่ำวันนี้

Anangay asset\All lanes\lanes\:

  • handle-encoding.txt = "index;0" → LaneHandle = index ของเลน (ยืนยันจาก HUD บนเซิร์ฟเวอร์ที่ใช้ได้จริง) — mapping ของเรา (city base=0 → handle = lane.Id) ถูกต้องแล้ว
  • lane-probe.txt = probe จากในเกมจริง (AetherCityManager): vehicle graph 14,171 lanes / 140,786 pts / 71,410 links; lane[0] w=5.00 pts=0..20 links=0..4 zone=0 — ตรงกับ city lane 0 ของเราทุกค่า; นับ region ใน RoadNetwork.json: city=14171 (ตรงเป๊ะ!), airport=15994, longqi=11963 (แยกกราฟตาม raid)
  • DataHandle (root+0x6C) = 0 ใน client จริง แต่ grep ทั้งสอง runtime config ไม่มี key นี้ = ทั้งเราและเขาส่ง default -1044702623 เหมือนกันแล้วเขาใช้ได้ → DataHandle ไม่ใช่ตัวการ
  • CSV ครบ: vehicle-lanes.csv (Handle;Index;Width;ZoneIndex;...;FirstX/Y/Z;LastX/Y/Z), vehicle-lane-links.csv, pedestrian-* (88,361 sidewalk lanes — ไว้ทำคนเดินเกาะทางเท้าจริงภายหลัง)
  • เหลือตัวสงสัยสุดท้าย = หน่วย Timestamp → ทดสอบ 3 ปุ่มใน Traffic Lab (WebUI) แล้ว re-enter world; หรือใช้ RPCSerializeAuto.lua อ่าน Logic ฝั่ง client เพิ่ม (ไฟล์อยู่ Anangay asset\Ananta lua server base original\LuaGen\AutoGen\)

📋 แผนปิดจ็อบ traffic (สถานะล่าสุด — ผูกกับ commit นี้)

เสร็จใน commit นี้แล้ว:

  • ✅ wire order ClientVehicleInitData แก้ตาม RPCSerializeAuto.lua (derived first, base last, ControlType=byte) — hex 89b ตรงสูตร
  • ✅ log เขียน best-effort (ไม่ฆ่า session อีก)
  • ✅ F บนรถ traffic/NPC → ข้าม flow ขึ้นรถ (ไม่เด้ง) — hijack จริงไว้ roadmap
  • ✅ เปลี่ยน spawn เป็น batch SyncAetherAIVehicleAddDatas (68571034) + NPC bind ตามหลัง (ไม่มี single spawn อีกต่อไป)
  • ✅ id base 750_000_100_000 ตาม HUD เซิร์ฟเวอร์ที่ใช้ได้
  • ✅ Traffic Lab ลบจาก WebUI; timestampMode (0=unix,1=×15,2=session,3=÷15) กับ serverVehicles/ambientIntersections ยังเรียกผ่าน POST /api/traffic/config ได้

ลำดับทดสอบเซสชันหน้า (relog หลังแก้ทุกครั้ง):

  1. เข้าเกมปกติ → มีรถไหม (batch + id base ใหม่ + wire ใหม่ = ควรเห็น)
  2. ไม่เห็น → curl timestampMode 3 → 2 → 1 (ดู devlog หัว Timestamp สำหรับเหตุผล pending-buffer)
  3. ยังไม่เห็น → สลับ ambientIntersections:true เทียบว่า client ปลุกระบบเองหรือไม่
  4. จับ hex แพ็กเก็ต AddDatas ใหม่จาก logs/packets-latest.log เทียบ WriteClientVehicleInitData ทีละ field อีกรอบ (list ใหม่มี marker/count ต่างจากเดิม — จุดเสี่ยงใหม่ที่ควรตรวจ)
  5. ถ้าทุกอย่างถูกแต่ยังไม่ render → อ่าน logic จริงฝั่ง client ต่อ: ux_game_all.dump.cs (impl stub ที่ 150532) + หา body เต็มจาก DecryptionVault (ถอด Lua/IL2CPP จาก L50)

ห้ามลืม: รถไฟใช้ได้จริงแล้ว — อย่าทำพังตอนแก้อย่างอื่น | MetroClientInfo/PoiActionCatalog เป็น field ไม่ใช่ property | ทุก DTO ใหม่ต้องเช็คลำดับจาก RPCSerializeAuto.lua ก่อนส่งจริง

หลักฐานใหม่ค่ำนี้ (ตัดตัวแปรออกหมดแล้ว):

  • fastpatch ของเซิร์ฟเวอร์ที่ traffic ใช้ได้ = 3 ไฟล์แปล UI ล้วน (changelog.txt, ClientGameSwitch.lua, UidLayerPanelStore.lua) → ไม่ต้องแพตช์ client — ทางส่งฝั่งเซิร์ฟเวอร์อย่างเดียวพอ
  • rpc_serializer.js/rpc_schema.json ของทีมจีน ไม่มี Aether = ไม่มีตัวอย่างไบต์สำเร็จรูป ต้องพิสูจน์เอง
  • RPCDeserializeAuto.lua ไม่มี reader ของ Aether → S2C ถูกอ่านด้วย C# ฝั่ง client → คำตอบ framing ที่แท้จริงอยู่ใน Assembly-CSharp-ULTIMATE\ux_rpc\payload_types.dump.cs + ux_rpc_stringliterals.json (ยังไม่ได้เปิด!)

ลำดับการทำ (แต่ละขั้นทดสอบ 1 relog):

  1. Timestamp ทดสอบ 4 โหมด — สมบัติหลักคือคำเตือนจาก metro: นาฬิกา client = ค่าเซิร์ฟเวอร์/15 → unix ปกติอาจใหญ่ไป 15 เท่า = รถค้าง pending ตลอด (ตรงอาการเป๊ะ) เพิ่ม mode 3 = unix/15 ใน CurrentAetherTimestamp() แล้วไลจ 3→0→1→2 ผ่าน curl -X POST http://127.0.0.1:5809/api/traffic/config -d "{\"timestampMode\":N}" (ปุ่ม Lab ถูกลบตามที่ผู้ใช้สั่ง — endpoint ยังอยู่)

    ✅ ก่อนปิดวัน: แพ็กเก็ตเราผ่านการตรวจโครงสร้างแล้ว

packets-latest.log (13:33:50): SyncAetherAIVehicleAddData body=89b — 89 ไบต์ = 0xFF complex marker + 88 ไบต์ field ครบตามลำดับ Lua ใหม่พอดี → framing/ลำดับ/ชนิดถูกหมด ผู้ใช้ยืนยัน: เลิกทดลอง timestamp (โดเมนเวลาเดียวกันกับ realTime ที่ส่งอยู่แล้ว)

สิ่งที่เหลือเป็นได้ได้แค่เงื่อนไขฝั่ง client — ลองเรียงใหม่ตามหลักฐาน:

  1. ใช้ batch SyncAetherAIVehicleAddDatas (68571034, List<init>) แทน single — สายสตรีมมิ่งของเซิร์ฟเวอร์จริงน่าจะใช้ตัวรวม (single อาจมีไว้สำหรับเคสพิเศษ) — งานแรกของเซสชันหน้า
  2. NPC คนขับต้องตามก่อน render (เราส่ง NpcAdd ตามหลัง AddData แล้ว — ลองส่งก่อน AddData หรือพร้อมกันใน notify เดียว)
  3. AetherVehicleManager จริงมี m_pendingVehicles + ArrivalServerTime + MAX_PENDING_SYNC_DATA=30 — เปิด dump UX.Game (ux_game_all.dump.cs บรรทัด ~150532 คือ impl ตัวจริงแต่ body โดน strip) ศึกษา field รอบ ๆ หาเงื่อนไข promote เป็น render
  4. ถ้ายังไม่รอด → แวะไปทาง ambient (client เป็นคน spawn) ที่เคยเห็นรถแล้ว: แก้ ZoneIndex ต่อแยกด้วยตำแหน่งไฟจาก vehicle-lanes.csv (มี ZoneIndex ทุกเลน!) แทนการใช้ 0
  5. สลับเป็น batch notify SyncAetherAIVehicleAddDatas = 68571034 (list) — single AddData อาจไม่เข้า MassTraffic; + เปลี่ยน id base เป็น 750000100000 ตามที่เห็นใน HUD เซิร์ฟเวอร์ที่ใช้ได้
  6. Hexdump ของเราเทียบ Lua เอง — ถอดแพ็กเก็ต AddData จาก logs/packets-latest.log (hex เปิดอยู่) แล้ว decode มือทีละ field ตาม WriteClientVehicleInitData เพื่อจับความต่างระดับ serializer (จุดเสี่ยง: Complex marker 0xFF, Int7 = value+1 bias, WriteList7Bit มี marker ก่อน count)
  7. เปิด ux_rpc/payload_types.dump.cs ค้น AetherAIVehicleAddData → framing จริงฝั่งอ่าน (Struct vs Complex ที่ระดับ method payload ไม่ใช่ type)
  8. ถ้าครบทุกขั้นยังไม่เห็น → อ่าน AetherVehicleManager ใน dump จริง (มี m_pendingVehicles + MAX_PENDING_SYNC_DATA=30 + ArrivalServerTime) หาเงื่อนไข "เลย้ยก pending เข้า render"

NPC คนขับ/คนเดิน: หลังรถเห็นแล้ว ค่อยใช้ vehicle-lanes/pedestrian-* CSV (88k เลน) ทำ SyncAetherAICrowdAdd/SyncAetherAIStaticNpcAddData ตามรูปแบบเดียวกัน

การทดลองถัดไป (หลังแก้ wire order แล้วยังไม่เห็นรถ): สงสัย หน่วย Timestamp — client เก็บรถ incoming ใน pending buffer จนนาฬิกาตัวเองถึง Timestamp+0.35s; ถ้านาฬิกาคนละฐาน = รถค้าง pending ตลอด = มองไม่เห็นทั้งที่ WebUI นับได้ เพิ่มสวิตช์ {"timestampMode": N} แล้ว: 0=unix (default), 1=unix×15 (สเกลนาฬิกาเกม), 2=วินาทีนับจากสตาร์ทเซิร์ฟเวอร์ (monotonic เล็ก) — ทดสอบทีละโหมด + re-enter world; โหมดไหนเห็นรถ = จบ จดโหมดนั้นไว้เป็น default

🔑 DecryptionVault (ได้มาช่วงเย็น — กุญแจไข client assets)

แตกไว้ที่: C:\Users\Phitchayut\Desktop\Anangay asset\DecryptionVault_extracted\DecryptionVault\ คือชุด RE ทั้งหมดของ L50 VFS (ระบบไฟล์เข้ารหัสของ client): index v5/v8 + entry payloads (LZ4) + descriptors ถอดสมบูรณ์, textures 89k ได้, มีกุญแจ XOR ครบ (KEYS-AND-CONSTANTS.md), โค้ด decoder C#/Python ใน code/, spec ใน formats/

ทำอะไรได้สำหรับบัคเรา: ถอด Lua ของ client ออกจาก L50 packages ได้ = ได้มาซึ่ง:

  1. RPCSerializeAuto.lua ตัวจริง → wire format ที่แน่นอนของทุก Aether packet (แก้ AddData มองไม่เห็น)
  2. Logic ไฟแดง/MassTraffic จริง (period tables, ZoneIndex mapping) → แก้รถ ambient ซ้อนทับ/ไม่มี AI
  3. Lua ของ DevPanel จีน → ทำ UI translator ให้ตรงจุดโดยไม่ต้องเดา

เส้นทางเซสชันหน้า (แนะนำให้เริ่มที่นี่ก่อนอื่น):

  1. คอมไพล์/รัน decoder จาก code/ (L50Index.cs + L50Payload.cs + Lz4Block) กับไฟล์เกมในเครื่องเรา (โฟลเดอร์เกม Ananta.exe — หาตำแหน่งก่อน)
  2. ระบุไฟล์ Lua ใน index (หาชื่อ/path ที่ขึ้นต้น lua / ui / rpc) → ถอดออกมา
  3. อ่าน RPCSerializeAuto.lua → เทียบ DTO ของเราทุกตัว (ClientVehicleInitData, LaneDatas, InitDatas, IntersectionUpdate)
  4. หา logic ไฟแดง (MassTraffic/IntersectionPeriod) → แก้ init intersections ให้ตรง
  5. ต่อ UI translator ด้วยพจนานุกรมที่เตรียมไว้ (หัวข้อ roadmap บน)

หมายเหตุ: เครื่องคนส่งมี decoded output 600,514 ไฟล์ที่ D:\Ananta\ufmapp_decoded\ (path ของเขา — ถ้าเรามีไฟล์ไม่ครบต้อง decode เองจาก client ในเครื่องเรา)

สิ่งที่ยังไม่ได้ทำ (roadmap)

  • UI Translator (DevPanel จีน → English) — แพ็กเกจ Ananta_UI_Translation_RU_EN (ใน Anangay asset) จากกลุ่มอื่น: หลักการ = fastpatch แทน Lua 1 ไฟล์ใน client แล้ว hook ข้อความ UI ด้วยพจนานุกรม 中文=English (1,352 entries ครอบ dev menu/settings/phone/talents; ไม่ครอบ dialogue/item names)
    • เตรียมไว้แล้ว: config/localization/translation.{en,ru}.txt + Ananta.Proxy/proxy/tools/generate_client_config_patch.DRMK.reference.js (generator ของ Drmk 15KB ที่มี Lua hook ฝังอยู่)
    • ต้องพอร์ต: Lua hook (~500 บรรทัด: HookTable + dictionary loader + env DRMK_DEV_MENU_DICT) จากไฟล์ reference เข้า generator ของเรา (74KB คนละสาย — ห้ามทับ!), เพิ่ม config keys ui.devMenuDictionary + ui.devMenuLanguage:"en" ใน config/private-server.json
    • ตรวจสอบความสำเร็จ: console ต้องขึ้น dev-menu dictionary: 1352 entries, 20 rules ตอนสตาร์ท; คู่มือเต็มอยู่ Anangay asset\Ananta_UI_Translation_RU_EN\README.txt (EN section ท้ายไฟล์)
    • ข้อควรระวัง: fastpatch พัง = เกมค้าง Initializing Game — แก้แล้วต้องทดสอบเข้าเกมทันที และเก็บ backup generator เดิมไว้ก่อนแก้
  • รถไฟสาย 9 (Longqi) / สายอื่นนอกเมือง — ปล่อยปิดไว้เพราะ scene ไม่มี MetroPath
  • ระบบ Borrow/hijack รถ traffic แบบเกมจริง (AskAetherAIBorrowVehicle → SyncAetherAIRaidVehicleRegressAether)
  • ตั๋วรถไฟ/บัส (SyncAetherAIBusStopEvent), MassBusRoute
  • ผู้โดยสารในรถ ambient (โหมด server-driven มีแล้ว)

🔥 ต่อยอดรอบดึก (session ถัดจากแผนข้างบน): เจอตัวการตายตัว + แก้ครบ

สาเหตุรากเหง้าที่รถ server-driven ไม่ขึ้น — "method id ปลอม"

เปิด ux_rpc/ux_game_all.dump.cs (ตาราง IGameSceneToClient_* จริงของ client 4229938) + เทียบ RPCMethodIdToName.lua:

ที่เราเคยใช้ ค่า ผลตรวจ
SyncAetherAIVehicleAddDatas (batch) 68571034 ❌ ไม่มีใน client เลย — client มีแค่ single SyncAetherAIVehicleAddData = 68916691 แพ็กเก็ต batch ถูก client เด้งทิ้งเงียบ ๆ มาตลอด = สาเหตุที่แก้ wire order แล้วยังไม่เห็นรถ
SyncAetherAIVehicleNpcRemove 68173578 ❌ ไม่มีจริง — ตัวจริงคือ SyncAetherAINpcRemove = 68332284 (รับ ulong pid เหมือนกัน)
single AddData 68916691 ✅ ตรง (hex 89b ที่เคย verify = single ตัวนี้แหละ)

แก้แล้ว: TrafficAndCrowdDriver.SpawnAetherVehiclesAsync ยิง single notify ต่อคัน (loop ใน C#), NpcRemove ใช้ SyncAetherAINpcRemove, ใส่ comment ปิดกั้น id ปลอมทั้งสองจุดใน MethodId.cs

บัค wire order อีกชุด: Intersection Init/Update

RPCSerializeAuto.lua (line 2506/2515) เขียน derived fields ก่อน base เหมือนกฎของ ClientVehicleInitData:

  • WriteClientTrafficIntersectionInitInfo: InstanceId(u64) → ZoneIndex(i32) → CurrentState(b) → Current/Next/RailPeriod(b×3)
  • WriteClientTrafficIntersectionPeriodUpdateInfo: IntersectionIndex(u64) → CurrentState(b) → Current/Next/Rail(b×3)

DTO เดิมเราเรียง period bytes ก่อน → wire พังทั้ง packet (แก้แล้วตามลำดับ Lua)

แก้ ambient MassTraffic: ZoneIndex จริงต่อแยก (หมดยุคสมมติ 0)

หลักฐาน: client มี AetherIntersectionManager — โหลด blob StaticIntersectionData (artifact id 111 = StaticIntersectionData.json ใน client res, AetherResourceConfig.json) ซึ่งมี List<StaticZoneInfo> = {ZoneIndex, Position, TrafficLights, TrafficLightPeriodControls} แล้ว BuildIntersectionData resolve init ที่เราส่ง ด้วย ZoneIndex → _zoneIndexToBlobIndex → ส่ง 0 ทุกแยก = entity ล้วนระเบิดไปที่เดียว (ตรงอาการรถซ้อน)

หา ZoneIndex จริงได้แล้ว: city zonegraph = vfc_1286_e108 (14,171 lanes ตรง probe เป๊ะ, 2,246 zones) ไฟจริงเมือง = vfc_1286_e104 world JSON (1,082 ไฟ / 171 แยก) — เอาตำแหน่งไฟไป match zone tag "Intersection" (point-in-bounds + fallback ≤25m): 171/171 แยก match ครบ = 224 zones → สร้าง ClientData/4229938/World/InterZones.json ไว้แล้ว (มี source กำกับ)

  • เซิร์ฟเวอร์โหลดตอน Initialize → GetAetherIntersectionZonePeriods() คืน 1 entry ต่อ zone (InstanceId = 900_000_000_000 + zoneIndex, ClientTrafficIntersectionInitInfo ใหม่)
  • ปุ่ม push update (SyncAetherAIIntersectionUpdateData) เปลี่ยนเป็น address ต่อ zone instance ด้วย
  • ⚠️ ยังไม่รู้ตาราง period จริงของ client (อยู่ใน blob) — ส่ง 0/1 ไปก่อน client ก้าวเอง; ถ้าอยากได้ blob จริงต้อง decode L50 จากเครื่องที่มีเกม (หา StaticIntersectionData.json)

ความรู้ใหม่จาก dump (AetherVehicleManager / AetherCity.Manager)

  • pending flow: SyncAetherAIVehicleAddData → EnqueuePendingVehicle (ArrivalServerTime = Timestamp + 0.35s) → CollectReadyPendingVehicles(currentServerTime) promote เป็น entity → Timestamp = unix ตามที่ผู้ใช้ยืนยันโดเมนเดียวกับ realTime ที่ส่งอยู่
  • DELTA_TIME_PER_SERVER_FRAME = 0.05 (server stream 20Hz — เรา 10Hz + delay buffer 0.35s ยังไหล่ทัน), MAX_PENDING_SYNC_DATA = 30
  • client คำนวณ speed limit เองจาก lane (EstimatedFreewaySpeedMps 16.67 / DefaultRoadSpeed 9.72, variance ±35% จาก RandomFraction) → Speed ที่เราส่งเป็นแค่ hint
  • ไฟจริงเมืองทั้งเมือง = vfc_1286_e104 (171 แยก) ส่วน TrafficSignals.json เรา (165 ไฟ/37 แยก จาก vfc_47) คือแค่โซนเริ่มเกม — IDM light logic ฝั่ง server-driven ยังใช้ตัวเดิมได้

ลำดับทดสอบ (relog ทุกครั้ง)

  1. เข้าเกมปกติ (ServerSpawnedVehicles=true default) → รถควรโผล่+วิ่งแล้ว (single 68916691 + wire 89b + IDM LaneDatas 10Hz, client render/interpolate ตาม zone graph ตัวเอง)
  2. รถโผล่แต่ติดตรง → curl -X POST http://127.0.0.1:5809/api/traffic/config -d "{\"pushIntersectionUpdates\":true}" ดูไฟ; ยังติด → hex แพ็กเก็ต LaneDatas ใหม่จาก logs/packets-latest.log เทียบ WriteClientVehicleLaneData
  3. ทดสอบ ambient แท้ (AI รถจาก client ล้วน): {"ambientIntersections": true} → re-enter → ถ้ารถ ambient วิ่งได้ครบ = ปิดจ็อบ; ถ้ายังซ้อน = blob ฝั่ง client ไม่ครอบ zone ไหนไปดู packet แรกที่ entity ไม่เกิด
  4. NpcAdd คนขับ (68685163) ตามหลังรถ — ถ้ารถโผล่แต่ไม่มีคนขับ = เช็ค WriteClientVehicleNpcInitData อีกรอบ (field ตรง dump แล้ว)

อย่าลืม: อย่ายิง id ปลอม (68571034/68173578) กลับมาอีก — comment เตือนวางไว้ใน MethodId.cs แล้ว | Metro ยังใช้ได้จริง ห้ามทำพัง | ทุก DTO ใหม่เช็คลำดับจาก RPCSerializeAuto.lua ก่อนเสมอ


🎁 20 ก.ย.: เช็คของใหม่ "AnantaGadgetLoader" → ทำเป็นระบบ server-side แล้ว

ของที่เอามาเช็ค (transfer.it zwGjHmhhjJQZ)

📁 เก็บไว้ที่ C:/Users/Phitchayut/Desktop/Anangay asset/AnantaGadgetLoader_v1.0/ — exe + คู่มือจีน + RE_extracted/ (Frida agent JS ที่แกะออกแล้ว + code object gadget_world)

AnantaGadgetLoader_v1.0.zip (55.6 MB) = PyInstaller(python313) + Frida — inject เข้า Ananta.exe แล้ว spawn gadget แบบ interactive รอบผู้เล่น (ตู้ vending, ตู้ claw, ประตู, ม้านั่ง, อาร์เคด, ร้านค้า) ไม่เขียนไฟล์ ปิดเกมหาย แยกจาก new-worldmod (AnantaWorldMod — ไม่ได้แถมมา) ที่ดูแล props ที่ไม่ interact ได้

วิธีทำงานจริง (แกะ gadget_world จาก PyInstaller → Frida JS agent 149KB):

  • หา singleton LX6.Item.GadgetMgr + LX6.Game.MyPlayerManager (ตำแหน่งผู้เล่น)
  • เรียก GadgetMgr.SyncGadgetGridAOIAddByIndex(indexX, indexZ, HashSet<ulong> exceptIds, List<int> sectorIdList, GridIndex playerStandardIndex) (private method) ทีละ cell 40m รัศมี 8 cell (320m) ทุก 0.7 วิ
  • ข้อมูล gadget ทั้งหมด baked ในเครื่อง client อยู่แล้ว — server/tool แค่บอกว่า "cell นี้ให้โหลด sector อะไร" (sector 0 = กลางแจ้ง, ของในอาคารอยู่ sector ต่อตึก — ตัว tool ใช้ cells.json จาก world dump)
  • บทเรียนของมัน: cell ที่ส่งตอน grid data ยังไม่พร้อม = หาย (ต้อง re-send), Remove แล้ว Add กลับมักไม่ผลิต ("poisoned" — ห้าม remove เอง), Add ซ้ำไม่ซ้อน (idempotent), LoadGridAndLocalDataRaid ห้ามเรียกเอง (แตก channel)

สิ่งที่ค้นพบและ implement ลงเซิร์ฟเวอร์เรา (ไม่ต้อง inject แล้ว!)

SyncGadgetGridAOIAddByIndex คือ internal ที่ handler ของ RPC จริงจาก server เรียก — SyncGadgetGridAOIDecrease = 68956797 (CSharpOnly, มีใน MethodId.cs เราอยู่แล้ว แต่ไม่เคยมีใครส่ง — config world.content.gadgets ก็วางรอไว้แต่ไม่มี implement)

wire format (จาก RPCSerializeAuto.lua line 4474-4830 + payload_types.dump.cs — 4 param เป็น struct ทั้งหมด = inline ไม่มี marker):

  1. GadgetGridAOIIncrease: PlayerStandardIndex(GridIndex X/Z) → addInfos(List = ส่งว่าง) → indexList(List = cell ที่สั่งโหลด) → addUniqueIds/removeIds/activeIds/inactiveIds(List ว่าง) → reason(byte: Init=0, Loaded=1, AOIMove=2, ...)
  2. GridAOIDecrease (add): SectorIdList(List = [0]) → BuildingId(i32=0) → StandardIndexList(cells เดียวกัน) → ExceptIds(ว่าง)
  3. GridAOIDecrease (remove): ว่างทั้งหมด (ไม่ remove ตามบทเรียน poisoned)
  4. GadgetExtraSyncInfo: DropLimit ว่าง

ไฟล์ใหม่: Ananta.RpcTypes/.../GadgetMethods.cs (DTO ตามข้างบน) + Ananta.Gameplay/World/GadgetStreamingEngine.cs (loop 700ms ต่อ session: หา cell จากตำแหน่งผู้เล่น → ส่ง cell ใหม่ในรัศมี 3 cell (config) budget 49/tick → re-send ทั้งหมดทุก 120 วิแบบ idempotent เพื่อกันคิวแรกตก) + driver implement ใน TrafficAndCrowdDriver (IGadgetStreamDriver) + wire-up ใน PrivateServerApplication — Build ผ่าน 0/0

สวิตช์: config/private-server.json → world.content.gadgets.{enabled,radiusCells,loadBudgetCells} (default เปิด radius 3)

ข้อจำกัดที่ยังเหลือ: sector [0] = กลางแจ้งเท่านั้น — gadget ในอพาร์ตเมนต์/อาคารต้องมีแผนที่ cell→sector (แบบ cells.json ของ tool) ต่อยอดด้วยการสกัด sector จาก client blob ภายหลัง

ทดสอบ: เข้าเกม → log server ต้องขึ้น [GADGET] Grid stream started → เดินไปตู้ vending/อาร์เคดในเมืองต้องเห็นของโผล่ + กด interact ได้; ถ้าไม่โผล่ เทียบ hex SyncGadgetGridAOIDecrease ใน packets-latest.log กับ Lua writer ทีละ field (ระวัง: อย่าส่ง cell ก่อน player transform พร้อม — engine เราข้าม tick นั้นแล้ว)


🕹️ 20 ก.ย. (ต่อ): new-worldmod + ทำให้ gadget "กดได้"

สถานะจากผู้เล่น: ตู้ vending โผล่แล้ว (ระบบสตรีมใช้ได้!) แต่กด interact ไม่ได้ + ส่ง new-worldmod.zip มา (ไฟล์ที่หายจากชุดเดิม)

new-worldmod.zip → เก็บที่ Anangay asset/new-worldmod/ (มี SOURCE ครบ)

  • world_mod.js = Frida agent สตรีม LX6.Item.SceneItemMgr.SyncSceneItemGridAOIAddByIndex(cx, cz, exceptIds:HashSet<u64>, sectorIdList, playerStandardIndex) (5 พารามิเตอร์ บน 4229938) — เหมือน gadget loader แต่คนละ manager (เสาไฟ/props/ของทำลายได้/เป้าจับโทเค็นไซส์)
  • cells.json = แผนที่ grid→sector จริง 5,116 cells สกัดจาก runtime client รูป [x, z, sectorBitmask] — ใช้ต่อยอดทำ indoor gadget sector ได้ภายหลัง (meta.baked=5116)
  • object_spawner.py (GUI) / inject.py (headless) / มีปุ่ม Save cells สำหรับ export แผนที่เมื่อเปลี่ยนแผนที่

สาเหตุ "กดไม่ได้" + แก้แล้ว

การ interact ของ gadget เป็น server round-trip ที่เซิร์ฟเวอร์เราไม่เคยตอบเลย (มีแต่ id ใน MethodId.cs ไม่มี handler):

  • กด F → client รัน graph ของ gadget → ยิง AskInteractCmd(InteractCmdData) (Notify, 67390408) และ/หรือ
  • AskSceneDeviceChangeState(SceneDeviceEntityType, SceneDeviceStateChangeInfo) (Invoke ต้องมี return SceneDeviceCheckIndexCallback{CheckIndex i32, Error u32}, 67226162)
  • AskSceneDeviceRecordValue(...) (67703474), AskSceneDeviceSendSignal(type, id, signalName) (67144566)
  • เซิร์ฟเวอร์จริงตอบ callback + ส่ง SyncSceneDeviceStateChange/ValueChange/SignalSend และ SyncInteractCmd(s) กลับมา → graph ของ gadget ได้ ACK แล้วค่อยเปิด UI/อนิเมชัน → ไม่มีตัวตอบ = กดแล้วเงียบ

wire format ยืนยันจาก ClientToGameSceneDelegate.lua (client เขียน args เอง: line 552/2230/892/1366/4068): type เป็น byte (SceneDeviceEntityType: Gadget=0, Destructible=1), statechangeinfo/valuechangeinfo เป็น WriteStruct inline, InteractCmdData = {CmdType u32, sender u64, receiver u64, broadCastType i32, CommandData List7Bit, stringParam1, stringParam2} — ⚠️ CommandDataLen ใน dump เป็น metadata ไม่ได้อยู่บน wire ห้ามใส่ใน DTO

แก้แล้ว (Build ผ่าน 0/0):

  • GadgetMethods.cs เพิ่ม DTO interaction ทั้งชุด (InteractCmdData, StateChangeInfo, ValueChangeInfo, CheckIndexCallback, Ask*/Sync* wrappers)
  • GameRouter.Gadgets.cs (ใหม่): รับ AskInteractCmd(s) → echo SyncInteractCmd/SyncInteractCmds (68260752/68976896) กลับ; รับ AskSceneDeviceChangeState/RecordValue → return SceneDeviceCheckIndexCallback{CheckIndex=ที่ส่งมา, Error=0} + ส่ง SyncSceneDeviceStateChange/ValueChange (68165561/68332775); รับ AskSceneDeviceSendSignal → ReturnEmptyOk + SyncSceneDeviceSignalSend (68477555)

ทดสอบ: เข้าเกม → เดินเข้าใกล้ตู้ vending → กด F → log server ต้องขึ้น [GADGET] interact cmd ... / change state ... แล้วหน้าเกมต้องเปิด UI/ทำงานตอบสนอง; ถ้ายังไม่ขึ้น prompt ปุ่ม F เลย = ต้องไล่ state ตอน spawn ต่อ (occupantInfos/states ใน GenerateGadget — จดไว้เป็นขั้นถัดไป) | ประตู/ยกของในอาคารอาจต้องใช้ AskGadgetSceneRoomTrigger (67413973, Invoke) เพิ่ม


🚶 20 ก.ย. (ต่อ 2): คนเดินถนน + NPC dialog + Quest

1. คนเดินตามถนนไม่มี — สาเหตุ: PedestrianNetwork.json มาจากกราฟผิดไฟล์

UrbanCrowdEngine มีระบบ spawn คนเดินบนทางเท้าอยู่แล้ว (โหลด World/PedestrianNetwork.json) แต่ไฟล์เดิมถูกสร้างจาก zonegraph vfc_1000_e8 (X -5667..3490, Z -3688..2083) ซึ่งไม่ใช่กราฟเมืองที่ผู้เล่นอยู่ — ผู้เล่นแถว (3121, 2475) อยู่ นอกครอบช่อง Z ของไฟล์เดิมทั้งหมด → หา lane ใกล้ ๆ ไม่เจอเลย → ไม่มีคนเดิน (เหลือแต่ NPC ตามร้านจาก Interiors.json)

แก้แล้ว: สร้างใหม่จาก vfc_1286_e107 = กราฟคนเดินของเมืองจริง (88,361 เลน Pedestrian ทั้งหมด, bounds X 264..4368 / Z 644..2823 ครอบเมือง, ใกล้ผู้เล่น 1,387 เลนในรัศมี 200m) ตัด PedestrianBanned ออก → PedestrianNetwork.json ใหม่ 87,879 เลน / 22.8MB / สคีมาเดิม (id, tags, pts, isCrosswalk + spatialGrid 100m) engine โหลดได้ทันทีไม่ต้องแก้โค้ด

📌 บทเรียน: e108 = กราฟรถเมือง (Vehicle ล้วน 21,333 เลน — subset 14,171 ที่ client โหลด), e107 = กราฟคนเดินเมือง, vfc_1000_e8 = กราฟอีกพื้นที่นึง (ห้ามใช้กับเมือง), vfc_1287_e0 = กราฟรถเมืองอีกเวอร์ชัน

2. NPC dialog (กด F คุยกับ NPC)

Client เดา dialog id เองจาก config ของ NPC แล้วขออนุมัติผ่าน server (ตัวหนังสือบทสนทนา baked ฝั่ง client — server แค่เปิด/ปิด):

  • AskAgentStartNpcDialog(agentEntityId u64, npcDialogId u32, dialogDuration f32) (Notify, 67438669) → ตอบ SyncAgentPrepareStartNpcDialog(agentId, npcDialogId, npcSoundId=0, virtualNpc=false, entireDialogId=0) (68710152) = เปิดหน้าคุย
  • AskAgentFinishNpcDialog(agentEntityId, npcDialogId) (67940690) → ตอบ SyncAgentFinishNpcDialog(agentId) (68922661) = ปิด

wire ยืนยันจาก ClientToGameSceneDelegate.lua line 2480/5414

3. Quest (Task) accept / submit / tracker

เดิม AskAcceptTask/AskSubmitTask แค่ ReturnEmptyOk ไม่มี push → quest ไม่ขึ้นใน tracker เลย ตอนนี้:

  • AskAcceptTask(taskId) / AskSubmitTask(taskId) / AskAcceptAndSetCurrentTask(taskId, spirit, ignoreSwitch) (ClientToGameDelegate.lua 3172/2656/7136) → ack + ส่ง SyncPlayerSingleTask(taskId, TaskViewData, TaskSpoonViewInfo, TaskStateData) (64917604) ด้วย State = Accepted(1)/Submited(3) (TaskState enum: NotAccept=0, Accepted=1, Submited=3, Aborted=4)
  • AcceptAndSetCurrent ส่ง SyncCurrentTask{Type=1, TaskId} เพิ่ม (payload เดียวกับตอน world entry)
  • DTO ใหม่ทั้งชุดใน TaskAndDialogMethods.cs: TaskViewCounter (recursive, Complex), IntList (List ใช้ WriteList = Int32 count ไม่ใช่ 7bit!), TaskSpoonViewInfo, TaskViewData (dict ใช้ WriteDict7Bit → Count=Int7 บังคับใส่ attribute), TaskStateData (struct inline)
  • ⚠️ จาก RPCSerializeBase.lua: WriteList = 0xFF + Int32 count, WriteList7Bit = 0xFF + 7bit count, WriteDict7Bit = 0xFF + 7bit count, complex null = 0x00 — เช็คทุก field กับ Base ตรงนี้ก่อนเขียน DTO ใหม่

ขั้นถัดไป (roadmap)

  • Quest counter (ฆ่ามอนสเตอร์ X ตัว / เก็บของ) ต้อง hook กับระบบ combat/inventory — ยังไม่ทำ
  • ถ้า NPC ยังไม่โชว์ prompt ปุ่ม F ต้องดูต่อที่ agent component ตอน spawn (SyncManagedLogicAgent / dialog config ของ formwork นั้น)
  • Indoor gadget sector: ใช้ cells.json ของ new-worldmod (Anangay asset/new-worldmod/new-worldmod/cells.json รูป [x, z, sectorBitmask] 5,116 cells) ทำ mapping cell→sector ต่อ

ทดสอบ: relog → (1) เดินตามถนน คนเดินควรเริ่มเกิดรอบตัวใน ~1-2 นาที (spawn 1 คน/2.5 วิ จนถึง 25) (2) เข้าไปคุย NPC ที่ร้าน/คนเดิน กด F = หน้าคุยเปิด (3) รับ/ส่งเควส = ขึ้น tracker | log: [CROWD] Loaded 87879 pedestrian ..., [NPC] start dialog ..., [TASK] accept task=...


🌃 20 ก.ย. (ต่อ 3): ปิดรอบ "ทำให้ครบทุกระบบที่ทำได้" — Build 0/0

1. SceneItem grid stream (เสาไฟ / props / สิ่งที่ทำลายได้) — AnantaWorldMod แบบ server-side

เจอ packet จริงที่คุม world props: SyncDestructibleGridAOIIncrease = 68934309 (client handler เรียก SceneItemMgr.SyncSceneItemGridAOIIncrease(DestructibleGridAOIIncrease) — id มีใน MethodId.cs เราอยู่แล้ว ไม่เคยมีคนส่ง)

  • payload = struct inline: PlayerStandardIndex(GridIndex) → addInfos(List = ว่าง) → indexList(List) → addUniqueIds/removeIds(List) → reason(byte) — wire จาก WriteDestructibleGridAOIIncrease (RPCSerializeAuto.lua 3293)
  • engine เดิม (GadgetStreamingEngine) push คู่กับ gadget packet ทุก batch — เสาไฟ/props เต็มถนนโดยไม่ต้อง inject

2. Indoor gadget sector — ใช้แผนที่ cell→sector จริงจาก new-worldmod

  • คัดลอก new-worldmod/cells.json → ClientData/4229938/World/CellSectors.json (5,116 cells, 94 sector ids รูป [x, z, sectorId...] — sector id เป็นเลข hash ใหญ่ ไม่ใช่ bitmask)
  • engine โหลดตอน Initialize → group cell ใน batch ตาม sector list เดียวกัน → ส่ง 1 packet ต่อ group (GridAOIDecrease.SectorIdList = sector จริงของ cell นั้น) — cell ไหนไม่มีใน map = [0] กลางแจ้ง
  • คุมด้วย config เดิม world.content.gadgets.loadAllSectors (ตอนนี้ = true → ใช้แผนที่จริง; false = sector 0 ล้วน)
  • ผล: gadget ในอพาร์ตเมนต์/ร้าน (ประตู ตู้เสื้อ เครื่องใช้) ควรเริ่มโผล่ตามตึก

3. ประตู / ห้องภายใน

  • AskGadgetSceneRoomTrigger(sceneRoomId i32, gadgetUniqueId u64, isEnter bool) (67413973, Invoke) → ack ReturnEmptyOk (client เดินเข้าห้องเอง) — handler ใหม่ใน GameRouter.Gadgets.cs
  • AskGadgetDoorTransfer มี handler accept อยู่แล้วใน Endpoints.cs (ไม่แตะ)

4. ยืมรถ traffic ขับได้ (borrow) — ปิด roadmap เก่า "hijack not supported"

flow จาก dump: กด F บนรถ traffic → client ยิง AskAetherAIBorrowVehicle(vehicleInstanceId) (67122370, Invoke) → server:

  1. ack + CityTrafficEngine.DetachForBorrow(entityId) (ถอดออกจากซิม + คืน id NPC)
  2. ลบคนขับ/ผู้โดยสารด้วย SyncAetherAINpcRemove
  3. push SyncAetherAIBorrowVehicle (68187552) + SyncAetherAIChangeVehicleControl (68321386, ControlType=2 RaidVehicle) → รถกลายเป็น raid vehicle ขับได้
  4. ลงจากรถ: AskAetherAIRaidVehicleRegressAether (67334266) → ตอบ ControlType=1 คืนสู่ traffic

ยังไม่ทำ (ติดข้อมูล/hook ใหญ่)

  • Quest counter แบบ active (ฆ่า X / เก็บ Y) — ต้อง hook ระบบ combat + parse TaskConfig ฝั่ง server
  • UI translator (fastpatch Lua) — เสี่ยงพัง boot ต้องมีเวลาทดสอบเข้าเกมทันที
  • Bus stop / Metro สาย 9 — scene ไม่มี path ของจริง
  • SyncSceneDeviceOccupantChange (คนนั่งม้านั่งซ้อนกัน) — รอทดสอบ interact รอบแรกก่อน

ลำดับทดสอบรวม (relog): (1) ถนนมีเสาไฟ/props + คนเดิน (2) ตู้ vending กด F ได้ (3) เข้าอาคารมี gadget ในบ้าน (4) กด F รถ traffic แล้วขับได้ (5) คุย NPC / รับ-ส่งเควส


🏠 20 ก.ย. (ต่อ 4): ระบบบ้าน (แก้ตกแมพ) + Quest ใช้ asset จริง 4,342 เควส — Build 0/0

1. บ้าน/interior ตกแมพ — สาเหตุ: server ไม่เคยเปิด SceneRoom

ระบบบ้านเดิมมีแค่ unlock สิทธิ์ใน PlayerInfo (HouseInfoList) — ตอนเดินเข้าประตู client ขอเข้าห้อง แต่ไม่มีใครตอบ → client ไม่โหลด geometry ของห้อง → ตกทะลุพื้น

กลไกจริง (จาก dump): ประตู/ห้อง = SceneRoom ทั้งหมด

  • C2S: AskGadgetSceneRoomTrigger(sceneRoomId, gadgetUniqueId, isEnter) (67413973), AskEnterSceneRoom(sceneRoomId, isVehicle, moveId) (67714973), AskExistSceneRoom(...) (67982277), AskAgentSceneRoomTrigger(sceneRoomId, spoonAgentId, isEnter) (67923734)
  • S2C ตอบ: SyncSpoonRoomState(sceneRoomId, enabled, taskId) (68542118) — ห้องโหลดเมื่อ enabled=true และปิดเมื่อออก (id มีใน MethodId.cs อยู่แล้ว ไม่เคยมีคนส่ง!)

แก้แล้ว: handler ทั้ง 4 ตัว (GameRouter.Gadgets.cs) → ack + push SyncSpoonRoomState ตาม isEnter

2. Quest system — ใช้ TaskConfig.json จริง 4,342 เควส

  • คัดลอก TaskConfig.json (7.4MB, จาก world dump _ALL_CONFIGS/table/ รูป {"records":[...]}) → ClientData/4229938/Configs/
  • TaskCatalog4229938 (ใหม่, Ananta.Core): Lazy parse → Id/Name/RelatedRaid/CounterCount/NextTasks ต่อเควส (client มี config ชุดเดียวกัน baked — ชื่อ/เป้าหมาย render ฝั่ง client เอง server แค่จัด state)
  • Task handlers ใหม่ (GameRouter.Endpoints.cs):
    • per-session state ใน session.Items["taskStates"] (State + int[] counters ขนาดตาม Counter ของ config)
    • AskAcceptTask → validate catalog + push SyncPlayerSingleTask (State=Accepted, CounterValues=0 ตามจำนวน counter)
    • AskSubmitTask → push Submited(3) + auto-accept NextTasks (เดินเควสได้ต่อเนื่องทั้งสาย)
    • AskAcceptAndSetCurrentTask → accept + SyncCurrentTask
    • AskFinishTaskCounter(taskId, counterIndex) → +1 counter แล้ว push (sandbox shortcut ที่ client มีอยู่แล้ว)
  • World entry: push SyncPlayerAllTask(taskInfos, [], currentTask=10001, EventPanelInfo ว่าง, loginGameServer=true) (64260853) ก่อนใช้งาน tracker + เดิม push SyncPlayerSingleTask ของเควสที่ค้าง Accepted จาก session เดิม
  • บ้าน: push SyncHouseFurnitureInfo({}) (66177525) ตอน world entry = บ้านว่างสะอาด ไม่ติด state เฟอร์นิเจอร์

ยังเปิดค้าง

  • Quest counter แบบ auto (ฆ่ามอนสเตอร์แล้วนับเอง) — ต้อง hook combat event กับ counter config ของแต่ละเควส (TaskEventConfig.json 3.3MB รออยู่)
  • ถ้าเข้าบ้านบางหลังยังตก: ดู log [ROOM] — ถ้า client ไม่ยิง ask เลยแปลว่าประตูบ้านนั้นใช้กลไกอื่น (map entrance ผ่าน MapentranceConfig) ค่อยตามต่อ
  • Quest progress รีเซ็ตตอนปิดเกม (state อยู่ใน session) — ถ้าต้องการถาวรค่อยย้ายลงไฟล์

🗄️ 20 ก.ย. (ต่อ 5): manifest.jsonl 600k ไฟล์ + เจอไฟล์เกมในเครื่อง — เปิดทาง decode เองได้

ของใหม่: manifest.jsonl (164MB, 600,514 entries = 77.67GB decoded)

คือดัชนีของ output ถอดชุดเต็มจากเครื่องผู้ส่ง (ufmapp_decoded): แต่ละบรรทัด = {c: container vfc_N, hash u64, off, len, kind, out, out_len, sha256}

  • kinds: opaque 316k / catalogue 133k (.cab = CAB record) / lz4+bulk 89k (.part) / lz4-full 61k (.obj) / media 243 (.mp4 XOR 0x76)
  • ข้อจำกัด: out เป็นชื่อ hash (nameHash→name ยังเป็นโจทย์เปิดของ vault เอง) และไฟล์จริง 77GB อยู่เครื่องผู้ส่ง

เจอไฟล์เกมในเครื่องเรา — decode เองได้!

  • E:\Ananta_beta\Ananta\Ananta_Data\StreamingAssets\Blocks = 653/1,680 containers (41.9GB — ชุดบางส่วน)
  • Desktop trunk_release = vfc_3000/3001 (AssetsNotPatch) บล็อกยักษ์ 131MB+
  • StreamingAssets/NoAB/*.pkg + numPath/ + ShaderBlobs = แหล่ง hash oracle ที่ vault รู้จัก
  • ข้อจำกัด: 341k/600k entries อยู่ใน container ที่ไม่มีในเครื่อง (ต้องโหลดเกมเต็มเพิ่ม หรือขอไฟล์จากผู้ส่ง)

VaultTool (ใหม่) — Anangay asset/VaultTool/VaultTool/

คอนโซล .NET ที่ใช้โค้ดของ DecryptionVault (L50Payload + Lz4Block.Decoder) ตรง ๆ:

  • scan <blocksDir> <manifest> <term> [maxHits] — ไล่ entry ตาม manifest อ่าน (c, off, len) → decode ตาม kind → ค้น ASCII term พร้อม context
  • extract <blocksDir> <container> <off> <len> <kind> <out> — แตก entry เดียว decode แล้วเขียนไฟล์
  • กลยุทธ์: ค้นชื่อ resource ตรง ๆ ไม่เวิร์ค (catalogue ไม่มี path) → สแกนหา "ชื่อ field เฉพาะ" ในเนื้อหา เช่น TrafficLightPeriodControls (จาก StaticIntersectionDataBlob) เพื่อหา entry ของ StaticIntersectionData.json (artifact 111) และ IntersectionPeriodConfig (artifact 7)
  • สแกนรอบแรก (catalogue+lz4 ทั้งหมด): 0 hit — ชื่อ resource ไม่โผล่ใน descriptor ตามที่ vault รายงาน
  • กำลังสแกน opaque (raw ล้วน สแกน byte ตรง) — อยู่ใน VaultTool/scan_period.log + scan_trunk.log

เป้าหมายถัดไปเมื่อเจอ

  1. StaticIntersectionData.json → วางใน ClientData → ambient traffic init ใช้ ZoneIndex+Period จริง 100% (แทน mapping จาก zonegraph)
  2. IntersectionPeriodConfig.asset → ตารางไฟแดงจริง
  3. แหล่ง sector อาคารเพิ่มเติมสำหรับ indoor gadget (เทียบกับ cells.json ของ worldmod)

🏆 20 ก.ย. (ต่อ 6): StaticIntersectionData ตัวจริงถูกถอดได้ — ambient traffic ใช้ข้อมูล 100% จริงแล้ว

ผลสแกนเต็มชุด (1,679/1,680 containers, 98GB trunk บน E:)

เจอ 4 ไฟล์ ที่มี TrafficLightPeriodControls (น่าจะครบทุกชุดที่เกมมี):

container zones ช่วง ZoneIndex หมายเหตุ
vfc_1286 171 59–2224 เมืองหลัก — ติดตั้งใช้แล้ว
vfc_1620 209 4–1792 อีก district
vfc_47 37 75–111 โซนเริ่มเกม (ตรง TrafficSignals เดิมเป๊ะ)
vfc_999 507 7–2122 master set ครบทุกแยก

ไฟล์ decode แล้วทั้งหมดอยู่ Anangay asset/VaultTool/static_intersection_*.raw

สิ่งที่ได้ในไฟล์ (โครงสร้างตรง StaticIntersectionDataBlob ของ client)

{"ZoneInfos":[{ZoneIndex, Position, TrafficLights:[{Index, CrosswalkControlIndex, VehicleControlIndex}], TrafficLightPeriodControls:[{TrafficLightControls:[{TrafficLightStateFlags}], VehicleLanesOpen:[laneHandles]}]}]}

  • ZoneIndex = zone จริงใน zonegraph (ยืนยัน: 138/171 ตรงกับ mapping ที่ derive เอง ส่วนที่ไม่ตรงคือชุด derive ของเราตก/เกิน — ไฟล์จริงชนะเด็ดขาด)
  • TrafficLightPeriodControls มี 1–7 แถวต่อแยก (ไม่ใช่ 2 เท่ากันหมด) — engine วน period ตามจำนวนแถวจริง
  • VehicleLanesOpen = เลน handle ที่เปิดในแต่ละ period (ไว้ทำ lane gating ต่อได้)

โค้ดฝั่ง server (Build 0/0)

  • ClientData/4229938/World/StaticIntersectionData.json (ติดตั้งแล้ว, strip BOM)
  • RoadNetworkModels.cs: + StaticIntersectionFile/StaticZoneInfoEntry/... models
  • CityTrafficEngine: โหลดไฟล์นี้เป็นแหล่งหลักของ GetAetherIntersectionZonePeriods — init 171 zone ด้วย ZoneIndex จริง, period วนในจำนวนแถวจริงของแต่ละแยก (แหล่งเดิม InterZones.json เป็น fallback)

บทเรียน VaultTool

  • scan แบบ multi-term แล้ว (คั่นด้วย comma); ครั้งหน้าแนะนำคำ: IntersectionPeriodConfig, MassConfig.prefab, SyncAetherAI, GadgetLodInfoHash
  • ชื่อ resource ไม่โผล่ใน catalogue (ยืนยันแล้ว 0 hit) — ต้องใช้เทคนิคค้นเนื้อหาด้วยชื่อ field เฉพาะแบบนี้ต่อไป
  • NoAB scripts/main/depends/maps/objects = ตาราง dependency tree ชื่อ+MD5 (โครงสร้าง: [u32 len][name][0x00] ซ้ำ จบด้วย len=0 + MD5 16B บาง record) — parse เก็บไว้ VaultTool/noab_names.json, format ยังต้อง reverse ต่อ (record บางชนิดไม่มี hash คั่น)

💬 20 ก.ย. (ต่อ 7): NPC interact เต็มรูปแบบ + ระบบเดต (InviteRide) — Build 0/0

สถาปัตยกรรมที่ค้นพบ

  • ปุ่ม F บน NPC ถูกคุมฝั่ง server ผ่าน agent property: SyncAgentPropertyChange(agentId, SyncAgentProperty{V, Type}) (S2C 68745768) / SyncFullAgentProperty (68299392) — AgentPropertyType.EnableInteract = 0 คือสวิตช์เปิด interaction
  • polymorphic framing: V = complex (0xFF + subclass fields) แล้วตามด้วย Type byte (client ใช้ Type เลือก subclass) — wire จาก WriteSyncAgentProperty (10046) + WriteAgentPropertyDataBool (394)
  • ระบบเดต = ตระกูล InviteRide: SyncInviteRideNpcInfo(instanceId, active) (64133673) ปลุก NPC เป็นคู่เดต, AgentPropertyType.InviteRideNpcInteract = 21 (List<{Index, Sprite, LabelId}>) = เมนู interact ตอนเดต, action ต่าง ๆ เป็น enum InviteRideActionType (ซื้อน้ำ/ตู้ claw/ลงจากตู้ vending)
  • ของขวัญ: AskInteractNpcWithGift(activityCfgId, itemId, count) (63477049)

สิ่งที่ implement

  1. AgentPropertyMethods.cs (DTO ใหม่): AgentPropertyDataBool, SyncAgentPropertyBoolChange, InviteRideNpcInteractItem, AgentPropertyDataInviteRide, SyncAgentPropertyInviteRideChange, SyncInviteRideNpcInfo, GmStartInviteRideNpc, AskInteractNpcWithGift — ทั้งหมดตามลำดับ Lua
  2. Hook ตอน spawn NPC (GameRouter.Npc.cs): หลัง SyncManagedLogicAgent ส่ง EnableInteract=true ให้ทุก static/crowd NPC ที่เสก → ปุ่ม F/เมนู interact โผล่บน NPC ทุกตัว
  3. Handlers เดต (GameRouter.Gadgets.cs):
    • GmStartInviteRideNpc(npcId) (65008383 — ยิงจาก GM console ของ client) → ack + SyncInviteRideNpcInfo(active=true) + push property เมนูเดต (Type 21)
    • AskInteractNpcWithGift → ack (ของขวัญ sandbox ไม่จำกัด)

ทดสอบ

  • เข้าเกม → NPC ทุกตัว (ร้าน/เดินถนน) ต้องมีปุ่ม F → กดแล้วเปิดบทสนทนา (dialog echo รอบก่อน)
  • เดต: GM console (ALT+F1) สั่ง StartInviteRideNpc → NPC กลายเป็นคู่เดต ตามไปทำกิจกรรม (ซื้อน้ำ/ตู้ claw ด้วยกัน)
  • ถ้าเมนูเดตว่าง: ต้องเติม InviteRideNpcInteractList จาก config จริง (ยังไม่เจอใน dump — ค้น LabelId ใน VaultTool ต่อได้)

🌐 20 ก.ย. (ต่อ 8): แปลเมนูเสก NPC ในเว็บจีน → อังกฤษ — Build 0/0

ปัญหา

เมนูเสก NPC ใน Web Debug Panel แสดงชื่อจีนจาก AgentConfig.json (888 ชื่อ unique) + UrbanDiversityPOIActionConfig.json (3,460 ท่า POI)

วิธีแก้ — ตารางแปล CN→EN แบบ segmentation + char fallback

  • เครื่องมือสร้างตาราง: Anangay asset/VaultTool/build_translation.py (+ char_en.json)
    • dictionary ~1,000 คำ CN→EN (สร้างจาก frequency analysis 4 รอบ: stances/demographics/actions/objects/locations/roles)
    • segment แบบ greedy longest-match → คำที่ไม่เจอ fallback char-level dict (261 chars)
    • ผล: agents แปลสมบูรณ์ 336/888, POI 1,864/3,460 — ที่เหลือเป็น partial อ่าน EN ได้ เหลือ CJK ปนนิดหน่อย (proper noun ส่วนใหญ่)
  • ตาราง: ClientData/4229938/Configs/{agent_names_en,poi_actions_en}.json
  • NameTranslator (ใหม่, อยู่ Core เพื่อไม่ให้ circular): lazy load ตาราง, ชื่อที่ไม่มีในตารางคืนค่าเดิม
  • DebugApiServer NPC catalog endpoint: ชื่อ agent + POI ผ่าน translator
  • ช่องค้นหายังค้นชื่อจีนเดิมได้: NpcCatalog4229938.Search match ทั้งชื่อ EN แปลแล้ว + ชื่อจีนเดิม + id
  • placeholder ค้นหาเปลี่ยนตัวอย่างเป็น EN/จีน (Security, Placeholder, 占位, Duck)

หมายเหตุ

  • ชื่อ teleport ใน panel มี EN อยู่แล้ว (Ivory Cove, Paradox Plaza ฯลฯ — cn เป็น secondary data)
  • อยากได้คุณภาพเต็ม: ขยาย dictionary ต่อใน build_translation.py แล้วรันใหม่ → ไฟล์ json regenerate อัตโนมัติ
  • ⚠️ ไม่ได้ git add/commit ทั้งหมดนี้ตามคำสั่งผู้ใช้ (repo ถูกลบ)

🎨 20 ก.ย. (ต่อ 9): WebUI ใหม่แบบ in-game settings + CBT2→CBT3 audio migration

1. Debug Panel รีดีไซน์ด้วย design system ของ PrivateServer1 (Drmk)

  • port design tokens ทั้งชุด (light/dark theme, การ์ด blur, pill nav, ปุ่ม filled/tonal, keycap font Bahnschrift) มาเขียน mapping ให้ DOM ของเรา (.card/.npc-tab/button/input/table/#clock/#log) — JS/endpoint เดิมไม่แตะ (72 functions ยังทำงานเหมือนเดิม)
  • header ใหม่: โลโก้ ANANTA (SVG จาก Drmk) + ปุ่มสลับธีม (จดจำค่าใน localStorage)
  • backup ของเดิม: DebugPanel/index.html.bak_before_restyle
  • build ผ่าน 0/0

2. CBT2 → CBT3: ความจริงเรื่อง Timeline/Animation + เสียงที่หายไป

  • Blocks: CBT2 เป็น subset ของ CBT3 เป๊ะ (654 ไฟล์ ทั้งหมดมีใน CBT3; CBT3 มีเพิ่มอีก 1,026) — Timeline/Animation อยู่ใน VFS blocks ดังนั้น CBT3 มีข้อมูลทุกอย่างที่ CBT2 มีอยู่แล้ว ไม่มีอะไรต้อง migrate ระดับ block
  • GameRes: CBT2 มี 32 ไฟล์ที่ CBT8 ไม่มี (2.15GB) = audio ทั้งเกม! CBT3 ถูกตัด Wwise banks ออกหมด (Bank/Streams + พากย์์ CN/JP/EN)
  • migrate แล้ว: คัดลอกครบ 32 ไฟล์เข้า CBT3 GameRes → เสียง/เพลง/พากย์กลับมาทั้งหมด
  • สคริปต์ใช้ซ้ำได้: Anangay asset/VaultTool/migrate_cbt2_to_cbt3.py (idempotent — รันใหม่ได้ทุกครั้งที่เกมอัปเดตแล้วตัดไฟล์)

3. repair-shop excerpts (DROP NEW)

เป็นชุดแพตช์อ้างอิงของระบบร้านซ่อมรถ/Shop/Teleport/SceneDevice จาก Drmk — ยังไม่ได้ port รอบนี้ (จุดที่น่าพอร์ตต่อ: GameRouter.SceneDevices.cs + shop_timeline_fix.lua เสริมระบบ shop/gadget ของเรา)

📁 ย้ายที่เก็บแล้ว (ไฟล์รับแล้ว): โฟลเดอร์ DROP NEW ถูกยุบรวมเข้า Anangay asset

  • Anangay asset/PrivateServer1/ — Drmk fork + WebUI ต้นฉบับ (DebugPanel: PrivateServer1/Drmk.Server/Drmk.App/DebugPanel/index.html)
  • Anangay asset/PrivateServer1.7z
  • Anangay asset/repair-shop/ + repair-shop.zip — excerpts ระบบซ่อมรถ/Shop/SceneDevice (ยังไม่พอร์ต)
  • งานที่ทำแล้วจากชุดนี้: Debug Panel เราใช้ design system ของ PrivateServer1 แล้ว (backup อยู่ที่ DebugPanel/index.html.bak_before_restyle)

🛠️ 27 ก.ย.: พอร์ตระบบร้านค้า/4S/tuning จาก repair-shop (DROP NEW) — Build 0/0

ระบบที่ได้ (โค้ด battle-tested จาก Drmk แปลง namespace)

ไฟล์ใหม่:

  • RpcTypes/.../Game/ShopMethods.cs — contracts ครบ: AskNpcShop(63563263), SyncFullCommodityInfos(64769566), AskNpcShopCommodityInfo(63699253), AskVehicleShopSpawnVehicle(63497070)+Result, AskBuyModifyParts(63701165, dict=Dict7Bit!), AskBuyCommodities(63614427), AskBuyCommodity(63970007), AskVehicleShopDestroyParking(63613647), ShopCommodityItem(Reader[366] — CommodityBadgeDiscountDict ต้อง Int7 ไม่งั้น desync 711 รายการ), NpcShopCommodityInfo(Reader[638])
  • Handlers/Game/GameRouter.Shop.cs — caches (ShopConfig→Group→Commodity chain, commodity→part/tag/suit, default suits, part tags, suit rows) + 7 handlers + BuildAppearance (default suit base + bought overrides merged by PartTag; stat tags 13/20/21/22 ไม่เข้า visual) + PushUnlockedVehiclesAsync + ShopInteriors (west/seaside พิกัดจริง) + Continental 4S shop (94100019, spawn 3607.9,-0.1,2353.9 f-31.2)
  • Handlers/Game/GameRouter.Spoon.cs — AskGetSpoonServerOutputPortIndexByType → port 0, AskDoSpoonServerActionByType → push สินค้าร้านใกล้ (60m) เข้าเมนูช่าง
  • Handlers/Game/VehicleTuningStore.cs — persist ซื้ออู้ต่อ pid ที่ save/vehicles/<pid>.json

แก้ไฟล์เดิม:

  • WorldEntryState + VehiclePartsByConfig/VehicleSuitByConfig/SessionUnlockedVehicleConfigs
  • SpawnDirectAsync + พารามิเตอร์ pushSummonStart (push SyncStartSummonVehicle 68681875 — ไม่งั้น autoshop ปฏิเสธ "ไม่ใช่รถตัวเอง") + appearance merge ใน SyncLogicVehicleEnter (Parts/SuitId จริง ไม่ใช่ว่าง)
  • BuildUnlockedResult(world) — รวมรถที่ซื้อ + parts/suit overrides (PublishGarageAsync โหลด tuning จากดิสก์ก่อน push)
  • data ใหม่ 5 ไฟล์ ใน Configs: ShopConfig(113)/ShopCommodityGroupConfig(117)/ShopCommodityConfig(2127)/VehiclePartConfig(1352)/VehiclePartSuitConfig(147) จาก world dump

ที่ตั้งใจไว้ตอนแรกแต่ไม่ได้ทำ:

  • SceneDevices.cs ของ Drmk — handler เดิมของเรา (Gadgets.cs) ทำ semantics เดียวกันแล้ว (ack CheckIndex ของ client + mirror Sync) จึงไม่แทน
  • GameRouter.Teleport.cs (373 บรรทัด: shop enter/leave teleport + CutscenePreview + fuse) — ชนกับ teleport flow สนามบินของเรา ต้องผสานระวัง → คิวถัดไป
  • proxy Lua fixes (shop_waypoint_fix ฯลฯ) — เป็น fastpatch ฝั่ง client ใช้เมื่อจำเป็น

ทดสอบ

  1. เข้าร้านซ่อมรถ (west: 1439,1.5,2379 / seaside: 2791,1.4,1286) → เมนูปรับแต่งต้องมีสินค้า (log [SHOP] npc shop=... pushed N)
  2. ซื้อชิ้นส่วน/สี/ชุดแต่ง → รถเปลี่ยนหน้าตา + กด F เข้ารถขับออกได้
  3. 4S dealer (94100019) ซื้อรถ → รถ spawn ที่จุดส่ง + snap ตำแหน่ง + ขึ้น garage list
  4. ซื้อแล้วรีสตาร์ทเซิร์ฟเวอร์ → ของยังอยู่ (save/vehicles/<pid>.json)

🔍 27 ก.ย. (ต่อ): วิเคราะห์ Player.log ของผู้เล่น — หลักฐานรถ aether และเทียบ DRMK

บริบทจาก Discord DRMK-PS

  • ผู้ใช้เขาแจ้ง "pedestrians ยืนนิ่งไม่เดิน" = ปัญหาเดียวกับที่เราแก้แล้ว (PedestrianNetwork.json จากกราฟเมืองที่ถูกต้อง e107) — เรานำหน้าเขา
  • polareve กำลังแก้ T-pose ในคัทซีนด้วยการถอด animation จาก CBT2 ฉีดเข้า CBT3 ("key is the injection method")

ผล diff ไฟล์ CBT2(E:\Ananta_beta\Ananta) vs CBT3(trunk 4229938)

  • Blocks: CBT2 ⊂ CBT3 เป๊ะ (0 ไฟล์ที่ CBT8 ไม่มี) — Timeline/Animation อยู่ใน blocks ดังนั้น CBT3 มีข้อมูลครบกว่า
  • GameRes: CBT2 มี 32 ไฟล์ (2.15GB) ที่ CBT8 ไม่มี = audio ทั้งเกม → migrate ครบแล้ว (migrate_cbt2_to_cbt3.py)
  • NoABFull ของ CBT3 ⊃ NoAB ของ CBT2 (ทุกไฟล์ใหญ่กว่า)
  • ⇒ การฉีด CBT2→CBT3 ของ polareve ต้องใช้ CBT2 ตัวเต็มของเขา (install เราเป็นชุดย่อย) + injection ผ่าน CoreLib VFSBuilder (DecryptionVault พิสูจน์แล้วว่าใช้ได้)

🔥 หลักฐานใหม่จาก Player.log (Sep 20 01:42-01:45 — ผู้เล่นทดสอบหลังแก้แล้ว)

[AetherVehicleLifecycle] Flush dropped missing BaseVehicle id=750000100001 type=NormalVehicle ×212

Chain ที่พิสูจน์ได้จาก stack trace:

  1. GameSceneToClientImpl.SyncAetherAIVehicleLaneDatas รับ LaneDatas ของเรา ✓
  2. AetherVehicleManager.OnSyncAetherVehicleLaneData เข้า pending ✓
  3. City.UpdateStep1 → FlushPendingAetherVehicles → FlushPendingVehicleImpl รันทุกเฟรม ✓ (timestamp promote โอเค)
  4. แต่ drop เพราะ "missing BaseVehicle" — BaseVehicle = drive-layer vehicle object ที่ถูกสร้าง asynchronously หลัง GO (prefab) instantiate

lifecycle vocabulary จาก IL2CPP stringliteral.json (pc_dump_new): Flush dropped missing BaseVehicle / Flush failed to create VDE / GO Instantiate / refresh waiting for BaseVehicle / AetherVehicleGODecision (PriorityRender/ShouldNotUseGo) / AetherVehicleUnit (EnsureUnit/GOLoaded) / Attach invariant failed / View invariant failed / hasBaseVehicle={0} sameVde={1}

สมมติฐานเหลืออยู่ (เรียงตามน่าจะเป็น)

  1. GO Decision ไม่ instantiate — AetherVehicleGODecision ตัดสินจากระยะ/visibility/อะไรบางอย่างที่เรายังไม่ส่ง (ตรวจ: log ต้องมี ShouldNotUseGo ปนอยู่ไหม)
  2. prefab/dependency ของรถ aether หายใน VFS ของ CBT3 — ตรงกับงาน CBT2→CBT3 ของ polareve (โมเดล legacy spawn เห็นได้ = โมเดลหลักมี แต่ GO variant ของ aether อาจอ้าง dependency อื่น)
  3. NpcAdd ต้องมาก่อน/พร้อม AddData

การทดลองถัดไป (ต้องใช้เกมจริง)

  1. ทดสอบรอบใหม่ด้วย build ปัจจุบัน → ส่ง log/Player.log กลับมา → ดูว่า drop เฉพาะรถไกลหรือ drop หมด
  2. grep log หา ShouldNotUseGo / GOLoaded / EnsureUnit / refresh waiting — แยกว่า GO decision บล็อก หรือ prefab load พัง
  3. ถ้า prefab พัง: ใช้ VaultTool หา path โมเดล (Assets/Res/Prefab/Drive/Vehicles/*.prefab) ใน VFS CBT3 เทียบ CBT2 → เจอตัวขาด = ยืนยันสาเหตุ → inject ตามแนว polareve
  4. ทดสอบสลับลำดับ NpcAdd ก่อน AddData (แก้ที่ GameRouter.Shop/Spoon ไม่เกี่ยว — อยู่ที่ CityTrafficEngine.SpawnAroundPlayerAsync)

🚗 27 ก.ย. (ต่อ): เจอสาเหตุรถ aether ไม่ขึ้นจากโค้ด PrivateServer1 — แก้แล้ว Build 0/0

หมายเหตุ: build 4229938 = CBT8 (แก้คำเรียกเดิม CBT3)

ค้นพบจาก GameRouter.Aether.cs ของ PrivateServer1 (2,406 บรรทัด, ใช้ได้จริงในเกม)

ลำดับ spawn รถ traffic ต่อคันที่ถูกต้อง (ของเราขาดขั้น 1-2 และ 4-5):

  1. SyncLogicVehicleEnter — spawn legacy scene entity = ตัวสร้าง BaseVehicle (ของที่ flush หา!)
  2. SyncSpawnVehicle (VehicleClientInfo, Interactable=false, Velocity=speed)
  3. SyncAetherAIVehicleAddData (68916691 — ของเรามีอยู่แล้ว wire ตรงกัน)
  4. SyncAetherAIChangeVehicleControl (ControlType=AetherVehicle)
  5. SyncAetherAIVehicleForceGo (forceGo=true — สั่งรถเริ่มวิ่ง)
  6. NpcAdd คนขับ + driver behavior task

นี่คือสาเหตุ "Flush dropped missing BaseVehicle" ที่เจอใน Player.log — เราส่งแค่ AddData (aether registration) โดยไม่มี legacy entity = ไม่มี BaseVehicle ให้ flush attach

ข้อยืนยันอื่น: ServerClock ของเขา = anchor unix + uptime (เหมือน mode 0 ของเรา → timestamp unix ถูกต้อง), intersection update wrapper ให้ wire เดียวกัน, VehicleColorId field name ต่างแต่ลำดับเดียวกัน = wire เดียวกัน

สิ่งที่แก้ (TrafficAndCrowdDriver.cs)

  • SpawnAetherVehiclesAsync: ต่อคันส่งครบ 5 ขั้น (LogicVehicleEnter → SpawnVehicle → AddData → ChangeVehicleControl → ForceGo)
  • RemoveAetherVehicleAsync: ส่งคู่ AetherRemove + SyncDestroyVehicle (legacy cleanup ตาม lifecycle "DetachRaidLocal (SyncRemoveVehicle)")
  • SyncAetherAIVehicleForceGo4229938 DTO ใหม่

รอทดสอบ

เข้าเกม → รถ traffic ต้องโผล่และวิ่งแล้ว (legacy entity ให้ BaseVehicle + aether AI ขับ + ForceGo เริ่ม) — ถ้ายังมี drop ใน Player.log ให้ดูว่า "refresh waiting for BaseVehicle" (รอโหลด ปกติ) หรือ drop ถาวร


🧵 27 ก.ย. (ต่อ): วิเคราะห์ log "Connection Lost" ของผู้เล่น — เจอ 3 บัค แก้ครบ Build 0/0

log ที่ผู้เล่นส่งมา (ทดสอบ 01:48-01:57) เผยอะไร

  1. unimplemented notify AskInteractCmd → accepted — ระบบ interact/เดต/dialog ของเราไม่เคยทำงานเลย ตั้งแต่แรก! สาเหตุ: GameRouter.Build() ใช้ whitelist Enabled4229938MethodIds — handler ใหม่ทุกตัวที่เพิ่งเขียนไม่ถูกลงทะเบียน ไม่ว่าจะมี [Handler] attribute แล้วก็ตาม
  2. [AETHER] init sent ×4 ใน 9 วินาที — SendAetherInitForNpcAsync ไม่มี dedup (ยิงซ้ำทุกครั้งที่ spawn NPC batch)
  3. [CROWD-MOTION-ERROR] Cannot access a disposed object: SemaphoreSlim สแปมหลัง client หลุด — background loop ยัง probe session ที่ dispose แล้ว
  4. client ปิด connection เอง ("forcibly closed by the remote host") หลัง world-ready ~50 วิ — สาเหตุฝั่ง client ยังไม่ชัด (ต้องดู Player.log ฝั่ง client ของรอบนั้น)

สิ่งที่แก้ (Build 0/0)

  • GameRouter.cs whitelist +17 method ids: AskInteractCmd/AskInteractCmds, AskAgentStart/FinishNpcDialog, GmStartInviteRideNpc, AskInteractNpcWithGift, AskGadgetSceneRoomTrigger, AskEnter/ExistSceneRoom, AskAgentSceneRoomTrigger, AskNpcShop, AskVehicleShopSpawnVehicle, AskVehicleShopDestroyParking, AskBuyModifyParts, AskGetSpoonServerOutputPortIndexByType, AskDoSpoonServerActionByType, AskFinishTaskCounter — ระบบ interact/dialog/เดต/scene room/ร้านค้า/spoon เดินจริงแล้ว
  • TrafficAndCrowdDriver.GetPlayerPosition: guard ObjectDisposedException → คืน no-player (สแปมหาย)
  • GameRouter.Npc.cs: aether init dedup ต่อ session

⚠️ บทเรียนสำคัญสำหรับทุก handler ใหม่

[Handler] attribute อย่างเดียวไม่พอ — ต้องเพิ่ม MethodId ใน Enabled4229938MethodIds (GameRouter.cs) ด้วยทุกครั้ง ไม่งั้น router ปฏิเสธเงียบ ๆ ด้วยข้อความ "unimplemented"


🚙 27 ก.ย. (ต่อ 2): เปลี่ยนรถ traffic เป็น AI-task drive mode (จาก PrivateServer1) — Build 0/0

ปัญหาจากผู้เล่น: รถบางคันไม่ขับ/ชนกันเละ/โผล่-หาย/ไม่มีคนขับ

สาเหตุ: สมองสองฝ่าย — server สตรีม IDM positions แย่งกับ client AI ที่ ForceGo สั่งวิ่ง

แก้: สลับเป็น AI-task drive mode ตาม PrivateServer1 (โค้ดที่ใช้ได้จริง)

  • พอร์ต VehicleAiTaskMethods.cs (CruiseAITaskParameters + enums + StuckCheckConfig) และ BehaviorTaskCatalog.cs
  • spawn ต่อคัน (หลัง ForceGo): BuildCruiseWaypoints (จุดปลาย lane chain 12 จุด) → SyncAssignVehicleAITask (CruiseType=GoToTargetInOrderLoop, flags=SlowDownWhenArriveTarget|DynamicSpeed, drivingFlags=DFStopForCars|Peds|Lights|SteerAroundObjects, min/max speed ±, AI config pool [16,20-30]) → SyncChangeVehicleAITaskState(Running)
  • หยุดสตรีม LaneDatas (IDM ฝั่ง server เหลือเป็น bookkeeping สำหรับ despawn อย่างเดียว) — สอง stream ชนกันทำให้รถสแนป/ชนกัน
  • DespawnDistance 220→420 ลด churn
  • คนขับ: NpcAdd wrapper + SyncDoAetherAgentBehaviorTaskDef (VehicleNpcTaskId = AnimState 3 จาก AetherNpcBehaviorTaskDefineConfig) — คนขับจะมีตัวตน/ทำท่าขับ
  • Despawn เอาตามระยะอย่างเดียวตอนนี้ (client ขับออกไกลเร็ว)

ค้างต่อ

  • บัค intro cutscene เลือกเพศ MC ในรถ (ต้องดู Player.log ตอนเกิด)
  • สกิล/พลังต่างตัวละครไม่ครบ — ตรวจ COMBAT-CATALOG perSpiritViews กับ FightSkillConfig รายตัว

🎬 27 ก.ย. (ต่อ 3): แผนพอร์ต Intro Cutscene (รถไฟ/กรอกชื่อ/เลือกเพศ) จาก PrivateServer1

หลักฐานว่าทำได้

ภาพจาก DRMK-PS: intro รถไฟเล่นได้จริงถึงขั้น "录入信息" (กรอกชื่อ) + เลือกชื่อเรียก "Chenxiu" + 确认 — โค้ดทั้งหมดอยู่ใน PrivateServer1 ที่เรามี

สถาปัตยกรรมของเขา: เควสขับคัทซีน

  • StoryRootTaskId = 60004938 (รากของ story chain ใน TaskConfig) — accept task รากนี้ = client เริ่มเล่น intro คัทซีนเอง (รถไฟ → กรอกชื่อ → เลือกเพศ → ขับรถ)
  • chain เดินผ่าน NextTasks ใน TaskConfig; แต่ละ task มี counters (TaskConfig.Counter) ที่ต้องเต็มก่อน complete แล้ว advance ไป task ถัดไป
  • client รายงาน event ผ่าน AskChangeTaskCounterValue / AskFinishTaskCounter

ไฟล์ที่ต้องพอร์ต (inventory + ลำดับ)

  1. Drmk.Core/ClientData/Client4229938/TaskCatalogRepository.cs (266 ln) — ChainFrom(root)/Counters(id)/Name(id) — เสริมเข้า TaskCatalog4229938 ที่เรามีอยู่แล้ว (TaskConfig ชุดเดียวกัน)
  2. WorldEntryState +3 fields: QuestStates(dict u32→byte), QuestCounterValues(dict u32→int[]), CurrentQuestTaskId(u32) — Drmk มีอยู่ line 218-225
  3. Handlers/Game/GameRouter.StoryFlow.cs (277 ln) — AcceptStoryTaskAsync / ReportCounterAsync / CompleteAndAdvanceAsync / StartStoryChainAsync / PushTaskContainerAsync / PushCurrentTaskAsync — แทนที่ handler AskAcceptTask/AskSubmitTask/AskFinishTaskCounter แบบง่ายที่เราเพิ่งทำ (ของเขา container-based ครบกว่า)
  4. Handlers/Game/GameRouter.SwitchTimeline.cs (1,161 ln) — ตัวขับ transition คัทซีน↔โลก (ReportPreSwitchSpiritFinish / AskSwitchSpiritComplete / SwitchTeleportTiming.Immediate|Apex) — เทียบกับ Switching.cs ของเราก่อนรวม (อาจทับกัน)
  5. DTO ที่มาพร้อม (หาใน Drmk RpcTypes): SyncPlayerTaskContainer + task container structures
  6. whitelist: AskChangeTaskCounterValue, AskDoGuide, AskFinishGuide (+ตัวที่ไฟล์ใหม่ส่ง)
  7. World entry ของเรา (WorldRouter) ต้องเปลี่ยน opening=false → accept story root (StartStoryChainAsync) หลัง ready — แล้ว client เล่น intro เอง จบแล้วถึงเข้า WORLD-MIN แบบเดิม

ลำดับทำ (รอบหน้า — งานสด ๆ ประมาณ 1 session)

  1. พอร์ต TaskCatalogRepository (ChainFrom/Counters/Name) + เติม 3 fields ใน WorldEntryState
  2. พอร์ต StoryFlow.cs ทั้งไฟล์ (แก้ namespace + GetStateIfExists) + ลบ handler เควสเก่าที่ทับ + whitelist เพิ่ม
  3. Build + ทดสอบ: เข้าเกม → intro รถไฟต้องเล่นถึงหน้ากรอกชื่อ/เลือกเพศ → confirm → คัทซีนรถ → โลก
  4. รอบหลัง: SwitchTimeline.cs (คัทซีนอื่น ๆ นอก intro) + สกิลตัวละครให้ครบ (COMBAT-CATALOG perSpiritViews vs FightSkillConfig)

⚠️ ข้อควรระวัง

  • PushTaskContainerAsync ต้องมาก่อน ack เสมอ (รูปแบบเดียวกับบทเรียนร้านค้า)
  • เควสเดิมที่เราทำ (accept/submit/chain) จะถูกแทน — อย่าลืมย้ายความสามารถ AskFinishTaskCounter ไปรวม
  • TaskCatalogRepository อ่าน TaskConfig.json ที่เราติดตั้งไว้แล้วใน Configs ✓

🎯 27 ก.ย. (ต่อ 4): minimal intro trigger ใส่แล้ว

  • WorldEntryState + QuestStates/QuestCounterValues/StoryChain/UnlockedQuestIds/CurrentQuestTaskId
  • World entry (V7 ready): push SyncPlayerSingleTask (story root 60004938, Accepted) + SyncCurrentTask(taskId=60004938) + SyncPlayerAllTask(CurrentTask=60004938) — client ควรเริ่ม intro รถไฟ/กรอกชื่อ/เลือกเพศเองเมื่อ current task = story root
  • TaskCatalogRepository พอร์ตแล้ว (ChainFrom/Counters/Name) พร้อมสำหรับ StoryFlow เต็ม
  • รอบหน้า: ถ้า intro เล่นถึงหน้ากรอกชื่อแต่ confirm แล้วค้าง → ต้องพอร์ต GameRouter.Quests.cs (container push) + StoryFlow.cs เต็มตามแผนด้านบน

🔍 27 ก.ย. (ต่อ 5): ผลจาก Player.log จริง (CBT8 trunk, ทดสอบ 03:01)

  • StoryClient รับ S011 ของเราแล้ว: CreateClientNode Nid 冲突 nid=1 old=S011PlayerNet new=StoryServer.Story.World.S011.S011PlayerNet — conflict+rebuild นี้อาจเป็น flow ปกติ (server-authoritative)
  • ผู้เล่นยืนยัน intro เล่นถึงเลือกเพศ MC ในรถได้ — แปลว่า intro flow ใช้ได้บางส่วนอยู่แล้ว บัคที่เหลือต้องระบุอาการเฉพาะ
  • AkxManagerSprint: Token ว่าง ×3 — น่าจะเป็น AI-chat service (ต้อง backend NetEase) ไม่ใช่คัทซีน
  • Metro got 0 gadget infos — AskMetroGadgetIds คืนว่าง (รถไฟไม่มี gadget info — ปรับได้ทีหลัง)
  • ทดสอบนี้ยังเห็น unimplemented AskInteractCmd — แปลว่าทดสอบด้วย build เก่า (ก่อน whitelist fix) — รอบหน้าทดสอบด้วย build ใหม่เท่านั้น

สิ่งที่ต้องทำต่อ (เรียงตามความคุ้ม)

  1. ผู้เล่นทดสอบด้วย build ใหม่ (whitelist ครบ + AI-task traffic) → รายงาน: รถวิ่งไหม / intro ติดตรงไหน / อาการเป๊ะ
  2. ถ้า intro ค้างหลังเลือกเพศ: พอร์ต GameRouter.Quests.cs (container push เต็ม) + StoryFlow.cs ตามแผน
  3. สกิลตัวละคร: เทียบ perSpiritViews กับ FightSkillConfig ราย spirit หาตัวขาด
  4. AskMetroGadgetIds คืน gadget จริงจาก RailStationConfig (เสริมประสบการณ์รถไฟ)

📦 27 ก.ย. (ต่อ 6): แผนดึง intro/content จาก CBT2 → CBT8 (NoAB injection)

หลักฐาน

  • client CBT8 ไม่เล่น intro คัทซีนเลย — intro content ถูกตัดออกจากชุด CBT8
  • client โหลด content เริ่มต้นจาก NoAB/NoABFull = ResourcePackage (log: "[ResourcePackage] index main/maps/depends path")
  • CBT2 NoAB: main=698 entries, CBT8 NoABFull main=1,260 entries — คนละชุด content จริง (CBT2 มี intro, CBT8 ถูกแทนที่)
  • CBT2 install: E:\Ananta_beta\Ananta\Ananta_Data\StreamingAssets\NoAB\{main,maps,depends,objects} / CBT8: ...\NoABFull\{เดียวกัน} + NoABFull.manifest (arraySize=133,398)

ขั้นตอน injection (session หน้า)

  1. Parse index ทั้งสอง ด้วย vault decoder (code/L50Index.cs — index v5/v8; โครงหัว: [u32 count][u32 count][u32 size][ffffffff]...) → ได้ entry list (hash/offset/len) ของ CBT2 NoAB และ CBT8 NoABFull
  2. Diff → CBT2-only entries = content ที่ CBT8 ตัดออก (intro คัทซีน/รถไฟ อยู่ในนี้)
  3. Decode ตัวอย่าง (L50Payload) ตรวจว่าใช่ intro จริง (ค้นคำว่า Timeline/Train/S011/Scene ใน payload)
  4. Inject 2 ทาง: a. ง่ายสุด: คัดลอกไฟล์ payload ของ CBT2 + เขียน NoABFull.manifest ใหม่ (ต่อท้าย entry array — รูปแบบ manifest ตาม code/L50Index.cs) b. ทางการ: CoreLib VFSBuilderBuildGroup สร้าง vfc ใหม่ (vault พิสูจน์แล้ว)
  5. ทดสอบ: เปิดเกม (ไม่ต้องรอ server) — client เล่น intro เองถ้า content ครบ

ข้อควรระวัง

  • NoABFull.manifest มี hash ตรวจ/ขนาด array — ผิดแม้บิตเดียว client อาจไม่ mount
  • CBT2 payload อาจอ้าง dependency ที่ CBT8 ไม่มี → ต้องเอา depends ของ CBT2 ตามไปด้วย
  • fastpatch.zip (server เราเสิร์ฟ) ไม่เกี่ยวกับ NoAB — ไม่กระทบ

🧩 สรุปโครง story ของ client (จาก dump)

  • 18 story worlds: S001(Player+Arrest), S002(Flee/Interact/Escort/Exam/Vehicle), S004(Concert), S005, S006, S009, S011(vehicle boarding), S019 — โค้ด story มีครบทุก build ไม่ได้หายไปไหน
  • intro cutscene = data-driven: scenario อยู่ใน TaskConfig chain (60004938) + timeline/scenario data — client มี engine ครบ รอ server ส่ง task/event ที่ถูกต้อง
  • ไม่มี dump เฉพาะ CBT2 — dump ที่มี (ULTIMATE + pc_dump_new) ตรงกับ CBT8 ทั้งหมด; ถ้าต้องการ dump CBT2 ใช้ GameAssembly.dll ของ E:\Ananta_beta\Ananta + Il2CppDumper สร้างเองได้
  • ไม่ใช่ปัญหาไฟล์: CBT2 blocks ⊂ CBT8 blocks, NoABFull ⊃ NoAB — content data อยู่ครบ ปัญหาอยู่ที่ story/event flow ฝั่ง server

สรุปประเด็น intro (สถานะปัจจุบัน)

  1. server push story root 60004938 (Accepted) + SyncCurrentTask แล้ว ✓
  2. client รับ S011 story node แล้ว ✓ (log: CreateClientNode rebuild)
  3. คัทซีนยังไม่เล่น → ขาด story event/command ถัดไปที่ server ต้องส่ง (เช่น scenario advance, cutscene trigger, dialogue events) — ต้องจับจาก Drmk StoryFlow หรือ log จริงเทียบ

ขั้นต่อไปที่แม่นที่สุด

ทดสอบด้วย build ใหม่ (whitelist ครบ) → เข้าเกม → เก็บ ทั้ง server log + Player.log → หา story command ที่ client รอ (StoryClientNetManager.ReceiveCommands จะโชว์ทุก command) → เทียบกับสิ่งที่ server ส่ง → เติม command ที่ขาด

✅ 27 ก.ย. (ต่อ 7): พอร์ต Quests.cs (container engine) เสร็จ — Build 0/0

  • GameRouter.Quests.cs (ใหม่): BuildTaskContainer/PushTaskContainerAsync (SyncPlayerAllTask จาก QuestStates จริง), AcceptQuestAsync (counter slots ตาม TaskConfig), SubmitQuestAsync (Submited + auto-advance NextTasks), ReportCounterAsync (all-full → submit เอง), handlers AskAcceptTask/AskAcceptAndSetCurrentTask/AskChangeTaskCounterValue/AskFinishTaskCounter/AskSubmitTask/AskDoGuide/AskFinishGuide
  • ลบ handler เควสเก่าซ้ำใน Endpoints.cs (AskAcceptTask/AskSubmitTask/AskFinishTaskCounter แบบง่าย) — World.cs อ้าง QuestEntry/QuestStateAccepted ตัวใหม่แล้ว
  • ตอนนี้ story chain 60004938 (intro) เดินได้ปลายทางถึงท้าย chain เมื่อ client รายงาน counter/submit

📱 27 ก.ย. (ต่อ 8): Social/Moments app — DTO + catalog + data พอร์ตแล้ว (รอ handlers)

  • พอร์ตแล้ว: SocialAppMethods4229938.cs (DTO โพสต์/คอมเมนต์ครบ) + SocialCatalogRepository4229938 + config 3 ไฟล์ (SocialMedia{,NPC,Comment}Config.json)
  • ค้าง: GameRouter.SocialApp.cs (506 ln) กับ PhoneAppContent.cs (419 ln) — อ้าง Drmk.Server.State.SessionState (SocialReadPosts/SocialLikedPosts/SocialPlayerComments) + config Gameplay.SocialApp — รอบหน้าพอร์ต handlers + เติม session fields (เก็บใน session.Items แทน state class ก็ได้)
  • whitelist เพิ่ม: AskMomentsPostSimpleInfos, AskMomentsPostInfos, AskMomentsUnreadMessage, AskMomentsHaveUnreadMessage, AskMomentsMarkRead, AskMomentsLikePost, AskMomentsSendCommentWithId, AskMomentsTapPostWithCount, AskServerGraffitoUrl, AskChangeHackerName, AskReadHackerNewPost, AskAcceptHackerPostTask

🌐 27 ก.ย.: วินิจฉัย "แปลภาษา dev panel ในเกมไม่เปลี่ยน" — พบว่าไม่เคย implement จริง

ตรวจสายครบ: translation.en.txt ✓ มี | private-server.json ไม่มี key | generator ไม่มี hook เลย (0 match) | fastpatch.zip สร้างโดย generator จึงไม่มี hook ฝั่ง client สรุป: ที่ "เคยทำ" มีแค่ไฟล์พจนานุกรม — Lua hook ไม่เคยถูกพอร์ตเข้า generator เลย

แผนแก้ (session หน้า — ตามลำดับ)

  1. อ่าน reference: Ananta.Proxy/proxy/tools/generate_client_config_patch.DRMK.reference.js (hook: env DRMK_DEV_MENU_DICT + โหลด dictionary + HookTable ~500 บรรทัด)
  2. พอร์ต hook เข้า generate_client_config_patch.js ของเรา (74KB — ห้ามทับ แก้จุดเดียว: สร้าง Lua hook ไฟล์ใหม่จาก dictionary + patch เข้า fastpatch)
  3. แหล่ง dictionary: config/localization/translation.en.txt (มีแล้ว) → generator อ่านแล้วฝัง
  4. fastpatch.zip ที่เสิร์ฟ = fastpatch_4229938.zip ข้าง server.js (สร้างจาก generator) — regenerate แล้ว client ต้องอ่าน Lua ใหม่ (log: "read lua bytes from fast patch" ต้องมีไฟล์ hook)
  5. ทดสอบ: console ต้องขึ้น dev-menu dictionary: 1352 entries, 20 rules

✅ 27 ก.ย. (ต่อ 9): fastpatch แปล dev panel สร้างสำเร็จ — พร้อมเทสต์

  • รัน generator ของ UI package ผ่าน shim (EN generator ชี้ config ของเรา) + NODE_PATH fengari + DRMK_DEV_MENU_DICT
  • ผล: Ananta.Proxy/proxy/public/fastpatch_4229938.zip (30KB) มี UidLayerPanelStore.lua 86.5KB ที่ฝัง dev-menu hook + dictionary (1,084 entries จาก translation.en.txt ของเรา)
  • คำสั่ง regenerate เมื่อไหร่ก็ได้: cd "Anangay asset/Ananta_UI_Translation_RU_EN/Drmk.Proxy/proxy/tools" && NODE_PATH="<proxy>/node_modules" DRMK_DEV_MENU_DICT="<ngp>/config/localization/translation.en.txt" node generate_client_config_patch.en.js (shim แก้ require เป็น absolute แล้ว)
  • ⚠️ Run-All ที่ rebuild fastpatch จาก generator ของเราจะทับ hook ทุกครั้งที่สตาร์ท — ต้องรันคำสั่งข้างบนซ้ำหลังสตาร์ท หรือรอบหน้า merge hook เข้า generator ของเราถาวร
  • ปล. dictionary เหลือ 1,084 entries (จาก 1,352 ตามที่คนทำไว้) — translation.en.txt ของเราอาจเป็นเวอร์ชันต่างจากของเขา

🚙 27 ก.ย. (ต่อ 10): แก้ traffic จาก feedback รอบทดสอบ — Build 0/0

feedback: รถแทบไม่ขยับ/บางคันขยับแล้วชนคันไม่ขยับ/หายกลางอากาศทั้งที่มองเห็น

  • หายกลางอากาศ = despawn ตามระยะที่คำนวณจากตำแหน่งจำลองของเรา (เพี้ยนเพราะ client ขับเองคนละเส้น) → รถ ClientDriven เปลี่ยนเป็นเก็บตามอายุ 5 นาที แทนระยะ
  • คันไม่ขับ = lane ที่ไม่มีทางต่อ (Next ว่าง) → ไม่เลือก spawn บน lane dead-end อีก
  • ชนกัน: คันที่ขับชนคันไม่ขับ — คันไม่ขับจะลดลงเมื่อ spawn เฉพาะ lane มีทางต่อ (ของเดิมที่เหลือจะหมดไปเอง)
  • (NPC ไม่มีอนิเมชั่น / Quest coming soon / Contacts ว่าง / CBT2 camera cutscene = คิวใหญ่ แยกทำรอบหน้า)

🔬 27 ก.ย. (ต่อ 11): ผลวิจัย opening trigger — คำตอบสุดท้ายของสายนี้

  • LoginManager.isFirstLogin / enterTutorial (Lua-exported) = ตัวตัดสิน intro ฝั่ง client — set จาก Lua login flow
  • LoginGateRouter ของ Drmk = เหมือนเราเป๊ะ (TryLogin → SyncRoleList(pid)) — ไม่ใช่ server
  • Story worlds 18 ตัวครบทุก build — ไม่ใช่โค้ดที่หาย
  • สรุป: intro เป็น client-side local state + story data ที่ server ส่งต่อ — client ที่เคยเล่น (local save บอกว่าเคยดู opening) จะข้ามเอง การบังคับให้เล่นใหม่ = ลบ local state ของ client หรือ push story task ให้ถูกจังหวะ (ทดสอบอยู่)
  • ปัจจัยที่ยังควบคุมได้จาก server และควรทดสอบ: story root task 60004938 (ใส่แล้ว) + story command stream (SyncStoryCoreClientInfo/ServerInfo — โครงมีใน dump)

คิวงานถัดไป (เรียงแล้ว)

  1. ทดสอบ build ปัจจุบัน (traffic AI-task + quest container + whitelist + fastpatch แปลภาษา)
  2. ถ้า intro ยังไม่เล่น: เทียบ StoryClientCommand stream จาก log (ReceiveCommands) กับที่ server ส่ง
  3. สกิลครบทุกตัว (COMBAT-CATALOG vs FightSkillConfig)
  4. Contacts app (NETWORK OF CONTACTS) — ผูก NPC จริงเข้า contact data
  5. CBT2 camera cutscene — ตามแผน NoAB injection

✅ 27 ก.ย. (ต่อ 12): audit "no dummy" รอบ 1 — Build 0/0

  • AskSceneDeviceRecordValue → เพิ่ม mirror SyncSceneDeviceValueChange (client apply ค่าจริงจาก Sync ไม่ใช่แค่ ack)
  • AskReadCommodities → handler จริง: push inventory ของร้านจาก ShopCommodityCache (เดิม fallback)
  • AskBuyCommodityToBag / AskBuyCommoditiesToBag → ack แบบ sandbox (เศรษฐกิจไม่จำกัด ตาม design)
  • placeholder ที่เหลือ (ทำไม่ได้จริงโดยไม่มี backend/document):
    • GadgetEntityInfo/DestructibleInfo/TaskEventInfo DTO stubs — list ว่าง by design (server ไม่ส่ง entity ใน grid stream)
    • Spoon port 0 — ตรงกับพฤติกรรม fallback ของ Drmk เอง
    • สกิลบางตัว / intro full flow / CBT2 cutscene — ตามแผนคิวใหญ่
  • ⚠️ server เดิม (Ananta.App.exe) ยังรันอยู่ล็อก DLL — ปิดแล้ว build ใหม่ก่อนทดสอบ

✅ 27 ก.ย. (ต่อ 13): merge dev-menu hook เข้า generator ถาวร — Build 0/0

  • ยก template ทั้งบล็อก (UID override + dev-menu hook + dictionary.lua + TranslateCore/RULES/ResolveRoot) จาก generator ของ UI translation package เข้า generate_client_config_patch.js ของเรา
  • เพิ่ม consts: showDiagnostics/languageOverride (+ buildLuaDictionary + dictPath resolution อ่าน ui.devMenuDictionary)
  • ผล: Run-All ที่ rebuild fastpatch ทุกสตาร์ท ตอนนี้ได้ patch แปล dev panel มาด้วยเสมอ — ไม่ต้องรันคำสั่งแยกอีก
  • dictionary = config/localization/translation.en.txt (1,084 entries) เปลี่ยน/เพิ่มคำแปลได้ที่ไฟล์นี้แล้ว regenerate
  • ตรวจแล้ว: fastpatch_4229938.zip มี hook ✓ (Lua 72.7KB)

⚠️ 27 ก.ย. (ต่อ 14): feedback รอบทดสอบ AI-task — สรุปสาเหตุ + คิวแก้

1. Reconnecting บ่อย — สงสัย handler ใหม่ตัวใด parse พังแล้ว exception ลอยขึ้น session (GetArgs ที่ shape ไม่ตรง) หรือ client พังที่ packet ใหม่ → ต้องดู server log ตอน reconnect (มี exception ไหน) + Player.log 2. รถไม่ขับ/ทับกัน/หาย — AI-task บางคันไม่ทำงาน (waypoint chain สั้น/ทิศผิด), spawn ทับกัน (ยังไม่มี min-gap), หาย = churn ยังเร็ว 3. ไม่มีคนขับ — NpcAdd ส่งแล้วแต่ NPC ไม่ render ในรถ (อาจต้องมี agent sync info แบบ static NPC หรือ behavior task ผูกผิดจังหวะ) 4. NPC อนิเมชั่นมั่ว — AmbientPoiActions สุ่ม 1-10 ไม่ตรงบริบท → ควรใช้ idle ตรงบริบท/ไม่สุ่ม 5. Quest coming soon + ไม่มี NPC รับเควสตามทาง/หมุด — ต้อง spawn quest NPC ตามตำแหน่ง task + push map marker (TaskConfig มีข้อมูล) — งานใหญ่ 6. Cutscene trigger ไม่ได้ — ผูกกับ story command stream (SyncStoryCoreClientInfo) ที่ยังไม่มี flow เต็ม 7. Talent/Contacts — คิวหลัง

ลำดับแก้ที่คิดว่าคุ้มสุด (รอบหน้า)

  1. กัน session ตายจาก handler exception — ครอบ try/catch ทุก handler ใหม่ + ตรวจ server log ตอน Reconnecting ว่า exception ตัวไหนยิงซ้ำ
  2. รถ: สลับทั้งระบบเป็น AI-task ล้วน — spawn เฉพาะ lane มี chain + min-gap ระหว่างรถ (แบบ Drmk line 814) + ลด churn
  3. คนขับ — ศึกษา NpcAdd ของ Drmk ว่าต้องมี AgentSyncClientInfo ประกอบไหม (แบบ static NPC ของเรา)
  4. NPC เดินรับเควส + หมุด — spawn quest NPC จากตำแหน่งใน TaskConfig/TaskEvent + marker push

✅ 27 ก.ย. (ต่อ 15): ปิดคิว 6 ข้อจาก HANDOFF — Build 0/0

ทำคิวงานถัดไปครบ 6 ข้อ (build ผ่าน 0 error 0 warning แล้ว — รอผู้เล่นทดสอบในเกม)

1. Reconnecting — session ไม่ตายจาก exception อีกต่อไป

  • ต้นตอที่เจอ (จากโค้ด ไม่ใช่แค่สมมติ): TcpServer.HandleClientAsync ปิด session ทันทีถ้ามี exception หลุดจาก dispatch โดยมี 2 ช่องทางหลุด: (a) RpcPacketReader.Parse พังแล้ว rethrow (RPC_PARSE_FAILED) (b) fallback reply ตอน handler exception ตัวมันเอง throw ซ้ำ
  • แก้: RpcRouter.DispatchAsync — parse พัง = log + ข้าม frame (session อยู่ต่อ); ครอบ fallback reply ด้วย try/catch ชั้นใน; RpcFrameDispatcher ครอบ router.DispatchAsync อีกชั้น (last-resort) — exception ที่เหลือเสียแค่ 1 frame ไม่ใช่ทั้ง session
  • log ใหม่: rpc parse failed ... — session kept / dispatch survived frame ... (session kept) — เจอบ่อย ๆ แปลว่ามี packet ที่ parse พังต้องตามแก้ DTO ต่อ

2. รถ AI-task — chain + min-gap + churn

  • spawn เฉพาะ lane มี chain จริง: เพิ่ม ChainHops() — ต้องมี lane ต่ออย่างน้อย 3 hops (เดิมเช็คแค่ Next.Count>0 เจอ lane ปลายทางแบบมี 1 ทางต่อสั้น ๆ รถก็นั่งแห้ง) + SpawnMinLaneLength 15→30 ม.
  • min-gap (แบบ Drmk): same-lane gap 25→30 ม. + junction clearance — ถ้ารถคันอื่นอยู่บน next lane ห่างปากทาง <15 ม. ไม่ spawn (เดิมเกิดมาทับท้ายรถพอดี) + world overlap 2.5→4 ม.
  • churn: lifetime รถ ClientDriven 5→15 นาที (อาการ "หายกลางอากาศ" คือ lifetime recycle เดิมตัดรอบสั้นไป — ตอนนี้ recycle ที่ 900 วิ)

3. คนขับรถ — เจอสาเหตุจริง: NpcFormworkId ผิดชนิด

  • สาเหตุ: เราส่ง spawnEntry.DriverNpcType (= 45200091 จาก VehicleSpawnConfig — เป็น persona id) เป็น NpcFormworkId แต่ client resolve field นี้กับ AgentConfig catalog (Drmk ยืนยันใน log ตัวเอง: "司机模板池 (AgentConfig,非 persona)") → id ไม่เจอ = NPC ถูก drop เงียบ ๆ = ไม่มีคนขับ
  • แก้: คนขับ/ผู้โดยสารถูกสุ่มจาก NpcCatalog4229938 (AgentConfig.json ที่มีอยู่แล้ว) เฉพาะ category=citizen ที่มี model
  • ครอบจักรยาน: คัดลอก AetherNpcBehaviorTaskDefineConfig.json (391 records) จากชุด Drmk มาใส่ ClientData/4229938/Configs/ — ก่อนหน้านี้ไฟล์นี้ไม่มี ทำให้ BehaviorTaskCatalog.VehicleNpcTaskId = 0 → ไม่เคยส่ง SyncDoAetherAgentBehaviorTaskDef ให้คนขับเลย ตอนนี้ task id resolve ได้ (AnimState=3 มี 46 rows)

4. NPC อนิเมชั่น idle ตรงบริบท

  • เดิม AmbientPoiActions = [1..10] สุ่มรวม 走路 (เดิน原地)/跑步 (วิ่ง)/受惊 (ตกใจ) — อนิเมชั่นพวกนี้บนคนยืนดูพัง
  • ตอนนี้เลือกจาก pool ที่สร้างจากชื่อใน UrbanDiversityPOIActionConfig: คนเดินถนน = pool 站姿* (ยืน: โทรศัพท์/มองไปมา/พิงผนัง/ปรบมือ ฯลฯ) + idle; คนในร้าน = 站姿 + 商品/售货机/购物 (เลือกของ/ใช้ตู้ vending)

5. เควสตามทาง + หมุด + Contacts app

  • Quest NPC + หมุด: ระบบใหม่ใน GameRouter.Quests.cs — ตอน accept task (หรือ chain advance) เซิร์ฟเวอร์เสก quest NPC 1 ตัวต่อ session ยืนข้างหน้าผู้เล่น ~10 ม. เยื้องขวา 4 ม. (citizen จาก AgentCatalog, EnableInteract ครบ = กด F คุยได้) และ push SyncTrackGPS (63861463) = หมุดแผนที่ (writer ตรงจาก RPCSerializeAuto.lua: TeamTrackGPS{TrackPosition, TrackGPSId, TrackGPSType} — ตัวนี้ยังไม่เคยมีใครส่ง ทั้งเราและ Drmk) — NPC ตัวเดียวกัน "เดิน" ไปจุดใหม่เมื่อเควสเปลี่ยน (ไม่รกแมพ)
    • DTO ใหม่: TeamTrackGPS4229938 ใน TaskAndDialogMethods.cs
    • ถ้า accept ตอน world-entry (transform ยังไม่พร้อม) จะข้ามแล้วจับตอน accept ถัดไปในเกม
  • Contacts app (NETWORK OF CONTACTS): แอปนี้อ่านจาก push เท่านั้น (ไม่มี C2S fetch — ตรวจ Lua delegate ครบ) → พอร์ต AddSpiritPhoneInfos (64199774) จาก Drmk (PushSpiritHandbookAsync) ยิงตอน LoginGame หลัง SyncPlayerInfo: 2 เบอร์ summon + 24 คอนแทกต์ชื่อ NPC จริง (AgentConfig → NameTranslator EN) ต่อทุกตัวละคร + implement C2S AskPhoneAddContact/EditContact/DeleteContact แบบ sandbox (เก็บใน session แล้ว re-push ทั้งเล่ม) + group/option handlers เป็น ack — whitelist เพิ่ม 8 ids แล้ว
  • DTO ใหม่: Ananta.RpcTypes/.../Game/PhoneMethods.cs

4b. สกิล (audit — ไม่ต้องแก้โค้ด)

  • เทียบ FightSkillConfig (88 styles) vs FightSpiritConfig (109 spirits): style ที่ผูก spirit ทั้ง 8 ตัว resolve ได้หมด / AllowsSkill รับทุก skill ใน SkillConfig อยู่แล้ว / ทุก style ปลดล็อก (UnlockedStyleIds) — ไม่เจอตัวขาดระดับข้อมูล ถ้ายังเจอสกิลหายในเกมให้เอา log [COMBAT] reject unknown-skill มา (ตอนนี้ไม่ควรขึ้นแล้ว)

6. CBT2 NoAB injection — parse + diff สำเร็จ (ขั้น decode/inject ยังเปิด)

  • เครื่องมือใหม่: Anangay asset/VaultTool/noab_diff.py + ผลที่ cbt2_only_main_names.json
  • ยืนยันโครง main: header [u32 count][u32 count][u32 size][0xFFFFFFFF] — CBT2 = 698 entries, CBT8 = 1260 (ตรง devlog เดิมเป๊ะ); ข้างในเป็น catalog ชื่อ asset แบบ ASCII อ่านได้
  • diff main: CBT2-only = 3,253 ชื่อ รวม scene 17 แห่ง เช่น sc_level1206_c_TrainBossBattle.unity, sc_room_*, raid หลายด่าน + ชุด QTE TimelineScript ครบ (Tap/Swipe/Shake/LongPress QTE + DodgeTrain) = ยืนยันเนื้อ intro/QTE รถไฟอยู่ใน CBT2 จริง
  • ยังติด: maps/depends เป็น native ResourcePackage format (ไม่มี ASCII เลย — records [u32 hash][u32 off][u32 len] ต้อง RE จาก UnityPlayer ResourcePackage_Bindings) และ NoABFull.manifest = 4B count (133,398) + record 22 ไบต์/แถว (หารลงตัวเป๊ะ: 2,934,756/133,398=22) แต่ยังไม่ยืนยัน semantics ของ field — ขั้น inject (คัดลอก payload + เขียน manifest ใหม่) ต้อง decode ตรงนี้ก่อน ไม่งั้นเสี่ยง mount ไม่ขึ้นทั้งเกม

บทเรียนใหม่ (เพิ่มในกฎเหล็ก)

  • NpcFormworkId ของ SyncAetherAIVehicleNpcAdd ต้องเป็น AgentConfig id เท่านั้น — ห้ามใช้ persona id จาก VehicleSpawnConfig (45200091/45200093) — client drop เงียบ
  • AetherNpcBehaviorTaskDefineConfig.json ต้องอยู่ใน Configs เสมอ — ถ้าหาย driver/passenger จะไม่มี behavior task (ยืนเฉย) แม้ formwork ถูก
  • handler ใหม่ whitelist ครบ แต่ client อาจส่ง C2S ที่ DTO เราไม่มี — parse พังตอนนี้ไม่ฆ่า session แล้ว เห็น rpc parse failed ใน log = เก็บ method id นั้นมาเพิ่ม DTO

วิธีทดสอบ (relog รอบเดียวเช็คได้หมด)

  1. เสถียรภาพ: เล่น 10 นาที ดูว่า Reconnecting หาย (ถ้ายังมี — ส่ง server log มาดูบรรทัด session kept/exception)
  2. รถ: รถวิ่งต่อเนื่องกว่าเดิม ไม่หายตรงหน้า (lifetime 15 นาที) ไม่เกิดทับท้ายรถที่แยก คนขับ+ผู้โดยสารต้องเห็นในรถทุกคัน
  3. คนเดิน: อนิเมชั่นต้องเป็นท่ายืนธรรมชาติ (โทรศัพท์/มองรอบ/พิงผนัง) — ไม่มีวิ่ง原地/ตกใจอีก
  4. เควส: รับเควส → NPC quest giver โผล่ข้างหน้า + หมุดบนแมพ กด F คุยได้; ส่งเควสแล้ว NPC เดินไปจุดใหม่ของเควสถัดไป
  5. โทรศัพท์: แอป NETWORK OF CONTACTS ต้องมีรายชื่อ (142857/206337 + ชื่อ NPC ภาษาอังกฤษ ~24 ราย) และเพิ่ม/ลบคอนแทกต์ได้