# คู่มือวิเคราะห์เชิงลึกและแนวทางแก้ไขระบบเซิร์ฟเวอร์ 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://: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_/mdk/shield/api/', methods=['POST'])` - แต่ตัวเกม Star Rail Global ส่งเมธอด **`GET`** มาที่ `loadConfig` * **วิธีแก้:** ```python @app.route('//mdk/shield/api/loadConfig', methods=['GET', 'POST']) @app.route('/hkrpg_/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