diff --git a/AGENTS.md b/AGENTS.md index e911f8e..cc2fde1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,3 +16,4 @@ - SDK และ game server ที่เปิดผ่าน launcher ต้องแสดงคอนโซลและ log ให้ผู้ใช้เห็น ห้ามซ่อนหน้าต่างหรือ redirect จนคอนโซลว่าง เว้นแต่ผู้ใช้ขอเอง เมนูแบบ interactive ปิดหลังทำรายการเสร็จ 5 วินาที โดยไม่ปิดเซิร์ฟเวอร์ตามไปด้วย - งาน progression/EX ต้องรักษาเส้นทาง Battle ที่ยืนยันกับ Client แล้ว การซิงก์ EX ส่งเฉพาะ avatar/material ที่เปลี่ยน ห้ามล้าง inventory ทั้งชุดเพื่อซิงก์สวิตช์ และให้นำการเปลี่ยนจากภายนอกเข้า Client หลังจบ Battle - ตัวอ่าน persistent ต้องไม่เขียนไฟล์กลับ การบันทึกจาก session เก่าต้องไม่ทับค่าที่ API เปลี่ยน และต้องเก็บ EX false ได้ ห้ามปลด EX อัตโนมัติจากการขึ้น E6 หรือปลดทุกตัวเมื่ออ่าน request ID ไม่ได้ +- สคริปต์วิเคราะห์ Client ต้องไม่สแกนทุก type/method ของ assembly บน main thread ผ่าน heartbeat ระหว่างเล่น ให้ตรวจเฉพาะ type ที่ระบุและจำกัดงานต่อเฟรม เก็บ probe นอก scripts/ เมื่อไม่ได้ใช้งาน เพราะทุกไฟล์ .lua ใน scripts/ ถูกส่งเข้า Client อัตโนมัติ diff --git a/README.md b/README.md index 1932863..db0524d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ SDK และ game server เปิดหน้าต่างคอนโซล | ค่าใน config | Environment override | ใช้สำหรับ | | --- | --- | --- | +| `player_uid` | `GAC_PLAYER_UID` | UID ผู้เล่น ใช้ร่วมกันทั้ง SDK และ game server; ต้องเป็นจำนวนเต็ม 1–4294967295 | | `client_version` | `GAC_CLIENT_VERSION` | เวอร์ชันที่แสดงใน log/API | | `game_host`, `game_port` | `GAC_GAME_HOST`, `GAC_GAME_PORT` | ที่อยู่ bind ของ game server | | `sdk_host`, `sdk_port` | `GAC_SDK_HOST`, `GAC_SDK_PORT` | ที่อยู่ bind ของ SDK server | @@ -145,7 +146,8 @@ go test ./... | Calyx | ผู้ใช้ยืนยันว่าเข้า Battle ได้แล้ว 17 ต.ค. หลังแก้เส้นทาง BattleInfo | | ตีมอนแล้วค้างหน้า Weakness | ผู้ใช้ยืนยัน Stagnant Shadow ที่เรียกมอนออกมาให้ตีเข้าสู้ได้แล้ว 17 ต.ค.; log มี `PVEBattleResult` ส่วนมอนทั่วไปยังไม่ได้ยืนยันแยก | | เปิดหน้าตัวละครแล้วค้าง | ผู้ใช้ยืนยันว่าหายหลังเปิดเกมใหม่; ไม่พบ PlayerBoard/Inventory error ใน log รอบใหม่ | -| วาร์ปแมพใหม่ | ปรับลำดับ response/notify และข้อมูล scene ตาม upstream; ยังต้องยืนยันปลายทางในเกม | +| วาร์ปแมพใหม่ | ผู้ใช้ยืนยัน Starfroster/Ferry Relay Station และ Moment of Joy วาร์ปได้แล้ว 17 ต.ค.; กรองกลุ่มที่ Client ระบุ `IsDelete=true` ด้วย SceneMetadata จากตาราง Client | +| Endgame | ตรวจข้อมูลทุกด่านที่ติดตั้ง 1,747 รายการห้อง/node บน 34 floors; เพิ่มจุดเกิดจริง, PF 4 cycles และ flow เปลี่ยนทีม/ห้อง/สรุปผลสำหรับด่านหลัก, Tierce และ Peak แล้ว รอผลใน Client ดู [ผล audit และข้อจำกัด](docs/endgame-audit.md) | | `SceneCastSkill` | ใช้รายการ monster entity ที่ server สร้างเพื่อตรวจเป้าหมายก่อนส่ง battle | ข้อมูลล่าสุดที่ตรวจในซอร์สและเอกสาร: **17 ตุลาคม 2026** @@ -240,3 +242,54 @@ python tools/audit_protocol_handlers.py "" ## ที่มา พอร์ตจาก `GaC-hkrpg-ps` ภาษา Rust โดยใช้แนวทางและข้อมูลประกอบจาก [HonkaiSlopRail/pearl-sr](https://git.xeondev.com/HonkaiSlopRail/pearl-sr) ภาษา Zig การแก้ใน Go ต้องตรวจเทียบทั้ง handler, payload และเวอร์ชัน Client ที่ใช้งานจริง + +### Traces: หน้าตัวละครและ GaC-Srtool + +- `AvatarPathData.avatar_path_skill_tree.point_id` สำหรับหน้า Traces ใช้เลข `AnchorType` จาก `AvatarSkillTreeConfig.json` ส่วน `BattleAvatar.skilltree_list.point_id` ใช้ `PointID` เต็ม อ้างอิง PearlSR commit `005d2c12db4781071f4b5b68e8f364587886a710` ใน `avatar_mgr.createSkillTree` และ `battle_mgr.createBattleAvatar` +- แปลง `data.skills` ในเซฟเป็นเลข Anchor เฉพาะตอนส่งข้อมูลหน้าตัวละคร ไม่เปลี่ยน ID ในเซฟหรือ Battle และไม่บังคับอัป Traces ให้เต็ม +- GaC-Srtool บันทึก Traces ผ่านการส่ง `freesr-data` ทั้งชุด ไม่ใช่ `/api/freesr/avatars` ซึ่งรับเฉพาะข้อมูลทั่วไปของตัวละคร โค้ดเว็บที่แก้ในโปรเจกต์ GaC-srtool เพิ่มพารามิเตอร์ `closeModal = true` ให้ `saveAvatarDetails` เพื่อแก้ ReferenceError ก่อนถึงขั้นส่งเซฟ ต้องใช้เว็บฉบับที่มีการแก้นี้ด้วย +- การแก้เซฟจากเว็บถูกนำเข้าเกมโดย watcher; ระหว่าง Battle จะรอจนจบก่อนส่ง sync การยืนยันผลหน้า Traces และการบันทึกจากเว็บยังต้องตรวจใน Client + +### Technique points และอาหาร + +- `resources/TechniqueRules.json` เก็บค่า MP, cap ของทีม, ค่าใช้ Technique ตามตาราง `MazeSkill` ของ Client และอาหารที่ฟื้น MP โดยไม่มีผลลด HP มี revision ต้นทางและ hash ของ GameAssembly ที่อ่านข้อมูล +- Phainon / Himeko Nova ใช้ cap 7 ตาม `lineup_mgr.buildLineup` ของ PearlSR revision ที่ระบุใน resource ตัวโหลดตรวจสมาชิกจาก **ค่า AvatarID ในช่องทีม** ไม่ใช่ key ของช่อง ไม่มีการบวก cap ทั้งสองตัวทับกันตาม upstream +- แจกอาหารใน resource ชนิดละ `initial_food_count` หนึ่งครั้งเมื่อเปิดกระเป๋า บันทึกจำนวนคงเหลือใน `persistent.technique_food_stock` กินด้วย `UseItem` หรือกินอัตโนมัติเมื่อแต้มไม่พอใช้ Technique; ห้ามกินระหว่าง Battle +- `persistent.auto_technique_food` เปิดโหมด auto; ค่าเริ่มต้นมาจาก `auto_food_default` ใน resource ปิด/เปิดได้ผ่าน `POST /api/technique` ด้วย `{"auto_food":false}` / `{"auto_food":true}` และอ่านสถานะด้วย `GET /api/technique` ใช้ SDK URL จาก config ตัวเฝ้าดูนำสวิตช์ใหม่เข้า session หลัง Battle โดยไม่ล้าง inventory +- MP ที่ใช้แล้วเก็บใน session; เริ่มห้อง Challenge ใหม่คืนค่าเริ่มต้นตาม resource การเปลี่ยนตัวละคร/วาร์ปไม่เติม MP เต็มโดยอัตโนมัติ +- Technique ที่ไม่เปิด Battle อ้างอิง `IgnoreBattle` และ `getBattleType` ของ PearlSR ตอบผล `NO_BATTLE` ให้มอนที่โดน ไม่บังคับเข้า Battle จากการแปะ debuff + +สถานะ: ผู้ใช้ยืนยัน **PF ปกติเข้าสู้และเปลี่ยนห้องได้** ก่อนรอบแก้ Technique/อาหาร ส่วนรอบนี้ยังต้องยืนยัน Traces, แต้ม, อาหาร/auto และ Starward ห้อง 3 จาก Client ห้ามถือว่า build ผ่านเป็นผลยืนยันการเล่น + +### Starward: ทีมและคะแนนที่เซฟไว้ + +- ผู้ใช้ยืนยันว่า PF Starward เล่นจบครบ 3 ห้องได้แล้ว +- เก็บทีมและบัฟรายห้องใน `persistent.tierce_progress` ตอนตั้งทีม/เริ่มเล่น อ่านกลับใน GetChallengeTierceData และเริ่มห้องที่เลือกด้วยทีมของห้องนั้น ไม่ใช้ทีมแรกแทนเมื่อเล่นแยกห้อง +- บันทึกคะแนนจริงจาก PVEBattleResult แยกแต่ละ node พร้อมผลที่ดีที่สุด การลองห้องเดียวใช้ผลที่เซฟของห้องอื่นประกอบ; การเล่นครบชุดเริ่มคะแนนของรอบใหม่จากศูนย์ +- `resources/ChallengeTargetConfig.json` เป็นข้อมูลเกณฑ์จาก Client: TOTAL_SCORE ใช้ตัดสินดาวสำหรับ PF/AS Starward พร้อมส่ง FinishedTargetList ทั้งหน้าสรุปและข้อมูลที่อ่านกลับ คะแนน 97,000 ได้เกณฑ์ PF 60,000 / 75,000 / 90,000 แต่ยังไม่ถึงเกณฑ์พิเศษ 99,000 +- รอบนี้ยังไม่เพิ่มการแจก reward, การจัดอันดับออนไลน์ หรือการประเมิน target ประเภท ROUNDS_LEFT/DEAD_AVATAR ของ MoC ส่วนข้อมูลประวัติ Endgame ปกติ/Peak เดิมยังมีรายการสำเร็จรูปตามข้อจำกัดในเอกสาร audit +- ต้องตรวจใน Client ว่ากลับมาแล้วทีมยังอยู่ เลือกเล่นห้องเดียวได้ และหน้าสรุปดาวตรงกับคะแนน Build ไม่ใช่การยืนยันผลเล่น + +### วาร์ป MoC จากสมุดผ่านฉากรถไฟ + +แก้ GetPamSkinData ที่เคยส่งสกินปอมปอม ID0 ซึ่งไม่มีในตาราง Client และทำให้ NPC โหลดล้ม อ่าน default/รายการที่ปลดจาก ProfileConfig และตรวจ ConfigEntityPath กับ PamSkinConfig.json แทน ค่าต้นทางมี revision/assembly hash ตรวจสอบได้ ยังรอผู้ใช้ยืนยันการวาร์ปรอบแก้สกินนี้ + +### Preset ทีมในเกม + +- เปิดไว้ 20 ช่องผ่าน `resources/TeamPresetConfig.json` (`preset_count` เป็นนโยบายจำนวนทีมที่ผู้ใช้ขอเพิ่ม; ขอบเขตสมาชิก 4 และชื่อ 14 ตัวอักษรอ้างอิง Client) +- GetAllLineupData ส่งรายการครบพร้อม index ปัจจุบัน รองรับสลับทีม ตั้งชื่อ เพิ่ม/ถอด/สลับสมาชิก และ MarkPresetLineup สำหรับทีมโปรด/Quick Lineup +- บันทึกใน `persistent.team_presets` และ `active_team_preset` เก็บ `lineups` เดิมเป็นสมาชิกทีมที่กำลังใช้อยู่เพื่อใช้กับ API เดิม ไม่เขียนไฟล์ตอนอ่านเพื่อสร้าง preset เปล่า +- เริ่มใช้ด้วยทีมเดิมในช่องแรก ช่องอื่นยังว่างจนจัดสมาชิกครั้งแรก จากนั้นเลือกทีมที่เซฟไว้ได้โดยไม่ต้องจัดใหม่ทุกครั้ง ปฏิเสธการสลับไปทีมว่างและการแก้ทีมระหว่าง Battle +- เมื่อมีการเซฟ preset การสู้ทั่วไป/Calyx ใช้สมาชิกทีมที่เลือก แต่คง stage/waves จากข้อมูลเดิม ห้อง Endgame ใช้ทีมประจำห้องของ ChallengeRun +- Build ผ่านยังไม่ยืนยันว่า UI ของ Client แสดงครบ 20 ช่องและใช้งาน Quick Lineup ได้ทุกหน้า ต้องตรวจเพิ่มในเกม การจัดทีม Endgame โดย request ExtraLineupType ยังคงเป็นคนละเส้นทางกับการแก้ preset ทีมเดินโลก + +### Freecam: คีย์บอร์ด + +- `Tab`: เปิด/ปิด Freecam; ลูกศร: เลื่อนกล้อง; `I/K`: ก้มเงย; `J/L`: หันซ้ายขวา +- `Page Up` / `Page Down`: ขึ้น/ลง; `+` / `-`: เพิ่ม/ลดความเร็ว +- Freecam ไม่อ่านเมาส์และไม่ใช้ `E/Q` คุมกล้อง เพื่อให้กดสกิลระหว่างสู้ได้โดยกล้องไม่ลอยขึ้นลง ปุ่มเมาส์ยังทำงานตามคำสั่งของเกมตามปกติ +- สคริปต์อัปเดตผ่าน heartbeat โดยไม่ต้อง build หรือรีสตาร์ตเซิร์ฟเวอร์ การใช้งานใน Fight ยังรอยืนยันจาก Client + +### Hide UI + +`Backslash` ซ่อนเฉพาะ HUD ที่ระบุชื่อไว้ใน `scripts/hide_ui.lua` เช่น HUD เดินโลก/ต่อสู้ ตัวเลขลอยและ waypoint โดยไม่ปิด UIRoot, กล้อง UI หรือ Canvas ทั้งฉาก จึงเหลือหน้าตัวละคร หน้ารายละเอียด EX Eidolon และหน้าต่างรางวัลไว้ ปิดโหมดแล้วคืนค่า CanvasGroup เดิมของ HUD ที่แก้ ถ้าเคยใช้สคริปต์ซ่อนทั้ง UI รุ่นเก่า แนะนำเปิดเกมใหม่หนึ่งครั้ง เพราะรุ่นเก่าไม่ได้เก็บค่า alpha/interactable เดิมทั้งหมดไว้ ผลหน้า EX ยังรอยืนยันจาก Client diff --git a/cmd/gameserver/main.go b/cmd/gameserver/main.go index b7c7df1..b14816e 100644 --- a/cmd/gameserver/main.go +++ b/cmd/gameserver/main.go @@ -14,6 +14,14 @@ func main() { fmt.Fprintln(os.Stderr, err) os.Exit(1) } + if _, err := common.LoadTechniqueRules(); err != nil { + fmt.Fprintln(os.Stderr, "technique resources:", err) + os.Exit(1) + } + if _, err := common.LoadTeamPresetRules(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } fmt.Println("============================================================") fmt.Printf(" [GaC hkrpg GO] Gameserver (Client %s)\n", cfg.ClientVersion) fmt.Printf(" Listening on UDP %s:%d (KCP Gateway)\n", cfg.GameHost, cfg.GamePort) diff --git a/docs/endgame-audit.md b/docs/endgame-audit.md new file mode 100644 index 0000000..1c62019 --- /dev/null +++ b/docs/endgame-audit.md @@ -0,0 +1,79 @@ +# Endgame audit — 17 ตุลาคม 2026 + +## ตรวจข้อมูลทุกห้องใน resource ชุดที่ติดตั้ง + +อ่าน RuntimeFloor และ RuntimeGroup ผ่าน GameCoreConfigLoader ของ Client แบบอ่านอย่างเดียว ครอบคลุมทุก MapEntrance ที่ ChallengeMazeConfig, ChallengeMazeTierceConfig และ ChallengePeakConfig อ้างถึง ไม่ได้สั่งเข้าแมพหรือแก้ actor/save ใน Client ข้อมูล SceneMetadata มี SHA-256 ของ GameAssembly และ capture ที่ใช้ + +| โหมด | รายการห้อง/node ที่ตรวจ | +| --- | ---: | +| Forgotten Hall / Memory / MoC | 1,268 | +| Apocalyptic Shadow | 184 | +| Pure Fiction | 224 | +| Tierce / Starward ห้องที่สาม | 16 | +| Anomaly Arbitration / Peak | 44 | +| Peak Hard | 11 | +| รวม | 1,747 | + +มี 843 ด่านหลัก, 16 ด่าน Tierce, 44 ด่าน Peak และ Hard variants 11 รายการ ใช้ 37 entrances บน 34 floors นับรายการข้อมูลที่อ้างถึง ไม่ใช่จำนวนห้องกายภาพที่ต่างกันทั้งหมด SceneMetadata ยังเก็บอีกสอง floors ของ teleport ที่ผู้ใช้ยืนยันแล้ว รวมเป็น 36 floors + +`tools/audit_endgame_resources.py` ตรวจ: + +- entrance อ้างถึง floor จริงและมี metadata กลุ่มครบ +- กลุ่มมอนเป็น Server group ที่ยังไม่ถูกลบ +- มี start anchor ของ floor และตำแหน่ง monster instance ตาม ConfigList +- ตัวโหลด Go มี scene monster source ของกลุ่มนั้น +- event ของด่านมี StageConfig และทุก wave มีมอน + +ผล audit **ไม่พบข้อมูลขาดตามเงื่อนไขข้างต้น** รันซ้ำได้ด้วย `python tools/audit_endgame_resources.py --output ` เครื่องมือนี้อ่านข้อมูล ไม่ใช่การลองเล่นทุกห้องในเกม + +## ปัญหาที่พบจากการลอง PF + +- Battle capture ของด่าน 30326041 ส่ง rounds_limit=0 เพราะ PF ไม่มี ChallengeCountDown ในตาราง แล้ว Go ไปใช้ cycle_count=0 ของ custom battle ผลใน Client จบหลัง cycle แรก +- ย้าย mode round limits ไป resources/BattleRules.json: PF 4, AS/Default 30 ที่มาคือ PearlSR revision `005d2c12db4781071f4b5b68e8f364587886a710`, `ChallegeStageManager.createChallegeStage` ไม่ใช้ custom cycle เป็น fallback ของ Endgame +- PF ธรรมดาส่งสอง LineupInfo ด้วย ExtraLineupType เดียวกัน แก้ทีมสองเป็น enum LineupChallenge2 และเก็บทั้งสองทีมใน run +- PVEBattleResult เดิมตอบรับอย่างเดียว ไม่มีการเปลี่ยนห้องหรือแจ้งผล Challenge เพิ่ม run state ใน session พร้อม score/rounds ที่ Client ส่งจริง และป้องกันผลซ้ำหรือ stage ไม่ตรงกับ battle ที่ active +- ด่านหลักสลับทีม/ห้องที่สองผ่าน SyncLineupNotify, ChallengeLineupNotify และ EnterSceneByServerScNotify แล้วแจ้ง ChallengeSettleNotify เมื่อจบ run +- Tierce ใช้ OnStageSettle/OnBigSettle และ request StartNext/Restart พร้อม CurTierceChallenge ปัจจุบัน เก็บทีมกับ buff แยกแต่ละ session ไม่ใช้ cache รวมทุกผู้เล่น +- Peak เปลี่ยนจาก arena/level/monster ที่กำหนดตายตัวเป็น Entrance, MazeGroupID, NpcMonsterIDList, EventIDList และ TagList ของ resource; Hard ใช้ HardEventIDList/HardTagList และสวิตช์ของ group นั้น เพิ่ม current-state/settle/confirm handlers +- ออกจาก Challenge ล้าง run และกลับไปใช้ custom battle เดิม ไม่บันทึกทีม/wave ของ Endgame ทับ Freesr/persistent + +## Protocol ที่ยังใช้ชื่อ obfuscated + +ตรวจชื่อ callback/event จาก Client ที่กำลังรันผ่าน read-only reflection เพิ่มจาก schema ต้นฉบับ: + +| ตาราง protocol | ความสัมพันธ์ที่ Client ระบุ | +| --- | --- | +| DOPHNDHILPA oneof field 9 / JCKGBFJJFFC | OnStageSettle | +| DOPHNDHILPA oneof field 7 / GEKNAKDAMJF | OnBigSettle | +| DOPHNDHILPA oneof field 3 / PEOKJEOOOJD | OnPendingStageSettle; ไม่เดาความหมายตัวเลขภายใน | +| DOPHNDHILPA oneof field 8 / FLIPNFHAMPO | OnMedal ไม่ใช่การเปลี่ยน stage | +| LHAKNKHBLBI | callback `b__25_0` | +| JMPJCAPKNGI | callback `b__21_0` | +| OKEDFNJJGHB | callback `b__32_0` | + +Handler resolve CmdID จากชื่อในตาราง revision เดียวกัน ไม่เพิ่ม alias ตัวเลขข้ามเวอร์ชัน ชื่อ request ฝั่ง Tierce ยังเป็นชื่อ obfuscated ของคู่ response ใน schema ความครบของ UI callback และลำดับ settlement หลังการเปลี่ยนนี้ยังต้องยืนยันใน Client ไม่ตีความ field ที่ยังไม่มีชื่อเพื่อแจกคะแนน/เหรียญหรือ reward สมมติ + +## สถานะการยืนยัน + +- ผู้ใช้ยืนยัน Starfroster/Ferry Relay Station และ Moment of Joy วาร์ปได้แล้ว +- PF/Tierce build ก่อนหน้านี้มี PVEBattleResult และผู้ใช้เล่นได้ แต่ติดหนึ่ง cycle/ไม่ไปห้องต่อ ไม่ถือว่าผ่าน flow +- Build ที่รวม cycle, run state และ settlement ชุดนี้ **รอผลลอง PF ธรรมดา, Tierce และโหมดอื่น** การตรวจ 1,747 รายการไม่ยืนยันว่าเล่นผ่านทั้งหมด +- รายการปลดล็อก/ประวัติที่เมนู GetChallenge/GetChallengePeakData แสดง ยังมีข้อมูลจำลองของ implementation เดิม ส่วน reward, ดาว/เหรียญ และการเก็บผล Endgame ข้ามการเปิดเกมยังไม่ใช่ระบบ progression ที่ยืนยันครบ ตัว run ใหม่ไม่สร้างผลรางวัลหรือดาวจากการชนะเฉย ๆ + +## ผลจากผู้ใช้และการแก้รอบ Technique + +ผู้ใช้ยืนยัน PF ปกติเข้าสู้และเปลี่ยนทีม/ห้องได้แล้ว แต่ Starward ค้างตอนเข้าห้อง 3 Log Client ระบุ `TeamManager._CreateMemberEntity` เกิด `IndexOutOfRangeException` ระหว่างสร้าง leader ส่วน disassembly อ่าน `TeamData._leaderSlotIdId` ก่อนนำไป index `_MemberDatas` จึงตรวจเส้นทางติดตั้งทีมเป็นลำดับแรก + +แก้ Tierce ให้ node 0/1/2 ส่ง ExtraLineupType `LineupChallenge` / `LineupChallenge2` / `LineupChallenge3` ตามชื่อ enum ใน protocol แทนการส่งทับทีมแรกทุก node และ GetCurLineupData คืนทีม Challenge ที่กำลังใช้อยู่ การเชื่อมโยงอาการกับทีมที่ว่างยังเป็นข้อสันนิษฐานจาก log/โค้ด ต้องยืนยันห้อง 3 ใน Client หลังเปลี่ยนนี้ + +Technique ไม่เปิด Battle อ้างอิงรายการ IgnoreBattle ของ PearlSR และตอบ MonsterBattleType.NO_BATTLE (4) จาก protocol ของ revision เดียวกัน ไม่สร้าง BattleInfo สำหรับผลนี้ ส่วนการโจมตีมอนปกติคงเส้นทาง resource stage เดิม + +## Starward ผ่านห้อง 3 และการเซฟทีม/คะแนน + +ผู้ใช้ยืนยันเล่นครบ 3 ห้องและจบรอบได้ ผล Client ที่ตรง server log ของ challenge 20275 คือ 33,000 + 28,500 + 35,500 = 97,000 การแยกประเภททีมแก้อาการห้อง 3 ในรอบที่ผู้ใช้ลองแล้ว + +เพิ่ม persistent.tierce_progress สำหรับทีม บัฟ และ best result ของแต่ละ node เปลี่ยน GetChallengeTierceData จากข้อมูลทุกห้องชนะสำเร็จรูปเป็นทีม/ผลจริงที่เซฟไว้ รองรับ single-node retry โดยนำผลห้องอื่นที่เซฟแล้วมาประกอบ + +อ่าน ChallengeTargetConfigRow ผ่าน Client lookup GBPODLFHCBI.LMBKNCAIDLC และเก็บตารางพร้อม assembly hash ใน ChallengeTargetConfig.json: TOTAL_SCORE enum7 มี PF targets 4001/4002/4003 = 60k/75k/90k และ target4000=99k; AS 5001/5002/5003=6000/7800/9900, target5000=10200 ไม่อนุมานดาวจากสถานะ WIN อย่างเดียว MoC targetประเภท ROUNDS_LEFT(6)/DEAD_AVATAR(2) ยังไม่ประเมินในรอบนี้ + +ทั้ง StageSettle และ BigSettle ส่ง FinishedTargetList เมื่อมีผลชนะครบทุก node ส่วน GetData คืน best record/targets ที่บันทึก การแจก reward และประวัติของโหมดปกติ/Peak ยังไม่รวมในส่วนนี้ การเก็บทีม/ดาวรอบใหม่ยังรอ Client ยืนยัน diff --git a/docs/scene-challenge-investigation.md b/docs/scene-challenge-investigation.md new file mode 100644 index 0000000..630753c --- /dev/null +++ b/docs/scene-challenge-investigation.md @@ -0,0 +1,69 @@ +# ตรวจ teleport และ Endgame — 17 ต.ค. 2026 + +## หลักฐานจาก Client + +- `Player.log` แจ้ง `[HoyoGroupManager] 尝试激活的Group不存在, FloorID=20571001 FloorName=Starfroster GroupID=216` ก่อน `MapDef.InitServerGroups` เกิด NullReferenceException +- เริ่ม Endgame มี NullReferenceException ที่ `ChallengeBossInstance._SyncExtraInfo` ผ่าน `_OnCmdStartChallengeScRsp` +- จึงมีปัญหาหลัง Client ได้รับแพ็กเก็ต แม้ server จะส่ง response/notify สำเร็จ + +## สิ่งที่แก้ + +เทียบซอร์ส PearlSR revision `005d2c12db4781071f4b5b68e8f364587886a710` ใน `scene_mgr.zig` และ `challenge_mgr.zig`: + +- Go ใช้ `LevelOutputConfig.PlaneType` เป็น protocol `SceneInfo.GameModeType` ทั้งที่เป็นคนละ enum; supplementary scene ยังมีค่าเริ่มต้นเป็น 0 แก้ adventure เป็น mode 1 และ challenge เป็น mode 4 ตามต้นฉบับ +- เติม world metadata จาก MapEntrance/MazePlane แทน world 0 ของ supplementary scene และนำการแทน world ด้วย ID เฉพาะออกจากสองตัวโหลดนี้ +- โหลด `FloorSavedValuesConfig.json` ซึ่งมีใน repository แต่ไม่ได้ถูกอ่าน เติมค่าที่ขาดตาม floor โดยรักษาค่าที่ root config ระบุไว้ ส่งให้ทั้ง adventure และ challenge ต้นฉบับใช้ MaxValue เป็นสถานะปลดล็อกเช่นเดียวกัน +- เพิ่ม `CurChallenge.StageInfo.CurBossBuffs` สำหรับ AS และ `CurStoryBuffs` สำหรับ PF พร้อม buff ที่เลือกและ maze buff จาก challenge data; ก่อนหน้านี้ไม่มี StageInfo ทั้งที่ Client ใช้สร้าง instance +- เปิด state ของ prop/monster group ใน challenge ตามต้นฉบับ และเพิ่ม log scene metadata / challenge state ที่ `server_debug.log` + +## ขอบเขตและสิ่งที่ต้องยืนยัน + +ยังไม่ได้ยืนยันผลใน Client หลังการแก้ เรายังไม่ทราบว่า Group 216 หายเพราะ scene initialization ที่ผิด หรือ resource/asset คนละ revision; `resources/res.json` ตรงกับ checkout PearlSR ข้างต้น จึงไม่ได้ลบหรือเปลี่ยนหมายเลขกลุ่มนี้เพื่อหลบ error + +ต้องลองวาร์ป Starfroster และ Moment of Joy รวมทั้งเริ่ม Endgame แล้วอ่าน Player.log รอบใหม่ ถ้ายังมี missing group หลังแก้ metadata/saved values ต้องเทียบ floor asset ของ Client ต่อ การเปลี่ยนรอบนี้ไม่ได้รับรอง Peak/Tierce หรือการผ่านทุก node ของ Endgame และยังมี fallback arena/position เก่าใน handlers ที่ต้องตรวจแยก ไม่ได้อ้างว่าเอา hardcode ออกทั้ง repository + +ไม่ได้แก้ battle converter, Calyx response, progression/EX หรือ inventory sync ในรอบนี้ จุดย้อนกลับที่ผู้ใช้ยืนยันก่อนเริ่มงานคือ tag `stable-2026-10-17-battle-eidolon` (commit `a681aff`) + +## ผลลองรอบ 19:44 และการแก้ต่อ + +ผู้ใช้ยืนยัน AS/PF บางห้องเข้าได้แต่เกิดใต้แมพ, Tierce 30225 ค้าง, PF Starward เข้าห้องได้แต่หลังตีมอนค้าง Weakness และ teleport ของแมพใหม่ยังไม่สำเร็จ ผลนี้ยังไม่ใช่การยืนยันว่ารอบแก้ก่อนสำเร็จ + +พบข้อมูลผิดที่ตรวจซอร์สได้: + +- root scene ของ entrance 3014003 ถูกตั้งชื่อ floor 30140001 แต่ MapEntrance ระบุ **30140003**; 3012602 ก็ถูกตั้งเป็น 30126001 แทน **30126002** เปลี่ยนให้ single-floor scene ใช้ plane/floor จาก MapEntrance จริง +- Tierce 30225 ใน challenge_data ระบุ entrance 3012602, monster 2034012, event 420554 แต่ตาราง resource ปัจจุบันระบุ **3012402 / 3024012 / 420584** ตัวโหลดจึงให้ ChallengeMazeConfig, ChallengeMazeTierceConfig และ StageConfig ปัจจุบันมีลำดับความสำคัญเหนือข้อมูล compiled เก่า +- อ่าน PreChallengeMazeID ของ Tierce จากตาราง: node 0/1 ใช้ข้อมูลสองฝั่งของด่านก่อนหน้า ส่วน node 2 ใช้ด่าน Tierce ไม่เลือกด่าน Tierce ตั้งแต่ node 0 และไม่เดา remap ตามเลข season +- เปลี่ยน response Tierce ให้ส่งเฉพาะ lineup ของ node ที่ active ตาม upstream +- นำสูตรตำแหน่งเดิมที่ยก Y ไปหา prop สูงสุดและเลื่อนเข้าหาจุดกำเนิดโลกออก ใช้ Anchor เมื่อมี; ถ้าตารางไม่มี Anchor ใช้ตำแหน่งมอนในกลุ่มจริงแทน ไม่สร้างกลุ่มหรือพิกัดสมมติ หากไม่มีตำแหน่งจะตอบ error พร้อมบันทึกเหตุผล การเกิดตรงจุดมอนนี้ยังต้องตรวจ Client โดยเฉพาะห้องที่ไม่มี Anchor +- เก็บ BattleConfig ของ AS/PF/MoC/Tierce ใน session; ทีม มอน และ buff มาจาก node/stage ที่เลือก ไม่เขียนทับ custom config ใน persistent และไม่ถูก Freesr reload เปลี่ยน wave ไปกลางห้อง จำนวนรอบใช้ ChallengeCountDown เมื่อระบุไว้ มิฉะนั้นใช้ค่าตั้ง rounds ของผู้ใช้เช่นต้นฉบับ AS/PF ไม่ระบุ countdown ในตาราง +- log PF Starward รอบสุดท้ายมี SceneCastSkill สอง request ที่ไม่มีทั้ง hit/assist targets เซิร์ฟเวอร์จึงส่ง response ว่างและ **ไม่ได้ส่ง BattleInfo** ในสอง request นั้น ยังไม่สรุปว่าเป็น error โหลดมอนของ Battle; เพิ่ม request detail เมื่อเกิดใน Challenge เพื่อแยกจากปัญหา wave + +ตอนตรวจรอบนี้ Player.log เป็น session หลังเปิดเกมใหม่แล้ว ไม่มี exception ของการกด teleport รอบก่อน จึงยังต้องอ่าน log หลังทำซ้ำโดยไม่ออกเกมก่อน ไม่ได้ลบ Group 216 หรืออ้างว่าการเปลี่ยน floor ของ Challenge แก้ teleport ของ Starfroster/Moment of Joy แล้ว + +Build ผ่านและวางที่ root ไม่เท่ากับ Client ผ่าน ยังรอลองหลังการแก้ข้างต้น Peak ยังมีเส้นทาง legacy แยกจาก session config ที่แก้ใน AS/PF/Tierce รอบนี้ + +## ยืนยันต้นเหตุจากตาราง Client หลังลองวาร์ปซ้ำ + +หลังผู้ใช้กด Ferry Relay Station เก็บ exception `MapDef.InitServerGroups` ได้อีกครั้ง แม้ mode/world ถูกต้องแล้ว ตรวจแบบอ่านอย่างเดียวผ่าน `GameCoreConfigLoader.LoadRtLevelFloorInfo(MapDef.GetRtFloorInfoPath(plane, floor))` และ `GroupInstanceCommonMap` พบ: + +- Starfroster floor 20571001 group 216 มี `LoadSide=Server`, `IsHoyoGroup=true`, **`IsDelete=true`** +- floor 20572001 group 98 มี **`IsDelete=true`** เช่นกัน ทั้งสองกลุ่มยังถูกส่งใน resources/res.json จึงไม่ใช่หลักฐานว่า Client ขาด asset ทั้งแมพ +- อ่าน StartGroupIndex/StartAnchorID และ AnchorList ของ runtime group ได้จุดเกิดจริงของห้อง Challenge ที่ Anchor.json เดิมไม่มี รวมทั้ง PF 30140003, Starward 30142001, AS 30141001/30132001 และ Tierce 30124002/30126002 + +เพิ่ม `resources/SceneMetadata.json` ครอบคลุม **8 floors ที่อ่านจริง** พร้อม SHA-256 ของ GameAssembly และ log capture ไม่มี path เครื่องใน resource ตัวโหลดใช้ metadata เฉพาะ Client version ที่ตรง runtime config และจำนวน group ครบ แล้วกรองกลุ่มที่ถูกลบ, เป็น Client-owned หรือไม่มีในตาราง floor นั้น ใช้กฎเดียวกันทุก floor ที่มี metadata ไม่มีเงื่อนไขเจาะจง group 216/98 ใน Go + +เติม start anchor และตำแหน่งมอนจาก runtime group โดยแปลง group transform เข้า world และ metres/degrees เป็นหน่วย protocol จุดเกิดของห้องที่ครอบคลุมจึงใช้ Anchor จริง ไม่ต้องยืนตรงมอนหรือเดาความสูง prop ส่วน floor อื่นยังใช้ข้อมูลเดิมและไม่อ้างว่าตรวจทุกแมพแล้ว ตัวแปลง log เป็น resource อยู่ที่ `tools/export_scene_metadata.py`; รับ log, assembly, client version และ output เป็น arguments และปฏิเสธ group capture ที่ไม่ครบ + +Script อ่านข้อมูลชั่วคราวถูกนำออกจาก scripts/ หลังเก็บข้อมูลเสร็จ ไม่เปลี่ยน scene/actor, patch method หรือ save ใน Client ผลวาร์ป/สู้หลัง build ที่รวม metadata **ยังรอผู้ใช้ยืนยัน** + +### ผลยืนยันวาร์ปหลัง build ที่รวม metadata + +ผู้ใช้ตอบคำถามลอง Ferry Relay Station และ Moment of Joy ว่า **ได้แล้ว** จึงยืนยันการวาร์ปสองปลายทางนี้ใน Client ได้จริง Log รอบเดียวกันมี entry 2057101 teleport 2057103/2057102 และ entry 2057201 teleport 2057202 โดย group list ไม่มีกลุ่มที่ถูกลบ 216/98 แล้ว ไม่พบ `MapDef.InitServerGroups`/missing-group error ใน Player.log รอบนี้ ผลนี้ยังไม่ยืนยัน PF/AS/Tierce หรือการตีมอนของ Endgame หลัง build ใหม่ + +## MoC จากสมุด: ค้างท้ายหน้าโหลดในฉากรถไฟ + +คำขอ EnterScene ระบุ entrance 100000104, floor 10000000 และ teleport 2206 Client ล้มที่ `MapNpcDef.get_ConfigEntityPath` ขณะโหลด MapNPC 227-400001 ตาราง RuntimeGroup ของ Client ระบุ NPCID=3001 (ปอมปอม) ในกลุ่มนี้ + +GetPamSkinData ของ Go เดิมตอบ cur_skin=0 และ unlock_skin_list=[0] แต่ Client PamSkinConfigRow lookup ยืนยันว่า ID0 คืน nil ขณะที่ ID252000 มี ConfigEntityPath จริง และตรงกับค่าปกติใน PearlSR services/scene.zig revision005d2c12 + +แก้ handler ให้อ่านค่าจาก ProfileConfig.pam_skins/current_pam_skin และตรวจทุก ID กับ PamSkinConfig.json ที่ export จาก Client พร้อม SHA-256 ของ GameAssembly ไม่แก้กลุ่ม227ทิ้ง ไม่เปลี่ยนเส้นทาง Battle ผลการวาร์ปหลังแก้นี้ยังรอ Client ยืนยัน diff --git a/internal/common/models.go b/internal/common/models.go index eeb0bb5..54441b9 100644 --- a/internal/common/models.go +++ b/internal/common/models.go @@ -331,17 +331,41 @@ func DefaultScene() Scene { return Scene{PlaneID: 20411, FloorID: 20411001, EntryID: 2041101} } +// Tierce teams and best measured node results survive leaving a scene/login. +type TierceNodeProgress struct { + Score uint32 `json:"score"` + Won bool `json:"won"` + Result []byte `json:"result,omitempty"` +} +type TierceProgress struct { + Teams [][]uint32 `json:"teams"` + Buffs []uint32 `json:"buffs"` + Nodes map[uint32]TierceNodeProgress `json:"nodes"` +} + +type TeamPreset struct { + Name string `json:"name"` + Avatars map[uint32]uint32 `json:"avatars"` + LeaderSlot uint32 `json:"leader_slot"` + Favourite bool `json:"favourite"` +} + type Persistent struct { - Lineups map[uint32]uint32 `json:"lineups"` - Position Position `json:"position"` - Scene Scene `json:"scene"` - MainCharacter MultiPathAvatar `json:"main_character"` - MarchType MultiPathAvatar `json:"march_type"` - EnableSWGlobal *bool `json:"enable_sw_global"` - EnableCastoriceGlobal *bool `json:"enable_castorice_global"` - EnableAhaGlobal *bool `json:"enable_aha_global"` - DressedSkins map[uint32]uint32 `json:"dressed_skins"` - ExRanksUnlocked map[uint32]bool `json:"ex_ranks_unlocked"` + TeamPresets map[uint32]*TeamPreset `json:"team_presets"` + ActiveTeamPreset uint32 `json:"active_team_preset"` + TierceProgress map[uint32]*TierceProgress `json:"tierce_progress"` + TechniqueFoodStock map[uint32]uint32 `json:"technique_food_stock"` + AutoTechniqueFood *bool `json:"auto_technique_food"` + Lineups map[uint32]uint32 `json:"lineups"` + Position Position `json:"position"` + Scene Scene `json:"scene"` + MainCharacter MultiPathAvatar `json:"main_character"` + MarchType MultiPathAvatar `json:"march_type"` + EnableSWGlobal *bool `json:"enable_sw_global"` + EnableCastoriceGlobal *bool `json:"enable_castorice_global"` + EnableAhaGlobal *bool `json:"enable_aha_global"` + DressedSkins map[uint32]uint32 `json:"dressed_skins"` + ExRanksUnlocked map[uint32]bool `json:"ex_ranks_unlocked"` } // DefaultPersistent mirrors the Rust Default impl. @@ -368,6 +392,11 @@ func DefaultPersistent() Persistent { // --------------------------------------------------------------------------- type FreesrData struct { + TeamPresets map[uint32]*TeamPreset `json:"-"` + ActiveTeamPreset uint32 `json:"-"` + TierceProgress map[uint32]*TierceProgress `json:"-"` + TechniqueFoodStock map[uint32]uint32 `json:"-"` + AutoTechniqueFood *bool `json:"-"` persistentBaseline map[string]any Lightcones []Lightcone `json:"lightcones"` @@ -413,6 +442,11 @@ func LoadFreesrData() (*FreesrData, error) { var p Persistent if err := json.Unmarshal(pers, &p); err == nil { data.Lineups = p.Lineups + data.TeamPresets = p.TeamPresets + data.ActiveTeamPreset = p.ActiveTeamPreset + data.TierceProgress = p.TierceProgress + data.TechniqueFoodStock = p.TechniqueFoodStock + data.AutoTechniqueFood = p.AutoTechniqueFood data.Position = p.Position data.Scene = p.Scene data.MainCharacter = p.MainCharacter diff --git a/internal/common/persistent_store.go b/internal/common/persistent_store.go index 99e69aa..253c158 100644 --- a/internal/common/persistent_store.go +++ b/internal/common/persistent_store.go @@ -39,7 +39,7 @@ func (f *FreesrData) persistentSnapshot() Persistent { ex[id] = avatar.ExRankUnlocked } } - return Persistent{Lineups: f.Lineups, Position: f.Position, Scene: f.Scene, + return Persistent{TeamPresets: f.TeamPresets, ActiveTeamPreset: f.ActiveTeamPreset, TierceProgress: f.TierceProgress, TechniqueFoodStock: f.TechniqueFoodStock, AutoTechniqueFood: f.AutoTechniqueFood, Lineups: f.Lineups, Position: f.Position, Scene: f.Scene, MainCharacter: f.MainCharacter, MarchType: f.MarchType, EnableSWGlobal: f.EnableSWGlobal, EnableCastoriceGlobal: f.EnableCastoriceGlobal, EnableAhaGlobal: f.EnableAhaGlobal, DressedSkins: f.DressedSkins, ExRanksUnlocked: ex} diff --git a/internal/common/proto_convert.go b/internal/common/proto_convert.go index 3dc3cf4..c9e0b60 100644 --- a/internal/common/proto_convert.go +++ b/internal/common/proto_convert.go @@ -162,34 +162,24 @@ func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic out.EquipRelicList = append(out.EquipRelicList, r.ToEquipRelic()) } - if len(a.Data.Skills) > 0 { - for pointID, level := range a.Data.Skills { - out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{ - PointId: pointID, - Level: level, - }) - } - } else if len(a.Data.SkillsByAnchorType) > 0 { - res, _ := GameRes() - for anchorType, level := range a.Data.SkillsByAnchorType { - pointID := anchorType - if res != nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil { - if pid, ok := res.AvatarSkillTreePoints[a.AvatarID][anchorType]; ok && pid != 0 { - pointID = pid - } + // AvatarPathData uses AnchorType numbers, whereas BattleAvatar uses full + // PointIDs. PearlSR avatar_mgr.createSkillTree and battle_mgr intentionally + // serialize different IDs for these two protocol messages. + if res, err := GameRes(); err == nil { + points := res.AvatarSkillTreePoints[a.AvatarID] + for anchor, pointID := range points { + var level uint32 + if len(a.Data.Skills) > 0 { + level = a.Data.Skills[pointID] + } else if len(a.Data.SkillsByAnchorType) > 0 { + level = a.Data.SkillsByAnchorType[anchor] + } else { + // Keep the existing no-import level behavior, using valid anchors. + level = 1 } - out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{ - PointId: pointID, - Level: level, - }) - } - } else { - if res, err := GameRes(); err == nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil { - for _, pid := range res.AvatarSkillTreePoints[a.AvatarID] { - out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{ - PointId: pid, - Level: 1, - }) + if level > 0 { + out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, + &goproto.AvatarPathSkillTree{PointId: anchor, Level: level}) } } } @@ -314,15 +304,16 @@ func ToLineupAvatars(f *FreesrData) []*goproto.LineupAvatar { // ToLineupInfo builds LineupInfo from a lineup map (order: ascending slot). func ToLineupInfo(lineups map[uint32]uint32) *goproto.LineupInfo { - maxMp := uint32(5) - if _, ok := lineups[1408]; ok { - maxMp = 8 + var maxMp, currentMP uint32 + if rules, err := LoadTechniqueRules(); err == nil { + maxMp = rules.MaxMP(lineups) + currentMP = rules.InitialMP } lineup := &goproto.LineupInfo{ ExtraLineupType: goproto.ExtraLineupType_ExtraLineupType_LineupNone, Name: "Squad 1", - Mp: maxMp, + Mp: currentMP, MaxMp: maxMp, } diff --git a/internal/common/proto_convert_test.go b/internal/common/proto_convert_test.go index c253d0c..60d4e40 100644 --- a/internal/common/proto_convert_test.go +++ b/internal/common/proto_convert_test.go @@ -36,7 +36,7 @@ func TestStandaloneBattleRelicWire(t *testing.T) { } func TestAvatarPathSkillTreePointIds(t *testing.T) { - // 1. avatar with explicit Skills map uses real PointIDs directly + // AvatarPathData uses resource AnchorType numbers; BattleAvatar uses PointIDs. av := AvatarJson{ AvatarID: 1213, Data: AvatarData{ @@ -55,8 +55,8 @@ func TestAvatarPathSkillTreePointIds(t *testing.T) { if len(proto.AvatarPathSkillTree) != 2 { t.Fatalf("expected 2 skill tree nodes, got %d", len(proto.AvatarPathSkillTree)) } - if proto.AvatarPathSkillTree[0].PointId != 1213001 || proto.AvatarPathSkillTree[1].PointId != 1213002 { - t.Fatalf("point ids must be real config point IDs, got %v", proto.AvatarPathSkillTree) + if proto.AvatarPathSkillTree[0].PointId != 1 || proto.AvatarPathSkillTree[1].PointId != 2 { + t.Fatalf("path point ids must be resource anchor numbers, got %v", proto.AvatarPathSkillTree) } // 2. avatar with only SkillsByAnchorType resolves from AvatarSkillTreeConfig.json @@ -73,8 +73,8 @@ func TestAvatarPathSkillTreePointIds(t *testing.T) { if len(protoAnchor.AvatarPathSkillTree) != 1 { t.Fatalf("expected 1 skill tree node, got %d", len(protoAnchor.AvatarPathSkillTree)) } - if protoAnchor.AvatarPathSkillTree[0].PointId != 1001001 { - t.Fatalf("expected resolved point ID 1001001 for March Point01, got %d", protoAnchor.AvatarPathSkillTree[0].PointId) + if protoAnchor.AvatarPathSkillTree[0].PointId != 1 { + t.Fatalf("expected anchor 1 for March Point01, got %d", protoAnchor.AvatarPathSkillTree[0].PointId) } } diff --git a/internal/common/res.go b/internal/common/res.go index c38db08..bd78e41 100644 --- a/internal/common/res.go +++ b/internal/common/res.go @@ -81,6 +81,8 @@ type AvatarConfig struct { } type JsonConfig struct { + MapEntrances map[uint32]MapEntranceEntry `json:"-"` + SceneAnchors map[uint32][]AnchorItem `json:"-"` // entryId -> "P_F" -> config LevelOutputConfigs map[uint32]map[string]*LevelOutputConfig `json:"levelOutputConfigs"` AvatarConfigs map[uint32]AvatarConfig `json:"avatarConfigs"` @@ -192,6 +194,7 @@ func GameRes() (*JsonConfig, error) { _ = Teleports() // ensure teleportDB is initialized loadSupplementaryResources(gameRes) + applySceneMetadata(gameRes) }) return gameRes, gameResErr } @@ -266,6 +269,8 @@ func loadSupplementaryResources(res *JsonConfig) { } planesByID := map[uint32]MazePlaneEntry{} + res.MapEntrances = entrancesByEntry + res.SceneAnchors = map[uint32][]AnchorItem{} if raw, err := readResourceFile("resources/MazePlane.json", "MazePlane.json"); err == nil { var list []MazePlaneEntry if json.Unmarshal(raw, &list) != nil { @@ -366,6 +371,7 @@ func loadSupplementaryResources(res *JsonConfig) { name: { IsEnteredSceneInfo: true, Scenes: groups, + WorldID: planesByID[sc.PlaneID].WorldID, }, } } @@ -377,6 +383,7 @@ func loadSupplementaryResources(res *JsonConfig) { var aFile AnchorConfigFile if json.Unmarshal(raw, &aFile) == nil && len(aFile.AnchorConfig) > 0 { for _, item := range aFile.AnchorConfig { + res.SceneAnchors[item.EntryID] = item.Anchor floorID := uint32(0) if ent, ok := entrancesByEntry[item.EntryID]; ok && ent.FloorID != 0 { floorID = ent.FloorID @@ -415,6 +422,72 @@ func loadSupplementaryResources(res *JsonConfig) { } } } + + // Supplementary scenes do not carry world metadata. Resolve it from the + // installed entrance/plane tables, including entries supplied by Anchor.json. + floorSavedValues := map[uint32]map[string]int32{} + if raw, err := readResourceFile("resources/FloorSavedValuesConfig.json"); err == nil { + var table struct { + Floors []struct { + FloorID uint32 `json:"FloorID"` + Values []struct { + Name string `json:"Name"` + MaxValue int32 `json:"MaxValue"` + } `json:"SavedValues"` + } `json:"saved_values"` + } + if err := json.Unmarshal(raw, &table); err == nil { + // Same unlocked scene defaults as PearlSR 005d2c12 scene_mgr.zig. + for _, floor := range table.Floors { + values := map[string]int32{} + for _, value := range floor.Values { + if value.Name != "" { + values[value.Name] = value.MaxValue + } + } + floorSavedValues[floor.FloorID] = values + } + } else { + LogInfo("[SCENE] Invalid FloorSavedValuesConfig: %v", err) + } + } + for entryID, configs := range res.LevelOutputConfigs { + entrance, ok := entrancesByEntry[entryID] + if !ok { + continue + } + // Legacy dumps label every entrance in a plane with its first floor. + // An entrance has an explicit floor in MapEntrance; do not derive it. + if len(configs) == 1 { + canonical := fmt.Sprintf("P%d_F%d", entrance.PlaneID, entrance.FloorID) + for name, config := range configs { + if name != canonical { + delete(configs, name) + configs[canonical] = config + } + break + } + } + plane, ok := planesByID[entrance.PlaneID] + if !ok { + continue + } + for name, config := range configs { + config.WorldID = plane.WorldID + var configPlane, configFloor uint32 + if n, _ := fmt.Sscanf(name, "P%d_F%d", &configPlane, &configFloor); n != 2 { + continue + } + if config.SavedValues == nil { + config.SavedValues = map[string]int32{} + } + for name, value := range floorSavedValues[configFloor] { + if _, exists := config.SavedValues[name]; !exists { + config.SavedValues[name] = value + } + } + } + } } // Teleports lazily parses teleports.json ({floor_id: {teleport_id: info}}). diff --git a/internal/common/runtime_config.go b/internal/common/runtime_config.go index 3a8bd35..ea69bba 100644 --- a/internal/common/runtime_config.go +++ b/internal/common/runtime_config.go @@ -15,6 +15,7 @@ import ( // RuntimeConfig is shared by both server processes. Deployment values belong // in configuration rather than in handlers or executable entry points. type RuntimeConfig struct { + PlayerUID uint32 `json:"player_uid"` BattleDiagnostics BattleDiagnosticsConfig `json:"battle_diagnostics"` ClientVersion string `json:"client_version"` GameHost string `json:"game_host"` @@ -67,6 +68,16 @@ func LoadRuntimeConfig() (RuntimeConfig, error) { *dest = uint16(n) } } + if value, ok := os.LookupEnv("GAC_PLAYER_UID"); ok { + n, err := strconv.ParseUint(value, 10, 32) + if err != nil || n == 0 { + return cfg, fmt.Errorf("GAC_PLAYER_UID must be an integer between 1 and 4294967295") + } + cfg.PlayerUID = uint32(n) + } + if cfg.PlayerUID == 0 { + return cfg, fmt.Errorf("runtime config requires a nonzero player_uid") + } if cfg.GamePort == 0 || cfg.SDKPort == 0 || strings.TrimSpace(cfg.ClientVersion) == "" { return cfg, fmt.Errorf("runtime config requires client_version, game_port and sdk_port") } diff --git a/internal/common/scene_metadata.go b/internal/common/scene_metadata.go new file mode 100644 index 0000000..0101d64 --- /dev/null +++ b/internal/common/scene_metadata.go @@ -0,0 +1,74 @@ +package common + +import ( + "encoding/json" + "fmt" +) + +// Scene metadata is exported from the installed Client's RuntimeFloor tables. +// A complete group list allows us to exclude deleted/client-owned groups +// without maintaining special cases for individual maps or group IDs. +type sceneMetadata struct { + ClientVersion string `json:"client_version"` + Floors map[uint32]struct { + StartAnchor *AnchorItem `json:"start_anchor"` + Monsters map[uint32]map[uint32]TeleportInfo `json:"monsters"` + GroupCount int `json:"group_count"` + Groups map[uint32]struct { + LoadSide string `json:"load_side"` + Deleted bool `json:"is_deleted"` + } `json:"groups"` + } `json:"floors"` +} + +func applySceneMetadata(res *JsonConfig) { + raw, err := ReadResourceFile("resources/SceneMetadata.json") + if err != nil { + return + } + var metadata sceneMetadata + if err := json.Unmarshal(raw, &metadata); err != nil { + LogInfo("[SCENE] Invalid SceneMetadata: %v", err) + return + } + config, err := LoadRuntimeConfig() + if err != nil || metadata.ClientVersion != config.ClientVersion { + LogInfo("[SCENE] SceneMetadata Client revision does not match runtime config; not applied") + return + } + for entryID, configs := range res.LevelOutputConfigs { + for name, scene := range configs { + var planeID, floorID uint32 + if n, _ := fmt.Sscanf(name, "P%d_F%d", &planeID, &floorID); n != 2 { + continue + } + floor, ok := metadata.Floors[floorID] + if !ok { + continue + } + if floor.GroupCount == 0 || len(floor.Groups) != floor.GroupCount { + LogInfo("[SCENE] Incomplete group metadata for floor %d; not applied", floorID) + continue + } + if floor.StartAnchor != nil { + res.SceneAnchors[entryID] = []AnchorItem{*floor.StartAnchor} + } + for groupID := range scene.Scenes { + group, exists := floor.Groups[groupID] + if !exists || group.Deleted || group.LoadSide == "Client" { + delete(scene.Scenes, groupID) + LogInfo("[SCENE] Excluded group entry=%d floor=%d group=%d present=%t deleted=%t side=%s", entryID, floorID, groupID, exists, group.Deleted, group.LoadSide) + continue + } + data := scene.Scenes[groupID] + for index := range data.Monsters { + monster := &data.Monsters[index] + if placement, ok := floor.Monsters[groupID][monster.InstID]; ok { + monster.Pos, monster.Rot = placement.Pos, placement.Rot + } + } + scene.Scenes[groupID] = data + } + } + } +} diff --git a/internal/common/team_presets.go b/internal/common/team_presets.go new file mode 100644 index 0000000..f1bddb7 --- /dev/null +++ b/internal/common/team_presets.go @@ -0,0 +1,87 @@ +package common + +import ( + "encoding/json" + "fmt" + "sync" +) + +type TeamPresetRules struct { + Count uint32 `json:"preset_count"` + MaxMembers uint32 `json:"max_members"` + MaxNameLength int `json:"max_name_length"` +} + +var teamPresetOnce sync.Once +var teamPresetRules TeamPresetRules +var teamPresetError error + +func LoadTeamPresetRules() (*TeamPresetRules, error) { + teamPresetOnce.Do(func() { + raw, err := ReadResourceFile("resources/TeamPresetConfig.json") + if err != nil { + teamPresetError = err + return + } + if err := json.Unmarshal(raw, &teamPresetRules); err != nil { + teamPresetError = err + return + } + if teamPresetRules.Count == 0 || teamPresetRules.MaxMembers == 0 || teamPresetRules.MaxNameLength < 1 { + teamPresetError = fmt.Errorf("invalid TeamPresetConfig") + } + }) + return &teamPresetRules, teamPresetError +} +func (f *FreesrData) TeamPreset(index uint32) *TeamPreset { + original := f.TeamPresets[index] + p := &TeamPreset{Name: fmt.Sprintf("Team %d", index+1), Avatars: map[uint32]uint32{}, Favourite: true} + if original != nil { + p.Name = original.Name + p.LeaderSlot = original.LeaderSlot + p.Favourite = original.Favourite + for slot, id := range original.Avatars { + p.Avatars[slot] = id + } + } + // Preserve edits made through the existing /api/persistent lineups field. + if index == f.ActiveTeamPreset { + p.Avatars = map[uint32]uint32{} + for slot, id := range f.Lineups { + p.Avatars[slot] = id + } + } + return p +} +func (f *FreesrData) SaveTeamPreset(index uint32, preset *TeamPreset) error { + if f.TeamPresets == nil { + f.TeamPresets = map[uint32]*TeamPreset{} + } + if preset.Avatars[preset.LeaderSlot] == 0 { + found := false + for slot, id := range preset.Avatars { + if id != 0 && (!found || slot < preset.LeaderSlot) { + preset.LeaderSlot = slot + found = true + } + } + } + old, oldLineup := f.TeamPresets[index], f.Lineups + f.TeamPresets[index] = preset + if index == f.ActiveTeamPreset { + f.Lineups = map[uint32]uint32{} + for slot, id := range preset.Avatars { + f.Lineups[slot] = id + } + } + if err := f.SavePersistentChecked(); err != nil { + if old == nil { + delete(f.TeamPresets, index) + } else { + f.TeamPresets[index] = old + } + f.Lineups = oldLineup + return err + } + return nil +} diff --git a/internal/common/technique_rules.go b/internal/common/technique_rules.go new file mode 100644 index 0000000..b968ee5 --- /dev/null +++ b/internal/common/technique_rules.go @@ -0,0 +1,107 @@ +package common + +import ( + "encoding/json" + "fmt" + "sync" +) + +type TechniqueFood struct { + RecoverMP uint32 `json:"recover_mp"` + MaxUse uint32 `json:"max_use"` + PileLimit uint32 `json:"pile_limit"` +} +type MazeSkillRule struct { + ID uint32 `json:"MazeSkillId"` + MPCost uint32 `json:"MPCost"` + Trigger string `json:"SkillTriggerKey"` +} +type TechniqueRules struct { + ClientVersion string `json:"client_version"` + BaseMaxMP uint32 `json:"base_max_mp"` + InitialMP uint32 `json:"initial_mp"` + MaxMPByAvatar map[uint32]uint32 `json:"max_mp_by_avatar"` + NonAttackTechniques []uint32 `json:"non_attack_techniques"` + Avatars map[uint32][]MazeSkillRule `json:"avatars"` + Foods map[uint32]TechniqueFood `json:"foods"` + InitialFoodCount uint32 `json:"initial_food_count"` + AutoFoodDefault bool `json:"auto_food_default"` +} + +var techniqueOnce sync.Once +var techniqueRules TechniqueRules +var techniqueErr error + +func LoadTechniqueRules() (*TechniqueRules, error) { + techniqueOnce.Do(func() { + raw, err := ReadResourceFile("resources/TechniqueRules.json") + if err != nil { + techniqueErr = err + return + } + if err = json.Unmarshal(raw, &techniqueRules); err != nil { + techniqueErr = err + return + } + cfg, err := LoadRuntimeConfig() + if err != nil { + techniqueErr = err + return + } + if techniqueRules.ClientVersion != cfg.ClientVersion || techniqueRules.BaseMaxMP == 0 || techniqueRules.InitialMP > techniqueRules.BaseMaxMP { + techniqueErr = fmt.Errorf("invalid or mismatched TechniqueRules client resource") + return + } + for id, f := range techniqueRules.Foods { + if id == 0 || f.RecoverMP == 0 || f.MaxUse == 0 || techniqueRules.InitialFoodCount > f.PileLimit { + techniqueErr = fmt.Errorf("invalid technique food %d", id) + return + } + } + }) + return &techniqueRules, techniqueErr +} +func (r *TechniqueRules) MaxMP(avatars map[uint32]uint32) uint32 { + maximum := r.BaseMaxMP + // PearlSR buildLineup selects the largest team cap; it does not add both caps. + for _, id := range avatars { + if value := r.MaxMPByAvatar[id]; value > maximum { + maximum = value + } + } + return maximum +} + +// Called only by an explicit inventory/cast operation, never by save readers. +func (f *FreesrData) EnsureTechniqueFoods() error { + if f.TechniqueFoodStock != nil && f.AutoTechniqueFood != nil { + return nil + } + r, err := LoadTechniqueRules() + if err != nil { + return err + } + if f.TechniqueFoodStock == nil { + f.TechniqueFoodStock = map[uint32]uint32{} + for id := range r.Foods { + f.TechniqueFoodStock[id] = r.InitialFoodCount + } + } + if f.AutoTechniqueFood == nil { + value := r.AutoFoodDefault + f.AutoTechniqueFood = &value + } + return f.SavePersistentChecked() +} + +// Refresh just the setting, and advance its merge baseline so a subsequent +// session position save cannot undo an external API toggle. +func (f *FreesrData) RefreshTechniqueSettings(p *Persistent) { + f.AutoTechniqueFood = p.AutoTechniqueFood + if snapshot, err := jsonObject(f.persistentSnapshot()); err == nil { + if f.persistentBaseline == nil { + f.persistentBaseline = map[string]any{} + } + f.persistentBaseline["auto_technique_food"] = snapshot["auto_technique_food"] + } +} diff --git a/internal/gameserver/battle_rules.go b/internal/gameserver/battle_rules.go index d4223a8..b10c531 100644 --- a/internal/gameserver/battle_rules.go +++ b/internal/gameserver/battle_rules.go @@ -14,8 +14,9 @@ import ( // not executable defaults. Missing or invalid rules must not silently produce a // different battle. type battleRules struct { - EntryAttackBuffs map[string]uint32 `json:"entry_attack_buffs"` - Source struct { + ChallengeRoundLimits map[common.BattleType]uint32 `json:"challenge_round_limits"` + EntryAttackBuffs map[string]uint32 `json:"entry_attack_buffs"` + Source struct { Revision string `json:"revision"` } `json:"source"` TechniqueBuffs map[uint32][]struct { diff --git a/internal/gameserver/challenge_run.go b/internal/gameserver/challenge_run.go new file mode 100644 index 0000000..57aff2a --- /dev/null +++ b/internal/gameserver/challenge_run.go @@ -0,0 +1,406 @@ +package gameserver + +import ( + "fmt" + "gac-hkrpg-go/internal/common" + "sort" + + goproto "gac-hkrpg-go/protocol/proto" + "google.golang.org/protobuf/proto" +) + +type challengeRoom struct { + Entrance, Group, Monster, Event uint32 + Avatars []uint32 + Buffs []uint32 +} + +// PearlSR 005d2c12 challenge_mgr.zig defines the three Tierce nodes 0..2. +const tierceNodeCount = 3 + +func rememberTierceTeams(s *Session, id uint32, teams [][]uint32, buffs []uint32, single bool, node uint32) error { + if s.TierceTeams == nil { + s.TierceTeams = map[uint32][][]uint32{} + } + if s.TierceBuffs == nil { + s.TierceBuffs = map[uint32][]uint32{} + } + p := s.JSON() + if p == nil { + return fmt.Errorf("player data is unavailable") + } + if p.TierceProgress == nil { + p.TierceProgress = map[uint32]*common.TierceProgress{} + } + saved := p.TierceProgress[id] + if saved == nil { + saved = &common.TierceProgress{Nodes: map[uint32]common.TierceNodeProgress{}} + p.TierceProgress[id] = saved + } + if s.TierceTeams[id] == nil { + s.TierceTeams[id] = saved.Teams + } + if s.TierceBuffs[id] == nil { + s.TierceBuffs[id] = saved.Buffs + } + if len(s.TierceTeams[id]) != tierceNodeCount { + s.TierceTeams[id] = make([][]uint32, tierceNodeCount) + } + if len(s.TierceBuffs[id]) != tierceNodeCount { + s.TierceBuffs[id] = make([]uint32, tierceNodeCount) + } + for index, team := range teams { + target := index + if single && len(teams) == 1 { + target = int(node) + } + if target >= tierceNodeCount { + continue + } + if len(team) > 0 { + s.TierceTeams[id][target] = append([]uint32(nil), dedupeTruncate4(team)...) + } + if index < len(buffs) { + s.TierceBuffs[id][target] = buffs[index] + } + } + saved.Teams, saved.Buffs = s.TierceTeams[id], s.TierceBuffs[id] + return p.SavePersistentChecked() +} + +// A run owns its teams, score and room independently of Freesr and other +// sessions. Rooms come from the installed ChallengeMaze/Tierce/Peak tables. +type challengeRun struct { + ID uint32 + Tierce, Peak, Single, PendingNext, Finished bool + Node uint32 + Rooms []challengeRoom + Scores []uint32 + Rounds uint32 + RoundCosts []uint32 + Cur *goproto.CurChallenge + TierceCur *goproto.CurTierceChallenge + PeakBuff uint32 + Results []*goproto.PMMOFOBCINL + Win bool +} + +func resourceRoom(stage ChallengeStageData, second bool, avatars []uint32, buff uint32) challengeRoom { + r := challengeRoom{Entrance: stage.Entrance, Group: stage.Group1, Monster: stage.Monster1, Event: stage.Event1} + if second { + r.Entrance, r.Group, r.Monster, r.Event = stage.Entrance2, stage.Group2, stage.Monster2, stage.Event2 + } + r.Avatars = append([]uint32(nil), dedupeTruncate4(avatars)...) + r.Buffs = []uint32{buff, stage.Buff} + return r +} + +func (run *challengeRun) lineupType() goproto.ExtraLineupType { + // Each Tierce node has its own protocol team. Updating team 1 while the + // client switches to team 3 leaves an empty TeamData and leader slot -1. + if run.Tierce { + switch run.Node { + case 1: + return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge2 + case 2: + return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge3 + } + } + if !run.Tierce && !run.Peak && run.Node > 0 { + return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge2 + } + return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge +} + +func (run *challengeRun) activeLineup(plane uint32) *goproto.LineupInfo { + return lineupInfoFromAvatars(run.Rooms[run.Node].Avatars, plane, run.lineupType(), 0) +} + +func challengeCurBuffs(id uint32, buffs []uint32) *goproto.ChallengeCurBuffInfo { + var valid []uint32 + seen := map[uint32]bool{} + for _, buff := range buffs { + if buff != 0 && !seen[buff] { + valid = append(valid, buff) + seen[buff] = true + } + } + switch { + case id > 20000 && id < 30000: + return &goproto.ChallengeCurBuffInfo{CurStoryBuffs: &goproto.ChallengeStoryBuffList{BuffList: valid}} + case id > 30000: + return &goproto.ChallengeCurBuffInfo{CurBossBuffs: &goproto.ChallengeBossBuffList{BuffList: valid, ChallengeBossConst: 1}} + } + return nil +} + +func advanceChallengeRoom(s *Session) (*goproto.SceneInfo, error) { + run := s.ChallengeRun + if run == nil || !run.PendingNext || int(run.Node)+1 >= len(run.Rooms) { + return nil, fmt.Errorf("challenge has no pending next room") + } + next := run.Node + 1 + r := run.Rooms[next] + if len(r.Avatars) == 0 { + return nil, fmt.Errorf("challenge %d node %d has no saved team", run.ID, next) + } + previousConfig := s.ChallengeBattleConfig + if err := setChallengeBattleConfig(s, r.Avatars, run.ID, r.Event, r.Buffs); err != nil { + return nil, err + } + scene, _, err := loadChallengeScene(s, r.Entrance, r.Group, r.Monster, r.Event, r.Avatars) + if err != nil { + s.ChallengeBattleConfig = previousConfig + return nil, err + } + run.Node, run.PendingNext = next, false + if run.Cur != nil { + run.Cur.ExtraLineupType = run.lineupType() + run.Cur.StageInfo = challengeCurBuffs(run.ID, r.Buffs) + } + if run.TierceCur != nil { + run.TierceCur.FINOCEGDBDI = next + run.TierceCur.LineupList = []*goproto.LineupInfo{run.activeLineup(scene.PlaneId)} + } + SceneDebugLog("[CHALLENGE] advanced id=%d tierce=%t node=%d entry=%d stage=%d team=%v", run.ID, run.Tierce, next, r.Entrance, r.Event, r.Avatars) + return scene, nil +} + +func sendChallengeRoom(s *Session, scene *goproto.SceneInfo) error { + run := s.ChallengeRun + lineup := run.activeLineup(scene.PlaneId) + if err := s.Send(&goproto.SyncLineupNotify{Lineup: lineup}); err != nil { + return err + } + if !run.Tierce && !run.Peak { + if err := s.Send(&goproto.ChallengeLineupNotify{ExtraLineupType: run.lineupType()}); err != nil { + return err + } + } + return s.Send(&goproto.EnterSceneByServerScNotify{Scene: scene, Lineup: lineup}) +} + +func init() { + handle("EnterChallengeNextPhase", func(s *Session, _ *goproto.EnterChallengeNextPhaseCsReq, rsp *goproto.EnterChallengeNextPhaseScRsp) error { + if run := s.ChallengeRun; run == nil || run.Tierce || run.Peak { + rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) + return nil + } + scene, err := advanceChallengeRoom(s) + if err != nil { + rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) + return nil + } + rsp.Scene = scene + return s.Send(&goproto.SyncLineupNotify{Lineup: s.ChallengeRun.activeLineup(scene.PlaneId)}) + }) + // PearlSR 005d2c12 retains obfuscated command names. Client callback + // metadata confirms b__25_0(LHAKNKHBLBI) and + // b__21_0(JMPJCAPKNGI); resolve IDs by these table names. + rawNamed("MHJNOBBOFOC", func(s *Session, _ []byte) error { + if run := s.ChallengeRun; run == nil || !run.Tierce { + return s.Send(&goproto.LHAKNKHBLBI{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) + } + scene, err := advanceChallengeRoom(s) + if err != nil { + return s.Send(&goproto.LHAKNKHBLBI{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) + } + if err := s.Send(&goproto.SyncLineupNotify{Lineup: s.ChallengeRun.activeLineup(scene.PlaneId)}); err != nil { + return err + } + return s.Send(&goproto.LHAKNKHBLBI{Scene: scene, CurTierceChallenge: s.ChallengeRun.TierceCur}) + }) + rawNamed("PKKFNODINEM", func(s *Session, _ []byte) error { + scene, err := restartChallengeRoom(s) + if err != nil { + return s.Send(&goproto.JMPJCAPKNGI{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) + } + return s.Send(&goproto.JMPJCAPKNGI{Scene: scene}) + }) + rawNamed("MENLHHCJNCE", func(s *Session, _ []byte) error { return s.Send(&goproto.OKEDFNJJGHB{}) }) + handle("RestartChallengePhase", func(s *Session, _ *goproto.RestartChallengePhaseCsReq, rsp *goproto.RestartChallengePhaseScRsp) error { + scene, err := restartChallengeRoom(s) + if err != nil { + rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) + } else { + rsp.Scene = scene + } + return nil + }) +} + +func restartChallengeRoom(s *Session) (*goproto.SceneInfo, error) { + run := s.ChallengeRun + if run == nil { + return nil, fmt.Errorf("no challenge to restart") + } + r := run.Rooms[run.Node] + if err := setChallengeBattleConfig(s, r.Avatars, run.ID, r.Event, r.Buffs); err != nil { + return nil, err + } + scene, _, err := loadChallengeScene(s, r.Entrance, r.Group, r.Monster, r.Event, r.Avatars) + if err != nil { + return nil, err + } + run.Finished, run.PendingNext = false, false + run.Win = false + run.Scores[run.Node] = 0 + if len(run.RoundCosts) > int(run.Node) { + run.RoundCosts[run.Node] = 0 + } + run.Rounds = 0 + for _, rounds := range run.RoundCosts { + run.Rounds += rounds + } + var results []*goproto.PMMOFOBCINL + for _, result := range run.Results { + if result.FINOCEGDBDI != run.Node { + results = append(results, result) + } + } + run.Results = results + if run.Cur != nil { + run.Cur.Status = goproto.ChallengeStatus_CHALLENGE_DOING + run.Cur.RoundCount = run.Rounds + run.Cur.ScoreId = run.Scores[0] + if len(run.Scores) > 1 { + run.Cur.ScoreTwo = run.Scores[1] + } + } + CURRENT_BATTLE.Delete(s.Token) + return scene, nil +} + +func finishTierceNode(s *Session, req *goproto.PVEBattleResultCsReq) error { + run := s.ChallengeRun + lineup := &goproto.GLLFJFJBIBC{} + for slot, id := range run.Rooms[run.Node].Avatars { + avatar := s.JSON().Avatars[id] + if avatar == nil { + continue + } + info := &goproto.CLDOIEGCNLD{AvatarId: id, AvatarType: uint32(goproto.AvatarType_AVATAR_FORMAL_TYPE), Slot: uint32(slot), Level: avatar.Level, PAGPNOKLNOM: &goproto.ChallengeBossEquipmentInfo{}, OKCGHKDMFAM: &goproto.ChallengeBossAvatarRelicInfo{}} + if cone := s.JSON().FindLightconeFor(id); cone != nil { + info.PAGPNOKLNOM = &goproto.ChallengeBossEquipmentInfo{UniqueId: cone.InternalUID, Tid: cone.ItemID, Level: cone.Level, Promotion: cone.Promotion, Rank: cone.Rank} + } + lineup.MNABNJOEHLF = append(lineup.MNABNJOEHLF, info) + } + result := &goproto.PMMOFOBCINL{FINOCEGDBDI: run.Node, ScoreId: run.Scores[run.Node], EndStatus: req.EndStatus, Lineup: lineup} + if req.Stt != nil { + for _, targets := range req.Stt.BattleTargetInfo { + result.BattleTargetList = append(result.BattleTargetList, targets.GetBattleTargetList()...) + } + } + run.Results = append(run.Results, result) + if err := saveTierceResult(s, result); err != nil { + return err + } + var total uint32 + for _, score := range run.Scores { + total += score + } + allWon := len(run.Results) == tierceNodeCount + for _, nodeResult := range run.Results { + if nodeResult.EndStatus != goproto.BattleEndStatus_BATTLE_END_WIN { + allWon = false + } + } + var finishedTargets []uint32 + if run.Finished && allWon { + finishedTargets = tierceTargets(run.ID, total) + } + win := req.EndStatus == goproto.BattleEndStatus_BATTLE_END_WIN + if run.Finished { + win = win && allWon + } + if run.Finished && getChallengeData().Tierce[run.ID].ClearScore > 0 { + win = win && total >= getChallengeData().Tierce[run.ID].ClearScore + } + run.Win = win + // Verified Client events: this oneof is OnStageSettle; the field-8 + // alternative is OnMedal and must not be used to switch stages. + if err := s.Send(&goproto.DOPHNDHILPA{MMCGBGDJIPN: &goproto.DOPHNDHILPA_IANBCDJPDJF{IANBCDJPDJF: &goproto.JCKGBFJJFFC{PAIAEPJIDPE: result, IsWin: win, FinishedTargetList: finishedTargets}}}); err != nil { + return err + } + if run.Finished { + sort.Slice(run.Results, func(i, j int) bool { return run.Results[i].FINOCEGDBDI < run.Results[j].FINOCEGDBDI }) + var score uint32 + for _, value := range run.Scores { + score += value + } + if err := s.Send(&goproto.DOPHNDHILPA{MMCGBGDJIPN: &goproto.DOPHNDHILPA_DFNNKENBOEN{DFNNKENBOEN: &goproto.GEKNAKDAMJF{ChallengeId: run.ID, IsWin: win, FinishedTargetList: finishedTargets, EGEHLOPFELK: &goproto.ChallengeTierceRecord{TotalScore: score}, GBMBAHHDONN: run.Results}}}); err != nil { + return err + } + } + return errResponseAlreadySent +} + +// Scores are supplied by the Client; Tierce targets are evaluated against the +// installed target table. A win alone does not grant score targets or rewards. +func finishChallengeBattle(s *Session, req *goproto.PVEBattleResultCsReq, rsp *goproto.PVEBattleResultScRsp, active bool) error { + run := s.ChallengeRun + if !active || run == nil || run.Finished || run.PendingNext || req.StageId != run.Rooms[run.Node].Event { + return nil + } + if req.EndStatus == goproto.BattleEndStatus_BATTLE_END_QUIT { + return nil + } + if req.Stt != nil { + run.Scores[run.Node] = req.Stt.ChallengeScore + if len(run.RoundCosts) != len(run.Rooms) { + run.RoundCosts = make([]uint32, len(run.Rooms)) + } + run.RoundCosts[run.Node] = req.Stt.RoundCnt + run.Rounds = 0 + for _, rounds := range run.RoundCosts { + run.Rounds += rounds + } + } + if run.Cur != nil { + run.Cur.RoundCount = run.Rounds + run.Cur.ScoreId = run.Scores[0] + if len(run.Scores) > 1 { + run.Cur.ScoreTwo = run.Scores[1] + } + } + SceneDebugLog("[CHALLENGE] result id=%d node=%d status=%s stage=%d scores=%v rounds=%d", run.ID, run.Node, req.EndStatus, req.StageId, run.Scores, run.Rounds) + if err := s.Send(rsp); err != nil { + return err + } + win := req.EndStatus == goproto.BattleEndStatus_BATTLE_END_WIN + run.Win = win + if win && !run.Single && int(run.Node)+1 < len(run.Rooms) { + run.PendingNext = true + if run.Tierce { + return finishTierceNode(s, req) + } + scene, err := advanceChallengeRoom(s) + if err != nil { + return err + } + if err := sendChallengeRoom(s, scene); err != nil { + return err + } + return errResponseAlreadySent + } + run.Finished = true + if run.Tierce { + return finishTierceNode(s, req) + } + if run.Peak { + if err := s.Send(&goproto.ChallengePeakSettleScNotify{PeakId: run.ID, IsWin: win, CyclesUsed: run.Rounds}); err != nil { + return err + } + return errResponseAlreadySent + } + if run.Cur != nil { + run.Cur.Status = goproto.ChallengeStatus_CHALLENGE_FAILED + if win { + run.Cur.Status = goproto.ChallengeStatus_CHALLENGE_FINISH + } + if err := s.Send(&goproto.ChallengeSettleNotify{ChallengeId: run.ID, IsWin: win, CurChallenge: proto.Clone(run.Cur).(*goproto.CurChallenge), Reward: &goproto.ItemList{}, ChallengeScore: run.Cur.ScoreId, ScoreTwo: run.Cur.ScoreTwo}); err != nil { + return err + } + } + return errResponseAlreadySent +} diff --git a/internal/gameserver/gateway.go b/internal/gameserver/gateway.go index 4169c02..f742c27 100644 --- a/internal/gameserver/gateway.go +++ b/internal/gameserver/gateway.go @@ -21,6 +21,11 @@ type Gateway struct { } func NewGateway(host string, port uint16) (*Gateway, error) { + cfg, err := common.LoadRuntimeConfig() + if err != nil { + return nil, err + } + playerUID = cfg.PlayerUID addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, itoa(int(port)))) if err != nil { return nil, err diff --git a/internal/gameserver/handlers_authentication.go b/internal/gameserver/handlers_authentication.go index 8969cc8..1ad307b 100644 --- a/internal/gameserver/handlers_authentication.go +++ b/internal/gameserver/handlers_authentication.go @@ -6,7 +6,8 @@ import ( goproto "gac-hkrpg-go/protocol/proto" ) -const playerUID uint32 = 1337 +// Loaded from the shared runtime configuration before the gateway accepts sessions. +var playerUID uint32 func init() { handle("PlayerGetToken", func(s *Session, req *goproto.PlayerGetTokenCsReq, rsp *goproto.PlayerGetTokenScRsp) error { @@ -23,7 +24,7 @@ func init() { rsp.ServerTimestampMs = uint64(time.Now().UnixMilli()) rsp.Stamina = 240 rsp.BasicInfo = &goproto.PlayerBasicInfo{ - Nickname: "RobinSR", + Nickname: "GaySR", Level: 70, WorldLevel: 6, Stamina: 240, diff --git a/internal/gameserver/handlers_battle.go b/internal/gameserver/handlers_battle.go index 6bb6eda..2b2bb4f 100644 --- a/internal/gameserver/handlers_battle.go +++ b/internal/gameserver/handlers_battle.go @@ -12,7 +12,11 @@ import ( // PearlSR 005d2c1 protocol/src/protocol.pb.zig: MonsterBattleType. // The Go schema retains obfuscated enum labels; zero is NONE, not TRIGGER. -const monsterBattleTrigger goproto.MonsterBattleType = 1 +const ( + monsterBattleTrigger goproto.MonsterBattleType = 1 + // PearlSR protocol.pb.zig: MONSTER_BATTLE_TYPE_NO_BATTLE. + monsterBattleNoBattle goproto.MonsterBattleType = 4 +) func init() { handle("StartCocoonStage", func(s *Session, req *goproto.StartCocoonStageCsReq, rsp *goproto.StartCocoonStageScRsp) error { @@ -109,7 +113,11 @@ func init() { }) handle("PVEBattleResult", func(s *Session, req *goproto.PVEBattleResultCsReq, rsp *goproto.PVEBattleResultScRsp) error { - CURRENT_BATTLE.Delete(s.Token) + current, active := CURRENT_BATTLE.Load(s.Token) + active = active && current.StageId == req.StageId + if active { + CURRENT_BATTLE.Delete(s.Token) + } rsp.EndStatus = req.EndStatus rsp.BattleId = req.BattleId rsp.StageId = req.StageId @@ -130,7 +138,7 @@ func init() { req.BattleId, report.TeamTotalDamage, report.RoundCnt, report.TotalBattleTurns, statusStr)) } } - return nil + return finishChallengeBattle(s, req, rsp, active) }) // Consume every target carrier in the current schema. Only entities sent @@ -163,6 +171,9 @@ func init() { req.AssistMonsterEntityIdList, req.AssistMonsterEntityInfo, s.MonsterEntities, targets) if len(targets) == 0 { + if s.ChallengeStageID != 0 { + SceneDebugLog("[CHALLENGE] attack without monster target request=%s", req.String()) + } // ไม่มี monster จริงถูกตี — ไม่เข้า (นี่คือ fix หลัก) SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d skill_index=%d -> no real monster target (battle skipped)", req.CastEntityId, req.CasterId, req.SkillIndex) @@ -173,6 +184,25 @@ func init() { SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> with %d monster(s)", req.CastEntityId, req.CasterId, len(targets)) + // PearlSR getBattleType acknowledges non-attacking techniques without + // starting a battle. The monster still needs a NO_BATTLE result. + if req.SkillIndex == 1 { + rules, err := common.LoadTechniqueRules() + if err != nil { + return err + } + avatarID := s.techniqueAvatar(req.CasterId) + for _, id := range rules.NonAttackTechniques { + if avatarID == id { + for _, target := range targets { + rsp.MonsterBattleInfo = append(rsp.MonsterBattleInfo, &goproto.HitMonsterBattleInfo{TargetMonsterEntityId: target, MonsterBattleType: monsterBattleNoBattle}) + } + SceneDebugLog("[TECHNIQUE] non-battle avatar=%d targets=%v", avatarID, targets) + return nil + } + } + } + battleInfo, err := createBattleInfo(s, req.CasterId, req.SkillIndex) if err != nil { return err @@ -189,11 +219,12 @@ func init() { return nil }) - // ตอบ MP cost ให้ client (ตอบ cast_entity_id กลับ, ไม่ตัด MP จริง) + // Cost and recovery use the same session MP as lineup and consumables. handle("SceneCastSkillCostMp", func(s *Session, req *goproto.SceneCastSkillCostMpCsReq, rsp *goproto.SceneCastSkillCostMpScRsp) error { - rsp.Retcode = 0 rsp.CastEntityId = req.CastEntityId - return nil + code, err := s.payTechnique(req.CastEntityId, req.SkillIndex) + rsp.Retcode = code + return err }) handle("GetCurBattleInfo", func(s *Session, req *goproto.GetCurBattleInfoCsReq, rsp *goproto.GetCurBattleInfoScRsp) error { @@ -215,6 +246,24 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) (*goproto.SceneBa if player == nil { return nil, fmt.Errorf("battle: player data is not loaded") } + if s.ChallengeBattleConfig != nil { + // Keep challenge waves and team local to this session. External + // Freesr reloads must not replace the stage the player just entered. + battlePlayer := *player + battlePlayer.BattleConfig = *s.ChallengeBattleConfig + player = &battlePlayer + } else if len(player.TeamPresets) > 0 { + // A selected native preset is the party the player sees in the scene. + // Keep the imported stage/waves while taking members from that party. + battlePlayer := *player + battlePlayer.BattleConfig.CustomBattleLineup = map[uint32]uint32{} + for slot, id := range player.Lineups { + if id != 0 { + battlePlayer.BattleConfig.CustomBattleLineup[slot] = id + } + } + player = &battlePlayer + } rules, err := loadBattleRules() if err != nil { diff --git a/internal/gameserver/handlers_challenge.go b/internal/gameserver/handlers_challenge.go index fc4858e..6c83244 100644 --- a/internal/gameserver/handlers_challenge.go +++ b/internal/gameserver/handlers_challenge.go @@ -3,6 +3,7 @@ package gameserver import ( "encoding/json" "fmt" + "gac-hkrpg-go/internal/common" "os" "sort" "sync" @@ -12,12 +13,14 @@ import ( // ChallengeDataConfig mirrors challenge_data.json (Rust: ChallengeConfigData). type ChallengeDataConfig struct { - StagesList []uint32 `json:"stages_list"` - GroupsList []uint32 `json:"groups_list"` - Challenges map[uint32]ChallengeStageData `json:"challenges"` - Tierce map[uint32]ChallengeTierceStageData `json:"tierce"` - Stages map[uint32]StageBattleData `json:"stages"` - Peak map[string]PeakStageData `json:"peak"` + TargetRules map[uint32]ChallengeTargetRule `json:"-"` + Countdowns map[uint32]uint32 `json:"-"` + StagesList []uint32 `json:"stages_list"` + GroupsList []uint32 `json:"groups_list"` + Challenges map[uint32]ChallengeStageData `json:"challenges"` + Tierce map[uint32]ChallengeTierceStageData `json:"tierce"` + Stages map[uint32]StageBattleData `json:"stages"` + Peak map[string]PeakStageData `json:"peak"` } type ChallengeStageData struct { @@ -33,13 +36,22 @@ type ChallengeStageData struct { Buff uint32 `json:"buff"` } +type ChallengeTargetRule struct { + ID uint32 `json:"ID"` + Type uint32 `json:"ChallengeTargetType"` + Param uint32 `json:"ChallengeTargetParam1"` +} + type ChallengeTierceStageData struct { - Entrance uint32 `json:"entrance"` - Group uint32 `json:"group"` - Monster uint32 `json:"monster"` - Event uint32 `json:"event"` - Targets []uint32 `json:"targets"` - Score uint32 `json:"score"` + SpecialTarget uint32 `json:"-"` + ClearScore uint32 `json:"-"` + PreChallengeID uint32 `json:"-"` + Entrance uint32 `json:"entrance"` + Group uint32 `json:"group"` + Monster uint32 `json:"monster"` + Event uint32 `json:"event"` + Targets []uint32 `json:"targets"` + Score uint32 `json:"score"` } type StageBattleData struct { @@ -48,6 +60,11 @@ type StageBattleData struct { } type PeakStageData struct { + Entrance uint32 `json:"-"` + Group uint32 `json:"-"` + GroupID uint32 `json:"-"` + Buffs []uint32 `json:"-"` + HardBuffs []uint32 `json:"-"` EventID uint32 `json:"event_id"` HardEventID uint32 `json:"hard_event_id"` MonsterID uint32 `json:"monster_id"` @@ -73,10 +90,166 @@ func getChallengeData() *ChallengeDataConfig { Log("Failed to parse challenge_data.json: " + err.Error()) challengeData = ChallengeDataConfig{} } + challengeData.Countdowns = map[uint32]uint32{} + challengeData.TargetRules = map[uint32]ChallengeTargetRule{} + if raw, err := common.ReadResourceFile("resources/ChallengeTargetConfig.json"); err == nil { + var table struct { + Entries []ChallengeTargetRule `json:"challenge_targets"` + } + if json.Unmarshal(raw, &table) == nil { + for _, entry := range table.Entries { + challengeData.TargetRules[entry.ID] = entry + } + } + } + + if challengeData.Challenges == nil { + challengeData.Challenges = map[uint32]ChallengeStageData{} + } + if challengeData.Tierce == nil { + challengeData.Tierce = map[uint32]ChallengeTierceStageData{} + } + if challengeData.Stages == nil { + challengeData.Stages = map[uint32]StageBattleData{} + } + if raw, err := common.ReadResourceFile("resources/ChallengeMazeConfig.json"); err == nil { + var table struct { + Entries []struct { + ID uint32 `json:"ID"` + Countdown uint32 `json:"ChallengeCountDown"` + GroupID uint32 `json:"GroupID"` + Entrance uint32 `json:"MapEntranceID"` + Entrance2 uint32 `json:"MapEntranceID2"` + Group1 uint32 `json:"MazeGroupID1"` + Group2 uint32 `json:"MazeGroupID2"` + Monsters1 []uint32 `json:"NpcMonsterIDList1"` + Monsters2 []uint32 `json:"NpcMonsterIDList2"` + Events1 []uint32 `json:"EventIDList1"` + Events2 []uint32 `json:"EventIDList2"` + Buff uint32 `json:"MazeBuffID"` + } `json:"challenge_config"` + } + if err := json.Unmarshal(raw, &table); err == nil { + for _, entry := range table.Entries { + challengeData.Countdowns[entry.ID] = entry.Countdown + challengeData.Challenges[entry.ID] = ChallengeStageData{GroupID: entry.GroupID, Entrance: entry.Entrance, Entrance2: entry.Entrance2, Group1: entry.Group1, Group2: entry.Group2, Monster1: lastResourceID(entry.Monsters1), Monster2: lastResourceID(entry.Monsters2), Event1: lastResourceID(entry.Events1), Event2: lastResourceID(entry.Events2), Buff: entry.Buff} + } + } + } + if raw, err := common.ReadResourceFile("resources/ChallengeMazeTierceConfig.json"); err == nil { + var table struct { + Entries []struct { + ID uint32 `json:"ID"` + PreChallengeID uint32 `json:"PreChallengeMazeID"` + Entrance uint32 `json:"MapEntranceID"` + Group uint32 `json:"MazeGroupID"` + Monsters []uint32 `json:"NpcMonsterIDList"` + Events []uint32 `json:"EventIDList"` + Targets []uint32 `json:"ChallengeTargetID"` + SpecialTarget uint32 `json:"ChallengeTierceTargetID"` + ClearScore uint32 `json:"IDBJENCBJHM"` + } `json:"challenge_tierce_config"` + } + if err := json.Unmarshal(raw, &table); err == nil { + for _, entry := range table.Entries { + value := challengeData.Tierce[entry.ID] + value.PreChallengeID, value.Entrance, value.Group = entry.PreChallengeID, entry.Entrance, entry.Group + value.SpecialTarget, value.ClearScore = entry.SpecialTarget, entry.ClearScore + value.Monster, value.Event, value.Targets = lastResourceID(entry.Monsters), lastResourceID(entry.Events), entry.Targets + challengeData.Tierce[entry.ID] = value + } + } + } + if raw, err := common.ReadResourceFile("resources/StageConfig.json"); err == nil { + var table struct { + Entries []struct { + ID uint32 `json:"StageID"` + Level uint32 `json:"Level"` + Monsters [][]uint32 `json:"MonsterList"` + } `json:"stage_config"` + } + if err := json.Unmarshal(raw, &table); err == nil { + for _, entry := range table.Entries { + challengeData.Stages[entry.ID] = StageBattleData{Level: entry.Level, Monsters: entry.Monsters} + } + } + } + loadPeakResources(&challengeData) }) return &challengeData } +func loadPeakResources(data *ChallengeDataConfig) { + if data.Peak == nil { + data.Peak = map[string]PeakStageData{} + } + if raw, err := common.ReadResourceFile("resources/ChallengePeakConfig.json"); err == nil { + var table struct { + Rows []struct { + ID uint32 `json:"ID"` + Entrance uint32 `json:"MapEntranceID"` + Group uint32 `json:"MazeGroupID"` + Events []uint32 `json:"EventIDList"` + Monsters []uint32 `json:"NpcMonsterIDList"` + Buffs []uint32 `json:"TagList"` + Targets []uint32 `json:"NormalTargetList"` + } `json:"challenge_peak_config"` + } + if err := json.Unmarshal(raw, &table); err == nil { + for _, row := range table.Rows { + value := data.Peak[itoa(int(row.ID))] + value.Entrance, value.Group, value.EventID, value.MonsterID = row.Entrance, row.Group, lastResourceID(row.Events), lastResourceID(row.Monsters) + value.Buffs, value.Targets = row.Buffs, row.Targets + data.Peak[itoa(int(row.ID))] = value + } + } + } + if raw, err := common.ReadResourceFile("resources/ChallengePeakBossConfig.json"); err == nil { + var table struct { + Rows []struct { + ID uint32 `json:"ID"` + Events []uint32 `json:"HardEventIDList"` + Buffs []uint32 `json:"HardTagList"` + Target uint32 `json:"HardTarget"` + } `json:"challenge_peak_boss_config"` + } + if err := json.Unmarshal(raw, &table); err == nil { + for _, row := range table.Rows { + value := data.Peak[itoa(int(row.ID))] + value.IsBoss = true + value.HardEventID, value.HardBuffs, value.HardTarget = lastResourceID(row.Events), row.Buffs, row.Target + data.Peak[itoa(int(row.ID))] = value + } + } + } + if raw, err := common.ReadResourceFile("resources/ChallengePeakGroupConfig.json"); err == nil { + var table struct { + Rows []struct { + ID uint32 `json:"ID"` + Pre []uint32 `json:"PreLevelIDList"` + Boss uint32 `json:"BossLevelID"` + } `json:"challenge_peak_group_config"` + } + if err := json.Unmarshal(raw, &table); err == nil { + for _, row := range table.Rows { + for _, id := range append(row.Pre, row.Boss) { + key := itoa(int(id)) + value := data.Peak[key] + value.GroupID = row.ID + data.Peak[key] = value + } + } + } + } +} + +func lastResourceID(ids []uint32) uint32 { + if len(ids) == 0 { + return 0 + } + return ids[len(ids)-1] +} + func getChallengeStagesList() []uint32 { cd := getChallengeData() if len(cd.StagesList) > 0 { @@ -206,6 +379,10 @@ func init() { handle("GetCurChallenge", func(s *Session, req *goproto.GetCurChallengeCsReq, rsp *goproto.GetCurChallengeScRsp) error { rsp.Retcode = 0 + if run := s.ChallengeRun; run != nil && !run.Tierce && !run.Peak { + rsp.CurChallenge = run.Cur + rsp.LineupList = []*goproto.LineupInfo{run.activeLineup(0)} + } return nil }) @@ -274,8 +451,22 @@ func init() { }) handle("GetCurChallengePeak", func(s *Session, req *goproto.GetCurChallengePeakCsReq, rsp *goproto.GetCurChallengePeakScRsp) error { - // Match upstream's empty current-state response until a Peak is active. rsp.Retcode = 0 + if run := s.ChallengeRun; run != nil && run.Peak { + rsp.PeakId, rsp.BossBuffId, rsp.HasPassed = run.ID, run.PeakBuff, run.Finished && run.Win + } + return nil + }) + handle("SetChallengePeakBossHardMode", func(s *Session, req *goproto.SetChallengePeakBossHardModeCsReq, rsp *goproto.SetChallengePeakBossHardModeScRsp) error { + if s.PeakHardModes == nil { + s.PeakHardModes = map[uint32]bool{} + } + s.PeakHardModes[req.PeakGroupId] = req.IsHardMode + rsp.PeakGroupId, rsp.IsHardMode = req.PeakGroupId, req.IsHardMode + return nil + }) + handle("ConfirmChallengePeakSettle", func(s *Session, req *goproto.ConfirmChallengePeakSettleCsReq, rsp *goproto.ConfirmChallengePeakSettleScRsp) error { + rsp.PeakId, rsp.IOBCIMIPEOM = req.PeakId, req.IOBCIMIPEOM return nil }) diff --git a/internal/gameserver/handlers_challenge_flow.go b/internal/gameserver/handlers_challenge_flow.go index 6c98778..1d6096e 100644 --- a/internal/gameserver/handlers_challenge_flow.go +++ b/internal/gameserver/handlers_challenge_flow.go @@ -2,7 +2,6 @@ package gameserver import ( "fmt" - "sort" "google.golang.org/protobuf/proto" @@ -22,6 +21,7 @@ type decodedChallengeStart struct { thirdAvatars []uint32 stageInfoList [][]uint32 buffID uint32 + buffIDs []uint32 } // --------------------------------------------------------------------------- @@ -62,51 +62,6 @@ func skipWireField(wireType uint32, buf *[]byte) { } } -func mapChallengeStageID(id uint32) uint32 { - cd := getChallengeData() - if _, ok := cd.Challenges[id]; ok { - return id - } - if _, ok := cd.Tierce[id]; ok { - return id - } - switch { - case id >= 5400 && id <= 5700: // MoC 1035/1036 -> 5301..5312 - rem := id % 100 - if rem < 1 { - rem = 1 - } - if rem > 12 { - rem = 12 - } - return 5300 + rem - case id >= 20200 && id <= 20300: // Pure Fiction -> 20251..20254 - rem := id % 10 - if rem < 1 { - rem = 1 - } - if rem > 4 { - rem = 4 - } - return 20250 + rem - case id >= 30200 && id <= 30300: // Apocalyptic Shadow -> 30201..30204 - rem := id % 10 - if rem < 1 { - rem = 1 - } - if rem > 4 { - rem = 4 - } - return 30200 + rem - case id > 1: - if _, ok := cd.Challenges[id]; !ok { - return id - 1 - } - return id - } - return id -} - func decodeStartChallengeReq(payload []byte) decodedChallengeStart { var pbReq goproto.StartChallengeCsReq if err := proto.Unmarshal(payload, &pbReq); err != nil { @@ -131,9 +86,11 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart { if pbReq.StageInfo != nil { if info := pbReq.StageInfo.BossInfo; info != nil { req.buffID = info.BuffOne + req.buffIDs = []uint32{info.BuffOne, info.BuffTwo} } if info := pbReq.StageInfo.StoryInfo; info != nil && req.buffID == 0 { req.buffID = info.BuffOne + req.buffIDs = []uint32{info.BuffOne, info.BuffTwo} } } return req @@ -147,6 +104,7 @@ func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart { req := decodedChallengeStart{challengeID: pbReq.ChallengeId, stageIndex: pbReq.FINOCEGDBDI, isSingleStage: pbReq.MPKMIKBFEHG} for index, stage := range pbReq.LineupTierceList { + req.buffIDs = append(req.buffIDs, stage.BuffId) var avatars []uint32 for _, avatar := range stage.TierceAvatarIdList { if avatar.Id != 0 { @@ -170,13 +128,15 @@ func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart { return req } -func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) { +func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32, []uint32) { var req goproto.SetChallengeTierceLineupCsReq if err := proto.Unmarshal(payload, &req); err != nil { - return 0, nil + return 0, nil, nil } var stages [][]uint32 + var buffs []uint32 for _, stage := range req.StageInfoList { + buffs = append(buffs, stage.BuffId) var avatars []uint32 for _, avatar := range stage.TierceAvatarIdList { if avatar.Id != 0 { @@ -185,54 +145,70 @@ func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) { } stages = append(stages, avatars) } - return req.ChallengeId, stages + return req.ChallengeId, stages, buffs } // --------------------------------------------------------------------------- // config setup shared by start handlers // --------------------------------------------------------------------------- -func setChallengeBattleConfig(player *common.FreesrData, avatars []uint32, challengeID uint32, eventID uint32, buff uint32) { - customLineup := map[uint32]uint32{} - for i, aid := range avatars { - customLineup[uint32(i)] = aid +// Build challenge configuration from the selected resource stage, never from +// Freesr custom monsters. Only the current session owns this configuration. +func setChallengeBattleConfig(s *Session, avatars []uint32, challengeID, eventID uint32, buffs []uint32) error { + stage, ok := getChallengeData().Stages[eventID] + if !ok || len(stage.Monsters) == 0 { + return fmt.Errorf("challenge stage %d has no resource waves", eventID) } - player.BattleConfig.CustomBattleLineup = customLineup - player.BattleConfig.StageID = eventID - - isPF := challengeID >= 20000 && challengeID < 30000 - isAS := challengeID >= 30000 - if isPF { - player.BattleConfig.CycleCount = 4 - player.BattleConfig.BattleType = common.BattleTypePF - } else if isAS { - player.BattleConfig.CycleCount = 30 - player.BattleConfig.BattleType = common.BattleTypeAS - } else { - player.BattleConfig.CycleCount = 30 - player.BattleConfig.BattleType = common.BattleTypeDefault + config := common.BattleConfig{StageID: eventID, CustomBattleLineup: map[uint32]uint32{}} + for i, id := range avatars { + config.CustomBattleLineup[uint32(i)] = id } - - if sb, ok := getChallengeData().Stages[eventID]; ok { - var waves [][]common.Monster - for _, wave := range sb.Monsters { - var ms []common.Monster - for _, mid := range wave { - ms = append(ms, common.Monster{Level: sb.Level, MonsterID: mid}) - } - waves = append(waves, ms) + switch { + case challengeID >= 20000 && challengeID < 30000: + config.BattleType = common.BattleTypePF + case challengeID >= 30000: + config.BattleType = common.BattleTypeAS + default: + config.BattleType = common.BattleTypeDefault + } + // ChallengeCountDown is supplied by the installed challenge table. + configID := challengeID + if tierce, ok := getChallengeData().Tierce[challengeID]; ok { + configID = tierce.PreChallengeID + } + config.CycleCount = getChallengeData().Countdowns[configID] + if config.CycleCount == 0 { + // AS/PF rows omit ChallengeCountDown. Their limits come from the + // sourced challenge rules, not the user's custom battle configuration. + rules, err := loadBattleRules() + if err != nil { + return err + } + config.CycleCount = rules.ChallengeRoundLimits[config.BattleType] + if config.CycleCount == 0 { + return fmt.Errorf("missing challenge round limit for %s", config.BattleType) } - player.BattleConfig.Monsters = waves } - - if buff != 0 { - player.BattleConfig.Blessings = []common.BattleBuffJson{{ - ID: buff, - Level: 1, - }} + for _, wave := range stage.Monsters { + if len(wave) == 0 { + return fmt.Errorf("challenge stage %d has an empty wave", eventID) + } + monsters := make([]common.Monster, 0, len(wave)) + for _, id := range wave { + monsters = append(monsters, common.Monster{Level: stage.Level, MonsterID: id}) + } + config.Monsters = append(config.Monsters, monsters) } - - player.SavePersistent() + seen := map[uint32]bool{} + for _, id := range buffs { + if id != 0 && !seen[id] { + config.Blessings = append(config.Blessings, common.BattleBuffJson{ID: id, Level: 1}) + seen[id] = true + } + } + s.ChallengeBattleConfig = &config + SceneDebugLog("[CHALLENGE] battle source=resource challenge=%d stage=%d waves=%v buffs=%v", challengeID, eventID, stage.Monsters, buffs) + return nil } func lineupInfoFromAvatars(avatars []uint32, planeID uint32, extraType goproto.ExtraLineupType, index uint32) *goproto.LineupInfo { @@ -257,7 +233,7 @@ func challengeAvatarsFallback(s *Session) []uint32 { } return out } - return []uint32{1304, 1313, 1406, 1004} + return nil } func dedupeTruncate4(in []uint32) []uint32 { @@ -291,36 +267,32 @@ func handleStartChallenge(s *Session, payload []byte) error { // cap team at 4 (client TeamManager IndexOutOfRange otherwise) chosenAvatars = dedupeTruncate4(chosenAvatars) - baseID := mapChallengeStageID(req.challengeID) + baseID := req.challengeID cd := getChallengeData() - entrance, group, monster, event, buff := uint32(3000101), uint32(5), uint32(4033010), uint32(30124011), uint32(0) - if c, ok := cd.Challenges[baseID]; ok { - entrance, group, monster, event = c.Entrance, c.Group1, c.Monster1, c.Event1 - if req.buffID != 0 { - buff = req.buffID - } else { - buff = c.Buff - } - } else if t, ok := cd.Tierce[baseID]; ok { - entrance, group, monster, event = t.Entrance, t.Group, t.Monster, t.Event - buff = req.buffID + stage, ok := cd.Challenges[baseID] + if !ok { + return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } - - if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) { - Log("[CHALLENGE] Arena entrance missing in res.json, fallback to 3000101") - entrance = 3000101 + entrance, group, monster, event := stage.Entrance, stage.Group1, stage.Monster1, stage.Event1 + buff := req.buffID + if buff == 0 { + buff = stage.Buff } Log("[CHALLENGE] Start: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")") Log("[CHALLENGE] Arena: entrance=" + itoa(int(entrance)) + " group=" + itoa(int(group)) + " monster=" + itoa(int(monster)) + " event=" + itoa(int(event))) - if player := s.JSON(); player != nil { - setChallengeBattleConfig(player, chosenAvatars, req.challengeID, event, buff) + previousConfig := s.ChallengeBattleConfig + if err := setChallengeBattleConfig(s, chosenAvatars, req.challengeID, event, []uint32{buff, cd.Challenges[baseID].Buff}); err != nil { + SceneDebugLog("[CHALLENGE] start rejected: %v", err) + return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } sceneInfo, _, err := loadChallengeScene(s, entrance, group, monster, event, chosenAvatars) if err != nil { - return err + s.ChallengeBattleConfig = previousConfig + SceneDebugLog("[CHALLENGE] scene rejected: %v", err) + return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } var lineups []*goproto.LineupInfo @@ -331,16 +303,45 @@ func handleStartChallenge(s *Session, payload []byte) error { secondAvatars = chosenAvatars } secondAvatars = dedupeTruncate4(secondAvatars) - lineups = append(lineups, lineupInfoFromAvatars(secondAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 1)) + lineups = append(lineups, lineupInfoFromAvatars(secondAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType_ExtraLineupType_LineupChallenge2, 0)) curChallenge := &goproto.CurChallenge{ ChallengeId: req.challengeID, Status: goproto.ChallengeStatus_CHALLENGE_DOING, RoundCount: 0, - ScoreId: scoreIDFor(req.challengeID), + ScoreId: 0, ScoreTwo: 0, ExtraLineupType: goproto.ExtraLineupType(1), } + // PearlSR 005d2c12 challenge_mgr.zig creates this mode-specific state + // before the Client recovers its challenge instance. Omitting it causes + // ChallengeBossInstance._SyncExtraInfo to dereference a missing message. + mazeBuff := cd.Challenges[baseID].Buff + buffs := make([]uint32, 0, 2) + for _, id := range []uint32{req.buffID, mazeBuff} { + if id != 0 { + buffs = append(buffs, id) + } + } + switch { + case req.challengeID > 20000 && req.challengeID < 30000: + curChallenge.StageInfo = &goproto.ChallengeCurBuffInfo{ + CurStoryBuffs: &goproto.ChallengeStoryBuffList{BuffList: buffs}, + } + case req.challengeID > 30000: + curChallenge.StageInfo = &goproto.ChallengeCurBuffInfo{ + CurBossBuffs: &goproto.ChallengeBossBuffList{BuffList: buffs, ChallengeBossConst: 1}, + } + } + rooms := []challengeRoom{resourceRoom(stage, false, chosenAvatars, req.buffID)} + if stage.Entrance2 != 0 && stage.Event2 != 0 { + var secondBuff uint32 + if len(req.buffIDs) > 1 { + secondBuff = req.buffIDs[1] + } + rooms = append(rooms, resourceRoom(stage, true, secondAvatars, secondBuff)) + } + s.ChallengeRun = &challengeRun{ID: req.challengeID, Rooms: rooms, Scores: make([]uint32, len(rooms)), Cur: curChallenge} rsp := &goproto.StartChallengeScRsp{ Retcode: 0, @@ -348,171 +349,68 @@ func handleStartChallenge(s *Session, payload []byte) error { LineupList: lineups, Scene: sceneInfo, } + SceneDebugLog("[CHALLENGE] start id=%d mapped=%d entry=%d floor=%d world=%d mode=%d buff=%v storyState=%t bossState=%t", req.challengeID, baseID, sceneInfo.EntryId, sceneInfo.FloorId, sceneInfo.WorldId, sceneInfo.GameModeType, buffs, curChallenge.GetStageInfo().GetCurStoryBuffs() != nil, curChallenge.GetStageInfo().GetCurBossBuffs() != nil) return s.Send(rsp) } -func scoreIDFor(challengeID uint32) uint32 { - if challengeID >= 20000 && challengeID < 30000 { - return 40000 - } - return 0 -} - // handleStartChallengeTierce uses the current Tierce request schema. func handleStartChallengeTierce(s *Session, payload []byte) error { req := decodeStartChallengeTierceReq(payload) if req.challengeID == 0 { return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } - baseID := mapChallengeStageID(req.challengeID) + baseID := req.challengeID Log("handle_start_challenge_tierce: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")") - tierceLineupsMu.Lock() - if len(req.stageInfoList) > 0 { - tierceLineups[req.challengeID] = req.stageInfoList + if err := rememberTierceTeams(s, req.challengeID, req.stageInfoList, req.buffIDs, req.isSingleStage, req.stageIndex); err != nil { + return err } - tierceLineupsMu.Unlock() - - var chosenAvatars []uint32 - switch req.stageIndex { - case 0: - chosenAvatars = req.firstAvatars - case 1: - if len(req.secondAvatars) > 0 { - chosenAvatars = req.secondAvatars - } else { - chosenAvatars = req.firstAvatars - } - case 2: - if len(req.thirdAvatars) > 0 { - chosenAvatars = req.thirdAvatars - } else { - chosenAvatars = req.firstAvatars - } + req.stageInfoList = s.TierceTeams[req.challengeID] + req.buffIDs = s.TierceBuffs[req.challengeID] + if int(req.stageIndex) < len(req.buffIDs) { + req.buffID = req.buffIDs[req.stageIndex] } - if len(chosenAvatars) == 0 { - tierceLineupsMu.Lock() - if list, ok := tierceLineups[req.challengeID]; ok && int(req.stageIndex) < len(list) { - chosenAvatars = list[req.stageIndex] - } - tierceLineupsMu.Unlock() + if req.stageIndex >= tierceNodeCount { + return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } + chosenAvatars := req.stageInfoList[req.stageIndex] if len(chosenAvatars) == 0 { - chosenAvatars = challengeAvatarsFallback(s) + return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_LINEUP_IS_EMPTY)}) } cd := getChallengeData() - entrance, group, monster, event, buff := uint32(3000101), uint32(5), uint32(4033010), uint32(30124011), uint32(0) - - if req.stageIndex == 0 { - if t, ok := cd.Tierce[baseID]; ok { - entrance, group, monster, event, buff = t.Entrance, t.Group, t.Monster, t.Event, req.buffID - } else if c, ok2 := cd.Challenges[baseID]; ok2 { - entrance, group, monster, event = c.Entrance, c.Group1, c.Monster1, c.Event1 - if req.buffID != 0 { - buff = req.buffID - } else { - buff = c.Buff - } - } - } else if req.stageIndex == 1 { - // Stage 2 (Node 2) - if c, ok := cd.Challenges[baseID-1]; ok && c.Entrance2 != 0 { - entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2 - if req.buffID != 0 { - buff = req.buffID - } else { - buff = c.Buff - } - } else if c, ok2 := cd.Challenges[baseID]; ok2 && c.Entrance2 != 0 { - entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2 - if req.buffID != 0 { - buff = req.buffID - } else { - buff = c.Buff - } - } else if t, ok3 := cd.Tierce[baseID]; ok3 { - entrance, group, monster, event, buff = t.Entrance, t.Group, t.Monster, t.Event, req.buffID - } - } else { - if t, ok := cd.Tierce[baseID]; ok { - entrance, group, monster, event, buff = t.Entrance, t.Group, t.Monster, t.Event, req.buffID - } + // PearlSR challenge_mgr.zig: Tierce nodes 0/1 use the preceding + // ChallengeMazeConfig's first/second node; node 2 uses Tierce config. + tierce, tierceOK := cd.Tierce[req.challengeID] + base, baseOK := cd.Challenges[tierce.PreChallengeID] + if !tierceOK || !baseOK || req.stageIndex > 2 { + return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } - - if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) { - Log("[CHALLENGE] Tierce arena entrance missing in res.json, fallback to 3000101") - entrance = 3000101 + entrance, group, monster, event := base.Entrance, base.Group1, base.Monster1, base.Event1 + switch req.stageIndex { + case 1: + entrance, group, monster, event = base.Entrance2, base.Group2, base.Monster2, base.Event2 + case 2: + entrance, group, monster, event = tierce.Entrance, tierce.Group, tierce.Monster, tierce.Event } - - if player := s.JSON(); player != nil { - setChallengeBattleConfig(player, chosenAvatars, req.challengeID, event, buff) + previousConfig := s.ChallengeBattleConfig + if err := setChallengeBattleConfig(s, chosenAvatars, req.challengeID, event, []uint32{req.buffID, base.Buff}); err != nil { + SceneDebugLog("[CHALLENGE] tierce start rejected: %v", err) + return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } sceneInfo, _, err := loadChallengeScene(s, entrance, group, monster, event, chosenAvatars) if err != nil { - return err + s.ChallengeBattleConfig = previousConfig + SceneDebugLog("[CHALLENGE] tierce scene rejected: %v", err) + return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } - // build all lineups for tierce_info - var allLineups [][]uint32 - firstLineup := req.firstAvatars - if len(firstLineup) == 0 && len(req.stageInfoList) > 0 { - firstLineup = req.stageInfoList[0] - } - if len(firstLineup) == 0 { - tierceLineupsMu.Lock() - if list, ok := tierceLineups[req.challengeID]; ok && len(list) > 0 { - firstLineup = list[0] - } - tierceLineupsMu.Unlock() - } - if len(firstLineup) == 0 { - firstLineup = chosenAvatars - } - allLineups = append(allLineups, firstLineup) - - if !req.isSingleStage { - secondLineup := req.secondAvatars - if len(secondLineup) == 0 && len(req.stageInfoList) > 1 { - secondLineup = req.stageInfoList[1] - } - if len(secondLineup) == 0 { - tierceLineupsMu.Lock() - if list, ok := tierceLineups[req.challengeID]; ok && len(list) > 1 { - secondLineup = list[1] - } - tierceLineupsMu.Unlock() - } - if len(secondLineup) == 0 { - secondLineup = firstLineup - } - allLineups = append(allLineups, secondLineup) - - thirdLineup := req.thirdAvatars - if len(thirdLineup) == 0 && len(req.stageInfoList) > 2 { - thirdLineup = req.stageInfoList[2] - } - if len(thirdLineup) == 0 { - tierceLineupsMu.Lock() - if list, ok := tierceLineups[req.challengeID]; ok && len(list) > 2 { - thirdLineup = list[2] - } - tierceLineupsMu.Unlock() - } - if len(thirdLineup) > 0 || req.stageIndex == 2 { - if len(thirdLineup) == 0 { - thirdLineup = firstLineup - } - allLineups = append(allLineups, thirdLineup) - } - } - - var tierceLineupList []*goproto.LineupInfo - for stIdx, stageAvatars := range allLineups { - tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx))) - } + // The response carries the active node's lineup, as upstream does. + // Other nodes are cached for subsequent requests, not installed as active teams. + tierceLineupList := []*goproto.LineupInfo{lineupInfoFromAvatars(chosenAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 0)} + SceneDebugLog("[CHALLENGE] tierce id=%d node=%d entry=%d floor=%d group=%d event=%d avatars=%v", req.challengeID, req.stageIndex, entrance, sceneInfo.FloorId, group, event, chosenAvatars) // Generated response resolves its command ID through Session.Send. rsp := &goproto.StartChallengeTierceScRsp{ @@ -525,18 +423,52 @@ func handleStartChallengeTierce(s *Session, payload []byte) error { MPKMIKBFEHG: req.isSingleStage, }, } + teams := req.stageInfoList + if len(teams) == 0 { + teams = s.TierceTeams[req.challengeID] + } + rooms := make([]challengeRoom, tierceNodeCount) + for node := range rooms { + var team []uint32 + if node < len(teams) { + team = teams[node] + } + if uint32(node) == req.stageIndex { + team = chosenAvatars + } + var selectedBuff uint32 + if node < len(req.buffIDs) { + selectedBuff = req.buffIDs[node] + } + rooms[node] = resourceRoom(base, node == 1, team, selectedBuff) + if node == 2 { + rooms[node].Entrance, rooms[node].Group, rooms[node].Monster, rooms[node].Event = tierce.Entrance, tierce.Group, tierce.Monster, tierce.Event + } + } + s.ChallengeRun = &challengeRun{ID: req.challengeID, Tierce: true, Single: req.isSingleStage, Node: req.stageIndex, Rooms: rooms, Scores: make([]uint32, len(rooms)), TierceCur: rsp.CurTierceChallenge} + // A single-node retry keeps the other nodes' measured best results. + if req.isSingleStage { + if saved := s.JSON().TierceProgress[req.challengeID]; saved != nil { + for node, result := range saved.Nodes { + if node < tierceNodeCount && node != req.stageIndex { + s.ChallengeRun.Scores[node] = result.Score + s.ChallengeRun.Results = append(s.ChallengeRun.Results, tierceNodeResult(node, result, s.TierceTeams[req.challengeID][node])) + } + } + } + } + + rsp.CurTierceChallenge.LineupList = []*goproto.LineupInfo{s.ChallengeRun.activeLineup(sceneInfo.PlaneId)} return s.Send(rsp) } func handleSetChallengeTierceLineup(s *Session, payload []byte) error { - challengeID, stages := decodeSetChallengeTierceLineupReq(payload) + challengeID, stages, buffs := decodeSetChallengeTierceLineupReq(payload) if challengeID == 0 { return s.Send(&goproto.SetChallengeTierceLineupScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } - if len(stages) > 0 { - tierceLineupsMu.Lock() - tierceLineups[challengeID] = stages - tierceLineupsMu.Unlock() + if err := rememberTierceTeams(s, challengeID, stages, buffs, false, 0); err != nil { + return err } return s.Send(&goproto.SetChallengeTierceLineupScRsp{}) } @@ -556,7 +488,6 @@ func leaveChallengeScene(s *Session, response proto.Message) error { } // loadChallengeScene preserves the player's saved overworld scene. entryID := player.Scene.EntryID - player.BattleConfig.CustomBattleLineup = nil CURRENT_BATTLE.Delete(s.Token) if err := s.Send(&goproto.QuitBattleScNotify{}); err != nil { return err @@ -578,51 +509,15 @@ func handleTakeChallengeReward(s *Session, payload []byte) error { return s.Send(&goproto.TakeChallengeRewardScRsp{GroupId: req.GroupId}) } -// buildGetChallengeTierceDataScRspReal builds the generated current response. -func buildGetChallengeTierceDataScRspReal() *goproto.GetChallengeTierceDataScRsp { - cd := getChallengeData() - - type tierceEntry struct { - id uint32 - targets []uint32 - score uint32 - } - var tierceStages []tierceEntry - for id, data := range cd.Tierce { - var targets []uint32 - switch { - case len(data.Targets) > 0: - targets = data.Targets - case id >= 30000: - targets = []uint32{5001, 5002, 5003, 5000} - case id >= 20000: - targets = []uint32{4001, 4002, 4003, 4000} - default: - targets = []uint32{601, 602, 603, 600} +// Return saved teams and measured scores, including genuinely unplayed stages. +func buildGetChallengeTierceDataScRspReal(s *Session) *goproto.GetChallengeTierceDataScRsp { + rsp := &goproto.GetChallengeTierceDataScRsp{} + for _, id := range sortedU32Keys2(getChallengeData().Tierce) { + var saved *common.TierceProgress + if p := s.JSON(); p != nil { + saved = p.TierceProgress[id] } - score := data.Score - if score == 0 { - if id >= 20000 && id < 30000 { - score = 40000 - } else { - score = 4000 - } - } - tierceStages = append(tierceStages, tierceEntry{id, targets, score}) - } - sort.Slice(tierceStages, func(i, j int) bool { return tierceStages[i].id < tierceStages[j].id }) - - var groups []*goproto.ChallengeTierceGroup - for _, e := range tierceStages { - groups = append(groups, &goproto.ChallengeTierceGroup{ - ChallengeId: e.id, - IsWin: true, - FinishedTargetList: e.targets, - }) - } - - return &goproto.GetChallengeTierceDataScRsp{ - ChallengeTierceGroups: groups, - Retcode: 0, + rsp.ChallengeTierceGroups = append(rsp.ChallengeTierceGroups, buildTierceGroup(id, saved)) } + return rsp } diff --git a/internal/gameserver/handlers_challenge_scene.go b/internal/gameserver/handlers_challenge_scene.go index 0c1746b..5fa6efa 100644 --- a/internal/gameserver/handlers_challenge_scene.go +++ b/internal/gameserver/handlers_challenge_scene.go @@ -3,8 +3,6 @@ package gameserver import ( "errors" "fmt" - "math" - "sync" "google.golang.org/protobuf/proto" @@ -16,11 +14,6 @@ import ( // tierce lineup store + challenge scene loading // --------------------------------------------------------------------------- -var ( - tierceLineups = map[uint32][][]uint32{} - tierceLineupsMu sync.Mutex -) - // loadChallengeScene is the Go port of scene.rs load_challenge_scene. func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID uint32, avatarIDs []uint32) (*goproto.SceneInfo, *goproto.MotionInfo, error) { player := s.JSON() @@ -41,101 +34,38 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u return nil, nil, err } - // 1. locate the boss monster position from scene config - var monsPos common.Position - var instID uint32 = 1 - fallbackPos := common.Position{X: -61000, Y: -2141, Z: -170700, RotY: 90000} - if grp, okg := scene.Scenes[targetGroupID]; okg && len(grp.Monsters) > 0 { - m := grp.Monsters[0] - monsPos = common.Position{X: m.Pos.X, Y: m.Pos.Y, Z: m.Pos.Z, RotY: m.Rot.Y} - instID = m.InstID - } else { - found := false - for _, gid := range sortedU32Keys2(scene.Scenes) { - g := scene.Scenes[gid] - if len(g.Monsters) > 0 { - m := g.Monsters[0] - monsPos = common.Position{X: m.Pos.X, Y: m.Pos.Y, Z: m.Pos.Z, RotY: m.Rot.Y} - instID = m.InstID - found = true - break - } - } - if !found { - monsPos = fallbackPos - } + // Use the selected group's resource placement, with no guessed arena + // height or offset towards world origin (arena floors need not be there). + group, ok := scene.Scenes[targetGroupID] + if !ok || len(group.Monsters) == 0 { + return nil, nil, fmt.Errorf("challenge entrance %d group %d has no monster placement", entryID, targetGroupID) } - - // 2. position the player safely between boss and arena center - // 2. detect arena floor platform height from props in scene - // In some challenge arenas (e.g. entrance 3012602 / AS Starward 30225), - // the boss monster pos in config is placed at Y = -193081 (22m below the floor), - // while the arena stage prop (102259) is at Y = -170770. - // Spawning at monsPos.Y puts the player under the map into the void. - hasFloorProp := false - var floorY int32 = -2147483648 - for _, g := range scene.Scenes { - for _, p := range g.Props { - dx := float64(p.Pos.X) - dz := float64(p.Pos.Z) - if math.Sqrt(dx*dx+dz*dz) < 150000.0 { // within 150 meters of center - if p.Pos.Y > floorY { - floorY = p.Pos.Y - hasFloorProp = true - } - } - } - } - if hasFloorProp && floorY > monsPos.Y { - monsPos.Y = floorY + placement := group.Monsters[0] + monsPos := common.Position{X: placement.Pos.X, Y: placement.Pos.Y, Z: placement.Pos.Z, RotY: placement.Rot.Y} + instID := placement.InstID + playerMotion := monsPos.ToMotionInfo() + spawnSource := "monster-placement" + if anchors := res.SceneAnchors[entryID]; len(anchors) > 0 { + anchor := anchors[0] // PearlSR scene_mgr.zig getAnchorMotion. + playerMotion = (&common.Position{X: anchor.Pos.X, Y: anchor.Pos.Y, Z: anchor.Pos.Z, RotY: anchor.Rot.Y}).ToMotionInfo() + spawnSource = "anchor" } + SceneDebugLog("[CHALLENGE] spawn entry=%d floor=%d source=%s player=%v monster=%v", entryID, floorID, spawnSource, playerMotion, monsPos) - centerVX := float64(-monsPos.X) - centerVZ := float64(-monsPos.Z) - distToCenter := math.Sqrt(centerVX*centerVX + centerVZ*centerVZ) + s.resetTechniqueRoom() - var spawnX, spawnY, spawnZ, spawnRotY, bossRotY int32 - if distToCenter > 1000.0 { - nx := centerVX / distToCenter - nz := centerVZ / distToCenter - offset := math.Min(6000.0, distToCenter*0.6) - spawnX = monsPos.X + int32(nx*offset) - spawnZ = monsPos.Z + int32(nz*offset) - - pDeg := int32(math.Round(math.Atan2(-nx, -nz) * 180.0 / math.Pi)) - if pDeg < 0 { - pDeg += 360 - } - bDeg := int32(math.Round(math.Atan2(nx, nz) * 180.0 / math.Pi)) - if bDeg < 0 { - bDeg += 360 - } - spawnY = monsPos.Y - spawnRotY = pDeg * 1000 - bossRotY = bDeg * 1000 - } else { - spawnX, spawnY, spawnZ, spawnRotY, bossRotY = 0, monsPos.Y, 6000, 180000, 0 - } - monsPos.RotY = bossRotY - - playerMotion := &goproto.MotionInfo{ - Pos: &goproto.Vector{X: spawnX, Y: spawnY, Z: spawnZ}, - Rot: &goproto.Vector{Y: spawnRotY}, - } // NOTE: keep player.Scene intact so returning to overworld or relogging spawns in overworld sceneInfo := &goproto.SceneInfo{ FloorId: floorID, PlaneId: planeID, EntryId: entryID, - GameModeType: scene.PlaneType, + GameModeType: sceneModeChallenge, LeaderEntityId: 1, WorldId: scene.WorldID, SceneIdentifier: &goproto.SceneIdentifier{FloorId: floorID}, SceneMissionInfo: &goproto.MissionStatusBySceneInfo{}, - } - if sceneInfo.WorldId == 100 { - sceneInfo.WorldId = 501 + FloorSavedData: scene.SavedValues, } sceneInfo.LightenSectionList = scene.Sections @@ -144,7 +74,7 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u s.PropEntities = map[uint32]PropEntry{} for _, gid := range sortedU32Keys2(scene.Scenes) { group := scene.Scenes[gid] - groupInfo := &goproto.SceneEntityGroupInfo{GroupId: gid} + groupInfo := &goproto.SceneEntityGroupInfo{State: 1, GroupId: gid} for i := range group.Props { prop := &group.Props[i] propEntityID++ @@ -165,7 +95,7 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u } // single boss monster (entity 30001, registered for SceneCastSkill battles) - monsterGroup := &goproto.SceneEntityGroupInfo{GroupId: targetGroupID} + monsterGroup := &goproto.SceneEntityGroupInfo{State: 1, GroupId: targetGroupID} s.MonsterEntities = map[uint32]uint32{30001: eventID} s.ChallengeStageID = eventID monsterGroup.EntityList = append(monsterGroup.EntityList, &goproto.SceneEntityInfo{ @@ -203,7 +133,7 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, BaseAvatarId: aid, MapLayer: 0, - Uid: 1337, + Uid: playerUID, }, }, }) @@ -229,10 +159,6 @@ func handleStartChallengePeak(s *Session, req *goproto.StartChallengePeakCsReq, } eventID := stage.EventID monsterID := stage.MonsterID - buff := stage.DefaultBuff - if req.BossBuffId != 0 { - buff = req.BossBuffId - } Log("[PEAK] Start: peak_id=" + itoa(int(peakID)) + " event=" + itoa(int(eventID)) + " monster=" + itoa(int(monsterID))) avatars := dedupeTruncate4(req.PeakAvatarIdList) @@ -240,34 +166,35 @@ func handleStartChallengePeak(s *Session, req *goproto.StartChallengePeakCsReq, avatars = challengeAvatarsFallback(s) } - if player := s.JSON(); player != nil { - customLineup := map[uint32]uint32{} - for i, aid := range avatars { - customLineup[uint32(i)] = aid - } - player.BattleConfig.CustomBattleLineup = customLineup - player.BattleConfig.StageID = eventID - player.BattleConfig.CycleCount = 30 - player.BattleConfig.BattleType = common.BattleTypeDefault - player.BattleConfig.Monsters = [][]common.Monster{{{Level: 68, MonsterID: monsterID}}} - if buff != 0 { - player.BattleConfig.Blessings = []common.BattleBuffJson{{ID: buff, Level: 1}} - } - player.SavePersistent() + buffs := append([]uint32(nil), stage.Buffs...) + if stage.IsBoss && s.PeakHardModes[stage.GroupID] { + eventID = stage.HardEventID + buffs = append([]uint32(nil), stage.HardBuffs...) } - - // standard arena 3000101 (group 2 = boss position) - sceneInfo, _, err := loadChallengeScene(s, 3000101, 2, monsterID, eventID, avatars) - if err != nil { - Log("[PEAK] Failed to load peak arena scene!") + if req.BossBuffId != 0 { + buffs = append(buffs, req.BossBuffId) + } + if stage.Entrance == 0 || stage.Group == 0 || eventID == 0 || len(avatars) == 0 { + rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) return nil } - - custom := map[uint32]uint32{} - for i, aid := range avatars { - custom[uint32(i)] = aid + previousConfig := s.ChallengeBattleConfig + if err := setChallengeBattleConfig(s, avatars, peakID, eventID, buffs); err != nil { + SceneDebugLog("[PEAK] stage rejected: %v", err) + rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) + return nil } - lineupInfo := common.ToLineupInfo(custom) + s.ChallengeBattleConfig.BattleType = common.BattleTypeAA + sceneInfo, _, err := loadChallengeScene(s, stage.Entrance, stage.Group, monsterID, eventID, avatars) + if err != nil { + s.ChallengeBattleConfig = previousConfig + SceneDebugLog("[PEAK] scene rejected: %v", err) + rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) + return nil + } + s.ChallengeRun = &challengeRun{ID: peakID, Peak: true, Single: true, Rooms: []challengeRoom{{Entrance: stage.Entrance, Group: stage.Group, Monster: monsterID, Event: eventID, Avatars: avatars, Buffs: buffs}}, Scores: make([]uint32, 1), PeakBuff: req.BossBuffId} + + lineupInfo := lineupInfoFromAvatars(avatars, sceneInfo.PlaneId, goproto.ExtraLineupType_ExtraLineupType_LineupChallenge, 0) return s.Send(&goproto.EnterSceneByServerScNotify{ Scene: sceneInfo, diff --git a/internal/gameserver/handlers_lineup.go b/internal/gameserver/handlers_lineup.go index d8e498c..266600b 100644 --- a/internal/gameserver/handlers_lineup.go +++ b/internal/gameserver/handlers_lineup.go @@ -3,110 +3,212 @@ package gameserver import ( "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" + "strings" + "unicode/utf16" + "unicode/utf8" ) func commonToLineupAvatars(f *common.FreesrData) []*goproto.LineupAvatar { return common.ToLineupAvatars(f) } - func init() { - handle("GetAllLineupData", func(s *Session, req *goproto.GetAllLineupDataCsReq, rsp *goproto.GetAllLineupDataScRsp) error { - player := s.JSON() - if player == nil { - return nil + handle("GetAllLineupData", func(s *Session, _ *goproto.GetAllLineupDataCsReq, rsp *goproto.GetAllLineupDataScRsp) error { + var err error + rsp.LineupList, err = allPresetLineups(s) + if p := s.JSON(); p != nil { + rsp.CurIndex = p.ActiveTeamPreset } - rsp.LineupList = append(rsp.LineupList, &goproto.LineupInfo{ - ExtraLineupType: goproto.ExtraLineupType_ExtraLineupType_LineupNone, - Name: "Squad 1", - Mp: 5, - MaxMp: 5, - AvatarList: commonToLineupAvatars(player), - }) + return err + }) + handle("GetCurLineupData", func(s *Session, _ *goproto.GetCurLineupDataCsReq, rsp *goproto.GetCurLineupDataScRsp) error { + rsp.Lineup = s.techniqueLineup() return nil }) - - handle("GetCurLineupData", func(s *Session, req *goproto.GetCurLineupDataCsReq, rsp *goproto.GetCurLineupDataScRsp) error { - player := s.JSON() - if player == nil { - return nil - } - rsp.Lineup = &goproto.LineupInfo{ - ExtraLineupType: goproto.ExtraLineupType_ExtraLineupType_LineupNone, - Name: "Squad 1", - Mp: 5, - MaxMp: 5, - AvatarList: commonToLineupAvatars(player), - } - return nil - }) - handle("JoinLineup", func(s *Session, req *goproto.JoinLineupCsReq, rsp *goproto.JoinLineupScRsp) error { - player := s.JSON() - if player == nil { + code, err := s.presetEditCode(req.Index, req.ExtraLineupType, req.IsVirtual) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + saved := s.JSON().TeamPreset(req.Index) + saved.Avatars[req.Slot] = req.BaseAvatarId + if code := validatePresetMembers(s.JSON(), saved.Avatars); code != 0 { + rsp.Retcode = code return nil } - player.Lineups[req.Slot] = req.BaseAvatarId - player.SavePersistent() - return refreshLineup(s) + return s.savePreset(req.Index, saved) }) - handle("ReplaceLineup", func(s *Session, req *goproto.ReplaceLineupCsReq, rsp *goproto.ReplaceLineupScRsp) error { - player := s.JSON() - if player == nil { + code, err := s.presetEditCode(req.Index, req.ExtraLineupType, req.IsVirtual) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + saved := s.JSON().TeamPreset(req.Index) + saved.Avatars = map[uint32]uint32{} + for _, a := range req.LineupSlotList { + saved.Avatars[a.Slot] = a.Id + } + if code := validatePresetMembers(s.JSON(), saved.Avatars); code != 0 { + rsp.Retcode = code return nil } - for _, item := range req.LineupSlotList { - player.Lineups[item.Slot] = item.Id - } - player.SavePersistent() - return refreshLineup(s) - }) - - handle("SwapLineup", func(s *Session, req *goproto.SwapLineupCsReq, rsp *goproto.SwapLineupScRsp) error { - player := s.JSON() - if player == nil { + if req.Index == s.JSON().ActiveTeamPreset && len(common.ToLineupAvatars(&common.FreesrData{Lineups: saved.Avatars, Avatars: s.JSON().Avatars})) == 0 { + rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_IS_EMPTY) return nil } - // MPIEGFCNBEN / ENBIBCPPLDC are the obfuscated src/dst slots; the swap - // is symmetric so the mapping direction doesn't matter. - srcSlot, dstSlot := req.MPIEGFCNBEN, req.ENBIBCPPLDC - srcAvatar := player.Lineups[srcSlot] - dstAvatar := player.Lineups[dstSlot] - player.Lineups[srcSlot] = dstAvatar - player.Lineups[dstSlot] = srcAvatar - player.SavePersistent() - return refreshLineup(s) - }) - - handle("SwitchLineupIndex", func(s *Session, req *goproto.SwitchLineupIndexCsReq, rsp *goproto.SwitchLineupIndexScRsp) error { - return refreshLineup(s) - }) - - handle("GetLineupAvatarData", func(s *Session, req *goproto.GetLineupAvatarDataCsReq, rsp *goproto.GetLineupAvatarDataScRsp) error { - player := s.JSON() - if player != nil { - for _, slot := range player.SortedLineupKeys() { - avatarID := player.Lineups[slot] - if avatarID == 0 { - continue + saved.LeaderSlot = req.LeaderSlot + if saved.Avatars[saved.LeaderSlot] == 0 { + for _, slot := range sortedU32Keys2(saved.Avatars) { + if saved.Avatars[slot] != 0 { + saved.LeaderSlot = slot + break } - rsp.AvatarDataList = append(rsp.AvatarDataList, &goproto.LineupAvatarData{ - AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, - Hp: 10000, - Id: avatarID, - }) } } - rsp.Retcode = 0 - return nil + return s.savePreset(req.Index, saved) + }) + handle("SwapLineup", func(s *Session, req *goproto.SwapLineupCsReq, rsp *goproto.SwapLineupScRsp) error { + code, err := s.presetEditCode(req.Index, req.ExtraLineupType, req.IsVirtual) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + saved := s.JSON().TeamPreset(req.Index) + src, dst := req.MPIEGFCNBEN, req.ENBIBCPPLDC + saved.Avatars[src], saved.Avatars[dst] = saved.Avatars[dst], saved.Avatars[src] + if code := validatePresetMembers(s.JSON(), saved.Avatars); code != 0 { + rsp.Retcode = code + return nil + } + if saved.LeaderSlot == src { + saved.LeaderSlot = dst + } else if saved.LeaderSlot == dst { + saved.LeaderSlot = src + } + return s.savePreset(req.Index, saved) + }) + handle("SwitchLineupIndex", func(s *Session, req *goproto.SwitchLineupIndexCsReq, rsp *goproto.SwitchLineupIndexScRsp) error { + code, err := s.presetEditCode(req.Index, goproto.ExtraLineupType_ExtraLineupType_LineupNone, false) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + if s.ChallengeRun != nil { + rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_NOT_ALLOW_EDIT) + return nil + } + p := s.JSON() + saved := p.TeamPreset(req.Index) + tmp := *p + tmp.Lineups = saved.Avatars + if len(common.ToLineupAvatars(&tmp)) == 0 { + rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_IS_EMPTY) + return nil + } + // Retain the outgoing team, including edits made through existing APIs. + if err := p.SaveTeamPreset(p.ActiveTeamPreset, p.TeamPreset(p.ActiveTeamPreset)); err != nil { + return err + } + previous := p.ActiveTeamPreset + p.ActiveTeamPreset = req.Index + if err := p.SaveTeamPreset(req.Index, saved); err != nil { + p.ActiveTeamPreset = previous + return err + } + rsp.Index = req.Index + return s.publishPreset(req.Index, saved) + }) + handle("SetLineupName", func(s *Session, req *goproto.SetLineupNameCsReq, rsp *goproto.SetLineupNameScRsp) error { + code, err := s.presetEditCode(req.Index, goproto.ExtraLineupType_ExtraLineupType_LineupNone, false) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + rules, err := common.LoadTeamPresetRules() + if err != nil { + return err + } + name := strings.TrimSpace(req.Name) + if !utf8.ValidString(name) || name == "" || len(utf16.Encode([]rune(name))) > rules.MaxNameLength { + rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_NAME_FORMAT_ERROR) + return nil + } + saved := s.JSON().TeamPreset(req.Index) + saved.Name = name + rsp.Index = req.Index + rsp.Name = name + return s.savePreset(req.Index, saved) + }) + handle("MarkPresetLineup", func(s *Session, req *goproto.MarkPresetLineupCsReq, rsp *goproto.MarkPresetLineupScRsp) error { + code, err := s.presetEditCode(req.Index, goproto.ExtraLineupType_ExtraLineupType_LineupNone, false) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + saved := s.JSON().TeamPreset(req.Index) + saved.Favourite = req.IECKKCHJMLP + rsp.Index = req.Index + rsp.IECKKCHJMLP = req.IECKKCHJMLP + return s.savePreset(req.Index, saved) }) - handle("QuitLineup", func(s *Session, req *goproto.QuitLineupCsReq, rsp *goproto.QuitLineupScRsp) error { + code, err := s.presetEditCode(req.Index, req.ExtraLineupType, req.IsVirtual) + rsp.Retcode = code + if err != nil || code != 0 { + return err + } + saved := s.JSON().TeamPreset(req.Index) + for slot, id := range saved.Avatars { + if id == req.BaseAvatarId { + delete(saved.Avatars, slot) + } + } + var nonempty bool + for _, id := range saved.Avatars { + if id != 0 { + nonempty = true + } + } + if !nonempty && req.Index == s.JSON().ActiveTeamPreset { + rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_ONLY_ONE_MEMBER) + return nil + } + if saved.Avatars[saved.LeaderSlot] == 0 { + for _, slot := range sortedU32Keys2(saved.Avatars) { + if saved.Avatars[slot] != 0 { + saved.LeaderSlot = slot + break + } + } + } + return s.savePreset(req.Index, saved) + }) + handle("GetLineupAvatarData", func(s *Session, _ *goproto.GetLineupAvatarDataCsReq, rsp *goproto.GetLineupAvatarDataScRsp) error { + if lineup := s.techniqueLineup(); lineup != nil { + for _, a := range lineup.AvatarList { + rsp.AvatarDataList = append(rsp.AvatarDataList, &goproto.LineupAvatarData{AvatarType: a.AvatarType, Hp: a.Hp, Id: a.Id}) + } + } return nil }) - handle("ChangeLineupLeader", func(s *Session, req *goproto.ChangeLineupLeaderCsReq, rsp *goproto.ChangeLineupLeaderScRsp) error { - rsp.Slot = req.Slot + if lineup := s.techniqueLineup(); lineup != nil { + for _, a := range lineup.AvatarList { + if a.Slot == req.Slot { + s.leaderSlot = req.Slot + rsp.Slot = req.Slot + if s.ChallengeRun == nil { + p := s.JSON() + saved := p.TeamPreset(p.ActiveTeamPreset) + saved.LeaderSlot = req.Slot + return p.SaveTeamPreset(p.ActiveTeamPreset, saved) + } + return nil + } + } + } + rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_NOT_VALID_LEADER) return nil }) } @@ -118,13 +220,7 @@ func refreshLineup(s *Session) error { return nil } - lineup := &goproto.LineupInfo{ - ExtraLineupType: goproto.ExtraLineupType_ExtraLineupType_LineupNone, - Name: "Squad 1", - AvatarList: commonToLineupAvatars(player), - MaxMp: 5, - Mp: 5, - } + lineup := s.techniqueLineup() notify := &goproto.SceneGroupRefreshScNotify{ FloorId: player.Scene.FloorID, @@ -135,8 +231,18 @@ func refreshLineup(s *Session) error { State: 0, GroupRefreshType: goproto.SceneGroupRefreshType_SCENE_GROUP_REFRESH_TYPE_LOADED, } + for _, slot := range sortedU32Keys2(s.partyActorSlots) { + if player.Lineups[slot] == 0 { + group.RefreshEntity = append(group.RefreshEntity, &goproto.SceneEntityRefreshInfo{RefreshType: &goproto.SceneEntityRefreshInfo_DeleteEntity{DeleteEntity: slot + 1}}) + } + } + s.partyActorSlots = map[uint32]bool{} for _, slot := range player.SortedLineupKeys() { avatarID := player.Lineups[slot] + if avatarID == 0 { + continue + } + s.partyActorSlots[slot] = true group.RefreshEntity = append(group.RefreshEntity, &goproto.SceneEntityRefreshInfo{ RefreshType: &goproto.SceneEntityRefreshInfo_AddEntity{ AddEntity: &goproto.SceneEntityInfo{ diff --git a/internal/gameserver/handlers_misc.go b/internal/gameserver/handlers_misc.go index 89f0dfb..3f01377 100644 --- a/internal/gameserver/handlers_misc.go +++ b/internal/gameserver/handlers_misc.go @@ -83,6 +83,11 @@ func init() { rsp.MaterialList = append(rsp.MaterialList, material) } } + foods, err := techniqueMaterials(player) + if err != nil { + return err + } + rsp.MaterialList = append(rsp.MaterialList, foods...) // Avatar IDs do not imply item IDs. The old 130000+avatar formula // produced nonexistent materials and broke the client's inventory init. return nil diff --git a/internal/gameserver/handlers_player.go b/internal/gameserver/handlers_player.go index b0fa58b..e631522 100644 --- a/internal/gameserver/handlers_player.go +++ b/internal/gameserver/handlers_player.go @@ -157,7 +157,7 @@ const heartbeatLua = `local function beta_text() if gameObject then local textComponent = gameObject:GetComponentInChildren(typeof(CS.RPG.Client.LocalizedText)) if textComponent then - textComponent.text = "GaC hkrpg GO server" + textComponent.text = "GaC hkrpg GO แล้วเรามาเกกันนะ" end end end diff --git a/internal/gameserver/handlers_raw.go b/internal/gameserver/handlers_raw.go index 9860c65..c93be91 100644 --- a/internal/gameserver/handlers_raw.go +++ b/internal/gameserver/handlers_raw.go @@ -58,6 +58,9 @@ func init() { handle("GetChallengeTierceController", func(s *Session, req *goproto.GetChallengeTierceControllerCsReq, rsp *goproto.GetChallengeTierceControllerScRsp) error { rsp.Retcode = 0 + if run := s.ChallengeRun; run != nil && run.Tierce { + rsp.CurTierceChallenge = run.TierceCur + } return nil }) handle("StartChallengePeak", handleStartChallengePeak) @@ -130,7 +133,7 @@ func challengeGroupStatsReply(s *Session, payload []byte) error { // challengeTierceReply sends only the response paired with the current request. func challengeTierceReply(s *Session, payload []byte) error { - return s.Send(buildGetChallengeTierceDataScRspReal()) + return s.Send(buildGetChallengeTierceDataScRspReal(s)) } func readUvarint(b []byte) (uint64, int) { diff --git a/internal/gameserver/handlers_scene.go b/internal/gameserver/handlers_scene.go index b4c56c8..c371359 100644 --- a/internal/gameserver/handlers_scene.go +++ b/internal/gameserver/handlers_scene.go @@ -16,6 +16,13 @@ import ( // Scene loading (Go port of handlers/scene.rs) // --------------------------------------------------------------------------- +// Protocol scene modes from PearlSR 005d2c12, manager/scene_mgr.zig and +// services/challenge.zig. MazePlane.PlaneType uses a different enumeration. +const ( + sceneModeAdventure uint32 = 1 + sceneModeChallenge uint32 = 4 +) + // firstSceneOfEntry returns the (configName, config) pair for an entry. // Rust iterated a HashMap arbitrarily; we pick the lexicographically first // name for determinism. @@ -291,7 +298,7 @@ func init() { } if err := s.Send(&goproto.EnterSceneByServerScNotify{ Scene: sceneInfo, - Lineup: common.ToLineupInfo(player.Lineups), + Lineup: s.techniqueLineup(), }); err != nil { return err } @@ -889,15 +896,12 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo FloorId: floorID, PlaneId: planeID, EntryId: entryID, - GameModeType: scene.PlaneType, + GameModeType: sceneModeAdventure, LeaderEntityId: 1, WorldId: scene.WorldID, SceneIdentifier: sceneIdent, FloorSavedData: scene.SavedValues, } - if sceneInfo.WorldId == 100 { - sceneInfo.WorldId = 501 - } sceneInfo.LightenSectionList = scene.Sections missionInfo := &goproto.MissionStatusBySceneInfo{} @@ -929,6 +933,8 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo s.PropEntities = map[uint32]PropEntry{} s.MonsterEntities = map[uint32]uint32{} s.ChallengeStageID = 0 + s.ChallengeBattleConfig = nil + s.ChallengeRun = nil for _, groupID := range sortedU32Keys2(scene.Scenes) { group := scene.Scenes[groupID] @@ -1003,9 +1009,15 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo } // player entities (lineup slots 0..3 -> entity ids 1..4) + s.partyActorSlots = map[uint32]bool{} + s.leaderSlot = player.TeamPreset(player.ActiveTeamPreset).LeaderSlot playerGroup := &goproto.SceneEntityGroupInfo{State: 0, GroupId: 0} for _, slot := range player.SortedLineupKeys() { avatarID := player.Lineups[slot] + if avatarID == 0 { + continue + } + s.partyActorSlots[slot] = true playerGroup.EntityList = append(playerGroup.EntityList, &goproto.SceneEntityInfo{ EntityId: slot + 1, Motion: playerPos, @@ -1019,7 +1031,17 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo }, }) } + if player.Lineups[s.leaderSlot] == 0 { + for _, slot := range player.SortedLineupKeys() { + if player.Lineups[slot] != 0 { + s.leaderSlot = slot + break + } + } + } + sceneInfo.LeaderEntityId = s.leaderSlot + 1 sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, playerGroup) + SceneDebugLog("[SCENE] entry=%d floor=%d plane=%d world=%d mode=%d savedValues=%d groups=%v", entryID, floorID, planeID, sceneInfo.WorldId, sceneInfo.GameModeType, len(sceneInfo.FloorSavedData), sortedU32Keys2(scene.Scenes)) if isEnterScene { // in-memory only: the scene is persisted by SceneEntityMove once the @@ -1034,7 +1056,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo if sendNotify { if err := s.Send(&goproto.EnterSceneByServerScNotify{ Scene: sceneInfo, - Lineup: common.ToLineupInfo(player.Lineups), + Lineup: s.techniqueLineup(), }); err != nil { return nil, err } diff --git a/internal/gameserver/handlers_ui.go b/internal/gameserver/handlers_ui.go index 4cfa46f..c400e26 100644 --- a/internal/gameserver/handlers_ui.go +++ b/internal/gameserver/handlers_ui.go @@ -54,9 +54,13 @@ func init() { // Implement real RotaterData before answering this one. handle("GetPamSkinData", func(s *Session, req *goproto.GetPamSkinDataCsReq, rsp *goproto.GetPamSkinDataScRsp) error { - rsp.Retcode = 0 - rsp.CurSkin = 0 - rsp.UnlockSkinList = []uint32{0} + profile, err := loadProfileResources() + if err != nil { + return err + } + rsp.CurSkin = profile.CurrentPamSkin + rsp.UnlockSkinList = append([]uint32(nil), profile.PamSkins...) + SceneDebugLog("GET_PAM_SKIN current=%d owned=%v", rsp.CurSkin, rsp.UnlockSkinList) return nil }) diff --git a/internal/gameserver/profile_resources.go b/internal/gameserver/profile_resources.go index 2b29ec7..1308b95 100644 --- a/internal/gameserver/profile_resources.go +++ b/internal/gameserver/profile_resources.go @@ -10,7 +10,9 @@ import ( ) type profileResources struct { - Source struct { + PamSkins []uint32 `json:"pam_skins"` + CurrentPamSkin uint32 `json:"current_pam_skin"` + Source struct { Revision string `json:"revision"` } `json:"source"` PersonalCards []uint32 `json:"personal_cards"` @@ -44,6 +46,37 @@ func loadProfileResources() (*profileResources, error) { profileError = fmt.Errorf("profile source, head frame or selected personal card is invalid") return } + if profileData.CurrentPamSkin == 0 || !containsU32(profileData.PamSkins, profileData.CurrentPamSkin) { + profileError = fmt.Errorf("selected Pam skin is missing or not owned") + return + } + pamRaw, err := common.ReadResourceFile("resources/PamSkinConfig.json") + if err != nil { + profileError = err + return + } + var pamTable struct { + Rows []struct { + ID uint32 `json:"SkinID"` + Path string `json:"ConfigEntityPath"` + } `json:"pam_skin_config"` + } + if err := json.Unmarshal(pamRaw, &pamTable); err != nil { + profileError = err + return + } + validSkins := map[uint32]bool{} + for _, row := range pamTable.Rows { + if row.ID != 0 && row.Path != "" { + validSkins[row.ID] = true + } + } + for _, id := range profileData.PamSkins { + if !validSkins[id] { + profileError = fmt.Errorf("Pam skin %d has no Client entity config", id) + return + } + } if !containsU32(profileData.PhoneCases, profileData.CurrentPhoneCase) { profileError = fmt.Errorf("selected phone case is not owned") return diff --git a/internal/gameserver/progression_sync.go b/internal/gameserver/progression_sync.go index d5f2b08..b5d7def 100644 --- a/internal/gameserver/progression_sync.go +++ b/internal/gameserver/progression_sync.go @@ -26,6 +26,7 @@ func syncPersistedExRanks(s *Session) error { if err != nil { return err } + player.RefreshTechniqueSettings(p) changed := player.RefreshExRanks(p) if s.pendingExRankSync == nil { s.pendingExRankSync = make(map[uint32]bool) diff --git a/internal/gameserver/session.go b/internal/gameserver/session.go index 6608bfe..a0bb1a9 100644 --- a/internal/gameserver/session.go +++ b/internal/gameserver/session.go @@ -45,9 +45,18 @@ type Session struct { // spawned in the current scene. Used by SceneCastSkill to decide whether an // attack actually hit a monster (the camera-detach bug fix). MonsterEntities map[uint32]uint32 + partyActorSlots map[uint32]bool // Set by the challenge scene loader, cleared when loading an overworld scene. - ChallengeStageID uint32 - pendingExRankSync map[uint32]bool + ChallengeStageID uint32 + ChallengeBattleConfig *common.BattleConfig + ChallengeRun *challengeRun + PeakHardModes map[uint32]bool + TierceTeams map[uint32][][]uint32 + TierceBuffs map[uint32][]uint32 + pendingExRankSync map[uint32]bool + techniqueMP *uint32 + leaderSlot uint32 + paidTechniqueCasts map[uint32]bool closed atomic.Bool closeOnce sync.Once @@ -89,6 +98,9 @@ func (s *Session) JSON() *common.FreesrData { // SetJSON installs (or reloads) the player data. func (s *Session) SetJSON(d *common.FreesrData) { s.jsonMu.Lock() + if s.json == nil && d != nil { + s.leaderSlot = d.TeamPreset(d.ActiveTeamPreset).LeaderSlot + } s.json = d s.jsonMu.Unlock() } diff --git a/internal/gameserver/team_presets.go b/internal/gameserver/team_presets.go new file mode 100644 index 0000000..6ed80e4 --- /dev/null +++ b/internal/gameserver/team_presets.go @@ -0,0 +1,103 @@ +package gameserver + +import ( + "gac-hkrpg-go/internal/common" + goproto "gac-hkrpg-go/protocol/proto" +) + +func presetLineup(p *common.FreesrData, index uint32) *goproto.LineupInfo { + saved := p.TeamPreset(index) + lineup := common.ToLineupInfo(saved.Avatars) + tmp := *p + tmp.Lineups = saved.Avatars + lineup.AvatarList = common.ToLineupAvatars(&tmp) + lineup.Index = index + lineup.Name = saved.Name + lineup.LeaderSlot = saved.LeaderSlot + lineup.PlaneId = p.Scene.PlaneID + lineup.IECKKCHJMLP = saved.Favourite && len(lineup.AvatarList) > 0 + if saved.Avatars[lineup.LeaderSlot] == 0 { + for _, a := range lineup.AvatarList { + lineup.LeaderSlot = a.Slot + break + } + } + return lineup +} +func allPresetLineups(s *Session) ([]*goproto.LineupInfo, error) { + rules, err := common.LoadTeamPresetRules() + if err != nil { + return nil, err + } + p := s.JSON() + if p == nil { + return nil, nil + } + var out []*goproto.LineupInfo + for index := uint32(0); index < rules.Count; index++ { + out = append(out, presetLineup(p, index)) + } + return out, nil +} +func (s *Session) presetEditCode(index uint32, extra goproto.ExtraLineupType, virtual bool) (uint32, error) { + rules, err := common.LoadTeamPresetRules() + if err != nil { + return 0, err + } + if s.JSON() == nil { + return uint32(goproto.Retcode_RET_LINEUP_NOT_EXIST), nil + } + if index >= rules.Count { + return uint32(goproto.Retcode_RET_LINEUP_INVALID_INDEX), nil + } + if extra != goproto.ExtraLineupType_ExtraLineupType_LineupNone || virtual { + return uint32(goproto.Retcode_RET_LINEUP_TYPE_NOT_MATCH), nil + } + if _, active := CURRENT_BATTLE.Load(s.Token); active { + return uint32(goproto.Retcode_RET_LINEUP_NOT_ALLOW_EDIT), nil + } + return 0, nil +} +func (s *Session) savePreset(index uint32, saved *common.TeamPreset) error { + if err := s.JSON().SaveTeamPreset(index, saved); err != nil { + return err + } + return s.publishPreset(index, saved) +} +func (s *Session) publishPreset(index uint32, saved *common.TeamPreset) error { + if index == s.JSON().ActiveTeamPreset && s.ChallengeRun == nil { + s.leaderSlot = saved.LeaderSlot + if err := refreshLineup(s); err != nil { + return err + } + } + lineups, err := allPresetLineups(s) + if err != nil { + return err + } + SceneDebugLog("[TEAM PRESET] saved index=%d name=%q team=%v", index, saved.Name, saved.Avatars) + return s.Send(&goproto.SyncAllPresetLineupNotify{CurIndex: s.JSON().ActiveTeamPreset, LineupList: lineups}) +} +func validatePresetMembers(p *common.FreesrData, avatars map[uint32]uint32) uint32 { + rules, err := common.LoadTeamPresetRules() + if err != nil { + return uint32(goproto.Retcode_RET_LINEUP_NOT_EXIST) + } + seen := map[uint32]bool{} + for slot, id := range avatars { + if slot >= rules.MaxMembers { + return uint32(goproto.Retcode_RET_LINEUP_INVALID_MEMBER_POS) + } + if id == 0 { + continue + } + if p.Avatars[id] == nil { + return uint32(goproto.Retcode_RET_LINEUP_AVATAR_NOT_EXIST) + } + if seen[id] { + return uint32(goproto.Retcode_RET_LINEUP_AVATAR_ALREADY_IN) + } + seen[id] = true + } + return 0 +} diff --git a/internal/gameserver/technique.go b/internal/gameserver/technique.go new file mode 100644 index 0000000..d9023c7 --- /dev/null +++ b/internal/gameserver/technique.go @@ -0,0 +1,173 @@ +package gameserver + +import ( + "fmt" + "gac-hkrpg-go/internal/common" + goproto "gac-hkrpg-go/protocol/proto" +) + +func (s *Session) techniqueLineup() *goproto.LineupInfo { + var lineup *goproto.LineupInfo + if run := s.ChallengeRun; run != nil { + lineup = run.activeLineup(0) + } else if p := s.JSON(); p != nil { + lineup = presetLineup(p, p.ActiveTeamPreset) + } else { + return nil + } + rules, err := common.LoadTechniqueRules() + if err != nil { + return lineup + } + if s.techniqueMP == nil { + v := rules.InitialMP + s.techniqueMP = &v + } + if *s.techniqueMP > lineup.MaxMp { + *s.techniqueMP = lineup.MaxMp + } + lineup.Mp = *s.techniqueMP + if int(s.leaderSlot) < len(lineup.AvatarList) { + lineup.LeaderSlot = s.leaderSlot + } + return lineup +} +func (s *Session) techniqueAvatar(entity uint32) uint32 { + lineup := s.techniqueLineup() + if lineup == nil { + return 0 + } + if entity == 0 { + entity = lineup.LeaderSlot + 1 + } + for _, a := range lineup.AvatarList { + if a.Slot+1 == entity { + return a.Id + } + } + return 0 +} +func (s *Session) resetTechniqueRoom() { + s.techniqueMP = nil + s.leaderSlot = 0 + s.paidTechniqueCasts = nil +} +func (s *Session) syncTechnique(reason goproto.SyncLineupReason) error { + return s.Send(&goproto.SyncLineupNotify{Lineup: s.techniqueLineup(), ReasonList: []goproto.SyncLineupReason{reason}}) +} +func techniqueMaterials(p *common.FreesrData) ([]*goproto.Material, error) { + if err := p.EnsureTechniqueFoods(); err != nil { + return nil, err + } + var out []*goproto.Material + for _, id := range sortedU32Keys2(p.TechniqueFoodStock) { + out = append(out, &goproto.Material{Tid: id, Num: p.TechniqueFoodStock[id]}) + } + return out, nil +} +func (s *Session) consumeTechniqueFood(id, count uint32) error { + p := s.JSON() + if p == nil { + return fmt.Errorf("no player") + } + if _, active := CURRENT_BATTLE.Load(s.Token); active { + return fmt.Errorf("cannot eat during battle") + } + rules, err := common.LoadTechniqueRules() + if err != nil { + return err + } + food, ok := rules.Foods[id] + if !ok || count == 0 || count > food.MaxUse { + return fmt.Errorf("unsupported food or count") + } + if err := p.EnsureTechniqueFoods(); err != nil { + return err + } + if p.TechniqueFoodStock[id] < count { + return fmt.Errorf("food %d is exhausted", id) + } + lineup := s.techniqueLineup() + if lineup == nil || lineup.Mp >= lineup.MaxMp { + return fmt.Errorf("technique points are full") + } + oldStock, oldMP := p.TechniqueFoodStock[id], *s.techniqueMP + p.TechniqueFoodStock[id] -= count + gain := uint64(food.RecoverMP)*uint64(count) + uint64(oldMP) + if gain > uint64(lineup.MaxMp) { + gain = uint64(lineup.MaxMp) + } + *s.techniqueMP = uint32(gain) + if err := p.SavePersistentChecked(); err != nil { + p.TechniqueFoodStock[id] = oldStock + *s.techniqueMP = oldMP + return err + } + if err := s.Send(&goproto.PlayerSyncScNotify{MaterialList: []*goproto.Material{{Tid: id, Num: p.TechniqueFoodStock[id]}}}); err != nil { + return err + } + SceneDebugLog("[TECHNIQUE] food=%d count=%d mp=%d/%d remaining=%d", id, count, *s.techniqueMP, lineup.MaxMp, p.TechniqueFoodStock[id]) + return s.syncTechnique(goproto.SyncLineupReason_SyncLineupReason_SyncReasonMpAdd) +} +func (s *Session) payTechnique(castID, skillIndex uint32) (uint32, error) { + if s.paidTechniqueCasts[castID] { + return 0, nil + } + rules, err := common.LoadTechniqueRules() + if err != nil { + return 0, err + } + avatar := s.techniqueAvatar(0) + skills := rules.Avatars[avatar] + if int(skillIndex) >= len(skills) { + return uint32(goproto.Retcode_RET_MAZE_NO_ABILITY), nil + } + cost := skills[skillIndex].MPCost + lineup := s.techniqueLineup() + if lineup == nil { + return uint32(goproto.Retcode_RET_LINEUP_IS_EMPTY), nil + } + p := s.JSON() + if err := p.EnsureTechniqueFoods(); err != nil { + return 0, err + } + if cost > lineup.MaxMp { + return uint32(goproto.Retcode_RET_MAZE_MP_NOT_ENOUGH), nil + } + for *s.techniqueMP < cost && p.AutoTechniqueFood != nil && *p.AutoTechniqueFood { + var selected uint32 + for _, id := range sortedU32Keys2(rules.Foods) { + if p.TechniqueFoodStock[id] > 0 { + selected = id + break + } + } + if selected == 0 { + break + } + if err := s.consumeTechniqueFood(selected, 1); err != nil { + return 0, err + } + } + if *s.techniqueMP < cost { + return uint32(goproto.Retcode_RET_MAZE_MP_NOT_ENOUGH), nil + } + *s.techniqueMP -= cost + if s.paidTechniqueCasts == nil { + s.paidTechniqueCasts = map[uint32]bool{} + } + s.paidTechniqueCasts[castID] = true + SceneDebugLog("[TECHNIQUE] cast=%d avatar=%d skill=%d cost=%d mp=%d/%d", castID, avatar, skillIndex, cost, *s.techniqueMP, lineup.MaxMp) + return 0, s.Send(&goproto.SceneCastSkillMpUpdateScNotify{CastEntityId: castID, Mp: *s.techniqueMP}) +} +func init() { + handle("UseItem", func(s *Session, req *goproto.UseItemCsReq, rsp *goproto.UseItemScRsp) error { + rsp.UseItemId = req.UseItemId + rsp.UseItemCount = req.UseItemCount + if err := s.consumeTechniqueFood(req.UseItemId, req.UseItemCount); err != nil { + SceneDebugLog("[TECHNIQUE] use item rejected: %v", err) + rsp.Retcode = uint32(goproto.Retcode_RET_ITEM_USE_TARGET_TYPE_INVALID) + } + return nil + }) +} diff --git a/internal/gameserver/tierce_progress.go b/internal/gameserver/tierce_progress.go new file mode 100644 index 0000000..0b80c72 --- /dev/null +++ b/internal/gameserver/tierce_progress.go @@ -0,0 +1,110 @@ +package gameserver + +import ( + "gac-hkrpg-go/internal/common" + goproto "gac-hkrpg-go/protocol/proto" + "google.golang.org/protobuf/proto" +) + +// ChallengeTargetConfigRow.ChallengeTargetType, captured from this Client. +const challengeTargetTotalScore uint32 = 7 + +func tierceTargets(id, total uint32) []uint32 { + stage := getChallengeData().Tierce[id] + ids := append([]uint32(nil), stage.Targets...) + if stage.SpecialTarget != 0 { + ids = append(ids, stage.SpecialTarget) + } + var finished []uint32 + for _, targetID := range ids { + r, ok := getChallengeData().TargetRules[targetID] + if ok && r.Type == challengeTargetTotalScore && total >= r.Param { + finished = append(finished, targetID) + } + } + return finished +} +func tierceBestRecord(saved *common.TierceProgress) (uint32, bool) { + var total uint32 + allWon := true + for node := uint32(0); node < tierceNodeCount; node++ { + n, ok := saved.Nodes[node] + total += n.Score + if !ok || !n.Won { + allWon = false + } + } + return total, allWon +} +func tierceNodeResult(node uint32, n common.TierceNodeProgress, teams ...[]uint32) *goproto.PMMOFOBCINL { + result := &goproto.PMMOFOBCINL{} + if len(n.Result) > 0 && proto.Unmarshal(n.Result, result) == nil { + return result + } + // Imported confirmed scores have no historical equipment snapshot. Supply + // known team IDs and empty wrappers rather than inventing gear/stats. + result.Lineup = &goproto.GLLFJFJBIBC{} + if len(teams) > 0 { + for slot, id := range teams[0] { + result.Lineup.MNABNJOEHLF = append(result.Lineup.MNABNJOEHLF, &goproto.CLDOIEGCNLD{AvatarId: id, Slot: uint32(slot), AvatarType: uint32(goproto.AvatarType_AVATAR_FORMAL_TYPE), PAGPNOKLNOM: &goproto.ChallengeBossEquipmentInfo{}, OKCGHKDMFAM: &goproto.ChallengeBossAvatarRelicInfo{}}) + } + } + result.FINOCEGDBDI = node + result.ScoreId = n.Score + if n.Won { + result.EndStatus = goproto.BattleEndStatus_BATTLE_END_WIN + } + return result +} +func saveTierceResult(s *Session, result *goproto.PMMOFOBCINL) error { + saved := s.JSON().TierceProgress[s.ChallengeRun.ID] + if saved.Nodes == nil { + saved.Nodes = map[uint32]common.TierceNodeProgress{} + } + node := result.FINOCEGDBDI + old, known := saved.Nodes[node] + won := result.EndStatus == goproto.BattleEndStatus_BATTLE_END_WIN + if !known || result.ScoreId > old.Score || (result.ScoreId == old.Score && won && !old.Won) { + raw, err := proto.Marshal(result) + if err != nil { + return err + } + saved.Nodes[node] = common.TierceNodeProgress{Score: result.ScoreId, Won: won, Result: raw} + } + return s.JSON().SavePersistentChecked() +} +func buildTierceGroup(id uint32, saved *common.TierceProgress) *goproto.ChallengeTierceGroup { + group := &goproto.ChallengeTierceGroup{ChallengeId: id} + if saved == nil { + return group + } + total, allWon := tierceBestRecord(saved) + group.Record = &goproto.ChallengeTierceRecord{TotalScore: total} + group.IsWin = allWon + if limit := getChallengeData().Tierce[id].ClearScore; limit != 0 && total < limit { + group.IsWin = false + } + if allWon { + group.FinishedTargetList = tierceTargets(id, total) + } + for node, team := range saved.Teams { + lineup := &goproto.CMOPLAHDEPN{FINOCEGDBDI: uint32(node)} + if node < len(saved.Buffs) { + lineup.BuffId = saved.Buffs[node] + } + for _, aid := range team { + lineup.TierceAvatarIdList = append(lineup.TierceAvatarIdList, &goproto.AvatarLineup{Id: aid, AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE}) + } + group.MOGEEAJCFNO = append(group.MOGEEAJCFNO, lineup) + } + for node := uint32(0); node < tierceNodeCount; node++ { + if n, ok := saved.Nodes[node]; ok { + var team []uint32 + if int(node) < len(saved.Teams) { + team = saved.Teams[node] + } + group.HGKCEEOONGA = append(group.HGKCEEOONGA, tierceNodeResult(node, n, team)) + } + } + return group +} diff --git a/internal/sdkserver/api.go b/internal/sdkserver/api.go index 2364fd9..9bfb234 100644 --- a/internal/sdkserver/api.go +++ b/internal/sdkserver/api.go @@ -38,6 +38,7 @@ func corsWrapper(next http.HandlerFunc) http.HandlerFunc { func registerAPIRoutes(mux *http.ServeMux) { mux.HandleFunc("/api/status", corsWrapper(apiStatus)) + mux.HandleFunc("/api/technique", corsWrapper(apiTechnique)) mux.HandleFunc("/api/freesr", corsWrapper(apiFreesrData)) mux.HandleFunc("/api/freesr/avatars", corsWrapper(apiFreesrAvatars)) mux.HandleFunc("/api/persistent", corsWrapper(apiPersistentData)) @@ -605,7 +606,6 @@ func apiGachabaseFetch(w http.ResponseWriter, r *http.Request) { } } - if targetURL == "" { writeJSON(w, map[string]any{"error": "missing url or id/type parameters", "status": 400}) return @@ -833,5 +833,3 @@ func apiBattleReportReset(w http.ResponseWriter, r *http.Request) { "message": "Battle report cleared", }) } - - diff --git a/internal/sdkserver/api_technique.go b/internal/sdkserver/api_technique.go new file mode 100644 index 0000000..2be6985 --- /dev/null +++ b/internal/sdkserver/api_technique.go @@ -0,0 +1,38 @@ +package sdkserver + +import ( + "encoding/json" + "gac-hkrpg-go/internal/common" + "net/http" +) + +// GET is read-only. POST changes only the auto-food switch in persistent. +func apiTechnique(w http.ResponseWriter, r *http.Request) { + apiMu.Lock() + defer apiMu.Unlock() + data, err := common.LoadFreesrData() + if err != nil { + writeJSON(w, map[string]any{"status": 500, "error": err.Error()}) + return + } + switch r.Method { + case http.MethodPost: + var req struct { + AutoFood *bool `json:"auto_food"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.AutoFood == nil { + writeJSON(w, map[string]any{"status": 400, "error": "auto_food must be a boolean"}) + return + } + data.AutoTechniqueFood = req.AutoFood + if err := data.SavePersistentChecked(); err != nil { + writeJSON(w, map[string]any{"status": 500, "error": err.Error()}) + return + } + case http.MethodGet: + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + writeJSON(w, map[string]any{"status": 200, "auto_food": data.AutoTechniqueFood, "food_stock": data.TechniqueFoodStock}) +} diff --git a/internal/sdkserver/server.go b/internal/sdkserver/server.go index 4a06555..11348bd 100644 --- a/internal/sdkserver/server.go +++ b/internal/sdkserver/server.go @@ -218,21 +218,21 @@ type accountPayload struct { Retcode int `json:"retcode"` } -func standardAccount() map[string]any { +func (a *appState) standardAccount() map[string]any { return map[string]any{ "area_code": "**", "email": "ReversedRooms", "country": "RU", "is_email_verify": "1", "token": "mostsecuretokenever", - "uid": "1337", + "uid": fmt.Sprint(a.runtime.PlayerUID), } } -func loginReply(w http.ResponseWriter) { +func (a *appState) loginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ - "account": standardAccount(), + "account": a.standardAccount(), "device_grant_required": false, "reactivate_required": false, "realperson_required": false, @@ -243,15 +243,15 @@ func loginReply(w http.ResponseWriter) { }) } -func granterLoginReply(w http.ResponseWriter) { +func (a *appState) granterLoginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "account_type": 1, - "combo_id": "1337", + "combo_id": fmt.Sprint(a.runtime.PlayerUID), "combo_token": "9065ad8507d5a1991cb6fddacac5999b780bbd92", "data": `{"guest":false}`, "heartbeat": false, - "open_id": "1337", + "open_id": fmt.Sprint(a.runtime.PlayerUID), }, "message": "OK", "retcode": 0, @@ -270,13 +270,13 @@ func riskyCheckReply(w http.ResponseWriter) { }) } -func apnLoginReply(w http.ResponseWriter) { +func (a *appState) apnLoginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "token": map[string]any{"token": "mostsecuretokenever", "token_type": 1}, "user_info": map[string]any{ - "aid": "1337", - "mid": "1337", + "aid": fmt.Sprint(a.runtime.PlayerUID), + "mid": fmt.Sprint(a.runtime.PlayerUID), "is_email_verify": 1, "area_code": "**", "country": "US", @@ -289,13 +289,13 @@ func apnLoginReply(w http.ResponseWriter) { }) } -func apnVerifyReply(w http.ResponseWriter) { +func (a *appState) apnVerifyReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "tokens": []any{map[string]any{"token": "mostsecuretokenever", "token_type": 1}}, "user_info": map[string]any{ - "aid": "1337", - "mid": "1337", + "aid": fmt.Sprint(a.runtime.PlayerUID), + "mid": fmt.Sprint(a.runtime.PlayerUID), "is_email_verify": 1, "area_code": "**", "country": "US", @@ -361,12 +361,12 @@ func Listen() error { mux := http.NewServeMux() mux.HandleFunc("/query_dispatch", func(w http.ResponseWriter, r *http.Request) { queryDispatch(w, r, runtime) }) mux.HandleFunc("/query_gateway", queryGateway(state)) - mux.HandleFunc("/{product_name}/mdk/shield/api/login", func(w http.ResponseWriter, r *http.Request) { loginReply(w) }) - mux.HandleFunc("/{product_name}/mdk/shield/api/verify", func(w http.ResponseWriter, r *http.Request) { loginReply(w) }) - mux.HandleFunc("/{product_name}/combo/granter/login/v2/login", func(w http.ResponseWriter, r *http.Request) { granterLoginReply(w) }) + mux.HandleFunc("/{product_name}/mdk/shield/api/login", func(w http.ResponseWriter, r *http.Request) { state.loginReply(w) }) + mux.HandleFunc("/{product_name}/mdk/shield/api/verify", func(w http.ResponseWriter, r *http.Request) { state.loginReply(w) }) + mux.HandleFunc("/{product_name}/combo/granter/login/v2/login", func(w http.ResponseWriter, r *http.Request) { state.granterLoginReply(w) }) mux.HandleFunc("/account/risky/api/check", func(w http.ResponseWriter, r *http.Request) { riskyCheckReply(w) }) - mux.HandleFunc("/account/ma-cn-passport/app/loginByPassword", func(w http.ResponseWriter, r *http.Request) { apnLoginReply(w) }) - mux.HandleFunc("/account/ma-cn-session/app/verify", func(w http.ResponseWriter, r *http.Request) { apnVerifyReply(w) }) + mux.HandleFunc("/account/ma-cn-passport/app/loginByPassword", func(w http.ResponseWriter, r *http.Request) { state.apnLoginReply(w) }) + mux.HandleFunc("/account/ma-cn-session/app/verify", func(w http.ResponseWriter, r *http.Request) { state.apnVerifyReply(w) }) mux.HandleFunc("/srtools", corsWrapper(srToolsSave)) registerAPIRoutes(mux) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/persistent b/persistent index ea10f2d..296e789 100644 --- a/persistent +++ b/persistent @@ -1,4 +1,6 @@ { + "active_team_preset": 2, + "auto_technique_food": true, "dressed_skins": {}, "enable_aha_global": true, "enable_castorice_global": true, @@ -9,22 +11,221 @@ "1308": true }, "lineups": { - "0": 1213, - "1": 1306, - "2": 1403, + "0": 1407, + "1": 1413, + "2": 1415, "3": 1409 }, "main_character": "FemaleHarmony", "march_type": "MarchHunt", "position": { - "rot_y": 167180, - "x": 72091, - "y": 7000, - "z": 110301 + "rot_y": 340000, + "x": 25709, + "y": 3473, + "z": -155597 }, "scene": { - "entry_id": 2050101, - "floor_id": 20501001, - "plane_id": 20501 + "entry_id": 2057201, + "floor_id": 20572001, + "plane_id": 20572 + }, + "team_presets": { + "0": { + "avatars": { + "0": 1308, + "1": 1403, + "2": 1507, + "3": 1414 + }, + "favourite": true, + "leader_slot": 0, + "name": "Team 1" + }, + "1": { + "avatars": { + "0": 1510, + "1": 1306, + "2": 1512, + "3": 1217 + }, + "favourite": true, + "leader_slot": 0, + "name": "SW5513 N1" + }, + "2": { + "avatars": { + "0": 1407, + "1": 1413, + "2": 1415, + "3": 1409 + }, + "favourite": true, + "leader_slot": 0, + "name": "SW5513 N2" + }, + "3": { + "avatars": { + "0": 1408, + "1": 1412, + "2": 1313, + "3": 1414 + }, + "favourite": true, + "leader_slot": 0, + "name": "SW5513 N3" + }, + "4": { + "avatars": { + "0": 1506, + "1": 1501, + "2": 1511, + "3": 1503 + }, + "favourite": true, + "leader_slot": 2, + "name": "SW20275 N2" + }, + "5": { + "avatars": { + "0": 1408, + "1": 1412, + "2": 1512, + "3": 1217 + }, + "favourite": true, + "leader_slot": 0, + "name": "SW20275 N3" + } + }, + "technique_food_stock": { + "400006": 96, + "401033": 100, + "402029": 100, + "408002": 100, + "408205": 100, + "408404": 100, + "408412": 100, + "408418": 100, + "408426": 100, + "408443": 100, + "408453": 100, + "408604": 100, + "408605": 100, + "408636": 100 + }, + "tierce_progress": { + "20275": { + "buffs": [ + 3031359, + 3031362, + 3031359 + ], + "nodes": { + "0": { + "score": 33000, + "won": true + }, + "1": { + "score": 28500, + "won": true + }, + "2": { + "score": 35500, + "won": true + } + }, + "teams": [ + [ + 1308, + 1403, + 1507, + 1414 + ], + [ + 1506, + 1501, + 1511, + 1503 + ], + [ + 1408, + 1412, + 1512, + 1217 + ] + ] + }, + "30215": { + "buffs": [ + 3111094, + 3111092, + 3111065 + ], + "nodes": { + "0": { + "result": "EgcIlb8FEKwPMKwPSAF6bgoYCOILEAMgUDINCLEBEJG0ARhQIAYoAToAChoI3QsQAxgBIFAyDQi0ARCNtAEYUCAGKAE6AAoaCOcLEAMYAiBQMg0InAEQmbQBGFAgBigBOgAKGgjfCxADGAMgUDINCLMBEI+0ARhQIAYoAToA", + "score": 1964, + "won": true + }, + "1": { + "result": "EgcIlb8FEP4NMP4NQAFIAXpsChcIgAsQAyBQMgwIahCEtAEYUCAGKAE6AAoaCIQLEAMYASBQMg0ItgEQiLQBGFAgBigBOgAKGQihChADGAIgUDIMCEcQ+rMBGFAgBigBOgAKGgiGCxADGAMgUDINCIcBEIu0ARhQIAYoAToA", + "score": 1790, + "won": true + }, + "2": { + "result": "EgcIlb8FENAPMNAPQAJIAXpuChgI/woQAyBQMg0IrAEQgLQBGFAgBigBOgAKGgiFCxADGAEgUDINCKgBEIm0ARhQIAYoAToAChoIhwsQAxgCIFAyDQi1ARCMtAEYUCAGKAE6AAoaCIELEAMYAyBQMg0IqwEQgrQBGFAgBigBOgA=", + "score": 2000, + "won": true + } + }, + "teams": [ + [ + 1506, + 1501, + 1511, + 1503 + ], + [ + 1408, + 1412, + 1313, + 1414 + ], + [ + 1407, + 1413, + 1415, + 1409 + ] + ] + }, + "5513": { + "buffs": [ + 0, + 0, + 0 + ], + "nodes": {}, + "teams": [ + [ + 1510, + 1306, + 1512, + 1217 + ], + [ + 1407, + 1413, + 1415, + 1409 + ], + [ + 1408, + 1412, + 1313, + 1414 + ] + ] + } } } \ No newline at end of file diff --git a/resources/BattleRules.json b/resources/BattleRules.json index 6207f15..1a6736f 100644 --- a/resources/BattleRules.json +++ b/resources/BattleRules.json @@ -8,6 +8,11 @@ "protocol/src/protocol.pb.zig" ] }, + "challenge_round_limits": { + "PF": 4, + "AS": 30, + "Default": 30 + }, "technique_buffs": { "1208": [ { diff --git a/resources/ChallengeTargetConfig.json b/resources/ChallengeTargetConfig.json new file mode 100644 index 0000000..68f1f67 --- /dev/null +++ b/resources/ChallengeTargetConfig.json @@ -0,0 +1,153 @@ +{ + "source": { + "client_version": "4.6.53", + "assembly_sha256": "c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144", + "table": "ChallengeTargetConfigRow" + }, + "challenge_targets": [ + { + "ChallengeTargetName": { + "hash": -1268508559, + "hash64": 13289745288470897026 + }, + "ChallengeTargetParam2": 0, + "ID": 600, + "ChallengeTargetType": 6, + "RewardID": 100344, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 33 + }, + { + "ChallengeTargetName": { + "hash": -128044084, + "hash64": 12508471847671960592 + }, + "ChallengeTargetParam2": 0, + "ID": 601, + "ChallengeTargetType": 6, + "RewardID": 100342, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 15 + }, + { + "ChallengeTargetName": { + "hash": -128044084, + "hash64": 12508471847671960592 + }, + "ChallengeTargetParam2": 0, + "ID": 602, + "ChallengeTargetType": 6, + "RewardID": 100343, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 30 + }, + { + "ChallengeTargetName": { + "hash": -128044080, + "hash64": 15997122175768211822 + }, + "ChallengeTargetParam2": 0, + "ID": 603, + "ChallengeTargetType": 2, + "RewardID": 100344, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 0 + }, + { + "ChallengeTargetName": { + "hash": -1005863068, + "hash64": 2824638414496714228 + }, + "ChallengeTargetParam2": 0, + "ID": 4000, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 99000 + }, + { + "ChallengeTargetName": { + "hash": -1856409346, + "hash64": 11408150447023752054 + }, + "ChallengeTargetParam2": 0, + "ID": 4001, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 60000 + }, + { + "ChallengeTargetName": { + "hash": -1856409346, + "hash64": 11408150447023752054 + }, + "ChallengeTargetParam2": 0, + "ID": 4002, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 75000 + }, + { + "ChallengeTargetName": { + "hash": -1856409346, + "hash64": 11408150447023752054 + }, + "ChallengeTargetParam2": 0, + "ID": 4003, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 90000 + }, + { + "ChallengeTargetName": { + "hash": -1005863068, + "hash64": 2824638414496714228 + }, + "ChallengeTargetParam2": 0, + "ID": 5000, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 10200 + }, + { + "ChallengeTargetName": { + "hash": -1856409346, + "hash64": 11408150447023752054 + }, + "ChallengeTargetParam2": 0, + "ID": 5001, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 6000 + }, + { + "ChallengeTargetName": { + "hash": -1856409346, + "hash64": 11408150447023752054 + }, + "ChallengeTargetParam2": 0, + "ID": 5002, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 7800 + }, + { + "ChallengeTargetName": { + "hash": -1856409346, + "hash64": 11408150447023752054 + }, + "ChallengeTargetParam2": 0, + "ID": 5003, + "ChallengeTargetType": 7, + "RewardID": 0, + "ChallengeTargetParam3": 0, + "ChallengeTargetParam1": 9900 + } + ] +} diff --git a/resources/PamSkinConfig.json b/resources/PamSkinConfig.json new file mode 100644 index 0000000..d293ed4 --- /dev/null +++ b/resources/PamSkinConfig.json @@ -0,0 +1,44 @@ +{ + "source": { + "client_version": "4.6.53", + "assembly_sha256": "c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144", + "table": "PamSkinConfigRow" + }, + "pam_skin_config": [ + { + "JsonPath": "Config/ConfigCharacter/NPC/Special/NPC_Special_Pam_00_Config.json", + "SkinIcon": "SpriteOutput/AvatarShopIcon/Pam/252000.png", + "ConfigEntityPath": "Config/ConfigEntity/NPC/Special/NPC_Special_Pam_00_Entity.json", + "ManikinPrefab": "Characters/CharacterPrefabs/Manikin/Special/Pam_00/Manikin_Special_Pam_00.prefab", + "SkinID": 252000 + }, + { + "JsonPath": "Config/ConfigCharacter/NPC/Special/NPC_Special_Pam_00_Config.json", + "SkinIcon": "SpriteOutput/AvatarShopIcon/Pam/252001.png", + "ConfigEntityPath": "Config/ConfigEntity/NPC/Special/NPC_Special_PamChef_00_Entity.json", + "ManikinPrefab": "Characters/CharacterPrefabs/Manikin/Special/PamChef_00/Manikin_Special_PamChef_00.prefab", + "SkinID": 252001 + }, + { + "JsonPath": "Config/ConfigCharacter/NPC/Special/NPC_Special_Pam_00_Config.json", + "SkinIcon": "SpriteOutput/AvatarShopIcon/Pam/252002.png", + "ConfigEntityPath": "Config/ConfigEntity/NPC/Special/NPC_Special_PamNighty_00_Entity.json", + "ManikinPrefab": "Characters/CharacterPrefabs/Manikin/Special/PamNighty_00/Manikin_Special_PamNighty_00.prefab", + "SkinID": 252002 + }, + { + "JsonPath": "Config/ConfigCharacter/NPC/Special/NPC_Special_Pam_00_Config.json", + "SkinIcon": "SpriteOutput/AvatarShopIcon/Pam/252003.png", + "ConfigEntityPath": "Config/ConfigEntity/NPC/Special/NPC_Special_PamGentle_00_Entity.json", + "ManikinPrefab": "Characters/CharacterPrefabs/Manikin/Special/PamGentle_00/Manikin_Special_PamGentle_00.prefab", + "SkinID": 252003 + }, + { + "JsonPath": "Config/ConfigCharacter/NPC/Special/NPC_Special_Pam_00_Config.json", + "SkinIcon": "SpriteOutput/AvatarShopIcon/Pam/252004.png", + "ConfigEntityPath": "Config/ConfigEntity/NPC/Special/NPC_Special_PamGentle_01_Entity.json", + "ManikinPrefab": "Characters/CharacterPrefabs/Manikin/Special/PamGentle_01/Manikin_Special_PamGentle_01.prefab", + "SkinID": 252004 + } + ] +} diff --git a/resources/ProfileConfig.json b/resources/ProfileConfig.json index c9de605..62a459d 100644 --- a/resources/ProfileConfig.json +++ b/resources/ProfileConfig.json @@ -5,7 +5,8 @@ "files": [ "gameserver/src/services/profile.zig", "gameserver/src/data.zig", - "protocol/src/protocol.pb.zig" + "protocol/src/protocol.pb.zig", + "gameserver/src/services/scene.zig" ] }, "personal_cards": [ @@ -57,5 +58,12 @@ ], "current_phone_case": 254001, "current_phone_theme": 0, - "current_chat_bubble": 0 + "current_chat_bubble": 0, + "pam_skins": [ + 252000, + 252001, + 252002, + 252003 + ], + "current_pam_skin": 252000 } diff --git a/resources/SceneMetadata.json b/resources/SceneMetadata.json new file mode 100644 index 0000000..8736a6b --- /dev/null +++ b/resources/SceneMetadata.json @@ -0,0 +1,5391 @@ +{ + "client_version": "4.6.53", + "source": { + "kind": "Client GameCoreConfigLoader.LoadRtLevelFloorInfo / GroupInstanceCommonMap", + "assembly_sha256": "c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144", + "capture_sha256": "84f4a5f5cd0570902a89b21f8ed139b2d7fe8fd928b5ddfa5612f709ef748804", + "previous_capture_sha256": [ + "e937e23ccd1b09ae432886afeeeae57999deee027328862c18fe6eca6a20570e" + ] + }, + "floors": { + "20571001": { + "plane_id": 20571, + "group_count": 167, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "11": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "43": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "44": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "45": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "46": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "47": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "48": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "49": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "50": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "51": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "52": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "53": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "54": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "55": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "56": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "57": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "59": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "61": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "62": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "63": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "66": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "67": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "68": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "69": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "70": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "105": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "106": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "107": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "108": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "109": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "110": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "111": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "112": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "113": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "114": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "115": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "116": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "117": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "118": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "119": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "120": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "121": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "122": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "123": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "126": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "127": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "128": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "129": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "130": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "131": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "132": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "133": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "134": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "135": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "137": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "138": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "139": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "145": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "146": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "147": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "148": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "149": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "150": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "151": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "152": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "153": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "154": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "155": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "156": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "157": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "158": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "159": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "160": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "161": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "162": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "163": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "165": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "164": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "166": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "167": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "168": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "169": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "170": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "171": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "172": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "173": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "174": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "175": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "177": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "178": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "179": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "176": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "180": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "181": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "182": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "183": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "184": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "185": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "186": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "187": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "188": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "189": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "190": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "191": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "192": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "193": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "194": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "195": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "196": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "197": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "198": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "200": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "202": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "203": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "204": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "205": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": true + }, + "201": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": true + }, + "208": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "209": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "210": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "211": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "212": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "213": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "214": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "216": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": true + }, + "219": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "220": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "221": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "222": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "223": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "224": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "225": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "226": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "227": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "228": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "229": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "217": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "218": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "230": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "231": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "232": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "233": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "234": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "235": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "236": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "237": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "238": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "239": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "240": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "241": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "242": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "243": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "244": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "245": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "247": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "248": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "250": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "177": { + "200001": { + "pos": { + "x": 47235, + "y": 657, + "z": 717848 + }, + "rot": { + "x": 0, + "y": 8116, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 23171, + "y": 0, + "z": 718072 + }, + "rot": { + "x": 0, + "y": 259562, + "z": 0 + } + }, + "200003": { + "pos": { + "x": -42335, + "y": 702, + "z": 731096 + }, + "rot": { + "x": 0, + "y": 232598, + "z": 0 + } + }, + "200004": { + "pos": { + "x": -46863, + "y": 657, + "z": 789834 + }, + "rot": { + "x": 0, + "y": 140258, + "z": 0 + } + }, + "200005": { + "pos": { + "x": -39584, + "y": 0, + "z": 741792 + }, + "rot": { + "x": 0, + "y": 161249, + "z": 0 + } + } + }, + "178": { + "200001": { + "pos": { + "x": -45171, + "y": 72862, + "z": 306238 + }, + "rot": { + "x": 0, + "y": 323624, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -17247, + "y": 72862, + "z": 332226 + }, + "rot": { + "x": 0, + "y": 294062, + "z": 0 + } + }, + "200003": { + "pos": { + "x": -116241, + "y": 72862, + "z": 305541 + }, + "rot": { + "x": 0, + "y": 31005, + "z": 0 + } + }, + "200004": { + "pos": { + "x": -142509, + "y": 72862, + "z": 346824 + }, + "rot": { + "x": 0, + "y": 109494, + "z": 0 + } + } + }, + "179": { + "200001": { + "pos": { + "x": 47270, + "y": 657, + "z": 717840 + }, + "rot": { + "x": 0, + "y": 339332, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -43194, + "y": 702, + "z": 731340 + }, + "rot": { + "x": 0, + "y": 282941, + "z": 0 + } + }, + "200003": { + "pos": { + "x": -46840, + "y": 657, + "z": 789610 + }, + "rot": { + "x": 0, + "y": 119352, + "z": 0 + } + }, + "200004": { + "pos": { + "x": -45640, + "y": 702, + "z": 768091 + }, + "rot": { + "x": 0, + "y": 164221, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 0, + "y": 19639, + "z": -98620 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "20572001": { + "plane_id": 20572, + "group_count": 92, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "11": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "14": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "15": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "16": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "17": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "18": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "20": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "21": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "22": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "23": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "24": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "25": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "26": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "27": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "28": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "29": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "30": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "31": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "32": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "33": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "34": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "35": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "36": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "37": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "38": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "39": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "40": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "41": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "42": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "43": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "44": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "45": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "46": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "47": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "48": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "49": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "50": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "51": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "52": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "53": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "54": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "55": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "56": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "57": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "58": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "59": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "60": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "61": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "62": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "63": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "64": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "65": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "66": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "67": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "68": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "69": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "70": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "71": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "72": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "73": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "74": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "75": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "76": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "77": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "78": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "79": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "84": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "85": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "87": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "88": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "89": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "90": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "91": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "92": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "93": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "94": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "98": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": true + }, + "99": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "100": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "101": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "102": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": {}, + "start_anchor": { + "pos": { + "x": 25709, + "y": 3473, + "z": -155597 + }, + "rot": { + "x": 0, + "y": 340000, + "z": 0 + }, + "ID": 1 + } + }, + "30140003": { + "plane_id": 30140, + "group_count": 13, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "11": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "6": { + "200001": { + "pos": { + "x": 14477, + "y": 9648, + "z": 126170 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": 14477, + "y": 9648, + "z": 126170 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 14477, + "y": 9648, + "z": 110170 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30142001": { + "plane_id": 30142, + "group_count": 14, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "21": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": true + }, + "22": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": true + }, + "23": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "24": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": true + }, + "25": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": true + }, + "26": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "7": { + "200001": { + "pos": { + "x": 312, + "y": 310365, + "z": -42 + }, + "rot": { + "x": 0, + "y": 135000, + "z": 0 + } + } + }, + "8": { + "200001": { + "pos": { + "x": 312, + "y": 310365, + "z": -42 + }, + "rot": { + "x": 0, + "y": 135000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 11487, + "y": 310285, + "z": -11212 + }, + "rot": { + "x": 0, + "y": 315000, + "z": 0 + }, + "ID": 1 + } + }, + "30124002": { + "plane_id": 30124, + "group_count": 6, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": -445057, + "y": 6706, + "z": 829714 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -445057, + "y": 6706, + "z": 829714 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -445057, + "y": 4781, + "z": 809714 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30141001": { + "plane_id": 30141, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": -12509, + "y": -14827, + "z": -48 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -12509, + "y": -14827, + "z": -48 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": -12509, + "y": -14827, + "z": -48 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "8": { + "200001": { + "pos": { + "x": -12509, + "y": -14827, + "z": -48 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -31509, + "y": -14827, + "z": -48 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30132001": { + "plane_id": 30132, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": 243660, + "y": 165544, + "z": 436520 + }, + "rot": { + "x": 0, + "y": 142000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": 243660, + "y": 165544, + "z": 436520 + }, + "rot": { + "x": 0, + "y": 142000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 255220, + "y": 165544, + "z": 421720 + }, + "rot": { + "x": 0, + "y": 322000, + "z": 0 + }, + "ID": 1 + } + }, + "30126002": { + "plane_id": 30126, + "group_count": 8, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "6": { + "200001": { + "pos": { + "x": 213, + "y": -193081, + "z": 137 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": 213, + "y": -193081, + "z": 137 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 210, + "y": -193066, + "z": 22220 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + }, + "ID": 1 + } + }, + "30101001": { + "plane_id": 30101, + "group_count": 6, + "groups": { + "1": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "2": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "3": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "2": { + "200001": { + "pos": { + "x": -61000, + "y": -2141, + "z": -170700 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "3": { + "200001": { + "pos": { + "x": -61000, + "y": -2141, + "z": -170700 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "4": { + "200001": { + "pos": { + "x": -61000, + "y": -2141, + "z": -176400 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -61000, + "y": -2141, + "z": -164360 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "5": { + "200001": { + "pos": { + "x": -61000, + "y": -2141, + "z": -176440 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -61000, + "y": -2141, + "z": -164390 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -41300, + "y": -2134, + "z": -170280 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + }, + "ID": 1 + } + }, + "30102001": { + "plane_id": 30102, + "group_count": 10, + "groups": { + "1": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "13": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "14": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "15": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "16": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": 8031, + "y": 19978, + "z": -54267 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": 8031, + "y": 19978, + "z": -54267 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "10": { + "200001": { + "pos": { + "x": 13850, + "y": 19978, + "z": -50910 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 1940, + "y": 19978, + "z": -57790 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": 13850, + "y": 19978, + "z": -50910 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 1940, + "y": 19978, + "z": -57790 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "13": { + "200001": { + "pos": { + "x": 8031, + "y": 19978, + "z": -54267 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "14": { + "200001": { + "pos": { + "x": 8031, + "y": 19978, + "z": -54267 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "15": { + "200001": { + "pos": { + "x": 13850, + "y": 19978, + "z": -50910 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 1940, + "y": 19978, + "z": -57790 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + }, + "16": { + "200001": { + "pos": { + "x": 13850, + "y": 19978, + "z": -50910 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 1940, + "y": 19978, + "z": -57790 + }, + "rot": { + "x": 0, + "y": 330000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -2210, + "y": 19977, + "z": -36430 + }, + "rot": { + "x": 0, + "y": 150000, + "z": 0 + }, + "ID": 1 + } + }, + "30102002": { + "plane_id": 30102, + "group_count": 8, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": -714570, + "y": 6270, + "z": 73130 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -714570, + "y": 6270, + "z": 73130 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": -714570, + "y": 6270, + "z": 67130 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -714570, + "y": 6270, + "z": 79130 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "8": { + "200001": { + "pos": { + "x": -714570, + "y": 6270, + "z": 67130 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -714570, + "y": 6270, + "z": 79130 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -697570, + "y": 6270, + "z": 73130 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + }, + "ID": 1 + } + }, + "30102003": { + "plane_id": 30102, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "4": { + "200001": { + "pos": { + "x": -30, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "5": { + "200001": { + "pos": { + "x": -30, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -6000, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 6000, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": -6000, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 6000, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": -30, + "y": -9077, + "z": -8581 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -30, + "y": -9077, + "z": -28581 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30102004": { + "plane_id": 30102, + "group_count": 8, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "3": { + "200001": { + "pos": { + "x": -2722, + "y": 144131, + "z": -14086 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "4": { + "200001": { + "pos": { + "x": -2722, + "y": 144131, + "z": -14086 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "5": { + "200001": { + "pos": { + "x": -2722, + "y": 144131, + "z": -20086 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -2722, + "y": 144131, + "z": -8086 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -2722, + "y": 144131, + "z": -20086 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -2722, + "y": 144131, + "z": -8086 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 18722, + "y": 144064, + "z": -14086 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + }, + "ID": 1 + } + }, + "30102005": { + "plane_id": 30102, + "group_count": 7, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "6": { + "200001": { + "pos": { + "x": 580, + "y": 8551, + "z": 54030 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": 580, + "y": 8551, + "z": 54030 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 580, + "y": 7849, + "z": 34030 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30102006": { + "plane_id": 30102, + "group_count": 14, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "10": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "13": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "14": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "11": { + "200001": { + "pos": { + "x": -7360, + "y": 1528, + "z": 16590 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "12": { + "200001": { + "pos": { + "x": -7360, + "y": 1528, + "z": 16590 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "13": { + "200001": { + "pos": { + "x": -405651, + "y": -44430, + "z": 115454 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "14": { + "200001": { + "pos": { + "x": -405651, + "y": -44430, + "z": 115454 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -27360, + "y": 1629, + "z": 16590 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30103001": { + "plane_id": 30103, + "group_count": 7, + "groups": { + "1": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": 88000, + "y": 1942, + "z": 91670 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": 88000, + "y": 1942, + "z": 91670 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "10": { + "200001": { + "pos": { + "x": 88000, + "y": 1942, + "z": 97000 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 88000, + "y": 1942, + "z": 85000 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": 88000, + "y": 1942, + "z": 97000 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 88000, + "y": 1942, + "z": 85000 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 67482, + "y": 1942, + "z": 91670 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30104001": { + "plane_id": 30104, + "group_count": 8, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "15": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": -16860, + "y": 2171, + "z": 54970 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": -16860, + "y": 2171, + "z": 54970 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "10": { + "200001": { + "pos": { + "x": -16860, + "y": 2171, + "z": 60970 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -16860, + "y": 2171, + "z": 48970 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": -16860, + "y": 2171, + "z": 60970 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": -16860, + "y": 2171, + "z": 48970 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -37860, + "y": 2171, + "z": 54970 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30105001": { + "plane_id": 30105, + "group_count": 12, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "12": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "14": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "16": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "19": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "20": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "33": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": 44217, + "y": 60366, + "z": 42470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": 44217, + "y": 60366, + "z": 42470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "10": { + "200001": { + "pos": { + "x": 44217, + "y": 60366, + "z": 48470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 44217, + "y": 60366, + "z": 36470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": 44217, + "y": 60366, + "z": 48470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + }, + "200002": { + "pos": { + "x": 44217, + "y": 60366, + "z": 36470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "20": { + "200001": { + "pos": { + "x": 44217, + "y": 60366, + "z": 42470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "33": { + "200001": { + "pos": { + "x": 44217, + "y": 60366, + "z": 42470 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 24217, + "y": 60366, + "z": 42470 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30121001": { + "plane_id": 30121, + "group_count": 7, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "6": { + "200001": { + "pos": { + "x": -117249, + "y": 11558, + "z": 62180 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": -117249, + "y": 11558, + "z": 62180 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -117420, + "y": 11517, + "z": 39653 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30121002": { + "plane_id": 30121, + "group_count": 7, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "6": { + "200001": { + "pos": { + "x": -40, + "y": -15915, + "z": -269070 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": -40, + "y": -15915, + "z": -269070 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -40, + "y": -16858, + "z": -289070 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30122001": { + "plane_id": 30122, + "group_count": 11, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "14": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "7": { + "200001": { + "pos": { + "x": 64440, + "y": 49957, + "z": 6160 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "8": { + "200001": { + "pos": { + "x": 64440, + "y": 49957, + "z": 6160 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": -147300, + "y": 60884, + "z": 6311 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "10": { + "200001": { + "pos": { + "x": -147300, + "y": 60884, + "z": 6311 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 88245, + "y": 47845, + "z": 6165 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + }, + "ID": 1 + } + }, + "30123001": { + "plane_id": 30123, + "group_count": 18, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "3": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "62": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "63": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "64": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "65": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "66": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "67": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "68": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "69": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "70": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "71": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "72": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": -34, + "y": 4677, + "z": 281712 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -34, + "y": 4677, + "z": 281712 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "72": { + "200001": { + "pos": { + "x": -30, + "y": 4677, + "z": 293080 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -34, + "y": 4615, + "z": 261712 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30124001": { + "plane_id": 30124, + "group_count": 4, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": false, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": -25754, + "y": 20486, + "z": -13340 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": -25754, + "y": 20486, + "z": -13340 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -25754, + "y": 20486, + "z": 6660 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + }, + "ID": 1 + } + }, + "30125001": { + "plane_id": 30125, + "group_count": 5, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": 268860, + "y": 79927, + "z": 95320 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": 268860, + "y": 79927, + "z": 95320 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 248860, + "y": 79927, + "z": 95320 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30126001": { + "plane_id": 30126, + "group_count": 14, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "10": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "11": { + "load_side": "Client", + "is_hoyo_group": false, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "13": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "14": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "13": { + "200001": { + "pos": { + "x": -705786, + "y": -58600, + "z": 5629 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "14": { + "200001": { + "pos": { + "x": -705786, + "y": -58600, + "z": 5629 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -680786, + "y": -58600, + "z": 5629 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + }, + "ID": 1 + } + }, + "30127001": { + "plane_id": 30127, + "group_count": 18, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "13": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "14": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "15": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "16": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "17": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "18": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "19": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "9": { + "200001": { + "pos": { + "x": -854, + "y": -26069, + "z": 16788 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "10": { + "200001": { + "pos": { + "x": -854, + "y": -26069, + "z": 16788 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": -1002, + "y": -51677, + "z": 5625 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "12": { + "200001": { + "pos": { + "x": -1002, + "y": -51677, + "z": 5625 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -854, + "y": -26069, + "z": -3212 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30130001": { + "plane_id": 30130, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "14": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "16": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "17": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "18": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "19": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "20": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "21": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "22": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "20": { + "200001": { + "pos": { + "x": 91, + "y": 81805, + "z": 196425 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "21": { + "200001": { + "pos": { + "x": 91, + "y": 81805, + "z": 196425 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 91, + "y": 81805, + "z": 176425 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30130002": { + "plane_id": 30130, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": -405, + "y": 26791, + "z": -197994 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": -405, + "y": 26791, + "z": -197994 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -405, + "y": 26687, + "z": -217994 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30131001": { + "plane_id": 30131, + "group_count": 6, + "groups": { + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "10": { + "200001": { + "pos": { + "x": -43120, + "y": 6010, + "z": -306010 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": -43120, + "y": 6010, + "z": -306010 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -23120, + "y": 6010, + "z": -306010 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + }, + "ID": 1 + } + }, + "30131002": { + "plane_id": 30131, + "group_count": 6, + "groups": { + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "4": { + "200001": { + "pos": { + "x": 4550, + "y": 393572, + "z": 320900 + }, + "rot": { + "x": 0, + "y": 81369, + "z": 0 + } + } + }, + "5": { + "200001": { + "pos": { + "x": 4550, + "y": 393572, + "z": 320900 + }, + "rot": { + "x": 0, + "y": 81369, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 24040, + "y": 393572, + "z": 323597 + }, + "rot": { + "x": 0, + "y": 261369, + "z": 0 + }, + "ID": 1 + } + }, + "30135001": { + "plane_id": 30135, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "8": { + "200001": { + "pos": { + "x": 110, + "y": 1244, + "z": -250 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "9": { + "200001": { + "pos": { + "x": 110, + "y": 1244, + "z": -250 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 110, + "y": 948, + "z": -20250 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30135002": { + "plane_id": 30135, + "group_count": 4, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "3": { + "200001": { + "pos": { + "x": 811, + "y": 4, + "z": 7462 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "4": { + "200001": { + "pos": { + "x": 811, + "y": 4, + "z": 7462 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 811, + "y": 4, + "z": -12538 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30136001": { + "plane_id": 30136, + "group_count": 9, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "6": { + "200001": { + "pos": { + "x": -78, + "y": 13745, + "z": 605253 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "7": { + "200001": { + "pos": { + "x": -78, + "y": 13745, + "z": 605253 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": -78, + "y": 13745, + "z": 585253 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30138001": { + "plane_id": 30138, + "group_count": 8, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "4": { + "200001": { + "pos": { + "x": 31573, + "y": 2412, + "z": 29 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + }, + "5": { + "200001": { + "pos": { + "x": 31573, + "y": 2412, + "z": 29 + }, + "rot": { + "x": 0, + "y": 270000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 14170, + "y": 2412, + "z": 30 + }, + "rot": { + "x": 0, + "y": 90000, + "z": 0 + }, + "ID": 1 + } + }, + "30140002": { + "plane_id": 30140, + "group_count": 12, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "7": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "8": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "9": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "10": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "11": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "12": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "13": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "10": { + "200001": { + "pos": { + "x": 0, + "y": 1000, + "z": -45700 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + }, + "11": { + "200001": { + "pos": { + "x": 0, + "y": 1000, + "z": -45700 + }, + "rot": { + "x": 0, + "y": 180000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 0, + "y": 1000, + "z": -65280 + }, + "rot": { + "x": 0, + "y": 0, + "z": 0 + }, + "ID": 1 + } + }, + "30144001": { + "plane_id": 30144, + "group_count": 6, + "groups": { + "1": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "2": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "3": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "4": { + "load_side": "Client", + "is_hoyo_group": true, + "is_deleted": false + }, + "5": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + }, + "6": { + "load_side": "Server", + "is_hoyo_group": true, + "is_deleted": false + } + }, + "monsters": { + "5": { + "200001": { + "pos": { + "x": 106486, + "y": 240098, + "z": 138440 + }, + "rot": { + "x": 0, + "y": 192000, + "z": 0 + } + } + }, + "6": { + "200001": { + "pos": { + "x": 106486, + "y": 240098, + "z": 138440 + }, + "rot": { + "x": 0, + "y": 192000, + "z": 0 + } + } + } + }, + "start_anchor": { + "pos": { + "x": 102680, + "y": 240533, + "z": 120500 + }, + "rot": { + "x": 0, + "y": 12000, + "z": 0 + }, + "ID": 1 + } + } + } +} diff --git a/resources/TeamPresetConfig.json b/resources/TeamPresetConfig.json new file mode 100644 index 0000000..09f07f3 --- /dev/null +++ b/resources/TeamPresetConfig.json @@ -0,0 +1,12 @@ +{ + "source": { + "client_version": "4.6.53", + "assembly_sha256": "c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144", + "member_limit": "RPG.Client.TeamModule._MAX_TEAM_MEMBER=4", + "name_limit": "ConstValueCommon.TeamName_Max=14", + "preset_count": "Server policy for user-requested extra saved teams; native TeamModule keeps regular teams in a List" + }, + "preset_count": 20, + "max_members": 4, + "max_name_length": 14 +} diff --git a/resources/TechniqueRules.json b/resources/TechniqueRules.json new file mode 100644 index 0000000..fa0a395 --- /dev/null +++ b/resources/TechniqueRules.json @@ -0,0 +1,3483 @@ +{ + "source": { + "revision": "005d2c12db4781071f4b5b68e8f364587886a710", + "files": [ + "gameserver/src/manager/lineup_mgr.zig", + "gameserver/src/services/battle.zig", + "gameserver/src/data.zig" + ], + "client_tables": [ + "ConstValueCommon", + "MazeSkill", + "AdventurePlayer", + "Item", + "ItemUseBuffData" + ], + "assembly_sha256": "c5e02e6e984f7e5577c78b995a8efeee2d06ee22e67d484ad5e859c8bf21e144" + }, + "client_version": "4.6.53", + "base_max_mp": 5, + "initial_mp": 5, + "max_mp_by_avatar": { + "1408": 7, + "1510": 7 + }, + "non_attack_techniques": [ + 1509, + 1504, + 1501, + 1414, + 1405, + 1404, + 1225, + 1321, + 1314, + 1312, + 1305, + 1302, + 1217, + 1108 + ], + "foods": { + "400006": { + "recover_mp": 2, + "max_use": 1, + "pile_limit": 99999 + }, + "408002": { + "recover_mp": 4, + "max_use": 1, + "pile_limit": 99999 + }, + "408404": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + }, + "408604": { + "recover_mp": 2, + "max_use": 1, + "pile_limit": 99999 + }, + "408205": { + "recover_mp": 2, + "max_use": 1, + "pile_limit": 99999 + }, + "408605": { + "recover_mp": 4, + "max_use": 1, + "pile_limit": 99999 + }, + "408412": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + }, + "402029": { + "recover_mp": 4, + "max_use": 1, + "pile_limit": 99999 + }, + "408418": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + }, + "408426": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + }, + "408636": { + "recover_mp": 4, + "max_use": 1, + "pile_limit": 99999 + }, + "401033": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + }, + "408443": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + }, + "408453": { + "recover_mp": 3, + "max_use": 1, + "pile_limit": 99999 + } + }, + "avatars": { + "1001": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100101, + "MPCost": 0, + "MazeSkillName": { + "hash": -409178268, + "hash64": 7167396225780900216 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100106, + "MazeSkillDesc": { + "hash": 2095340966, + "hash64": 6612596470888090439 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100102, + "MPCost": 1, + "MazeSkillName": { + "hash": -1975262209, + "hash64": 17659968616357298260 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100107, + "MazeSkillDesc": { + "hash": 529257025, + "hash64": 2038436705635778325 + } + } + ], + "1002": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100201, + "MPCost": 0, + "MazeSkillName": { + "hash": -308421719, + "hash64": 14275235920482067028 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100206, + "MazeSkillDesc": { + "hash": -2098869781, + "hash64": 1805175858405147815 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100202, + "MPCost": 1, + "MazeSkillName": { + "hash": 1257662222, + "hash64": 16960016932285839873 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100207, + "MazeSkillDesc": { + "hash": -532785840, + "hash64": 6563058564954010889 + } + } + ], + "1003": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100301, + "MPCost": 0, + "MazeSkillName": { + "hash": 1277865198, + "hash64": 4667262901714996457 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100306, + "MazeSkillDesc": { + "hash": -512582864, + "hash64": 10733091117566922244 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100302, + "MPCost": 1, + "MazeSkillName": { + "hash": -288218743, + "hash64": 3952145331818453083 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100307, + "MazeSkillDesc": { + "hash": -2078666805, + "hash64": 6985254401382730069 + } + } + ], + "1004": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1378621747, + "hash64": 13347454347127946391 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100406, + "MazeSkillDesc": { + "hash": 1429934413, + "hash64": 3537312459530059262 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100402, + "MPCost": 1, + "MazeSkillName": { + "hash": -1350261608, + "hash64": 588095609729431192 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100407, + "MazeSkillDesc": { + "hash": -1298948942, + "hash64": 4593365716097439213 + } + } + ], + "1005": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1330058632, + "hash64": 9566545702834250764 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100506, + "MazeSkillDesc": { + "hash": -1278745966, + "hash64": 17946010303407283627 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1398824723, + "hash64": 7896034686358619582 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100507, + "MazeSkillDesc": { + "hash": 1450137389, + "hash64": 16804974714797740554 + } + } + ], + "1006": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100601, + "MPCost": 0, + "MazeSkillName": { + "hash": -1229302083, + "hash64": 11138214395449219435 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100606, + "MazeSkillDesc": { + "hash": -1177989417, + "hash64": 14940796022188903327 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100602, + "MPCost": 1, + "MazeSkillName": { + "hash": 336781858, + "hash64": 10774627959751021657 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100607, + "MazeSkillDesc": { + "hash": 388094524, + "hash64": 12191499035274400963 + } + } + ], + "1008": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100801, + "MPCost": 0, + "MazeSkillName": { + "hash": 1170202103, + "hash64": 12282331437308123444 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100806, + "MazeSkillDesc": { + "hash": -620245959, + "hash64": 14625486861923124678 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100802, + "MPCost": 1, + "MazeSkillName": { + "hash": -1558681252, + "hash64": 216834749675897312 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100807, + "MazeSkillDesc": { + "hash": 945837982, + "hash64": 13769321104939833752 + } + } + ], + "1009": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 100901, + "MPCost": 0, + "MazeSkillName": { + "hash": -1538478276, + "hash64": 17776577080397137299 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 100906, + "MazeSkillDesc": { + "hash": 966040958, + "hash64": 2375527577524260346 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 100902, + "MPCost": 1, + "MazeSkillName": { + "hash": 1190405079, + "hash64": 2386633272090929861 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 100907, + "MazeSkillDesc": { + "hash": -600042983, + "hash64": 3072723029562542703 + } + } + ], + "1013": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 101301, + "MPCost": 0, + "MazeSkillName": { + "hash": 1277865101, + "hash64": 7265243453548084349 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 101306, + "MazeSkillDesc": { + "hash": -512582961, + "hash64": 9923503321146675920 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 101302, + "MPCost": 1, + "MazeSkillName": { + "hash": -288218840, + "hash64": 15839247237085848906 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 101307, + "MazeSkillDesc": { + "hash": -2078666902, + "hash64": 15922884948676304948 + } + } + ], + "1014": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 101401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1378621650, + "hash64": 1705476568878360541 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 101406, + "MazeSkillDesc": { + "hash": 1429934316, + "hash64": 2349585899956578359 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 101402, + "MPCost": 1, + "MazeSkillName": { + "hash": -1350261705, + "hash64": 2131679287400046228 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 101407, + "MazeSkillDesc": { + "hash": -1298949039, + "hash64": 7390757777689990238 + } + } + ], + "1015": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 101501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1330058729, + "hash64": 17752871237353419341 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 101506, + "MazeSkillDesc": { + "hash": -1278746063, + "hash64": 6960843341344428304 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 101502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1398824626, + "hash64": 17763112812948076962 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 101507, + "MazeSkillDesc": { + "hash": 1450137292, + "hash64": 16442474104826542263 + } + } + ], + "1101": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110101, + "MPCost": 0, + "MazeSkillName": { + "hash": -772573505, + "hash64": 6846218851126158188 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110106, + "MazeSkillDesc": { + "hash": 1731945729, + "hash64": 11023793643540982133 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110102, + "MPCost": 1, + "MazeSkillName": { + "hash": 793510436, + "hash64": 2715450797506126586 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110107, + "MazeSkillDesc": { + "hash": -996937626, + "hash64": 11436907837945778959 + } + } + ], + "1102": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110201, + "MPCost": 0, + "MazeSkillName": { + "hash": -389491954, + "hash64": 9883595818263082514 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110206, + "MazeSkillDesc": { + "hash": 2115027280, + "hash64": 17187533410502805959 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110202, + "MPCost": 1, + "MazeSkillName": { + "hash": -1955575895, + "hash64": 9652132674578135568 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110207, + "MazeSkillDesc": { + "hash": 548943339, + "hash64": 10756779759283891465 + } + } + ], + "1103": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110301, + "MPCost": 0, + "MazeSkillName": { + "hash": 914469961, + "hash64": 12109955181888881364 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110306, + "MazeSkillDesc": { + "hash": -875978101, + "hash64": 12606688954753840988 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110302, + "MPCost": 1, + "MazeSkillName": { + "hash": -1814413394, + "hash64": 3756565497528464574 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110307, + "MazeSkillDesc": { + "hash": 690105840, + "hash64": 12198672556603723277 + } + } + ], + "1104": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1297551512, + "hash64": 11107422624534154216 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110406, + "MazeSkillDesc": { + "hash": 1348864178, + "hash64": 17007584638237134520 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110402, + "MPCost": 1, + "MazeSkillName": { + "hash": -268532429, + "hash64": 16465630562442615338 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110407, + "MazeSkillDesc": { + "hash": -217219763, + "hash64": 6539914285719267064 + } + } + ], + "1105": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1693453869, + "hash64": 7277256686755245661 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110506, + "MazeSkillDesc": { + "hash": -1642141203, + "hash64": 952056578607537733 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110502, + "MPCost": 1, + "MazeSkillName": { + "hash": -127369928, + "hash64": 15608032157879312817 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110507, + "MazeSkillDesc": { + "hash": -76057262, + "hash64": 17306671154267909824 + } + } + ], + "1106": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110601, + "MPCost": 0, + "MazeSkillName": { + "hash": -1310372318, + "hash64": 223493276539282815 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110606, + "MazeSkillDesc": { + "hash": -1259059652, + "hash64": 9464646140826966244 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110602, + "MPCost": 1, + "MazeSkillName": { + "hash": 1418511037, + "hash64": 7797918780304376175 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110607, + "MazeSkillDesc": { + "hash": 1469823703, + "hash64": 3213812874235900115 + } + } + ], + "1107": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110701, + "MPCost": 0, + "MazeSkillName": { + "hash": -6410403, + "hash64": 14450181475850822293 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110706, + "MazeSkillDesc": { + "hash": 44902263, + "hash64": 3836073715458767018 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110702, + "MPCost": 1, + "MazeSkillName": { + "hash": 1559673538, + "hash64": 18369560404921726697 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110707, + "MazeSkillDesc": { + "hash": 1610986204, + "hash64": 14148159078548416211 + } + } + ], + "1108": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110801, + "MPCost": 0, + "MazeSkillName": { + "hash": 1089131868, + "hash64": 8153524235163083859 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110806, + "MazeSkillDesc": { + "hash": -701316194, + "hash64": 13438372841013799758 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110802, + "MPCost": 1, + "MazeSkillName": { + "hash": -476952073, + "hash64": 5581274634827364645 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110807, + "MazeSkillDesc": { + "hash": 2027567161, + "hash64": 5306097469786164471 + } + } + ], + "1109": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 110901, + "MPCost": 0, + "MazeSkillName": { + "hash": -1901873513, + "hash64": 11228457345009729593 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 110906, + "MazeSkillDesc": { + "hash": 602645721, + "hash64": 10217434787850415800 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 110902, + "MPCost": 1, + "MazeSkillName": { + "hash": -335789572, + "hash64": 6748729869023268701 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 110907, + "MazeSkillDesc": { + "hash": -2126237634, + "hash64": 6098890592682475784 + } + } + ], + "1110": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 111001, + "MPCost": 0, + "MazeSkillName": { + "hash": -2076535517, + "hash64": 15150800378172726275 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 111006, + "MazeSkillDesc": { + "hash": 427983717, + "hash64": 8534023398314514662 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 111002, + "MPCost": 1, + "MazeSkillName": { + "hash": 652347838, + "hash64": 13120985949993331134 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 111007, + "MazeSkillDesc": { + "hash": -1138100224, + "hash64": 16110430404622445360 + } + } + ], + "1111": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 111101, + "MPCost": 0, + "MazeSkillName": { + "hash": -772573602, + "hash64": 10349082851003234409 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 111106, + "MazeSkillDesc": { + "hash": 1731945632, + "hash64": 17003194835914812072 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 111102, + "MPCost": 1, + "MazeSkillName": { + "hash": 793510339, + "hash64": 2389965797571204027 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 111107, + "MazeSkillDesc": { + "hash": -996937723, + "hash64": 1299757451627166961 + } + } + ], + "1112": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 111201, + "MPCost": 0, + "MazeSkillName": { + "hash": -389492051, + "hash64": 1260943115534770639 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 111206, + "MazeSkillDesc": { + "hash": 2115027183, + "hash64": 4285199541129811237 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 111202, + "MPCost": 1, + "MazeSkillName": { + "hash": -1955575992, + "hash64": 6603069620288058335 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 111207, + "MazeSkillDesc": { + "hash": 548943242, + "hash64": 9643765315118065946 + } + } + ], + "1201": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120101, + "MPCost": 0, + "MazeSkillName": { + "hash": 833399726, + "hash64": 15449248475832413741 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120106, + "MazeSkillDesc": { + "hash": -957048336, + "hash64": 7269823453229386155 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120102, + "MPCost": 1, + "MazeSkillName": { + "hash": -732684215, + "hash64": 11250454493905179031 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120107, + "MazeSkillDesc": { + "hash": 1771835019, + "hash64": 2418127287111454188 + } + } + ], + "1202": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120201, + "MPCost": 0, + "MazeSkillName": { + "hash": 1855036639, + "hash64": 1228507346141437587 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120206, + "MazeSkillDesc": { + "hash": 64588577, + "hash64": 14804461332310173647 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120202, + "MPCost": 1, + "MazeSkillName": { + "hash": -873846716, + "hash64": 9081283787816824161 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120207, + "MazeSkillDesc": { + "hash": 1630672518, + "hash64": 1946062360018704091 + } + } + ], + "1203": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120301, + "MPCost": 0, + "MazeSkillName": { + "hash": -853643740, + "hash64": 14921260664847506875 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120306, + "MazeSkillDesc": { + "hash": 1650875494, + "hash64": 9475231028894075740 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120302, + "MPCost": 1, + "MazeSkillName": { + "hash": 1875239615, + "hash64": 407305986897280805 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120307, + "MazeSkillDesc": { + "hash": 84791553, + "hash64": 3346481737184906490 + } + } + ], + "1204": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120401, + "MPCost": 0, + "MazeSkillName": { + "hash": -1673767555, + "hash64": 12409592796134345726 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120406, + "MazeSkillDesc": { + "hash": -1622454889, + "hash64": 11888543467354279894 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120402, + "MPCost": 1, + "MazeSkillName": { + "hash": -107683614, + "hash64": 10737622511105836382 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120407, + "MazeSkillDesc": { + "hash": -56370948, + "hash64": 17611379369879172222 + } + } + ], + "1205": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120501, + "MPCost": 0, + "MazeSkillName": { + "hash": -87480638, + "hash64": 10962804638162316450 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120506, + "MazeSkillDesc": { + "hash": -36167972, + "hash64": 11772405415508530407 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120502, + "MPCost": 1, + "MazeSkillName": { + "hash": -1653564579, + "hash64": 8076542147616280791 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120507, + "MazeSkillDesc": { + "hash": -1602251913, + "hash64": 15520305118120011199 + } + } + ], + "1206": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120601, + "MPCost": 0, + "MazeSkillName": { + "hash": 934156275, + "hash64": 8423397986596961808 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120606, + "MazeSkillDesc": { + "hash": 985468941, + "hash64": 8615686049159534185 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120602, + "MPCost": 1, + "MazeSkillName": { + "hash": -1794727080, + "hash64": 13765756069260958959 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120607, + "MazeSkillDesc": { + "hash": -1743414414, + "hash64": 15199328177885337382 + } + } + ], + "1207": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120701, + "MPCost": 0, + "MazeSkillName": { + "hash": -1774524104, + "hash64": 7772964500389717150 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120706, + "MazeSkillDesc": { + "hash": -1723211438, + "hash64": 13199932322362735632 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120702, + "MPCost": 1, + "MazeSkillName": { + "hash": 954359251, + "hash64": 2344652690839497328 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120707, + "MazeSkillDesc": { + "hash": 1005671917, + "hash64": 16785736742705838560 + } + } + ], + "1208": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120801, + "MPCost": 0, + "MazeSkillName": { + "hash": -1882187199, + "hash64": 7496111430639057474 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120806, + "MazeSkillDesc": { + "hash": 622332035, + "hash64": 5874395407850933043 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120802, + "MPCost": 1, + "MazeSkillName": { + "hash": -316103258, + "hash64": 10862253222576674377 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120807, + "MazeSkillDesc": { + "hash": -2106551320, + "hash64": 14210331761894318844 + } + } + ], + "1209": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 120901, + "MPCost": 0, + "MazeSkillName": { + "hash": -295900282, + "hash64": 16709232332582688415 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 120906, + "MazeSkillDesc": { + "hash": -2086348344, + "hash64": 14156190954966080191 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 120902, + "MPCost": 1, + "MazeSkillName": { + "hash": -1861984223, + "hash64": 7812468394086013885 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 120907, + "MazeSkillDesc": { + "hash": 642535011, + "hash64": 886923627624368721 + } + } + ], + "1210": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121001, + "MPCost": 0, + "MazeSkillName": { + "hash": -752887288, + "hash64": 10688606865408419494 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121006, + "MazeSkillDesc": { + "hash": 1751631946, + "hash64": 862185648281832514 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121002, + "MPCost": 1, + "MazeSkillName": { + "hash": 813196653, + "hash64": 6712756793364059725 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121007, + "MazeSkillDesc": { + "hash": -977251409, + "hash64": 11879321968483870839 + } + } + ], + "1211": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121101, + "MPCost": 0, + "MazeSkillName": { + "hash": 833399629, + "hash64": 12669434865665280095 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121106, + "MazeSkillDesc": { + "hash": -957048433, + "hash64": 14392303202063321149 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121102, + "MPCost": 1, + "MazeSkillName": { + "hash": -732684312, + "hash64": 4061955428160981606 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121107, + "MazeSkillDesc": { + "hash": 1771834922, + "hash64": 14577827286602391346 + } + } + ], + "1212": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121201, + "MPCost": 0, + "MazeSkillName": { + "hash": 1855036542, + "hash64": 9745581525166064926 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121206, + "MazeSkillDesc": { + "hash": 64588480, + "hash64": 192309360550744636 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121202, + "MPCost": 1, + "MazeSkillName": { + "hash": -873846813, + "hash64": 11112065563141558288 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121207, + "MazeSkillDesc": { + "hash": 1630672421, + "hash64": 5031782993838660218 + } + } + ], + "1213": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121301, + "MPCost": 0, + "MazeSkillName": { + "hash": -853643837, + "hash64": 4372540871743201313 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121306, + "MazeSkillDesc": { + "hash": 1650875397, + "hash64": 14489216271821504627 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121302, + "MPCost": 1, + "MazeSkillName": { + "hash": 1875239518, + "hash64": 9475568948866361404 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121307, + "MazeSkillDesc": { + "hash": 84791456, + "hash64": 9712792367226815812 + } + } + ], + "1214": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121401, + "MPCost": 0, + "MazeSkillName": { + "hash": -1673767652, + "hash64": 11401698789744291554 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121406, + "MazeSkillDesc": { + "hash": -1622454986, + "hash64": 10437847531199355528 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121402, + "MPCost": 1, + "MazeSkillName": { + "hash": -107683711, + "hash64": 2782558590568050138 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121407, + "MazeSkillDesc": { + "hash": -56371045, + "hash64": 14010657515952277660 + } + } + ], + "1215": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121501, + "MPCost": 0, + "MazeSkillName": { + "hash": -87480735, + "hash64": 5335395971178502765 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121506, + "MazeSkillDesc": { + "hash": -36168069, + "hash64": 13254173331116903509 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121502, + "MPCost": 1, + "MazeSkillName": { + "hash": -1653564676, + "hash64": 12457401862655513247 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121507, + "MazeSkillDesc": { + "hash": -1602252010, + "hash64": 15084701016272342411 + } + } + ], + "1217": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121701, + "MPCost": 0, + "MazeSkillName": { + "hash": -1774524201, + "hash64": 3802296745210570954 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121706, + "MazeSkillDesc": { + "hash": -1723211535, + "hash64": 14530688239792473122 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121702, + "MPCost": 1, + "MazeSkillName": { + "hash": 954359154, + "hash64": 516782729611931093 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121707, + "MazeSkillDesc": { + "hash": 1005671820, + "hash64": 11624218172893699888 + } + } + ], + "1218": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 121801, + "MPCost": 0, + "MazeSkillName": { + "hash": -1882187296, + "hash64": 1039035584336157564 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 121806, + "MazeSkillDesc": { + "hash": 622331938, + "hash64": 10355654472734274756 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 121802, + "MPCost": 1, + "MazeSkillName": { + "hash": -316103355, + "hash64": 7501144399624937739 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 121807, + "MazeSkillDesc": { + "hash": -2106551417, + "hash64": 17085024604064028610 + } + } + ], + "1220": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 122001, + "MPCost": 0, + "MazeSkillName": { + "hash": -752887257, + "hash64": 6775418162195150239 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 122006, + "MazeSkillDesc": { + "hash": 1751632109, + "hash64": 11970203698591565262 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 122002, + "MPCost": 1, + "MazeSkillName": { + "hash": 813196684, + "hash64": 6456012457901507982 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 122007, + "MazeSkillDesc": { + "hash": -977251246, + "hash64": 13130999159118940850 + } + } + ], + "1221": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 122101, + "MPCost": 0, + "MazeSkillName": { + "hash": 833399660, + "hash64": 4053873866049189788 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 122106, + "MazeSkillDesc": { + "hash": -957048270, + "hash64": 1111951714146496272 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 122102, + "MPCost": 1, + "MazeSkillName": { + "hash": -732684281, + "hash64": 6239317635272711068 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 122107, + "MazeSkillDesc": { + "hash": 1771835085, + "hash64": 238594236434548148 + } + } + ], + "1222": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 122201, + "MPCost": 0, + "MazeSkillName": { + "hash": 1855036573, + "hash64": 3849154143900143701 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 122206, + "MazeSkillDesc": { + "hash": 64588643, + "hash64": 14628242379411847797 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 122202, + "MPCost": 1, + "MazeSkillName": { + "hash": -873846782, + "hash64": 15410650678549243062 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 122207, + "MazeSkillDesc": { + "hash": 1630672584, + "hash64": 5124460769199616497 + } + } + ], + "1223": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 122301, + "MPCost": 0, + "MazeSkillName": { + "hash": -853643806, + "hash64": 3796948899241296632 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 122306, + "MazeSkillDesc": { + "hash": 1650875560, + "hash64": 14072351706510779288 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 122302, + "MPCost": 1, + "MazeSkillName": { + "hash": 1875239549, + "hash64": 12161591570344377161 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 122307, + "MazeSkillDesc": { + "hash": 84791619, + "hash64": 7331937498405324692 + } + } + ], + "1224": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 122401, + "MPCost": 0, + "MazeSkillName": { + "hash": -1673767621, + "hash64": 4370225213093700398 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 122406, + "MazeSkillDesc": { + "hash": -1622454823, + "hash64": 4904444896667445382 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 122402, + "MPCost": 1, + "MazeSkillName": { + "hash": -107683680, + "hash64": 10016485439313012878 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 122407, + "MazeSkillDesc": { + "hash": -56370882, + "hash64": 15647288159057728010 + } + } + ], + "1225": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 122501, + "MPCost": 0, + "MazeSkillName": { + "hash": -87480704, + "hash64": 10812799885117646800 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 122506, + "MazeSkillDesc": { + "hash": -36167906, + "hash64": 5810913718886039197 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 122502, + "MPCost": 1, + "MazeSkillName": { + "hash": -1653564645, + "hash64": 1530181017577923764 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 122507, + "MazeSkillDesc": { + "hash": -1602251847, + "hash64": 12850188275526964967 + } + } + ], + "1301": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130101, + "MPCost": 0, + "MazeSkillName": { + "hash": 470004489, + "hash64": 9542453894545228145 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130106, + "MazeSkillDesc": { + "hash": -1320443573, + "hash64": 10541564435789382060 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130102, + "MPCost": 1, + "MazeSkillName": { + "hash": 2036088430, + "hash64": 3360487722339078520 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130107, + "MazeSkillDesc": { + "hash": 245640368, + "hash64": 5562920905072903810 + } + } + ], + "1302": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130201, + "MPCost": 0, + "MazeSkillName": { + "hash": 1773966404, + "hash64": 12911778675486244250 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130206, + "MazeSkillDesc": { + "hash": -16481658, + "hash64": 3063599867371528873 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130202, + "MPCost": 1, + "MazeSkillName": { + "hash": 207882463, + "hash64": 11425235413948414185 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130207, + "MazeSkillDesc": { + "hash": -1582565599, + "hash64": 16932544734887370542 + } + } + ], + "1303": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130301, + "MPCost": 0, + "MazeSkillName": { + "hash": -1217038977, + "hash64": 7691112083820263974 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130306, + "MazeSkillDesc": { + "hash": 1287480257, + "hash64": 18044540834508831356 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130302, + "MPCost": 1, + "MazeSkillName": { + "hash": 349044964, + "hash64": 11834454727943609281 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130307, + "MazeSkillDesc": { + "hash": -1441403098, + "hash64": 8613207922415128267 + } + } + ], + "1304": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130401, + "MPCost": 0, + "MazeSkillName": { + "hash": -1754837790, + "hash64": 1323918044215332911 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130406, + "MazeSkillDesc": { + "hash": -1703525124, + "hash64": 5062045506526205295 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130402, + "MPCost": 1, + "MazeSkillName": { + "hash": 974045565, + "hash64": 16567600065872116138 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130407, + "MazeSkillDesc": { + "hash": 1025358231, + "hash64": 18193530606352423312 + } + } + ], + "1305": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130501, + "MPCost": 0, + "MazeSkillName": { + "hash": -450875875, + "hash64": 2017566580543452419 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130506, + "MazeSkillDesc": { + "hash": -399563209, + "hash64": 11025634036926086494 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1115208066, + "hash64": 13767216777888606662 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130507, + "MazeSkillDesc": { + "hash": 1166520732, + "hash64": 455257827743238246 + } + } + ], + "1306": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130601, + "MPCost": 0, + "MazeSkillName": { + "hash": 853086040, + "hash64": 18236695673118149637 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130606, + "MazeSkillDesc": { + "hash": 904398706, + "hash64": 1701180564644341153 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130602, + "MPCost": 1, + "MazeSkillName": { + "hash": -712997901, + "hash64": 14461742583067562944 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130607, + "MazeSkillDesc": { + "hash": -661685235, + "hash64": 3362245437701992584 + } + } + ], + "1307": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130701, + "MPCost": 0, + "MazeSkillName": { + "hash": -2137919341, + "hash64": 13573658896210233064 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130706, + "MazeSkillDesc": { + "hash": -2086606675, + "hash64": 13892133039839441403 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130702, + "MPCost": 1, + "MazeSkillName": { + "hash": -571835400, + "hash64": 512443182632047687 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130707, + "MazeSkillDesc": { + "hash": -520522734, + "hash64": 695459077229433057 + } + } + ], + "1308": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130801, + "MPCost": 0, + "MazeSkillName": { + "hash": -1963257434, + "hash64": 3460880993869642288 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130806, + "MazeSkillDesc": { + "hash": 541261800, + "hash64": 14026038269608448687 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130802, + "MPCost": 1, + "MazeSkillName": { + "hash": 765625921, + "hash64": 11840270823406051578 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130807, + "MazeSkillDesc": { + "hash": -1024822141, + "hash64": 967771142511918975 + } + } + ], + "1309": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 130901, + "MPCost": 0, + "MazeSkillName": { + "hash": -659295519, + "hash64": 11370658557962046378 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 130906, + "MazeSkillDesc": { + "hash": 1845223715, + "hash64": 4457406280360053971 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 130902, + "MPCost": 1, + "MazeSkillName": { + "hash": 906788422, + "hash64": 4156067759209941856 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 130907, + "MazeSkillDesc": { + "hash": -883659640, + "hash64": 1649180163628891133 + } + } + ], + "1310": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 131001, + "MPCost": 0, + "MazeSkillName": { + "hash": -833957523, + "hash64": 1779566317384379297 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 131006, + "MazeSkillDesc": { + "hash": 1670561711, + "hash64": 2664659257709224312 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 131002, + "MPCost": 1, + "MazeSkillName": { + "hash": 1894925832, + "hash64": 9510422988243012937 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 131007, + "MazeSkillDesc": { + "hash": 104477770, + "hash64": 9705658436839065530 + } + } + ], + "1312": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 131201, + "MPCost": 0, + "MazeSkillName": { + "hash": 1773966307, + "hash64": 9309864755000194572 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 131206, + "MazeSkillDesc": { + "hash": -16481755, + "hash64": 2531780274071035476 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 131202, + "MPCost": 1, + "MazeSkillName": { + "hash": 207882366, + "hash64": 12644930252354663883 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 131207, + "MazeSkillDesc": { + "hash": -1582565696, + "hash64": 12441167846913322690 + } + } + ], + "1313": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 131301, + "MPCost": 0, + "MazeSkillName": { + "hash": -1217039074, + "hash64": 13134189772038804713 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 131306, + "MazeSkillDesc": { + "hash": 1287480160, + "hash64": 15215290221697269912 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 131302, + "MPCost": 1, + "MazeSkillName": { + "hash": 349044867, + "hash64": 3337866037342755198 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 131307, + "MazeSkillDesc": { + "hash": -1441403195, + "hash64": 9953092651596815743 + } + } + ], + "1314": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 131401, + "MPCost": 0, + "MazeSkillName": { + "hash": -1754837887, + "hash64": 1651424626805937716 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 131406, + "MazeSkillDesc": { + "hash": -1703525221, + "hash64": 17617614312486066663 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 131402, + "MPCost": 1, + "MazeSkillName": { + "hash": 974045468, + "hash64": 2520838839296475492 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 131407, + "MazeSkillDesc": { + "hash": 1025358134, + "hash64": 12974490376721122103 + } + } + ], + "1315": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 131501, + "MPCost": 0, + "MazeSkillName": { + "hash": -450875972, + "hash64": 7699368494831502617 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 131506, + "MazeSkillDesc": { + "hash": -399563306, + "hash64": 7992855595841265213 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 131502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1115207969, + "hash64": 8215251067945386146 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 131507, + "MazeSkillDesc": { + "hash": 1166520635, + "hash64": 12797862089220535378 + } + } + ], + "1317": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 131701, + "MPCost": 0, + "MazeSkillName": { + "hash": -2137919438, + "hash64": 6397905497786805095 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 131706, + "MazeSkillDesc": { + "hash": -2086606772, + "hash64": 18165145077911102466 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 131702, + "MPCost": 1, + "MazeSkillName": { + "hash": -571835497, + "hash64": 57125599071598718 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 131707, + "MazeSkillDesc": { + "hash": -520522831, + "hash64": 10736830432531124410 + } + } + ], + "1321": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 132101, + "MPCost": 0, + "MazeSkillName": { + "hash": 470004423, + "hash64": 18098688243116219778 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 132106, + "MazeSkillDesc": { + "hash": -1320443507, + "hash64": 11974584881331479572 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 132102, + "MPCost": 1, + "MazeSkillName": { + "hash": 2036088364, + "hash64": 13399339357695741358 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 132107, + "MazeSkillDesc": { + "hash": 245640434, + "hash64": 14767888194181230511 + } + } + ], + "1401": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140101, + "MPCost": 0, + "MazeSkillName": { + "hash": 2075977720, + "hash64": 454845101700708075 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140106, + "MazeSkillDesc": { + "hash": -389815022, + "hash64": 9725174384766037701 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140102, + "MPCost": 1, + "MazeSkillName": { + "hash": 509893779, + "hash64": 1751855416720834442 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140107, + "MazeSkillDesc": { + "hash": -1955898963, + "hash64": 12660013239584791248 + } + } + ], + "1402": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140201, + "MPCost": 0, + "MazeSkillName": { + "hash": -2118233027, + "hash64": 1966056971090194159 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140206, + "MazeSkillDesc": { + "hash": -289058473, + "hash64": 13510107043767211834 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140202, + "MPCost": 1, + "MazeSkillName": { + "hash": -552149086, + "hash64": 15731360280174184625 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140207, + "MazeSkillDesc": { + "hash": 1277025468, + "hash64": 7935412248989722568 + } + } + ], + "1403": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140301, + "MPCost": 0, + "MazeSkillName": { + "hash": -531946110, + "hash64": 5239786145274848727 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140306, + "MazeSkillDesc": { + "hash": 1297228444, + "hash64": 133250347442617510 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140302, + "MPCost": 1, + "MazeSkillName": { + "hash": -2098030051, + "hash64": 17165613157094425809 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140307, + "MazeSkillDesc": { + "hash": -268855497, + "hash64": 18119276797108627483 + } + } + ], + "1404": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1410571167, + "hash64": 17758792574522905561 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140406, + "MazeSkillDesc": { + "hash": 1397984993, + "hash64": 9417023020764122439 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140402, + "MPCost": 1, + "MazeSkillName": { + "hash": -1318312188, + "hash64": 3996373843590284449 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140407, + "MazeSkillDesc": { + "hash": -1330898362, + "hash64": 7729122561036206080 + } + } + ], + "1405": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1298109212, + "hash64": 13371130920977299100 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140506, + "MazeSkillDesc": { + "hash": -1310695386, + "hash64": 12336573762793590820 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1430774143, + "hash64": 7004141407051383926 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140507, + "MazeSkillDesc": { + "hash": 1418187969, + "hash64": 14996694594203529724 + } + } + ], + "1406": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140601, + "MPCost": 0, + "MazeSkillName": { + "hash": -1197352663, + "hash64": 14112576446355730382 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140606, + "MazeSkillDesc": { + "hash": -1209938837, + "hash64": 5956919146234350973 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140602, + "MPCost": 1, + "MazeSkillName": { + "hash": 368731278, + "hash64": 13579606201892672641 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140607, + "MazeSkillDesc": { + "hash": 356145104, + "hash64": 5671664757179850513 + } + } + ], + "1407": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140701, + "MPCost": 0, + "MazeSkillName": { + "hash": 388934254, + "hash64": 10422642191455892222 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140706, + "MazeSkillDesc": { + "hash": 376348080, + "hash64": 14686497556512958337 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140702, + "MPCost": 1, + "MazeSkillName": { + "hash": -1177149687, + "hash64": 13625415826491208696 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140707, + "MazeSkillDesc": { + "hash": -1189735861, + "hash64": 8045843599180456677 + } + } + ], + "1408": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140801, + "MPCost": 0, + "MazeSkillName": { + "hash": -639609205, + "hash64": 1992743788433090917 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140806, + "MazeSkillDesc": { + "hash": 1189565349, + "hash64": 12025058142599389916 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140802, + "MPCost": 2, + "MazeSkillName": { + "hash": 926474736, + "hash64": 15698647733121983720 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140807, + "MazeSkillDesc": { + "hash": -1539318006, + "hash64": 12625831754524189396 + } + } + ], + "1409": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 140901, + "MPCost": 0, + "MazeSkillName": { + "hash": 946677712, + "hash64": 3147159170743419267 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 140906, + "MazeSkillDesc": { + "hash": -1519115030, + "hash64": 6479816846298286943 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 140902, + "MPCost": 1, + "MazeSkillName": { + "hash": -619406229, + "hash64": 5891725525251977979 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 140907, + "MazeSkillDesc": { + "hash": 1209768325, + "hash64": 10406907882247340068 + } + } + ], + "1410": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 141001, + "MPCost": 0, + "MazeSkillName": { + "hash": 489690706, + "hash64": 11944856110428471091 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 141006, + "MazeSkillDesc": { + "hash": -1976102036, + "hash64": 13767911249956379628 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 141002, + "MPCost": 1, + "MazeSkillName": { + "hash": 2055774647, + "hash64": 5312190190633944782 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 141007, + "MazeSkillDesc": { + "hash": -410018095, + "hash64": 16263292621958595376 + } + } + ], + "1412": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 141201, + "MPCost": 0, + "MazeSkillName": { + "hash": -2118233124, + "hash64": 5371521412845348483 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 141206, + "MazeSkillDesc": { + "hash": -289058570, + "hash64": 16999769545446988662 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 141202, + "MPCost": 1, + "MazeSkillName": { + "hash": -552149183, + "hash64": 3116655097203063783 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 141207, + "MazeSkillDesc": { + "hash": 1277025371, + "hash64": 2801665436406134099 + } + } + ], + "1413": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 141301, + "MPCost": 0, + "MazeSkillName": { + "hash": -531946207, + "hash64": 9140026706826724866 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 141306, + "MazeSkillDesc": { + "hash": 1297228347, + "hash64": 1715446894585646794 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 141302, + "MPCost": 1, + "MazeSkillName": { + "hash": -2098030148, + "hash64": 2913103344148703887 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 141307, + "MazeSkillDesc": { + "hash": -268855594, + "hash64": 2951637289692801888 + } + } + ], + "1414": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 141401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1410571070, + "hash64": 67106946983897497 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 141406, + "MazeSkillDesc": { + "hash": 1397984896, + "hash64": 2160987786533385657 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 141402, + "MPCost": 1, + "MazeSkillName": { + "hash": -1318312285, + "hash64": 5208314178196239862 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 141407, + "MazeSkillDesc": { + "hash": -1330898459, + "hash64": 14814599236698059612 + } + } + ], + "1415": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 141501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1298109309, + "hash64": 8949187277120431914 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 141506, + "MazeSkillDesc": { + "hash": -1310695483, + "hash64": 9557525825574660933 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 141502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1430774046, + "hash64": 9056043832688306428 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 141507, + "MazeSkillDesc": { + "hash": 1418187872, + "hash64": 15488513032824331964 + } + } + ], + "1501": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150101, + "MPCost": 0, + "MazeSkillName": { + "hash": 1712582483, + "hash64": 3757955956709794823 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150106, + "MazeSkillDesc": { + "hash": -753210259, + "hash64": 12701230057601635288 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150102, + "MPCost": 1, + "MazeSkillName": { + "hash": -1016300872, + "hash64": 13289284020934301842 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150107, + "MazeSkillDesc": { + "hash": 812873682, + "hash64": 16669740328513847450 + } + } + ], + "1502": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150201, + "MPCost": 0, + "MazeSkillName": { + "hash": 2095664034, + "hash64": 4810308524936679689 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150206, + "MazeSkillDesc": { + "hash": -370128708, + "hash64": 4971753447192285036 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150202, + "MPCost": 1, + "MazeSkillName": { + "hash": 529580093, + "hash64": 7266805724164537094 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150207, + "MazeSkillDesc": { + "hash": -1936212649, + "hash64": 10948613988922354983 + } + } + ], + "1503": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150301, + "MPCost": 0, + "MazeSkillName": { + "hash": -895341347, + "hash64": 17142355289770080045 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150306, + "MazeSkillDesc": { + "hash": 933833207, + "hash64": 14733884642631965899 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150302, + "MPCost": 1, + "MazeSkillName": { + "hash": 670742594, + "hash64": 5157871884830685271 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150307, + "MazeSkillDesc": { + "hash": -1795050148, + "hash64": 15926317047657164336 + } + } + ], + "1504": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1329500932, + "hash64": 1389031072413113332 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150406, + "MazeSkillDesc": { + "hash": 1316914758, + "hash64": 4211506675359497665 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150402, + "MPCost": 1, + "MazeSkillName": { + "hash": -236583009, + "hash64": 12867382235478748678 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150407, + "MazeSkillDesc": { + "hash": -249169183, + "hash64": 8101610877150904726 + } + } + ], + "1505": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1661504449, + "hash64": 15425350230712886497 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150506, + "MazeSkillDesc": { + "hash": -1674090623, + "hash64": 17036915178587174691 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150502, + "MPCost": 1, + "MazeSkillName": { + "hash": -95420508, + "hash64": 14118112538289972194 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150507, + "MazeSkillDesc": { + "hash": -108006682, + "hash64": 737410782675411827 + } + } + ], + "1506": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150601, + "MPCost": 0, + "MazeSkillName": { + "hash": -1278422898, + "hash64": 10428075745571345240 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150606, + "MazeSkillDesc": { + "hash": -1291009072, + "hash64": 3622258917485038030 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150602, + "MPCost": 1, + "MazeSkillName": { + "hash": 1450460457, + "hash64": 16223708826358830299 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150607, + "MazeSkillDesc": { + "hash": 1437874283, + "hash64": 11303538385647456502 + } + } + ], + "1507": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150701, + "MPCost": 0, + "MazeSkillName": { + "hash": 25539017, + "hash64": 14406622552495046915 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150706, + "MazeSkillDesc": { + "hash": 12952843, + "hash64": 2869166191153370373 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150702, + "MPCost": 1, + "MazeSkillName": { + "hash": 1591622958, + "hash64": 13881553542824543554 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150707, + "MazeSkillDesc": { + "hash": 1579036784, + "hash64": 6125585426472386667 + } + } + ], + "1508": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150801, + "MPCost": 0, + "MazeSkillName": { + "hash": -720679440, + "hash64": 960760698718392670 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150806, + "MazeSkillDesc": { + "hash": 1108495114, + "hash64": 16588235450481698256 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150802, + "MPCost": 1, + "MazeSkillName": { + "hash": 2008203915, + "hash64": 12135909780657622773 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150807, + "MazeSkillDesc": { + "hash": -457588827, + "hash64": 5301410710111123969 + } + } + ], + "1509": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 150901, + "MPCost": 0, + "MazeSkillName": { + "hash": 583282475, + "hash64": 12882880370011756818 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 150906, + "MazeSkillDesc": { + "hash": -1882510267, + "hash64": 14619375238605622139 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 150902, + "MPCost": 1, + "MazeSkillName": { + "hash": -2145600880, + "hash64": 2459816397934550364 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 150907, + "MazeSkillDesc": { + "hash": -316426326, + "hash64": 15588719541521103678 + } + } + ], + "1510": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 151001, + "MPCost": 0, + "MazeSkillName": { + "hash": 408620471, + "hash64": 2865500817494238718 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 151006, + "MazeSkillDesc": { + "hash": -2057172271, + "hash64": 131417256778058576 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 151002, + "MPCost": 2, + "MazeSkillName": { + "hash": -1157463470, + "hash64": 13622808486996066725 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 151007, + "MazeSkillDesc": { + "hash": 671711084, + "hash64": 11490600883707864325 + } + } + ], + "1511": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 151101, + "MPCost": 0, + "MazeSkillName": { + "hash": 1712582386, + "hash64": 3741464500807155193 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 151106, + "MazeSkillDesc": { + "hash": -753210356, + "hash64": 16609432096917925587 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 151102, + "MPCost": 1, + "MazeSkillName": { + "hash": -1016300969, + "hash64": 9338050691448443876 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 151107, + "MazeSkillDesc": { + "hash": 812873585, + "hash64": 16745130354377095692 + } + } + ], + "1512": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 151201, + "MPCost": 0, + "MazeSkillName": { + "hash": 2095663937, + "hash64": 9340863523125731685 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 151206, + "MazeSkillDesc": { + "hash": -370128805, + "hash64": 4389046258364939919 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 151202, + "MPCost": 1, + "MazeSkillName": { + "hash": 529579996, + "hash64": 3388774110602993525 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 151207, + "MazeSkillDesc": { + "hash": -1936212746, + "hash64": 18315676922132643325 + } + } + ], + "1513": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 151301, + "MPCost": 0, + "MazeSkillName": { + "hash": -895341444, + "hash64": 16336237140419929447 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 151306, + "MazeSkillDesc": { + "hash": 933833110, + "hash64": 10570393683477251553 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 151302, + "MPCost": 1, + "MazeSkillName": { + "hash": 670742497, + "hash64": 18205664071745730245 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 151307, + "MazeSkillDesc": { + "hash": -1795050245, + "hash64": 15923306502229133165 + } + } + ], + "8001": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800101, + "MPCost": 0, + "MazeSkillName": { + "hash": -409172757, + "hash64": 2272387677045854308 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800106, + "MazeSkillDesc": { + "hash": 2095346477, + "hash64": 1898460966108692845 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800102, + "MPCost": 1, + "MazeSkillName": { + "hash": -1975256698, + "hash64": 11302906050668873768 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800107, + "MazeSkillDesc": { + "hash": 529262536, + "hash64": 16483929648311348985 + } + } + ], + "8002": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800201, + "MPCost": 0, + "MazeSkillName": { + "hash": -308416208, + "hash64": 9819929196829072746 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800206, + "MazeSkillDesc": { + "hash": -2098864270, + "hash64": 10224401200005241369 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800202, + "MPCost": 1, + "MazeSkillName": { + "hash": 1257667733, + "hash64": 6258873691795835465 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800207, + "MazeSkillDesc": { + "hash": -532780329, + "hash64": 6549647465960858061 + } + } + ], + "8003": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800301, + "MPCost": 0, + "MazeSkillName": { + "hash": 1277870709, + "hash64": 1180690222940171337 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800306, + "MazeSkillDesc": { + "hash": -512577353, + "hash64": 11776032426742128118 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800302, + "MPCost": 1, + "MazeSkillName": { + "hash": -288213232, + "hash64": 1230517724533104534 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800307, + "MazeSkillDesc": { + "hash": -2078661294, + "hash64": 14110825837563752083 + } + } + ], + "8004": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800401, + "MPCost": 0, + "MazeSkillName": { + "hash": 1378627258, + "hash64": 8749580268865374493 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800406, + "MazeSkillDesc": { + "hash": 1429939924, + "hash64": 10285705706762181410 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800402, + "MPCost": 1, + "MazeSkillName": { + "hash": -1350256097, + "hash64": 9195175882694563019 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800407, + "MazeSkillDesc": { + "hash": -1298943431, + "hash64": 5584058003341593822 + } + } + ], + "8005": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800501, + "MPCost": 0, + "MazeSkillName": { + "hash": -1330053121, + "hash64": 12146392795149152635 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800506, + "MazeSkillDesc": { + "hash": -1278740455, + "hash64": 16862893642989879098 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800502, + "MPCost": 1, + "MazeSkillName": { + "hash": 1398830234, + "hash64": 6798638682963056227 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800507, + "MazeSkillDesc": { + "hash": 1450142900, + "hash64": 7903771081530865657 + } + } + ], + "8006": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800601, + "MPCost": 0, + "MazeSkillName": { + "hash": -1229296572, + "hash64": 1091007384546666047 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800606, + "MazeSkillDesc": { + "hash": -1177983906, + "hash64": 5795694876727301978 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800602, + "MPCost": 1, + "MazeSkillName": { + "hash": 336787369, + "hash64": 8924450514707672264 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800607, + "MazeSkillDesc": { + "hash": 388100035, + "hash64": 3294428927941625822 + } + } + ], + "8007": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800701, + "MPCost": 0, + "MazeSkillName": { + "hash": 356990345, + "hash64": 15742939142924889903 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800706, + "MazeSkillDesc": { + "hash": 408303011, + "hash64": 5490527401060946700 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800702, + "MPCost": 1, + "MazeSkillName": { + "hash": -1209093596, + "hash64": 16993820888723114817 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800707, + "MazeSkillDesc": { + "hash": -1157780930, + "hash64": 14617182532847161176 + } + } + ], + "8008": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800801, + "MPCost": 0, + "MazeSkillName": { + "hash": 1170207614, + "hash64": 683768001394594727 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800806, + "MazeSkillDesc": { + "hash": -620240448, + "hash64": 11921931130232294040 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800802, + "MPCost": 1, + "MazeSkillName": { + "hash": -1558675741, + "hash64": 2024266439611249878 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800807, + "MazeSkillDesc": { + "hash": 945843493, + "hash64": 7316400916913040095 + } + } + ], + "8009": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 800901, + "MPCost": 0, + "MazeSkillName": { + "hash": -1538472765, + "hash64": 15288977251596849725 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 800906, + "MazeSkillDesc": { + "hash": 966046469, + "hash64": 17566693146905236065 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 800902, + "MPCost": 1, + "MazeSkillName": { + "hash": 1190410590, + "hash64": 15428691907476764016 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 800907, + "MazeSkillDesc": { + "hash": -600037472, + "hash64": 5576694297895119399 + } + } + ], + "8010": [ + { + "SkillTriggerKey": "NormalAtk", + "MazeSkillId": 801001, + "MPCost": 0, + "MazeSkillName": { + "hash": -1995459705, + "hash64": 16878737073916625878 + }, + "MazeSkilltype": 1, + "Priority": 0, + "RelatedAvatarSkill": 801006, + "MazeSkillDesc": { + "hash": 509059529, + "hash64": 1324203628626229274 + } + }, + { + "SkillTriggerKey": "MazeSkill", + "MazeSkillId": 801002, + "MPCost": 1, + "MazeSkillName": { + "hash": -429375764, + "hash64": 8349953878222823392 + }, + "MazeSkilltype": 2, + "Priority": 0, + "RelatedAvatarSkill": 801007, + "MazeSkillDesc": { + "hash": 2075143470, + "hash64": 7022399418414218491 + } + } + ] + }, + "initial_food_count": 100, + "auto_food_default": true +} diff --git a/run.ps1 b/run.ps1 index a10703a..9031f8b 100644 --- a/run.ps1 +++ b/run.ps1 @@ -82,7 +82,7 @@ try { Merge-Settings $settings (Get-Content -LiteralPath $localPath -Raw | ConvertFrom-Json) } $variables = @{ - client_version='GAC_CLIENT_VERSION'; game_host='GAC_GAME_HOST'; game_port='GAC_GAME_PORT'; + player_uid='GAC_PLAYER_UID'; client_version='GAC_CLIENT_VERSION'; game_host='GAC_GAME_HOST'; game_port='GAC_GAME_PORT'; sdk_host='GAC_SDK_HOST'; sdk_port='GAC_SDK_PORT'; public_game_host='GAC_PUBLIC_GAME_HOST'; public_sdk_url='GAC_PUBLIC_SDK_URL' } @@ -90,6 +90,10 @@ try { $value = [Environment]::GetEnvironmentVariable($variables[$key]) if ($null -ne $value) { $settings | Add-Member -MemberType NoteProperty -Name $key -Value $value -Force } } + $configuredUID = [uint32]0 + if (-not [uint32]::TryParse([string]$settings.player_uid, [ref]$configuredUID) -or $configuredUID -eq 0) { + throw 'player_uid must be an integer between 1 and 4294967295.' + } foreach ($key in @('game_port','sdk_port')) { $port = 0 if (-not [int]::TryParse([string]$settings.$key, [ref]$port) -or $port -lt 1 -or $port -gt [uint16]::MaxValue) { diff --git a/scripts/freecam.lua b/scripts/freecam.lua index a010d1a..bd2c06c 100644 --- a/scripts/freecam.lua +++ b/scripts/freecam.lua @@ -5,10 +5,13 @@ local Time = CS.UnityEngine.Time local Camera = CS.UnityEngine.Camera local GameObject = CS.UnityEngine.GameObject +-- Camera control tuning, in degrees per second (independent of frame rate). +local KEYBOARD_TURN_SPEED = 90.0 + _G.__FREECAM_ENABLED = _G.__FREECAM_ENABLED or false _G.__FREECAM_SPEED = _G.__FREECAM_SPEED or 15.0 -local RAINBOW_TEXT = "นี่คือเวอร์ชั่นทดสอบของเกย์ C is gay and i love to play Honkai Star Gay" +local RAINBOW_TEXT = "นี่คือเวอร์ชั่นทดสอบของเกย์ Honkai Star Gay" local function show_ui(msg) pcall(function() @@ -71,8 +74,23 @@ end function _G.__FREECAM_TICK() pcall(function() + -- Render callbacks run for several cameras/passes in one frame. Consume + -- key-down events and integrate movement only once per Unity frame. + local frame = Time.frameCount + if _G.__FC_INPUT_FRAME == frame then + if _G.__FREECAM_ENABLED then + local cam = get_active_cam() + if cam and _G.__FC_POS then + cam.transform.position = _G.__FC_POS + cam.transform.eulerAngles = _G.__FC_EULER + end + end + return + end + _G.__FC_INPUT_FRAME = frame if Input.GetKeyDown(KeyCode.Tab) then _G.__FREECAM_ENABLED = not _G.__FREECAM_ENABLED + _G.__FC_LAST_TIME = nil local cam = get_active_cam() if _G.__FREECAM_ENABLED then if cam then @@ -80,7 +98,7 @@ function _G.__FREECAM_TICK() _G.__FC_EULER = cam.transform.eulerAngles set_cinemachine_enabled(cam, false) end - show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrow Keys: Fly | Right-Click: Look | WASD: Walk") + show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Up/Down | I/J/K/L: Look") else if cam then set_cinemachine_enabled(cam, true) @@ -107,19 +125,23 @@ function _G.__FREECAM_TICK() -- Speed adjustment (+ / -) if Input.GetKeyDown(KeyCode.Equals) or Input.GetKeyDown(KeyCode.KeypadPlus) then _G.__FREECAM_SPEED = math.min(300.0, _G.__FREECAM_SPEED * 1.5) - show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrow Keys: Fly") + show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Up/Down | I/J/K/L: Look") end if Input.GetKeyDown(KeyCode.Minus) or Input.GetKeyDown(KeyCode.KeypadMinus) then _G.__FREECAM_SPEED = math.max(0.5, _G.__FREECAM_SPEED / 1.5) - show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrow Keys: Fly") + show_ui("[FreeCam: ON] Speed: " .. string.format("%.1f", _G.__FREECAM_SPEED) .. " | Arrows: Fly | PgUp/PgDn: Up/Down | I/J/K/L: Look") end - -- Mouse Look (Hold Right Mouse Button) - if Input.GetMouseButton(1) then - local rotX = Input.GetAxis("Mouse X") or 0 - local rotY = Input.GetAxis("Mouse Y") or 0 - _G.__FC_EULER = Vector3(_G.__FC_EULER.x - rotY * 2.5, _G.__FC_EULER.y + rotX * 2.5, 0) - end + -- Keyboard-only look: no mouse click is needed, so turning + -- the camera does not require pressing a battle action button. + local pitch, yaw = 0, 0 + if Input.GetKey(KeyCode.I) then pitch = pitch - 1 end + if Input.GetKey(KeyCode.K) then pitch = pitch + 1 end + if Input.GetKey(KeyCode.J) then yaw = yaw - 1 end + if Input.GetKey(KeyCode.L) then yaw = yaw + 1 end + _G.__FC_EULER = Vector3( + _G.__FC_EULER.x + pitch * KEYBOARD_TURN_SPEED * dt, + _G.__FC_EULER.y + yaw * KEYBOARD_TURN_SPEED * dt, 0) -- Compute 3D direction vectors from camera Euler angles local radY = _G.__FC_EULER.y * (math.pi / 180.0) @@ -144,10 +166,10 @@ function _G.__FREECAM_TICK() if Input.GetKey(KeyCode.LeftArrow) then move = move - right end - if Input.GetKey(KeyCode.PageUp) or Input.GetKey(KeyCode.E) or Input.GetKey(KeyCode.RightShift) then + if Input.GetKey(KeyCode.PageUp) then move = move + Vector3(0, 1, 0) end - if Input.GetKey(KeyCode.PageDown) or Input.GetKey(KeyCode.Q) or Input.GetKey(KeyCode.RightControl) then + if Input.GetKey(KeyCode.PageDown) then move = move - Vector3(0, 1, 0) end diff --git a/scripts/hide_ui.lua b/scripts/hide_ui.lua index c79030c..ccd9d2b 100644 --- a/scripts/hide_ui.lua +++ b/scripts/hide_ui.lua @@ -1,158 +1,111 @@ --- ============================================================================== --- Hide UI Lua Script for Honkai: Star Rail --- Press "ฃ" (Backslash '\' on keyboard) to toggle UI visibility --- Completely hides 100% of UI elements (HUD, Minimap, UID, Texts, Battle UI, WorldUI) --- Matches the clean visual result of 3Dmigoto/SRMI shader-override mod! --- ============================================================================== - +-- Backslash toggles HUD only. Keep pages, dialogs and UI cameras visible. local Input = CS.UnityEngine.Input local KeyCode = CS.UnityEngine.KeyCode local Time = CS.UnityEngine.Time local GameObject = CS.UnityEngine.GameObject local CanvasGroup = CS.UnityEngine.CanvasGroup -local Canvas = CS.UnityEngine.Canvas local Camera = CS.UnityEngine.Camera +-- Names observed under UIRoot in this Client, plus the existing battle HUD. +-- Never target an ancestor shared by HUD and character/reward dialogs. +local HUD_PATHS = { + "UIRoot/Page/MazeMainPage(Clone)", + "UIRoot/Page/BattleGamePhaseUI(Clone)", + "UIRoot/BelowPage/AdventureWaypointsPage(Clone)", + "UIRoot/BelowPage/MazeJumpingText(Clone)", + "UIRoot/BelowPage/MazeMonsterWarningPage(Clone)", + "UIRoot/AboveDialog/BetaHintDialog(Clone)", + "VersionText" +} + +-- The old script did not record CanvasGroup state. Undo its blanket hide once +-- on live upgrade; a fresh Client session avoids any lost legacy fade state. +if _G.__HIDE_UI_REGISTERED and _G.__HIDE_UI_SCHEMA ~= 2 and _G.__HIDE_UI_ENABLED then + pcall(function() + local cams = Camera.allCameras + for i = 0, cams.Length - 1 do + local mask = (_G.__ORIG_CAM_MASKS or {})[cams[i]:GetInstanceID()] + if mask ~= nil then cams[i].cullingMask = mask end + end + local function restore(obj) + if obj then + local cg = obj:GetComponent(typeof(CanvasGroup)) + if cg then cg.alpha = 1; cg.blocksRaycasts = true; cg.interactable = true end + end + end + local root = GameObject.Find("UIRoot") + if root then + restore(root) + local t = root.transform + for i = 0, t.childCount - 1 do + local c = t:GetChild(i); restore(c.gameObject) + for j = 0, c.childCount - 1 do restore(c:GetChild(j).gameObject) end + end + end + local canvases = GameObject.FindObjectsOfType(typeof(CS.UnityEngine.Canvas)) + for i = 0, canvases.Length - 1 do restore(canvases[i].gameObject) end + end) +end +_G.__HIDE_UI_SCHEMA = 2 _G.__HIDE_UI_ENABLED = _G.__HIDE_UI_ENABLED or false -_G.__HIDE_UI_LAST_FRAME = _G.__HIDE_UI_LAST_FRAME or 0 -_G.__ORIG_CAM_MASKS = _G.__ORIG_CAM_MASKS or {} -_G.__HIDE_UI_PERIODIC = _G.__HIDE_UI_PERIODIC or 0 +_G.__HIDE_UI_TARGETS = _G.__HIDE_UI_TARGETS or {} -local function set_canvas_group_state(obj, visible) - if not obj then return end - pcall(function() - local cg = obj:GetComponent(typeof(CanvasGroup)) - if not cg then - cg = obj:AddComponent(typeof(CanvasGroup)) - end - if cg then - cg.alpha = visible and 1.0 or 0.0 - cg.blocksRaycasts = visible - cg.interactable = visible - end - end) -end - -local function apply_camera_culling(visible) - pcall(function() - local allCams = Camera.allCameras - if allCams and allCams.Length > 0 then - for i = 0, allCams.Length - 1 do - local c = allCams[i] - if c and c.name then - local lower = string.lower(c.name) - -- Target UICamera and UI overlay cameras, never touch Main Camera - if (string.find(lower, "ui") ~= nil) and (string.find(lower, "main") == nil) then - local camId = c:GetInstanceID() - if not visible then - if not _G.__ORIG_CAM_MASKS[camId] and c.cullingMask ~= 0 then - _G.__ORIG_CAM_MASKS[camId] = c.cullingMask - end - c.cullingMask = 0 - else - if _G.__ORIG_CAM_MASKS[camId] ~= nil and _G.__ORIG_CAM_MASKS[camId] ~= 0 then - c.cullingMask = _G.__ORIG_CAM_MASKS[camId] - else - c.cullingMask = 32 -- Default layer 5 (UI) - end - end - end - end - end - end - end) -end - -local function apply_ui_root(visible) - pcall(function() - local uiRoot = GameObject.Find("/UIRoot") or GameObject.Find("UIRoot") - if uiRoot then - set_canvas_group_state(uiRoot, visible) - local tr = uiRoot.transform - for i = 0, tr.childCount - 1 do - local child = tr:GetChild(i).gameObject - set_canvas_group_state(child, visible) - local ctr = child.transform - for j = 0, ctr.childCount - 1 do - set_canvas_group_state(ctr:GetChild(j).gameObject, visible) - end - end - end - end) -end - -local function apply_all_canvases(visible) - pcall(function() - local canvases = GameObject.FindObjectsOfType(typeof(Canvas)) - if canvases and canvases.Length > 0 then - for i = 0, canvases.Length - 1 do - local cv = canvases[i] - if cv and cv.gameObject then - set_canvas_group_state(cv.gameObject, visible) - end - end - end - end) -end - -local function set_ui_visible(visible) - pcall(function() - -- 1. UI Camera Culling Mask - apply_camera_culling(visible) - - -- 2. Entire /UIRoot tree (Page, HUD, Dialog, AboveDialog, Top, Popup, Guide, etc.) - apply_ui_root(visible) - - -- 3. Specific known clones / watermarks +local function restore_hud() + for id, state in pairs(_G.__HIDE_UI_TARGETS) do pcall(function() - local specific = { - "UIRoot/Page/BattleGamePhaseUI(Clone)", - "UIRoot/AboveDialog/BetaHintDialog(Clone)", - "VersionText" - } - for _, path in ipairs(specific) do - local obj = GameObject.Find(path) - if obj then - set_canvas_group_state(obj, visible) + if state.group then + if state.added then + CS.UnityEngine.Object.Destroy(state.group) + else + state.group.alpha = state.alpha + state.group.blocksRaycasts = state.raycast + state.group.interactable = state.interactable end end end) + _G.__HIDE_UI_TARGETS[id] = nil + end +end - -- 4. All active Canvases in scene (ScreenSpace & WorldSpace billboards/nameplates) - apply_all_canvases(visible) - end) +local function hide_hud() + for _, path in ipairs(HUD_PATHS) do + pcall(function() + local obj = GameObject.Find(path) + if not obj then return end + local id = obj:GetInstanceID() + local state = _G.__HIDE_UI_TARGETS[id] + if not state then + local group = obj:GetComponent(typeof(CanvasGroup)) + local added = not group + if added then group = obj:AddComponent(typeof(CanvasGroup)) end + state = {group=group, added=added, alpha=group.alpha, + raycast=group.blocksRaycasts, interactable=group.interactable} + _G.__HIDE_UI_TARGETS[id] = state + end + state.group.alpha = 0 + state.group.blocksRaycasts = false + state.group.interactable = false + end) + end end function _G.__HIDE_UI_TICK() pcall(function() - local curFrame = Time.frameCount - if _G.__HIDE_UI_LAST_FRAME == curFrame then - return - end - _G.__HIDE_UI_LAST_FRAME = curFrame - - -- Physical key '\' / 'ฃ' (KeyCode.Backslash) - if Input.GetKeyDown(KeyCode.Backslash) then + local frame = Time.frameCount + if _G.__HIDE_UI_LAST_FRAME == frame then return end + _G.__HIDE_UI_LAST_FRAME = frame + local toggled = Input.GetKeyDown(KeyCode.Backslash) + if toggled then _G.__HIDE_UI_ENABLED = not _G.__HIDE_UI_ENABLED - - set_ui_visible(not _G.__HIDE_UI_ENABLED) - - if _G.__HIDE_UI_ENABLED then - CS.UnityEngine.Debug.Log("[UI Toggle] UI Hidden (ซ่อน UI ทั้งหมด 100% เรียบร้อย)") - else - CS.UnityEngine.Debug.Log("[UI Toggle] UI Restored (แสดง UI ตามปกติแล้ว)") - end + if not _G.__HIDE_UI_ENABLED then restore_hud() end end - - -- While hidden, keep enforcing to prevent newly spawned UI from appearing + -- Only the small named HUD list is checked, four times a second. + -- New pages/dialogs never inherit a hidden UIRoot or UI camera. if _G.__HIDE_UI_ENABLED then - apply_camera_culling(false) - apply_ui_root(false) - - _G.__HIDE_UI_PERIODIC = (_G.__HIDE_UI_PERIODIC or 0) + 1 - if _G.__HIDE_UI_PERIODIC >= 30 then - _G.__HIDE_UI_PERIODIC = 0 - apply_all_canvases(false) + local now = Time.unscaledTime + if toggled or now >= (_G.__HIDE_UI_NEXT_SCAN or 0) then + _G.__HIDE_UI_NEXT_SCAN = now + 0.25 + hide_hud() end end end) @@ -173,5 +126,5 @@ if not _G.__HIDE_UI_REGISTERED then pcall(function() CS.UnityEngine.Application.onBeforeRender('+', function() _G.__HIDE_UI_TICK() end) end) pcall(function() CS.UnityEngine.Application.onBeforeRender = CS.UnityEngine.Application.onBeforeRender + function() _G.__HIDE_UI_TICK() end end) - CS.UnityEngine.Debug.Log("[UI Toggle Script] 100% Clean HideUI loaded! Press 'ฃ' to toggle UI.") + CS.UnityEngine.Debug.Log("[UI Toggle Script] HUD-only HideUI loaded! Press 'ฃ' to toggle UI.") end diff --git a/server.json b/server.json index 17fc690..62afb29 100644 --- a/server.json +++ b/server.json @@ -1,4 +1,5 @@ { + "player_uid": 52010, "client_version": "4.6.53", "game_host": "0.0.0.0", "game_port": 23301, diff --git a/tools/audit_endgame_resources.py b/tools/audit_endgame_resources.py new file mode 100644 index 0000000..95a7cd3 --- /dev/null +++ b/tools/audit_endgame_resources.py @@ -0,0 +1,79 @@ +"""Audit every installed Endgame room against stage tables and Client metadata. + +This reads data only. It does not establish that a room or battle plays in the +Client, nor does it validate reward/settlement callbacks. +""" +import argparse +import collections +import json +import pathlib + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path(__file__).resolve().parents[1]) +parser.add_argument("--output", type=pathlib.Path, required=True) +args = parser.parse_args() +def read(name): + return json.loads((args.root / name).read_text(encoding="utf-8")) +metadata = read("resources/SceneMetadata.json")["floors"] +entrances = {row["ID"]: row for row in read("resources/MapEntrance.json")["map_entrance_config"]} +stages = {row["StageID"]: row for row in read("resources/StageConfig.json")["stage_config"]} +root_scenes = read("res.json")["levelOutputConfigs"] +supplementary = {row["entryID"]: row for row in read("resources/res.json")["scene_config"]} +checks = [] +def check(mode, identifier, node, entry, group, events, config_ids): + problems = [] + entrance = entrances.get(entry) + floor_id = entrance["FloorID"] if entrance else None + floor = metadata.get(str(floor_id)) + if floor is None or len(floor["groups"]) != floor["group_count"]: + problems.append("missing or incomplete Client floor metadata") + else: + group_data = floor["groups"].get(str(group)) + if not group_data or group_data["is_deleted"] or group_data["load_side"] == "Client": + problems.append("invalid server group") + if not floor.get("start_anchor"): + problems.append("missing Client start anchor") + placements = floor.get("monsters", {}).get(str(group), {}) + for config_id in config_ids: + if str(config_id) not in placements: + problems.append(f"missing Client monster placement {config_id}") + source = root_scenes.get(str(entry), {}) + if source: + monsters = [m for scene in source.values() for m in scene["scenes"].get(str(group), {}).get("monsters", [])] + else: + monsters = [m for m in supplementary.get(entry, {}).get("monsters", []) if m["groupId"] == group] + if not monsters: + problems.append("missing scene monster source used by Go loader") + for event in events: + stage = stages.get(event) + if not stage or not stage["MonsterList"] or any(not wave for wave in stage["MonsterList"]): + problems.append(f"missing stage waves {event}") + checks.append({"mode": mode, "id": identifier, "node": node, "entry": entry, + "floor": floor_id, "group": group, "events": events, "issues": problems}) + +maze = read("resources/ChallengeMazeConfig.json")["challenge_config"] +tierce = read("resources/ChallengeMazeTierceConfig.json")["challenge_tierce_config"] +peak = read("resources/ChallengePeakConfig.json")["challenge_peak_config"] +for row in maze: + mode = "PF" if 20000 < row["ID"] < 30000 else "AS" if row["ID"] > 30000 else "Memory/MoC" + for index in (1, 2): + suffix = str(index) + if row.get("EventIDList" + suffix): + check(mode, row["ID"], index - 1, row["MapEntranceID" + ("2" if index == 2 else "")], + row["MazeGroupID" + suffix], row["EventIDList" + suffix], row["ConfigList" + suffix]) +for row in tierce: + check("Tierce", row["ID"], 2, row["MapEntranceID"], row["MazeGroupID"], row["EventIDList"], row["ConfigList"]) +for row in peak: + # Peak's selected monster instance is resolved from its configured group. + check("Peak", row["ID"], 0, row["MapEntranceID"], row["MazeGroupID"], row["EventIDList"], []) +by_id = {row["ID"]: row for row in peak} +for row in read("resources/ChallengePeakBossConfig.json")["challenge_peak_boss_config"]: + base = by_id[row["ID"]] + check("Peak hard", row["ID"], 0, base["MapEntranceID"], base["MazeGroupID"], row["HardEventIDList"], []) +report = {"scope": "resource and Client metadata audit; no gameplay verification", + "counts": dict(collections.Counter(row["mode"] for row in checks)), + "checked_rooms": len(checks), "floors": len({row["floor"] for row in checks}), + "issues": [row for row in checks if row["issues"]]} +args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") +print(json.dumps({key: value for key, value in report.items() if key != "issues"})) +print(f"Issues: {len(report['issues'])}; details: {args.output}") diff --git a/tools/export_scene_metadata.py b/tools/export_scene_metadata.py new file mode 100644 index 0000000..9d0d8df --- /dev/null +++ b/tools/export_scene_metadata.py @@ -0,0 +1,74 @@ +"""Export complete RuntimeFloor group captures from a Client Player.log. + +The capture must include FLOOR/GROUP/END records from a read-only Client +GameCoreConfigLoader probe. Inputs and Client revision are explicit arguments. +""" +import argparse +import hashlib +import json +import pathlib +import re + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("log", type=pathlib.Path) +parser.add_argument("--assembly", type=pathlib.Path, required=True) +parser.add_argument("--client-version", required=True) +parser.add_argument("--output", type=pathlib.Path, required=True) +parser.add_argument("--merge", action="store_true", help="Preserve compatible previously captured floors in output") +args = parser.parse_args() +floors, completed = {}, set() +placements = {} +placement_completed = set() +for line in args.log.read_text(encoding="utf-8", errors="replace").splitlines(): + if "[GAC_SCENE_PLACEMENT] " in line: + row = line.split("[GAC_SCENE_PLACEMENT] ", 1)[1] + end_placement = re.fullmatch(r"END (\d+)", row) + if end_placement: + placement_completed.add(end_placement[1]) + position = re.fullmatch(r"(ANCHOR|MONSTER) (\d+) (\d+)(?: (\d+))? pos=([^,]+),([^,]+),([^ ]+) rot=(.+)", row) + if position: + kind, floor, first_id, inst_id, x, y, z, rot = position.groups() + # Unity metres/degrees -> protocol position/rotation units. + value = {"pos": {"x": round(float(x)*1000), "y": round(float(y)*1000), "z": round(float(z)*1000)}, "rot": {"x": 0, "y": round(float(rot)*1000), "z": 0}} + target = placements.setdefault(floor, {"monsters": {}}) + if kind == "ANCHOR": + value["ID"] = int(first_id) + target["start_anchor"] = value + else: + target["monsters"].setdefault(first_id, {})[inst_id] = value + continue + if "[GAC_SCENE_META]" not in line: + continue + row = line.split("[GAC_SCENE_META] ", 1)[1] + start = re.fullmatch(r"FLOOR (\d+) (\d+) startGroup=(\d+) anchor=(\d+) count=(\d+)", row) + group = re.fullmatch(r"GROUP (\d+) (\d+) side=(\w+): \d+ hoyo=(true|false) deleted=(true|false)", row) + end = re.fullmatch(r"END (\d+)", row) + if start: + plane, floor, start_group, anchor, count = start.groups() + floors[floor] = {"plane_id": int(plane), "group_count": int(count), "groups": {}} + completed.discard(floor) + elif group: + floor, group_id, side, hoyo, deleted = group.groups() + if floor in floors: + floors[floor]["groups"][group_id] = {"load_side": side, "is_hoyo_group": hoyo == "true", "is_deleted": deleted == "true"} + elif end: + completed.add(end[1]) +if not floors or any(key not in completed or len(value["groups"]) != value["group_count"] for key, value in floors.items()): + raise SystemExit("Incomplete scene metadata capture; no output written") +for key, value in placements.items(): + if key in floors and key in placement_completed: + floors[key].update(value) +result = {"client_version": args.client_version, "source": { + "kind": "Client GameCoreConfigLoader.LoadRtLevelFloorInfo / GroupInstanceCommonMap", + "assembly_sha256": hashlib.file_digest(args.assembly.open("rb"), "sha256").hexdigest(), + "capture_sha256": hashlib.sha256(args.log.read_bytes()).hexdigest(), +}, "floors": floors} +if args.merge and args.output.exists(): + previous = json.loads(args.output.read_text(encoding="utf-8")) + if previous["client_version"] != result["client_version"] or previous["source"]["assembly_sha256"] != result["source"]["assembly_sha256"]: + raise SystemExit("Cannot merge metadata from different Clients") + previous["floors"].update(floors) + result["floors"] = previous["floors"] + result["source"]["previous_capture_sha256"] = previous["source"].get("previous_capture_sha256", []) + [previous["source"]["capture_sha256"]] +args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") +print(f"Exported {len(floors)} floors to {args.output}") diff --git a/tools/scene_metadata_probe.lua b/tools/scene_metadata_probe.lua new file mode 100644 index 0000000..64763ec --- /dev/null +++ b/tools/scene_metadata_probe.lua @@ -0,0 +1,74 @@ +-- Read-only template. Replace __FLOOR_INPUTS__ with Lua pairs from the +-- installed MapEntrance table, and __CAPTURE_KEY__ with a unique string. +-- Read a bounded batch per heartbeat; never enter a map or alter entities. +do + local key = '__CAPTURE_KEY__' + _G.__gacSceneExports = _G.__gacSceneExports or {} + local progress = _G.__gacSceneExports[key] or 1 + local floors = __FLOOR_INPUTS__ + local function log(kind, text) + CS.UnityEngine.Debug.Log('[GAC_SCENE_' .. kind .. '] ' .. tostring(text)) + end + local function world(group, item) + local yaw = group.RotY * math.pi / 180 + return group.PosX + item.PosX * math.cos(yaw) + item.PosZ * math.sin(yaw), + group.PosY + item.PosY, + group.PosZ - item.PosX * math.sin(yaw) + item.PosZ * math.cos(yaw), + group.RotY + item.RotY + end + local function position(kind, prefix, group, item) + local x, y, z, rot = world(group, item) + log(kind, prefix .. ' pos=' .. tostring(x) .. ',' .. tostring(y) .. ',' .. tostring(z) .. ' rot=' .. tostring(rot)) + end + local batchSize = 4 + for index = progress, math.min(progress + batchSize - 1, #floors) do + local pair = floors[index] + local ok, err = pcall(function() + local path = CS.RPG.Client.MapDef.GetRtFloorInfoPath(pair[1], pair[2]) + local floor = CS.RPG.GameCore.GameCoreConfigLoader.LoadRtLevelFloorInfo(path) + if floor == nil then error('missing RuntimeFloor ' .. path) end + local floorID = tostring(pair[2]) + log('META', 'FLOOR ' .. tostring(pair[1]) .. ' ' .. floorID + .. ' startGroup=' .. tostring(floor.StartGroupIndex) .. ' anchor=' .. tostring(floor.StartAnchorID) + .. ' count=' .. tostring(floor.GroupInstanceCommonMap.Count)) + local iterator = floor.GroupInstanceCommonMap:GetEnumerator() + while iterator:MoveNext() do + local entry = iterator.Current + log('META', 'GROUP ' .. floorID .. ' ' .. tostring(entry.Key) + .. ' side=' .. tostring(entry.Value.LoadSide) .. ' hoyo=' .. tostring(entry.Value.IsHoyoGroup) + .. ' deleted=' .. tostring(entry.Value.IsDelete)) + end + log('META', 'END ' .. floorID) + for i = 0, floor.GroupInstanceList.Length - 1 do + local group = floor.GroupInstanceList[i] + local common = floor.GroupInstanceCommonMap[group.ID] + if not group.IsDelete and common ~= nil and not common.IsDelete + and (i == floor.StartGroupIndex or tostring(common.LoadSide):find('Server', 1, true)) then + local data = CS.RPG.GameCore.GameCoreConfigLoader.LoadRtLevelGroupInfo(group.GroupPath) + if data == nil then error('missing RuntimeGroup ' .. tostring(group.GroupPath)) end + if i == floor.StartGroupIndex and data.AnchorList ~= nil then + for j = 0, data.AnchorList.Length - 1 do + local anchor = data.AnchorList[j] + if not anchor.IsDelete and anchor.ID == floor.StartAnchorID then + position('PLACEMENT', 'ANCHOR ' .. floorID .. ' ' .. tostring(anchor.ID), group, anchor) + end + end + end + if data.MonsterList ~= nil then + for j = 0, data.MonsterList.Length - 1 do + local monster = data.MonsterList[j] + if not monster.IsDelete and not monster.IsClientOnly then + position('PLACEMENT', 'MONSTER ' .. floorID .. ' ' .. tostring(group.ID) + .. ' ' .. tostring(monster.ID), group, monster) + end + end + end + end + end + log('PLACEMENT', 'END ' .. floorID) + end) + if not ok then log('META', 'ERROR ' .. tostring(pair[2]) .. ' ' .. tostring(err)) end + progress = index + 1 + end + _G.__gacSceneExports[key] = progress +end