Files
GaCy-GO/docs/GACY-4.6-TECHNICAL-ANALYSIS.md
T

724 lines
43 KiB
Markdown

# คู่มือวิเคราะห์เชิงลึกและแนวทางแก้ไขระบบเซิร์ฟเวอร์ Star Rail 4.6 (GaCy & GaC-hkrpg-go)
> สำหรับนำไปใช้เป็นบริบท (Context) และ Prompt ให้ AI (เช่น GPT-4o / Claude 3.5 Sonnet) นำไปเขียนโค้ดและแก้ไขระบบให้ถูกต้องสมบูรณ์ 100%
---
## 1. ปัญหาที่เกิดขึ้นจริง (Problem Statement)
### 1.1 อาการที่เกิดขึ้นกับตัวเกม Client
* **หน้าต่างข้อความว่างเปล่าที่ 0%:**
เมื่อผู้ใช้ล็อกอินผ่านหน้า UI ของ HoYoverse เข้าไปแล้ว ตัวเกมจะตัดเข้าสู่หน้าจอโหลดฉากอวกาศ (รูปรถไฟ Astral Express บิน) และแถบดาวน์โหลดหยุดนิ่งอยู่ที่ **`0 %`**
* **ไดอะล็อกแจ้งเตือนสีขาว ไม่มี Text:**
หลังจากค้างที่ `0 %` สักครู่ ตัวเกมจะเด้งหน้าต่างโมดอลสีขาวขึ้นมาตรงกลางจอ โดย **ไม่มีข้อความใดๆ ปรากฏ (Blank Text)** มีเพียงไอคอน/ปุ่มเครื่องหมายถูกสำหรับกดยืนยันด้านล่าง
* **วนลูปไม่รู้จบ:**
เมื่อผู้ใช้กดปุ่มยืนยันบนหน้าต่างนั้น ตัวเกมจะสั่ง `RestartApp()` หรือรีโหลดตัวเองกลับไปยังหน้าต่างล็อกอินเริ่มต้นทันที และเมื่อล็อกอินใหม่ก็จะวนกลับมาอาการเดิม
### 1.2 สภาพแวดล้อมระบบ (Environment)
* **เวอร์ชัน Client:** `OSBETAAndroid4.6.54_D16927089_A16927089_L16927077` (และรองรับ `OSBETAAndroid4.6.53`)
* **สถาปัตยกรรมเซิร์ฟเวอร์:**
- **HTTP SDK & Dispatch Gateway:** รันที่พอร์ต `21000` (Flask บน Android Chaquopy Python หรือ Go `internal/sdkserver`)
- **GameServer (UDP KCP):** รันที่พอร์ต `23301` (KCP Protocol Handler สำหรับจัดการ Gameplay/Lineup/Scene)
- **ไฟล์คอนฟิกหลัก:** `server.json`, `persistent.json`, `hotfix.json`
---
## 2. การวิเคราะห์สาเหตุเชิงลึก (Deep Technical Root Cause Analysis)
### 2.1 กลไกการโหลดของตัวเกม Star Rail 4.6 (Unity il2cpp Lifecycle)
ลำดับขั้นตอนที่ตัวเกมทำงานหลังจากกด Log in มีดังนี้:
1. **Authentication:**
- Client ยิง HTTP POST ไปที่ `/account/ma-passport/api/appLoginByPassword` หรือ `/hkrpg_global/mdk/shield/api/login`
- Server ตอบกลับ JSON: `{"retcode": 0, "data": {"account_type": 1, "uid": "1", "token": "gacy_token"}}`
2. **Query Dispatch:**
- Client ยิง HTTP GET ไปที่ `/query_dispatch?version=...`
- Server ตอบกลับ Base64 ของ Protobuf Message `Dispatch` โดยมี `region_list` ชี้ `dispatch_url` ไปที่ `http://<IP>:21000/query_gateway`
3. **Query Gateway:**
- Client ยิง HTTP GET ไปที่ `/query_gateway?version=OSBETAAndroid4.6.54_D16927089_A16927089_L16927077&...`
- Server ต้องตอบกลับ Base64 ของ Protobuf Message `Gateserver`
4. **Hotfix & Asset Verification (จุดที่เกิดปัญหา):**
- ตัวเกม C++ ใน Unity il2cpp จะแกะ Message `Gateserver` แล้วนำฟิลด์ `asset_bundle_url`, `ex_resource_url`, `lua_url`, `lua_version` ไปเริ่มต้นโมดูล **PatchDownloader**
- **กรณีที่ 1 (URL เป็นค่าว่าง):** ถ้าเซิร์ฟเวอร์ส่ง `asset_bundle_url = ""` (เช่น การตั้งค่า `send_hotfix = false`) ตัว PatchDownloader จะเกิด Fatal Exception
- **กรณีที่ 2 (Protobuf Tag ผิด):** ถ้าโครงสร้าง Tag ของ Protobuf ไม่ตรงกับเวอร์ชัน 4.6 ตัวเกมจะแกะค่า Port เป็น URL และแกะ URL เป็นค่าว่าง
- **ผลลัพธ์ของ Exception:** ในโค้ด Native C++ ของเกม Error ในจังหวะนี้ไม่มีการผูก Key ข้อความแปลภาษา (Missing Localized String) กล่องโมดอลแจ้งเตือนจึงแสดงเป็น **หน้าต่างว่างเปล่าสีขาว** และปุ่มยืนยันจะผูกคำสั่ง `RestartGame()` ทำให้เกิดอาการค้างลูป
5. **UDP KCP Connection (พอร์ต 23301):**
- เมื่อผ่านการตรวจ Hotfix ไปได้ ตัวเกมจะสร้าง Socket UDP KCP ไปยัง `ip:port` ที่ระบุใน `Gateserver`
- แพ็กเก็ตแรกที่ส่งคือ `PlayerGetTokenCsReq` (CmdID = 5 ในเวอร์ชัน 4.6)
- หาก Server แมป CmdID ผิดเป็นเวอร์ชันเก่า (เช่น 88) Server จะขึ้น `Unhandled cmd_id 5` และตัดการเชื่อมต่อ
---
### 2.2 ตารางเปรียบเทียบ Protobuf Wire Format: `Gateserver` (4.3 vs 4.6)
นี่คือจุดที่ทำให้ระบบพังอย่างเงียบๆ หากใช้ไฟล์ Schema เก่า:
| ฟิลด์ข้อมูล | ชนิดข้อมูล (Type) | 4.3 Tag (Schema เก่าของ GaCy) | 4.6 Tag (Schema แท้ของ Star Rail 4.6) | ผลกระทบเมื่อส่งผิดไปให้ Client 4.6 |
|---|---|---|---|---|
| `ip` | string (wire 2) | Tag 2 | **Tag 3** | Client 4.6 ไม่สนใจ Tag 2 |
| `ex_resource_url` | string (wire 2) | Tag 5 | **Tag 5** | ค่าตรงกัน |
| `asset_bundle_url` | string (wire 2) | Tag 15 | **Tag 6** | **CRITICAL:** Client 4.6 อ่าน Tag 15 เป็น boolean `unk2` ทำให้มองไม่เห็น `asset_bundle_url`! |
| `lua_url` | string (wire 2) | Tag 3 | **Tag 7** | **CRITICAL:** Client 4.6 อ่าน Tag 3 เป็น `ip`! |
| `port` | uint32 (wire 0) | Tag 6 | **Tag 9** | **CRITICAL:** Client 4.6 อ่าน Tag 6 เป็น `asset_bundle_url` (เอาเลขพอร์ต 23301 ไปเป็น URL ดาวน์โหลด)! |
| `unk1` | bool (wire 0) | - | **Tag 10** | ค่าคงที่ `true` |
| `unk2` | bool (wire 0) | - | **Tag 15** | ค่าคงที่ `true` |
| `unk3` | bool (wire 0) | - | **Tag 77** | ค่าคงที่ `true` |
| `unk4` | bool (wire 0) | - | **Tag 454** | ค่าคงที่ `true` |
| `ifix_url` | string (wire 2) | - | **Tag 694** | URL ifix |
| `unk5` | bool (wire 0) | - | **Tag 802** | ค่าคงที่ `true` |
| `unk6` | bool (wire 0) | - | **Tag 918** | ค่าคงที่ `true` |
| `ifix_version` | string (wire 2) | - | **Tag 1286** | ค่าคงที่ `"0"` (ต้องเป็น string ห้ามส่ง int) |
| `asset_bundle_url_b`| string (wire 2) | - | **Tag 1379**| URL สำรอง (ใส่ค่าเดียวกับ `asset_bundle_url`) |
| `lua_version` | string (wire 2) | - | **Tag 1516** | ค่าเวอร์ชัน Lua (เช่น `"16927297"`, ต้องเป็น string) |
| `unk7` | bool (wire 0) | - | **Tag 1559**| ค่าคงที่ `true` |
| `unk8` | bool (wire 0) | - | **Tag 1975**| ค่าคงที่ `true` |
---
### 2.3 การตัด Hash ท้ายสตริงเวอร์ชัน (Version Query String Mismatch)
* ตัวเกมยิงมาที่ URL:
`GET /query_gateway?version=OSBETAAndroid4.6.54_D16927089_A16927089_L16927077`
* แต่ในไฟล์ `hotfix.json` บันทึกคีย์ไว้แบบสั้น:
`"OSBETAAndroid4.6.54"`
* **ข้อผิดพลาดเดิม:** โค้ดใช้ `hotfix.get(version)` ตรงๆ ทำให้ได้ค่า `None` ส่งผลให้ URL ใน `Gateserver` กลายเป็นค่าว่าง
* **การแก้ไขที่ถูกต้อง:** ต้องตัดเอาเฉพาะส่วนหน้าก่อนเครื่องหมาย `_`:
```python
clean_version = version.split('_')[0] if version else version
hotfix_entry = hotfix_dict.get(version) or hotfix_dict.get(clean_version)
```
---
### 2.4 ตาราง CmdID บน UDP KCP ในเวอร์ชัน 4.6 (เปรียบเทียบกับ 4.3)
เมื่อตัวเกมผ่าน 0% ไปแล้ว จะเริ่มยิง KCP ถ้า CmdID ไม่ตรง Server จะพิมพ์ `Unhandled cmd_id` และ Client จะหลุด:
| Command Name | CmdID ในเวอร์ชัน 4.3 | CmdID ในเวอร์ชัน 4.6 | ประเภท |
|---|---|---|---|
| `PlayerGetTokenCsReq` | 88 | **5** | Client -> Server (คำสั่งแรกสุดของ KCP) |
| `PlayerGetTokenScRsp` | 44 | **71** | Server -> Client |
| `PlayerLoginCsReq` | 94 | **74** | Client -> Server |
| `PlayerLoginScRsp` | 9 | **19** | Server -> Client |
| `PlayerLoginFinishCsReq` | 84 | **9** | Client -> Server |
| `PlayerLoginFinishScRsp` | 13 | **13** | Server -> Client |
| `PlayerHeartBeatCsReq` | 98 | **45** | Heartbeat |
| `PlayerHeartBeatScRsp` | 99 | **16** | Heartbeat Response |
| `GetCurSceneInfoCsReq` | 1488 | **1459** | โหลดข้อมูลฉาก |
| `GetCurSceneInfoScRsp` | 1444 | **1418** | ตอบข้อมูลฉาก |
| `SceneCastSkillCsReq` | 1424 | **1405** | กดใช้สกิลในฉาก |
| `SceneCastSkillScRsp` | 1403 | **1471** | ตอบรับสกิล |
| `SceneEntityMoveCsReq` | 1494 | **1474** | เดินในฉาก |
| `SceneEntityMoveScRsp` | 1409 | **1419** | ตำแหน่งการเดิน |
| `GetCurLineupDataCsReq`| 788 | **731** | ข้อมูลทีมปัจจุบัน |
| `GetCurLineupDataScRsp`| 744 | **787** | ตอบข้อมูลทีมปัจจุบัน |
| `GetAllLineupDataCsReq`| 794 | **704** | ข้อมูลทีมทั้งหมด |
| `GetAllLineupDataScRsp`| 709 | **794** | ตอบข้อมูลทีมทั้งหมด |
| `ChangeLineupLeaderCsReq`| 784 | **798** | เปลี่ยนตัวละครหลัก |
| `ChangeLineupLeaderScRsp`| 713 | **747** | ตอบรับเปลี่ยนตัวละคร |
| `GetBagCsReq` | 588 | **574** | ข้อมูลกระเป๋า |
| `GetBagScRsp` | 544 | **519** | ตอบข้อมูลกระเป๋า |
| `GetAvatarDataCsReq` | 388 | **374** | ข้อมูลตัวละคร |
| `GetAvatarDataScRsp` | 344 | **319** | ตอบข้อมูลตัวละคร |
| `PVEBattleResultCsReq` | 188 | **174** | ผลการต่อสู้ |
| `PVEBattleResultScRsp` | 144 | **119** | ตอบรับผลการต่อสู้ |
| `StartCocoonStageCsReq`| 1438 | **1444** | เริ่มดันเจี้ยน |
| `StartCocoonStageScRsp`| 1416 | **1481** | ตอบเริ่มดันเจี้ยน |
---
### 2.5 ทำไมถึงห้ามนำ binary ของ ppSR มาใส่ตรงๆ?
ในไฟล์ `ppsr.pyc` ของ ppSR มีการเขียนฟังก์ชันป้องกันการแจกจ่าย (`check_access` / `qyqwiourtyqot`) โดยจะนำ Timestamp มา Hash ร่วมกับคีย์ลับ (HMAC-SHA256) เพื่อสร้าง Password รายวัน/สัปดาห์ หากรหัสใน `persistent["key"]` ไม่ตรง ตัวโปรแกรมจะเรียก `input("Enter the correct password: ")` ซึ่งสำหรับแอพแบบ Embedded บน Android จะทำให้เซิร์ฟเวอร์หยุดรอ Input และไม่สามารถทำงานต่อได้
**ดังนั้น:** เราต้องสร้างตัว Encode และ Logic 4.6 ภายในโค้ดของเราเอง (ทั้งใน Python runner และ Go backend) โดยไม่ต้องพึ่งพาหรือก๊อปปี้ binary ของ ppSR มาใช้งาน
---
## 3. ข้อมูล CDN Hotfix 4.6 ที่แท้จริง (Official 4.6 CDN URLs)
ให้ใช้ชุดข้อมูล JSON นี้สำหรับบรรจุลงใน `hotfix.json`:
```json
{
"OSBETAAndroid4.6.54": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_16927089_a94e4821d0e7_f07f57da7e245a",
"asset_bundle_url_b": "https://autopatchos.starrails.com/asb/BetaLive/output_16927089_a94e4821d0e7_f07f57da7e245a",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_16932918_4a2e3f79111d_3e2af64ea2b108",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_16927297_c46b951e256b_0abc31ea529d71",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253_6d871f8bca6eb4",
"lua_version": "16927297",
"ifix_version": "0"
},
"OSBETAAndroid4.6.53": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_16818953_d4345bbd54a2_ae00cce42191d1",
"asset_bundle_url_b": "https://autopatchos.starrails.com/asb/BetaLive/output_16818953_d4345bbd54a2_ae00cce42191d1",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_16833202_959c316ef547_335871eaabab71",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_16819203_679727d45bd7_d086e33f0af119",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253_6d871f8bca6eb4",
"lua_version": "16819203",
"ifix_version": "0"
},
"OSBETAWin4.6.54": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_16927089_a94e4821d0e7_f07f57da7e245a",
"asset_bundle_url_b": "https://autopatchos.starrails.com/asb/BetaLive/output_16927089_a94e4821d0e7_f07f57da7e245a",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_16932918_4a2e3f79111d_3e2af64ea2b108",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_16927297_c46b951e256b_0abc31ea529d71",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253_6d871f8bca6eb4",
"lua_version": "16927297",
"ifix_version": "0"
}
}
```
---
## 4. โค้ดตัวอย่างสำหรับแก้ปัญหา (Ready-to-use Implementation)
### 4.1 ตัวสร้าง Wire Format Protobuf `Gateserver` 4.6 (Python Standalone Serializer)
ฟังก์ชันนี้ทำงานโดยเขียน Byte Stream ตามสเปก Protobuf Wire Format โดยตรง ไม่ต้องพึ่งพา `classes_pb2.pyc` เก่า:
```python
import base64
def encode_varint(value: int) -> bytes:
bits = value & 0x7fffffffffffffff
buf = bytearray()
while True:
b = bits & 0x7f
bits >>= 7
if bits:
buf.append(b | 0x80)
else:
buf.append(b)
break
return bytes(buf)
def encode_field(tag: int, wire_type: int, payload: bytes) -> bytes:
key = (tag << 3) | wire_type
return encode_varint(key) + payload
def build_gateserver_4_6(gate_ip: str, game_port: int, hf_data: dict) -> bytes:
parts = []
# Tag 3: ip (string)
ip_bytes = gate_ip.encode('utf-8')
parts.append(encode_field(3, 2, encode_varint(len(ip_bytes)) + ip_bytes))
# Tag 5: ex_resource_url (string)
ex_res = hf_data.get('ex_resource_url', '').encode('utf-8')
if ex_res:
parts.append(encode_field(5, 2, encode_varint(len(ex_res)) + ex_res))
# Tag 6: asset_bundle_url (string)
asb = hf_data.get('asset_bundle_url', '').encode('utf-8')
if asb:
parts.append(encode_field(6, 2, encode_varint(len(asb)) + asb))
# Tag 7: lua_url (string)
lua_url = hf_data.get('lua_url', '').encode('utf-8')
if lua_url:
parts.append(encode_field(7, 2, encode_varint(len(lua_url)) + lua_url))
# Tag 9: port (uint32)
parts.append(encode_field(9, 0, encode_varint(game_port)))
# Boolean flags (unk1 .. unk8)
bool_tags = [10, 15, 77, 454, 802, 918, 1559, 1975]
for tag in bool_tags:
parts.append(encode_field(tag, 0, encode_varint(1)))
# Tag 694: ifix_url (string)
ifix_url = hf_data.get('ifix_url', '').encode('utf-8')
if ifix_url:
parts.append(encode_field(694, 2, encode_varint(len(ifix_url)) + ifix_url))
# Tag 1286: ifix_version (string)
ifix_ver = str(hf_data.get('ifix_version', '0')).encode('utf-8')
parts.append(encode_field(1286, 2, encode_varint(len(ifix_ver)) + ifix_ver))
# Tag 1379: asset_bundle_url_b (string)
asb_b = (hf_data.get('asset_bundle_url_b') or hf_data.get('asset_bundle_url', '')).encode('utf-8')
if asb_b:
parts.append(encode_field(1379, 2, encode_varint(len(asb_b)) + asb_b))
# Tag 1516: lua_version (string)
lua_ver = str(hf_data.get('lua_version', '16927297')).encode('utf-8')
parts.append(encode_field(1516, 2, encode_varint(len(lua_ver)) + lua_ver))
raw_protobuf = b''.join(parts)
return base64.b64encode(raw_protobuf)
```
### 4.2 โค้ด Handler สำหรับ `query_gateway` (Python Flask)
```python
@app.route('/query_gateway', methods=['GET', 'HEAD'])
def query_gateway():
from flask import request
if request.method == 'HEAD':
return '', 200
version = request.args.get('version', '')
if not version:
return '', 200
# 1. ตัด build hash ออก เช่น OSBETAAndroid4.6.54_D16927089... -> OSBETAAndroid4.6.54
clean_ver = version.split('_')[0]
# 2. ค้นหา Hotfix Data
hf = hotfix_dict.get(version) or hotfix_dict.get(clean_ver)
if not hf:
# Fallback หาคีย์ 4.6 ตัวที่ใกล้เคียงที่สุด
for k in hotfix_dict:
if '4.6' in k and 'Android' in k:
hf = hotfix_dict[k]
break
# 3. กำหนด Target IP (127.0.0.1 หากเล่นบนเครื่องเดียวกัน หรือ LAN IP)
client_ip = request.remote_addr
target_ip = "127.0.0.1" if client_ip in ("127.0.0.1", "localhost") else get_local_lan_ip()
# 4. ตรวจสอบว่าเปิดส่ง Hotfix หรือไม่ (ต้องเป็น True เสมอสำหรับ 4.6)
should_send_hotfix = Manager.persistent.get("send_hotfix", True)
hotfix_payload = hf if should_send_hotfix else {}
# 5. สร้าง Base64 Protobuf 4.6 แท้
response_b64 = build_gateserver_4_6(target_ip, 23301, hotfix_payload)
return response_b64
```
### 4.3 โค้ดสำหรับเซิร์ฟเวอร์ Go (`internal/sdkserver/server.go`)
ใน Go backend ของ `GaC-hkrpg-go`:
```go
func (a *appState) getOrInsertHotfix(version, dispatchSeed string) VersionConfig {
a.mu.Lock()
defer a.mu.Unlock()
// 1. Direct match
if cfg, ok := a.hotfixMap[version]; ok {
return cfg
}
// 2. Stripped prefix match (สำคัญมาก: ตัด _D... ออก)
cleanVer := strings.Split(version, "_")[0]
if cfg, ok := a.hotfixMap[cleanVer]; ok {
return cfg
}
// 3. Fallback to OSBETAAndroid4.6.54 if version contains 4.6
if strings.Contains(version, "4.6") {
if cfg, ok := a.hotfixMap["OSBETAAndroid4.6.54"]; ok {
return cfg
}
}
return VersionConfig{}
}
```
และใน struct `Gateserver` ของ Protobuf ใน Go (`StarRail.pb.go`) ให้แน่ใจว่า Tag ตรงกับสเปก 4.6:
```protobuf
message Gateserver {
string ip = 3;
string ex_resource_url = 5;
string asset_bundle_url = 6;
string lua_url = 7;
uint32 port = 9;
bool unk1 = 10;
bool unk2 = 15;
bool unk3 = 77;
bool unk4 = 454;
string ifix_url = 694;
bool unk5 = 802;
bool unk6 = 918;
string ifix_version = 1286;
string asset_bundle_url_b = 1379;
string lua_version = 1516;
bool unk7 = 1559;
bool unk8 = 1975;
}
```
---
## 5. สรุปเช็คลิสต์สำหรับการทดสอบ (Verification Checklist)
- [ ] **เปิดใช้งาน `send_hotfix: true`:** ตรวจสอบว่าใน `persistent.json` และโค้ดตัวอ่าน persistent ไม่มีส่วนใดไปบังคับเซฟทับเป็น `false`
- [ ] **Hotfix URLs ไม่ว่างเปล่า:** เมื่อ Client ยิง `/query_gateway` ต้องได้ URL ที่ขึ้นต้นด้วย `https://autopatchos.starrails.com/...` เสมอ
- [ ] **Tag 6 และ Tag 9 ถูกต้อง:** ค่าของ Tag 6 ต้องเป็นสตริง URL (`asset_bundle_url`) และ Tag 9 ต้องเป็นตัวเลขพอร์ต `23301` (ไม่ใช่สลับกัน)
- [ ] **รองรับคำสั่งแรกของ KCP (CmdID 5):** เมนู Console ของ Gameserver ต้องไม่ขึ้นว่า `Unhandled cmd_id 5` ตอนที่หลุดจากหน้า 0% เข้าสู่การเชื่อมต่อฉากเกม
- [ ] **ไม่มีระบบเช็ค Discord License หรือถาม Password:** เซิร์ฟเวอร์ต้องเริ่มต้นทำงานได้ทันทีแบบไม่ต้องรอ Console Input ใดๆ
---
## 6. รวมบัคและข้อผิดพลาดทั้งหมดที่เกิดขึ้นในระบบ (Comprehensive Bug Registry & Failure Post-Mortem)
ส่วนนี้ได้รวบรวมทุกบัค, Error Log, สาเหตุที่แท้จริง (Root Cause), และโค้ดแก้ไข (Fix) ที่เกิดขึ้นตลอดการพัฒนาและการดีบัก GaCy และ GaC-hkrpg-go ไว้อย่างละเอียดที่สุด เพื่อให้ AI สามารถนำไปใช้ป้องกันและแก้ปัญหาได้อย่างครอบคลุม:
---
### Bug #1: ค้าง 0% และเกิด Alert Dialog สีขาวว่างเปล่า (White Blank Dialog Loop)
* **Log อาการ:**
```text
[HTTP] GET /query_gateway?version=OSBETAAndroid4.6.54_D16927089... -> 200
[Client]: หน้าจอขึ้นรูปรถไฟ Astral Express บิน แถบดาวน์โหลดค้างที่ 0 %
[Client]: สักครู่มี Dialog สีขาวไม่มีตัวหนังสือเด้งขึ้นมา พอกดปุ่ม ตัวเกมสั่ง Restart
```
* **สาเหตุ:**
1. ค่า `send_hotfix` ถูกตั้งเป็น `false` หรือโค้ดส่ง `asset_bundle_url = ""` ว่างเปล่ากลับไป
2. ใน Star Rail 4.6 โมดูล `PatchDownloader` ของ Unity il2cpp บังคับว่าต้องมี URL ดาวน์โหลด Asset Bundle ถ้าไม่มีหรือต่อไม่ติด จะเกิด Fatal Error ทันที
3. เนื่องจากตัวเกมไม่ได้ผูกข้อความแปลภาษาสำหรับกรณี Asset URL ว่างไว้ ทำให้หน้าต่างแสดงผลเป็นสีขาวล้วน (Empty Text)
* **วิธีแก้:**
- ต้องเปิด `send_hotfix: true` เสมอ
- ฟิลด์ `asset_bundle_url`, `ex_resource_url`, `lua_url` ต้องมี URL ของ CDN แท้ (เช่น `https://autopatchos.starrails.com/asb/BetaLive/...`) บรรจุอยู่เสมอ ห้ามเป็นค่าว่าง
---
### Bug #2: เซิร์ฟเวอร์ส่ง IP `0.0.0.0` กลับไปใน Gateserver
* **Log อาการ:**
```text
[Dispatch] Gateserver response serialized: ip="0.0.0.0", port=23301
[Client]: Connection refused / Timeout ทันทีหลังจบ HTTP Gateway
```
* **สาเหตุ:**
- โค้ดของ Dispatch ไปหยิบค่า `_ds_mod.HOST` ซึ่งตั้งค่าไว้เป็น `"0.0.0.0"` (เพื่อให้ Server Socket บายด์รับทุก Interface) ส่งกลับไปใน Protobuf
- Client ในระบบ Android หรืออุปกรณ์อื่นภายนอก ไม่สามารถเชื่อมต่อไปยัง IP `0.0.0.0` ได้
* **วิธีแก้:**
```python
def resolve_client_gate_ip():
from flask import request
client_ip = request.remote_addr if request else "127.0.0.1"
if not client_ip or client_ip in ("127.0.0.1", "::1", "localhost"):
return "127.0.0.1"
# หากเป็นเครื่องอื่น ให้ตอบด้วย LAN IP ของเซิร์ฟเวอร์
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('8.8.8.8', 80))
return s.getsockname()[0]
except Exception:
return "127.0.0.1"
finally:
s.close()
```
---
### Bug #3: Protobuf Tag Inversion (4.3 vs 4.6 Tag Mismatch)
* **Log อาการ:**
```text
[Client Debug]: AssetBundle download failed: invalid url format "23301"
[Client]: Game crash / Restart loop at 0%
```
* **สาเหตุ:**
- Schema เก่า (4.3) มี `port` อยู่ที่ **Tag 6** และ `asset_bundle_url` อยู่ที่ **Tag 15**
- ใน 4.6 สลับเป็น `asset_bundle_url` อยู่ที่ **Tag 6** และ `port` อยู่ที่ **Tag 9**
- เมื่อ Client 4.6 อ่านไบต์ที่สร้างจาก 4.3 ตัวเกมจะอ่านค่าพอร์ต (`23301`) ไปเป็น String URL ของ Asset Bundle
* **วิธีแก้:**
- แก้ไขตัวสร้าง Protobuf ให้ Tag 6 เป็น String URL และ Tag 9 เป็น uint32 พอร์ตเกม
---
### Bug #4: Protobuf Type Crash บน `lua_version` และ `ifix_version` (`TypeError: int expected str`)
* **Log อาการ:**
```text
TypeError: 0 has type int, but expected one of: bytes, str
File "dispatchserver.py", line 113, in query_gateway
response.lua_version = hf.get('lua_version', 0)
```
* **สาเหตุ:**
- ในนิยาม Protobuf 4.6 ฟิลด์ `lua_version` (Tag 1516) และ `ifix_version` (Tag 1286) ถูกกำหนดชนิดข้อมูลเป็น `string` (ไม่ใช่ uint32)
- เมื่อโค้ดส่ง default value เป็นเลข `0` (int) ตัวไลบรารี Protobuf ของ Python จะ Raise `TypeError` ทันที ทำให้ HTTP 500
* **วิธีแก้:**
```python
response.lua_version = str(hf.get('lua_version', '16927297'))
response.ifix_version = str(hf.get('ifix_version', '0'))
```
---
### Bug #5: Protobuf AttributeError ฟิลด์ `unk9` และ `unk10` ไม่มีอยู่จริง
* **Log อาการ:**
```text
AttributeError: Protocol message Gateserver has no "unk9" field.
```
* **สาเหตุ:**
- โค้ด Patch พยายามส่งตัวแปรเดา: `unk1=True, ..., unk9=True, unk10=True`
- ในสเปก Protobuf ของ Star Rail 4.6 ฟิลด์ boolean unk มีถึงแค่ **`unk8`** (Tag 1975) เท่านั้น การส่ง `unk9` ทำให้เกิด Crash
* **วิธีแก้:**
- ส่งเฉพาะ boolean flags `unk1` ถึง `unk8` (Tags: 10, 15, 77, 454, 802, 918, 1559, 1975)
---
### Bug #6: บัคฟังก์ชัน Auto-Heal ทับค่า `send_hotfix = False` อัตโนมัติทุกครั้งที่เปิด
* **Log อาการ:**
```text
[Patch] persistent.json validated and auto-healed with complete avatar/scene defaults (send_hotfix=False)
```
* **สาเหตุ:**
- ในไฟล์ `run_ppsr.py` และ `PersistentRepository.kt` มีโค้ดตรวจสอบสุขภาพไฟล์ `persistent.json` แต่คนเขียนเดิมใส่เงื่อนไขไว้ว่า:
```python
if data.get("send_hotfix") is not False:
data["send_hotfix"] = False
modified = True
```
- ส่งผลให้ไม่ว่าผู้ใช้จะแก้ไฟล์เป็น `true` อย่างไร พอเปิดแอพ เซิร์ฟเวอร์จะเซฟทับกลับเป็น `false` ทันที
* **วิธีแก้:**
```python
if "send_hotfix" not in data:
data["send_hotfix"] = True
modified = True
```
---
### Bug #7: การหาคีย์ Hotfix ไม่เจอเพราะ Client ส่ง Hash ต่อท้ายเวอร์ชัน
* **Log อาการ:**
```text
KeyError: 'OSBETAAndroid4.6.54_D16927089_A16927089_L16927077'
Couldn't find OSBETAAndroid4.6.54_D16927089_A16927089_L16927077 in hotfix.json
```
* **สาเหตุ:**
- ตัวเกม Client ยิง Query String มาเป็น `version=OSBETAAndroid4.6.54_D16927089_A16927089_L16927077`
- แต่ใน `hotfix.json` บันทึกไว้เฉพาะ `OSBETAAndroid4.6.54`
- โค้ดดึง `hotfix[version]` ตรงๆ ทำให้เกิด `KeyError` หรือได้ค่าว่าง
* **วิธีแก้:**
```python
clean_ver = version.split('_')[0] if version else version
hf = hotfix.get(version) or hotfix.get(clean_ver)
```
---
### Bug #8: เซิร์ฟเวอร์ Beta ของทางการปิดตัว (`retcode: 3 / 测试资格校验失败`)
* **Log อาการ:**
```text
Tag 12 (varint): 3
Tag 13 (string): client version not match
Tag 252 (string): 测试资格校验失败,请重新登录
```
* **สาเหตุ:**
- ฟังก์ชัน `check_hotfix` ของ ppSR ดั้งเดิม ออกแบบมาให้ยิง Proxy ต่อไปยัง `https://beta-release01-asia.starrails.com/query_gateway` เพื่อดึงข้อมูล Hotfix สดๆ
- แต่เมื่อระยะเวลาทดสอบ Beta จบลง เซิร์ฟเวอร์ทางการจะปฏิเสธคำขอทั้งหมดและส่ง retcode 3
- ส่งผลให้ `gateserver.asset_bundle_url` ที่ตอบกลับมาเป็นค่าว่าง `""` ถ้าเซิร์ฟเวอร์ไม่มีการ Hardcode URL สำรองไว้ใน `hotfix.json` ระบบจะพังทันที
* **วิธีแก้:**
- ต้องมี URL CDN ของ 4.6 ที่บันทึกไว้ในเครื่อง (`hotfix.json`) เสมอ และหากดึงสดล้มเหลว ให้ใช้ข้อมูลในเครื่องตอบกลับ Client ทันที
---
### Bug #9: HTTP 405 Method Not Allowed บน `/hkrpg_global/mdk/shield/api/loadConfig`
* **Log อาการ:**
```text
[HTTP] GET /hkrpg_global/mdk/shield/api/loadConfig -> 405 Method Not Allowed
```
* **สาเหตุ:**
- เส้นทาง API Shield ของ Flask ถูกลงทะเบียนไว้เฉพาะเมธอด `POST`:
`@app.route('/hkrpg_<region>/mdk/shield/api/<action>', methods=['POST'])`
- แต่ตัวเกม Star Rail Global ส่งเมธอด **`GET`** มาที่ `loadConfig`
* **วิธีแก้:**
```python
@app.route('/<path:dummy>/mdk/shield/api/loadConfig', methods=['GET', 'POST'])
@app.route('/hkrpg_<region>/mdk/shield/api/loadConfig', methods=['GET', 'POST'])
def shield_load_config(*args, **kwargs):
return jsonify({
"retcode": 0,
"message": "OK",
"data": {
"id": 6,
"game_key": "hkrpg_global",
"client": "ANDROID",
"identity": "I_IDENTITY",
"guest": False,
"ignore_clean": 0,
"scene": "S_NORMAL"
}
})
```
---
### Bug #10: HTTP 404 บน Telemetry & SDK Configuration Endpoints
* **Log อาการ:**
```text
[HTTP] GET /combo/box/api/config/sdk/combo -> 404
[HTTP] GET /combo/box/api/config/sdk/drmSwitch -> 404
[HTTP] POST /data_abtest_api/config/experiment/list -> 404
[HTTP] GET /admin/mi18n/plat_oversea/... -> 404
```
* **สาเหตุ:**
- ตัวเกม Star Rail Global มีการเรียก Endpoint ย่อยต่างๆ สำหรับตรวจสอบ DRM, A/B Testing และไฟล์ภาษา i18n
- หากเซิร์ฟเวอร์ตอบกลับเป็น 404 HTML ตัวเกมบางเวอร์ชันจะหยุดทำงาน
* **วิธีแก้:**
- เพิ่ม Universal Fallback Handler ดักจับเส้นทางเหล่านี้แล้วตอบ JSON 200 OK:
```python
@app.errorhandler(404)
def handle_404(e):
from flask import request, jsonify
path = request.path
if any(k in path for k in ['combo', 'config', 'drm', 'experiment', 'abtest', 'mi18n', 'font']):
return jsonify({"retcode": 0, "message": "OK", "data": {}}), 200
if any(k in path for k in ['login', 'passport', 'token', 'auth', 'shield']):
return jsonify({
"retcode": 0,
"message": "OK",
"data": {
"account_type": 1,
"uid": "1",
"token": "gacy_token_ok",
"email": "player@gacy.local"
}
}), 200
return jsonify({"retcode": 0, "message": "OK"}), 200
```
---
### Bug #11: UDP KCP `Unhandled cmd_id 5` (`PlayerGetTokenCsReq`)
* **Log อาการ:**
```text
Dispatch server listening on 0.0.0.0:21000
Forwarding to 127.0.0.1:23301
[GameServer]: Unhandled cmd_id 5
```
* **สาเหตุ:**
- ใน Star Rail 4.6 เมื่อตัวเกมผ่านหน้า 0% ไปได้ จะส่งคำสั่งแรกคือ `PlayerGetTokenCsReq`
- หมายเลขคำสั่งใน 4.6 คือ **`5`** (ใน 4.3 คือ 88)
- เซิร์ฟเวอร์ไม่มี Handler สำหรับหมายเลข 5 จึงทิ้งแพ็กเก็ต ทำให้ Client ตัดการเชื่อมต่อ
* **วิธีแก้:**
- อัปเดตตาราง `cmd_dict` ให้ `PlayerGetTokenCsReq` ชี้ไปที่ `5` และ `PlayerGetTokenScRsp` ชี้ไปที่ `71`
- แมปฟังก์ชัน `PlayerGetTokenCsReq.handle` เข้ากับ CmdID `5` ใน `router.py`
---
### Bug #12: UDP KCP `Unhandled cmd_id 74` (`PlayerLoginCsReq`)
* **Log อาการ:**
```text
PlayerGetTokenCsReq (5) -> PlayerGetTokenScRsp (71)
[GameServer]: Unhandled cmd_id 74
```
* **สาเหตุ:**
- หลังจาก Token ผ่าน Client จะส่งคำสั่งล็อกอินเข้าเกม `PlayerLoginCsReq`
- ใน 4.6 หมายเลขคำสั่งคือ **`74`** (ใน 4.3 คือ 94) และผลตอบรับ `PlayerLoginScRsp` คือ **`19`** (ใน 4.3 คือ 9)
* **วิธีแก้:**
- แมป `PlayerLoginCsReq: 74` และ `PlayerLoginScRsp: 19` ใน `router.py`
---
### Bug #13: Socket Crash `OSError: [Errno 98] Address already in use`
* **Log อาการ:**
```text
OSError: [Errno 98] Address already in use: ('0.0.0.0', 21000)
```
* **สาเหตุ:**
- เมื่อผู้ใช้กด Stop Server แล้วกด Start Server ใหม่ทันที Socket ของ Werkzeug/Flask ยังอยู่ในสถานะ `TIME_WAIT` ของระบบปฏิบัติการ
* **วิธีแก้:**
- ทำ Patch ครอบฟังก์ชัน `werkzeug.serving.make_server` เพื่อเซ็ตแฟล็ก Socket:
```python
import socket, struct
def _patched_make_server(*args, **kwargs):
kwargs.pop('options', None)
server = _orig_make_server(*args, **kwargs)
if hasattr(server, 'socket') and server.socket:
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# ปิด Linger เพื่อให้ปล่อยพอร์ตทันทีเมื่อสั่งปิด
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
return server
```
---
### Bug #14: การหยุดทำงานของเซิร์ฟเวอร์ทำให้ทั้งแอพ Android ดับ (`System.exit(0)`)
* **Log อาการ:**
```text
[Shutdown]: Force-stopping server execution...
Process 12345 exited with code 0 (App crashed/closed to home screen)
```
* **สาเหตุ:**
- โค้ด Python ดั้งเดิมมักเรียก `sys.exit(0)` เมื่อต้องการปิดเซิร์ฟเวอร์
- ในสภาพแวดล้อม Chaquopy บน Android กระบวนการของ Python ทำงานอยู่บน JVM Process เดียวกันกับ UI ของ Android Application การเรียก `sys.exit()` จึงทำให้ Android Process ดับทั้งแอพ
* **วิธีแก้:**
- ดักจับ `sys.exit` ใน Python:
```python
def _safe_exit(code=0):
print(f"[Sys] Intercepted sys.exit({code}), prevented Android process exit.")
raise KeyboardInterrupt()
sys.exit = _safe_exit
```
---
### Bug #15: ค้าง Thread เพราะคำสั่ง `input()` (Headless Blocking)
* **Log อาการ:**
```text
[CLI]: Enter the correct password:
(Server hangs indefinitely, no logs appear, ports not listening)
```
* **สาเหตุ:**
- สคริปต์เซิร์ฟเวอร์มีการเรียก `builtins.input()` เพื่อรอการป้อนข้อมูลจาก Console
- บน Android ไม่มี stdin ให้พิมพ์ ทำให้ Thread นั้นติดบล็อกตลอดกาล
* **วิธีแก้:**
- Mock คำสั่ง `builtins.input`:
```python
import builtins, time
def _mock_input(prompt=""):
if prompt:
print(f"[CLI]: {prompt}")
time.sleep(1)
return ""
builtins.input = _mock_input
```
---
### Bug #16: `KeyError` ใน `Manager.load_persistent` เมื่อ Persistent ขาดคีย์บางตัว
* **Log อาการ:**
```text
KeyError: 'march_path'
File "game/services/manager.py", line 45, in load_persistent
```
* **สาเหตุ:**
- ในไฟล์ `persistent.json` ที่ผู้ใช้แก้ไขเอง อาจไม่มีฟิลด์ย่อย เช่น `tb_gender`, `tb_path`, `march_path`, `skin`
- โค้ดใน `manager.pyc` เข้าถึงคีย์เหล่านี้ตรงๆ ด้วย `persistent['avatar']['march_path']`
* **วิธีแก้:**
- สร้างคลาส `SafeDict` ครอบ Dictionary ของ `persistent`:
```python
class SafeDict(dict):
def __getitem__(self, key):
if key not in self:
defaults = {
'tb_gender': 'Female',
'tb_path': 'Elation',
'march_path': 'Preservation',
'leader': 0,
'send_hotfix': True,
'key': '',
'skin': {},
'lineup': [1511, 1502, 1506, 1308],
'buff': [],
'global_passive': [],
'ex_rank': []
}
if key in defaults:
return defaults[key]
val = super().__getitem__(key)
if isinstance(val, dict) and not isinstance(val, SafeDict):
val = SafeDict(val)
self[key] = val
return val
```
---
### Bug #17: การเกิดบัคจาก ppSR License Check (`qyqwiourtyqot`)
* **Log อาการ:**
```text
Incorrect or expired password.
You can get the password for free from https://discord.gg/ppSR
Enter the correct password:
```
* **สาเหตุ:**
- การนำเอาไฟล์ `build.zip` หรือ `ppsr.pyc` จากโปรเจกต์ ppSR มาใช้ตรงๆ จะติดระบบตรวจสอบสิทธิ์ที่ผู้พัฒนา ppSR เขียนไว้
- ระบบนี้ใช้ฟังก์ชัน `qyqwiourtyqot()` ใน `util/binary.pyc` ทำ HMAC-SHA256 ตรวจสอบกับรหัสที่แจกใน Discord รายสัปดาห์
* **วิธีแก้:**
- **ห้ามนำ binary ที่คอมไพล์แล้วของ ppSR มาใส่ในระบบ GaCy**
- ใช้ตัวสร้าง Protobuf และตัวจัดการคำสั่งที่เป็นอิสระตามโค้ดในหัวข้อที่ 4