fix: restore battle entry and character profile initialization
This commit is contained in:
1 parent
cfbbc7b970
commit
5c4d0d4149
16 files changed
+888
-228
No files matched your search
@@ -138,12 +138,13 @@ go test ./...
|
||||
| รายการ | สถานะ |
|
||||
| --- | --- |
|
||||
| Error 2000 ตอน login | แก้ `GetCurBattleInfo` ให้ตอบสำเร็จพร้อมข้อมูลว่างเมื่อไม่มี battle; รอยืนยันในเกม |
|
||||
| Calyx เข้า fight ไม่ได้ | ยังอยู่ระหว่างตรวจ `NullReferenceException` ใน `CreateBattleServerCheckResult` |
|
||||
| ตีมอนแล้วค้างหน้า Weakness | พบ stack trace ในฟังก์ชันตรวจ battle เดียวกับ Calyx; ยังไม่ยืนยันว่าแก้แล้ว |
|
||||
| Calyx | ผู้ใช้ยืนยันว่าเข้า Battle ได้แล้ว 17 ต.ค. หลังแก้เส้นทาง BattleInfo |
|
||||
| ตีมอนแล้วค้างหน้า Weakness | ผู้ใช้ยืนยัน Stagnant Shadow ที่เรียกมอนออกมาให้ตีเข้าสู้ได้แล้ว 17 ต.ค.; log มี `PVEBattleResult` ส่วนมอนทั่วไปยังไม่ได้ยืนยันแยก |
|
||||
| เปิดหน้าตัวละครแล้วค้าง | ผู้ใช้ยืนยันว่าหายหลังเปิดเกมใหม่; ไม่พบ PlayerBoard/Inventory error ใน log รอบใหม่ |
|
||||
| วาร์ปแมพใหม่ | ปรับลำดับ response/notify และข้อมูล scene ตาม upstream; ยังต้องยืนยันปลายทางในเกม |
|
||||
| `SceneCastSkill` | ใช้รายการ monster entity ที่ server สร้างเพื่อตรวจเป้าหมายก่อนส่ง battle |
|
||||
|
||||
ข้อมูลล่าสุดที่ตรวจในซอร์สและเอกสาร: **16 ตุลาคม 2026**
|
||||
ข้อมูลล่าสุดที่ตรวจในซอร์สและเอกสาร: **17 ตุลาคม 2026**
|
||||
|
||||
## เก็บข้อมูลเมื่อพบปัญหา
|
||||
|
||||
@@ -166,7 +167,20 @@ go test ./...
|
||||
|
||||
สำหรับการตรวจ 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 ไม่ใช่การยืนยันว่าอาการค้างหายแล้ว
|
||||
ประวัติรอบ 16 ต.ค. พบ main affix ผิดใน inventory ชุดเดิมและแก้ข้อมูลไปแล้ว ผลนั้นไม่ใช่หลักฐานว่า inventory ที่นำเข้าภายหลังหรือการเข้า Battle ปัจจุบันผ่าน ดูผลแต่ละรอบในบันทึกตรวจสอบ
|
||||
|
||||
### การแก้ Battle รอบ 17 ตุลาคม 2026
|
||||
|
||||
- `SceneCastSkill` อ่านทั้ง hit targets, assist IDs และ assist groups และตอบ `MonsterBattleType=TRIGGER_BATTLE` ตาม enum ของต้นฉบับ; ค่าเริ่มต้นศูนย์หมายถึง `NONE`
|
||||
- โหลดธาตุตัวละครจาก `resources/AvatarConfig.json` เพิ่มเติม เพราะ `avatarConfigs` ใน `res.json` อาจว่าง
|
||||
- กฎบัฟเฉพาะตัวละคร, global passive, entry attack และ invasion อยู่ใน `resources/BattleRules.json` พร้อม upstream revision ต้องนำไฟล์นี้ไปด้วยเมื่อแจก binary; ไม่มีการฝัง ID ของกฎเหล่านี้เป็น fallback ในตัว executable
|
||||
- Battle SP แปลงจากอัตราส่วนในข้อมูลที่นำเข้าเป็นสเกล 10000 และ HP มอนที่เป็นศูนย์คงศูนย์ตามรูปแบบ upstream
|
||||
- Invasion ใช้สถานะ challenge ที่โหลดจริง `battle_config.enable_invasion_support` เปิดบัฟ support เพิ่มได้ (ค่าเริ่มต้น false)
|
||||
- เลิกสร้าง Material ID ด้วยสูตร `130000 + AvatarID` ซึ่ง Client หาไม่พบ และไม่ตอบข้อมูลว่างให้โมดูล RogueTourn/ChenLing ที่ยังไม่รองรับ
|
||||
|
||||
สถานะรอบนี้: build ผ่าน วาง binary ที่ root แล้ว ผู้ใช้ยืนยันทั้ง Calyx และ Stagnant Shadow เข้า Battle ได้ โดย log ของเส้นทางตีมอนเวลา 11:53:08 มี BattleInfo และเวลา 11:53:16 มี `PVEBattleResult` ผลนี้ยังไม่ใช่การรับรองโหมดต่อสู้ทุกประเภท
|
||||
|
||||
แก้โปรไฟล์เพิ่มเติม: field 4 ของ GetPlayerBoardDataScRsp คือการ์ดส่วนตัว, field 8 คือรูปโปรไฟล์ และ field 9 คือตัวละครช่วยเหลือ โค้ดเดิมใส่ ID ผิดประเภทและไม่มี head frame/display data ปรับให้ใช้ `resources/AvatarPlayerIcon.json`, `resources/ProfileConfig.json` และทีมจริง พร้อมเติม ArchiveData ทั้งสอง response ผู้ใช้ยืนยันหน้าตัวละครหายค้างหลังเปิดเกมใหม่
|
||||
|
||||
## โครงสร้างซอร์ส
|
||||
|
||||
|
||||
@@ -75,3 +75,41 @@ Calyx และ overworld ตายที่ stack เดียวกัน ส
|
||||
4. เปลี่ยนเฉพาะส่วนที่มีหลักฐาน แล้วตรวจ Calyx และ overworld battle แยกกัน
|
||||
|
||||
สถานะ: ยังไม่มีการยืนยันว่าเข้า fight ได้
|
||||
|
||||
## รอบ 17 ตุลาคม 2026: แก้เส้นทางร่วมและรอผล Client
|
||||
|
||||
ตรวจ source ปัจจุบันก่อนแก้แล้วพบดังนี้:
|
||||
|
||||
1. `HitMonsterBattleInfo` ไม่ได้ตั้ง `monster_battle_type` จึงส่งค่า NONE=0 แทน TRIGGER_BATTLE=1 (อ้างอิง enum ใน `protocol/src/protocol.pb.zig` ของ revision ที่ระบุข้างต้น) แก้ให้ระบุ enum ชัดเจน
|
||||
2. เพิ่มการอ่าน `assist_monster_entity_info.entity_id_list` ควบคู่รายการเป้าหมายอีกสองช่อง ยังคงตรวจว่าเป็นมอนที่ส่งในฉากจริง และบันทึก incoming IDs, รายการมอนในฉากและผลจับคู่ ไม่มีการสร้างเป้าหมายปลอม
|
||||
3. `res.json` ชุดปัจจุบันมี `avatarConfigs` ว่าง ตัวโหลดเดิมไม่ได้ใช้ `AvatarConfig.json` จึงหา entry attack buff ไม่ได้ เพิ่มการโหลดธาตุจากตารางนี้และ mapping ธาตุจาก resource กฎ Battle
|
||||
4. ปรับ owner, targets และ dynamic values ของบัฟตาม PearlSR รวมถึง entry attack สำหรับ Calyx แยก invasion ของ challenge ออกจาก custom stage และสร้าง map ก่อนใส่ battle targets
|
||||
5. กฎเฉพาะตัวละครแยกเป็น `resources/BattleRules.json` ระบุ revision ต้นทาง รองรับการเปิด/ปิด global passive จาก persistent ตามเดิม
|
||||
6. แปลง Battle SP จากสัดส่วน Cur/Max เป็นสเกล 10000 และเลิกบังคับ Monster MaxHP=10000 เมื่อค่าต้นทางเป็นศูนย์
|
||||
7. ลบการเดา Material IDs ด้วย 130000+AvatarID ซึ่งตรงกับ `TryCreateItemData fail` ใน Client และกัน automatic empty response ของ RogueTourn/ChenLing ที่มี NRE ใน log
|
||||
|
||||
ข้อจำกัด: log ตีมอนล่าสุดก่อนแก้แสดงว่าไม่พบเป้าหมายที่ผ่าน filter แต่ไม่มี incoming ID list จึงยังไม่ทราบเหตุที่จับคู่ไม่ผ่าน ส่วน capture Calyx ที่มีอยู่เป็นข้อมูลวันที่ 16 ต.ค. ไม่ใช่ทีมที่นำเข้าภายหลัง ชุด build รอบนี้เพิ่มข้อมูลที่จำเป็นต่อการแยกสองกรณีนี้
|
||||
|
||||
การคัดลอก config.json จาก PearlSR ไม่ได้ทำให้ Go อ่านไฟล์นั้น Go ยังคงใช้ freesr-data.json/persistent สำหรับทีมและ custom battle การใช้ custom stage กับดอกไม้มีในต้นฉบับเช่นกัน จึงไม่ถือเป็น regression ด้วยเหตุผลนั้นเพียงข้อเดียว
|
||||
|
||||
Build ผ่านและวาง gameserver-go.exe ที่ root แล้ว เปิด SDK/Game server พร้อมคอนโซลเพื่อให้ผู้ใช้ลองทั้ง Calyx และ overworld ผลใน Client ยังรอยืนยัน ไม่มีการแก้ข้อมูลทีมใน freesr-data.json
|
||||
|
||||
### ผลลองจริง 11:53 และการแก้หน้าตัวละคร
|
||||
|
||||
- 11:53:08 `SceneCastSkill` รับ assist target 30007 และจับคู่กับมอนในฉากได้ ส่ง response พร้อม BattleInfo ขนาด 2286 ไบต์ snapshot SHA-256 battle `f9bb3cf79debf6678f9f466e917e6625e87793aa5fbaf275d90a3f2f39a1fe94` stage 30511021
|
||||
- 11:53:16 Client ส่ง PVEBattleResultCsReq และผู้ใช้ยืนยันว่าเข้าสู้ได้แล้ว ยังไม่ระบุผลดอกไม้โดยตรง
|
||||
- พบตัวขวางการลองเพิ่ม: เปิด C แล้วติดหน้าตัวละคร Log มี `_SyncPlayerBoardData`, Inventory `_AddUniqueItems`/`_AddItem` และ GetItemsInTypes
|
||||
- เทียบต้นฉบับพบ field 4 ของ GetPlayerBoardDataScRsp เป็น unlocked_personal_card_list แต่ Go ส่ง ID รูปโปรไฟล์; field 9 เป็น assist_avatar_id_list แต่ Go ส่ง ID การ์ด อีกทั้งไม่ได้ตั้ง HeadFrameInfo และ DisplayAvatarVec
|
||||
- ใน assembly จุด return address 0xdd02ad8 อยู่หลัง throw ที่ 0xdd02ad3 ซึ่งรับ branch จากการตรวจ ItemData+0x18 เป็น null ที่ 0xdd02841 สอดคล้องกับการส่ง ID ของผิดประเภท ยังไม่อ้างว่าเป็นข้อมูลอาวุธ/relic เสีย
|
||||
- แก้ profile fields ให้ตรง schema, อ่าน head icons จาก AvatarPlayerIcon.json และ owned cosmetics จาก ProfileConfig.json ที่ระบุ revision เติม ArchiveData จากข้อมูลผู้เล่นแทน null และไม่ตอบว่างให้ SocialPlay/PetMarble ซึ่งพบ exception เพิ่มในรอบใหม่
|
||||
- Build รอบโปรไฟล์ผ่านและแทนที่ root executable พร้อมเปิดเซิร์ฟเวอร์ใหม่แล้ว ผลหน้าตัวละครรอเปิดเกมใหม่ยืนยัน
|
||||
|
||||
ผู้ใช้ชี้แจงผลสำเร็จว่าเป็น Stagnant Shadow สำหรับวัสดุเลื่อนขั้นที่เรียกมอนออกมาจากเงาแล้วตี ไม่ใช่ผลทดสอบ Calyx หรือมอนทั่วไปทั้งหมด คง Calyx เป็นรายการที่ยังรอตรวจ
|
||||
|
||||
หลังเปิดเกมรอบ 11:56 ผู้ใช้ยืนยันอาการหน้าตัวละครหายแล้ว และ Player.log รอบใหม่ไม่พบ PlayerBoard/Inventory/Archive exceptions เดิม ยังพบ `_OnGetCurBattleInfoScRsp` ตอน login หนึ่งครั้ง ซึ่งต้องแยกจากผลสำเร็จของการตีมอน และยังไม่มี Calyx request ใหม่ใน log รอบนี้
|
||||
|
||||
### ยืนยันผลท้ายรอบ
|
||||
|
||||
ผู้ใช้ตอบคำถามแยกเฉพาะ Calyx ว่า **Calyx เข้าได้แล้ว** จึงยืนยันผลลองใน Client ได้ทั้ง Calyx, Stagnant Shadow ผ่าน SceneCastSkill และการเปิดหน้าตัวละคร หลักฐาน Calyx ในส่วนนี้เป็นคำยืนยันโดยตรงของผู้ใช้ ไม่ใช่การอนุมานจาก build หรือจาก log ของ Stagnant Shadow ผลยังไม่ครอบคลุมโหมด challenge/มอนทั่วไปทุกชนิด และ login GetCurBattleInfo exception เป็นปัญหาที่บันทึกแยกไว้
|
||||
|
||||
ตรวจ log หลังคำยืนยันพบ Calyx request cmd 1305 เวลา 11:56:29 และ response cmd 1371 ขนาด 2279 ไบต์ capture เป็น StartQuickCocoonStageRsp stage 30511021, battle SHA-256 `092528d29fc9bdfc025b89d8904d8a4f3ca706bb2cf777726b03f5a29d9233f7` จึงมีหลักฐาน response ของรอบเดียวกันประกอบคำยืนยันด้วย
|
||||
@@ -199,6 +199,23 @@ func (a *AvatarJson) SPMaxVal() uint32 {
|
||||
return 10000
|
||||
}
|
||||
|
||||
// Battle SP is a ratio in ten-thousandths (PearlSR createBattleAvatar).
|
||||
// FreeSR imports may instead carry absolute energy and its capacity; use
|
||||
// their ratio so both absolute and already scaled imports serialize alike.
|
||||
const battleRatioScale uint32 = 10000
|
||||
|
||||
func (a *AvatarJson) BattleSPCur() uint32 {
|
||||
maximum := a.SPMaxVal()
|
||||
if maximum == 0 {
|
||||
return 0
|
||||
}
|
||||
current := a.SPCur()
|
||||
if current > maximum {
|
||||
current = maximum
|
||||
}
|
||||
return uint32(uint64(current) * uint64(battleRatioScale) / uint64(maximum))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Battle config (freesr-data.json)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -253,15 +270,16 @@ type RogueMagicScepter struct {
|
||||
}
|
||||
|
||||
type BattleConfig struct {
|
||||
BattleType BattleType `json:"battle_type"`
|
||||
Monsters [][]Monster `json:"monsters"`
|
||||
Blessings []BattleBuffJson `json:"blessings"`
|
||||
StageID uint32 `json:"stage_id"`
|
||||
CycleCount uint32 `json:"cycle_count"`
|
||||
PathResonanceID uint32 `json:"path_resonance_id"`
|
||||
CustomStats []SubAffix `json:"custom_stats"`
|
||||
Scepters []RogueMagicScepter `json:"scepters"`
|
||||
CustomBattleLineup map[uint32]uint32 `json:"custom_battle_lineup"`
|
||||
EnableInvasionSupport bool `json:"enable_invasion_support"`
|
||||
BattleType BattleType `json:"battle_type"`
|
||||
Monsters [][]Monster `json:"monsters"`
|
||||
Blessings []BattleBuffJson `json:"blessings"`
|
||||
StageID uint32 `json:"stage_id"`
|
||||
CycleCount uint32 `json:"cycle_count"`
|
||||
PathResonanceID uint32 `json:"path_resonance_id"`
|
||||
CustomStats []SubAffix `json:"custom_stats"`
|
||||
Scepters []RogueMagicScepter `json:"scepters"`
|
||||
CustomBattleLineup map[uint32]uint32 `json:"custom_battle_lineup"`
|
||||
}
|
||||
|
||||
func DefaultBattleConfig() BattleConfig {
|
||||
|
||||
@@ -206,8 +206,8 @@ func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, rel
|
||||
EnhancedId: enhancedID,
|
||||
IGDEHGIKFJA: exRanks,
|
||||
SpBar: &goproto.SpBarInfo{
|
||||
CurSp: a.SPCur(),
|
||||
MaxSp: a.SPMaxVal(),
|
||||
CurSp: a.BattleSPCur(),
|
||||
MaxSp: battleRatioScale,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -227,11 +227,12 @@ func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, rel
|
||||
var buffs []*goproto.BattleBuff
|
||||
for _, buffID := range a.Techniques {
|
||||
buffs = append(buffs, &goproto.BattleBuff{
|
||||
Id: buffID,
|
||||
Level: 1,
|
||||
OwnerId: index,
|
||||
WaveFlag: 0xffffffff,
|
||||
DynamicValues: map[string]float32{"SkillIndex": 2.0},
|
||||
Id: buffID,
|
||||
Level: 1,
|
||||
OwnerId: index,
|
||||
WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{0},
|
||||
DynamicValues: map[string]float32{"SkillIndex": 0},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -310,13 +311,10 @@ func ToLineupInfo(lineups map[uint32]uint32) *goproto.LineupInfo {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (m *Monster) ToSceneMonster() *goproto.SceneMonster {
|
||||
hp := m.MaxHP
|
||||
if hp == 0 {
|
||||
hp = 10000
|
||||
}
|
||||
return &goproto.SceneMonster{
|
||||
MonsterId: m.MonsterID,
|
||||
MaxHp: hp,
|
||||
// Zero requests the stage's normal HP, as in PearlSR addMonsterWaves.
|
||||
MaxHp: m.MaxHP,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -75,6 +75,7 @@ type LevelOutputConfig struct {
|
||||
|
||||
type AvatarConfig struct {
|
||||
WeaknessBuffID uint32 `json:"weaknessBuffId"`
|
||||
DamageType string `json:"damageType"`
|
||||
}
|
||||
|
||||
type JsonConfig struct {
|
||||
@@ -185,6 +186,23 @@ func GameRes() (*JsonConfig, error) {
|
||||
}
|
||||
|
||||
func loadSupplementaryResources(res *JsonConfig) {
|
||||
// PearlSR's resource export keeps avatar data in AvatarConfig.json;
|
||||
// res.json may legitimately contain an empty avatarConfigs object.
|
||||
if raw, err := readResourceFile("resources/AvatarConfig.json"); err == nil {
|
||||
var avatars struct {
|
||||
Entries []struct {
|
||||
ID uint32 `json:"AvatarID"`
|
||||
DamageType string `json:"DamageType"`
|
||||
} `json:"avatar_config"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &avatars); err == nil {
|
||||
for _, avatar := range avatars.Entries {
|
||||
cfg := res.AvatarConfigs[avatar.ID]
|
||||
cfg.DamageType = avatar.DamageType
|
||||
res.AvatarConfigs[avatar.ID] = cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
entrancesByEntry := map[uint32]MapEntranceEntry{}
|
||||
if raw, err := readResourceFile("resources/MapEntrance.json", "MapEntrance.json"); err == nil {
|
||||
var list []MapEntranceEntry
|
||||
@@ -369,4 +387,3 @@ func Teleports() map[uint32]map[uint32]TeleportInfo {
|
||||
})
|
||||
return teleportDB
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
// Game IDs and character exceptions are resource data with an upstream revision,
|
||||
// not executable defaults. Missing or invalid rules must not silently produce a
|
||||
// different battle.
|
||||
type battleRules struct {
|
||||
EntryAttackBuffs map[string]uint32 `json:"entry_attack_buffs"`
|
||||
Source struct {
|
||||
Revision string `json:"revision"`
|
||||
} `json:"source"`
|
||||
TechniqueBuffs map[uint32][]struct {
|
||||
ID uint32 `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
DynamicValues map[string]float32 `json:"dynamic_values"`
|
||||
} `json:"technique_buffs"`
|
||||
GlobalPassives []struct {
|
||||
Setting string `json:"setting"`
|
||||
BuffID uint32 `json:"buff_id"`
|
||||
OwnerSlot uint32 `json:"owner_slot"`
|
||||
} `json:"global_passives"`
|
||||
IgnoreToughness struct {
|
||||
Avatars []uint32 `json:"avatars"`
|
||||
BuffID uint32 `json:"buff_id"`
|
||||
} `json:"ignore_toughness"`
|
||||
BattleTargets []struct {
|
||||
BattleType string `json:"battle_type"`
|
||||
Targets map[uint32][]struct {
|
||||
ID uint32 `json:"id"`
|
||||
Progress uint32 `json:"progress"`
|
||||
TotalProgress uint32 `json:"total_progress"`
|
||||
} `json:"targets"`
|
||||
} `json:"battle_targets"`
|
||||
InvasionBuffs map[uint32]uint32 `json:"invasion_buffs"`
|
||||
InvasionSupportBuff uint32 `json:"invasion_support_buff"`
|
||||
}
|
||||
|
||||
var battleRulesOnce sync.Once
|
||||
var loadedBattleRules battleRules
|
||||
var battleRulesErr error
|
||||
|
||||
func loadBattleRules() (*battleRules, error) {
|
||||
battleRulesOnce.Do(func() {
|
||||
raw, err := os.ReadFile("resources/BattleRules.json")
|
||||
if err != nil {
|
||||
battleRulesErr = err
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &loadedBattleRules); err != nil {
|
||||
battleRulesErr = err
|
||||
return
|
||||
}
|
||||
if loadedBattleRules.Source.Revision == "" {
|
||||
battleRulesErr = fmt.Errorf("battle rules need a source revision")
|
||||
return
|
||||
}
|
||||
for _, rules := range loadedBattleRules.TechniqueBuffs {
|
||||
for _, rule := range rules {
|
||||
if rule.ID == 0 || (rule.Owner != "avatar" && rule.Owner != "leader") {
|
||||
battleRulesErr = fmt.Errorf("invalid technique buff rule: %+v", rule)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, rule := range loadedBattleRules.GlobalPassives {
|
||||
if rule.BuffID == 0 || rule.OwnerSlot >= 4 {
|
||||
battleRulesErr = fmt.Errorf("invalid global passive rule: %+v", rule)
|
||||
return
|
||||
}
|
||||
switch rule.Setting {
|
||||
case "enable_sw_global", "enable_castorice_global", "enable_aha_global":
|
||||
default:
|
||||
battleRulesErr = fmt.Errorf("unknown global passive setting %q", rule.Setting)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if battleRulesErr != nil {
|
||||
return nil, fmt.Errorf("load resources/BattleRules.json: %w", battleRulesErr)
|
||||
}
|
||||
return &loadedBattleRules, nil
|
||||
}
|
||||
|
||||
func battleBuff(id, owner uint32, dynamic map[string]float32) *goproto.BattleBuff {
|
||||
values := map[string]float32{"SkillIndex": 0}
|
||||
for key, value := range dynamic {
|
||||
values[key] = value
|
||||
}
|
||||
return &goproto.BattleBuff{Id: id, Level: 1, OwnerId: owner,
|
||||
WaveFlag: ^uint32(0), TargetIndexList: []uint32{0}, DynamicValues: values}
|
||||
}
|
||||
|
||||
func (r *battleRules) techniqueBuffs(a *common.AvatarJson, slot, leader uint32, imported []*goproto.BattleBuff) []*goproto.BattleBuff {
|
||||
if len(a.Techniques) == 0 {
|
||||
return nil
|
||||
}
|
||||
buffs := imported
|
||||
// Explicit character rules in PearlSR replace the generic technique. The
|
||||
// user's empty techniques array still disables techniques for that avatar.
|
||||
if rules, ok := r.TechniqueBuffs[a.AvatarID]; ok {
|
||||
buffs = nil
|
||||
for _, rule := range rules {
|
||||
owner := slot
|
||||
if rule.Owner == "leader" {
|
||||
owner = leader
|
||||
}
|
||||
buffs = append(buffs, battleBuff(rule.ID, owner, rule.DynamicValues))
|
||||
}
|
||||
}
|
||||
if containsU32(r.IgnoreToughness.Avatars, a.AvatarID) {
|
||||
found := false
|
||||
for _, buff := range buffs {
|
||||
if buff.Id == r.IgnoreToughness.BuffID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
buffs = append(buffs, battleBuff(r.IgnoreToughness.BuffID, slot, nil))
|
||||
}
|
||||
}
|
||||
// Global passives are handled once, with their configured owner and setting.
|
||||
result := make([]*goproto.BattleBuff, 0, len(buffs))
|
||||
for _, buff := range buffs {
|
||||
global := false
|
||||
for _, rule := range r.GlobalPassives {
|
||||
if buff.Id == rule.BuffID {
|
||||
global = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !global {
|
||||
result = append(result, buff)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *battleRules) addGlobalPassives(battle *goproto.SceneBattleInfo, player *common.FreesrData) {
|
||||
for _, rule := range r.GlobalPassives {
|
||||
var enabled *bool
|
||||
switch rule.Setting {
|
||||
case "enable_sw_global":
|
||||
enabled = player.EnableSWGlobal
|
||||
case "enable_castorice_global":
|
||||
enabled = player.EnableCastoriceGlobal
|
||||
case "enable_aha_global":
|
||||
enabled = player.EnableAhaGlobal
|
||||
}
|
||||
if enabled != nil && !*enabled {
|
||||
continue
|
||||
}
|
||||
owner := rule.OwnerSlot
|
||||
// A shorter lineup must not refer to an absent actor.
|
||||
present := false
|
||||
for _, avatar := range battle.BattleAvatarList {
|
||||
if avatar.Index == owner {
|
||||
present = true
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
owner = battle.BattleAvatarList[0].Index
|
||||
}
|
||||
battle.BuffList = append(battle.BuffList, battleBuff(rule.BuffID, owner, nil))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *battleRules) addTargets(battle *goproto.SceneBattleInfo, kind common.BattleType) {
|
||||
for _, rule := range r.BattleTargets {
|
||||
if !strings.EqualFold(rule.BattleType, string(kind)) {
|
||||
continue
|
||||
}
|
||||
for key, targets := range rule.Targets {
|
||||
list := &goproto.BattleTargetList{}
|
||||
for _, target := range targets {
|
||||
list.BattleTargetList = append(list.BattleTargetList, &goproto.BattleTarget{
|
||||
Id: target.ID, Progress: target.Progress, TotalProgress: target.TotalProgress})
|
||||
}
|
||||
battle.BattleTargetInfo[key] = list
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,11 @@ func rawNamed(request string, fn func(*Session, []byte) error) {
|
||||
var dangerFamilies = []string{
|
||||
"GridFight", "ChessRogue", "EvolveBuild", "ChimeraDuel",
|
||||
"Jukebox", "CycleScore", "MusicAlbum", "DrinkMaker",
|
||||
// Player.log 2026-10-17: these callbacks dereference state absent from
|
||||
// the automatic empty response. Leave unsupported modules unanswered.
|
||||
"RogueTourn", "ChenLing",
|
||||
// Same failure observed in the fresh verification login.
|
||||
"SocialPlay", "PetMarble",
|
||||
}
|
||||
|
||||
func isDangerCmd(cmdID uint16) bool {
|
||||
|
||||
@@ -10,6 +10,10 @@ import (
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
// PearlSR 005d2c1 protocol/src/protocol.pb.zig: MonsterBattleType.
|
||||
// The Go schema retains obfuscated enum labels; zero is NONE, not TRIGGER.
|
||||
const monsterBattleTrigger goproto.MonsterBattleType = 1
|
||||
|
||||
func init() {
|
||||
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))
|
||||
@@ -17,7 +21,10 @@ func init() {
|
||||
rsp.PropEntityId = req.PropEntityId
|
||||
rsp.CocoonId = req.CocoonId
|
||||
rsp.Wave = req.Wave
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
battleInfo, err := createBattleInfo(s, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
rsp.BattleInfo = battleInfo
|
||||
return nil
|
||||
@@ -29,7 +36,10 @@ func init() {
|
||||
rsp.Retcode = 0
|
||||
rsp.CocoonId = req.CocoonId
|
||||
rsp.Wave = req.Wave
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
battleInfo, err := createBattleInfo(s, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
rsp.BattleInfo = battleInfo
|
||||
@@ -38,7 +48,10 @@ func init() {
|
||||
|
||||
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))
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
battleInfo, err := createBattleInfo(s, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
rsp.Retcode = 0
|
||||
@@ -86,7 +99,10 @@ func init() {
|
||||
})
|
||||
|
||||
handle("SceneEnterStage", func(s *Session, req *goproto.SceneEnterStageCsReq, rsp *goproto.SceneEnterStageScRsp) error {
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
battleInfo, err := createBattleInfo(s, 0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
rsp.BattleInfo = battleInfo
|
||||
return nil
|
||||
@@ -100,26 +116,8 @@ func init() {
|
||||
return nil
|
||||
})
|
||||
|
||||
// ================================================================
|
||||
// SceneCastSkill — THE CAMERA-DETACH BUG FIX
|
||||
// (บัคกล้องหลุดจากตัวละครตอนกดตี)
|
||||
//
|
||||
// อาการ: กดโจมตีใน overworld แล้วกล้องหลุดออกจากตัวละคร
|
||||
//
|
||||
// สาเหตุ (จากโค้ด Rust เดิม): handler ตอบ_info + monster_battle_info
|
||||
// ให้ client เข้าสู่การต่อสู้กับ target ที่ "ผ่าน filter แบบหลวมๆ"
|
||||
// filter เดิม: id > 30000 (monster ในฉาก) || id < 1000 (avatar entity ของผู้เล่น!)
|
||||
// ถ้า targets ว่างและอยู่ใน arena (entry >= 3000000) ยัง "หลอก" target
|
||||
// เป็น 30001 อีกชั้น
|
||||
// พอ client ได้_info กับ "monster" ที่จริงคือ prop/NPC/ตัวผู้เล่นเอง
|
||||
// หรือ monster ที่ไม่มี event_id มันจะเริ่ม transition ที่โหลดไม่สำเร็จ
|
||||
// → camera ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร
|
||||
//
|
||||
// FIX: ยอมรับเฉพาะ entity ที่ server spawn เป็น "monster จริง" ในฉากปัจจุบัน
|
||||
// (เก็บไว้ใน Session.MonsterEntities ตอน load_scene) ถ้าไม่มี monster จริง
|
||||
// ถูกโจมตี → ตอบ retcode 0 + cast_entity_id เฉยๆ ไม่เข้า
|
||||
// client จะเล่นแอนิเมชันตีปกติ กล้องค้างกับตัวละคร
|
||||
// ================================================================
|
||||
// Consume every target carrier in the current schema. Only entities sent
|
||||
// as monsters in this scene may start a battle; never fabricate a target.
|
||||
handle("SceneCastSkill", func(s *Session, req *goproto.SceneCastSkillCsReq, rsp *goproto.SceneCastSkillScRsp) error {
|
||||
rsp.CastEntityId = req.CastEntityId
|
||||
|
||||
@@ -138,6 +136,14 @@ func init() {
|
||||
for _, id := range req.AssistMonsterEntityIdList {
|
||||
addTarget(id)
|
||||
}
|
||||
for _, group := range req.AssistMonsterEntityInfo {
|
||||
for _, id := range group.GetEntityIdList() {
|
||||
addTarget(id)
|
||||
}
|
||||
}
|
||||
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d skill_index=%d hits=%v assists=%v groups=%v scene_monsters=%v matched=%v",
|
||||
req.CastEntityId, req.CasterId, req.SkillIndex, req.HitTargetEntityIdList,
|
||||
req.AssistMonsterEntityIdList, req.AssistMonsterEntityInfo, s.MonsterEntities, targets)
|
||||
|
||||
if len(targets) == 0 {
|
||||
// ไม่มี monster จริงถูกตี — ไม่เข้า (นี่คือ fix หลัก)
|
||||
@@ -150,12 +156,16 @@ func init() {
|
||||
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> with %d monster(s)",
|
||||
req.CastEntityId, req.CasterId, len(targets))
|
||||
|
||||
battleInfo := createBattleInfo(s, req.CasterId, req.SkillIndex)
|
||||
battleInfo, err := createBattleInfo(s, req.CasterId, req.SkillIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
for _, id := range targets {
|
||||
rsp.MonsterBattleInfo = append(rsp.MonsterBattleInfo, &goproto.HitMonsterBattleInfo{
|
||||
TargetMonsterEntityId: id,
|
||||
MonsterBattleType: monsterBattleTrigger,
|
||||
})
|
||||
}
|
||||
rsp.BattleInfo = battleInfo
|
||||
@@ -183,23 +193,26 @@ func init() {
|
||||
}
|
||||
|
||||
// createBattleInfo is the Go port of.rs create_battle_info.
|
||||
func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBattleInfo {
|
||||
func createBattleInfo(s *Session, casterID, skillIndex uint32) (*goproto.SceneBattleInfo, error) {
|
||||
player := s.JSON()
|
||||
if player == nil {
|
||||
Log("data is not set!")
|
||||
return &goproto.SceneBattleInfo{}
|
||||
return nil, fmt.Errorf("battle: player data is not loaded")
|
||||
}
|
||||
|
||||
rules, err := loadBattleRules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
battleInfo := &goproto.SceneBattleInfo{
|
||||
StageId: player.BattleConfig.StageID,
|
||||
LogicRandomSeed: randUint32(),
|
||||
BattleId: 1,
|
||||
RoundsLimit: player.BattleConfig.CycleCount,
|
||||
WorldLevel: 6,
|
||||
StageId: player.BattleConfig.StageID,
|
||||
LogicRandomSeed: randUint32(),
|
||||
BattleId: 1,
|
||||
RoundsLimit: player.BattleConfig.CycleCount,
|
||||
WorldLevel: 6,
|
||||
BattleTargetInfo: make(map[uint32]*goproto.BattleTargetList),
|
||||
}
|
||||
|
||||
isCalyx := casterID == 0 && skillIndex == 0
|
||||
hasDahlia := false
|
||||
firstAvatarID := uint32(0)
|
||||
firstAvatarIdx := uint32(9999)
|
||||
|
||||
@@ -217,14 +230,6 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
|
||||
// avatars
|
||||
lineup(func(avatarIndex, lineupID uint32) {
|
||||
if firstAvatarID == 0 {
|
||||
firstAvatarID = lineupID
|
||||
firstAvatarIdx = avatarIndex
|
||||
}
|
||||
if !hasDahlia {
|
||||
hasDahlia = lineupID == 1321
|
||||
}
|
||||
|
||||
avatarID := lineupID
|
||||
isTrailblazer := lineupID == 8001
|
||||
isMarch := lineupID == 1001
|
||||
@@ -239,81 +244,51 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
return
|
||||
}
|
||||
|
||||
if firstAvatarID == 0 {
|
||||
firstAvatarID = avatarID
|
||||
firstAvatarIdx = avatarIndex
|
||||
}
|
||||
|
||||
battleAvatar, techs := avatar.ToBattleAvatarProto(
|
||||
avatarIndex,
|
||||
player.FindLightconeFor(avatarID),
|
||||
player.FindRelicsFor(avatarID),
|
||||
)
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, techs...)
|
||||
|
||||
res, err := common.GameRes()
|
||||
if casterID > 0 && avatarIndex == casterID-1 && err == nil {
|
||||
if avatarConfig, okc := res.AvatarConfigs[avatarID]; okc && !containsU32(avatar.Techniques, 1000119) {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: avatarConfig.WeaknessBuffID,
|
||||
Level: 1,
|
||||
OwnerId: avatarIndex,
|
||||
WaveFlag: 0xffffffff,
|
||||
DynamicValues: map[string]float32{"SkillIndex": float32(skillIndex)},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, rules.techniqueBuffs(avatar, avatarIndex, firstAvatarIdx, techs)...)
|
||||
battleInfo.BattleAvatarList = append(battleInfo.BattleAvatarList, battleAvatar)
|
||||
|
||||
// hardcoded march buff
|
||||
if avatar.AvatarID == 1224 {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 122401,
|
||||
Level: 3,
|
||||
WaveFlag: 0xffffffff,
|
||||
OwnerId: avatarIndex,
|
||||
DynamicValues: map[string]float32{"#ADF_1": 3, "#ADF_2": 3},
|
||||
TargetIndexList: []uint32{0},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// resolve character-specific technique buffs dynamically
|
||||
firstAvatarAttackID := uint32(0)
|
||||
if res, err := common.GameRes(); err == nil {
|
||||
if cfg, ok := res.AvatarConfigs[firstAvatarID]; ok {
|
||||
firstAvatarAttackID = cfg.WeaknessBuffID
|
||||
}
|
||||
if len(battleInfo.BattleAvatarList) == 0 {
|
||||
return nil, fmt.Errorf("battle: lineup has no available avatars")
|
||||
}
|
||||
|
||||
len0 := len(battleInfo.BuffList)
|
||||
hasReplaced := false
|
||||
var dahliaBuffs []*goproto.BattleBuff
|
||||
for i, buff := range battleInfo.BuffList {
|
||||
isLast := i == len0-1
|
||||
|
||||
if buff.Id == 141202 || buff.Id == 141403 {
|
||||
buff.OwnerId = firstAvatarIdx
|
||||
continue
|
||||
}
|
||||
|
||||
if hasDahlia && isCalyx && buff.Id >= 1000111 && buff.Id <= 1000117 && !hasReplaced && firstAvatarAttackID != 0 {
|
||||
buff.Id = firstAvatarAttackID
|
||||
buff.OwnerId = firstAvatarIdx
|
||||
hasReplaced = true
|
||||
}
|
||||
|
||||
if hasDahlia && isCalyx && !hasReplaced && firstAvatarAttackID != 0 && isLast {
|
||||
dahliaBuffs = append(dahliaBuffs,
|
||||
&goproto.BattleBuff{
|
||||
Id: 1000121, Level: 1, WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{firstAvatarIdx},
|
||||
},
|
||||
&goproto.BattleBuff{
|
||||
Id: firstAvatarAttackID, Level: 1, WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{firstAvatarIdx},
|
||||
DynamicValues: map[string]float32{"SkillIndex": float32(firstAvatarIdx)},
|
||||
},
|
||||
)
|
||||
// PearlSR always supplies an entry attack, including Calyx. Resolve the
|
||||
// actor from the scene's actual slot IDs, rather than assuming cast_entity
|
||||
// identifies a lineup position.
|
||||
attackSlot, attackID := firstAvatarIdx, firstAvatarID
|
||||
for _, avatar := range battleInfo.BattleAvatarList {
|
||||
if casterID != 0 && avatar.Index+1 == casterID {
|
||||
attackSlot, attackID = avatar.Index, avatar.Id
|
||||
break
|
||||
}
|
||||
}
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, dahliaBuffs...)
|
||||
res, err := common.GameRes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("battle: load avatar resources: %w", err)
|
||||
}
|
||||
cfg := res.AvatarConfigs[attackID]
|
||||
attackBuffID := rules.EntryAttackBuffs[cfg.DamageType]
|
||||
if attackBuffID == 0 {
|
||||
attackBuffID = cfg.WeaknessBuffID
|
||||
}
|
||||
if attackBuffID != 0 {
|
||||
entrySkillIndex := skillIndex
|
||||
if casterID == 0 {
|
||||
entrySkillIndex = 1
|
||||
}
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, battleBuff(attackBuffID, attackSlot, map[string]float32{"SkillIndex": float32(entrySkillIndex)}))
|
||||
} else {
|
||||
return nil, fmt.Errorf("battle: avatar %d has no entry attack in res.json", attackID)
|
||||
}
|
||||
|
||||
// custom stats for avatars
|
||||
for _, stat := range player.BattleConfig.CustomStats {
|
||||
@@ -344,15 +319,16 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
// blessings
|
||||
for _, blessing := range player.BattleConfig.Blessings {
|
||||
buff := &goproto.BattleBuff{
|
||||
Id: blessing.ID,
|
||||
Level: blessing.Level,
|
||||
WaveFlag: 0xffffffff,
|
||||
OwnerId: 0xffffffff,
|
||||
Id: blessing.ID,
|
||||
Level: blessing.Level,
|
||||
WaveFlag: 0xffffffff,
|
||||
OwnerId: 0xffffffff,
|
||||
TargetIndexList: []uint32{0},
|
||||
}
|
||||
if blessing.DynamicKey != nil {
|
||||
buff.DynamicValues = map[string]float32{blessing.DynamicKey.Key: float32(blessing.DynamicKey.Value)}
|
||||
} else {
|
||||
buff.DynamicValues = map[string]float32{}
|
||||
buff.DynamicValues = map[string]float32{"SkillIndex": 0}
|
||||
}
|
||||
for _, dv := range blessing.DynamicValues {
|
||||
if _, exists := buff.DynamicValues[dv.Key]; exists {
|
||||
@@ -363,31 +339,7 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, buff)
|
||||
}
|
||||
|
||||
// pf score object
|
||||
if player.BattleConfig.BattleType == common.BattleTypePF {
|
||||
if battleInfo.StageId >= 30309011 {
|
||||
battleInfo.BattleTargetInfo[1] = &goproto.BattleTargetList{
|
||||
BattleTargetList: []*goproto.BattleTarget{{Id: 10003}},
|
||||
}
|
||||
} else {
|
||||
battleInfo.BattleTargetInfo[1] = &goproto.BattleTargetList{
|
||||
BattleTargetList: []*goproto.BattleTarget{{Id: 10002}},
|
||||
}
|
||||
}
|
||||
for i := uint32(2); i <= 4; i++ {
|
||||
battleInfo.BattleTargetInfo[i] = &goproto.BattleTargetList{}
|
||||
}
|
||||
battleInfo.BattleTargetInfo[5] = &goproto.BattleTargetList{
|
||||
BattleTargetList: []*goproto.BattleTarget{{Id: 2001}, {Id: 2002}},
|
||||
}
|
||||
}
|
||||
|
||||
// Apocalyptic Shadow
|
||||
if player.BattleConfig.BattleType == common.BattleTypeAS {
|
||||
battleInfo.BattleTargetInfo[1] = &goproto.BattleTargetList{
|
||||
BattleTargetList: []*goproto.BattleTarget{{Id: 90005}},
|
||||
}
|
||||
}
|
||||
rules.addTargets(battleInfo, player.BattleConfig.BattleType)
|
||||
|
||||
// SU
|
||||
if player.BattleConfig.BattleType == common.BattleTypeSU {
|
||||
@@ -403,58 +355,33 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
battleInfo.MonsterWaveList = common.ToSceneMonsterWaves(player.BattleConfig.Monsters)
|
||||
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
||||
|
||||
// global buffs
|
||||
hasCastorice, hasSilverWolf := false, false
|
||||
hasAha := false
|
||||
for _, buff := range battleInfo.BuffList {
|
||||
if buff.Id == 140703 {
|
||||
hasCastorice = true
|
||||
}
|
||||
if buff.Id == 150602 {
|
||||
hasSilverWolf = true
|
||||
}
|
||||
if buff.Id == 151104 {
|
||||
hasAha = true
|
||||
}
|
||||
}
|
||||
if !hasCastorice && (player.EnableCastoriceGlobal == nil || *player.EnableCastoriceGlobal) {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 140703, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
|
||||
})
|
||||
}
|
||||
if !hasSilverWolf && (player.EnableSWGlobal == nil || *player.EnableSWGlobal) {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 150602, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
|
||||
})
|
||||
}
|
||||
if !hasAha && (player.EnableAhaGlobal == nil || *player.EnableAhaGlobal) {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 151104, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
|
||||
})
|
||||
}
|
||||
rules.addGlobalPassives(battleInfo, player)
|
||||
|
||||
// Stage invasion buff (pearl-sr 4.6.53: e.g. Peak 11 stage 4 30511021/30511022 -> buff 3034004)
|
||||
if invasionID := getStageInvasionID(battleInfo.StageId); invasionID != 0 {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 3034000 + invasionID,
|
||||
Level: 1,
|
||||
OwnerId: firstAvatarIdx,
|
||||
WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{0},
|
||||
DynamicValues: map[string]float32{"SkillIndex": 1},
|
||||
})
|
||||
// Aha Invasion support buff
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 3034013,
|
||||
Level: 1,
|
||||
OwnerId: firstAvatarIdx,
|
||||
WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{0},
|
||||
DynamicValues: map[string]float32{"SkillIndex": 1},
|
||||
})
|
||||
// Invasion support belongs to an active challenge, not a custom battle
|
||||
// merely reusing the same stage. The scene loader records that state.
|
||||
if s.ChallengeStageID != 0 {
|
||||
if invasionID := getStageInvasionID(s.ChallengeStageID); invasionID != 0 {
|
||||
buffID, ok := rules.InvasionBuffs[invasionID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("battle: invasion %d has no buff resource", invasionID)
|
||||
}
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, battleBuff(buffID, firstAvatarIdx, map[string]float32{"SkillIndex": 1}))
|
||||
if player.BattleConfig.EnableInvasionSupport {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, battleBuff(rules.InvasionSupportBuff, firstAvatarIdx, map[string]float32{"SkillIndex": 1}))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(battleInfo.MonsterWaveList) == 0 {
|
||||
return nil, fmt.Errorf("battle: stage %d has no configured waves", battleInfo.StageId)
|
||||
}
|
||||
for i, wave := range battleInfo.MonsterWaveList {
|
||||
if len(wave.MonsterList) == 0 {
|
||||
return nil, fmt.Errorf("battle: stage %d wave %d is empty", battleInfo.StageId, i)
|
||||
}
|
||||
}
|
||||
Log(fmt.Sprintf("[BATTLE] prepared stage=%d avatars=%d waves=%d buffs=%d challenge_stage=%d", battleInfo.StageId, len(battleInfo.BattleAvatarList), len(battleInfo.MonsterWaveList), len(battleInfo.BuffList), s.ChallengeStageID))
|
||||
|
||||
return battleInfo
|
||||
return battleInfo, nil
|
||||
}
|
||||
|
||||
type stageInvasionConfigData struct {
|
||||
|
||||
@@ -167,6 +167,7 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u
|
||||
// single boss monster (entity 30001, registered for SceneCastSkill battles)
|
||||
monsterGroup := &goproto.SceneEntityGroupInfo{GroupId: targetGroupID}
|
||||
s.MonsterEntities = map[uint32]uint32{30001: eventID}
|
||||
s.ChallengeStageID = eventID
|
||||
monsterGroup.EntityList = append(monsterGroup.EntityList, &goproto.SceneEntityInfo{
|
||||
InstId: instID,
|
||||
GroupId: targetGroupID,
|
||||
|
||||
@@ -37,6 +37,25 @@ func (sm *syncMap) Delete(k uint32) { muBattle.Lock(); sm.init(); delete(sm.m, k
|
||||
|
||||
func randUint32() uint32 { return rand.Uint32() }
|
||||
|
||||
func playerArchive(s *Session) *goproto.ArchiveData {
|
||||
archive := &goproto.ArchiveData{}
|
||||
if player := s.JSON(); player != nil {
|
||||
for id := range player.Avatars {
|
||||
archive.ArchiveAvatarIdList = append(archive.ArchiveAvatarIdList, id)
|
||||
}
|
||||
sortU32Slice(archive.ArchiveAvatarIdList)
|
||||
seen := map[uint32]bool{}
|
||||
for _, equipment := range player.Lightcones {
|
||||
if !seen[equipment.ItemID] {
|
||||
seen[equipment.ItemID] = true
|
||||
archive.ArchiveEquipmentIdList = append(archive.ArchiveEquipmentIdList, equipment.ItemID)
|
||||
}
|
||||
}
|
||||
sortU32Slice(archive.ArchiveEquipmentIdList)
|
||||
}
|
||||
return archive
|
||||
}
|
||||
|
||||
func init() {
|
||||
handle("GetBag", func(s *Session, req *goproto.GetBagCsReq, rsp *goproto.GetBagScRsp) error {
|
||||
player := s.JSON()
|
||||
@@ -55,17 +74,18 @@ func init() {
|
||||
{Tid: 101, Num: 999999}, // Normal Pass
|
||||
{Tid: 102, Num: 999999}, // Special Pass
|
||||
}
|
||||
for _, aid := range BaseAvatarIDs {
|
||||
rsp.MaterialList = append(rsp.MaterialList, &goproto.Material{
|
||||
Tid: 130000 + aid,
|
||||
Num: 10,
|
||||
})
|
||||
}
|
||||
// Avatar IDs do not imply item IDs. The old 130000+avatar formula
|
||||
// produced nonexistent materials and broke the client's inventory init.
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("GetArchiveData", func(s *Session, req *goproto.GetArchiveDataCsReq, rsp *goproto.GetArchiveDataScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
rsp.ArchiveData = playerArchive(s)
|
||||
return nil
|
||||
})
|
||||
handle("GetUpdatedArchiveData", func(s *Session, req *goproto.GetUpdatedArchiveDataCsReq, rsp *goproto.GetUpdatedArchiveDataScRsp) error {
|
||||
rsp.ArchiveData = playerArchive(s)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -95,24 +95,49 @@ func init() {
|
||||
})
|
||||
|
||||
handle("GetPlayerBoardData", func(s *Session, req *goproto.GetPlayerBoardDataCsReq, rsp *goproto.GetPlayerBoardDataScRsp) error {
|
||||
profile, err := loadProfileResources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
rsp.CurrentHeadIconId = 200001
|
||||
rsp.CurrentPersonalCardId = 253001
|
||||
rsp.CurrentHeadIconId = profile.Icons[0]
|
||||
rsp.CurrentPersonalCardId = profile.CurrentPersonalCard
|
||||
rsp.HeadFrameInfo = profile.HeadFrame
|
||||
rsp.NBIEJKLENOB = &goproto.DisplayAvatarVec{IsDisplay: true}
|
||||
rsp.Signature = "Trailblazer"
|
||||
rsp.JJDDEKIAOFC = []uint32{200001, 200002, 200101, 200102, 200103, 200104}
|
||||
rsp.KJAABDABLJJ = []uint32{253001, 253002}
|
||||
// PearlSR 005d2c1 GetPlayerBoardDataScRsp: field 4 is personal
|
||||
// cards, field 8 is head icons, and field 9 is assist avatar IDs.
|
||||
rsp.JJDDEKIAOFC = profile.PersonalCards
|
||||
for _, id := range profile.Icons {
|
||||
rsp.KNINLDPFLMA = append(rsp.KNINLDPFLMA, &goproto.HeadIconData{Id: id})
|
||||
}
|
||||
if player := s.JSON(); player != nil {
|
||||
for _, slot := range player.SortedLineupKeys() {
|
||||
id := player.Lineups[slot]
|
||||
if _, ok := player.Avatars[id]; !ok {
|
||||
continue
|
||||
}
|
||||
rsp.KJAABDABLJJ = append(rsp.KJAABDABLJJ, id)
|
||||
rsp.NBIEJKLENOB.DisplayAvatarList = append(rsp.NBIEJKLENOB.DisplayAvatarList,
|
||||
&goproto.DisplayAvatarData{AvatarId: id, Pos: slot})
|
||||
}
|
||||
}
|
||||
SceneDebugLog("GET_PLAYER_BOARD_DATA sent")
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("GetPhoneData", func(s *Session, req *goproto.GetPhoneDataCsReq, rsp *goproto.GetPhoneDataScRsp) error {
|
||||
profile, err := loadProfileResources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
rsp.CurPhoneTheme = 1
|
||||
rsp.CurPhoneCase = 200001
|
||||
rsp.CurChatBubble = 220001
|
||||
rsp.OwnedPhoneThemes = []uint32{1}
|
||||
rsp.OwnedPhoneCases = []uint32{200001}
|
||||
rsp.OwnedChatBubbles = []uint32{220001}
|
||||
rsp.CurPhoneTheme = profile.CurrentPhoneTheme
|
||||
rsp.CurPhoneCase = profile.CurrentPhoneCase
|
||||
rsp.CurChatBubble = profile.CurrentChatBubble
|
||||
rsp.OwnedPhoneThemes = profile.PhoneThemes
|
||||
rsp.OwnedPhoneCases = profile.PhoneCases
|
||||
rsp.OwnedChatBubbles = profile.ChatBubbles
|
||||
SceneDebugLog("GET_PHONE_DATA sent")
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -928,6 +928,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo
|
||||
|
||||
s.PropEntities = map[uint32]PropEntry{}
|
||||
s.MonsterEntities = map[uint32]uint32{}
|
||||
s.ChallengeStageID = 0
|
||||
|
||||
for _, groupID := range sortedU32Keys2(scene.Scenes) {
|
||||
group := scene.Scenes[groupID]
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
type profileResources struct {
|
||||
Source struct {
|
||||
Revision string `json:"revision"`
|
||||
} `json:"source"`
|
||||
PersonalCards []uint32 `json:"personal_cards"`
|
||||
CurrentPersonalCard uint32 `json:"current_personal_card"`
|
||||
HeadFrame *goproto.HeadFrameInfo `json:"head_frame"`
|
||||
PhoneCases []uint32 `json:"phone_cases"`
|
||||
PhoneThemes []uint32 `json:"phone_themes"`
|
||||
ChatBubbles []uint32 `json:"chat_bubbles"`
|
||||
CurrentPhoneCase uint32 `json:"current_phone_case"`
|
||||
CurrentPhoneTheme uint32 `json:"current_phone_theme"`
|
||||
CurrentChatBubble uint32 `json:"current_chat_bubble"`
|
||||
Icons []uint32 `json:"-"`
|
||||
}
|
||||
|
||||
var profileOnce sync.Once
|
||||
var profileData profileResources
|
||||
var profileError error
|
||||
|
||||
func loadProfileResources() (*profileResources, error) {
|
||||
profileOnce.Do(func() {
|
||||
raw, err := os.ReadFile("resources/ProfileConfig.json")
|
||||
if err != nil {
|
||||
profileError = err
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &profileData); err != nil {
|
||||
profileError = err
|
||||
return
|
||||
}
|
||||
if profileData.Source.Revision == "" || profileData.HeadFrame == nil || !containsU32(profileData.PersonalCards, profileData.CurrentPersonalCard) {
|
||||
profileError = fmt.Errorf("profile source, head frame or selected personal card is invalid")
|
||||
return
|
||||
}
|
||||
if !containsU32(profileData.PhoneCases, profileData.CurrentPhoneCase) {
|
||||
profileError = fmt.Errorf("selected phone case is not owned")
|
||||
return
|
||||
}
|
||||
raw, err = os.ReadFile("resources/AvatarPlayerIcon.json")
|
||||
if err != nil {
|
||||
profileError = err
|
||||
return
|
||||
}
|
||||
var icons struct {
|
||||
Entries []struct {
|
||||
ID uint32 `json:"ID"`
|
||||
} `json:"player_icon_config"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &icons); err != nil {
|
||||
profileError = err
|
||||
return
|
||||
}
|
||||
for _, icon := range icons.Entries {
|
||||
if icon.ID != 0 {
|
||||
profileData.Icons = append(profileData.Icons, icon.ID)
|
||||
}
|
||||
}
|
||||
if len(profileData.Icons) == 0 {
|
||||
profileError = fmt.Errorf("AvatarPlayerIcon.json has no icons")
|
||||
}
|
||||
})
|
||||
if profileError != nil {
|
||||
return nil, fmt.Errorf("load profile resources: %w", profileError)
|
||||
}
|
||||
return &profileData, nil
|
||||
}
|
||||
@@ -44,6 +44,8 @@ type Session struct {
|
||||
// spawned in the current scene. Used by SceneCastSkill to decide whether an
|
||||
// attack actually hit a monster (the camera-detach bug fix).
|
||||
MonsterEntities map[uint32]uint32
|
||||
// Set by the challenge scene loader, cleared when loading an overworld scene.
|
||||
ChallengeStageID uint32
|
||||
|
||||
closed atomic.Bool
|
||||
closeOnce sync.Once
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"source": {
|
||||
"repository": "https://git.xeondev.com/HonkaiSlopRail/pearl-sr",
|
||||
"revision": "005d2c12db4781071f4b5b68e8f364587886a710",
|
||||
"files": [
|
||||
"gameserver/src/manager/battle_mgr.zig",
|
||||
"gameserver/src/data.zig",
|
||||
"protocol/src/protocol.pb.zig"
|
||||
]
|
||||
},
|
||||
"technique_buffs": {
|
||||
"1208": [
|
||||
{
|
||||
"id": 120801,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 120802,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1224": [
|
||||
{
|
||||
"id": 122402,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 122403,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 122401,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"#ADF_1": 3.0,
|
||||
"#ADF_2": 3.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1304": [
|
||||
{
|
||||
"id": 130403,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 3.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1306": [
|
||||
{
|
||||
"id": 130601,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 130602,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1308": [
|
||||
{
|
||||
"id": 130803,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1309": [
|
||||
{
|
||||
"id": 130901,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 130902,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1310": [
|
||||
{
|
||||
"id": 131001,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 131002,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1000112,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1412": [
|
||||
{
|
||||
"id": 141201,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1000121,
|
||||
"owner": "leader",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1414": [
|
||||
{
|
||||
"id": 141401,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1000121,
|
||||
"owner": "leader",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1510": [
|
||||
{
|
||||
"id": 151001,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 151002,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"1503": [
|
||||
{
|
||||
"id": 150301,
|
||||
"owner": "avatar",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1000121,
|
||||
"owner": "leader",
|
||||
"dynamic_values": {
|
||||
"SkillIndex": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"global_passives": [
|
||||
{
|
||||
"setting": "enable_castorice_global",
|
||||
"buff_id": 140703,
|
||||
"owner_slot": 1
|
||||
},
|
||||
{
|
||||
"setting": "enable_sw_global",
|
||||
"buff_id": 150602,
|
||||
"owner_slot": 1
|
||||
},
|
||||
{
|
||||
"setting": "enable_aha_global",
|
||||
"buff_id": 151104,
|
||||
"owner_slot": 1
|
||||
}
|
||||
],
|
||||
"ignore_toughness": {
|
||||
"avatars": [
|
||||
1006,
|
||||
1308,
|
||||
1317
|
||||
],
|
||||
"buff_id": 1000119
|
||||
},
|
||||
"battle_targets": [
|
||||
{
|
||||
"battle_type": "PF",
|
||||
"targets": {
|
||||
"1": [
|
||||
{
|
||||
"id": 10002,
|
||||
"total_progress": 80000
|
||||
}
|
||||
],
|
||||
"2": [],
|
||||
"3": [],
|
||||
"5": [
|
||||
{
|
||||
"id": 2001
|
||||
},
|
||||
{
|
||||
"id": 2002
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"battle_type": "AS",
|
||||
"targets": {
|
||||
"1": [
|
||||
{
|
||||
"id": 90005,
|
||||
"progress": 2000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"invasion_buffs": {
|
||||
"2": 3034002,
|
||||
"3": 3034003,
|
||||
"4": 3034004
|
||||
},
|
||||
"invasion_support_buff": 3034013,
|
||||
"entry_attack_buffs": {
|
||||
"Physical": 1000111,
|
||||
"Fire": 1000112,
|
||||
"Ice": 1000113,
|
||||
"Thunder": 1000114,
|
||||
"Wind": 1000115,
|
||||
"Quantum": 1000116,
|
||||
"Imaginary": 1000117
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"source": {
|
||||
"repository": "https://git.xeondev.com/HonkaiSlopRail/pearl-sr",
|
||||
"revision": "005d2c12db4781071f4b5b68e8f364587886a710",
|
||||
"files": [
|
||||
"gameserver/src/services/profile.zig",
|
||||
"gameserver/src/data.zig",
|
||||
"protocol/src/protocol.pb.zig"
|
||||
]
|
||||
},
|
||||
"personal_cards": [
|
||||
253000,
|
||||
253001,
|
||||
253002,
|
||||
253003,
|
||||
253004,
|
||||
253005
|
||||
],
|
||||
"current_personal_card": 253001,
|
||||
"head_frame": {
|
||||
"head_frame_item_id": 226004,
|
||||
"head_frame_expire_time": 4294967295
|
||||
},
|
||||
"phone_cases": [
|
||||
254000,
|
||||
254001
|
||||
],
|
||||
"phone_themes": [
|
||||
221000,
|
||||
221001,
|
||||
221002,
|
||||
221003,
|
||||
221004,
|
||||
221005,
|
||||
221006,
|
||||
221007,
|
||||
221008,
|
||||
221009,
|
||||
221010,
|
||||
221011,
|
||||
221012,
|
||||
221013
|
||||
],
|
||||
"chat_bubbles": [
|
||||
220000,
|
||||
220001,
|
||||
220002,
|
||||
220003,
|
||||
220004,
|
||||
220005,
|
||||
220006,
|
||||
220007,
|
||||
220008,
|
||||
220009,
|
||||
220010,
|
||||
220012
|
||||
],
|
||||
"current_phone_case": 254001,
|
||||
"current_phone_theme": 0,
|
||||
"current_chat_bubble": 0
|
||||
}
|
||||
Reference in new issue
Block a user