package gameserver import ( "sort" "google.golang.org/protobuf/proto" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) // Full Go port of challenge.rs: StartChallenge / Tierce / Peak flows with the // hand-built 4.6.51 response layouts (cmd 1787 / 8973). type decodedChallengeStart struct { challengeID uint32 stageIndex uint32 isSingleStage bool firstAvatars []uint32 secondAvatars []uint32 thirdAvatars []uint32 stageInfoList [][]uint32 buffID uint32 } // --------------------------------------------------------------------------- // protobuf wire helpers (mirroring the Rust prost decoding loops) // --------------------------------------------------------------------------- func skipWireField(wireType uint32, buf *[]byte) { b := *buf switch wireType { case 0: _, n := readUvarint(b) if n == 0 { *buf = nil } else { *buf = b[n:] } case 1: if len(b) >= 8 { *buf = b[8:] } else { *buf = nil } case 2: l, n := readUvarint(b) if n == 0 || int(l) > len(b[n:]) { *buf = nil } else { *buf = b[n+int(l):] } case 5: if len(b) >= 4 { *buf = b[4:] } else { *buf = nil } default: *buf = nil } } func mapChallengeStageID(id uint32) uint32 { switch { case id >= 5400 && id <= 5600: // MoC 1035/1036 -> 5301..5312 rem := id % 100 if rem < 1 { rem = 1 } if rem > 12 { rem = 12 } return 5300 + rem case id >= 20200 && id <= 20300: // Pure Fiction -> 20251..20254 rem := id % 10 if rem < 1 { rem = 1 } if rem > 4 { rem = 4 } return 20250 + rem case id >= 30200 && id <= 30300: // Apocalyptic Shadow -> 30201..30204 rem := id % 10 if rem < 1 { rem = 1 } if rem > 4 { rem = 4 } return 30200 + rem case id > 1: cd := getChallengeData() if _, ok := cd.Challenges[id]; !ok { return id - 1 } return id } return id } func decodeStartChallengeReq(payload []byte) decodedChallengeStart { var req decodedChallengeStart buf := payload for len(buf) > 0 { tag, n := readUvarint(buf) if n == 0 { break } buf = buf[n:] fieldNumber := uint32(tag >> 3) wireType := uint32(tag & 0x7) switch { case (fieldNumber == 14 || fieldNumber == 12) && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] if req.challengeID == 0 { req.challengeID = uint32(v) } case fieldNumber == 6 && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] req.firstAvatars = append(req.firstAvatars, uint32(v)) case fieldNumber == 6 && wireType == 2: sub, ok := readLenDelim(buf) if !ok { buf = nil break } buf = buf[len(buf)-len(sub):] // consumed below buf = buf[len(sub):] for len(sub) > 0 { v, n3 := readUvarint(sub) if n3 == 0 { break } sub = sub[n3:] req.firstAvatars = append(req.firstAvatars, uint32(v)) } case fieldNumber == 8 && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] req.secondAvatars = append(req.secondAvatars, uint32(v)) case fieldNumber == 8 && wireType == 2: sub, ok := readLenDelim(buf) if !ok { buf = nil break } buf = buf[len(sub):] for len(sub) > 0 { v, n3 := readUvarint(sub) if n3 == 0 { break } sub = sub[n3:] req.secondAvatars = append(req.secondAvatars, uint32(v)) } case fieldNumber == 4 && wireType == 2: // AvatarLineup: id = tag 1 sub, ok := readLenDelim(buf) if !ok { buf = nil break } buf = buf[len(sub):] for len(sub) > 0 { stag, n3 := readUvarint(sub) if n3 == 0 { break } sfn := uint32(stag >> 3) swt := uint32(stag & 0x7) if sfn == 1 && swt == 0 { v, n4 := readUvarint(sub[n3:]) if n4 == 0 { break } sub = sub[n3+n4:] req.firstAvatars = append(req.firstAvatars, uint32(v)) } else { skipWireField(swt, &sub) } } case fieldNumber == 10 && wireType == 2: // ChallengeBuffInfo { story_info = 9, boss_info = 10 } sub, ok := readLenDelim(buf) if !ok { buf = nil break } buf = buf[len(sub):] for len(sub) > 0 { stag, n3 := readUvarint(sub) if n3 == 0 { break } sfn := uint32(stag >> 3) swt := uint32(stag & 0x7) sub2 := sub[n3:] if swt == 2 && (sfn == 9 || sfn == 10) { inner, ok2 := readLenDelim(sub2) if !ok2 { break } for len(inner) > 0 { t2, n4 := readUvarint(inner) if n4 == 0 { break } f2 := uint32(t2 >> 3) w2 := uint32(t2 & 0x7) if w2 == 0 && (f2 == 10 || f2 == 15) { v, n5 := readUvarint(inner[n4:]) if n5 == 0 { break } inner = inner[n4+n5:] if req.buffID == 0 { req.buffID = uint32(v) } } else { skipWireField(w2, &inner) } } sub = sub2 // skip the whole len-delimited field _, n6 := readUvarint(sub2) if n6 == 0 { sub = nil } else { sub = sub2[n6:] } } else { skipWireField(swt, &sub) } } default: skipWireField(wireType, &buf) } } return req } func readLenDelim(buf []byte) ([]byte, bool) { l, n := readUvarint(buf) if n == 0 || int(l) > len(buf[n:]) { return nil, false } return buf[n : n+int(l)], true } type tierceStageEntry struct { buff uint32 lineup []uint32 } func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart { var req decodedChallengeStart buf := payload var stagesAvatars []tierceStageEntry for len(buf) > 0 { tag, n := readUvarint(buf) if n == 0 { break } buf = buf[n:] fieldNumber := uint32(tag >> 3) wireType := uint32(tag & 0x7) switch { case (fieldNumber == 10 || fieldNumber == 1) && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] req.stageIndex = uint32(v) case (fieldNumber == 15 || fieldNumber == 3) && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] req.challengeID = uint32(v) case (fieldNumber == 5 || fieldNumber == 13) && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] req.isSingleStage = v != 0 case (fieldNumber == 12 || fieldNumber == 14) && wireType == 2: sub, ok := readLenDelim(buf) if !ok { buf = nil break } buf = buf[len(sub):] var stageBuff uint32 var stageLineup []uint32 for len(sub) > 0 { subTag, n2 := readUvarint(sub) if n2 == 0 { break } subFn := uint32(subTag >> 3) subWt := uint32(subTag & 0x7) switch { case (subFn == 6 || subFn == 5) && subWt == 0: v, n3 := readUvarint(sub[n2:]) if n3 == 0 { sub = nil break } sub = sub[n2+n3:] stageBuff = uint32(v) case (subFn == 4 || subFn == 9) && subWt == 2: abuf, ok2 := readLenDelim(sub[n2:]) if !ok2 { sub = nil break } sub = sub[n2+len(abuf):] var aid uint32 for len(abuf) > 0 { atag, n4 := readUvarint(abuf) if n4 == 0 { break } afn := uint32(atag >> 3) awt := uint32(atag & 0x7) if (afn == 7 || afn == 9) && awt == 0 { v, n5 := readUvarint(abuf[n4:]) if n5 == 0 { break } abuf = abuf[n4+n5:] aid = uint32(v) } else { skipWireField(awt, &abuf) } } if aid != 0 { stageLineup = append(stageLineup, aid) } default: skipWireField(subWt, &sub) } } stagesAvatars = append(stagesAvatars, tierceStageEntry{buff: stageBuff, lineup: stageLineup}) default: skipWireField(wireType, &buf) } } for _, st := range stagesAvatars { req.stageInfoList = append(req.stageInfoList, st.lineup) } if len(stagesAvatars) > 0 { req.firstAvatars = stagesAvatars[0].lineup if req.stageIndex == 0 && stagesAvatars[0].buff != 0 { req.buffID = stagesAvatars[0].buff } } if len(stagesAvatars) > 1 { req.secondAvatars = stagesAvatars[1].lineup if req.stageIndex == 1 && stagesAvatars[1].buff != 0 { req.buffID = stagesAvatars[1].buff } } if len(stagesAvatars) > 2 { req.thirdAvatars = stagesAvatars[2].lineup if req.stageIndex == 2 && stagesAvatars[2].buff != 0 { req.buffID = stagesAvatars[2].buff } } return req } func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) { var challengeID uint32 var stages [][]uint32 buf := payload for len(buf) > 0 { tag, n := readUvarint(buf) if n == 0 { break } buf = buf[n:] fieldNumber := uint32(tag >> 3) wireType := uint32(tag & 0x7) switch { case fieldNumber == 7 && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] challengeID = uint32(v) case fieldNumber == 12 && wireType == 2: sub, ok := readLenDelim(buf) if !ok { buf = nil break } buf = buf[len(sub):] var stageLineup []uint32 for len(sub) > 0 { subTag, n2 := readUvarint(sub) if n2 == 0 { break } subFn := uint32(subTag >> 3) subWt := uint32(subTag & 0x7) if subFn == 9 && subWt == 2 { abuf, ok2 := readLenDelim(sub[n2:]) if !ok2 { sub = nil break } sub = sub[n2+len(abuf):] var aid uint32 for len(abuf) > 0 { atag, n4 := readUvarint(abuf) if n4 == 0 { break } afn := uint32(atag >> 3) awt := uint32(atag & 0x7) if afn == 9 && awt == 0 { v, n5 := readUvarint(abuf[n4:]) if n5 == 0 { break } abuf = abuf[n4+n5:] aid = uint32(v) } else { skipWireField(awt, &abuf) } } if aid != 0 { stageLineup = append(stageLineup, aid) } } else { skipWireField(subWt, &sub) } } stages = append(stages, stageLineup) default: skipWireField(wireType, &buf) } } return challengeID, stages } // --------------------------------------------------------------------------- // battle config setup shared by start handlers // --------------------------------------------------------------------------- func setChallengeBattleConfig(player *common.FreesrData, avatars []uint32, challengeID uint32, eventID uint32, buff uint32) { customLineup := map[uint32]uint32{} for i, aid := range avatars { customLineup[uint32(i)] = aid } player.BattleConfig.CustomBattleLineup = customLineup player.BattleConfig.StageID = eventID isPF := challengeID >= 20000 && challengeID < 30000 isAS := challengeID >= 30000 if isPF { player.BattleConfig.CycleCount = 4 player.BattleConfig.BattleType = common.BattleTypePF } else if isAS { player.BattleConfig.CycleCount = 30 player.BattleConfig.BattleType = common.BattleTypeAS } else { player.BattleConfig.CycleCount = 30 player.BattleConfig.BattleType = common.BattleTypeDefault } if sb, ok := getChallengeData().Stages[eventID]; ok { var waves [][]common.Monster for _, wave := range sb.Monsters { var ms []common.Monster for _, mid := range wave { ms = append(ms, common.Monster{Level: sb.Level, MonsterID: mid}) } waves = append(waves, ms) } player.BattleConfig.Monsters = waves } if buff != 0 { player.BattleConfig.Blessings = []common.BattleBuffJson{{ ID: buff, Level: 1, }} } player.SavePersistent() } func lineupInfoFromAvatars(avatars []uint32, planeID uint32, extraType goproto.ExtraLineupType, index uint32) *goproto.LineupInfo { custom := map[uint32]uint32{} for i, aid := range avatars { custom[uint32(i)] = aid } lineup := common.ToLineupInfo(custom) lineup.PlaneId = planeID lineup.ExtraLineupType = extraType lineup.Index = index return lineup } func challengeAvatarsFallback(s *Session) []uint32 { if player := s.JSON(); player != nil { var out []uint32 for _, slot := range player.SortedLineupKeys() { if v := player.Lineups[slot]; v != 0 { out = append(out, v) } } return out } return []uint32{1304, 1313, 1406, 1004} } func dedupeTruncate4(in []uint32) []uint32 { seen := map[uint32]bool{} var out []uint32 for _, v := range in { if !seen[v] { seen[v] = true out = append(out, v) } if len(out) == 4 { break } } return out } // looksLikeChallengeID: known challenge-id ranges across endgame modes. func looksLikeChallengeID(v uint64) bool { switch { case v >= 101 && v <= 119, v == 900, v >= 1001 && v <= 1035, v >= 2001 && v <= 2025, v >= 3001 && v <= 3020, v >= 5000 && v <= 5700, // MoC floors incl. 4.6 phases v >= 20000 && v <= 20300, // Pure Fiction v >= 30000 && v <= 30300: // Apocalyptic Shadow return true } cd := getChallengeData() _, okC := cd.Challenges[uint32(v)] _, okT := cd.Tierce[uint32(v)] return okC || okT } // detectChallengeID: when the strict decoder misses the challenge id (field // layout drift between client builds), scan top-level varint fields for the // first value that looks like a challenge id. func detectChallengeID(payload []byte) (uint32, int) { buf := payload for len(buf) > 0 { tag, n := readUvarint(buf) if n == 0 { break } buf = buf[n:] wireType := uint32(tag & 0x7) if wireType == 0 { v, n2 := readUvarint(buf) if n2 == 0 { break } buf = buf[n2:] if looksLikeChallengeID(v) { return uint32(v), int(tag >> 3) } } else { skipWireField(wireType, &buf) } } return 0, 0 } // handleStartChallenge ports handle_start_challenge: respond cmd 1787 with // retcode(3) + cur_challenge(5) + lineup_list(6) + scene(10) + stage_info(13). func handleStartChallenge(s *Session, payload []byte) error { req := decodeStartChallengeReq(payload) if req.challengeID == 0 { if id, field := detectChallengeID(payload); id != 0 { Log("handle_start_challenge: strict decode missed, detected challenge_id=" + itoa(int(id)) + " at field " + itoa(field)) req.challengeID = id } else { PacketLog("[CHALLENGE] challenge_id=0 payload=" + hexDump(payload, 128)) } } Log("handle_start_challenge: challenge_id=" + itoa(int(req.challengeID))) chosenAvatars := req.firstAvatars if len(chosenAvatars) == 0 { chosenAvatars = challengeAvatarsFallback(s) } // cap team at 4 (client TeamManager IndexOutOfRange otherwise) chosenAvatars = dedupeTruncate4(chosenAvatars) baseID := mapChallengeStageID(req.challengeID) cd := getChallengeData() entrance, group, monster, event, buff := uint32(3014101), uint32(5), uint32(4033010), uint32(30124011), uint32(0) if c, ok := cd.Challenges[baseID]; ok { entrance, group, monster, event = c.Entrance, c.Group1, c.Monster1, c.Event1 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } // the 4.6.51 client has no MapEntrance 3014101 (Rust lesson: Lua // GotoMapParam nil -> stuck loading) even though res.json has it if entrance == 3014101 { entrance = 3000101 } else if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) { Log("[CHALLENGE] Arena entrance missing in res.json, fallback to 3000101") entrance = 3000101 } Log("[CHALLENGE] Start: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")") Log("[CHALLENGE] Arena: entrance=" + itoa(int(entrance)) + " group=" + itoa(int(group)) + " monster=" + itoa(int(monster)) + " event=" + itoa(int(event))) if player := s.JSON(); player != nil { setChallengeBattleConfig(player, chosenAvatars, req.challengeID, event, buff) } sceneInfo, _, err := loadChallengeScene(s, entrance, group, monster, event, chosenAvatars) if err != nil { return err } custom := map[uint32]uint32{} for i, aid := range chosenAvatars { custom[uint32(i)] = aid } lineupInfo := common.ToLineupInfo(custom) curChallenge := &goproto.CurChallenge{ ChallengeId: req.challengeID, Status: 1, // CHALLENGE_DOING RoundCount: 0, ScoreId: scoreIDFor(req.challengeID), ScoreTwo: 0, ExtraLineupType: goproto.ExtraLineupType(1), } chalBuf, err := proto.Marshal(curChallenge) if err != nil { return err } lineupBuf, err := proto.Marshal(lineupInfo) if err != nil { return err } sceneBuf, err := proto.Marshal(sceneInfo) if err != nil { return err } // 4.6.51: StartChallengeScRsp.stage_info (tag 13) - boss node info echoes // the selected buff so the client can set up the challenge UI stageInfo := &goproto.ChallengeStageInfo{} if req.buffID != 0 { stageInfo.BossInfo = &goproto.ChallengeBossInfo{ FirstNode: &goproto.ChallengeBossSingleNodeInfo{BuffId: req.buffID}, SecondNode: &goproto.ChallengeBossSingleNodeInfo{BuffId: req.buffID}, } } stageBuf, err := proto.Marshal(stageInfo) if err != nil { return err } var body []byte body = append(body, 0x18, 0x00) // tag 3: retcode = 0 body = append(body, 0x2A) // tag 5: cur_challenge body = appendUvarint(body, uint64(len(chalBuf))) body = append(body, chalBuf...) body = append(body, 0x32) // tag 6: lineup_list body = appendUvarint(body, uint64(len(lineupBuf))) body = append(body, lineupBuf...) body = append(body, 0x52) // tag 10: scene body = appendUvarint(body, uint64(len(sceneBuf))) body = append(body, sceneBuf...) body = append(body, 0x6A) // tag 13: stage_info body = appendUvarint(body, uint64(len(stageBuf))) body = append(body, stageBuf...) return s.SendRaw(&NetPacket{CmdType: 1787, Body: body}) } func scoreIDFor(challengeID uint32) uint32 { if challengeID >= 20000 && challengeID < 30000 { return 40000 } return 0 } // handleStartChallengeTierce ports handle_start_challenge_tierce -> cmd 8973. func handleStartChallengeTierce(s *Session, payload []byte) error { req := decodeStartChallengeTierceReq(payload) baseID := mapChallengeStageID(req.challengeID) Log("handle_start_challenge_tierce: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")") tierceLineupsMu.Lock() if len(req.stageInfoList) > 0 { tierceLineups[req.challengeID] = req.stageInfoList } tierceLineupsMu.Unlock() var chosenAvatars []uint32 switch req.stageIndex { case 0: chosenAvatars = req.firstAvatars case 1: chosenAvatars = req.secondAvatars case 2: chosenAvatars = req.thirdAvatars } if len(chosenAvatars) == 0 { tierceLineupsMu.Lock() if list, ok := tierceLineups[req.challengeID]; ok && int(req.stageIndex) < len(list) { chosenAvatars = list[req.stageIndex] } tierceLineupsMu.Unlock() } if len(chosenAvatars) == 0 { chosenAvatars = challengeAvatarsFallback(s) } cd := getChallengeData() entrance, group, monster, event, buff := uint32(3014101), uint32(5), uint32(4033010), uint32(30124011), uint32(0) switch { case req.stageIndex == 2: if t, ok := cd.Tierce[baseID]; ok { entrance, group, monster, event, buff = t.Entrance, t.Group, t.Monster, t.Event, req.buffID } else if c, ok2 := cd.Challenges[baseID]; ok2 { entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } case req.stageIndex == 1: if c, ok := cd.Challenges[baseID]; ok { entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } else { entrance, group, monster, event = 3014101, 6, 8013010, 30124012 } default: if c, ok := cd.Challenges[baseID]; ok { entrance, group, monster, event = c.Entrance, c.Group1, c.Monster1, c.Event1 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } } // the 4.6.51 client cannot load arena 3014101 (no MapEntrance) - fall // back to the standard arena, mirroring handleStartChallenge if entrance == 3014101 { entrance = 3000101 } else if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) { Log("[CHALLENGE] Tierce arena entrance missing in res.json, fallback to 3000101") entrance = 3000101 } if player := s.JSON(); player != nil { setChallengeBattleConfig(player, chosenAvatars, req.challengeID, event, buff) } sceneInfo, _, err := loadChallengeScene(s, entrance, group, monster, event, chosenAvatars) if err != nil { return err } // build all lineups for tierce_info var allLineups [][]uint32 firstLineup := req.firstAvatars if len(firstLineup) == 0 && len(req.stageInfoList) > 0 { firstLineup = req.stageInfoList[0] } if len(firstLineup) == 0 { tierceLineupsMu.Lock() if list, ok := tierceLineups[req.challengeID]; ok && len(list) > 0 { firstLineup = list[0] } tierceLineupsMu.Unlock() } if len(firstLineup) == 0 { firstLineup = chosenAvatars } allLineups = append(allLineups, firstLineup) if !req.isSingleStage { secondLineup := req.secondAvatars if len(secondLineup) == 0 && len(req.stageInfoList) > 1 { secondLineup = req.stageInfoList[1] } if len(secondLineup) == 0 { tierceLineupsMu.Lock() if list, ok := tierceLineups[req.challengeID]; ok && len(list) > 1 { secondLineup = list[1] } tierceLineupsMu.Unlock() } if len(secondLineup) == 0 { secondLineup = firstLineup } allLineups = append(allLineups, secondLineup) thirdLineup := req.thirdAvatars if len(thirdLineup) == 0 && len(req.stageInfoList) > 2 { thirdLineup = req.stageInfoList[2] } if len(thirdLineup) == 0 { tierceLineupsMu.Lock() if list, ok := tierceLineups[req.challengeID]; ok && len(list) > 2 { thirdLineup = list[2] } tierceLineupsMu.Unlock() } if len(thirdLineup) > 0 || req.stageIndex == 2 { if len(thirdLineup) == 0 { thirdLineup = firstLineup } allLineups = append(allLineups, thirdLineup) } } // ChallengeTierceChallengeInfo: stage_index=4, challenge_id=6, is_single_stage=14, lineup_list=15 var tierceInfo []byte tierceInfo = append(tierceInfo, 0x20) // tag 4: stage_index tierceInfo = appendUvarint(tierceInfo, uint64(req.stageIndex)) tierceInfo = append(tierceInfo, 0x30) // tag 6: challenge_id tierceInfo = appendUvarint(tierceInfo, uint64(req.challengeID)) if req.isSingleStage { tierceInfo = append(tierceInfo, 0x70, 0x01) // tag 14: true } for stIdx, stageAvatars := range allLineups { lineup := lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx)) lineupBuf, err := proto.Marshal(lineup) if err != nil { return err } tierceInfo = append(tierceInfo, 0x7A) // tag 15: lineup_list tierceInfo = appendUvarint(tierceInfo, uint64(len(lineupBuf))) tierceInfo = append(tierceInfo, lineupBuf...) } sceneBuf, err := proto.Marshal(sceneInfo) if err != nil { return err } // StartChallengeTierceScRsp cmd 8973: retcode=12, challenge_tierce_info=4, scene=9 var body []byte body = append(body, 0x60, 0x00) // tag 12: retcode = 0 body = append(body, 0x22) // tag 4 body = appendUvarint(body, uint64(len(tierceInfo))) body = append(body, tierceInfo...) body = append(body, 0x4A) // tag 9: scene body = appendUvarint(body, uint64(len(sceneBuf))) body = append(body, sceneBuf...) return s.SendRaw(&NetPacket{CmdType: 8973, Body: body}) } func handleSetChallengeTierceLineup(s *Session, payload []byte) error { challengeID, stages := decodeSetChallengeTierceLineupReq(payload) if len(stages) > 0 { tierceLineupsMu.Lock() tierceLineups[challengeID] = stages tierceLineupsMu.Unlock() } if err := s.SendRaw(&NetPacket{CmdType: 8999, Body: []byte{0x08, 0x00}}); err != nil { return err } _ = s.SendRaw(&NetPacket{CmdType: 8995, Body: []byte{0x60, 0x00}}) return nil } func handleLeaveChallenge(s *Session, _ []byte) error { if player := s.JSON(); player != nil { player.BattleConfig.CustomBattleLineup = nil player.SavePersistent() } if err := s.SendRaw(&NetPacket{CmdType: 1716, Body: []byte{0x20, 0x00}}); err != nil { return err } _ = s.SendRaw(&NetPacket{CmdType: 1781, Body: []byte{0x40, 0x00}}) return nil } func handleLeaveChallengeTierce(s *Session, _ []byte) error { if player := s.JSON(); player != nil { player.BattleConfig.CustomBattleLineup = nil player.SavePersistent() } if err := s.SendRaw(&NetPacket{CmdType: 8997, Body: []byte{0x38, 0x00}}); err != nil { return err } _ = s.SendRaw(&NetPacket{CmdType: 8982, Body: []byte{0x68, 0x00}}) return nil } func handleTakeChallengeReward(s *Session, payload []byte) error { var groupID uint32 buf := payload for len(buf) > 0 { tag, n := readUvarint(buf) if n == 0 { break } buf = buf[n:] fnNum := uint32(tag >> 3) wt := uint32(tag & 0x7) if fnNum == 4 && wt == 0 { v, n2 := readUvarint(buf) if n2 == 0 { break } buf = buf[n2:] groupID = uint32(v) break } skipWireField(wt, &buf) } var body []byte body = append(body, 0x10) // tag 2: group_id body = appendUvarint(body, uint64(groupID)) body = append(body, 0x30, 0x00) // tag 6: retcode = 0 return s.SendRaw(&NetPacket{CmdType: 1704, Body: body}) } // buildGetChallengeTierceDataScRsp — verbatim port of the Rust builder. func buildGetChallengeTierceDataScRspReal() []byte { cd := getChallengeData() type tierceEntry struct { id uint32 targets []uint32 score uint32 } var tierceStages []tierceEntry for id, data := range cd.Tierce { var targets []uint32 switch { case len(data.Targets) > 0: targets = data.Targets case id >= 30000: targets = []uint32{5001, 5002, 5003, 5000} case id >= 20000: targets = []uint32{4001, 4002, 4003, 4000} default: targets = []uint32{601, 602, 603, 600} } score := data.Score if score == 0 { if id >= 20000 && id < 30000 { score = 40000 } else { score = 4000 } } tierceStages = append(tierceStages, tierceEntry{id, targets, score}) } sort.Slice(tierceStages, func(i, j int) bool { return tierceStages[i].id < tierceStages[j].id }) var rsp []byte rsp = append(rsp, 0x40, 0x00) // tag 8: retcode = 0 rsp = append(rsp, 0x60, 0x00) // legacy tag 12: retcode = 0 for _, e := range tierceStages { var tierceData []byte // tag 3: challenge_id tierceData = append(tierceData, 0x18) tierceData = appendUvarint(tierceData, uint64(e.id)) // tag 6: is_passed = true tierceData = append(tierceData, 0x30, 0x01) // tag 7: finished_target_list (packed) var targetsBuf []byte for _, t := range e.targets { targetsBuf = appendUvarint(targetsBuf, uint64(t)) } tierceData = append(tierceData, 0x3A) tierceData = appendUvarint(tierceData, uint64(len(targetsBuf))) tierceData = append(tierceData, targetsBuf...) // tag 4: result_list (3 stages) for stageIdx := uint32(0); stageIdx < 3; stageIdx++ { var result []byte result = append(result, 0x18) // tag 3: score_id result = appendUvarint(result, uint64(e.score)) result = append(result, 0x48) // tag 9: stage_index result = appendUvarint(result, uint64(stageIdx)) result = append(result, 0x50, 0x01) // tag 10: end_status = WIN tierceData = append(tierceData, 0x22) tierceData = appendUvarint(tierceData, uint64(len(result))) tierceData = append(tierceData, result...) } // tag 8: stage_info_list (3 stages) for stageIdx := uint32(0); stageIdx < 3; stageIdx++ { var sinfo []byte sinfo = append(sinfo, 0x70) // tag 14: stage_index sinfo = appendUvarint(sinfo, uint64(stageIdx)) tierceData = append(tierceData, 0x42) tierceData = appendUvarint(tierceData, uint64(len(sinfo))) tierceData = append(tierceData, sinfo...) } // tag 4: challenge_info_list (4.5.52) rsp = append(rsp, 0x22) rsp = appendUvarint(rsp, uint64(len(tierceData))) rsp = append(rsp, tierceData...) // tag 10: challenge_info_list (legacy) rsp = append(rsp, 0x52) rsp = appendUvarint(rsp, uint64(len(tierceData))) rsp = append(rsp, tierceData...) } return rsp }