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

43 KiB

คู่มือวิเคราะห์เชิงลึกและแนวทางแก้ไขระบบเซิร์ฟเวอร์ 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 กลายเป็นค่าว่าง
  • การแก้ไขที่ถูกต้อง: ต้องตัดเอาเฉพาะส่วนหน้าก่อนเครื่องหมาย _:
    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:

{
    "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 เก่า:

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)

@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:

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:

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 อาการ:
    [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 อาการ:
    [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 ได้
  • วิธีแก้:
    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 อาการ:
    [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 อาการ:
    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
  • วิธีแก้:
    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 อาการ:
    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 อาการ:
    [Patch] persistent.json validated and auto-healed with complete avatar/scene defaults (send_hotfix=False)
    
  • สาเหตุ:
    • ในไฟล์ run_ppsr.py และ PersistentRepository.kt มีโค้ดตรวจสอบสุขภาพไฟล์ persistent.json แต่คนเขียนเดิมใส่เงื่อนไขไว้ว่า:
      if data.get("send_hotfix") is not False:
          data["send_hotfix"] = False
          modified = True
      
    • ส่งผลให้ไม่ว่าผู้ใช้จะแก้ไฟล์เป็น true อย่างไร พอเปิดแอพ เซิร์ฟเวอร์จะเซฟทับกลับเป็น false ทันที
  • วิธีแก้:
    if "send_hotfix" not in data:
        data["send_hotfix"] = True
        modified = True
    

Bug #7: การหาคีย์ Hotfix ไม่เจอเพราะ Client ส่ง Hash ต่อท้ายเวอร์ชัน

  • Log อาการ:
    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 หรือได้ค่าว่าง
  • วิธีแก้:
    clean_ver = version.split('_')[0] if version else version
    hf = hotfix.get(version) or hotfix.get(clean_ver)
    

Bug #8: เซิร์ฟเวอร์ Beta ของทางการปิดตัว (retcode: 3 / 测试资格校验失败)

  • Log อาการ:
    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 อาการ:
    [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
  • วิธีแก้:
    @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 อาการ:
    [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:
    @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 อาการ:
    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 อาการ:
    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 อาการ:
    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:
    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 อาการ:
    [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:
    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 อาการ:
    [CLI]: Enter the correct password: 
    (Server hangs indefinitely, no logs appear, ports not listening)
    
  • สาเหตุ:
    • สคริปต์เซิร์ฟเวอร์มีการเรียก builtins.input() เพื่อรอการป้อนข้อมูลจาก Console
    • บน Android ไม่มี stdin ให้พิมพ์ ทำให้ Thread นั้นติดบล็อกตลอดกาล
  • วิธีแก้:
    • Mock คำสั่ง builtins.input:
    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 อาการ:
    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:
    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 อาการ:
    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