fix: sync current handlers, repair relic data and restore visible launcher consoles
This commit is contained in:
1 parent
163d4433e6
commit
74f3c4411b
46 files changed
+4565
-1368
No files matched your search
@@ -3,3 +3,10 @@ gameserver.log
|
|||||||
server_debug.log
|
server_debug.log
|
||||||
*.log
|
*.log
|
||||||
*.exe~
|
*.exe~
|
||||||
|
battle_last.json
|
||||||
|
server.local.json
|
||||||
|
*.next.exe
|
||||||
|
battle_last.bin
|
||||||
|
battle_last_response.bin
|
||||||
|
battle_last_capture.json
|
||||||
|
battle_diagnostics/
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Repository rules
|
||||||
|
|
||||||
|
## ห้าม hardcode ค่าที่เปลี่ยนตามการใช้งาน
|
||||||
|
|
||||||
|
- ห้ามใส่ path ของเครื่อง, ชื่อผู้ใช้, Desktop, ไดรฟ์ติดตั้ง, path ของเกม/Proxy/Go, IP, port, URL ของ deployment หรือเวอร์ชัน Client ลงใน source code หรือ startup scripts
|
||||||
|
- ค่ารันระบบต้องมาจาก `server.json` โดย override ผ่าน `server.local.json` หรือ environment ได้ ใช้ตัวโหลดร่วมกันและตรวจค่าก่อนเริ่มทำงาน ห้ามมี fallback ที่ซ่อนค่าคงที่ไว้ในโค้ด
|
||||||
|
- ค่าเฉพาะเครื่องต้องอยู่ในไฟล์ local ที่ถูก ignore หรือ environment ห้าม commit ข้อมูลเฉพาะเครื่อง ห้ามเดาตำแหน่งโปรแกรมจากชื่อโฟลเดอร์หรือเลขเวอร์ชัน
|
||||||
|
- ค่าจากระบบเกม เช่น StageID, MonsterID, floor, group, anchor, level, buff และรายการปลดล็อก ต้องอ่านจาก resource/config ที่ตรงกับ Client เมื่อมีตารางรองรับ ห้ามเพิ่มการแก้บัคแบบเจาะจง ID โดยไม่มีข้อมูลรองรับ
|
||||||
|
- main affix ของ relic ต้องตรวจในกลุ่ม MainAffixGroup ของชิ้นนั้น ห้ามใช้ ID ข้ามกลุ่มหรือเดาจากเลข stat ทั่วไป และห้ามแก้ converter ให้บังคับเปลี่ยน ID เฉพาะตัวละครเพื่อกลบข้อมูล inventory ที่ผิด
|
||||||
|
- ค่าคงที่ที่เป็นนิยามของ protocol, enum, schema, โครงสร้างโปรเจกต์ และขอบเขตของชนิดข้อมูลใช้ได้เมื่อมีที่มา ตรวจสอบได้ และระบุชื่อที่สื่อความหมาย ห้ามใช้ข้อยกเว้นนี้แทนค่าตั้งระบบ
|
||||||
|
- ห้ามอ้างว่าเอา hardcode ออกทั้ง repository หากตรวจหรือแก้เพียงบางส่วน ต้องบอกขอบเขตและส่วนที่ยังเหลือจริง
|
||||||
|
- เมื่อแก้ startup/config ต้องตรวจว่าทั้ง SDK dispatch, game gateway และ launcher ใช้ค่าเดียวกัน พร้อมอัปเดต README และทดสอบ config override/ค่าผิดก่อน build
|
||||||
|
- ห้ามอ้างว่า build/test ผ่านแล้วแปลว่า Client เข้า battle หรือโหลดฉากได้ ต้องยืนยันจาก Client หรือรายงานว่ายังรอตรวจ
|
||||||
|
- handler ต้อง resolve CmdID จากชื่อในตาราง protocol ที่มี revision อ้างอิง ห้ามรับ alias เลขจากหลายเวอร์ชันหรือส่ง response หลายเลขเพื่อเดา callback; ถ้า schema ยัง obfuscated ต้องบันทึกข้อสันนิษฐานและข้อจำกัดไว้
|
||||||
|
- อย่าเปลี่ยนค่า protocol เช่น ClientDownloadData Lua format ตามเลข patch ของ Client โดยไม่มีต้นฉบับรองรับ และอย่าเปลี่ยนแค่ข้อความเวอร์ชันเพื่ออ้างว่าอัปเดต handler แล้ว
|
||||||
|
- SDK และ game server ที่เปิดผ่าน launcher ต้องแสดงคอนโซลและ log ให้ผู้ใช้เห็น ห้ามซ่อนหน้าต่างหรือ redirect จนคอนโซลว่าง เว้นแต่ผู้ใช้ขอเอง เมนูแบบ interactive ปิดหลังทำรายการเสร็จ 5 วินาที โดยไม่ปิดเซิร์ฟเวอร์ตามไปด้วย
|
||||||
@@ -1,102 +1,201 @@
|
|||||||
# GaC hkrpg GO — StarRail 4.6.51 Private Server (ภาษา Go)
|
# GaC hkrpg GO
|
||||||
|
|
||||||
โอนย้าย (port) จากโปรเจกต์ Rust `GaC-hkrpg-ps` มาเป็นภาษา Go ทั้งหมด
|
เซิร์ฟเวอร์จำลอง Honkai: Star Rail ภาษา Go พอร์ตจาก `GaC-hkrpg-ps` และอ้างอิงการทำงานจาก [PearlSR](https://git.xeondev.com/HonkaiSlopRail/pearl-sr) ประกอบด้วย game server, SDK/dispatch server และ REST API สำหรับ GaC-Srtool
|
||||||
พร้อม **แก้บัคกล้องหลุดจากตัวละครตอนกดตี (SceneCastSkill)** ตั้งแต่ต้นทาง
|
|
||||||
|
|
||||||
---
|
โปรเจกต์อยู่ระหว่างพัฒนา โดยกำลังตรวจการใช้งานกับ Client **4.6.53 OS** ตารางคำสั่งและ handler ที่แก้ล่าสุดอ้างอิง PearlSR commit `005d2c12db4781071f4b5b68e8f364587886a710` รายละเอียดอยู่ใน [บันทึกอัปเดต protocol](docs/protocol-update.md) การตรงกันของ schema ไม่ได้ยืนยันว่าเกมทุกระบบทำงานครบ
|
||||||
|
|
||||||
## บัคกล้องหลุด (SceneCastSkill) — สาเหตุและวิธีแก้
|
## เริ่มใช้งานบน Windows
|
||||||
|
|
||||||
อาการ: กดตีใน overworld แล้วกล้องหลุดออกจากตัวละคร
|
1. ติดตั้ง **Go 1.27.0 ขึ้นไป** ตาม `go.mod` และตรวจด้วย `go version`
|
||||||
|
2. เตรียม Client, launcher/proxy และการ redirect dispatch ให้เชื่อมต่อเซิร์ฟเวอร์ที่กำหนดใน `server.json`
|
||||||
|
3. เก็บไฟล์ข้อมูลและโฟลเดอร์ `resources` ไว้ใน root ของโปรเจกต์ และตั้งค่า launcher ตามหัวข้อ config ด้านล่าง
|
||||||
|
4. เปิด `run.bat` แล้วเลือกเมนูที่ต้องการ
|
||||||
|
|
||||||
สาเหตุ (จาก `gameserver/src/net/handlers/battle.rs` เดิม):
|
| เมนู | การทำงาน |
|
||||||
handler ของ `SceneCastSkillCsReq` ตอบ `battle_info` + `monster_battle_info` ให้ client
|
| --- | --- |
|
||||||
เข้าสู่การต่อสู้กับ target ที่ผ่าน filter แบบหลวม:
|
| `1` หรือ Enter | Build ทั้งสองเซิร์ฟเวอร์ แล้วเปิดเซิร์ฟเวอร์และเกม |
|
||||||
|
| `2` | เปิดทั้งสองเซิร์ฟเวอร์จาก `.exe` ที่มีอยู่ |
|
||||||
|
| `3` | เปิดเฉพาะ SDK server |
|
||||||
|
| `4` | เปิดเฉพาะ game server |
|
||||||
|
| `5` | เปิดทั้งสองเซิร์ฟเวอร์จาก `.exe` ที่มีอยู่ และเรียก Gay Proxy |
|
||||||
|
| `6` | Build ทั้งสอง `.exe` อย่างเดียว |
|
||||||
|
| `7` | ออก |
|
||||||
|
| `8` หรือ `stop` | หยุดเซิร์ฟเวอร์ของโปรเจกต์นี้ |
|
||||||
|
|
||||||
```
|
เมนู 1 และ 6 หยุดเมื่อ build ไม่ผ่าน ส่วนเมนู 2–5 ใช้ binary ที่มีอยู่ หลังแก้ซอร์สจึงต้อง build ใหม่ก่อนใช้เมนูเหล่านั้น
|
||||||
filter เดิม: id > 30_000 (monster) || id < 1_000 (avatar entity ของผู้เล่นเอง!)
|
|
||||||
|
`run.bat` เรียก `run.ps1` ผ่าน PowerShell โดยอ่าน path เกม/Proxy จาก config ไม่มีการเดา Desktop, ไดรฟ์ หรือชื่อโฟลเดอร์ตามเวอร์ชัน ต้องตั้ง `launcher.client_path` เป็นไฟล์ launcher/เกมที่จะเปิด และ `launcher.proxy_path` เป็นไฟล์ Proxy ที่ต้องการ ถ้าเว้นว่างจะเปิดเซิร์ฟเวอร์และแสดงวิธีตั้งค่า
|
||||||
|
|
||||||
|
SDK และ game server เปิดหน้าต่างคอนโซลแยกกันให้เห็น log โดยตรง หลังทำเมนูที่เลือกเสร็จ หน้าต่าง `run.bat` จะปิดเองใน 5 วินาที ส่วนหน้าต่างเซิร์ฟเวอร์ยังเปิดอยู่ ใช้ `run.bat stop` เพื่อหยุด หรือปิดหน้าต่างเซิร์ฟเวอร์ที่ต้องการ สคริปต์หยุดเฉพาะ process ที่ path executable ตรงกับ root นี้ ก่อนเริ่มใหม่หรือแทนที่ binary ที่ build สำเร็จ การเรียกด้วย argument เช่น `run.bat build` จะคืนค่าทันทีเมื่อทำงานเสร็จโดยไม่รอ 5 วินาที
|
||||||
|
|
||||||
|
## Config และกฎห้าม hardcode
|
||||||
|
|
||||||
|
`AGENTS.md` กำหนดให้ค่าที่เปลี่ยนตามเครื่อง/deployment/เวอร์ชันต้องมาจาก config, environment หรือ resource ห้ามฝัง path, IP, port หรือ fallback เฉพาะเครื่องใน source และ startup scripts
|
||||||
|
|
||||||
|
ลำดับความสำคัญ: **environment > local config > config หลัก**
|
||||||
|
|
||||||
|
- `server.json`: ค่าเครือข่ายและเวอร์ชันที่แชร์กันระหว่าง launcher, game server และ SDK server
|
||||||
|
- `server.local.json`: override เฉพาะเครื่อง ไฟล์นี้ถูก ignore โดย Git คัดลอก `server.local.example.json` แล้วใส่ path ของเครื่องได้
|
||||||
|
- `GAC_CONFIG`: เลือกไฟล์หลักอื่น โดย local override ใช้ชื่อเดียวกันลงท้าย `.local.json`
|
||||||
|
- path แบบ relative ใน launcher อ้างอิง root ของโปรเจกต์
|
||||||
|
|
||||||
|
| ค่าใน config | Environment override | ใช้สำหรับ |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `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 |
|
||||||
|
| `public_game_host` | `GAC_PUBLIC_GAME_HOST` | ที่อยู่ game server ที่ gateway แจ้ง Client |
|
||||||
|
| `public_sdk_url` | `GAC_PUBLIC_SDK_URL` | Base URL ที่ dispatch แจ้ง Client |
|
||||||
|
| `launcher.client_path` | `GAC_CLIENT_PATH` | ไฟล์ launcher/เกมที่จะเปิด |
|
||||||
|
| `launcher.proxy_path` | `GAC_PROXY_PATH` | ไฟล์ Proxy ที่จะเปิด |
|
||||||
|
| `launcher.go_command` | `GAC_GO_COMMAND` | คำสั่ง Go จาก PATH หรือ path ที่กำหนด |
|
||||||
|
|
||||||
|
เมื่อเปลี่ยนเครือข่ายให้ตั้งทั้ง bind และค่าที่ประกาศให้ Client เข้าถึงได้ โดยเฉพาะ port ใน `public_sdk_url` เมื่อ SDK รับการเชื่อมต่อโดยตรง ค่าเหล่านี้มีค่าเริ่มต้นอยู่ในไฟล์ config และไม่มี fallback เครือข่ายในโค้ด ต้องรีสตาร์ตเซิร์ฟเวอร์หลังเปลี่ยน config การเปลี่ยน `client_version` ไม่ได้แปลง protocol หรือทรัพยากรของเกมให้อัตโนมัติ
|
||||||
|
|
||||||
|
ตรวจ config โดยไม่เปิดเกมหรือเซิร์ฟเวอร์:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\run.bat check
|
||||||
```
|
```
|
||||||
|
|
||||||
เมื่อ client ส่ง hit target เป็น prop / NPC / ตัวผู้เล่นเอง server จะสั่งเข้า battle
|
## ทำไมมี executable สองตัว
|
||||||
กับ "monster" ที่ไม่มีอยู่จริง → battle transition โหลดไม่สำเร็จ → กล้อง battle
|
|
||||||
ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร นอกจากนี้ถ้าอยู่ใน arena
|
|
||||||
(entry >= 3000000) โค้ดเดิมยัง "หลอก" target เป็น 30001 เสมอ
|
|
||||||
|
|
||||||
วิธีแก้ (ใน `internal/gameserver/handlers_battle.go`):
|
| ไฟล์ | หน้าที่ | พอร์ต |
|
||||||
1. ตอนโหลดฉาก server เก็บรายการ **monster entity จริง** ไว้ใน `Session.MonsterEntities`
|
| --- | --- | --- |
|
||||||
(entity_id → event_id)
|
| `gameserver-go.exe` | รับการเชื่อมต่อเกมผ่าน KCP และจัดการผู้เล่น ฉาก และ battle | UDP ตาม `game_port` |
|
||||||
2. `SceneCastSkill` ยอมรับเฉพาะ hit target ที่เป็น monster จริงเท่านั้น
|
| `sdkserver-go.exe` | Dispatch, login, ข้อมูล hotfix และ REST API | HTTP ตาม `sdk_port` |
|
||||||
3. ถ้าไม่มี monster จริงถูกตี → ตอบ `retcode 0 + cast_entity_id` เฉยๆ **ไม่เข้า battle**
|
|
||||||
client เล่นแอนิเมชันตีปกติ กล้องค้างกับตัวละคร
|
|
||||||
4. ตีโดน monster จริง (รวมบอสใน arena 30001) → เข้า battle ตามปกติ
|
|
||||||
|
|
||||||
ยืนยันด้วย test อัตโนมัติ: `internal/gameserver/smoke_test.go`
|
การเปิดระบบครบต้องรันทั้งสองตัว `.exe` ที่ build เสร็จจะอยู่ใน root ของโปรเจกต์ ให้เปิดจาก root เพื่อให้เซิร์ฟเวอร์หาไฟล์ข้อมูลและบันทึก log ได้ถูกตำแหน่ง
|
||||||
(จำลอง client เต็มรูปแบบ: handshake → login → กดตี ทั้งกรณี ผ่าน UDP จริง)
|
|
||||||
|
|
||||||
## สถาปัตยกรรม
|
## Build และรันเอง
|
||||||
|
|
||||||
```
|
รันคำสั่งจาก root ของโปรเจกต์:
|
||||||
cmd/gameserver เกมเซิร์ฟเวอร์ (UDP :23301, KCP)
|
|
||||||
cmd/sdkserver dispatch + login + SRTools (HTTP :21000)
|
|
||||||
protocol/proto StarRail.pb.go — generate จาก proto 4.6.51 (4,932 messages)
|
|
||||||
protocol/proto/cmdid.go — ตาราง CmdID (2,114 ids) generate จาก "// CmdID:" comments
|
|
||||||
pkg/kcp KCP สาย mihoyo (header 28 ไบต์: conv+token) — port จาก kcp.rs 1:1
|
|
||||||
internal/common FreesrData / persistent / res.json / teleports.json + proto converters
|
|
||||||
internal/gameserver gateway, packet framing, session, dispatch, handlers ทั้งหมด
|
|
||||||
internal/sdkserver query_dispatch / query_gateway / auth / srtools / versions.json
|
|
||||||
```
|
|
||||||
|
|
||||||
ไฟล์ข้อมูลใช้ชุดเดิมกับ Rust PS ทุกไฟล์ (freesr-data.json, persistent, res.json,
|
```powershell
|
||||||
teleports.json, challenge_data.json, mission_unlocks.json, versions.json) — save เดิมใช้ได้ทันที
|
|
||||||
|
|
||||||
## การใช้งาน
|
|
||||||
|
|
||||||
```
|
|
||||||
run_go.bat เมนู 1/2/3/4 เหมือนเดิม
|
|
||||||
gameserver-go.exe เกมเซิร์ฟเวอร์
|
|
||||||
sdkserver-go.exe SDK/dispatch
|
|
||||||
```
|
|
||||||
|
|
||||||
ต้องแก้ hosts ของเครื่อง client แบบเดียวกับ Rust PS (หรือใช้ Fiddler/proxy เดิม)
|
|
||||||
|
|
||||||
## Build จากซอร์ส
|
|
||||||
|
|
||||||
ต้องมี Go 1.27+ และ protoc + protoc-gen-go (เฉพาะตอนแก้ proto):
|
|
||||||
|
|
||||||
```
|
|
||||||
go build -o gameserver-go.exe ./cmd/gameserver
|
go build -o gameserver-go.exe ./cmd/gameserver
|
||||||
go build -o sdkserver-go.exe ./cmd/sdkserver
|
go build -o sdkserver-go.exe ./cmd/sdkserver
|
||||||
go test ./... # รวม smoke test จำลอง client จริง
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## สิ่งที่พอร์ตครบแล้ว
|
เปิดคนละ terminal:
|
||||||
|
|
||||||
- Network: mihoyo KCP (conv+token), NetOperation handshake (0xFF/0x145/0x194),
|
```powershell
|
||||||
packet framing (0x9D74C714/0xD7A152C8), freesr-data.json hot-reload watcher
|
.\sdkserver-go.exe
|
||||||
- Login flow ครบ (GetToken/PlayerLogin/GetBasicInfo/PlayerLoginFinish + ContentPackage)
|
```
|
||||||
- ฉาก: GetCurSceneInfo/EnterScene (โหลด props/NPCs/monsters จาก res.json, teleport
|
|
||||||
resolution, fallback จุดเกิด), SceneEntityMove (บันทึกตำแหน่ง), InteractProp,
|
|
||||||
GetNpcTakenReward, GetSceneMapInfo, GetEnteredScene, GetUnlockTeleport
|
|
||||||
- ทีม: GetAllLineupData/GetCurLineupData/JoinLineup/ReplaceLineup/SwapLineup/
|
|
||||||
SwitchLineupIndex/ChangeLineupLeader + SceneGroupRefresh/SyncLineup notify
|
|
||||||
- ตัวละคร/ไอเทม: GetAvatarData (+skin), สวม/ถอด lightcone/relic, RankUpAvatar, GetBag
|
|
||||||
- ภารกิจ: GetMissionStatus (curversion finished), GetMissionData, GetQuestData,
|
|
||||||
GetMainMissionCustomValue, UpdateTrackMainMission (จาก mission_unlocks.json)
|
|
||||||
- บทเรียน: ปดล็อคครบ 1249 tutorials + 855 guides (กันกล้อง/อินพุตค้าง)
|
|
||||||
- ต่อสู้: SceneCastSkill (**แก้บัคกล้องหลุดแล้ว**), StartCocoonStage,
|
|
||||||
StartQuickCocoonStage, SceneEnterStage, PVEBattleResult, GetCurBattleInfo,
|
|
||||||
SceneCastSkillCostMp, บัฟ global (Castorice/SW/Aha 151104)
|
|
||||||
- Challenge: GetChallenge, GetCurChallenge, GetChallengePeakData, GetCurChallengePeak,
|
|
||||||
GetActivityScheduleConfig, StartChallengePeak (cmd 8948 → arena 3000101),
|
|
||||||
StartChallenge → 1787 layout 4.6.51, Tierce (8973) + ทุก raw reply ที่ตอบค้าง
|
|
||||||
(1708/1716/1781/1704/8997/8999/8923/8920/... ครบตาม packet.rs เดิม)
|
|
||||||
- Dummy ปลอดภัย: ตอบว่างเฉพาะรายการที่ปลอดภัย ที่เหลือ drop เงียบ
|
|
||||||
(บทเรียนจาก Rust: ตอบว่างมั่วทำให้ client NRE จอดำ)
|
|
||||||
|
|
||||||
## ต่างจาก Rust ที่ควรรู้
|
```powershell
|
||||||
|
.\gameserver-go.exe
|
||||||
|
```
|
||||||
|
|
||||||
- นาฬิกา KCP เป็นมิลลิวินาที (Rust ใช้วินาที — ทำให้ RTT/เวลา retransmit เพี้ยน)
|
ซอร์สมีไฟล์ protobuf ที่ generate แล้ว การ build ปกติไม่ต้องใช้ `protoc` หากแก้ schema จึงต้องติดตั้ง `protoc` และ `protoc-gen-go` เพื่อ generate ใหม่
|
||||||
- มี ticker ขับ KCP ทุก 50ms ต่อ session (retransmission ทำงานจริง)
|
|
||||||
- ชื่อ message ใช้ตาม proto 4.6.51 จริง (แก้ปัญหาชื่อเก่าที่หายไป)
|
สำหรับนักพัฒนาที่ต้องการรันชุดทดสอบ:
|
||||||
- StartChallenge ยังไม่รองรับ challenge_id ที่อยู่นอก challenge_data.json
|
|
||||||
(fallback arena 3014101 ทำงานเหมือนเดิม)
|
```powershell
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
ผลทดสอบฝั่ง Go และการ build สำเร็จไม่ได้ยืนยันว่า Client โหลดฉากหรือเข้า battle สำเร็จ ต้องตรวจในเกมและอ่าน `Player.log` ประกอบด้วย
|
||||||
|
|
||||||
|
## ไฟล์ข้อมูล
|
||||||
|
|
||||||
|
| ไฟล์หรือโฟลเดอร์ | ใช้สำหรับ |
|
||||||
|
| --- | --- |
|
||||||
|
| `freesr-data.json` | ตัวละคร, lightcone, relic และ `battle_config` |
|
||||||
|
| `persistent` | ทีม, ตำแหน่ง, ฉาก และค่าผู้เล่นที่บันทึกไว้ |
|
||||||
|
| `res.json` | ข้อมูลฉากหลักในรูปแบบที่ Go loader ใช้ |
|
||||||
|
| `resources/` | ข้อมูลฉากและตารางประกอบ เช่น MapEntrance, MazePlane, Anchor และ StageConfig |
|
||||||
|
| `teleports.json` | ตำแหน่ง teleport แยกตาม floor |
|
||||||
|
| `challenge_data.json` | ข้อมูล challenge |
|
||||||
|
| `mission_unlocks.json` | ข้อมูลภารกิจและการปลดล็อก |
|
||||||
|
| `hotfix.json`, `versions.json` | ข้อมูลเวอร์ชันและทรัพยากร Client สำหรับ SDK server |
|
||||||
|
| `scripts/` | Lua ที่ส่งให้ Client ผ่าน heartbeat |
|
||||||
|
|
||||||
|
สำรอง `freesr-data.json` และ `persistent` ก่อนเปลี่ยนชุดข้อมูล `res.json` ใน root กับ `resources/res.json` มีโครงสร้างต่างกันและ loader ใช้ร่วมกัน จึงควรเก็บไว้ตามตำแหน่งเดิม
|
||||||
|
|
||||||
|
**Calyx ใช้ `battle_config` ใน `freesr-data.json` เป็นข้อมูลจำลองการต่อสู้** จึงอาจได้ StageID และมอนสเตอร์ต่างจากดอกไม้ที่กดในเกม
|
||||||
|
|
||||||
|
## REST API / GaC-Srtool
|
||||||
|
|
||||||
|
เมื่อ SDK server ทำงาน เปิด URL ที่ตั้งใน `public_sdk_url` เพื่อดูรายการ endpoint หน้า root และ `/dashboard` ตอบข้อมูล JSON ของ API gateway
|
||||||
|
|
||||||
|
| Method | Endpoint | หน้าที่ |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| GET | `/api/status` | อ่านสถานะและข้อมูลผู้เล่น |
|
||||||
|
| GET, POST | `/api/freesr` | อ่านหรืออัปเดต FreeSR data |
|
||||||
|
| GET, POST | `/api/freesr/avatars` | อ่านหรืออัปเดตข้อมูลตัวละคร |
|
||||||
|
| GET, POST | `/api/persistent` | อ่านหรืออัปเดตข้อมูล persistent |
|
||||||
|
| POST | `/api/exrank/toggle` | สลับการปลดล็อก EX rank |
|
||||||
|
| POST | `/api/avatar/rank` | เปลี่ยน rank ตัวละคร |
|
||||||
|
| POST | `/api/avatar/max_all` | ปรับข้อมูลตัวละครตามการทำงานของ handler |
|
||||||
|
| POST | `/api/teleport` | อัปเดตข้อมูลจุดหมาย teleport |
|
||||||
|
|
||||||
|
รูปแบบ request และเงื่อนไขการอัปเดตอยู่ใน `internal/sdkserver/api.go` การอัปเดตข้อมูลผ่าน API ยังต้องตรวจผลการโหลดข้อมูลของ Client ด้วย
|
||||||
|
|
||||||
|
## สถานะและปัญหาที่กำลังตรวจ
|
||||||
|
|
||||||
|
มี handler สำหรับ login, ทีม, ตัวละคร, ไอเทม, ฉาก, teleport, ภารกิจ, Calyx และ challenge แต่ความครบของ handler ไม่ได้หมายถึงรองรับทุกกรณีใน Client
|
||||||
|
|
||||||
|
| รายการ | สถานะ |
|
||||||
|
| --- | --- |
|
||||||
|
| Error 2000 ตอน login | แก้ `GetCurBattleInfo` ให้ตอบสำเร็จพร้อมข้อมูลว่างเมื่อไม่มี battle; รอยืนยันในเกม |
|
||||||
|
| Calyx เข้า fight ไม่ได้ | ยังอยู่ระหว่างตรวจ `NullReferenceException` ใน `CreateBattleServerCheckResult` |
|
||||||
|
| ตีมอนแล้วค้างหน้า Weakness | พบ stack trace ในฟังก์ชันตรวจ battle เดียวกับ Calyx; ยังไม่ยืนยันว่าแก้แล้ว |
|
||||||
|
| วาร์ปแมพใหม่ | ปรับลำดับ response/notify และข้อมูล scene ตาม upstream; ยังต้องยืนยันปลายทางในเกม |
|
||||||
|
| `SceneCastSkill` | ใช้รายการ monster entity ที่ server สร้างเพื่อตรวจเป้าหมายก่อนส่ง battle |
|
||||||
|
|
||||||
|
ข้อมูลล่าสุดที่ตรวจในซอร์สและเอกสาร: **16 ตุลาคม 2026**
|
||||||
|
|
||||||
|
## เก็บข้อมูลเมื่อพบปัญหา
|
||||||
|
|
||||||
|
| ไฟล์ | ข้อมูลที่ช่วยวิเคราะห์ |
|
||||||
|
| --- | --- |
|
||||||
|
| `gameserver.log` | คำขอและแพ็กเก็ตตอบกลับ |
|
||||||
|
| `server_debug.log` | รายละเอียด battle, scene และ teleport |
|
||||||
|
| `battle_last.json` | Payload battle ล่าสุดที่สร้างสำหรับ Calyx หรือการตีมอน |
|
||||||
|
| `battle_last_response.bin`, `battle_last.bin` | ไบต์ response ที่ส่งจริง และ nested battle_info ที่ตัดจาก response นั้น |
|
||||||
|
| `battle_last_capture.json`, `battle_diagnostics/` | CmdID, เวลา, SHA-256 และ snapshot ของแต่ละรอบ |
|
||||||
|
| `Player.log` ของ Client | Exception และจุดที่ Client โหลดข้อมูลไม่สำเร็จ |
|
||||||
|
|
||||||
|
ตำแหน่ง `Player.log` บน Windows สำหรับ Client ที่ใช้ในงานนี้:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%USERPROFILE%\AppData\LocalLow\Cognosphere\Star Rail\Player.log
|
||||||
|
```
|
||||||
|
|
||||||
|
เมื่อรายงานปัญหาให้ระบุเวอร์ชัน Client, เวลาที่เกิด, การกระทำก่อนค้าง และรหัส Cocoon/Entry/Teleport ถ้ามี พร้อม log ช่วงเดียวกันทั้ง server และ Client ข้อความ `sent packet` ยืนยันเพียงว่า server ส่งแพ็กเก็ตแล้ว ส่วนการโหลดสำเร็จต้องดูฝั่ง Client
|
||||||
|
|
||||||
|
สำหรับการตรวจ battle ทีละส่วน ดู [บันทึกเทียบ upstream](docs/battle-investigation.md) ค่า `battle_diagnostics.client_battle_type` ใน local config เปิด probe ที่อ่าน payload ด้วย protobuf parser ของ Client และเขียน `[GAC_BATTLE_WIRE]` ลง Player.log ชื่อ class ต้องอ้างอิง stack trace ของ Client รุ่นนั้น ตั้งค่าว่างเพื่อปิด probe
|
||||||
|
|
||||||
|
ผลตรวจล่าสุดพบ relic `63095` ของ Dan Heng IL ใช้ main affix `11` ที่ไม่มีในตารางของลูกแก้ว จึงแก้ inventory เป็น `10` (Imaginary DMG) ใน `freesr-data.json` แล้ว ต้องเปิดเกมใหม่เพื่อโหลดข้อมูลและยืนยันการเข้า battle ผล build ไม่ใช่การยืนยันว่าอาการค้างหายแล้ว
|
||||||
|
|
||||||
|
## โครงสร้างซอร์ส
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/gameserver/ จุดเริ่ม game server
|
||||||
|
cmd/sdkserver/ จุดเริ่ม SDK/dispatch server
|
||||||
|
internal/common/ โมเดลข้อมูล, resource loader และ protobuf converters
|
||||||
|
internal/gameserver/ Gateway, session, dispatch และ game handlers
|
||||||
|
internal/sdkserver/ HTTP dispatch, login และ REST API
|
||||||
|
protocol/proto/ Schema, generated protobuf และตาราง CmdID
|
||||||
|
pkg/kcp/ KCP transport
|
||||||
|
resources/ ข้อมูลประกอบ
|
||||||
|
scripts/ Client Lua
|
||||||
|
```
|
||||||
|
|
||||||
|
## อัปเดต protocol และ handler
|
||||||
|
|
||||||
|
ใช้ checkout ของ PearlSR ที่ตรวจ revision แล้วเป็นแหล่งข้อมูล ไม่แก้ generated Go ด้วยมือ:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tools/sync_protocol_commands.py "<path-to-pearl-sr-checkout>"
|
||||||
|
protoc --go_out=. --go_opt=paths=source_relative protocol/proto/StarRail.proto protocol/proto/current_handlers.proto
|
||||||
|
python tools/audit_protocol_handlers.py "<path-to-pearl-sr-checkout>"
|
||||||
|
```
|
||||||
|
|
||||||
|
คำสั่ง sync อัปเดตตารางชื่อ/CmdID ส่วนการแก้ field ต้องตรวจจาก schema ต้นฉบับแล้วแก้ `.proto` ก่อน generate ใหม่ ตัว audit ตรวจ CmdID, คู่ request/response และชุดหมายเลข field ของ message ที่มีชื่อร่วมกัน ไม่ตรวจชนิด field, default values, asset หรือผลใน Client
|
||||||
|
|
||||||
|
`ClientDownloadDataLuaVersion = 51` เป็นค่า version ของ payload ที่ upstream ปัจจุบันยังใช้ส่ง Lua จึงไม่เปลี่ยนเป็น `53` ตาม `client_version` โดยไม่มีหลักฐานรองรับ ส่วน Tutorial อ่าน ID จาก `resources/TutorialData.json` และ `resources/TutorialGuideGroup.json` หลังเปลี่ยน resource ให้รีสตาร์ต server
|
||||||
|
|
||||||
|
## ที่มา
|
||||||
|
|
||||||
|
พอร์ตจาก `GaC-hkrpg-ps` ภาษา Rust โดยใช้แนวทางและข้อมูลประกอบจาก [HonkaiSlopRail/pearl-sr](https://git.xeondev.com/HonkaiSlopRail/pearl-sr) ภาษา Zig การแก้ใน Go ต้องตรวจเทียบทั้ง handler, payload และเวอร์ชัน Client ที่ใช้งานจริง
|
||||||
+14
-6
@@ -2,23 +2,31 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gac-hkrpg-go/internal/common"
|
||||||
"gac-hkrpg-go/internal/gameserver"
|
"gac-hkrpg-go/internal/gameserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
cfg, err := common.LoadRuntimeConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
fmt.Println("============================================================")
|
fmt.Println("============================================================")
|
||||||
fmt.Println(" [GaC hkrpg GO] Gameserver (4.6.51) - ONLINE")
|
fmt.Printf(" [GaC hkrpg GO] Gameserver (Client %s)\n", cfg.ClientVersion)
|
||||||
fmt.Println(" Listening on UDP port 23301 (KCP Gateway)")
|
fmt.Printf(" Listening on UDP %s:%d (KCP Gateway)\n", cfg.GameHost, cfg.GamePort)
|
||||||
fmt.Println(" Ready for StarRail client connection...")
|
fmt.Println(" Ready for StarRail client connection...")
|
||||||
fmt.Println("============================================================")
|
fmt.Println("============================================================")
|
||||||
|
|
||||||
gw, err := gameserver.NewGateway("0.0.0.0", 23301)
|
gw, err := gameserver.NewGateway(cfg.GameHost, cfg.GamePort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("failed to bind gateway:", err)
|
fmt.Fprintln(os.Stderr, "failed to bind gateway:", err)
|
||||||
return
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := gw.Listen(); err != nil {
|
if err := gw.Listen(); err != nil {
|
||||||
fmt.Println("gateway error:", err)
|
fmt.Fprintln(os.Stderr, "gateway error:", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"gac-hkrpg-go/internal/sdkserver"
|
"gac-hkrpg-go/internal/sdkserver"
|
||||||
)
|
)
|
||||||
@@ -9,10 +10,10 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
fmt.Println("============================================================")
|
fmt.Println("============================================================")
|
||||||
fmt.Println(" [GaC hkrpg GO] SDK/Dispatch server - ONLINE")
|
fmt.Println(" [GaC hkrpg GO] SDK/Dispatch server - ONLINE")
|
||||||
fmt.Println(" Listening on HTTP port 21000")
|
|
||||||
fmt.Println("============================================================")
|
fmt.Println("============================================================")
|
||||||
|
|
||||||
if err := sdkserver.Listen(); err != nil {
|
if err := sdkserver.Listen(); err != nil {
|
||||||
fmt.Println("sdkserver error:", err)
|
fmt.Fprintln(os.Stderr, "sdkserver error:", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Battle investigation — 2026-10-16
|
||||||
|
|
||||||
|
## หลักฐานที่ใช้อ้างอิง
|
||||||
|
|
||||||
|
- Upstream: [HonkaiSlopRail/pearl-sr](https://git.xeondev.com/HonkaiSlopRail/pearl-sr)
|
||||||
|
- ตรวจ remote ใหม่แล้วที่ commit `005d2c12db4781071f4b5b68e8f364587886a710`
|
||||||
|
- ไฟล์หลัก: `gameserver/src/services/battle.zig`, `gameserver/src/manager/battle_mgr.zig`, `gameserver/src/data/game_config.zig`, `protocol/src/protocol.pb.zig`
|
||||||
|
- Client ล่าสุดยัง NRE ที่ `CreateBattleServerCheckResult` ผ่านตัวแปลง BattleAvatar/รายการ relic และ `NMBMHCNGCFG.BDNFEMGIEIH`
|
||||||
|
- Payload ที่บันทึกเวลา 21:34 มีตัวละคร 4 ตัว แต่ละตัวมี relic 6 ชิ้นและ sub-affix 24 ค่า แม้เลิกส่ง inventory UniqueId ใน BattleRelic แล้ว Client ก็ยังล้ม
|
||||||
|
|
||||||
|
## เทียบทีละส่วน
|
||||||
|
|
||||||
|
| ส่วน | สิ่งที่ตรวจพบ | ข้อสรุปที่ใช้ได้ |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Quick Calyx response | Field numbers: retcode=1, battle_info=5, wave=8, cocoon_id=11 ตรง upstream | ยังไม่พบ field-number mismatch ใน wrapper |
|
||||||
|
| SceneBattleInfo | ตรวจชุด field numbers ของ 20 fields ตรงกัน | ชื่อ `JAOIOKBBMFC` ของ Go คือ field 7 ซึ่ง upstream เรียก monster_wave_length |
|
||||||
|
| BattleAvatar | ตรวจชุด field numbers ของ 18 fields ตรงกัน | ยังต้องดูค่าที่ parser ของ Client ถอดได้จริง |
|
||||||
|
| Relic/lightcone | BattleRelic, RelicAffix และ BattleEquipment มีชุด field numbers ตรงกัน | จำนวน sub-affix ฝั่ง Go อย่างเดียวไม่ยืนยันว่า Client ถอดได้ครบ |
|
||||||
|
| Monster/wave | SceneMonster, SceneMonsterWave, SceneMonsterWaveParam มีชุด field numbers ตรงกัน | ยังไม่มีหลักฐานให้สลับ WaveParam เป็น nil อีก |
|
||||||
|
| Buff | Go ใช้ชื่อ OwnerId แต่ upstream ใช้ owner_index บน field 3 เหมือนกัน | ชื่อ Go ไม่ใช่หลักฐานว่า field บน wire ผิด |
|
||||||
|
| แหล่งข้อมูล | Go ใช้ freesr-data.json/persistent; PearlSR ใช้ config.json/selected avatars | Payload ทั้งสองยังไม่เท่ากันในระดับค่าข้อมูล |
|
||||||
|
| EX rank | Upstream append รายการ ExRankAvatarID ให้ทุก battle avatar; Go ส่งรายการตามตัวละครที่ปลดล็อก | เป็นข้อแตกต่างที่บันทึกไว้ ยังไม่ชี้ว่าเป็นสาเหตุ NRE |
|
||||||
|
| Global/technique buffs | เจ้าของบัฟ, target list และ dynamic values บางตัวต่างกัน | ต้องตรวจแยกหลังระบุตำแหน่งที่ parser/ตัวตรวจล้ม |
|
||||||
|
|
||||||
|
การตรวจ field numbers ข้างต้นไม่ได้ยืนยัน default values, Client schema ของรุ่นที่รันจริง หรือผลการคำนวณค่าสถานะใน Client
|
||||||
|
|
||||||
|
## ขั้นแรกที่เพิ่มในรอบนี้: เก็บ wire และอ่านกลับบน Client
|
||||||
|
|
||||||
|
บันทึกหลังส่ง response สำเร็จจาก Session.Send แทนการบันทึกระหว่างสร้าง battle:
|
||||||
|
|
||||||
|
- `battle_last_response.bin`: protobuf body ของ response ที่ส่งจริง
|
||||||
|
- `battle_last.bin`: bytes ของ nested battle_info ที่ตัดออกจาก response เดิมโดยตรง ไม่ marshal ใหม่
|
||||||
|
- `battle_last.json`: ข้อมูลสำหรับอ่านด้วยตา
|
||||||
|
- `battle_last_capture.json`: CmdID, ชื่อ response, StageID, เวลา UTC และ SHA-256
|
||||||
|
- `battle_diagnostics/`: snapshot ของแต่ละรอบ เก็บทั้งข้อมูลเดิมและรอบต่อมา
|
||||||
|
|
||||||
|
เปิด read-only Client probe ด้วย `battle_diagnostics.client_battle_type` ใน local config ชื่อ class ต้องเอาจาก stack trace ของ Client รุ่นที่กำลังใช้งาน ไม่ฝังชื่อ class ที่เปลี่ยนตามเวอร์ชันใน Go/Lua
|
||||||
|
|
||||||
|
Probe ถูกส่งครั้งเดียวใน heartbeat หลัง response battle โดยใช้ protobuf parser ของ Client อ่าน bytes เดียวกับ capture จากนั้นเขียน `[GAC_BATTLE_WIRE]` ลง Player.log พร้อม decoded JSON และ descriptor field numbers ไม่แก้ตัวละคร relic บัฟ หรือ callback การต่อสู้
|
||||||
|
|
||||||
|
หาก parser/Reflection ไม่ถูกเปิดให้ Lua ใช้ จะมี `probe_error` ใน Player.log แทน ห้ามตีความว่า capture ฝั่ง Go ผ่านแล้วแปลว่า Client decode ผ่าน
|
||||||
|
|
||||||
|
## ข้อสรุปที่ถอนออก
|
||||||
|
|
||||||
|
Stack trace เดิมเพียงอย่างเดียวไม่ได้พิสูจน์ว่า WaveParam=nil หรือ SceneMonster.ExtraInfo=nil เป็นสาเหตุ การใส่ ExtraInfo หรือการลบ UniqueId ยังไม่ได้ทำให้การเข้า battle สำเร็จ จึงไม่ถือเป็นการแก้ที่ยืนยันผลแล้ว
|
||||||
|
|
||||||
|
## ผลตรวจรอบ 21:47: main affix ที่ไม่มีในตาราง
|
||||||
|
|
||||||
|
Calyx และ overworld ตายที่ stack เดียวกัน ส่วน probe รุ่นแรกทำงานแล้วแต่รายงาน `protobuf parser property not found` จึงไม่มี Client readback และยังใช้ผลของ probe ยืนยันการ decode ไม่ได้
|
||||||
|
|
||||||
|
แก้การอ่านตำแหน่ง exception: `0xc4eb77c` เป็น return address หลังคำสั่ง `call` ที่ `0xc4eb777` เช่นเดียวกับ frame `0x166b23d2` ที่อยู่หลัง `call 0xc4eb0c0` ดังนั้นจุดนี้มาจาก branch หลัง lookup **main affix** ไม่พบ (`0xc4eb2b0`–`0xc4eb2b3`) ไม่ใช่ branch ตรวจ sub-affix array ที่กระโดดไป `0xc4eb77c` แล้วมี return address `0xc4eb781` การตีความ sub-affix array เป็น null ในรอบก่อนจึงถูกถอนออก
|
||||||
|
|
||||||
|
ตรวจ payload จริงและ inventory พบรายการนี้:
|
||||||
|
|
||||||
|
| Inventory UID | Avatar | Relic | main_affix_id เดิม | ผลตรวจ |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 483 | 1213 (Dan Heng IL) | 63095 | 11 | ไม่มีใน main affix group 55 ของลูกแก้ว |
|
||||||
|
|
||||||
|
เทียบกับ [relics.json](https://github.com/Mar-7th/StarRailRes/blob/dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487/index_new/en/relics.json) และ [relic_main_affixes.json](https://github.com/Mar-7th/StarRailRes/blob/dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487/index_new/en/relic_main_affixes.json): group 55 มี ID 1–10 โดย ID 10 คือ `ImaginaryAddedRatio` และ `resources/AvatarConfig.json` ระบุ Avatar 1213 เป็น Imaginary
|
||||||
|
|
||||||
|
แก้ **ข้อมูล inventory เท่านั้น** จาก 11 เป็น 10 ใน `freesr-data.json` ไม่มีเงื่อนไขพิเศษสำหรับ Avatar/Relic นี้ใน converter สำรองข้อมูลและ log ก่อนแก้ไว้ใน `battle_diagnostics/main-affix-repair/` แล้ว และปิด probe ใน local config ขณะลองผลการแก้ข้อมูล
|
||||||
|
|
||||||
|
ข้อจำกัดของชุดข้อมูลอ้างอิง: ไม่พบ relic ใหม่ 63295/63296 ของ Avatar 1511 จึงไม่ได้รับรอง inventory ทุกชิ้น แต่ชิ้นที่อยู่ในทีมของ payload รอบนี้มีครบและพบ main affix ผิดหนึ่งชิ้นข้างต้น
|
||||||
|
|
||||||
|
## ขั้นต่อไปหลังแก้ main affix
|
||||||
|
|
||||||
|
1. เปิดเกมใหม่ให้ Client โหลด inventory ที่แก้แล้ว และลอง Calyx/overworld ด้วยทีมเดิม
|
||||||
|
2. หากยังล้ม ให้เก็บ stack ใหม่และเทียบ capture ว่าส่ง main affix 10 แล้ว ก่อนเปลี่ยนส่วนอื่น
|
||||||
|
|
||||||
|
## แนวทางตรวจเพิ่มเติมหากยังล้ม
|
||||||
|
|
||||||
|
1. เทียบ SHA-256 ให้ตรงกับรอบที่ Client ล้ม
|
||||||
|
2. เทียบ schema/ข้อมูล BattleAvatar และ nested relic ที่ Client ถอดได้กับ capture ฝั่ง Go
|
||||||
|
3. หากข้อมูลตรงกัน ให้แยกตัวละครและอุปกรณ์ที่ทำให้ตัวตรวจค่าสถานะล้มโดยมี control payload ก่อนแก้ resource หรือสูตรบัฟ
|
||||||
|
4. เปลี่ยนเฉพาะส่วนที่มีหลักฐาน แล้วตรวจ Calyx และ overworld battle แยกกัน
|
||||||
|
|
||||||
|
สถานะ: ยังไม่มีการยืนยันว่าเข้า fight ได้
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Protocol และ handler — 2026-10-16
|
||||||
|
|
||||||
|
เป้าหมาย Client: 4.6.53 OS ตาม config ที่ใช้อยู่
|
||||||
|
|
||||||
|
แหล่งอ้างอิง: [PearlSR commit 005d2c12db4781071f4b5b68e8f364587886a710](https://git.xeondev.com/HonkaiSlopRail/pearl-sr/commit/005d2c12db4781071f4b5b68e8f364587886a710) โดยตรวจ `protocol/src/root.zig`, `protocol/src/protocol.pb.zig` และ `gameserver/src/services/{challenge,misc,avatar}.zig` หลัง fetch remote ใหม่
|
||||||
|
|
||||||
|
## สิ่งที่เปลี่ยน
|
||||||
|
|
||||||
|
| ส่วน | การแก้ |
|
||||||
|
| --- | --- |
|
||||||
|
| ตารางคำสั่ง | Generate ชื่อ/CmdID 2,690 รายการจาก upstream เก็บ revision และตรวจ conflict กับ Go schema เมื่อเริ่มโปรแกรม |
|
||||||
|
| `handle` / raw dispatch | เลิกระบุเวอร์ชันเก่าในข้อความ log และให้ raw handler resolve request ตามชื่อจากตารางปัจจุบัน |
|
||||||
|
| alias เก่า | นำเลข request หลายเวอร์ชันและ fixed-body replies ที่ไม่มีชื่อปัจจุบันรองรับออก บางเลขเคยชี้ไปยัง response/notification เช่น 8909, 8924, 8935 |
|
||||||
|
| Tierce lineup | ใช้ request 8992 → response 8979; challenge_id field 4 และ stage_info_list field 5 ตาม upstream เพิ่ม generated message ที่ Go เดิมขาด |
|
||||||
|
| Tierce data | ตอบ GetChallengeTierceDataScRsp ครั้งเดียว เลิกส่งสำเนาไป cmd 8994 และเลิกรับ response 8979/8981 เป็น request |
|
||||||
|
| Challenge reward | รับ request 1747 → response 1748; อ่าน group_id field 10 และส่ง group_id field 12 / retcode field 9 ด้วย generated type แทน request 1739 → cmd 1704 ที่เป็นคนละระบบ |
|
||||||
|
| Challenge statistics | อ่าน request ด้วย generated type และตอบ group เดิมตาม upstream; ไม่ส่งคะแนนประวัติที่แต่งขึ้น ปรับ statistics สามชนิดเป็น oneof ตาม schema |
|
||||||
|
| ออกจาก Challenge | ส่ง response ตาม mode ที่ร้องขอเพียงชนิดเดียว ล้าง battle แล้วใช้ฉาก overworld ที่เก็บไว้ของผู้เล่น เลิกบังคับไปฉาก/พิกัดคงที่ |
|
||||||
|
| Peak | ลงทะเบียน StartChallengePeak ที่มี implementation อยู่แล้ว และ LeaveChallengePeak ด้วยชื่อปัจจุบัน; GetCurChallengePeak ตอบสถานะว่างแบบ upstream แทน PeakID 1 ที่แต่งขึ้น |
|
||||||
|
| Endgame / SwitchHand | ส่ง generated response โดย resolve CmdID ตามชื่อ แทน raw bytes และเลขที่ฝังใน handler |
|
||||||
|
| Avatar cmd 400 | อ่าน field จาก generated HIGLJHLNODG แทนการสแกนเลขทุก field และเลิกยิง response 5 หมายเลข; ใช้ LPOLGDLAADM ซึ่งอยู่คู่ติดกันใน schema ชื่อและความสัมพันธ์เชิงความหมายยังเป็น obfuscated จึงยังต้องยืนยัน callback ใน Client |
|
||||||
|
| ReplaceLineup | เพิ่ม field 14 `OMDLCGEMBLA` ที่ Go schema ขาดตาม upstream โดยไม่เดาค่าให้ field นี้ |
|
||||||
|
| Tutorial | เลิกใช้รายการ ID จาก Rust 4.6.51; อ่าน TutorialID/GroupID จาก resource JSON ที่ติดตั้งแทน และรายงาน error หากไฟล์หาย/ไม่มี ID |
|
||||||
|
| Activity | นำ generated list เก่าที่ไม่ได้ใช้แล้วออก ไม่เปิด activity panel เพิ่มก่อนมี implementation รองรับข้อมูลของมัน |
|
||||||
|
| Lua heartbeat | คงค่า version ของ ClientDownloadData ไว้ที่ 51 เพราะ `services/misc.zig` ของ upstream revision นี้ยังส่งค่านี้ ไม่อนุมานจากเลข client patch |
|
||||||
|
|
||||||
|
Go message บางชื่อเป็น alias เก่าของชื่อที่ upstream ตั้งใหม่ เช่น StartQuickCocoonStage/QuickStartCocoonStage และ TakeChallengeReward/TakeChallengeCumulativeReward คงชนิด Go เดิมในจุดที่ wire layout ตรงกัน แล้วผูกหมายเลขคำสั่งกับตารางปัจจุบัน
|
||||||
|
|
||||||
|
## ผล source audit
|
||||||
|
|
||||||
|
- CmdID 2,690 รายการตรงกับ upstream
|
||||||
|
- คู่ request/response เดิม 865 คู่ตรงกับ upstream
|
||||||
|
- ชุดหมายเลข field ของ message ที่มีชื่อร่วมกัน 4,435 ชนิดตรงกันหลังนับ oneof ทั้งสองฝั่ง
|
||||||
|
- การตรวจนี้ไม่ครอบคลุมความหมายของชื่อ obfuscated, ชนิด/default ของทุก field, resource รุ่นที่ตรง Client หรือผลการเล่นจริง
|
||||||
|
|
||||||
|
เพิ่ม `tools/sync_protocol_commands.py` สำหรับ generate ตารางคำสั่ง และ `tools/audit_protocol_handlers.py` สำหรับตรวจซอร์สซ้ำ โดยระบุ checkout ของ upstream เป็น argument ไม่ฝัง path เครื่อง
|
||||||
|
|
||||||
|
## ขอบเขตที่ยังเหลือ
|
||||||
|
|
||||||
|
การเปลี่ยนนี้อัปเดต dispatch/schema/handlers ข้างต้น ไม่ได้อ้างว่า implementation ของทุกระบบเสร็จแล้ว ข้อมูลปลดล็อก, สูตร fallback ด่าน, บัฟ และสถิติบางส่วนของโปรเจกต์ยังต้องตรวจแยกตาม resource จริง การ build หรือ source audit ผ่านไม่ยืนยันว่า Calyx, teleport หรือ Endgame ทำงานสำเร็จใน Client
|
||||||
+1
-1
@@ -14531,7 +14531,7 @@
|
|||||||
{
|
{
|
||||||
"level": 15,
|
"level": 15,
|
||||||
"relic_id": 63095,
|
"relic_id": 63095,
|
||||||
"main_affix_id": 11,
|
"main_affix_id": 10,
|
||||||
"sub_affixes": [
|
"sub_affixes": [
|
||||||
{
|
{
|
||||||
"sub_affix_id": 7,
|
"sub_affix_id": 7,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func (m *MultiPathAvatar) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MultiPathAvatarType in the 4.6.51 proto uses the avatar id itself as the
|
// MultiPathAvatarType in the loaded schema uses the avatar id itself as the
|
||||||
// enum value (Mar7thKnightType=1001, BoyWarriorType=8001, ...), so the proto
|
// enum value (Mar7thKnightType=1001, BoyWarriorType=8001, ...), so the proto
|
||||||
// value is simply the numeric id (0 for Unk).
|
// value is simply the numeric id (0 for Unk).
|
||||||
func (m MultiPathAvatar) ProtoType() uint32 { return uint32(m) }
|
func (m MultiPathAvatar) ProtoType() uint32 { return uint32(m) }
|
||||||
|
|||||||
@@ -44,11 +44,12 @@ func relicAffixes(subs []SubAffix) []*goproto.RelicAffix {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relic) ToBattleRelic() *goproto.BattleRelic {
|
func (r *Relic) ToBattleRelic() *goproto.BattleRelic {
|
||||||
|
// Match PearlSR's standalone battle relic format. Inventory UniqueID is
|
||||||
|
// only sent by ToProtoRelic/ToEquipRelic; battle carries the full affixes.
|
||||||
return &goproto.BattleRelic{
|
return &goproto.BattleRelic{
|
||||||
Id: r.RelicID,
|
Id: r.RelicID,
|
||||||
Level: r.Level,
|
Level: r.Level,
|
||||||
MainAffixId: r.MainAffixID,
|
MainAffixId: r.MainAffixID,
|
||||||
UniqueId: r.UniqueID(),
|
|
||||||
SubAffixList: relicAffixes(r.SubAffixes),
|
SubAffixList: relicAffixes(r.SubAffixes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,19 +317,26 @@ func (m *Monster) ToSceneMonster() *goproto.SceneMonster {
|
|||||||
return &goproto.SceneMonster{
|
return &goproto.SceneMonster{
|
||||||
MonsterId: m.MonsterID,
|
MonsterId: m.MonsterID,
|
||||||
MaxHp: hp,
|
MaxHp: hp,
|
||||||
CurHp: hp,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func monsterWave(waveID uint32, monsters []Monster) *goproto.SceneMonsterWave {
|
func monsterWave(monsters []Monster) *goproto.SceneMonsterWave {
|
||||||
|
maxLevel := uint32(0)
|
||||||
|
for _, monster := range monsters {
|
||||||
|
if monster.Level > maxLevel {
|
||||||
|
maxLevel = monster.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxLevel == 0 {
|
||||||
|
maxLevel = 95
|
||||||
|
}
|
||||||
|
|
||||||
wave := &goproto.SceneMonsterWave{
|
wave := &goproto.SceneMonsterWave{
|
||||||
WaveId: waveID,
|
|
||||||
MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)),
|
MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)),
|
||||||
// NOTE: WaveParam MUST be nil! In client 4.6.53 GameAssembly.dll
|
// Include a level so the client can validate this wave directly instead
|
||||||
// AdventureModule.CreateBattleServerCheckResult (RVA 0x166b2ef6),
|
// of looking up monster data from the battle stage's static table. This
|
||||||
// if WaveParam is non-nil it tries to dereference [wave_param + 0x10]
|
// matters when a Calyx request reuses a custom battle stage ID.
|
||||||
// and throws NullReferenceException. When nil, it safely jumps over.
|
WaveParam: &goproto.SceneMonsterWaveParam{Level: maxLevel},
|
||||||
WaveParam: nil,
|
|
||||||
}
|
}
|
||||||
for i := range monsters {
|
for i := range monsters {
|
||||||
wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster())
|
wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster())
|
||||||
@@ -338,8 +346,8 @@ func monsterWave(waveID uint32, monsters []Monster) *goproto.SceneMonsterWave {
|
|||||||
|
|
||||||
func ToSceneMonsterWaves(waves [][]Monster) []*goproto.SceneMonsterWave {
|
func ToSceneMonsterWaves(waves [][]Monster) []*goproto.SceneMonsterWave {
|
||||||
out := make([]*goproto.SceneMonsterWave, 0, len(waves))
|
out := make([]*goproto.SceneMonsterWave, 0, len(waves))
|
||||||
for i, wave := range waves {
|
for _, wave := range waves {
|
||||||
out = append(out, monsterWave(uint32(i+1), wave))
|
out = append(out, monsterWave(wave))
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStandaloneBattleRelicWire(t *testing.T) {
|
||||||
|
relic := Relic{
|
||||||
|
RelicID: 61151, Level: 15, MainAffixID: 1, InternalUID: 42,
|
||||||
|
SubAffixes: []SubAffix{{SubAffixID: 7, Count: 3, Step: 3}, {SubAffixID: 8, Count: 3, Step: 0}},
|
||||||
|
}
|
||||||
|
encoded, err := proto.Marshal(relic.ToBattleRelic())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var decoded goproto.BattleRelic
|
||||||
|
if err := proto.Unmarshal(encoded, &decoded); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if decoded.UniqueId != 0 {
|
||||||
|
t.Fatal("standalone battle relic must not use inventory ID")
|
||||||
|
}
|
||||||
|
if decoded.Id != 61151 || decoded.MainAffixId != 1 || len(decoded.SubAffixList) != 2 {
|
||||||
|
t.Fatalf("relic or affixes lost on wire: %v", &decoded)
|
||||||
|
}
|
||||||
|
if decoded.SubAffixList[0].Cnt != 3 || decoded.SubAffixList[0].Step != 3 || decoded.SubAffixList[1].Step != 0 {
|
||||||
|
t.Fatalf("sub-affix values changed on wire: %v", decoded.SubAffixList)
|
||||||
|
}
|
||||||
|
if relic.ToProtoRelic().UniqueId != 42 || relic.ToEquipRelic().RelicUniqueId != 42 {
|
||||||
|
t.Fatal("inventory and equipped relic references must retain their IDs")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuntimeConfig is shared by both server processes. Deployment values belong
|
||||||
|
// in configuration rather than in handlers or executable entry points.
|
||||||
|
type RuntimeConfig struct {
|
||||||
|
BattleDiagnostics BattleDiagnosticsConfig `json:"battle_diagnostics"`
|
||||||
|
ClientVersion string `json:"client_version"`
|
||||||
|
GameHost string `json:"game_host"`
|
||||||
|
GamePort uint16 `json:"game_port"`
|
||||||
|
SDKHost string `json:"sdk_host"`
|
||||||
|
SDKPort uint16 `json:"sdk_port"`
|
||||||
|
PublicGameHost string `json:"public_game_host"`
|
||||||
|
PublicSDKURL string `json:"public_sdk_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The obfuscated protobuf class name is version-specific and comes from the
|
||||||
|
// client's stack trace. Empty disables the read-only client wire probe.
|
||||||
|
type BattleDiagnosticsConfig struct {
|
||||||
|
ClientBattleType string `json:"client_battle_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadRuntimeConfig() (RuntimeConfig, error) {
|
||||||
|
path := os.Getenv("GAC_CONFIG")
|
||||||
|
if path == "" {
|
||||||
|
path = "server.json"
|
||||||
|
}
|
||||||
|
var cfg RuntimeConfig
|
||||||
|
for i, filename := range []string{path, filepath.Join(filepath.Dir(path), strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))+".local.json")} {
|
||||||
|
raw, err := os.ReadFile(filename)
|
||||||
|
if i == 1 && os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return cfg, fmt.Errorf("read runtime config %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf}), &cfg); err != nil {
|
||||||
|
return cfg, fmt.Errorf("parse runtime config %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, dest := range map[string]*string{
|
||||||
|
"GAC_CLIENT_VERSION": &cfg.ClientVersion, "GAC_GAME_HOST": &cfg.GameHost,
|
||||||
|
"GAC_SDK_HOST": &cfg.SDKHost, "GAC_PUBLIC_GAME_HOST": &cfg.PublicGameHost,
|
||||||
|
"GAC_PUBLIC_SDK_URL": &cfg.PublicSDKURL,
|
||||||
|
} {
|
||||||
|
if value, ok := os.LookupEnv(name); ok {
|
||||||
|
*dest = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, dest := range map[string]*uint16{"GAC_GAME_PORT": &cfg.GamePort, "GAC_SDK_PORT": &cfg.SDKPort} {
|
||||||
|
if value, ok := os.LookupEnv(name); ok {
|
||||||
|
n, err := strconv.ParseUint(value, 10, 16)
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
return cfg, fmt.Errorf("%s must be a port between 1 and 65535", name)
|
||||||
|
}
|
||||||
|
*dest = uint16(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
for name, host := range map[string]string{"game_host": cfg.GameHost, "sdk_host": cfg.SDKHost, "public_game_host": cfg.PublicGameHost} {
|
||||||
|
if strings.TrimSpace(host) == "" || strings.ContainsAny(host, "/\\ \t\r\n") {
|
||||||
|
return cfg, fmt.Errorf("invalid %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u, err := url.Parse(cfg.PublicSDKURL)
|
||||||
|
if err != nil || u.Hostname() == "" || (u.Scheme != "http" && u.Scheme != "https") || u.RawQuery != "" || u.Fragment != "" || u.User != nil {
|
||||||
|
return cfg, fmt.Errorf("public_sdk_url must be an HTTP(S) base URL")
|
||||||
|
}
|
||||||
|
cfg.PublicSDKURL = strings.TrimRight(cfg.PublicSDKURL, "/")
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c RuntimeConfig) SDKAddress() string {
|
||||||
|
return net.JoinHostPort(c.SDKHost, strconv.Itoa(int(c.SDKPort)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Addresses and ports below are deliberately different fixture values, not
|
||||||
|
// deployment defaults. These tests must never bind sockets or start servers.
|
||||||
|
func TestRuntimeConfigOverrides(t *testing.T) {
|
||||||
|
t.Chdir(t.TempDir())
|
||||||
|
for _, name := range []string{"GAC_CONFIG", "GAC_CLIENT_VERSION", "GAC_GAME_HOST", "GAC_GAME_PORT", "GAC_SDK_HOST", "GAC_SDK_PORT", "GAC_PUBLIC_GAME_HOST", "GAC_PUBLIC_SDK_URL"} {
|
||||||
|
t.Setenv(name, "")
|
||||||
|
if err := os.Unsetenv(name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base := `{"client_version":"fixture","game_host":"localhost","game_port":32101,"sdk_host":"localhost","sdk_port":32102,"public_game_host":"example.invalid","public_sdk_url":"https://example.invalid/base/"}`
|
||||||
|
if err := os.WriteFile("server.json", []byte(base), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
local := append([]byte{0xef, 0xbb, 0xbf}, []byte(`{"game_port":32201,"launcher":{"client_path":"fixture"}}`)...)
|
||||||
|
if err := os.WriteFile("server.local.json", local, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err := LoadRuntimeConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cfg.GamePort != 32201 || cfg.SDKPort != 32102 || cfg.PublicSDKURL != "https://example.invalid/base" {
|
||||||
|
t.Fatalf("merge failed: %+v", cfg)
|
||||||
|
}
|
||||||
|
t.Setenv("GAC_GAME_PORT", "32301")
|
||||||
|
t.Setenv("GAC_PUBLIC_GAME_HOST", "gateway.example.invalid")
|
||||||
|
cfg, err = LoadRuntimeConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cfg.GamePort != 32301 || cfg.PublicGameHost != "gateway.example.invalid" {
|
||||||
|
t.Fatalf("environment override failed: %+v", cfg)
|
||||||
|
}
|
||||||
|
t.Setenv("GAC_GAME_PORT", "65536")
|
||||||
|
if _, err := LoadRuntimeConfig(); err == nil {
|
||||||
|
t.Fatal("out-of-range port accepted")
|
||||||
|
}
|
||||||
|
t.Setenv("GAC_GAME_PORT", "0")
|
||||||
|
if _, err := LoadRuntimeConfig(); err == nil {
|
||||||
|
t.Fatal("zero port accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeConfigRequiresFile(t *testing.T) {
|
||||||
|
t.Setenv("GAC_CONFIG", filepath.Join(t.TempDir(), "missing.json"))
|
||||||
|
if _, err := LoadRuntimeConfig(); err == nil {
|
||||||
|
t.Fatal("missing config silently accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package gameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gac-hkrpg-go/internal/common"
|
||||||
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed battle_wire_probe.lua
|
||||||
|
var battleWireProbe string
|
||||||
|
|
||||||
|
type battleCapture struct {
|
||||||
|
ResponseName string `json:"response_name"`
|
||||||
|
CmdID uint16 `json:"cmd_id"`
|
||||||
|
ResponseSHA256 string `json:"response_sha256"`
|
||||||
|
BattleSHA256 string `json:"battle_sha256"`
|
||||||
|
CapturedUTC time.Time `json:"captured_utc"`
|
||||||
|
StageID uint32 `json:"stage_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveBattleCapture(msg proto.Message, cmdID uint16, response []byte) ([]byte, *battleCapture, error) {
|
||||||
|
carrier, ok := msg.(interface {
|
||||||
|
GetBattleInfo() *goproto.SceneBattleInfo
|
||||||
|
})
|
||||||
|
if !ok || carrier.GetBattleInfo() == nil {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
battle := carrier.GetBattleInfo()
|
||||||
|
wire, err := extractBattleWire(msg, response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
pretty, err := (protojson.MarshalOptions{Indent: " "}).Marshal(battle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
meta := &battleCapture{
|
||||||
|
ResponseName: string(msg.ProtoReflect().Descriptor().Name()), CmdID: cmdID,
|
||||||
|
ResponseSHA256: fmt.Sprintf("%x", sha256.Sum256(response)),
|
||||||
|
BattleSHA256: fmt.Sprintf("%x", sha256.Sum256(wire)),
|
||||||
|
CapturedUTC: time.Now().UTC(), StageID: battle.StageId,
|
||||||
|
}
|
||||||
|
info, err := json.MarshalIndent(meta, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
files := map[string][]byte{"battle_last_response.bin": response, "battle_last.bin": wire, "battle_last.json": pretty, "battle_last_capture.json": info}
|
||||||
|
// Keep earlier failures intact so consecutive attempts can be compared.
|
||||||
|
archive := filepath.Join("battle_diagnostics", meta.CapturedUTC.Format("20060102T150405.000000000Z")+"_"+meta.ResponseSHA256[:12])
|
||||||
|
if err := os.MkdirAll(archive, 0700); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for name, data := range files {
|
||||||
|
if err := os.WriteFile(filepath.Join(archive, name), data, 0600); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(name, data, 0600); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wire, meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBattleWire(msg proto.Message, response []byte) ([]byte, error) {
|
||||||
|
field := msg.ProtoReflect().Descriptor().Fields().ByName("battle_info")
|
||||||
|
if field == nil {
|
||||||
|
return nil, fmt.Errorf("response has no battle_info field")
|
||||||
|
}
|
||||||
|
for len(response) > 0 {
|
||||||
|
number, kind, n := protowire.ConsumeTag(response)
|
||||||
|
if n < 0 {
|
||||||
|
return nil, protowire.ParseError(n)
|
||||||
|
}
|
||||||
|
response = response[n:]
|
||||||
|
if number == protowire.Number(field.Number()) && kind == protowire.BytesType {
|
||||||
|
wire, n := protowire.ConsumeBytes(response)
|
||||||
|
if n < 0 {
|
||||||
|
return nil, protowire.ParseError(n)
|
||||||
|
}
|
||||||
|
return append([]byte(nil), wire...), nil
|
||||||
|
}
|
||||||
|
n = protowire.ConsumeFieldValue(number, kind, response)
|
||||||
|
if n < 0 {
|
||||||
|
return nil, protowire.ParseError(n)
|
||||||
|
}
|
||||||
|
response = response[n:]
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("battle_info missing from serialized response")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBattleProbe(wire []byte, typeName, hash string) []byte {
|
||||||
|
// Values are quoted as Lua strings; base64 contains no executable syntax.
|
||||||
|
replacer := strings.NewReplacer("__TYPE_NAME__", strconv.Quote(typeName), "__PAYLOAD__", strconv.Quote(base64.StdEncoding.EncodeToString(wire)), "__CAPTURE_HASH__", strconv.Quote(hash))
|
||||||
|
return []byte(replacer.Replace(battleWireProbe))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) captureBattleResponse(msg proto.Message, cmdID uint16, body []byte) {
|
||||||
|
wire, meta, err := saveBattleCapture(msg, cmdID, body)
|
||||||
|
if err != nil {
|
||||||
|
Log(fmt.Sprintf("[BATTLE CAPTURE] %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if meta == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log(fmt.Sprintf("[BATTLE CAPTURE] cmd=%d stage=%d response_sha256=%s battle_sha256=%s", cmdID, meta.StageID, meta.ResponseSHA256, meta.BattleSHA256))
|
||||||
|
cfg, err := common.LoadRuntimeConfig()
|
||||||
|
if err != nil {
|
||||||
|
Log(fmt.Sprintf("[BATTLE PROBE] config error: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cfg.BattleDiagnostics.ClientBattleType == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.battleProbeMu.Lock()
|
||||||
|
s.battleProbe = makeBattleProbe(wire, cfg.BattleDiagnostics.ClientBattleType, meta.BattleSHA256)
|
||||||
|
s.battleProbeMu.Unlock()
|
||||||
|
Log("[BATTLE PROBE] queued read-only client parser check for next heartbeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) takeBattleProbe() []byte {
|
||||||
|
s.battleProbeMu.Lock()
|
||||||
|
defer s.battleProbeMu.Unlock()
|
||||||
|
probe := s.battleProbe
|
||||||
|
s.battleProbe = nil
|
||||||
|
return probe
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package gameserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCaptureUsesExactResponseBattleBytes(t *testing.T) {
|
||||||
|
t.Chdir(t.TempDir())
|
||||||
|
// Fixture values exercise zero-step affixes and map ordering on the wire.
|
||||||
|
battle := &goproto.SceneBattleInfo{BattleAvatarList: []*goproto.BattleAvatar{{Id: 1204, RelicList: []*goproto.BattleRelic{{Id: 61151, SubAffixList: []*goproto.RelicAffix{{AffixId: 7, Cnt: 3, Step: 0}}}}}}, BuffList: []*goproto.BattleBuff{{DynamicValues: map[string]float32{"first": 1, "second": 2}}}}
|
||||||
|
rsp := &goproto.StartQuickCocoonStageRsp{BattleInfo: battle}
|
||||||
|
body, err := proto.Marshal(rsp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wire, meta, err := saveBattleCapture(rsp, 1371, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if meta.ResponseSHA256 != fmt.Sprintf("%x", sha256.Sum256(body)) {
|
||||||
|
t.Fatal("response checksum mismatch")
|
||||||
|
}
|
||||||
|
var decoded goproto.SceneBattleInfo
|
||||||
|
if err := proto.Unmarshal(wire, &decoded); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !proto.Equal(battle, &decoded) {
|
||||||
|
t.Fatal("captured battle differs from outgoing battle")
|
||||||
|
}
|
||||||
|
written, err := os.ReadFile("battle_last_response.bin")
|
||||||
|
if err != nil || !bytes.Equal(written, body) {
|
||||||
|
t.Fatalf("response not preserved: %v", err)
|
||||||
|
}
|
||||||
|
written, err = os.ReadFile("battle_last.bin")
|
||||||
|
if err != nil || !bytes.Equal(written, wire) {
|
||||||
|
t.Fatalf("battle wire not preserved: %v", err)
|
||||||
|
}
|
||||||
|
probe := string(makeBattleProbe(wire, "FixtureBattleType", meta.BattleSHA256))
|
||||||
|
if strings.Contains(probe, "__TYPE_NAME__") || !strings.Contains(probe, meta.BattleSHA256) {
|
||||||
|
t.Fatal("probe placeholders not populated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProbeDoesNotRunWithoutBattle(t *testing.T) {
|
||||||
|
t.Chdir(t.TempDir())
|
||||||
|
_, meta, err := saveBattleCapture(&goproto.GetCurBattleInfoScRsp{}, 171, nil)
|
||||||
|
if err != nil || meta != nil {
|
||||||
|
t.Fatalf("empty login response captured: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("battle_last.bin"); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("empty response wrote a capture")
|
||||||
|
}
|
||||||
|
s := &Session{}
|
||||||
|
s.battleProbe = []byte("fixture")
|
||||||
|
if string(s.takeBattleProbe()) != "fixture" || len(s.takeBattleProbe()) != 0 {
|
||||||
|
t.Fatal("probe was not consumed exactly once")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBattleWireRejectsTruncatedResponse(t *testing.T) {
|
||||||
|
if _, err := extractBattleWire(&goproto.StartQuickCocoonStageRsp{}, []byte{0x2a, 0x7f}); err == nil {
|
||||||
|
t.Fatal("truncated payload accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
-- Read-only: decode the captured battle with the client's own protobuf parser.
|
||||||
|
-- This does not start battles, modify actors, replace handlers, or change saves.
|
||||||
|
do
|
||||||
|
local captureHash = __CAPTURE_HASH__
|
||||||
|
if _G.__gacBattleWireProbeV2Hash ~= captureHash then
|
||||||
|
_G.__gacBattleWireProbeV2Hash = captureHash
|
||||||
|
local function log(text)
|
||||||
|
CS.UnityEngine.Debug.Log('[GAC_BATTLE_WIRE] ' .. captureHash .. ' ' .. tostring(text))
|
||||||
|
end
|
||||||
|
local ok, err = pcall(function()
|
||||||
|
local typeName = __TYPE_NAME__
|
||||||
|
local assemblies = CS.System.AppDomain.CurrentDomain:GetAssemblies()
|
||||||
|
local messageType = nil
|
||||||
|
for i = 0, assemblies.Length - 1 do
|
||||||
|
messageType = assemblies[i]:GetType(typeName)
|
||||||
|
if messageType ~= nil then break end
|
||||||
|
end
|
||||||
|
if messageType == nil then error('message type not found: ' .. typeName) end
|
||||||
|
-- IL2CPP builds can expose the parser as a non-public member.
|
||||||
|
-- Include both visibility levels instead of assuming a public getter.
|
||||||
|
local flags = CS.System.Reflection.BindingFlags.Public
|
||||||
|
+ CS.System.Reflection.BindingFlags.NonPublic
|
||||||
|
+ CS.System.Reflection.BindingFlags.Static
|
||||||
|
+ CS.System.Reflection.BindingFlags.Instance
|
||||||
|
local properties = messageType:GetProperties(flags)
|
||||||
|
local parser, descriptor = nil, nil
|
||||||
|
log('message_type=' .. tostring(messageType.FullName) .. ' properties=' .. tostring(properties.Length))
|
||||||
|
for i = 0, properties.Length - 1 do
|
||||||
|
local property = properties[i]
|
||||||
|
local getter = property:GetGetMethod(true)
|
||||||
|
if getter ~= nil and getter.IsStatic then
|
||||||
|
local propertyType = tostring(property.PropertyType.FullName)
|
||||||
|
log('static_property=' .. tostring(property.Name) .. ' type=' .. propertyType)
|
||||||
|
if string.find(propertyType, 'Google.Protobuf.MessageParser', 1, true) then
|
||||||
|
parser = property:GetValue(nil, nil)
|
||||||
|
elseif propertyType == 'Google.Protobuf.Reflection.MessageDescriptor' then
|
||||||
|
descriptor = property:GetValue(nil, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local fields = messageType:GetFields(flags)
|
||||||
|
for i = 0, fields.Length - 1 do
|
||||||
|
local field = fields[i]
|
||||||
|
local fieldType = tostring(field.FieldType.FullName)
|
||||||
|
log('member=' .. tostring(field.Name) .. ' type=' .. fieldType .. ' static=' .. tostring(field.IsStatic))
|
||||||
|
if field.IsStatic then
|
||||||
|
if parser == nil and string.find(fieldType, 'Google.Protobuf.MessageParser', 1, true) then
|
||||||
|
parser = field:GetValue(nil)
|
||||||
|
elseif descriptor == nil and fieldType == 'Google.Protobuf.Reflection.MessageDescriptor' then
|
||||||
|
descriptor = field:GetValue(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local bytes = CS.System.Convert.FromBase64String(__PAYLOAD__)
|
||||||
|
local message = nil
|
||||||
|
if parser ~= nil then
|
||||||
|
message = parser:ParseFrom(bytes)
|
||||||
|
else
|
||||||
|
-- A generated message can decode through MergeFrom even when
|
||||||
|
-- its static parser metadata was stripped from reflection.
|
||||||
|
message = CS.System.Activator.CreateInstance(messageType)
|
||||||
|
local methods = messageType:GetMethods(flags)
|
||||||
|
local merge = nil
|
||||||
|
for i = 0, methods.Length - 1 do
|
||||||
|
local method = methods[i]
|
||||||
|
local parameters = method:GetParameters()
|
||||||
|
if not method.IsStatic and parameters.Length == 1
|
||||||
|
and tostring(parameters[0].ParameterType.FullName) == 'Google.Protobuf.CodedInputStream' then
|
||||||
|
merge = method
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if merge == nil then error('no parser or CodedInputStream decoder on ' .. typeName) end
|
||||||
|
local input = CS.Google.Protobuf.CodedInputStream(bytes)
|
||||||
|
local arguments = CS.System.Array.CreateInstance(typeof(CS.System.Object), 1)
|
||||||
|
arguments:SetValue(input, 0)
|
||||||
|
merge:Invoke(message, arguments)
|
||||||
|
end
|
||||||
|
log('type=' .. typeName .. ' bytes=' .. tostring(bytes.Length))
|
||||||
|
log('decoded=' .. message:ToString())
|
||||||
|
if descriptor ~= nil then
|
||||||
|
local fields = descriptor.Fields:InFieldNumberOrder()
|
||||||
|
local iterator = fields:GetEnumerator()
|
||||||
|
while iterator:MoveNext() do
|
||||||
|
local field = iterator.Current
|
||||||
|
local nested = field.MessageType
|
||||||
|
log('field=' .. tostring(field.FieldNumber) .. ' name=' .. tostring(field.Name)
|
||||||
|
.. ' type=' .. tostring(field.FieldType)
|
||||||
|
.. ' nested=' .. (nested ~= nil and tostring(nested.FullName) or ''))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
if not ok then log('probe_error=' .. tostring(err)) end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package gameserver
|
package gameserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -42,6 +43,10 @@ var BaseAvatarIDs = common.BaseAvatarIDs
|
|||||||
|
|
||||||
type handlerFunc func(s *Session, payload []byte) error
|
type handlerFunc func(s *Session, payload []byte) error
|
||||||
|
|
||||||
|
// errResponseAlreadySent tells the typed dispatcher that the handler has
|
||||||
|
// explicitly sent its response early to preserve packet ordering.
|
||||||
|
var errResponseAlreadySent = errors.New("response already sent")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
typedHandlers = map[uint16]handlerFunc{}
|
typedHandlers = map[uint16]handlerFunc{}
|
||||||
dummyResponses = map[uint16]uint16{} // req cmd -> rsp cmd (empty body reply)
|
dummyResponses = map[uint16]uint16{} // req cmd -> rsp cmd (empty body reply)
|
||||||
@@ -59,12 +64,12 @@ func handle[ReqT any, RspT any](name string, fn func(s *Session, req *ReqT, rsp
|
|||||||
func handleNamed[ReqT any, RspT any](name, rspName string, fn func(s *Session, req *ReqT, rsp *RspT) error) {
|
func handleNamed[ReqT any, RspT any](name, rspName string, fn func(s *Session, req *ReqT, rsp *RspT) error) {
|
||||||
reqID, ok := goproto.CmdIDByName[name+"CsReq"]
|
reqID, ok := goproto.CmdIDByName[name+"CsReq"]
|
||||||
if !ok {
|
if !ok {
|
||||||
Log("[DISPATCH] proto 4.6.51 has no " + name + "CsReq; handler skipped")
|
Log("[DISPATCH] loaded protocol has no " + name + "CsReq; handler skipped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rspID, ok := goproto.CmdIDByName[rspName]
|
rspID, ok := goproto.CmdIDByName[rspName]
|
||||||
if !ok {
|
if !ok {
|
||||||
Log("[DISPATCH] proto 4.6.51 has no " + rspName + "; handler skipped")
|
Log("[DISPATCH] loaded protocol has no " + rspName + "; handler skipped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
typedHandlers[uint16(reqID)] = func(s *Session, payload []byte) error {
|
typedHandlers[uint16(reqID)] = func(s *Session, payload []byte) error {
|
||||||
@@ -78,7 +83,9 @@ func handleNamed[ReqT any, RspT any](name, rspName string, fn func(s *Session, r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rsp := new(RspT)
|
rsp := new(RspT)
|
||||||
if err := fn(s, req, rsp); err != nil {
|
if err := fn(s, req, rsp); errors.Is(err, errResponseAlreadySent) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.Send(any(rsp).(proto.Message))
|
return s.Send(any(rsp).(proto.Message))
|
||||||
@@ -91,12 +98,25 @@ func dummy(name string) {
|
|||||||
reqID, ok1 := goproto.CmdIDByName[name+"CsReq"]
|
reqID, ok1 := goproto.CmdIDByName[name+"CsReq"]
|
||||||
rspID, ok2 := goproto.CmdIDByName[name+"ScRsp"]
|
rspID, ok2 := goproto.CmdIDByName[name+"ScRsp"]
|
||||||
if !ok1 || !ok2 {
|
if !ok1 || !ok2 {
|
||||||
Log("[DISPATCH] proto 4.6.51 has no dummy pair " + name + "; skipped")
|
Log("[DISPATCH] loaded protocol has no dummy pair " + name + "; skipped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dummyResponses[uint16(reqID)] = uint16(rspID)
|
dummyResponses[uint16(reqID)] = uint16(rspID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rawNamed binds only the request declared by the current command table.
|
||||||
|
// Old numeric aliases can collide with responses after a protocol update.
|
||||||
|
func rawNamed(request string, fn func(*Session, []byte) error) {
|
||||||
|
id, ok := goproto.CmdIDByName[request]
|
||||||
|
if !ok {
|
||||||
|
panic("raw handler request missing from protocol: " + request)
|
||||||
|
}
|
||||||
|
if _, exists := rawHandlers[id]; exists {
|
||||||
|
panic("duplicate raw handler: " + request)
|
||||||
|
}
|
||||||
|
rawHandlers[id] = fn
|
||||||
|
}
|
||||||
|
|
||||||
// dangerFamilies are client modules that Sync(nil) -> NRE -> black screen when
|
// dangerFamilies are client modules that Sync(nil) -> NRE -> black screen when
|
||||||
// they receive an empty ScRsp (lesson recorded in the Rust packet.rs / HANDOFF):
|
// they receive an empty ScRsp (lesson recorded in the Rust packet.rs / HANDOFF):
|
||||||
// never auto-answer these, drop them silently instead.
|
// never auto-answer these, drop them silently instead.
|
||||||
@@ -112,15 +132,8 @@ func isDangerCmd(cmdID uint16) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// CycleScore cmd ids (4143/4146) and GridFight range carry generic names
|
// This module requires populated rotation state before it can be answered.
|
||||||
switch cmdID {
|
if name == "GetMapRotationDataCsReq" || strings.HasPrefix(name, "ChimeraGet") {
|
||||||
case 4143, 4146, 8541, 5410, 5449, 7142, 3174, 9252, 8175, 6995, 6988:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// GetMapRotationData: an empty reply NREs the client's map init
|
|
||||||
// (AdventurePhase._EnterStep1_InitializeMapInfo) - drop until real
|
|
||||||
// RotaterData is implemented
|
|
||||||
if cmdID == 6827 {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
package gameserver
|
|
||||||
|
|
||||||
// Code generated from the March7th PS ActivityConfig (real 4.6.51 client
|
|
||||||
// activity ids). DO NOT EDIT manually.
|
|
||||||
|
|
||||||
type activityScheduleEntry struct{ activityID, panelID uint32 }
|
|
||||||
|
|
||||||
var realActivitySchedule = []activityScheduleEntry{
|
|
||||||
{1001501, 10015},
|
|
||||||
{1001701, 10017},
|
|
||||||
{1002101, 10191},
|
|
||||||
{1002501, 10025},
|
|
||||||
{1002601, 10192},
|
|
||||||
{1004101, 10041},
|
|
||||||
{1006201, 10066},
|
|
||||||
{1010801, 10187},
|
|
||||||
{2000603, 20016},
|
|
||||||
{2001101, 20011},
|
|
||||||
{2001102, 20011},
|
|
||||||
{2001103, 20011},
|
|
||||||
{2001104, 20011},
|
|
||||||
{2001105, 20011},
|
|
||||||
{2001106, 20011},
|
|
||||||
{2001107, 20011},
|
|
||||||
{2001108, 20011},
|
|
||||||
{2001109, 20011},
|
|
||||||
{2002303, 20024},
|
|
||||||
{2002502, 20027},
|
|
||||||
{2006301, 20064},
|
|
||||||
{2006302, 20064},
|
|
||||||
{2006400, 20064},
|
|
||||||
{2008201, 20091},
|
|
||||||
{2008401, 20092},
|
|
||||||
{2008601, 20087},
|
|
||||||
{2008701, 20087},
|
|
||||||
{2009100, 20091},
|
|
||||||
{2009200, 20092},
|
|
||||||
{2009301, 20093},
|
|
||||||
{2101001, 21010},
|
|
||||||
{2101101, 21011},
|
|
||||||
{3000801, 30008},
|
|
||||||
{4000208, 40002},
|
|
||||||
{4000301, 40003},
|
|
||||||
{4000302, 40003},
|
|
||||||
{4000303, 40003},
|
|
||||||
{4000304, 40003},
|
|
||||||
{4000305, 40003},
|
|
||||||
{4000306, 40003},
|
|
||||||
{4000307, 40003},
|
|
||||||
{5000002, 50000},
|
|
||||||
{5000003, 50000},
|
|
||||||
{5000004, 50000},
|
|
||||||
{5000005, 50000},
|
|
||||||
{5000006, 50000},
|
|
||||||
{5000007, 50000},
|
|
||||||
{5000008, 50000},
|
|
||||||
{5000009, 50000},
|
|
||||||
{5000010, 50000},
|
|
||||||
{5000011, 50000},
|
|
||||||
{5000013, 50000},
|
|
||||||
{5000105, 50001},
|
|
||||||
{5000106, 50001},
|
|
||||||
{5000107, 50001},
|
|
||||||
{5000201, 50002},
|
|
||||||
{5000202, 50002},
|
|
||||||
{5000203, 50002},
|
|
||||||
{5000204, 50002},
|
|
||||||
{5000301, 50003},
|
|
||||||
{5000302, 50003},
|
|
||||||
{5000303, 50003},
|
|
||||||
{5000304, 50003},
|
|
||||||
{5000408, 50004},
|
|
||||||
{5000501, 50005},
|
|
||||||
{5000502, 50005},
|
|
||||||
{5000503, 50005},
|
|
||||||
{5000504, 50005},
|
|
||||||
{5000505, 50005},
|
|
||||||
{5000601, 50006},
|
|
||||||
{5000602, 50006},
|
|
||||||
{5000603, 50006},
|
|
||||||
{5000604, 50006},
|
|
||||||
{5000605, 50006},
|
|
||||||
{5000702, 50007},
|
|
||||||
{5000902, 50009},
|
|
||||||
{5001202, 50012},
|
|
||||||
{5001401, 50014},
|
|
||||||
{5001501, 50015},
|
|
||||||
{5001802, 50018},
|
|
||||||
{5002102, 50021},
|
|
||||||
{5002202, 50022},
|
|
||||||
{5002402, 50024},
|
|
||||||
{5002602, 50026},
|
|
||||||
{5002902, 50029},
|
|
||||||
{5003402, 50034},
|
|
||||||
{5003502, 50035},
|
|
||||||
{5004102, 50041},
|
|
||||||
{5004202, 50042},
|
|
||||||
{5004502, 50045},
|
|
||||||
{5006302, 50063},
|
|
||||||
{5006802, 50068},
|
|
||||||
{5007001, 50070},
|
|
||||||
{5007002, 50070},
|
|
||||||
{5007003, 50070},
|
|
||||||
{5007004, 50070},
|
|
||||||
{5007005, 50070},
|
|
||||||
{5007007, 50070},
|
|
||||||
{5007302, 50073},
|
|
||||||
{5007402, 50074},
|
|
||||||
{5009202, 50092},
|
|
||||||
{5009402, 50094},
|
|
||||||
{5010102, 50101},
|
|
||||||
{5011102, 50111},
|
|
||||||
{5011202, 50112},
|
|
||||||
{5011402, 50114},
|
|
||||||
{5011602, 50116},
|
|
||||||
{5012501, 50125},
|
|
||||||
{5012502, 50125},
|
|
||||||
{5012503, 50125},
|
|
||||||
{5012504, 50125},
|
|
||||||
{5012505, 50125},
|
|
||||||
{5012601, 50126},
|
|
||||||
{5012701, 50127},
|
|
||||||
{5012901, 50129},
|
|
||||||
{5013201, 50132},
|
|
||||||
{5013202, 50132},
|
|
||||||
{5013301, 50133},
|
|
||||||
{5013401, 50134},
|
|
||||||
{5013402, 50134},
|
|
||||||
{5013501, 50135},
|
|
||||||
{5013502, 50135},
|
|
||||||
{5013701, 50137},
|
|
||||||
{5014201, 50142},
|
|
||||||
{5014202, 50142},
|
|
||||||
{5014301, 50143},
|
|
||||||
{5014302, 50143},
|
|
||||||
{5014303, 50143},
|
|
||||||
{5014304, 50143},
|
|
||||||
{5014305, 50143},
|
|
||||||
{5014311, 50143},
|
|
||||||
{5014312, 50143},
|
|
||||||
{5014313, 50143},
|
|
||||||
{5014314, 50143},
|
|
||||||
{5014315, 50143},
|
|
||||||
{5015401, 50154},
|
|
||||||
{6000101, 60001},
|
|
||||||
{6000302, 60003},
|
|
||||||
{6000901, 60009},
|
|
||||||
{6001402, 60014},
|
|
||||||
{6001601, 60016},
|
|
||||||
{6002301, 60023},
|
|
||||||
{6002401, 60024},
|
|
||||||
{7100001, 71000},
|
|
||||||
{7100101, 71001},
|
|
||||||
{7110001, 71100},
|
|
||||||
{7110101, 71101},
|
|
||||||
}
|
|
||||||
@@ -13,33 +13,25 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
handle("StartCocoonStage", func(s *Session, req *goproto.StartCocoonStageCsReq, rsp *goproto.StartCocoonStageScRsp) error {
|
handle("StartCocoonStage", func(s *Session, req *goproto.StartCocoonStageCsReq, rsp *goproto.StartCocoonStageScRsp) error {
|
||||||
Log(fmt.Sprintf("[BATTLE] StartCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d PropEntityId=%d", req.CocoonId, req.Wave, req.WorldLevel, req.PropEntityId))
|
Log(fmt.Sprintf("[BATTLE] StartCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d PropEntityId=%d", req.CocoonId, req.Wave, req.WorldLevel, req.PropEntityId))
|
||||||
battleInfo := createBattleInfo(s, 0, 0)
|
|
||||||
normalizeCalyxBattleInfo(battleInfo, req.Wave, req.WorldLevel)
|
|
||||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
|
||||||
|
|
||||||
challengeCnt := req.Wave
|
|
||||||
if challengeCnt == 0 {
|
|
||||||
challengeCnt = 1
|
|
||||||
}
|
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
rsp.PropEntityId = req.PropEntityId
|
rsp.PropEntityId = req.PropEntityId
|
||||||
rsp.CocoonId = req.CocoonId
|
rsp.CocoonId = req.CocoonId
|
||||||
rsp.Wave = req.Wave
|
rsp.Wave = req.Wave
|
||||||
rsp.ChallengeCnt = challengeCnt
|
battleInfo := createBattleInfo(s, 0, 0)
|
||||||
|
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||||
rsp.BattleInfo = battleInfo
|
rsp.BattleInfo = battleInfo
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// NOTE: 4.6.51 proto names this message StartQuickCocoonStageCsReq/Rsp
|
// The Go schema names this message StartQuickCocoonStageCsReq/Rsp
|
||||||
handleNamed("StartQuickCocoonStage", "StartQuickCocoonStageRsp", func(s *Session, req *goproto.StartQuickCocoonStageCsReq, rsp *goproto.StartQuickCocoonStageRsp) error {
|
handleNamed("StartQuickCocoonStage", "StartQuickCocoonStageRsp", func(s *Session, req *goproto.StartQuickCocoonStageCsReq, rsp *goproto.StartQuickCocoonStageRsp) error {
|
||||||
Log(fmt.Sprintf("[BATTLE] StartQuickCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d", req.CocoonId, req.Wave, req.WorldLevel))
|
Log(fmt.Sprintf("[BATTLE] StartQuickCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d", req.CocoonId, req.Wave, req.WorldLevel))
|
||||||
battleInfo := createBattleInfo(s, 0, 0)
|
|
||||||
normalizeCalyxBattleInfo(battleInfo, req.Wave, req.WorldLevel)
|
|
||||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
|
||||||
|
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
rsp.CocoonId = req.CocoonId
|
rsp.CocoonId = req.CocoonId
|
||||||
rsp.Wave = req.Wave
|
rsp.Wave = req.Wave
|
||||||
|
battleInfo := createBattleInfo(s, 0, 0)
|
||||||
|
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||||
|
|
||||||
rsp.BattleInfo = battleInfo
|
rsp.BattleInfo = battleInfo
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -47,7 +39,6 @@ func init() {
|
|||||||
handleNamed("StartQuickFarmElement", "StartQuickFarmElementRsp", func(s *Session, req *goproto.StartQuickFarmElementCsReq, rsp *goproto.StartQuickFarmElementRsp) error {
|
handleNamed("StartQuickFarmElement", "StartQuickFarmElementRsp", func(s *Session, req *goproto.StartQuickFarmElementCsReq, rsp *goproto.StartQuickFarmElementRsp) error {
|
||||||
Log(fmt.Sprintf("[BATTLE] StartQuickFarmElement: FarmID=%d WorldLevel=%d", req.PAOFHFLFFHD, req.WorldLevel))
|
Log(fmt.Sprintf("[BATTLE] StartQuickFarmElement: FarmID=%d WorldLevel=%d", req.PAOFHFLFFHD, req.WorldLevel))
|
||||||
battleInfo := createBattleInfo(s, 0, 0)
|
battleInfo := createBattleInfo(s, 0, 0)
|
||||||
normalizeCalyxBattleInfo(battleInfo, 1, req.WorldLevel)
|
|
||||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||||
|
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
@@ -115,24 +106,24 @@ func init() {
|
|||||||
//
|
//
|
||||||
// อาการ: กดโจมตีใน overworld แล้วกล้องหลุดออกจากตัวละคร
|
// อาการ: กดโจมตีใน overworld แล้วกล้องหลุดออกจากตัวละคร
|
||||||
//
|
//
|
||||||
// สาเหตุ (จากโค้ด Rust เดิม): handler ตอบ battle_info + monster_battle_info
|
// สาเหตุ (จากโค้ด Rust เดิม): handler ตอบ_info + monster_battle_info
|
||||||
// ให้ client เข้าสู่การต่อสู้กับ target ที่ "ผ่าน filter แบบหลวมๆ"
|
// ให้ client เข้าสู่การต่อสู้กับ target ที่ "ผ่าน filter แบบหลวมๆ"
|
||||||
// filter เดิม: id > 30000 (monster ในฉาก) || id < 1000 (avatar entity ของผู้เล่น!)
|
// filter เดิม: id > 30000 (monster ในฉาก) || id < 1000 (avatar entity ของผู้เล่น!)
|
||||||
// ถ้า targets ว่างและอยู่ใน arena (entry >= 3000000) ยัง "หลอก" target
|
// ถ้า targets ว่างและอยู่ใน arena (entry >= 3000000) ยัง "หลอก" target
|
||||||
// เป็น 30001 อีกชั้น
|
// เป็น 30001 อีกชั้น
|
||||||
// พอ client ได้ battle_info กับ "monster" ที่จริงคือ prop/NPC/ตัวผู้เล่นเอง
|
// พอ client ได้_info กับ "monster" ที่จริงคือ prop/NPC/ตัวผู้เล่นเอง
|
||||||
// หรือ monster ที่ไม่มี event_id มันจะเริ่ม battle transition ที่โหลดไม่สำเร็จ
|
// หรือ monster ที่ไม่มี event_id มันจะเริ่ม transition ที่โหลดไม่สำเร็จ
|
||||||
// → battle camera ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร
|
// → camera ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร
|
||||||
//
|
//
|
||||||
// FIX: ยอมรับเฉพาะ entity ที่ server spawn เป็น "monster จริง" ในฉากปัจจุบัน
|
// FIX: ยอมรับเฉพาะ entity ที่ server spawn เป็น "monster จริง" ในฉากปัจจุบัน
|
||||||
// (เก็บไว้ใน Session.MonsterEntities ตอน load_scene) ถ้าไม่มี monster จริง
|
// (เก็บไว้ใน Session.MonsterEntities ตอน load_scene) ถ้าไม่มี monster จริง
|
||||||
// ถูกโจมตี → ตอบ retcode 0 + cast_entity_id เฉยๆ ไม่เข้า battle
|
// ถูกโจมตี → ตอบ retcode 0 + cast_entity_id เฉยๆ ไม่เข้า
|
||||||
// client จะเล่นแอนิเมชันตีปกติ กล้องค้างกับตัวละคร
|
// client จะเล่นแอนิเมชันตีปกติ กล้องค้างกับตัวละคร
|
||||||
// ================================================================
|
// ================================================================
|
||||||
handle("SceneCastSkill", func(s *Session, req *goproto.SceneCastSkillCsReq, rsp *goproto.SceneCastSkillScRsp) error {
|
handle("SceneCastSkill", func(s *Session, req *goproto.SceneCastSkillCsReq, rsp *goproto.SceneCastSkillScRsp) error {
|
||||||
rsp.CastEntityId = req.CastEntityId
|
rsp.CastEntityId = req.CastEntityId
|
||||||
|
|
||||||
// เฉพาะ monster entity จริงเท่านั้นที่ trigger battle
|
// เฉพาะ monster entity จริงเท่านั้นที่ trigger
|
||||||
var targets []uint32
|
var targets []uint32
|
||||||
seen := map[uint32]bool{}
|
seen := map[uint32]bool{}
|
||||||
addTarget := func(id uint32) {
|
addTarget := func(id uint32) {
|
||||||
@@ -149,14 +140,14 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
// ไม่มี monster จริงถูกตี — ไม่เข้า battle (นี่คือ fix หลัก)
|
// ไม่มี monster จริงถูกตี — ไม่เข้า (นี่คือ fix หลัก)
|
||||||
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d skill_index=%d -> no real monster target (battle skipped)",
|
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d skill_index=%d -> no real monster target (battle skipped)",
|
||||||
req.CastEntityId, req.CasterId, req.SkillIndex)
|
req.CastEntityId, req.CasterId, req.SkillIndex)
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> battle with %d monster(s)",
|
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> with %d monster(s)",
|
||||||
req.CastEntityId, req.CasterId, len(targets))
|
req.CastEntityId, req.CasterId, len(targets))
|
||||||
|
|
||||||
battleInfo := createBattleInfo(s, req.CasterId, req.SkillIndex)
|
battleInfo := createBattleInfo(s, req.CasterId, req.SkillIndex)
|
||||||
@@ -179,39 +170,19 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
handle("GetCurBattleInfo", func(s *Session, req *goproto.GetCurBattleInfoCsReq, rsp *goproto.GetCurBattleInfoScRsp) error {
|
handle("GetCurBattleInfo", func(s *Session, req *goproto.GetCurBattleInfoCsReq, rsp *goproto.GetCurBattleInfoScRsp) error {
|
||||||
if info, ok := CURRENT_BATTLE.Load(s.Token); ok {
|
// Login queries this even when no battle is active. An absent battle
|
||||||
|
// is a successful empty result, not RET_BATTLE_STAGE_NOT_MATCH.
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
|
if info, ok := CURRENT_BATTLE.Load(s.Token); ok {
|
||||||
rsp.BattleInfo = info
|
rsp.BattleInfo = info
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rsp.Retcode = 2000 // RET_BATTLE_STAGE_NOT_MATCH
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCalyxBattleInfo(battleInfo *goproto.SceneBattleInfo, reqWave uint32, worldLevel uint32) {
|
// createBattleInfo is the Go port of.rs create_battle_info.
|
||||||
if worldLevel > 0 {
|
|
||||||
battleInfo.WorldLevel = worldLevel
|
|
||||||
}
|
|
||||||
if reqWave > 0 && int(reqWave) <= len(battleInfo.MonsterWaveList) {
|
|
||||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:reqWave]
|
|
||||||
} else if len(battleInfo.MonsterWaveList) > 1 {
|
|
||||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:1]
|
|
||||||
}
|
|
||||||
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
|
||||||
for _, w := range battleInfo.MonsterWaveList {
|
|
||||||
// NOTE: In client 4.6.53 GameAssembly.dll 0x166b2fdd, client verifies:
|
|
||||||
// (wave.WaveId & ~8) == 1. If wave_id != 1, it jumps to 0x166b3161
|
|
||||||
// and throws NullReferenceException in AdventureModule.CreateBattleServerCheckResult.
|
|
||||||
// Therefore every wave in Calyx / FarmElement MUST have WaveId = 1 and WaveParam = nil.
|
|
||||||
w.WaveId = 1
|
|
||||||
w.WaveParam = nil
|
|
||||||
w.BattleStageId = battleInfo.StageId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// createBattleInfo is the Go port of battle.rs create_battle_info.
|
|
||||||
func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBattleInfo {
|
func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBattleInfo {
|
||||||
player := s.JSON()
|
player := s.JSON()
|
||||||
if player == nil {
|
if player == nil {
|
||||||
@@ -431,9 +402,6 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
|||||||
// monsters
|
// monsters
|
||||||
battleInfo.MonsterWaveList = common.ToSceneMonsterWaves(player.BattleConfig.Monsters)
|
battleInfo.MonsterWaveList = common.ToSceneMonsterWaves(player.BattleConfig.Monsters)
|
||||||
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
||||||
for _, wave := range battleInfo.MonsterWaveList {
|
|
||||||
wave.BattleStageId = battleInfo.StageId
|
|
||||||
}
|
|
||||||
|
|
||||||
// global buffs
|
// global buffs
|
||||||
hasCastorice, hasSilverWolf := false, false
|
hasCastorice, hasSilverWolf := false, false
|
||||||
|
|||||||
@@ -274,12 +274,8 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
handle("GetCurChallengePeak", func(s *Session, req *goproto.GetCurChallengePeakCsReq, rsp *goproto.GetCurChallengePeakScRsp) error {
|
handle("GetCurChallengePeak", func(s *Session, req *goproto.GetCurChallengePeakCsReq, rsp *goproto.GetCurChallengePeakScRsp) error {
|
||||||
// reverted to the state of the build that reached Pom-Pom:
|
// Match upstream's empty current-state response until a Peak is active.
|
||||||
// empty peak_id (a fabricated peak_id:1 crashed CreateMemberEntityLeader)
|
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
rsp.HasPassed = true
|
|
||||||
rsp.PeakId = 1
|
|
||||||
rsp.BossBuffId = 0
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -293,13 +289,9 @@ func init() {
|
|||||||
EndTime: 1924992000,
|
EndTime: 1924992000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// NOTE: the real 4.6.51 activity schedule (realActivitySchedule, from
|
// Advertise only the supported Endgame schedules. Exposing the full
|
||||||
// the March7th dump) BLACK-SCREENS the events page: the client renders
|
// ActivityConfig before implementing its activity-data handlers makes
|
||||||
// real event panels, then requests each activity's data (GetActivity-
|
// the Client request unavailable state and can stall the events page.
|
||||||
// HotData/GetActiveActivityData/...) which we cannot fill yet - NRE.
|
|
||||||
// The synthetic list below resolves to nothing and keeps the page open
|
|
||||||
// (empty but usable). Re-enable the real list only after implementing
|
|
||||||
// per-activity data handlers.
|
|
||||||
|
|
||||||
// Apocalyptic Shadow tab requires an active schedule with panel 21001
|
// Apocalyptic Shadow tab requires an active schedule with panel 21001
|
||||||
for actID := uint32(21001); actID <= 21030; actID++ {
|
for actID := uint32(21001); actID <= 21030; actID++ {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gameserver
|
package gameserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -9,8 +10,8 @@ import (
|
|||||||
goproto "gac-hkrpg-go/protocol/proto"
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Full Go port of challenge.rs: StartChallenge / Tierce / Peak flows with the
|
// Challenge flows use the generated request and response layouts. Command
|
||||||
// hand-built 4.6.51 response layouts (cmd 1787 / 8973).
|
// numbers are resolved through the current protocol table at registration/send.
|
||||||
|
|
||||||
type decodedChallengeStart struct {
|
type decodedChallengeStart struct {
|
||||||
challengeID uint32
|
challengeID uint32
|
||||||
@@ -106,433 +107,89 @@ func mapChallengeStageID(id uint32) uint32 {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLenDelimAndAdvance(buf *[]byte) ([]byte, bool) {
|
|
||||||
b := *buf
|
|
||||||
l, n := readUvarint(b)
|
|
||||||
if n == 0 || int(l) > len(b[n:]) {
|
|
||||||
*buf = nil
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
sub := b[n : n+int(l)]
|
|
||||||
*buf = b[n+int(l):]
|
|
||||||
return sub, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func readLenDelim(buf []byte) ([]byte, bool) {
|
|
||||||
l, n := readUvarint(buf)
|
|
||||||
if n == 0 || int(l) > len(buf[n:]) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return buf[n : n+int(l)], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||||
var req decodedChallengeStart
|
|
||||||
|
|
||||||
var pbReq goproto.StartChallengeCsReq
|
var pbReq goproto.StartChallengeCsReq
|
||||||
if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 {
|
if err := proto.Unmarshal(payload, &pbReq); err != nil {
|
||||||
req.challengeID = pbReq.ChallengeId
|
return decodedChallengeStart{}
|
||||||
req.firstAvatars = pbReq.FirstLineup
|
}
|
||||||
if len(req.firstAvatars) == 0 && len(pbReq.AvatarLineupFirst) > 0 {
|
req := decodedChallengeStart{challengeID: pbReq.ChallengeId,
|
||||||
for _, av := range pbReq.AvatarLineupFirst {
|
firstAvatars: pbReq.FirstLineup, secondAvatars: pbReq.SecondLineup}
|
||||||
if av.Id != 0 {
|
if len(req.firstAvatars) == 0 {
|
||||||
req.firstAvatars = append(req.firstAvatars, av.Id)
|
for _, avatar := range pbReq.AvatarLineupFirst {
|
||||||
|
if avatar.Id != 0 {
|
||||||
|
req.firstAvatars = append(req.firstAvatars, avatar.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.secondAvatars = pbReq.SecondLineup
|
if len(req.secondAvatars) == 0 {
|
||||||
if len(req.secondAvatars) == 0 && len(pbReq.AvatarLineupSecond) > 0 {
|
for _, avatar := range pbReq.AvatarLineupSecond {
|
||||||
for _, av := range pbReq.AvatarLineupSecond {
|
if avatar.Id != 0 {
|
||||||
if av.Id != 0 {
|
req.secondAvatars = append(req.secondAvatars, avatar.Id)
|
||||||
req.secondAvatars = append(req.secondAvatars, av.Id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pbReq.StageInfo != nil {
|
if pbReq.StageInfo != nil {
|
||||||
if pbReq.StageInfo.BossInfo != nil && pbReq.StageInfo.BossInfo.BuffOne != 0 {
|
if info := pbReq.StageInfo.BossInfo; info != nil {
|
||||||
req.buffID = pbReq.StageInfo.BossInfo.BuffOne
|
req.buffID = info.BuffOne
|
||||||
} else if pbReq.StageInfo.StoryInfo != nil && pbReq.StageInfo.StoryInfo.BuffOne != 0 {
|
}
|
||||||
req.buffID = pbReq.StageInfo.StoryInfo.BuffOne
|
if info := pbReq.StageInfo.StoryInfo; info != nil && req.buffID == 0 {
|
||||||
|
req.buffID = info.BuffOne
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return req
|
return req
|
||||||
}
|
|
||||||
|
|
||||||
buf := payload
|
|
||||||
for len(buf) > 0 {
|
|
||||||
tag, n := readUvarint(buf)
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n:]
|
|
||||||
fieldNumber := uint32(tag >> 3)
|
|
||||||
wireType := uint32(tag & 0x7)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case (fieldNumber == 14 || fieldNumber == 12) && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
if req.challengeID == 0 {
|
|
||||||
req.challengeID = uint32(v)
|
|
||||||
}
|
|
||||||
case fieldNumber == 6 && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
req.firstAvatars = append(req.firstAvatars, uint32(v))
|
|
||||||
case fieldNumber == 6 && wireType == 2:
|
|
||||||
sub, ok := readLenDelimAndAdvance(&buf)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for len(sub) > 0 {
|
|
||||||
v, n3 := readUvarint(sub)
|
|
||||||
if n3 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sub = sub[n3:]
|
|
||||||
req.firstAvatars = append(req.firstAvatars, uint32(v))
|
|
||||||
}
|
|
||||||
case fieldNumber == 8 && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
req.secondAvatars = append(req.secondAvatars, uint32(v))
|
|
||||||
case fieldNumber == 8 && wireType == 2:
|
|
||||||
sub, ok := readLenDelimAndAdvance(&buf)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for len(sub) > 0 {
|
|
||||||
v, n3 := readUvarint(sub)
|
|
||||||
if n3 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sub = sub[n3:]
|
|
||||||
req.secondAvatars = append(req.secondAvatars, uint32(v))
|
|
||||||
}
|
|
||||||
case (fieldNumber == 4 || fieldNumber == 15) && wireType == 2:
|
|
||||||
sub, ok := readLenDelimAndAdvance(&buf)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for len(sub) > 0 {
|
|
||||||
stag, n3 := readUvarint(sub)
|
|
||||||
if n3 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sfn := uint32(stag >> 3)
|
|
||||||
swt := uint32(stag & 0x7)
|
|
||||||
if sfn == 1 && swt == 0 {
|
|
||||||
v, n4 := readUvarint(sub[n3:])
|
|
||||||
if n4 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sub = sub[n3+n4:]
|
|
||||||
if fieldNumber == 4 {
|
|
||||||
req.firstAvatars = append(req.firstAvatars, uint32(v))
|
|
||||||
} else {
|
|
||||||
req.secondAvatars = append(req.secondAvatars, uint32(v))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
skipWireField(swt, &sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case fieldNumber == 10 && wireType == 2:
|
|
||||||
sub, ok := readLenDelimAndAdvance(&buf)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for len(sub) > 0 {
|
|
||||||
stag, n3 := readUvarint(sub)
|
|
||||||
if n3 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sfn := uint32(stag >> 3)
|
|
||||||
swt := uint32(stag & 0x7)
|
|
||||||
sub = sub[n3:]
|
|
||||||
if swt == 2 && (sfn == 9 || sfn == 10) {
|
|
||||||
inner, ok2 := readLenDelimAndAdvance(&sub)
|
|
||||||
if !ok2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for len(inner) > 0 {
|
|
||||||
t2, n4 := readUvarint(inner)
|
|
||||||
if n4 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
f2 := uint32(t2 >> 3)
|
|
||||||
w2 := uint32(t2 & 0x7)
|
|
||||||
inner = inner[n4:]
|
|
||||||
if w2 == 0 && (f2 == 10 || f2 == 15) {
|
|
||||||
v, n5 := readUvarint(inner)
|
|
||||||
if n5 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
inner = inner[n5:]
|
|
||||||
if req.buffID == 0 {
|
|
||||||
req.buffID = uint32(v)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
skipWireField(w2, &inner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
skipWireField(swt, &sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
skipWireField(wireType, &buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type tierceStageEntry struct {
|
|
||||||
buff uint32
|
|
||||||
lineup []uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart {
|
func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart {
|
||||||
var req decodedChallengeStart
|
|
||||||
var stagesAvatars []tierceStageEntry
|
|
||||||
|
|
||||||
var pbReq goproto.StartChallengeTierceCsReq
|
var pbReq goproto.StartChallengeTierceCsReq
|
||||||
if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 {
|
if err := proto.Unmarshal(payload, &pbReq); err != nil {
|
||||||
req.challengeID = pbReq.ChallengeId
|
return decodedChallengeStart{}
|
||||||
req.stageIndex = pbReq.FINOCEGDBDI
|
}
|
||||||
req.isSingleStage = pbReq.MPKMIKBFEHG
|
req := decodedChallengeStart{challengeID: pbReq.ChallengeId,
|
||||||
for _, lt := range pbReq.LineupTierceList {
|
stageIndex: pbReq.FINOCEGDBDI, isSingleStage: pbReq.MPKMIKBFEHG}
|
||||||
var l []uint32
|
for index, stage := range pbReq.LineupTierceList {
|
||||||
for _, av := range lt.TierceAvatarIdList {
|
var avatars []uint32
|
||||||
if av.Id != 0 {
|
for _, avatar := range stage.TierceAvatarIdList {
|
||||||
l = append(l, av.Id)
|
if avatar.Id != 0 {
|
||||||
|
avatars = append(avatars, avatar.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stagesAvatars = append(stagesAvatars, tierceStageEntry{
|
req.stageInfoList = append(req.stageInfoList, avatars)
|
||||||
buff: lt.BuffId,
|
if uint32(index) == req.stageIndex {
|
||||||
lineup: l,
|
req.buffID = stage.BuffId
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buf := payload
|
|
||||||
for len(buf) > 0 {
|
|
||||||
tag, n := readUvarint(buf)
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n:]
|
|
||||||
fieldNumber := uint32(tag >> 3)
|
|
||||||
wireType := uint32(tag & 0x7)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case (fieldNumber == 10 || fieldNumber == 1 || fieldNumber == 5) && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
req.stageIndex = uint32(v)
|
|
||||||
case (fieldNumber == 15 || fieldNumber == 3) && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
req.challengeID = uint32(v)
|
|
||||||
case (fieldNumber == 2 || fieldNumber == 13) && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
req.isSingleStage = v != 0
|
|
||||||
case (fieldNumber == 9 || fieldNumber == 12 || fieldNumber == 14) && wireType == 2:
|
|
||||||
sub, ok := readLenDelimAndAdvance(&buf)
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var stageBuff uint32
|
|
||||||
var stageLineup []uint32
|
|
||||||
for len(sub) > 0 {
|
|
||||||
subTag, n2 := readUvarint(sub)
|
|
||||||
if n2 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
subFn := uint32(subTag >> 3)
|
|
||||||
subWt := uint32(subTag & 0x7)
|
|
||||||
sub = sub[n2:]
|
|
||||||
switch {
|
|
||||||
case (subFn == 10 || subFn == 6 || subFn == 5) && subWt == 0:
|
|
||||||
v, n3 := readUvarint(sub)
|
|
||||||
if n3 == 0 {
|
|
||||||
sub = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sub = sub[n3:]
|
|
||||||
stageBuff = uint32(v)
|
|
||||||
case (subFn == 9 || subFn == 4) && subWt == 2:
|
|
||||||
abuf, ok2 := readLenDelimAndAdvance(&sub)
|
|
||||||
if !ok2 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
var aid uint32
|
|
||||||
for len(abuf) > 0 {
|
|
||||||
atag, n4 := readUvarint(abuf)
|
|
||||||
if n4 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
afn := uint32(atag >> 3)
|
|
||||||
awt := uint32(atag & 0x7)
|
|
||||||
abuf = abuf[n4:]
|
|
||||||
if (afn == 1 || afn == 7 || afn == 9) && awt == 0 {
|
|
||||||
v, n5 := readUvarint(abuf)
|
|
||||||
if n5 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
abuf = abuf[n5:]
|
|
||||||
aid = uint32(v)
|
|
||||||
} else {
|
|
||||||
skipWireField(awt, &abuf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if aid != 0 {
|
if len(req.stageInfoList) > 0 {
|
||||||
stageLineup = append(stageLineup, aid)
|
req.firstAvatars = req.stageInfoList[0]
|
||||||
}
|
}
|
||||||
default:
|
if len(req.stageInfoList) > 1 {
|
||||||
skipWireField(subWt, &sub)
|
req.secondAvatars = req.stageInfoList[1]
|
||||||
}
|
}
|
||||||
|
if len(req.stageInfoList) > 2 {
|
||||||
|
req.thirdAvatars = req.stageInfoList[2]
|
||||||
}
|
}
|
||||||
stagesAvatars = append(stagesAvatars, tierceStageEntry{buff: stageBuff, lineup: stageLineup})
|
|
||||||
default:
|
|
||||||
skipWireField(wireType, &buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, st := range stagesAvatars {
|
|
||||||
req.stageInfoList = append(req.stageInfoList, st.lineup)
|
|
||||||
}
|
|
||||||
if len(stagesAvatars) > 0 {
|
|
||||||
req.firstAvatars = stagesAvatars[0].lineup
|
|
||||||
if req.stageIndex == 0 && stagesAvatars[0].buff != 0 {
|
|
||||||
req.buffID = stagesAvatars[0].buff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(stagesAvatars) > 1 {
|
|
||||||
req.secondAvatars = stagesAvatars[1].lineup
|
|
||||||
if req.stageIndex == 1 && stagesAvatars[1].buff != 0 {
|
|
||||||
req.buffID = stagesAvatars[1].buff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(stagesAvatars) > 2 {
|
|
||||||
req.thirdAvatars = stagesAvatars[2].lineup
|
|
||||||
if req.stageIndex == 2 && stagesAvatars[2].buff != 0 {
|
|
||||||
req.buffID = stagesAvatars[2].buff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) {
|
func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) {
|
||||||
var challengeID uint32
|
var req goproto.SetChallengeTierceLineupCsReq
|
||||||
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
var stages [][]uint32
|
var stages [][]uint32
|
||||||
buf := payload
|
for _, stage := range req.StageInfoList {
|
||||||
|
var avatars []uint32
|
||||||
for len(buf) > 0 {
|
for _, avatar := range stage.TierceAvatarIdList {
|
||||||
tag, n := readUvarint(buf)
|
if avatar.Id != 0 {
|
||||||
if n == 0 {
|
avatars = append(avatars, avatar.Id)
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n:]
|
|
||||||
fieldNumber := uint32(tag >> 3)
|
|
||||||
wireType := uint32(tag & 0x7)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case fieldNumber == 7 && wireType == 0:
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
challengeID = uint32(v)
|
|
||||||
case fieldNumber == 12 && wireType == 2:
|
|
||||||
sub, ok := readLenDelim(buf)
|
|
||||||
if !ok {
|
|
||||||
buf = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[len(sub):]
|
|
||||||
var stageLineup []uint32
|
|
||||||
for len(sub) > 0 {
|
|
||||||
subTag, n2 := readUvarint(sub)
|
|
||||||
if n2 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
subFn := uint32(subTag >> 3)
|
|
||||||
subWt := uint32(subTag & 0x7)
|
|
||||||
if subFn == 9 && subWt == 2 {
|
|
||||||
abuf, ok2 := readLenDelim(sub[n2:])
|
|
||||||
if !ok2 {
|
|
||||||
sub = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sub = sub[n2+len(abuf):]
|
|
||||||
var aid uint32
|
|
||||||
for len(abuf) > 0 {
|
|
||||||
atag, n4 := readUvarint(abuf)
|
|
||||||
if n4 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
afn := uint32(atag >> 3)
|
|
||||||
awt := uint32(atag & 0x7)
|
|
||||||
if afn == 9 && awt == 0 {
|
|
||||||
v, n5 := readUvarint(abuf[n4:])
|
|
||||||
if n5 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
abuf = abuf[n4+n5:]
|
|
||||||
aid = uint32(v)
|
|
||||||
} else {
|
|
||||||
skipWireField(awt, &abuf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if aid != 0 {
|
stages = append(stages, avatars)
|
||||||
stageLineup = append(stageLineup, aid)
|
|
||||||
}
|
}
|
||||||
} else {
|
return req.ChallengeId, stages
|
||||||
skipWireField(subWt, &sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stages = append(stages, stageLineup)
|
|
||||||
default:
|
|
||||||
skipWireField(wireType, &buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return challengeID, stages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// battle config setup shared by start handlers
|
// config setup shared by start handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func setChallengeBattleConfig(player *common.FreesrData, avatars []uint32, challengeID uint32, eventID uint32, buff uint32) {
|
func setChallengeBattleConfig(player *common.FreesrData, avatars []uint32, challengeID uint32, eventID uint32, buff uint32) {
|
||||||
@@ -618,63 +275,12 @@ func dedupeTruncate4(in []uint32) []uint32 {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// looksLikeChallengeID: known challenge-id ranges across endgame modes.
|
|
||||||
func looksLikeChallengeID(v uint64) bool {
|
|
||||||
switch {
|
|
||||||
case v >= 101 && v <= 119, v == 900,
|
|
||||||
v >= 1001 && v <= 1035,
|
|
||||||
v >= 2001 && v <= 2025,
|
|
||||||
v >= 3001 && v <= 3020,
|
|
||||||
v >= 5000 && v <= 5700, // MoC floors incl. 4.6 phases
|
|
||||||
v >= 20000 && v <= 20300, // Pure Fiction
|
|
||||||
v >= 30000 && v <= 30300: // Apocalyptic Shadow
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
cd := getChallengeData()
|
|
||||||
_, okC := cd.Challenges[uint32(v)]
|
|
||||||
_, okT := cd.Tierce[uint32(v)]
|
|
||||||
return okC || okT
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectChallengeID: when the strict decoder misses the challenge id (field
|
|
||||||
// layout drift between client builds), scan top-level varint fields for the
|
|
||||||
// first value that looks like a challenge id.
|
|
||||||
func detectChallengeID(payload []byte) (uint32, int) {
|
|
||||||
buf := payload
|
|
||||||
for len(buf) > 0 {
|
|
||||||
tag, n := readUvarint(buf)
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n:]
|
|
||||||
wireType := uint32(tag & 0x7)
|
|
||||||
if wireType == 0 {
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
if looksLikeChallengeID(v) {
|
|
||||||
return uint32(v), int(tag >> 3)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
skipWireField(wireType, &buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleStartChallenge ports handle_start_challenge: respond cmd 1787 with
|
// handleStartChallenge ports handle_start_challenge: respond cmd 1787 with
|
||||||
// retcode(3) + cur_challenge(5) + lineup_list(6) + scene(10) + stage_info(13).
|
// retcode(3) + cur_challenge(5) + lineup_list(6) + scene(10) + stage_info(13).
|
||||||
func handleStartChallenge(s *Session, payload []byte) error {
|
func handleStartChallenge(s *Session, payload []byte) error {
|
||||||
req := decodeStartChallengeReq(payload)
|
req := decodeStartChallengeReq(payload)
|
||||||
if req.challengeID == 0 {
|
if req.challengeID == 0 {
|
||||||
if id, field := detectChallengeID(payload); id != 0 {
|
return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
|
||||||
Log("handle_start_challenge: strict decode missed, detected challenge_id=" + itoa(int(id)) + " at field " + itoa(field))
|
|
||||||
req.challengeID = id
|
|
||||||
} else {
|
|
||||||
PacketLog("[CHALLENGE] challenge_id=0 payload=" + hexDump(payload, 128))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Log("handle_start_challenge: challenge_id=" + itoa(int(req.challengeID)))
|
Log("handle_start_challenge: challenge_id=" + itoa(int(req.challengeID)))
|
||||||
|
|
||||||
@@ -742,12 +348,7 @@ func handleStartChallenge(s *Session, payload []byte) error {
|
|||||||
LineupList: lineups,
|
LineupList: lineups,
|
||||||
Scene: sceneInfo,
|
Scene: sceneInfo,
|
||||||
}
|
}
|
||||||
rspBuf, err := proto.Marshal(rsp)
|
return s.Send(rsp)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.SendRaw(&NetPacket{CmdType: 1787, Body: rspBuf})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scoreIDFor(challengeID uint32) uint32 {
|
func scoreIDFor(challengeID uint32) uint32 {
|
||||||
@@ -757,9 +358,12 @@ func scoreIDFor(challengeID uint32) uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStartChallengeTierce ports handle_start_challenge_tierce -> cmd 8998 (4.6.51).
|
// handleStartChallengeTierce uses the current Tierce request schema.
|
||||||
func handleStartChallengeTierce(s *Session, payload []byte) error {
|
func handleStartChallengeTierce(s *Session, payload []byte) error {
|
||||||
req := decodeStartChallengeTierceReq(payload)
|
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 := mapChallengeStageID(req.challengeID)
|
||||||
Log("handle_start_challenge_tierce: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")")
|
Log("handle_start_challenge_tierce: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")")
|
||||||
|
|
||||||
@@ -910,8 +514,8 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
|
|||||||
tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx)))
|
tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4.6.51 StartChallengeTierceScRsp (CmdID 8998)
|
// Generated response resolves its command ID through Session.Send.
|
||||||
rsp46 := &goproto.StartChallengeTierceScRsp{
|
rsp := &goproto.StartChallengeTierceScRsp{
|
||||||
Retcode: 0,
|
Retcode: 0,
|
||||||
Scene: sceneInfo,
|
Scene: sceneInfo,
|
||||||
CurTierceChallenge: &goproto.CurTierceChallenge{
|
CurTierceChallenge: &goproto.CurTierceChallenge{
|
||||||
@@ -921,110 +525,61 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
|
|||||||
MPKMIKBFEHG: req.isSingleStage,
|
MPKMIKBFEHG: req.isSingleStage,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rspBuf, err := proto.Marshal(rsp46)
|
return s.Send(rsp)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.SendRaw(&NetPacket{CmdType: 8998, Body: rspBuf})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSetChallengeTierceLineup(s *Session, payload []byte) error {
|
func handleSetChallengeTierceLineup(s *Session, payload []byte) error {
|
||||||
challengeID, stages := decodeSetChallengeTierceLineupReq(payload)
|
challengeID, stages := decodeSetChallengeTierceLineupReq(payload)
|
||||||
|
if challengeID == 0 {
|
||||||
|
return s.Send(&goproto.SetChallengeTierceLineupScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
|
||||||
|
}
|
||||||
if len(stages) > 0 {
|
if len(stages) > 0 {
|
||||||
tierceLineupsMu.Lock()
|
tierceLineupsMu.Lock()
|
||||||
tierceLineups[challengeID] = stages
|
tierceLineups[challengeID] = stages
|
||||||
tierceLineupsMu.Unlock()
|
tierceLineupsMu.Unlock()
|
||||||
}
|
}
|
||||||
// 4.6.51 SetChallengeTierceLineupScRsp (cmd 8985, retcode tag 1 = 0)
|
return s.Send(&goproto.SetChallengeTierceLineupScRsp{})
|
||||||
if err := s.SendRaw(&NetPacket{CmdType: 8985, Body: []byte{0x08, 0x00}}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 8999, Body: []byte{0x08, 0x00}})
|
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 8995, Body: []byte{0x60, 0x00}})
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLeaveChallenge(s *Session, _ []byte) error {
|
func handleLeaveChallenge(s *Session, _ []byte) error {
|
||||||
var returnEntry uint32 = 102020107
|
return leaveChallengeScene(s, &goproto.LeaveChallengeScRsp{})
|
||||||
if player := s.JSON(); player != nil {
|
|
||||||
player.BattleConfig.CustomBattleLineup = nil
|
|
||||||
// Always return safely to Belobog Admin District 102020107
|
|
||||||
player.Scene.EntryID = 102020107
|
|
||||||
player.Scene.PlaneID = 10202
|
|
||||||
player.Scene.FloorID = 10202001
|
|
||||||
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
|
|
||||||
player.SavePersistent()
|
|
||||||
}
|
|
||||||
// 4.6.51 LeaveChallengeScRsp (cmd 1771, tag 10 retcode 0)
|
|
||||||
if err := s.SendRaw(&NetPacket{CmdType: 1771, Body: []byte{0x50, 0x00}}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 1716, Body: []byte{0x20, 0x00}})
|
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 1781, Body: []byte{0x40, 0x00}})
|
|
||||||
|
|
||||||
// Restore the player back to the open world
|
|
||||||
_, _ = loadScene(s, returnEntry, true, nil, nil)
|
|
||||||
_ = refreshLineup(s)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLeaveChallengeTierce(s *Session, _ []byte) error {
|
func handleLeaveChallengeTierce(s *Session, _ []byte) error {
|
||||||
var returnEntry uint32 = 102020107
|
return leaveChallengeScene(s, &goproto.LeaveChallengeTierceScRsp{})
|
||||||
if player := s.JSON(); player != nil {
|
}
|
||||||
player.BattleConfig.CustomBattleLineup = nil
|
|
||||||
// Always return safely to Belobog Admin District 102020107
|
func leaveChallengeScene(s *Session, response proto.Message) error {
|
||||||
player.Scene.EntryID = 102020107
|
player := s.JSON()
|
||||||
player.Scene.PlaneID = 10202
|
if player == nil || player.Scene.EntryID == 0 {
|
||||||
player.Scene.FloorID = 10202001
|
return fmt.Errorf("challenge return scene is unavailable")
|
||||||
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
|
|
||||||
player.SavePersistent()
|
|
||||||
}
|
}
|
||||||
// 4.6.51 LeaveChallengeTierceScRsp (cmd 8983, tag 13 retcode 0)
|
// loadChallengeScene preserves the player's saved overworld scene.
|
||||||
if err := s.SendRaw(&NetPacket{CmdType: 8983, Body: []byte{0x68, 0x00}}); err != nil {
|
entryID := player.Scene.EntryID
|
||||||
|
player.BattleConfig.CustomBattleLineup = nil
|
||||||
|
CURRENT_BATTLE.Delete(s.Token)
|
||||||
|
if err := s.Send(&goproto.QuitBattleScNotify{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 8997, Body: []byte{0x38, 0x00}})
|
if _, err := loadScene(s, entryID, true, true, nil, nil); err != nil {
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 8982, Body: []byte{0x68, 0x00}})
|
return err
|
||||||
|
}
|
||||||
// Restore the player back to the open world
|
if err := refreshLineup(s); err != nil {
|
||||||
_, _ = loadScene(s, returnEntry, true, nil, nil)
|
return err
|
||||||
_ = refreshLineup(s)
|
}
|
||||||
return nil
|
return s.Send(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTakeChallengeReward(s *Session, payload []byte) error {
|
func handleTakeChallengeReward(s *Session, payload []byte) error {
|
||||||
var groupID uint32
|
var req goproto.TakeChallengeRewardCsReq
|
||||||
buf := payload
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
||||||
for len(buf) > 0 {
|
return err
|
||||||
tag, n := readUvarint(buf)
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
buf = buf[n:]
|
return s.Send(&goproto.TakeChallengeRewardScRsp{GroupId: req.GroupId})
|
||||||
fnNum := uint32(tag >> 3)
|
|
||||||
wt := uint32(tag & 0x7)
|
|
||||||
if fnNum == 4 && wt == 0 {
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n2:]
|
|
||||||
groupID = uint32(v)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
skipWireField(wt, &buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body []byte
|
|
||||||
body = append(body, 0x10) // tag 2: group_id
|
|
||||||
body = appendUvarint(body, uint64(groupID))
|
|
||||||
body = append(body, 0x30, 0x00) // tag 6: retcode = 0
|
|
||||||
|
|
||||||
return s.SendRaw(&NetPacket{CmdType: 1704, Body: body})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildGetChallengeTierceDataScRsp — verbatim port of the Rust builder.
|
// buildGetChallengeTierceDataScRspReal builds the generated current response.
|
||||||
func buildGetChallengeTierceDataScRspReal() []byte {
|
func buildGetChallengeTierceDataScRspReal() *goproto.GetChallengeTierceDataScRsp {
|
||||||
cd := getChallengeData()
|
cd := getChallengeData()
|
||||||
|
|
||||||
type tierceEntry struct {
|
type tierceEntry struct {
|
||||||
@@ -1066,17 +621,8 @@ func buildGetChallengeTierceDataScRspReal() []byte {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
rspMsg := &goproto.GetChallengeTierceDataScRsp{
|
return &goproto.GetChallengeTierceDataScRsp{
|
||||||
ChallengeTierceGroups: groups,
|
ChallengeTierceGroups: groups,
|
||||||
Retcode: 0,
|
Retcode: 0,
|
||||||
}
|
}
|
||||||
body, err := proto.Marshal(rspMsg)
|
|
||||||
if err == nil {
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
var rsp []byte
|
|
||||||
rsp = append(rsp, 0x40, 0x00) // tag 8: retcode = 0
|
|
||||||
rsp = append(rsp, 0x60, 0x00) // legacy tag 12: retcode = 0
|
|
||||||
return rsp
|
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_RelicAvatar{RelicAvatar: inner}
|
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_RelicAvatar{RelicAvatar: inner}
|
||||||
case goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC:
|
case goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC:
|
||||||
// 4.6.51 proto: recomended_relic_info_list is repeated GDFKJKDLEPM
|
// Loaded schema: recomended_relic_info_list is repeated GDFKJKDLEPM
|
||||||
// (obfuscated, contents unused by this PS) — reply with empty entries
|
// (obfuscated, contents unused by this PS) — reply with empty entries
|
||||||
inner := &goproto.BigDataRecommendAvatarRelic{}
|
inner := &goproto.BigDataRecommendAvatarRelic{}
|
||||||
for range BaseAvatarIDs {
|
for range BaseAvatarIDs {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// The three optional PlayerSettingInfo sub-messages are required by the
|
// The three optional PlayerSettingInfo sub-messages are required by the
|
||||||
// 4.6.51 client: leaving them unset NREs inside _CmdGetBasicInfoScRsp.
|
// Client requires these when processing _CmdGetBasicInfoScRsp.
|
||||||
handle("GetBasicInfo", func(s *Session, req *goproto.GetBasicInfoCsReq, rsp *goproto.GetBasicInfoScRsp) error {
|
handle("GetBasicInfo", func(s *Session, req *goproto.GetBasicInfoCsReq, rsp *goproto.GetBasicInfoScRsp) error {
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
rsp.Gender = 2
|
rsp.Gender = 2
|
||||||
@@ -40,6 +40,10 @@ func init() {
|
|||||||
|
|
||||||
var finalData []byte
|
var finalData []byte
|
||||||
finalData = append(finalData, heartbeatLua...)
|
finalData = append(finalData, heartbeatLua...)
|
||||||
|
if probe := s.takeBattleProbe(); len(probe) > 0 {
|
||||||
|
finalData = append(finalData, '\n')
|
||||||
|
finalData = append(finalData, probe...)
|
||||||
|
}
|
||||||
|
|
||||||
// Load all .lua files from scripts/ directory in deterministic order
|
// Load all .lua files from scripts/ directory in deterministic order
|
||||||
if entries, err := os.ReadDir("scripts"); err == nil {
|
if entries, err := os.ReadDir("scripts"); err == nil {
|
||||||
@@ -62,7 +66,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rsp.DownloadData = &goproto.ClientDownloadData{
|
rsp.DownloadData = &goproto.ClientDownloadData{
|
||||||
Version: 51,
|
Version: goproto.ClientDownloadDataLuaVersion,
|
||||||
Time: int64(rsp.ServerTimeMs),
|
Time: int64(rsp.ServerTimeMs),
|
||||||
Data: finalData,
|
Data: finalData,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,94 +39,43 @@ func init() {
|
|||||||
dummy(name)
|
dummy(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- fixed-body raw replies (cmd -> rsp cmd + body bytes) ----
|
// Bind requests by the current command names; do not retain numeric aliases
|
||||||
simpleReply := func(reqIDs []uint16, rspID uint16, body []byte) {
|
// from earlier builds, which may now identify responses or notifications.
|
||||||
for _, id := range reqIDs {
|
rawNamed("GetChallengeGroupStatisticsCsReq", challengeGroupStatsReply)
|
||||||
id := id
|
rawNamed("GetChallengeTierceDataCsReq", challengeTierceReply)
|
||||||
rawHandlers[id] = func(s *Session, payload []byte) error {
|
rawNamed("StartChallengeCsReq", handleStartChallenge)
|
||||||
return s.SendRaw(&NetPacket{CmdType: rspID, Body: body})
|
rawNamed("StartChallengeTierceCsReq", handleStartChallengeTierce)
|
||||||
}
|
rawNamed("SetChallengeTierceLineupCsReq", handleSetChallengeTierceLineup)
|
||||||
}
|
rawNamed("LeaveChallengeCsReq", handleLeaveChallenge)
|
||||||
}
|
rawNamed("LeaveChallengeTierceCsReq", handleLeaveChallengeTierce)
|
||||||
|
rawNamed("TakeChallengeCumulativeRewardCsReq", handleTakeChallengeReward)
|
||||||
|
// The request/response names remain obfuscated in the upstream schema.
|
||||||
|
rawNamed("HIGLJHLNODG", handleExRankUnlock)
|
||||||
|
|
||||||
simpleReply([]uint16{8909}, 8923, []byte{0x60, 0x00}) // tag 12 retcode 0
|
rawNamed("GetSwitchHandDataCsReq", func(s *Session, payload []byte) error {
|
||||||
simpleReply([]uint16{8924, 8968}, 8920, []byte{0x10, 0x00}) // tag 2 retcode 0
|
return s.Send(&goproto.GetSwitchHandDataScRsp{})
|
||||||
simpleReply([]uint16{8935}, 8915, []byte{0x08, 0x00}) // tag 1 retcode 0
|
})
|
||||||
simpleReply([]uint16{8917}, 8929, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{8933}, 8906, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{8919}, 8948, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{8910}, 8921, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{8975}, 8992, []byte{0x40, 0x00}) // tag 8 retcode 0
|
|
||||||
simpleReply([]uint16{8990}, 8977, []byte{0x40, 0x00}) // tag 8 retcode 0
|
|
||||||
simpleReply([]uint16{8977}, 8999, []byte{0x50, 0x00}) // tag 10 retcode 0
|
|
||||||
simpleReply([]uint16{2966}, 2955, []byte{0x30, 0x00}) // tag 6 retcode 0
|
|
||||||
simpleReply([]uint16{2916}, 2906, []byte{0x18, 0x00}) // tag 3 retcode 0
|
|
||||||
simpleReply([]uint16{2919}, 2928, []byte{0x48, 0x00}) // tag 9 retcode 0
|
|
||||||
simpleReply([]uint16{2974}, 2919, []byte{0x18, 0x00}) // GetFriendListInfo tag 3 retcode 0
|
|
||||||
simpleReply([]uint16{740}, 733, []byte{0x58, 0x00}) // tag 11 retcode 0
|
|
||||||
simpleReply([]uint16{1748}, 1745, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{1765}, 1730, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{1742}, 1772, []byte{0x08, 0x00}) // tag 1 retcode 0
|
|
||||||
simpleReply([]uint16{1738}, 1748, nil) // empty body
|
|
||||||
simpleReply([]uint16{188}, 181, []byte{0x50, 0x00}) // tag 10 retcode 0
|
|
||||||
simpleReply([]uint16{4130}, 4124, []byte{0x70, 0x00}) // tag 14 retcode 0
|
|
||||||
simpleReply([]uint16{8110, 8115}, 8108, []byte{0x18, 0x00}) // GetSwitchHandData tag 3 retcode 0 (4.6.51 CmdID: 8108)
|
|
||||||
simpleReply([]uint16{8993, 8991}, 8981, []byte{0x70, 0x00}) // 4.6.51: tag 14 retcode 0
|
|
||||||
simpleReply([]uint16{8987}, 8986, []byte{0x38, 0x00}) // ConfirmChallengeTierceStageSettle tag 7 retcode 0
|
|
||||||
|
|
||||||
// ---- 1775 | 1711 -> GetChallengeGroupStatisticsScRsp (cmd 1708) ----
|
handle("GetChallengeTierceController", func(s *Session, req *goproto.GetChallengeTierceControllerCsReq, rsp *goproto.GetChallengeTierceControllerScRsp) error {
|
||||||
rawHandlers[1775] = challengeGroupStatsReply
|
rsp.Retcode = 0
|
||||||
rawHandlers[1711] = challengeGroupStatsReply
|
return nil
|
||||||
|
})
|
||||||
|
handle("StartChallengePeak", handleStartChallengePeak)
|
||||||
|
rawNamed("LeaveChallengePeakCsReq", func(s *Session, payload []byte) error {
|
||||||
|
return leaveChallengeScene(s, &goproto.LeaveChallengePeakScRsp{})
|
||||||
|
})
|
||||||
|
|
||||||
// ---- 8995 | 8979 | 8981 -> tierce data (cmd 8996 + legacy 8994) ----
|
|
||||||
rawHandlers[8995] = challengeTierceReply
|
|
||||||
rawHandlers[8979] = challengeTierceReply
|
|
||||||
rawHandlers[8981] = challengeTierceReply
|
|
||||||
|
|
||||||
// ---- challenge flow (StartChallenge family) ----
|
|
||||||
rawHandlers[1731] = handleStartChallenge
|
|
||||||
rawHandlers[1793] = handleStartChallenge
|
|
||||||
rawHandlers[8986] = handleStartChallengeTierce
|
|
||||||
rawHandlers[8988] = handleStartChallengeTierce
|
|
||||||
rawHandlers[8978] = handleSetChallengeTierceLineup
|
|
||||||
rawHandlers[9000] = handleSetChallengeTierceLineup
|
|
||||||
rawHandlers[1705] = handleLeaveChallenge
|
|
||||||
rawHandlers[1760] = handleLeaveChallenge
|
|
||||||
rawHandlers[1788] = handleLeaveChallenge
|
|
||||||
rawHandlers[8989] = handleLeaveChallengeTierce
|
|
||||||
rawHandlers[1739] = handleTakeChallengeReward
|
|
||||||
|
|
||||||
// ---- 400 -> Ex-Rank / Affinitive Eidolon Unlock ----
|
|
||||||
rawHandlers[400] = handleExRankUnlock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleExRankUnlock handles CmdID 400 (Affinitive Eidolon / Ex-Rank unlock request from client).
|
// handleExRankUnlock handles CmdID 400 (Affinitive Eidolon / Ex-Rank unlock request from client).
|
||||||
func handleExRankUnlock(s *Session, payload []byte) error {
|
func handleExRankUnlock(s *Session, payload []byte) error {
|
||||||
Log(fmt.Sprintf("[EX-RANK] Received cmd 400 (len: %d) payload: %x", len(payload), payload))
|
Log(fmt.Sprintf("[EX-RANK] Received cmd 400 (len: %d) payload: %x", len(payload), payload))
|
||||||
|
|
||||||
var parsedVal uint32
|
var req goproto.HIGLJHLNODG
|
||||||
buf := payload
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
||||||
for len(buf) > 0 {
|
return err
|
||||||
tag, n := readUvarint(buf)
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n:]
|
|
||||||
wireType := tag & 0x7
|
|
||||||
fieldNum := tag >> 3
|
|
||||||
if wireType == 0 {
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 > 0 {
|
|
||||||
buf = buf[n2:]
|
|
||||||
Log(fmt.Sprintf("[EX-RANK] field %d = %d", fieldNum, v))
|
|
||||||
if v >= 100000 || (v >= 1000 && v < 10000 && parsedVal == 0) {
|
|
||||||
parsedVal = uint32(v)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skipWireField(uint32(wireType), &buf)
|
|
||||||
}
|
}
|
||||||
|
parsedVal := req.OJFIGABCKLG
|
||||||
|
|
||||||
var avatarID uint32
|
var avatarID uint32
|
||||||
if parsedVal >= 100000 {
|
if parsedVal >= 100000 {
|
||||||
@@ -162,99 +111,23 @@ func handleExRankUnlock(s *Session, payload []byte) error {
|
|||||||
player.SavePersistent()
|
player.SavePersistent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send candidate response packets so client RPC callback resolves without spinning
|
// The matching response is declared by the current upstream schema.
|
||||||
// Candidates in CmdAvatarType: 395 (CmdAvatarType_ENKEIJOLCNO), 354, 355, 364, 341
|
return s.Send(&goproto.LPOLGDLAADM{})
|
||||||
for _, rspID := range []uint16{395, 354, 355, 364, 341} {
|
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: rspID, Body: []byte{}})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// challengeGroupStatsReply handles CmdGetChallengeGroupStatisticsCsReq (1775/1711)
|
// challengeGroupStatsReply uses the current typed statistics response.
|
||||||
// and returns GetChallengeGroupStatisticsScRsp (1708) using standard 4.6.51 proto.
|
|
||||||
func challengeGroupStatsReply(s *Session, payload []byte) error {
|
func challengeGroupStatsReply(s *Session, payload []byte) error {
|
||||||
groupID := uint32(100)
|
|
||||||
var req goproto.GetChallengeGroupStatisticsCsReq
|
var req goproto.GetChallengeGroupStatisticsCsReq
|
||||||
if err := proto.Unmarshal(payload, &req); err == nil && req.GroupId != 0 {
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
||||||
groupID = req.GroupId
|
|
||||||
} else {
|
|
||||||
buf := payload
|
|
||||||
for len(buf) > 0 {
|
|
||||||
tag, n := readUvarint(buf)
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf = buf[n:]
|
|
||||||
fieldNumber := tag >> 3
|
|
||||||
wireType := tag & 0x7
|
|
||||||
if (fieldNumber == 5 || fieldNumber == 12) && wireType == 0 {
|
|
||||||
v, n2 := readUvarint(buf)
|
|
||||||
if n2 > 0 {
|
|
||||||
groupID = uint32(v)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skipWireField(uint32(wireType), &buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rsp := &goproto.GetChallengeGroupStatisticsScRsp{
|
|
||||||
GroupId: groupID,
|
|
||||||
Retcode: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case groupID >= 3000:
|
|
||||||
// Apocalyptic Shadow: challenge_boss (field 1)
|
|
||||||
rsp.ChallengeBoss = &goproto.ChallengeBossStatistics{
|
|
||||||
RecordId: 1,
|
|
||||||
PPBHLLOJNEK: &goproto.AANLJBLOOFO{
|
|
||||||
Level: 4,
|
|
||||||
ScoreId: 8000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case groupID >= 2000 && groupID < 3000:
|
|
||||||
// Pure Fiction: challenge_story (field 3)
|
|
||||||
rsp.ChallengeStory = &goproto.ChallengeStoryStatistics{
|
|
||||||
RecordId: 1,
|
|
||||||
PPBHLLOJNEK: &goproto.EIKPHEMHIOH{
|
|
||||||
ScoreId: 80000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case groupID >= 1000 && groupID < 2000:
|
|
||||||
// Memory of Chaos: challenge_default (field 11)
|
|
||||||
rsp.ChallengeDefault = &goproto.ChallengeStatistics{
|
|
||||||
RecordId: 1,
|
|
||||||
PPBHLLOJNEK: &goproto.ADKJKMKBFDC{
|
|
||||||
RoundCount: 20,
|
|
||||||
Level: 12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := proto.Marshal(rsp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.SendRaw(&NetPacket{CmdType: 1708, Body: body})
|
// Match upstream: echo the group without fabricating historical scores.
|
||||||
|
return s.Send(&goproto.GetChallengeGroupStatisticsScRsp{GroupId: req.GroupId})
|
||||||
}
|
}
|
||||||
|
|
||||||
// challengeTierceReply ports the 8995/8979/8981 arm: cmd 8996 (4.6.51) plus
|
// challengeTierceReply sends only the response paired with the current request.
|
||||||
// legacy cmd 8994 with the same tierce body.
|
|
||||||
func challengeTierceReply(s *Session, payload []byte) error {
|
func challengeTierceReply(s *Session, payload []byte) error {
|
||||||
body := buildGetChallengeTierceDataScRsp()
|
return s.Send(buildGetChallengeTierceDataScRspReal())
|
||||||
if err := s.SendRaw(&NetPacket{CmdType: 8996, Body: body}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = s.SendRaw(&NetPacket{CmdType: 8994, Body: body})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildGetChallengeTierceDataScRsp mirrors challenge::build_get_challenge_tierce_data_sc_rsp
|
|
||||||
// (hand-built protobuf, verified against the 4.6.51 client).
|
|
||||||
func buildGetChallengeTierceDataScRsp() []byte {
|
|
||||||
return buildGetChallengeTierceDataScRspReal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readUvarint(b []byte) (uint64, int) {
|
func readUvarint(b []byte) (uint64, int) {
|
||||||
|
|||||||
@@ -52,6 +52,57 @@ func firstSceneOfEntry(res *common.JsonConfig, entryID uint32) (string, *common.
|
|||||||
return "", nil, false
|
return "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sceneOfEntryForTeleport selects the floor config that actually owns the
|
||||||
|
// requested teleport. Some resource sets put several floors under one entry;
|
||||||
|
// choosing the lexicographically first config can then send a valid scene
|
||||||
|
// transition with the wrong floor and spawn position.
|
||||||
|
func sceneOfEntryForTeleport(res *common.JsonConfig, entryID, floorID, teleportID uint32) (string, *common.LevelOutputConfig, bool) {
|
||||||
|
configs := res.LevelOutputConfigs[entryID]
|
||||||
|
names := make([]string, 0, len(configs))
|
||||||
|
for name := range configs {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
if floorID != 0 {
|
||||||
|
want := fmt.Sprintf("_F%d", floorID)
|
||||||
|
for _, name := range names {
|
||||||
|
cfg := configs[name]
|
||||||
|
if len(name) > len(want) && name[len(name)-len(want):] == want && sceneHasTeleport(cfg, teleportID) {
|
||||||
|
return name, cfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if teleportID != 0 {
|
||||||
|
for _, name := range names {
|
||||||
|
cfg := configs[name]
|
||||||
|
if sceneHasTeleport(cfg, teleportID) {
|
||||||
|
return name, cfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if floorID != 0 {
|
||||||
|
want := fmt.Sprintf("_F%d", floorID)
|
||||||
|
for _, name := range names {
|
||||||
|
if len(name) > len(want) && name[len(name)-len(want):] == want {
|
||||||
|
return name, configs[name], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstSceneOfEntry(res, entryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneHasTeleport(cfg *common.LevelOutputConfig, teleportID uint32) bool {
|
||||||
|
if cfg == nil || teleportID == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, group := range cfg.Scenes {
|
||||||
|
if _, ok := group.Teleports[teleportID]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// parsePlaneFloor splits "P<plane>_F<floor>".
|
// parsePlaneFloor splits "P<plane>_F<floor>".
|
||||||
func parsePlaneFloor(name string) (uint32, uint32, error) {
|
func parsePlaneFloor(name string) (uint32, uint32, error) {
|
||||||
if len(name) < 2 || name[0] != 'P' {
|
if len(name) < 2 || name[0] != 'P' {
|
||||||
@@ -108,7 +159,7 @@ func init() {
|
|||||||
defaultScene.FloorId = player.Scene.FloorID
|
defaultScene.FloorId = player.Scene.FloorID
|
||||||
}
|
}
|
||||||
|
|
||||||
scene, err := loadScene(s, entryID, false, nil, nil)
|
scene, err := loadScene(s, entryID, false, false, nil, nil)
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rsp.Scene = scene
|
rsp.Scene = scene
|
||||||
@@ -120,7 +171,7 @@ func init() {
|
|||||||
|
|
||||||
handle("EnterScene", func(s *Session, req *goproto.EnterSceneCsReq, rsp *goproto.EnterSceneScRsp) error {
|
handle("EnterScene", func(s *Session, req *goproto.EnterSceneCsReq, rsp *goproto.EnterSceneScRsp) error {
|
||||||
Log(fmt.Sprintf("on_enter_scene_cs_req: entry_id=%d, teleport_id=%d, interact_id=%d", req.EntryId, req.TeleportId, req.InteractId))
|
Log(fmt.Sprintf("on_enter_scene_cs_req: entry_id=%d, teleport_id=%d, interact_id=%d", req.EntryId, req.TeleportId, req.InteractId))
|
||||||
SceneDebugLog("ENTER_SCENE entry_id=%d teleport_id=%d interact_id=%d", req.EntryId, req.TeleportId, req.InteractId)
|
SceneDebugLog("ENTER_SCENE entry_id=%d teleport_id=%d interact_id=%d scene_identifier=%v", req.EntryId, req.TeleportId, req.InteractId, req.SceneIdentifier)
|
||||||
|
|
||||||
res, err := common.GameRes()
|
res, err := common.GameRes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -222,7 +273,7 @@ func init() {
|
|||||||
teleportID = &v
|
teleportID = &v
|
||||||
}
|
}
|
||||||
|
|
||||||
sceneInfo, err := loadScene(s, resolvedEntry, true, teleportID, sceneIdent)
|
sceneInfo, err := loadScene(s, resolvedEntry, true, false, teleportID, sceneIdent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SceneDebugLog("ENTER_SCENE_FAILED requested_entry=%d resolved_entry=%d err=%v", req.EntryId, resolvedEntry, err)
|
SceneDebugLog("ENTER_SCENE_FAILED requested_entry=%d resolved_entry=%d err=%v", req.EntryId, resolvedEntry, err)
|
||||||
Log(fmt.Sprintf("Failed to enter scene %d: %v", resolvedEntry, err))
|
Log(fmt.Sprintf("Failed to enter scene %d: %v", resolvedEntry, err))
|
||||||
@@ -230,11 +281,21 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
SceneDebugLog("ENTER_SCENE_RESOLVED requested_entry=%d resolved_entry=%d", req.EntryId, resolvedEntry)
|
SceneDebugLog("ENTER_SCENE_RESOLVED requested_entry=%d resolved_entry=%d floor_id=%d plane_id=%d teleport_id=%d", req.EntryId, resolvedEntry, sceneInfo.FloorId, sceneInfo.PlaneId, teleportIDValue(teleportID))
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
rsp.SceneIdentifier = sceneInfo.SceneIdentifier
|
rsp.SceneIdentifier = sceneInfo.SceneIdentifier
|
||||||
|
rsp.IsOverMap = true
|
||||||
rsp.IsCloseMap = req.IsCloseMap
|
rsp.IsCloseMap = req.IsCloseMap
|
||||||
return nil
|
if err := s.Send(rsp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.Send(&goproto.EnterSceneByServerScNotify{
|
||||||
|
Scene: sceneInfo,
|
||||||
|
Lineup: common.ToLineupInfo(player.Lineups),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errResponseAlreadySent
|
||||||
})
|
})
|
||||||
|
|
||||||
handle("InteractProp", func(s *Session, req *goproto.InteractPropCsReq, rsp *goproto.InteractPropScRsp) error {
|
handle("InteractProp", func(s *Session, req *goproto.InteractPropCsReq, rsp *goproto.InteractPropScRsp) error {
|
||||||
@@ -268,14 +329,14 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// decodeSceneIdentifiersLenient: the 4.6.51 client's cmd-1461 payload can
|
// decodeSceneIdentifiersLenient: some Client scene-map payloads can
|
||||||
// fail strict proto parsing (field layout drift between builds) — walk the
|
// fail strict proto parsing (field layout drift between builds) — walk the
|
||||||
// wire format manually and pull floor_id/content_id out of field 2.
|
// wire format manually and pull floor_id/content_id out of field 2.
|
||||||
// GetSceneMapInfo as a raw handler: the 4.6.51 client's cmd-1461 payload
|
// GetSceneMapInfo as a raw handler: some Client scene-map payloads
|
||||||
// can fail strict proto parsing (layout drift), so fall back to a manual
|
// can fail strict proto parsing (layout drift), so fall back to a manual
|
||||||
// wire walk instead of dropping the request (the map UI stalls without
|
// wire walk instead of dropping the request (the map UI stalls without
|
||||||
// this reply - no pillars, "navigation loading").
|
// this reply - no pillars, "navigation loading").
|
||||||
rawHandlers[1461] = func(s *Session, payload []byte) error {
|
rawNamed("GetSceneMapInfoCsReq", func(s *Session, payload []byte) error {
|
||||||
res, err := common.GameRes()
|
res, err := common.GameRes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -352,7 +413,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return s.Send(rsp)
|
return s.Send(rsp)
|
||||||
}
|
})
|
||||||
|
|
||||||
handle("SceneEntityMove", func(s *Session, req *goproto.SceneEntityMoveCsReq, rsp *goproto.SceneEntityMoveScRsp) error {
|
handle("SceneEntityMove", func(s *Session, req *goproto.SceneEntityMoveCsReq, rsp *goproto.SceneEntityMoveScRsp) error {
|
||||||
player := s.JSON()
|
player := s.JSON()
|
||||||
@@ -696,9 +757,9 @@ func sortedU32Keys2[V any](m map[uint32]V) []uint32 {
|
|||||||
|
|
||||||
func sortedU32Keys[V any](m map[uint32]V) []uint32 { return sortedU32Keys2(m) }
|
func sortedU32Keys[V any](m map[uint32]V) []uint32 { return sortedU32Keys2(m) }
|
||||||
|
|
||||||
// loadScene is the Go port of load_scene. When isEnterScene is true it also
|
// loadScene is the Go port of load_scene. When isEnterScene is true it updates
|
||||||
// sends EnterSceneByServerScNotify and persists the new position.
|
// the player's scene; sendNotify controls whether it also emits the notify.
|
||||||
func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32, reqSceneIdent *goproto.SceneIdentifier) (*goproto.SceneInfo, error) {
|
func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, teleportID *uint32, reqSceneIdent *goproto.SceneIdentifier) (*goproto.SceneInfo, error) {
|
||||||
player := s.JSON()
|
player := s.JSON()
|
||||||
if player == nil {
|
if player == nil {
|
||||||
return nil, fmt.Errorf("data is not set!")
|
return nil, fmt.Errorf("data is not set!")
|
||||||
@@ -708,7 +769,15 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
name, scene, ok := firstSceneOfEntry(res, entryID)
|
floorHint := uint32(0)
|
||||||
|
if reqSceneIdent != nil {
|
||||||
|
floorHint = reqSceneIdent.FloorId
|
||||||
|
}
|
||||||
|
requestedTeleport := uint32(0)
|
||||||
|
if teleportID != nil {
|
||||||
|
requestedTeleport = *teleportID
|
||||||
|
}
|
||||||
|
name, scene, ok := sceneOfEntryForTeleport(res, entryID, floorHint, requestedTeleport)
|
||||||
if !ok {
|
if !ok {
|
||||||
Log(fmt.Sprintf("Map Entrance Not Found %d", entryID))
|
Log(fmt.Sprintf("Map Entrance Not Found %d", entryID))
|
||||||
return nil, fmt.Errorf("Map Entrance Not Found %d", entryID)
|
return nil, fmt.Errorf("Map Entrance Not Found %d", entryID)
|
||||||
@@ -750,6 +819,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SceneDebugLog("LOAD_SCENE_TELEPORT entry_id=%d config=%s floor_id=%d teleport_id=%d matched=%t pos=(%d,%d,%d)", entryID, name, floorID, *teleportID, found, jsonPos.X, jsonPos.Y, jsonPos.Z)
|
||||||
} else if isEnterScene || jsonPos.Y < -5000 {
|
} else if isEnterScene || jsonPos.Y < -5000 {
|
||||||
// first teleport in the scene, else the teleport DB, else spawn near the first prop
|
// first teleport in the scene, else the teleport DB, else spawn near the first prop
|
||||||
var firstTP *common.TeleportInfo
|
var firstTP *common.TeleportInfo
|
||||||
@@ -861,7 +931,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
|
|||||||
|
|
||||||
for _, groupID := range sortedU32Keys2(scene.Scenes) {
|
for _, groupID := range sortedU32Keys2(scene.Scenes) {
|
||||||
group := scene.Scenes[groupID]
|
group := scene.Scenes[groupID]
|
||||||
groupInfo := &goproto.SceneEntityGroupInfo{GroupId: groupID}
|
groupInfo := &goproto.SceneEntityGroupInfo{State: 1, GroupId: groupID}
|
||||||
|
|
||||||
// props
|
// props
|
||||||
for i := range group.Props {
|
for i := range group.Props {
|
||||||
@@ -924,6 +994,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sceneInfo.EntityList = append(sceneInfo.EntityList, groupInfo.EntityList...)
|
||||||
if len(group.Props) == 0 && len(group.Npcs) == 0 && len(group.Monsters) == 0 {
|
if len(group.Props) == 0 && len(group.Npcs) == 0 && len(group.Monsters) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -959,6 +1030,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
|
|||||||
player.Scene.PlaneID = planeID
|
player.Scene.PlaneID = planeID
|
||||||
player.Position = jsonPos
|
player.Position = jsonPos
|
||||||
|
|
||||||
|
if sendNotify {
|
||||||
if err := s.Send(&goproto.EnterSceneByServerScNotify{
|
if err := s.Send(&goproto.EnterSceneByServerScNotify{
|
||||||
Scene: sceneInfo,
|
Scene: sceneInfo,
|
||||||
Lineup: common.ToLineupInfo(player.Lineups),
|
Lineup: common.ToLineupInfo(player.Lineups),
|
||||||
@@ -966,10 +1038,18 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sceneInfo, nil
|
return sceneInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func teleportIDValue(id *uint32) uint32 {
|
||||||
|
if id == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return *id
|
||||||
|
}
|
||||||
|
|
||||||
func hasAvatar(player *common.FreesrData, id uint32) bool {
|
func hasAvatar(player *common.FreesrData, id uint32) bool {
|
||||||
_, ok := player.Avatars[id]
|
_, ok := player.Avatars[id]
|
||||||
return ok
|
return ok
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import (
|
|||||||
// tutorial modal (which locks camera + input).
|
// tutorial modal (which locks camera + input).
|
||||||
func init() {
|
func init() {
|
||||||
handle("GetTutorial", func(s *Session, req *goproto.GetTutorialCsReq, rsp *goproto.GetTutorialScRsp) error {
|
handle("GetTutorial", func(s *Session, req *goproto.GetTutorialCsReq, rsp *goproto.GetTutorialScRsp) error {
|
||||||
|
tutorialIDs, _, err := getTutorialResourceIDs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
for _, id := range tutorialIDs {
|
for _, id := range tutorialIDs {
|
||||||
rsp.TutorialList = append(rsp.TutorialList, &goproto.Tutorial{
|
rsp.TutorialList = append(rsp.TutorialList, &goproto.Tutorial{
|
||||||
@@ -19,6 +23,10 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
handle("GetTutorialGuide", func(s *Session, req *goproto.GetTutorialGuideCsReq, rsp *goproto.GetTutorialGuideScRsp) error {
|
handle("GetTutorialGuide", func(s *Session, req *goproto.GetTutorialGuideCsReq, rsp *goproto.GetTutorialGuideScRsp) error {
|
||||||
|
_, tutorialGuideIDs, err := getTutorialResourceIDs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
for _, id := range tutorialGuideIDs {
|
for _, id := range tutorialGuideIDs {
|
||||||
rsp.TutorialGuideList = append(rsp.TutorialGuideList, &goproto.TutorialGuide{
|
rsp.TutorialGuideList = append(rsp.TutorialGuideList, &goproto.TutorialGuide{
|
||||||
|
|||||||
@@ -1,115 +1,75 @@
|
|||||||
package gameserver
|
package gameserver
|
||||||
|
|
||||||
// Generated from the Rust tutorial.rs lists (client 4.6.51).
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
var tutorialIDs = []uint32{
|
// Tutorial IDs come from the installed Client resource tables, not a list
|
||||||
1001, 1002, 1003, 1004, 1005, 1007, 1008, 1010, 2000, 2001, 2002, 2003, 2004, 2008, 2010, 2011, 2012, 2013, 3001, 3002,
|
// copied from an older server build. Restart after replacing these resources.
|
||||||
3003, 3004, 3005, 3006, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3202, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009,
|
var tutorialResourceCache struct {
|
||||||
4010, 4011, 4013, 4014, 4015, 4017, 4018, 4019, 4020, 4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031,
|
sync.Once
|
||||||
4032, 4033, 4034, 4035, 4036, 4037, 4038, 4039, 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4048, 4049, 4050, 4051,
|
tutorials []uint32
|
||||||
4052, 4053, 4054, 4055, 4056, 4057, 4058, 4059, 4060, 4061, 4063, 4066, 4067, 4068, 4070, 4071, 4072, 4073, 4074, 4075,
|
guides []uint32
|
||||||
4076, 4077, 4078, 4079, 4081, 4083, 4084, 4085, 4097, 4098, 4112, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009,
|
err error
|
||||||
5010, 5011, 5012, 5013, 5014, 5015, 5016, 5017, 5018, 5019, 5021, 5022, 5023, 5024, 5025, 5026, 5027, 5028, 5029, 5030,
|
|
||||||
5031, 5032, 5033, 5034, 5035, 5036, 5038, 5039, 5041, 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054,
|
|
||||||
5055, 5056, 5057, 5058, 5059, 5060, 5061, 5062, 5063, 5064, 5065, 5070, 5071, 5072, 5073, 5074, 5075, 5077, 5078, 5079,
|
|
||||||
5081, 5082, 5083, 5084, 5085, 5086, 5087, 5088, 5089, 5090, 5091, 5092, 5093, 5094, 5095, 5096, 5097, 5098, 5099, 5100,
|
|
||||||
5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108, 5109, 5110, 5111, 5112, 5113, 5114, 5115, 5116, 5130, 5131, 5132, 5133,
|
|
||||||
5134, 5135, 5140, 5141, 5142, 5143, 5144, 5145, 5146, 5150, 5151, 5152, 5153, 5154, 5155, 5156, 5157, 5158, 5159, 5160,
|
|
||||||
5161, 5162, 5163, 5164, 5172, 5173, 5174, 5175, 5176, 5177, 5178, 5179, 5180, 5181, 5182, 5183, 5184, 5185, 5186, 5187,
|
|
||||||
5188, 5189, 5190, 5191, 5192, 5193, 5194, 5195, 5196, 5197, 5198, 5199, 5200, 5201, 5202, 5210, 5211, 5212, 5213, 5214,
|
|
||||||
5215, 5216, 5217, 5218, 5219, 5220, 5221, 5222, 5301, 5302, 5303, 5304, 5305, 5306, 5307, 5308, 5309, 5310, 5311, 5312,
|
|
||||||
5313, 5314, 5316, 5317, 5318, 5319, 5320, 5321, 5322, 5323, 5324, 5325, 5326, 5350, 5351, 5352, 5355, 5356, 5357, 5358,
|
|
||||||
5359, 5360, 5361, 5365, 5366, 5367, 5368, 5369, 5370, 5371, 5372, 5373, 5374, 5375, 5376, 5377, 5378, 5380, 5381, 5382,
|
|
||||||
5383, 5384, 5385, 5386, 5387, 5388, 5389, 5390, 5391, 5392, 5393, 5394, 5395, 5397, 5398, 5399, 5400, 5401, 5402, 5403,
|
|
||||||
5404, 5405, 5406, 5407, 5408, 5409, 5410, 5450, 5451, 5452, 5453, 5454, 5455, 5456, 5457, 5458, 5459, 5460, 5461, 5462,
|
|
||||||
5463, 5464, 5465, 5510, 5511, 5512, 5513, 5514, 5515, 5517, 5518, 5520, 5521, 5522, 5523, 5524, 5525, 5530, 5531, 5532,
|
|
||||||
5533, 5534, 5535, 5536, 5537, 5540, 5550, 5551, 5552, 5553, 5554, 5555, 5556, 5560, 5561, 5562, 5563, 5564, 5565, 5566,
|
|
||||||
5567, 5568, 5569, 5600, 5601, 5602, 5603, 5604, 5605, 5606, 5607, 5608, 5609, 5610, 5611, 5612, 5613, 5614, 5615, 5700,
|
|
||||||
5701, 5702, 5703, 5704, 5705, 5706, 5707, 5708, 5709, 5710, 5711, 5712, 5713, 5714, 5715, 5716, 5717, 5718, 5750, 5751,
|
|
||||||
5752, 5753, 5754, 5755, 5756, 5757, 5758, 5759, 5760, 5761, 5762, 5763, 5764, 5765, 5766, 5767, 5768, 5769, 5770, 5771,
|
|
||||||
5772, 5773, 5774, 5775, 5801, 5802, 5803, 5804, 5805, 5806, 5810, 5811, 5812, 5813, 5814, 5815, 5816, 5817, 5901, 5902,
|
|
||||||
5903, 5904, 5905, 5906, 5907, 5908, 5909, 5910, 5911, 5912, 5913, 5914, 5915, 5916, 5917, 5918, 5921, 5922, 5923, 5924,
|
|
||||||
5925, 5926, 5927, 5928, 5929, 5930, 5931, 5933, 5934, 5936, 5937, 5938, 5939, 5940, 5950, 5961, 5962, 5963, 5964, 5965,
|
|
||||||
5966, 5967, 5968, 5969, 5970, 5971, 5972, 5973, 5974, 5975, 5976, 5977, 5978, 5979, 5980, 6000, 6001, 6002, 6003, 6004,
|
|
||||||
6005, 6006, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014, 6015, 6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024,
|
|
||||||
6025, 6026, 6027, 6028, 6029, 6030, 6031, 6050, 6051, 6052, 6053, 6054, 6055, 6100, 6102, 6103, 6104, 6105, 6106, 6107,
|
|
||||||
6108, 6109, 6110, 6111, 6112, 6113, 6114, 6115, 6116, 6117, 6118, 6119, 6120, 6121, 6122, 6123, 6124, 6125, 6126, 6127,
|
|
||||||
6128, 6129, 6130, 6131, 6132, 6133, 6134, 6135, 6136, 6137, 6138, 6139, 6141, 6142, 6193, 6194, 6195, 6196, 6197, 6198,
|
|
||||||
6199, 6200, 6300, 6301, 6302, 6303, 6304, 6305, 6306, 6307, 6308, 6309, 6310, 6311, 6312, 6313, 6314, 6315, 6316, 6317,
|
|
||||||
6318, 6319, 6320, 6321, 6322, 6323, 6324, 6325, 6326, 6327, 6328, 6329, 6330, 6331, 6332, 6333, 6334, 6335, 6336, 6337,
|
|
||||||
6338, 6339, 6340, 6341, 6342, 6343, 6400, 6401, 6402, 6403, 6404, 6405, 6406, 6408, 6409, 6410, 6411, 6412, 6450, 6451,
|
|
||||||
6452, 6453, 6454, 6455, 6456, 6500, 6501, 6503, 6505, 6506, 6507, 6508, 6509, 6510, 6511, 6512, 6513, 6514, 6515, 6516,
|
|
||||||
6517, 6518, 6519, 6601, 6602, 6603, 6604, 6605, 6606, 6607, 6608, 6609, 6610, 6611, 6612, 6613, 6614, 6700, 6701, 6702,
|
|
||||||
6703, 6704, 6705, 6706, 6707, 6708, 6709, 6710, 6711, 6712, 6713, 6714, 6715, 6716, 6717, 6718, 6719, 6750, 6751, 6752,
|
|
||||||
6753, 6754, 6755, 6756, 6757, 6758, 6759, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013,
|
|
||||||
7014, 7015, 7016, 7017, 7023, 7024, 7025, 7026, 7027, 7028, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 8001, 8002, 8003,
|
|
||||||
8005, 8006, 8007, 8009, 8012, 8013, 8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8101, 8102, 8103, 8104, 8105, 8106,
|
|
||||||
8107, 8108, 8109, 8110, 8111, 8112, 8113, 8114, 8115, 8116, 8117, 8118, 8119, 8120, 8121, 8122, 8123, 8124, 8125, 8126,
|
|
||||||
8127, 8128, 8129, 8130, 8201, 8202, 8203, 8204, 8205, 8206, 8301, 8302, 8303, 8304, 8305, 8306, 8307, 8308, 8309, 8310,
|
|
||||||
8311, 8312, 8313, 8314, 8315, 8316, 8317, 8318, 8319, 8320, 8321, 8322, 8323, 8324, 8325, 8326, 8327, 8328, 8401, 8404,
|
|
||||||
8405, 8406, 8501, 8502, 8503, 8504, 8505, 8506, 8508, 8509, 8601, 8602, 8603, 8604, 8605, 8606, 8607, 8608, 8609, 8611,
|
|
||||||
8612, 8613, 8614, 8621, 8625, 8626, 8630, 8631, 8632, 8633, 8634, 8635, 8636, 8637, 8638, 8639, 8640, 8641, 8700, 8701,
|
|
||||||
8702, 8703, 8704, 8705, 8706, 8707, 8708, 8709, 8710, 8711, 8712, 8713, 8714, 8715, 8716, 8717, 8718, 8719, 8720, 8721,
|
|
||||||
8722, 8723, 8724, 8725, 8802, 8803, 8806, 8807, 8808, 8809, 8811, 8812, 8813, 8905, 8906, 8907, 8908, 8909, 8910, 8911,
|
|
||||||
8931, 8932, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010, 9011, 9012, 9013, 9014, 9015, 9016, 9017, 9018,
|
|
||||||
9019, 9020, 9021, 9022, 9023, 9024, 9025, 9026, 9027, 9028, 9029, 9030, 9031, 9032, 9033, 9034, 9035, 9036, 9037, 9038,
|
|
||||||
9039, 9040, 9041, 9042, 9043, 9044, 9045, 9046, 9047, 9048, 9049, 9050, 9051, 9052, 9053, 9054, 9055, 9056, 9057, 9058,
|
|
||||||
9059, 9060, 9061, 9062, 9063, 9065, 9066, 9067, 9068, 9070, 9071, 9072, 9073, 9074, 9075, 9076, 9077, 9078, 9079, 9080,
|
|
||||||
9081, 9082, 9083, 9084, 9085, 9086, 9087, 9088, 9090, 9091, 9092, 9093, 9094, 9095, 9096, 9097, 9098, 9099, 9100, 9110,
|
|
||||||
9111, 9112, 9113, 9114, 9115, 9116, 9117, 9118, 9119, 9120, 9121, 9122, 9123, 9124, 9125, 9126, 9127, 9128, 9129, 9130,
|
|
||||||
9131, 9132, 9133, 9134, 9135, 9136, 9137, 9201, 9202, 9203, 9204, 9205, 9206, 9207, 9208, 9209, 9210, 9211, 9212, 9213,
|
|
||||||
9214, 9301, 9302, 9303, 9304, 9305, 9306, 9307, 9308, 9309, 9310, 9311, 9313, 9314, 9315, 9316, 9317, 9318, 9319, 9321,
|
|
||||||
9322, 9323, 9324, 9325, 9401, 9402, 9403, 9404, 9405, 9501, 9502, 9503, 9504, 9505, 9506, 9507, 9508, 9509, 9510, 9511,
|
|
||||||
9512, 9513, 9514, 9515, 9598, 9599, 9610, 9611, 9612, 9613, 9614, 9615, 9616, 9617, 9618, 9619, 9620, 9621, 9622, 9623,
|
|
||||||
9624, 9625, 9626, 9627, 9628, 9629, 9630, 9631, 9650, 9651, 9701, 9702, 9703, 9704, 9801, 9802, 9803, 9804, 9805, 9806,
|
|
||||||
9807, 9808, 9809, 9810, 9811, 9812, 9813, 9814, 9815, 9816, 9817, 9818, 9819, 9820, 9901, 9903, 9904, 9905, 9906, 9908,
|
|
||||||
10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009, 10031, 10032, 10033, 10034, 10036, 10037, 10038, 10039, 10040, 10041, 10042,
|
|
||||||
10043, 10045, 43000, 43001, 43002, 43003, 43004, 43005, 43006, 43007, 43008, 43009, 43010, 43011, 44001, 44002, 44003, 44004, 44005, 44006,
|
|
||||||
44007, 44008, 44009, 44010, 44011, 44012, 44013, 44014, 45001,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tutorialGuideIDs = []uint32{
|
func getTutorialResourceIDs() ([]uint32, []uint32, error) {
|
||||||
1101, 1102, 1103, 1104, 1105, 1108, 1109, 1110, 1116, 1117, 1118, 1119, 2006, 2007, 2008, 2105, 2106, 2107, 2200, 2201,
|
tutorialResourceCache.Do(func() {
|
||||||
2202, 2203, 2204, 2205, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2218, 2219, 2220, 2221,
|
var tutorialFile struct {
|
||||||
2222, 2223, 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, 2232, 2233, 2234, 2235, 2236, 2237, 2238, 2242, 2243, 2244,
|
Entries []struct {
|
||||||
2245, 2246, 2247, 2249, 2251, 2252, 2253, 2254, 2260, 3007, 3105, 3106, 3107, 3108, 3109, 3201, 3202, 3203, 3204, 3205,
|
ID uint32 `json:"TutorialID"`
|
||||||
3206, 3207, 3208, 3209, 3210, 3211, 3212, 3213, 3214, 3215, 3216, 3217, 3218, 3219, 3221, 3222, 3223, 3225, 3227, 3228,
|
} `json:"tutorial_config"`
|
||||||
3229, 3230, 3231, 3232, 4001, 4101, 4102, 4103, 4104, 4105, 4106, 4107, 4109, 4110, 5101, 5102, 5103, 5104, 5105, 5106,
|
}
|
||||||
6001, 6002, 6003, 6004, 6008, 6009, 6010, 6011, 6012, 6014, 6015, 6018, 6020, 6021, 6023, 6024, 6025, 6026, 6027, 6028,
|
var guideFile struct {
|
||||||
6029, 6030, 6031, 6032, 6033, 6034, 6035, 6036, 6037, 6038, 6039, 6040, 6041, 6042, 6043, 6044, 6045, 6046, 6047, 6048,
|
Entries []struct {
|
||||||
6049, 6050, 6051, 6052, 6053, 6054, 6055, 6056, 6057, 6058, 6059, 6060, 6061, 6062, 6063, 6064, 6065, 6066, 6067, 6068,
|
ID uint32 `json:"GroupID"`
|
||||||
6069, 6070, 6071, 6072, 6073, 6074, 6075, 6076, 6077, 6078, 6079, 6080, 6081, 6082, 6083, 6085, 6086, 6087, 6088, 6089,
|
} `json:"tutorial_guide_config"`
|
||||||
6090, 6091, 6092, 6093, 6094, 6095, 6096, 6097, 6098, 6099, 6100, 6101, 6102, 6103, 6104, 6105, 6106, 6107, 6108, 6109,
|
}
|
||||||
6110, 6111, 6112, 6113, 6114, 6116, 6117, 6118, 6119, 6120, 6122, 6124, 6125, 6126, 6127, 6128, 6129, 6130, 6301, 6302,
|
load := func(name string, dst any) error {
|
||||||
6303, 6304, 6305, 6306, 6307, 6308, 6309, 6310, 6311, 6312, 6314, 6315, 6316, 6317, 6318, 6319, 6320, 6321, 6322, 6323,
|
path := filepath.Join("resources", name)
|
||||||
6324, 6325, 6326, 6327, 6328, 6329, 6330, 6331, 6332, 6333, 6334, 6335, 6336, 6337, 6338, 6339, 6340, 6341, 6342, 6343,
|
data, err := os.ReadFile(path)
|
||||||
6345, 6346, 6347, 6348, 6349, 6350, 6351, 6352, 6354, 6355, 6356, 6357, 6358, 6359, 6360, 6361, 6362, 6363, 6364, 6365,
|
if err != nil {
|
||||||
6366, 6367, 6368, 6369, 6371, 6372, 6374, 6375, 6376, 6377, 6378, 6379, 6380, 7090, 7091, 7092, 7100, 7101, 7501, 7502,
|
return fmt.Errorf("tutorial resource %s: %w", path, err)
|
||||||
7503, 7504, 7506, 7507, 7508, 7509, 7511, 7514, 7515, 8001, 8002, 8003, 8004, 8006, 8007, 8008, 8010, 8011, 8012, 8013,
|
}
|
||||||
8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025, 8026, 8027, 8028, 8038, 8039, 8047, 8050, 8051,
|
if err := json.Unmarshal(data, dst); err != nil {
|
||||||
8052, 8055, 8056, 8057, 8058, 8059, 8061, 8062, 8063, 8064, 8065, 8066, 8067, 8069, 8070, 8072, 8073, 8074, 8075, 8076,
|
return fmt.Errorf("tutorial resource %s: %w", path, err)
|
||||||
8078, 8079, 8080, 8090, 8091, 8092, 8093, 8094, 8095, 8096, 8100, 8101, 8102, 8103, 8104, 8105, 8106, 8107, 8108, 8109,
|
}
|
||||||
8110, 8111, 8112, 8113, 8122, 8123, 8124, 8130, 8131, 8132, 8133, 8134, 8135, 8136, 8137, 8138, 8139, 8140, 8141, 8142,
|
return nil
|
||||||
8143, 8144, 8145, 8146, 8147, 8148, 8149, 8150, 8151, 8152, 8153, 8154, 8155, 8156, 8157, 8158, 8160, 8161, 8162, 8163,
|
}
|
||||||
8164, 8165, 8166, 8167, 8168, 8170, 8171, 8172, 8173, 8174, 8175, 8176, 8180, 8181, 8182, 8183, 8184, 8185, 8186, 8187,
|
if err := load("TutorialData.json", &tutorialFile); err != nil {
|
||||||
8188, 8189, 8190, 8191, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205, 8206, 8207,
|
tutorialResourceCache.err = err
|
||||||
8208, 8209, 8211, 8212, 8213, 8301, 8302, 8303, 8304, 8305, 8306, 8310, 8340, 8341, 8342, 8343, 8344, 8345, 8346, 8701,
|
return
|
||||||
8702, 8703, 8704, 8802, 8803, 8804, 8805, 9101, 9102, 9103, 9104, 9105, 9107, 9108, 9109, 9110, 9111, 9112, 9113, 9114,
|
}
|
||||||
9115, 9116, 9117, 9118, 9119, 9120, 9201, 9202, 9203, 9204, 9205, 9206, 9207, 9208, 9209, 9210, 9211, 9212, 9301, 9303,
|
if err := load("TutorialGuideGroup.json", &guideFile); err != nil {
|
||||||
9304, 9305, 9306, 9308, 9309, 9310, 9312, 9313, 9314, 9315, 9316, 9317, 9318, 9319, 9320, 9321, 9323, 9324, 9325, 9326,
|
tutorialResourceCache.err = err
|
||||||
9327, 9330, 9331, 9332, 9335, 9336, 9337, 9338, 9339, 9340, 9341, 9342, 9343, 9344, 9345, 9601, 9602, 9603, 9604, 9605,
|
return
|
||||||
9701, 9702, 9801, 9802, 9803, 9804, 9811, 9812, 9813, 9814, 9815, 9816, 9817, 9818, 9819, 9820, 9821, 9822, 9823, 9824,
|
}
|
||||||
9825, 9826, 9827, 9829, 9830, 9831, 9832, 9833, 9834, 9835, 9836, 9837, 9838, 9839, 9840, 9841, 9842, 9843, 9844, 9845,
|
collect := func(ids []uint32) []uint32 {
|
||||||
9846, 9847, 9848, 9849, 9850, 9851, 9861, 9862, 9863, 9864, 9871, 9872, 9901, 9902, 9903, 9904, 9905, 9911, 9913, 9915,
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||||
9920, 9921, 9922, 9923, 9925, 9926, 9931, 9932, 9933, 9935, 9936, 9937, 9938, 9941, 9942, 9943, 9944, 9945, 9946, 9947,
|
out := ids[:0]
|
||||||
9951, 9952, 9953, 9954, 9955, 9956, 9957, 9958, 9959, 9960, 9961, 9962, 9963, 9964, 9965, 9967, 9968, 9969, 9970, 9971,
|
for _, id := range ids {
|
||||||
9973, 9974, 9975, 9976, 9978, 9979, 9980, 9981, 9982, 9983, 9984, 9985, 9986, 9987, 9988, 9989, 9990, 9991, 9992, 9993,
|
if id != 0 && (len(out) == 0 || id != out[len(out)-1]) {
|
||||||
9994, 9995, 10000, 10001, 10002, 10003, 10005, 10010, 10011, 10012, 10013, 10014, 10015, 10016, 10017, 10020, 10021, 10022, 10030, 10031,
|
out = append(out, id)
|
||||||
10032, 10033, 10034, 10040, 10041, 10042, 10043, 10044, 10045, 10050, 10051, 10052, 10053, 10054, 10055, 10060, 10061, 10062, 10070, 10071,
|
}
|
||||||
10072, 10073, 10080, 10081, 10082, 10083, 10084, 10085, 10086, 10087, 10088, 10090, 10091, 10092, 10093, 10096, 10100, 10101, 10102, 10103,
|
}
|
||||||
10111, 10112, 10113, 10201, 10202, 10203, 10204, 10310, 10311, 10312, 10313, 10314, 10315, 10401, 10502, 16505, 16506, 16507, 16508, 16509,
|
return out
|
||||||
31001, 31102, 31103, 31105, 31106, 31109, 31204, 31206, 45001, 100001, 100002, 100003, 100004, 100005, 100006, 100007, 100008, 100009, 100010, 100011,
|
}
|
||||||
100012, 100013, 100014, 100015, 100016, 100017, 100021, 100022, 100023, 100024, 100025, 100026, 100027, 100028, 100029, 100030, 100031, 100032, 100033, 100034,
|
for _, entry := range tutorialFile.Entries {
|
||||||
100035, 100036, 100037, 100038, 100039, 100040, 100041, 100042, 100043, 100044, 100045, 100046, 100047, 100048, 100049, 100050, 100051, 100052, 100053, 100054,
|
tutorialResourceCache.tutorials = append(tutorialResourceCache.tutorials, entry.ID)
|
||||||
100055, 100056, 100057, 100058, 100059, 100060, 100061, 100062, 100063, 100064, 100065, 100066, 100067, 100068, 100069,
|
}
|
||||||
|
for _, entry := range guideFile.Entries {
|
||||||
|
tutorialResourceCache.guides = append(tutorialResourceCache.guides, entry.ID)
|
||||||
|
}
|
||||||
|
tutorialResourceCache.tutorials = collect(tutorialResourceCache.tutorials)
|
||||||
|
tutorialResourceCache.guides = collect(tutorialResourceCache.guides)
|
||||||
|
if len(tutorialResourceCache.tutorials) == 0 || len(tutorialResourceCache.guides) == 0 {
|
||||||
|
tutorialResourceCache.err = fmt.Errorf("tutorial resource tables contain no IDs")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tutorialResourceCache.tutorials, tutorialResourceCache.guides, tutorialResourceCache.err
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
goproto "gac-hkrpg-go/protocol/proto"
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handlers for cmds the real 4.6.51 client sends during normal play that were
|
// UI requests bound to the command names in the loaded protocol.
|
||||||
// dropped in the first test session (2026-10-07 01:00) — several of them gate
|
|
||||||
// UI state (pause menu, Endgame entry, map rotation) on the server's reply.
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// client pauses itself when opening menus/map — must be acked
|
// client pauses itself when opening menus/map — must be acked
|
||||||
@@ -16,12 +14,11 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// the Endgame entrance waits for this before opening its UI
|
// The Endgame entrance waits for this response before opening its UI.
|
||||||
// (4.6.51: CsReq 4190 -> ScRsp 4112, retcode = tag 14 — the Rust build
|
handle("GetUnreleasedBlockInfo", func(s *Session, req *goproto.GetUnreleasedBlockInfoCsReq, rsp *goproto.GetUnreleasedBlockInfoScRsp) error {
|
||||||
// answered the wrong id 4130 and the client stalled)
|
rsp.Retcode = 0
|
||||||
rawHandlers[4190] = func(s *Session, payload []byte) error {
|
return nil
|
||||||
return s.SendRaw(&NetPacket{CmdType: 4112, Body: []byte{0x70, 0x00}})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
handle("TakeQuestReward", func(s *Session, req *goproto.TakeQuestRewardCsReq, rsp *goproto.TakeQuestRewardScRsp) error {
|
handle("TakeQuestReward", func(s *Session, req *goproto.TakeQuestRewardCsReq, rsp *goproto.TakeQuestRewardScRsp) error {
|
||||||
rsp.Retcode = 0
|
rsp.Retcode = 0
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type Session struct {
|
|||||||
|
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
nextSceneSave int64
|
nextSceneSave int64
|
||||||
|
battleProbeMu sync.Mutex
|
||||||
|
battleProbe []byte
|
||||||
PropEntities map[uint32]PropEntry
|
PropEntities map[uint32]PropEntry
|
||||||
// MonsterEntities tracks real overworld monster entities (entity_id -> event_id)
|
// MonsterEntities tracks real overworld monster entities (entity_id -> event_id)
|
||||||
// spawned in the current scene. Used by SceneCastSkill to decide whether an
|
// spawned in the current scene. Used by SceneCastSkill to decide whether an
|
||||||
@@ -127,7 +129,7 @@ func (s *Session) Consume(data []byte) error {
|
|||||||
s.kcpMu.Unlock()
|
s.kcpMu.Unlock()
|
||||||
|
|
||||||
for _, pkt := range packets {
|
for _, pkt := range packets {
|
||||||
if pkt.CmdType == 31 { // CmdPlayerLogoutCsReq
|
if pkt.CmdType == goproto.CmdIDByName["PlayerLogoutCsReq"] {
|
||||||
Log("[PACKET] Player logged out")
|
Log("[PACKET] Player logged out")
|
||||||
s.Close()
|
s.Close()
|
||||||
return nil
|
return nil
|
||||||
@@ -156,7 +158,11 @@ func (s *Session) Send(msg proto.Message) error {
|
|||||||
PacketLog(fmt.Sprintf("[PACKET SEND] cmd_id: %d %s (len: %d)", cmdID, name, len(body)))
|
PacketLog(fmt.Sprintf("[PACKET SEND] cmd_id: %d %s (len: %d)", cmdID, name, len(body)))
|
||||||
Log(fmt.Sprintf("sent packet with CmdID: %d (%s)", cmdID, name))
|
Log(fmt.Sprintf("sent packet with CmdID: %d (%s)", cmdID, name))
|
||||||
|
|
||||||
return s.SendRaw(&NetPacket{CmdType: cmdID, Body: body})
|
if err := s.SendRaw(&NetPacket{CmdType: cmdID, Body: body}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.captureBattleResponse(msg, cmdID, body)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendRaw writes an already-framed packet into the KCP stream.
|
// SendRaw writes an already-framed packet into the KCP stream.
|
||||||
|
|||||||
@@ -50,9 +50,14 @@ func apiRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
cfg, err := common.LoadRuntimeConfig()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
writeJSON(w, map[string]any{
|
writeJSON(w, map[string]any{
|
||||||
"name": "GaC-hkrpg-go API Gateway",
|
"name": "GaC-hkrpg-go API Gateway",
|
||||||
"version": "4.6.53",
|
"version": cfg.ClientVersion,
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"description": "Backend connection endpoints for freesr-data, persistent, and GaC-Srtool",
|
"description": "Backend connection endpoints for freesr-data, persistent, and GaC-Srtool",
|
||||||
"endpoints": map[string]string{
|
"endpoints": map[string]string{
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package sdkserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gac-hkrpg-go/internal/common"
|
||||||
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfiguredDispatchAndGateway(t *testing.T) {
|
||||||
|
// Fixture endpoints must appear on the wire instead of any local defaults.
|
||||||
|
cfg := common.RuntimeConfig{PublicSDKURL: "https://sdk.example.invalid/prefix", PublicGameHost: "game.example.invalid", GamePort: 32123}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
queryDispatch(w, httptest.NewRequest("GET", "/query_dispatch", nil), cfg)
|
||||||
|
raw, err := base64.StdEncoding.DecodeString(w.Body.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var dispatch goproto.Dispatch
|
||||||
|
if err := proto.Unmarshal(raw, &dispatch); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(dispatch.RegionList) != 1 || dispatch.RegionList[0].DispatchUrl != cfg.PublicSDKURL+"/query_gateway" {
|
||||||
|
t.Fatalf("wrong dispatch: %v", &dispatch)
|
||||||
|
}
|
||||||
|
state := &appState{runtime: cfg, hotfixMap: map[string]VersionConfig{"fixture": {}}}
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
queryGateway(state)(w, httptest.NewRequest("GET", "/query_gateway?version=fixture", nil))
|
||||||
|
raw, err = base64.StdEncoding.DecodeString(w.Body.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var gateway goproto.Gateserver
|
||||||
|
if err := proto.Unmarshal(raw, &gateway); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if gateway.Ip != cfg.PublicGameHost || gateway.Port != uint32(cfg.GamePort) {
|
||||||
|
t.Fatalf("wrong gateway: %v", &gateway)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package sdkserver is the Go port of the Rust sdkserver: HTTP dispatch +
|
// Package sdkserver is the Go port of the Rust sdkserver: HTTP dispatch +
|
||||||
// auth + SRTools save upload, listening on :21000.
|
// auth + SRTools save upload, using the shared runtime configuration.
|
||||||
package sdkserver
|
package sdkserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,13 +11,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"gac-hkrpg-go/internal/common"
|
||||||
google "google.golang.org/protobuf/proto"
|
google "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
goproto "gac-hkrpg-go/protocol/proto"
|
goproto "gac-hkrpg-go/protocol/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Port = 21000
|
|
||||||
|
|
||||||
// VersionConfig is one entry of versions.json.
|
// VersionConfig is one entry of versions.json.
|
||||||
type VersionConfig struct {
|
type VersionConfig struct {
|
||||||
AssetBundleURL string `json:"asset_bundle_url"`
|
AssetBundleURL string `json:"asset_bundle_url"`
|
||||||
@@ -27,6 +26,7 @@ type VersionConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type appState struct {
|
type appState struct {
|
||||||
|
runtime common.RuntimeConfig
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
hotfixMap map[string]VersionConfig
|
hotfixMap map[string]VersionConfig
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ func fetchHotfix(version, dispatchSeed string) (VersionConfig, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseVersionString extracts region/branch, e.g. "OSBETAWin4.6.51" -> OS,BETA.
|
// parseVersionString extracts region/branch, e.g. a version with the OSBETAWin prefix -> OS,BETA.
|
||||||
func parseVersionString(s string) (string, string) {
|
func parseVersionString(s string) (string, string) {
|
||||||
branches := []string{"PREbeta", "BETA", "PROD", "DEV", "PRE", "GM", "CECREATION"}
|
branches := []string{"PREbeta", "BETA", "PROD", "DEV", "PRE", "GM", "CECREATION"}
|
||||||
if len(s) < 2 {
|
if len(s) < 2 {
|
||||||
@@ -154,14 +154,14 @@ func writeJSON(w http.ResponseWriter, v any) {
|
|||||||
_ = json.NewEncoder(w).Encode(v)
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryDispatch(w http.ResponseWriter, _ *http.Request) {
|
func queryDispatch(w http.ResponseWriter, _ *http.Request, runtime common.RuntimeConfig) {
|
||||||
rsp := &goproto.Dispatch{
|
rsp := &goproto.Dispatch{
|
||||||
Retcode: 0,
|
Retcode: 0,
|
||||||
RegionList: []*goproto.RegionInfo{{
|
RegionList: []*goproto.RegionInfo{{
|
||||||
Name: "GaC hkrpg GO",
|
Name: "GaC hkrpg GO",
|
||||||
Title: "GaC hkrpg GO",
|
Title: "GaC hkrpg GO",
|
||||||
EnvType: "9",
|
EnvType: "9",
|
||||||
DispatchUrl: fmt.Sprintf("http://127.0.0.1:%d/query_gateway", Port),
|
DispatchUrl: runtime.PublicSDKURL + "/query_gateway",
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
bin, err := google.Marshal(rsp)
|
bin, err := google.Marshal(rsp)
|
||||||
@@ -186,8 +186,8 @@ func queryGateway(state *appState) http.HandlerFunc {
|
|||||||
cfg := state.getOrInsertHotfix(version, seed)
|
cfg := state.getOrInsertHotfix(version, seed)
|
||||||
|
|
||||||
rsp := &goproto.Gateserver{
|
rsp := &goproto.Gateserver{
|
||||||
Ip: "127.0.0.1",
|
Ip: state.runtime.PublicGameHost,
|
||||||
Port: 23301,
|
Port: uint32(state.runtime.GamePort),
|
||||||
AssetBundleUrl: cfg.AssetBundleURL,
|
AssetBundleUrl: cfg.AssetBundleURL,
|
||||||
AssetBundleUrlB: cfg.AssetBundleURL,
|
AssetBundleUrlB: cfg.AssetBundleURL,
|
||||||
ExResourceUrl: cfg.ExResourceURL,
|
ExResourceUrl: cfg.ExResourceURL,
|
||||||
@@ -344,11 +344,15 @@ func srToolsSave(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Listen starts the HTTP sdkserver.
|
// Listen starts the HTTP sdkserver.
|
||||||
func Listen() error {
|
func Listen() error {
|
||||||
state := &appState{hotfixMap: loadHotfix()}
|
runtime, err := common.LoadRuntimeConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state := &appState{hotfixMap: loadHotfix(), runtime: runtime}
|
||||||
Log(fmt.Sprintf("loaded %d hotfix versions", len(state.hotfixMap)))
|
Log(fmt.Sprintf("loaded %d hotfix versions", len(state.hotfixMap)))
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/query_dispatch", queryDispatch)
|
mux.HandleFunc("/query_dispatch", func(w http.ResponseWriter, r *http.Request) { queryDispatch(w, r, runtime) })
|
||||||
mux.HandleFunc("/query_gateway", queryGateway(state))
|
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/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}/mdk/shield/api/verify", func(w http.ResponseWriter, r *http.Request) { loginReply(w) })
|
||||||
@@ -366,9 +370,9 @@ func Listen() error {
|
|||||||
notFound(w, r)
|
notFound(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
addr := fmt.Sprintf("0.0.0.0:%d", Port)
|
addr := runtime.SDKAddress()
|
||||||
Log("sdkserver is listening at " + addr)
|
Log("sdkserver is listening at " + addr)
|
||||||
Log(fmt.Sprintf("REST API Gateway is live at http://localhost:%d/ (for GaC-Srtool)", Port))
|
Log(fmt.Sprintf("REST API Gateway is live at %s/ (for GaC-Srtool)", runtime.PublicSDKURL))
|
||||||
return http.ListenAndServe(addr, mux)
|
return http.ListenAndServe(addr, mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93775,9 +93775,12 @@ type GetChallengeGroupStatisticsScRsp struct {
|
|||||||
GroupId uint32 `protobuf:"varint,5,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
|
GroupId uint32 `protobuf:"varint,5,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"`
|
||||||
Retcode uint32 `protobuf:"varint,9,opt,name=retcode,proto3" json:"retcode,omitempty"`
|
Retcode uint32 `protobuf:"varint,9,opt,name=retcode,proto3" json:"retcode,omitempty"`
|
||||||
MEGBIPBAFBP *BBBIAPEHABA `protobuf:"bytes,1461,opt,name=MEGBIPBAFBP,proto3" json:"MEGBIPBAFBP,omitempty"`
|
MEGBIPBAFBP *BBBIAPEHABA `protobuf:"bytes,1461,opt,name=MEGBIPBAFBP,proto3" json:"MEGBIPBAFBP,omitempty"`
|
||||||
ChallengeBoss *ChallengeBossStatistics `protobuf:"bytes,1,opt,name=challenge_boss,json=challengeBoss,proto3" json:"challenge_boss,omitempty"`
|
// Types that are valid to be assigned to EDKOHAAMONH:
|
||||||
ChallengeStory *ChallengeStoryStatistics `protobuf:"bytes,3,opt,name=challenge_story,json=challengeStory,proto3" json:"challenge_story,omitempty"`
|
//
|
||||||
ChallengeDefault *ChallengeStatistics `protobuf:"bytes,11,opt,name=challenge_default,json=challengeDefault,proto3" json:"challenge_default,omitempty"`
|
// *GetChallengeGroupStatisticsScRsp_ChallengeBoss
|
||||||
|
// *GetChallengeGroupStatisticsScRsp_ChallengeStory
|
||||||
|
// *GetChallengeGroupStatisticsScRsp_ChallengeDefault
|
||||||
|
EDKOHAAMONH isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH `protobuf_oneof:"EDKOHAAMONH"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -93833,27 +93836,65 @@ func (x *GetChallengeGroupStatisticsScRsp) GetMEGBIPBAFBP() *BBBIAPEHABA {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GetChallengeGroupStatisticsScRsp) GetEDKOHAAMONH() isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH {
|
||||||
|
if x != nil {
|
||||||
|
return x.EDKOHAAMONH
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (x *GetChallengeGroupStatisticsScRsp) GetChallengeBoss() *ChallengeBossStatistics {
|
func (x *GetChallengeGroupStatisticsScRsp) GetChallengeBoss() *ChallengeBossStatistics {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
|
if x, ok := x.EDKOHAAMONH.(*GetChallengeGroupStatisticsScRsp_ChallengeBoss); ok {
|
||||||
return x.ChallengeBoss
|
return x.ChallengeBoss
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetChallengeGroupStatisticsScRsp) GetChallengeStory() *ChallengeStoryStatistics {
|
func (x *GetChallengeGroupStatisticsScRsp) GetChallengeStory() *ChallengeStoryStatistics {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
|
if x, ok := x.EDKOHAAMONH.(*GetChallengeGroupStatisticsScRsp_ChallengeStory); ok {
|
||||||
return x.ChallengeStory
|
return x.ChallengeStory
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetChallengeGroupStatisticsScRsp) GetChallengeDefault() *ChallengeStatistics {
|
func (x *GetChallengeGroupStatisticsScRsp) GetChallengeDefault() *ChallengeStatistics {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
|
if x, ok := x.EDKOHAAMONH.(*GetChallengeGroupStatisticsScRsp_ChallengeDefault); ok {
|
||||||
return x.ChallengeDefault
|
return x.ChallengeDefault
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH interface {
|
||||||
|
isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH()
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetChallengeGroupStatisticsScRsp_ChallengeBoss struct {
|
||||||
|
ChallengeBoss *ChallengeBossStatistics `protobuf:"bytes,1,opt,name=challenge_boss,json=challengeBoss,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetChallengeGroupStatisticsScRsp_ChallengeStory struct {
|
||||||
|
ChallengeStory *ChallengeStoryStatistics `protobuf:"bytes,3,opt,name=challenge_story,json=challengeStory,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetChallengeGroupStatisticsScRsp_ChallengeDefault struct {
|
||||||
|
ChallengeDefault *ChallengeStatistics `protobuf:"bytes,11,opt,name=challenge_default,json=challengeDefault,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetChallengeGroupStatisticsScRsp_ChallengeBoss) isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetChallengeGroupStatisticsScRsp_ChallengeStory) isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetChallengeGroupStatisticsScRsp_ChallengeDefault) isGetChallengeGroupStatisticsScRsp_EDKOHAAMONH() {
|
||||||
|
}
|
||||||
|
|
||||||
type ChallengeBossSingleNodeInfo struct {
|
type ChallengeBossSingleNodeInfo struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
MaxScore uint32 `protobuf:"varint,4,opt,name=max_score,json=maxScore,proto3" json:"max_score,omitempty"`
|
MaxScore uint32 `protobuf:"varint,4,opt,name=max_score,json=maxScore,proto3" json:"max_score,omitempty"`
|
||||||
@@ -208294,6 +208335,7 @@ func (x *ReplaceLineupCsReq) GetLineupSlotList() []*LineupSlotData {
|
|||||||
type ReplaceLineupScRsp struct {
|
type ReplaceLineupScRsp struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Retcode uint32 `protobuf:"varint,8,opt,name=retcode,proto3" json:"retcode,omitempty"`
|
Retcode uint32 `protobuf:"varint,8,opt,name=retcode,proto3" json:"retcode,omitempty"`
|
||||||
|
OMDLCGEMBLA uint32 `protobuf:"varint,14,opt,name=OMDLCGEMBLA,proto3" json:"OMDLCGEMBLA,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -208335,6 +208377,13 @@ func (x *ReplaceLineupScRsp) GetRetcode() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ReplaceLineupScRsp) GetOMDLCGEMBLA() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OMDLCGEMBLA
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// CmdID: 723
|
// CmdID: 723
|
||||||
type ExtraLineupDestroyNotify struct {
|
type ExtraLineupDestroyNotify struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@@ -345159,14 +345208,15 @@ const file_protocol_proto_StarRail_proto_rawDesc = "" +
|
|||||||
"\vEEJCPNAEKLJ\x18\f \x01(\rR\vEEJCPNAEKLJ\x12\x19\n" +
|
"\vEEJCPNAEKLJ\x18\f \x01(\rR\vEEJCPNAEKLJ\x12\x19\n" +
|
||||||
"\bbuff_two\x18\x0f \x01(\rR\abuffTwo\"=\n" +
|
"\bbuff_two\x18\x0f \x01(\rR\abuffTwo\"=\n" +
|
||||||
" GetChallengeGroupStatisticsCsReq\x12\x19\n" +
|
" GetChallengeGroupStatisticsCsReq\x12\x19\n" +
|
||||||
"\bgroup_id\x18\x05 \x01(\rR\agroupId\"\xd0\x02\n" +
|
"\bgroup_id\x18\x05 \x01(\rR\agroupId\"\xe5\x02\n" +
|
||||||
" GetChallengeGroupStatisticsScRsp\x12\x19\n" +
|
" GetChallengeGroupStatisticsScRsp\x12\x19\n" +
|
||||||
"\bgroup_id\x18\x05 \x01(\rR\agroupId\x12\x18\n" +
|
"\bgroup_id\x18\x05 \x01(\rR\agroupId\x12\x18\n" +
|
||||||
"\aretcode\x18\t \x01(\rR\aretcode\x12/\n" +
|
"\aretcode\x18\t \x01(\rR\aretcode\x12/\n" +
|
||||||
"\vMEGBIPBAFBP\x18\xb5\v \x01(\v2\f.BBBIAPEHABAR\vMEGBIPBAFBP\x12?\n" +
|
"\vMEGBIPBAFBP\x18\xb5\v \x01(\v2\f.BBBIAPEHABAR\vMEGBIPBAFBP\x12A\n" +
|
||||||
"\x0echallenge_boss\x18\x01 \x01(\v2\x18.ChallengeBossStatisticsR\rchallengeBoss\x12B\n" +
|
"\x0echallenge_boss\x18\x01 \x01(\v2\x18.ChallengeBossStatisticsH\x00R\rchallengeBoss\x12D\n" +
|
||||||
"\x0fchallenge_story\x18\x03 \x01(\v2\x19.ChallengeStoryStatisticsR\x0echallengeStory\x12A\n" +
|
"\x0fchallenge_story\x18\x03 \x01(\v2\x19.ChallengeStoryStatisticsH\x00R\x0echallengeStory\x12C\n" +
|
||||||
"\x11challenge_default\x18\v \x01(\v2\x14.ChallengeStatisticsR\x10challengeDefault\"\x8c\x01\n" +
|
"\x11challenge_default\x18\v \x01(\v2\x14.ChallengeStatisticsH\x00R\x10challengeDefaultB\r\n" +
|
||||||
|
"\vEDKOHAAMONH\"\x8c\x01\n" +
|
||||||
"\x1bChallengeBossSingleNodeInfo\x12\x1b\n" +
|
"\x1bChallengeBossSingleNodeInfo\x12\x1b\n" +
|
||||||
"\tmax_score\x18\x04 \x01(\rR\bmaxScore\x12 \n" +
|
"\tmax_score\x18\x04 \x01(\rR\bmaxScore\x12 \n" +
|
||||||
"\vMBHCJPONJOM\x18\x05 \x01(\bR\vMBHCJPONJOM\x12\x15\n" +
|
"\vMBHCJPONJOM\x18\x05 \x01(\bR\vMBHCJPONJOM\x12\x15\n" +
|
||||||
@@ -353098,9 +353148,10 @@ const file_protocol_proto_StarRail_proto_rawDesc = "" +
|
|||||||
"\vleader_slot\x18\r \x01(\rR\n" +
|
"\vleader_slot\x18\r \x01(\rR\n" +
|
||||||
"leaderSlot\x12 \n" +
|
"leaderSlot\x12 \n" +
|
||||||
"\vPLIEODDEEHG\x18\x0e \x01(\bR\vPLIEODDEEHG\x129\n" +
|
"\vPLIEODDEEHG\x18\x0e \x01(\bR\vPLIEODDEEHG\x129\n" +
|
||||||
"\x10lineup_slot_list\x18\x0f \x03(\v2\x0f.LineupSlotDataR\x0elineupSlotList\".\n" +
|
"\x10lineup_slot_list\x18\x0f \x03(\v2\x0f.LineupSlotDataR\x0elineupSlotList\"P\n" +
|
||||||
"\x12ReplaceLineupScRsp\x12\x18\n" +
|
"\x12ReplaceLineupScRsp\x12\x18\n" +
|
||||||
"\aretcode\x18\b \x01(\rR\aretcode\"X\n" +
|
"\aretcode\x18\b \x01(\rR\aretcode\x12 \n" +
|
||||||
|
"\vOMDLCGEMBLA\x18\x0e \x01(\rR\vOMDLCGEMBLA\"X\n" +
|
||||||
"\x18ExtraLineupDestroyNotify\x12<\n" +
|
"\x18ExtraLineupDestroyNotify\x12<\n" +
|
||||||
"\x11extra_lineup_type\x18\a \x01(\x0e2\x10.ExtraLineupTypeR\x0fextraLineupType\"\xa9\x01\n" +
|
"\x11extra_lineup_type\x18\a \x01(\x0e2\x10.ExtraLineupTypeR\x0fextraLineupType\"\xa9\x01\n" +
|
||||||
"&VirtualLineupTrialAvatarChangeScNotify\x12 \n" +
|
"&VirtualLineupTrialAvatarChangeScNotify\x12 \n" +
|
||||||
@@ -379094,6 +379145,11 @@ func file_protocol_proto_StarRail_proto_init() {
|
|||||||
(*CakeRaceBetCoinPVECsReq_BOEJNBKDJLM)(nil),
|
(*CakeRaceBetCoinPVECsReq_BOEJNBKDJLM)(nil),
|
||||||
(*CakeRaceBetCoinPVECsReq_BCFPLAPCFPL)(nil),
|
(*CakeRaceBetCoinPVECsReq_BCFPLAPCFPL)(nil),
|
||||||
}
|
}
|
||||||
|
file_protocol_proto_StarRail_proto_msgTypes[849].OneofWrappers = []any{
|
||||||
|
(*GetChallengeGroupStatisticsScRsp_ChallengeBoss)(nil),
|
||||||
|
(*GetChallengeGroupStatisticsScRsp_ChallengeStory)(nil),
|
||||||
|
(*GetChallengeGroupStatisticsScRsp_ChallengeDefault)(nil),
|
||||||
|
}
|
||||||
file_protocol_proto_StarRail_proto_msgTypes[908].OneofWrappers = []any{
|
file_protocol_proto_StarRail_proto_msgTypes[908].OneofWrappers = []any{
|
||||||
(*DOPHNDHILPA_IIHFEDLHOPI)(nil),
|
(*DOPHNDHILPA_IIHFEDLHOPI)(nil),
|
||||||
(*DOPHNDHILPA_DFNNKENBOEN)(nil),
|
(*DOPHNDHILPA_DFNNKENBOEN)(nil),
|
||||||
|
|||||||
@@ -13437,9 +13437,11 @@ message GetChallengeGroupStatisticsScRsp {
|
|||||||
uint32 group_id = 5;
|
uint32 group_id = 5;
|
||||||
uint32 retcode = 9;
|
uint32 retcode = 9;
|
||||||
BBBIAPEHABA MEGBIPBAFBP = 1461;
|
BBBIAPEHABA MEGBIPBAFBP = 1461;
|
||||||
|
oneof EDKOHAAMONH {
|
||||||
ChallengeBossStatistics challenge_boss = 1;
|
ChallengeBossStatistics challenge_boss = 1;
|
||||||
ChallengeStoryStatistics challenge_story = 3;
|
ChallengeStoryStatistics challenge_story = 3;
|
||||||
ChallengeStatistics challenge_default = 11;
|
ChallengeStatistics challenge_default = 11;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChallengeBossSingleNodeInfo {
|
message ChallengeBossSingleNodeInfo {
|
||||||
@@ -25147,6 +25149,7 @@ message ReplaceLineupCsReq {
|
|||||||
// CmdID: 715
|
// CmdID: 715
|
||||||
message ReplaceLineupScRsp {
|
message ReplaceLineupScRsp {
|
||||||
uint32 retcode = 8;
|
uint32 retcode = 8;
|
||||||
|
uint32 OMDLCGEMBLA = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CmdID: 723
|
// CmdID: 723
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.12
|
||||||
|
// protoc v7.36.0
|
||||||
|
// source: protocol/proto/current_handlers.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// PearlSR 005d2c12db4781071f4b5b68e8f364587886a710:
|
||||||
|
// protocol/src/protocol.pb.zig, SetChallengeTierceLineup messages.
|
||||||
|
// These named messages are missing from the original Go schema.
|
||||||
|
type SetChallengeTierceLineupCsReq struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
ChallengeId uint32 `protobuf:"varint,4,opt,name=challenge_id,json=challengeId,proto3" json:"challenge_id,omitempty"`
|
||||||
|
StageInfoList []*LineupTierce `protobuf:"bytes,5,rep,name=stage_info_list,json=stageInfoList,proto3" json:"stage_info_list,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupCsReq) Reset() {
|
||||||
|
*x = SetChallengeTierceLineupCsReq{}
|
||||||
|
mi := &file_protocol_proto_current_handlers_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupCsReq) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SetChallengeTierceLineupCsReq) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupCsReq) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_protocol_proto_current_handlers_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SetChallengeTierceLineupCsReq.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SetChallengeTierceLineupCsReq) Descriptor() ([]byte, []int) {
|
||||||
|
return file_protocol_proto_current_handlers_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupCsReq) GetChallengeId() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ChallengeId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupCsReq) GetStageInfoList() []*LineupTierce {
|
||||||
|
if x != nil {
|
||||||
|
return x.StageInfoList
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetChallengeTierceLineupScRsp struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Retcode uint32 `protobuf:"varint,1,opt,name=retcode,proto3" json:"retcode,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupScRsp) Reset() {
|
||||||
|
*x = SetChallengeTierceLineupScRsp{}
|
||||||
|
mi := &file_protocol_proto_current_handlers_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupScRsp) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SetChallengeTierceLineupScRsp) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupScRsp) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_protocol_proto_current_handlers_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SetChallengeTierceLineupScRsp.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SetChallengeTierceLineupScRsp) Descriptor() ([]byte, []int) {
|
||||||
|
return file_protocol_proto_current_handlers_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SetChallengeTierceLineupScRsp) GetRetcode() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Retcode
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_protocol_proto_current_handlers_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
const file_protocol_proto_current_handlers_proto_rawDesc = "" +
|
||||||
|
"\n" +
|
||||||
|
"%protocol/proto/current_handlers.proto\x1a\x1dprotocol/proto/StarRail.proto\"y\n" +
|
||||||
|
"\x1dSetChallengeTierceLineupCsReq\x12!\n" +
|
||||||
|
"\fchallenge_id\x18\x04 \x01(\rR\vchallengeId\x125\n" +
|
||||||
|
"\x0fstage_info_list\x18\x05 \x03(\v2\r.LineupTierceR\rstageInfoList\"9\n" +
|
||||||
|
"\x1dSetChallengeTierceLineupScRsp\x12\x18\n" +
|
||||||
|
"\aretcode\x18\x01 \x01(\rR\aretcodeB\x1dZ\x1bgac-hkrpg-go/protocol/protob\x06proto3"
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_protocol_proto_current_handlers_proto_rawDescOnce sync.Once
|
||||||
|
file_protocol_proto_current_handlers_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_protocol_proto_current_handlers_proto_rawDescGZIP() []byte {
|
||||||
|
file_protocol_proto_current_handlers_proto_rawDescOnce.Do(func() {
|
||||||
|
file_protocol_proto_current_handlers_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_protocol_proto_current_handlers_proto_rawDesc), len(file_protocol_proto_current_handlers_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_protocol_proto_current_handlers_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_protocol_proto_current_handlers_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_protocol_proto_current_handlers_proto_goTypes = []any{
|
||||||
|
(*SetChallengeTierceLineupCsReq)(nil), // 0: SetChallengeTierceLineupCsReq
|
||||||
|
(*SetChallengeTierceLineupScRsp)(nil), // 1: SetChallengeTierceLineupScRsp
|
||||||
|
(*LineupTierce)(nil), // 2: LineupTierce
|
||||||
|
}
|
||||||
|
var file_protocol_proto_current_handlers_proto_depIdxs = []int32{
|
||||||
|
2, // 0: SetChallengeTierceLineupCsReq.stage_info_list:type_name -> LineupTierce
|
||||||
|
1, // [1:1] is the sub-list for method output_type
|
||||||
|
1, // [1:1] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_protocol_proto_current_handlers_proto_init() }
|
||||||
|
func file_protocol_proto_current_handlers_proto_init() {
|
||||||
|
if File_protocol_proto_current_handlers_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file_protocol_proto_StarRail_proto_init()
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_protocol_proto_current_handlers_proto_rawDesc), len(file_protocol_proto_current_handlers_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 0,
|
||||||
|
},
|
||||||
|
GoTypes: file_protocol_proto_current_handlers_proto_goTypes,
|
||||||
|
DependencyIndexes: file_protocol_proto_current_handlers_proto_depIdxs,
|
||||||
|
MessageInfos: file_protocol_proto_current_handlers_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_protocol_proto_current_handlers_proto = out.File
|
||||||
|
file_protocol_proto_current_handlers_proto_goTypes = nil
|
||||||
|
file_protocol_proto_current_handlers_proto_depIdxs = nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
option go_package = "gac-hkrpg-go/protocol/proto";
|
||||||
|
import "protocol/proto/StarRail.proto";
|
||||||
|
|
||||||
|
// PearlSR 005d2c12db4781071f4b5b68e8f364587886a710:
|
||||||
|
// protocol/src/protocol.pb.zig, SetChallengeTierceLineup messages.
|
||||||
|
// These named messages are missing from the original Go schema.
|
||||||
|
message SetChallengeTierceLineupCsReq {
|
||||||
|
uint32 challenge_id = 4;
|
||||||
|
repeated LineupTierce stage_info_list = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetChallengeTierceLineupScRsp {
|
||||||
|
uint32 retcode = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package proto
|
||||||
|
|
||||||
|
// ClientDownloadDataLuaVersion is the version value used for Lua downloads by
|
||||||
|
// PearlSR services/misc.zig at source revision
|
||||||
|
// 005d2c12db4781071f4b5b68e8f364587886a710 (client 4.6.53).
|
||||||
|
// Do not derive it from the configured client patch number.
|
||||||
|
const ClientDownloadDataLuaVersion uint32 = 51
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,164 +1,4 @@
|
|||||||
@echo off
|
@echo off
|
||||||
title GaC hkrpg GO Server (4.6.53)
|
setlocal
|
||||||
cd /d "%~dp0"
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0run.ps1" %*
|
||||||
|
exit /b %errorlevel%
|
||||||
echo ==============================================================================
|
|
||||||
echo [ GaC hkrpg GO ] StarRail 4.6.53 Server and Launcher
|
|
||||||
echo gameserver : UDP 23301 (KCP Gateway)
|
|
||||||
echo sdkserver : HTTP 21000 (Dispatch / Login / SRTools)
|
|
||||||
echo ==============================================================================
|
|
||||||
echo.
|
|
||||||
echo [1] Start Server and Star Rail Game / Proxy (Default)
|
|
||||||
echo [2] Start Both Servers (gameserver + sdkserver)
|
|
||||||
echo [3] Start SDK Server ONLY (HTTP 21000)
|
|
||||||
echo [4] Start Game Server ONLY (UDP 23301)
|
|
||||||
echo [5] Start Server and Gay Proxy
|
|
||||||
echo [6] Build / Rebuild binaries (Go)
|
|
||||||
echo [7] Exit
|
|
||||||
echo.
|
|
||||||
echo ==============================================================================
|
|
||||||
|
|
||||||
set "choice=%~1"
|
|
||||||
if not defined choice set /p choice="Select [1-7] (Press Enter for 1): "
|
|
||||||
if not defined choice set "choice=1"
|
|
||||||
set "choice=%choice: =%"
|
|
||||||
if not defined choice set "choice=1"
|
|
||||||
|
|
||||||
if "%choice%"=="1" goto opt_game
|
|
||||||
if "%choice%"=="2" goto opt_servers_only
|
|
||||||
if "%choice%"=="3" goto opt_sdk_only
|
|
||||||
if "%choice%"=="4" goto opt_gs_only
|
|
||||||
if "%choice%"=="5" goto opt_gay_proxy
|
|
||||||
if "%choice%"=="6" goto opt_build
|
|
||||||
if "%choice%"=="7" exit /b 0
|
|
||||||
|
|
||||||
if /i "%choice%"=="sdk" goto opt_sdk_only
|
|
||||||
if /i "%choice%"=="gs" goto opt_gs_only
|
|
||||||
if /i "%choice%"=="servers" goto opt_servers_only
|
|
||||||
if /i "%choice%"=="game" goto opt_game
|
|
||||||
if /i "%choice%"=="proxy" goto opt_gay_proxy
|
|
||||||
if /i "%choice%"=="build" goto opt_build
|
|
||||||
|
|
||||||
echo Unknown option "%choice%", starting servers only...
|
|
||||||
goto opt_servers_only
|
|
||||||
|
|
||||||
:start_core_servers
|
|
||||||
if not exist "gameserver-go.exe" (
|
|
||||||
echo [ERROR] gameserver-go.exe not found! Please build first - Option 6
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
if not exist "sdkserver-go.exe" (
|
|
||||||
echo [ERROR] sdkserver-go.exe not found! Please build first - Option 6
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo [*] Cleaning old server processes...
|
|
||||||
taskkill /f /im sdkserver-go.exe >nul 2>&1
|
|
||||||
taskkill /f /im gameserver-go.exe >nul 2>&1
|
|
||||||
|
|
||||||
echo [*] Starting SDK Server [Port 21000]...
|
|
||||||
start "GaC GO - SDK Server [Port 21000]" /d "%~dp0" cmd /k sdkserver-go.exe
|
|
||||||
|
|
||||||
echo [*] Starting Game Server [Port 23301]...
|
|
||||||
start "GaC GO - Game Server [Port 23301]" /d "%~dp0" cmd /k gameserver-go.exe
|
|
||||||
exit /b 0
|
|
||||||
|
|
||||||
:opt_sdk_only
|
|
||||||
if not exist "sdkserver-go.exe" (
|
|
||||||
echo [ERROR] sdkserver-go.exe not found! Please build first - Option 6
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo [*] Cleaning old sdkserver process...
|
|
||||||
taskkill /f /im sdkserver-go.exe >nul 2>&1
|
|
||||||
|
|
||||||
echo [*] Starting SDK Server [Port 21000]...
|
|
||||||
start "GaC GO - SDK Server [Port 21000]" /d "%~dp0" cmd /k sdkserver-go.exe
|
|
||||||
goto finish
|
|
||||||
|
|
||||||
:opt_gs_only
|
|
||||||
if not exist "gameserver-go.exe" (
|
|
||||||
echo [ERROR] gameserver-go.exe not found! Please build first - Option 6
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
echo [*] Cleaning old gameserver process...
|
|
||||||
taskkill /f /im gameserver-go.exe >nul 2>&1
|
|
||||||
|
|
||||||
echo [*] Starting Game Server [Port 23301]...
|
|
||||||
start "GaC GO - Game Server [Port 23301]" /d "%~dp0" cmd /k gameserver-go.exe
|
|
||||||
goto finish
|
|
||||||
|
|
||||||
:opt_game
|
|
||||||
call :start_core_servers
|
|
||||||
if errorlevel 1 goto :eof
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo [*] Launching Star Rail Client / Proxy...
|
|
||||||
set "GAME_DIR="
|
|
||||||
if exist "%USERPROFILE%\Desktop\StarRail_4.6.53_OS\StarRail.exe" set "GAME_DIR=%USERPROFILE%\Desktop\StarRail_4.6.53_OS"
|
|
||||||
if not defined GAME_DIR if exist "%~dp0..\StarRail_4.6.53_OS\StarRail.exe" set "GAME_DIR=%~dp0..\StarRail_4.6.53_OS"
|
|
||||||
if not defined GAME_DIR if exist "%USERPROFILE%\Desktop\StarRail_4.6.51_OS\StarRail.exe" set "GAME_DIR=%USERPROFILE%\Desktop\StarRail_4.6.51_OS"
|
|
||||||
if not defined GAME_DIR if exist "D:\HoYoPlay\games\Star Rail Games\StarRail.exe" set "GAME_DIR=D:\HoYoPlay\games\Star Rail Games"
|
|
||||||
if not defined GAME_DIR if exist "%~dp0..\StarRail_4.6.51_OS\StarRail.exe" set "GAME_DIR=%~dp0..\StarRail_4.6.51_OS"
|
|
||||||
|
|
||||||
if defined GAME_DIR (
|
|
||||||
echo [*] Found game client at: %GAME_DIR%
|
|
||||||
if exist "%GAME_DIR%\GaCy Launcher.exe" (
|
|
||||||
echo [*] Starting GaCy Launcher...
|
|
||||||
start "GaCy Launcher" /d "%GAME_DIR%" "%GAME_DIR%\GaCy Launcher.exe"
|
|
||||||
) else if exist "%GAME_DIR%\launcher.exe" (
|
|
||||||
echo [*] Starting Launcher...
|
|
||||||
start "Proxy Launcher" /d "%GAME_DIR%" "%GAME_DIR%\launcher.exe"
|
|
||||||
) else if exist "%GAME_DIR%\StarRail.exe" (
|
|
||||||
echo [*] Starting StarRail.exe...
|
|
||||||
start "Star Rail Client" /d "%GAME_DIR%" "%GAME_DIR%\StarRail.exe"
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
echo [!] Game client not found automatically. Please open StarRail manually.
|
|
||||||
)
|
|
||||||
goto finish
|
|
||||||
|
|
||||||
:opt_gay_proxy
|
|
||||||
call :start_core_servers
|
|
||||||
if errorlevel 1 goto :eof
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo [*] Launching Gay Proxy...
|
|
||||||
set "PROXY_BAT="
|
|
||||||
if exist "%USERPROFILE%\Desktop\Gay Proxy\run_proxy.bat" set "PROXY_BAT=%USERPROFILE%\Desktop\Gay Proxy\run_proxy.bat"
|
|
||||||
if not defined PROXY_BAT if exist "%~dp0..\Gay Proxy\run_proxy.bat" set "PROXY_BAT=%~dp0..\Gay Proxy\run_proxy.bat"
|
|
||||||
|
|
||||||
if defined PROXY_BAT (
|
|
||||||
start "Gay Proxy" cmd /k "%PROXY_BAT%"
|
|
||||||
) else (
|
|
||||||
echo [!] Gay Proxy not found.
|
|
||||||
)
|
|
||||||
goto finish
|
|
||||||
|
|
||||||
:opt_servers_only
|
|
||||||
call :start_core_servers
|
|
||||||
if errorlevel 1 goto :eof
|
|
||||||
goto finish
|
|
||||||
|
|
||||||
:opt_build
|
|
||||||
where go >nul 2>nul || set "PATH=%PATH%;C:\Program Files\Go\bin"
|
|
||||||
echo [*] Building gameserver-go.exe...
|
|
||||||
go build -o gameserver-go.exe ./cmd/gameserver || (pause & goto :eof)
|
|
||||||
echo [*] Building sdkserver-go.exe...
|
|
||||||
go build -o sdkserver-go.exe ./cmd/sdkserver || (pause & goto :eof)
|
|
||||||
echo [OK] Build successful!
|
|
||||||
pause
|
|
||||||
goto :eof
|
|
||||||
|
|
||||||
:finish
|
|
||||||
echo.
|
|
||||||
echo ==============================================================================
|
|
||||||
echo [OK] Started successfully!
|
|
||||||
echo Close server windows to stop them.
|
|
||||||
echo Closing this launcher window in 5 seconds...
|
|
||||||
echo ==============================================================================
|
|
||||||
timeout /t 5 >nul 2>&1 || ping 127.0.0.1 -n 6 >nul
|
|
||||||
exit
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
param([string]$Action)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$interactiveMenu = [string]::IsNullOrWhiteSpace($Action)
|
||||||
|
# User-requested delay after an interactive menu action finishes.
|
||||||
|
$menuCloseDelaySeconds = 5
|
||||||
|
$launcherExitCode = 0
|
||||||
|
Set-Location -LiteralPath $PSScriptRoot
|
||||||
|
|
||||||
|
function Merge-Settings($Base, $Override) {
|
||||||
|
foreach ($property in $Override.PSObject.Properties) {
|
||||||
|
$existing = $Base.PSObject.Properties[$property.Name]
|
||||||
|
if ($existing -and $property.Value -is [pscustomobject] -and $existing.Value -is [pscustomobject]) {
|
||||||
|
Merge-Settings $existing.Value $property.Value
|
||||||
|
} else {
|
||||||
|
$Base | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function Resolve-ProjectPath([string]$Path) {
|
||||||
|
if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
|
||||||
|
return [IO.Path]::GetFullPath((Join-Path $PSScriptRoot $Path))
|
||||||
|
}
|
||||||
|
function Stop-ProjectServer([string]$Name) {
|
||||||
|
$target = Resolve-ProjectPath ($Name + '.exe')
|
||||||
|
Get-Process -Name $Name -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
if ($_.Path -and [string]::Equals($_.Path, $target, [StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
Stop-Process -Id $_.Id -ErrorAction Stop
|
||||||
|
if (-not $_.WaitForExit(5000)) { throw "Server did not stop: $target" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function Start-ProjectServer([string]$Name) {
|
||||||
|
$path = Resolve-ProjectPath ($Name + '.exe')
|
||||||
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "$Name is not built. Run build first." }
|
||||||
|
Stop-ProjectServer $Name
|
||||||
|
$process = Start-Process -FilePath $path -WorkingDirectory $PSScriptRoot -WindowStyle Normal -PassThru
|
||||||
|
if ($process.WaitForExit(250)) { throw "$Name exited with code $($process.ExitCode). Run .\$Name.exe in a terminal to inspect the startup error." }
|
||||||
|
Write-Host "Started $Name (PID $($process.Id)) in its own visible console."
|
||||||
|
}
|
||||||
|
function Build-Servers {
|
||||||
|
$go = $env:GAC_GO_COMMAND
|
||||||
|
if ([string]::IsNullOrWhiteSpace($go)) { $go = $settings.launcher.go_command }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($go)) { throw 'Set launcher.go_command or GAC_GO_COMMAND.' }
|
||||||
|
$command = Get-Command -Name $go -CommandType Application -ErrorAction Stop
|
||||||
|
foreach ($name in @('gameserver', 'sdkserver')) {
|
||||||
|
$next = Resolve-ProjectPath ($name + '-go.next.exe')
|
||||||
|
& $command.Source build -o $next ('./cmd/' + $name)
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Build failed: $name. Existing binaries were not replaced." }
|
||||||
|
}
|
||||||
|
foreach ($name in @('gameserver-go', 'sdkserver-go')) {
|
||||||
|
$next = Resolve-ProjectPath ($name + '.next.exe')
|
||||||
|
$target = Resolve-ProjectPath ($name + '.exe')
|
||||||
|
Stop-ProjectServer $name
|
||||||
|
Move-Item -LiteralPath $next -Destination $target -Force
|
||||||
|
}
|
||||||
|
Write-Host 'Build successful. Binaries are in the project root.'
|
||||||
|
}
|
||||||
|
function Start-External([string]$Setting, [string]$EnvironmentName) {
|
||||||
|
$path = [Environment]::GetEnvironmentVariable($EnvironmentName)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($path)) { $path = $settings.launcher.$Setting }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($path)) {
|
||||||
|
Write-Host "Servers started. Set launcher.$Setting or $EnvironmentName to launch the external program."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$path = Resolve-ProjectPath $path
|
||||||
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Configured program does not exist: $path" }
|
||||||
|
$directory = Split-Path -Parent $path
|
||||||
|
if ([IO.Path]::GetExtension($path) -in @('.bat', '.cmd')) {
|
||||||
|
Start-Process -FilePath 'cmd.exe' -ArgumentList ('/d /c ""' + $path + '""') -WorkingDirectory $directory -WindowStyle Hidden
|
||||||
|
} else {
|
||||||
|
Start-Process -FilePath $path -WorkingDirectory $directory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$configPath = $env:GAC_CONFIG
|
||||||
|
if ([string]::IsNullOrWhiteSpace($configPath)) { $configPath = 'server.json' }
|
||||||
|
$configPath = Resolve-ProjectPath $configPath
|
||||||
|
$env:GAC_CONFIG = $configPath
|
||||||
|
$settings = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
||||||
|
$localPath = [IO.Path]::ChangeExtension($configPath, 'local.json')
|
||||||
|
if (Test-Path -LiteralPath $localPath) {
|
||||||
|
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';
|
||||||
|
sdk_host='GAC_SDK_HOST'; sdk_port='GAC_SDK_PORT'; public_game_host='GAC_PUBLIC_GAME_HOST';
|
||||||
|
public_sdk_url='GAC_PUBLIC_SDK_URL'
|
||||||
|
}
|
||||||
|
foreach ($key in $variables.Keys) {
|
||||||
|
$value = [Environment]::GetEnvironmentVariable($variables[$key])
|
||||||
|
if ($null -ne $value) { $settings | Add-Member -MemberType NoteProperty -Name $key -Value $value -Force }
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
throw "$key must be a port between 1 and $([uint16]::MaxValue)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($key in @('client_version','game_host','sdk_host','public_game_host','public_sdk_url')) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($settings.$key)) { throw "Missing setting: $key" }
|
||||||
|
}
|
||||||
|
foreach ($key in @('game_host','sdk_host','public_game_host')) {
|
||||||
|
if ([string]$settings.$key -match '[/\\\s]') { throw "Invalid host: $key" }
|
||||||
|
}
|
||||||
|
$uri = $null
|
||||||
|
if (-not [Uri]::TryCreate($settings.public_sdk_url, [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -notin @('http','https') -or $uri.Query -or $uri.Fragment -or $uri.UserInfo) {
|
||||||
|
throw 'public_sdk_url must be an HTTP(S) base URL.'
|
||||||
|
}
|
||||||
|
if ($Action -eq 'check') { Write-Host 'Configuration valid.'; exit 0 }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Action)) {
|
||||||
|
Write-Host "GaC hkrpg GO | Client $($settings.client_version)"
|
||||||
|
Write-Host "Game UDP $($settings.game_host):$($settings.game_port) | SDK HTTP $($settings.sdk_host):$($settings.sdk_port)"
|
||||||
|
Write-Host '[1] Build, start servers and game (default)'
|
||||||
|
Write-Host '[2] Start servers | [3] SDK only | [4] Game server only'
|
||||||
|
Write-Host '[5] Start servers and proxy | [6] Build only | [7] Exit | [8] Stop servers'
|
||||||
|
$Action = Read-Host 'Select'
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Action)) { $Action = '1' }
|
||||||
|
}
|
||||||
|
switch ($Action.Trim().ToLowerInvariant()) {
|
||||||
|
{ $_ -in @('1','game') } { Build-Servers; Start-ProjectServer 'sdkserver-go'; Start-ProjectServer 'gameserver-go'; Start-External 'client_path' 'GAC_CLIENT_PATH'; break }
|
||||||
|
{ $_ -in @('2','servers') } { Start-ProjectServer 'sdkserver-go'; Start-ProjectServer 'gameserver-go'; break }
|
||||||
|
{ $_ -in @('3','sdk') } { Start-ProjectServer 'sdkserver-go'; break }
|
||||||
|
{ $_ -in @('4','gs') } { Start-ProjectServer 'gameserver-go'; break }
|
||||||
|
{ $_ -in @('5','proxy') } { Start-ProjectServer 'sdkserver-go'; Start-ProjectServer 'gameserver-go'; Start-External 'proxy_path' 'GAC_PROXY_PATH'; break }
|
||||||
|
{ $_ -in @('6','build') } { Build-Servers; break }
|
||||||
|
'7' { break }
|
||||||
|
{ $_ -in @('8','stop') } { Stop-ProjectServer 'sdkserver-go'; Stop-ProjectServer 'gameserver-go'; break }
|
||||||
|
default { throw "Unknown action: $Action" }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
$launcherExitCode = 1
|
||||||
|
}
|
||||||
|
if ($interactiveMenu) {
|
||||||
|
Write-Host "Launcher closes in $menuCloseDelaySeconds seconds. Server windows stay open."
|
||||||
|
Start-Sleep -Seconds $menuCloseDelaySeconds
|
||||||
|
}
|
||||||
|
exit $launcherExitCode
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
cd /d "%~dp0"
|
|
||||||
call run.bat %*
|
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"client_version": "4.6.53",
|
||||||
|
"game_host": "0.0.0.0",
|
||||||
|
"game_port": 23301,
|
||||||
|
"sdk_host": "0.0.0.0",
|
||||||
|
"sdk_port": 21000,
|
||||||
|
"public_game_host": "127.0.0.1",
|
||||||
|
"public_sdk_url": "http://127.0.0.1:21000",
|
||||||
|
"battle_diagnostics": {
|
||||||
|
"client_battle_type": ""
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"client_path": "",
|
||||||
|
"proxy_path": "",
|
||||||
|
"go_command": "go"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"launcher": {
|
||||||
|
"client_path": "",
|
||||||
|
"proxy_path": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,3 +31,26 @@
|
|||||||
- ใน `GetCurBattleInfo` ตัด fallback ปลอมออก หากไม่มี Token ใน `CURRENT_BATTLE` ให้ตอบ `RET_BATTLE_STAGE_NOT_MATCH` (2000) ป้องกัน NRE
|
- ใน `GetCurBattleInfo` ตัด fallback ปลอมออก หากไม่มี Token ใน `CURRENT_BATTLE` ให้ตอบ `RET_BATTLE_STAGE_NOT_MATCH` (2000) ป้องกัน NRE
|
||||||
- ใน `StartCocoonStage` และ `StartQuickCocoonStage` ส่ง `battleInfo` ที่สมบูรณ์พร้อมเวฟที่ถูกต้อง และเพิ่ม Log แสดงค่า `CocoonId`, `Wave`, `WorldLevel`
|
- ใน `StartCocoonStage` และ `StartQuickCocoonStage` ส่ง `battleInfo` ที่สมบูรณ์พร้อมเวฟที่ถูกต้อง และเพิ่ม Log แสดงค่า `CocoonId`, `Wave`, `WorldLevel`
|
||||||
4. คอมไพล์ `gameserver-go.exe` ใหม่และรีสตาร์ทกระบวนการ
|
4. คอมไพล์ `gameserver-go.exe` ใหม่และรีสตาร์ทกระบวนการ
|
||||||
|
|
||||||
|
## Codex battle investigation correction — main affix (2026-10-16)
|
||||||
|
- Client stack RVA 0xc4eb77c is the return address after the throw call at 0xc4eb777, reached when main-affix lookup returns null (branch at 0xc4eb2b3). It does NOT prove a null sub-affix array; that throw call would return to 0xc4eb781. Earlier interpretation is withdrawn.
|
||||||
|
- Captured Calyx/SceneCastSkill payload and inventory UID 483: avatar 1213, relic 63095, main_affix_id 11. Reference relic main group 55 contains IDs 1–10, with 10=ImaginaryAddedRatio. Local AvatarConfig confirms Imaginary for 1213.
|
||||||
|
- Repaired only this inventory field in freesr-data.json: 11 -> 10. No converter ID override. All 24 relic main affixes in the captured team validate against reference data. Reference lacks new relics 63295/63296, so whole inventory is not fully validated.
|
||||||
|
- Source: Mar-7th/StarRailRes commit dbe8cdfcfb0bf657b9fe1cf92d5537f118ccb487, index_new/en/relics.json and relic_main_affixes.json.
|
||||||
|
- Backup: ignored battle_diagnostics/main-affix-repair/. Build completed; root gameserver-go.exe/sdkserver-go.exe updated. In-game battle success still requires a new client run.
|
||||||
|
- First readback probe reported parser property not found. V2 includes non-public fields and CodedInputStream fallback, but local diagnostic probe is disabled for the repair verification. V2 is not yet verified in client.
|
||||||
|
|
||||||
|
## Handler/dispatch update against PearlSR 4.6.53 reference
|
||||||
|
- Synced 2,690 current command names/IDs from upstream revision 005d2c12db4781071f4b5b68e8f364587886a710; generated table preserves verified old Go type aliases and fails on conflicts.
|
||||||
|
- Source audit: all 865 existing request/response pairs and 4,435 shared message field-number sets match. This does not validate all field types/defaults or client behavior. Include oneof members (including enum-valued wrappers) when auditing; ignoring oneof produces false missing-field reports.
|
||||||
|
- Replaced numeric raw registrations/old alias ranges with current named bindings. Removed multi-response guesses. Added missing typed Tierce lineup request8992/response8979, fields4/5; fixed cumulative reward request1747/response1748, group10 -> group12/retcode9.
|
||||||
|
- Current schema statistics fields1/3/11 are a oneof; do not claim they are absent. Updated Go schema to oneof; statistics handler now echoes group without fabricated past scores, matching upstream.
|
||||||
|
- Added ReplaceLineupScRsp field14; regenerated protobuf. Peak start/leave bindings restored; empty current Peak response no longer fabricates PeakID1. Challenge leave uses saved overworld scene rather than fixed destination.
|
||||||
|
- Tutorial handlers load IDs from installed TutorialData/TutorialGuideGroup resources, removing the embedded old list. Removed unused generated activity list.
|
||||||
|
- ClientDownloadData version51 remains because current upstream still uses51 for Lua payloads; do not infer53 from client patch. Avatar command400 response pairing remains semantically obfuscated; current adjacent schema response is used instead of spraying5 candidates, still requires client verification.
|
||||||
|
- Added reproducible offline source sync/audit tools and docs/protocol-update.md; README/AGENTS updated. Final run.bat build completed, both root executables updated at22:16 local time. No in-game success claimed; no test suite run in this turn.
|
||||||
|
|
||||||
|
## Launcher visibility preference
|
||||||
|
- User explicitly requested visible SDK/game server consoles and launcher menu closure after5 seconds.
|
||||||
|
- run.ps1 now launches both root server executables with WindowStyle Normal and no stdout/stderr redirection; each server console remains independent when run.bat exits.
|
||||||
|
- Interactive menu waits5 seconds after the selected action finishes, preserving exit codes. Argument-based build/check/stop returns immediately. README/AGENTS updated. No binary rebuild needed for this script-only change; did not launch or stop running servers.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""Compare command IDs and message field numbers with a local PearlSR checkout.
|
||||||
|
|
||||||
|
This is a source audit, not a client compatibility test. It does not verify
|
||||||
|
field types, default values, assets, or gameplay behavior.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("upstream", type=pathlib.Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
root = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
upstream = args.upstream / "protocol/src"
|
||||||
|
command_source = (upstream / "root.zig").read_text(encoding="utf-8")
|
||||||
|
commands = {n: int(v) for n, v in re.findall(r"\bCmd(\w+)\s*=\s*(\d+)", command_source)}
|
||||||
|
go_commands = {}
|
||||||
|
command_conflicts = []
|
||||||
|
for filename in ("cmdid.go", "upstream_cmdids.go"):
|
||||||
|
source = (root / "protocol/proto" / filename).read_text(encoding="utf-8")
|
||||||
|
for name, value in re.findall(r'^\s*"(\w+)":\s*(\d+),', source, re.M):
|
||||||
|
number = int(value)
|
||||||
|
if name in go_commands and go_commands[name] != number:
|
||||||
|
command_conflicts.append((name, go_commands[name], number))
|
||||||
|
go_commands[name] = number
|
||||||
|
command_differences = [(n, go_commands.get(n), v) for n, v in commands.items() if go_commands.get(n) != v]
|
||||||
|
command_differences.extend(command_conflicts)
|
||||||
|
|
||||||
|
zig_messages = {}
|
||||||
|
source = (upstream / "protocol.pb.zig").read_text(encoding="utf-8")
|
||||||
|
for name, body in re.findall(r"^pub const (\w+) = struct \{\n(.*?)^};", source, re.M | re.S):
|
||||||
|
descriptor = re.search(r"^ pub const _desc_table = .\{\n(.*?)^ };", body, re.M | re.S)
|
||||||
|
if descriptor is None:
|
||||||
|
continue
|
||||||
|
numbers = {int(v) for v in re.findall(r"^ \.\w+ = fd\((\d+),", descriptor[1], re.M)}
|
||||||
|
for union in re.findall(r"pub const _union_desc = .\{(.*?)\n };", body, re.S):
|
||||||
|
numbers.update(int(v) for v in re.findall(r"\.\w+ = fd\((\d+),", union))
|
||||||
|
zig_messages[name] = numbers
|
||||||
|
|
||||||
|
go_messages, wrappers = {}, {}
|
||||||
|
for path in (root / "protocol/proto").glob("*.pb.go"):
|
||||||
|
for name, body in re.findall(r"^type (\w+) struct \{\n(.*?)^}", path.read_text(encoding="utf-8"), re.M | re.S):
|
||||||
|
numbers = {int(v) for v in re.findall(r'protobuf:"[^,]+,(\d+),', body)}
|
||||||
|
go_messages[name] = numbers
|
||||||
|
if ',oneof"' in body and "_" in name:
|
||||||
|
wrappers.setdefault(name.split("_", 1)[0], set()).update(numbers)
|
||||||
|
for name, numbers in wrappers.items():
|
||||||
|
go_messages[name].update(numbers)
|
||||||
|
shared = go_messages.keys() & zig_messages.keys()
|
||||||
|
field_differences = [(n, sorted(go_messages[n]), sorted(zig_messages[n])) for n in sorted(shared)
|
||||||
|
if go_messages[n] != zig_messages[n]]
|
||||||
|
|
||||||
|
current_pairs = {}
|
||||||
|
for name, number in commands.items():
|
||||||
|
for suffix in ("CsReq", "CSReq", "Req"):
|
||||||
|
if name.endswith(suffix):
|
||||||
|
stem = name[:-len(suffix)]
|
||||||
|
candidates = [commands[n] for n in (stem + "ScRsp", stem + "CSScRsp", stem + "Rsp") if n in commands]
|
||||||
|
if len(candidates) == 1:
|
||||||
|
current_pairs[number] = candidates[0]
|
||||||
|
break
|
||||||
|
source = (root / "protocol/proto/cmdpairs.go").read_text(encoding="utf-8")
|
||||||
|
pairs = {int(a): int(b) for a, b in re.findall(r"^\s*(\d+):\s*(\d+),", source, re.M)}
|
||||||
|
pair_differences = [(a, b, current_pairs.get(a)) for a, b in pairs.items() if current_pairs.get(a) != b]
|
||||||
|
print(f"Command IDs: {len(commands)} compared, {len(command_differences)} differences")
|
||||||
|
print(f"Existing request/response pairs: {len(pairs)} compared, {len(pair_differences)} differences")
|
||||||
|
print(f"Shared message field-number sets: {len(shared)} compared, {len(field_differences)} differences")
|
||||||
|
for difference in command_differences + pair_differences + field_differences:
|
||||||
|
print(difference)
|
||||||
|
raise SystemExit(bool(command_differences or pair_differences or field_differences))
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Generate current command names from a local PearlSR checkout; no network I/O."""
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("upstream", type=pathlib.Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
checkout = args.upstream.resolve()
|
||||||
|
revision = subprocess.check_output(
|
||||||
|
["git", "-C", str(checkout), "rev-parse", "HEAD"], text=True
|
||||||
|
).strip()
|
||||||
|
source = (checkout / "protocol/src/root.zig").read_text(encoding="utf-8")
|
||||||
|
enum = source.split("pub const CmdID = enum(u16) {", 1)[1].split("};", 1)[0]
|
||||||
|
commands = re.findall(r"\bCmd(\w+)\s*=\s*(\d+)\s*,", enum)
|
||||||
|
if not commands or len({name for name, _ in commands}) != len(commands):
|
||||||
|
raise SystemExit("Missing or duplicate upstream command names")
|
||||||
|
if len({int(value) for _, value in commands}) != len(commands):
|
||||||
|
raise SystemExit("Duplicate upstream command IDs")
|
||||||
|
lines = [
|
||||||
|
"// Code generated by tools/sync_protocol_commands.py. DO NOT EDIT.",
|
||||||
|
"// Source: https://git.xeondev.com/HonkaiSlopRail/pearl-sr protocol/src/root.zig",
|
||||||
|
"// Revision: " + revision,
|
||||||
|
"package proto", "",
|
||||||
|
'const CommandSourceRevision = "' + revision + '"', "",
|
||||||
|
"var upstreamCmdIDByName = map[string]uint16{",
|
||||||
|
]
|
||||||
|
lines += [f'\t"{name}": {value},' for name, value in commands]
|
||||||
|
lines += ["}", "", "func init() {",
|
||||||
|
"\tfor name, id := range upstreamCmdIDByName {",
|
||||||
|
"\t\tif previous, ok := CmdIDByName[name]; ok && previous != id {",
|
||||||
|
'\t\t\tpanic("command ID conflict between schema and upstream: " + name)',
|
||||||
|
"\t\t}", "\t\tCmdIDByName[name] = id", "\t\tNameByCmdID[id] = name",
|
||||||
|
"\t}", "}", ""]
|
||||||
|
root = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
base_source = (root / "protocol/proto/cmdid.go").read_text(encoding="utf-8")
|
||||||
|
base_commands = {n: int(v) for n, v in re.findall(r'^\s*"(\w+)":\s*(\d+),', base_source, re.M)}
|
||||||
|
conflicts = [n for n, v in commands if n in base_commands and base_commands[n] != int(v)]
|
||||||
|
if conflicts:
|
||||||
|
raise SystemExit("Update the base schema command IDs first: " + ", ".join(conflicts))
|
||||||
|
target = root / "protocol/proto/upstream_cmdids.go"
|
||||||
|
target.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
subprocess.run(["gofmt", "-w", str(target)], check=True)
|
||||||
|
print(f"Generated {len(commands)} commands from {revision}")
|
||||||
Reference in new issue
Block a user