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 { cd := getChallengeData() if _, ok := cd.Challenges[id]; ok { return id } if _, ok := cd.Tierce[id]; ok { return id } switch { case id >= 5400 && id <= 5700: // 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: if _, ok := cd.Challenges[id]; !ok { return id - 1 } return id } return id } func readLenDelimAndAdvance(buf *[]byte) ([]byte, bool) { b := *buf l, n := readUvarint(b) if n == 0 || int(l) > len(b[n:]) { *buf = nil return nil, false } sub := b[n : n+int(l)] *buf = b[n+int(l):] return sub, true } 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 } func decodeStartChallengeReq(payload []byte) decodedChallengeStart { var req decodedChallengeStart var pbReq goproto.StartChallengeCsReq if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 { req.challengeID = pbReq.ChallengeId req.firstAvatars = pbReq.FirstLineup if len(req.firstAvatars) == 0 && len(pbReq.AvatarLineupFirst) > 0 { for _, av := range pbReq.AvatarLineupFirst { if av.Id != 0 { req.firstAvatars = append(req.firstAvatars, av.Id) } } } req.secondAvatars = pbReq.SecondLineup if len(req.secondAvatars) == 0 && len(pbReq.AvatarLineupSecond) > 0 { for _, av := range pbReq.AvatarLineupSecond { if av.Id != 0 { req.secondAvatars = append(req.secondAvatars, av.Id) } } } if pbReq.StageInfo != nil { if pbReq.StageInfo.BossInfo != nil && pbReq.StageInfo.BossInfo.BuffOne != 0 { req.buffID = pbReq.StageInfo.BossInfo.BuffOne } else if pbReq.StageInfo.StoryInfo != nil && pbReq.StageInfo.StoryInfo.BuffOne != 0 { req.buffID = pbReq.StageInfo.StoryInfo.BuffOne } } return req } 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 := readLenDelimAndAdvance(&buf) if !ok { break } 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 := readLenDelimAndAdvance(&buf) if !ok { break } 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 || fieldNumber == 15) && wireType == 2: sub, ok := readLenDelimAndAdvance(&buf) if !ok { break } 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:] if fieldNumber == 4 { req.firstAvatars = append(req.firstAvatars, uint32(v)) } else { req.secondAvatars = append(req.secondAvatars, uint32(v)) } } else { skipWireField(swt, &sub) } } case fieldNumber == 10 && wireType == 2: sub, ok := readLenDelimAndAdvance(&buf) if !ok { break } for len(sub) > 0 { stag, n3 := readUvarint(sub) if n3 == 0 { break } sfn := uint32(stag >> 3) swt := uint32(stag & 0x7) sub = sub[n3:] if swt == 2 && (sfn == 9 || sfn == 10) { inner, ok2 := readLenDelimAndAdvance(&sub) if !ok2 { break } for len(inner) > 0 { t2, n4 := readUvarint(inner) if n4 == 0 { break } f2 := uint32(t2 >> 3) w2 := uint32(t2 & 0x7) inner = inner[n4:] if w2 == 0 && (f2 == 10 || f2 == 15) { v, n5 := readUvarint(inner) if n5 == 0 { break } inner = inner[n5:] if req.buffID == 0 { req.buffID = uint32(v) } } else { skipWireField(w2, &inner) } } } else { skipWireField(swt, &sub) } } default: skipWireField(wireType, &buf) } } return req } type tierceStageEntry struct { buff uint32 lineup []uint32 } func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart { var req decodedChallengeStart var stagesAvatars []tierceStageEntry var pbReq goproto.StartChallengeTierceCsReq if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 { req.challengeID = pbReq.ChallengeId req.stageIndex = pbReq.FINOCEGDBDI req.isSingleStage = pbReq.MPKMIKBFEHG for _, lt := range pbReq.LineupTierceList { var l []uint32 for _, av := range lt.TierceAvatarIdList { if av.Id != 0 { l = append(l, av.Id) } } stagesAvatars = append(stagesAvatars, tierceStageEntry{ buff: lt.BuffId, lineup: l, }) } } else { 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 == 10 || fieldNumber == 1 || fieldNumber == 5) && 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 == 2 || fieldNumber == 13) && wireType == 0: v, n2 := readUvarint(buf) if n2 == 0 { buf = nil break } buf = buf[n2:] req.isSingleStage = v != 0 case (fieldNumber == 9 || fieldNumber == 12 || fieldNumber == 14) && wireType == 2: sub, ok := readLenDelimAndAdvance(&buf) if !ok { break } 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) sub = sub[n2:] switch { case (subFn == 10 || subFn == 6 || subFn == 5) && subWt == 0: v, n3 := readUvarint(sub) if n3 == 0 { sub = nil break } sub = sub[n3:] stageBuff = uint32(v) case (subFn == 9 || subFn == 4) && subWt == 2: abuf, ok2 := readLenDelimAndAdvance(&sub) if !ok2 { break } var aid uint32 for len(abuf) > 0 { atag, n4 := readUvarint(abuf) if n4 == 0 { break } afn := uint32(atag >> 3) awt := uint32(atag & 0x7) abuf = abuf[n4:] if (afn == 1 || afn == 7 || afn == 9) && awt == 0 { v, n5 := readUvarint(abuf) if n5 == 0 { break } abuf = abuf[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(3000101), 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 } } else if t, ok := cd.Tierce[baseID]; ok { entrance, group, monster, event = t.Entrance, t.Group, t.Monster, t.Event buff = req.buffID } 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 } var lineups []*goproto.LineupInfo lineups = append(lineups, lineupInfoFromAvatars(chosenAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 0)) secondAvatars := req.secondAvatars if len(secondAvatars) == 0 { secondAvatars = chosenAvatars } secondAvatars = dedupeTruncate4(secondAvatars) lineups = append(lineups, lineupInfoFromAvatars(secondAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 1)) curChallenge := &goproto.CurChallenge{ ChallengeId: req.challengeID, Status: goproto.ChallengeStatus_CHALLENGE_DOING, RoundCount: 0, ScoreId: scoreIDFor(req.challengeID), ScoreTwo: 0, ExtraLineupType: goproto.ExtraLineupType(1), } rsp := &goproto.StartChallengeScRsp{ Retcode: 0, CurChallenge: curChallenge, LineupList: lineups, Scene: sceneInfo, } rspBuf, err := proto.Marshal(rsp) if err != nil { return err } return s.SendRaw(&NetPacket{CmdType: 1787, Body: rspBuf}) } func scoreIDFor(challengeID uint32) uint32 { if challengeID >= 20000 && challengeID < 30000 { return 40000 } return 0 } // handleStartChallengeTierce ports handle_start_challenge_tierce -> cmd 8998 (4.6.51). 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: if len(req.secondAvatars) > 0 { chosenAvatars = req.secondAvatars } else { chosenAvatars = req.firstAvatars } case 2: if len(req.thirdAvatars) > 0 { chosenAvatars = req.thirdAvatars } else { chosenAvatars = req.firstAvatars } } 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(3000101), uint32(5), uint32(4033010), uint32(30124011), uint32(0) if req.stageIndex == 0 { 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.Entrance, c.Group1, c.Monster1, c.Event1 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } } else if req.stageIndex == 1 { // Stage 2 (Node 2) if c, ok := cd.Challenges[baseID-1]; ok && c.Entrance2 != 0 { entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } else if c, ok2 := cd.Challenges[baseID]; ok2 && c.Entrance2 != 0 { entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2 if req.buffID != 0 { buff = req.buffID } else { buff = c.Buff } } else if t, ok3 := cd.Tierce[baseID]; ok3 { entrance, group, monster, event, buff = t.Entrance, t.Group, t.Monster, t.Event, req.buffID } } else { if t, ok := cd.Tierce[baseID]; ok { entrance, group, monster, event, buff = t.Entrance, t.Group, t.Monster, t.Event, req.buffID } } 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) } } var tierceLineupList []*goproto.LineupInfo for stIdx, stageAvatars := range allLineups { tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx))) } // 4.6.51 StartChallengeTierceScRsp (CmdID 8998) rsp46 := &goproto.StartChallengeTierceScRsp{ Retcode: 0, Scene: sceneInfo, CurTierceChallenge: &goproto.CurTierceChallenge{ LineupList: tierceLineupList, ChallengeId: req.challengeID, FINOCEGDBDI: req.stageIndex, MPKMIKBFEHG: req.isSingleStage, }, } rspBuf, err := proto.Marshal(rsp46) if err != nil { return err } return s.SendRaw(&NetPacket{CmdType: 8998, Body: rspBuf}) } func handleSetChallengeTierceLineup(s *Session, payload []byte) error { challengeID, stages := decodeSetChallengeTierceLineupReq(payload) if len(stages) > 0 { tierceLineupsMu.Lock() tierceLineups[challengeID] = stages tierceLineupsMu.Unlock() } // 4.6.51 SetChallengeTierceLineupScRsp (cmd 8985, retcode tag 1 = 0) if err := s.SendRaw(&NetPacket{CmdType: 8985, Body: []byte{0x08, 0x00}}); err != nil { return err } _ = s.SendRaw(&NetPacket{CmdType: 8999, Body: []byte{0x08, 0x00}}) _ = s.SendRaw(&NetPacket{CmdType: 8995, Body: []byte{0x60, 0x00}}) return nil } func handleLeaveChallenge(s *Session, _ []byte) error { var returnEntry uint32 = 102020107 if player := s.JSON(); player != nil { player.BattleConfig.CustomBattleLineup = nil // Always return safely to Belobog Admin District 102020107 player.Scene.EntryID = 102020107 player.Scene.PlaneID = 10202 player.Scene.FloorID = 10202001 player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566} player.SavePersistent() } // 4.6.51 LeaveChallengeScRsp (cmd 1771, tag 10 retcode 0) if err := s.SendRaw(&NetPacket{CmdType: 1771, Body: []byte{0x50, 0x00}}); err != nil { return err } _ = s.SendRaw(&NetPacket{CmdType: 1716, Body: []byte{0x20, 0x00}}) _ = s.SendRaw(&NetPacket{CmdType: 1781, Body: []byte{0x40, 0x00}}) // Restore the player back to the open world _, _ = loadScene(s, returnEntry, true, nil, nil) _ = refreshLineup(s) return nil } func handleLeaveChallengeTierce(s *Session, _ []byte) error { var returnEntry uint32 = 102020107 if player := s.JSON(); player != nil { player.BattleConfig.CustomBattleLineup = nil // Always return safely to Belobog Admin District 102020107 player.Scene.EntryID = 102020107 player.Scene.PlaneID = 10202 player.Scene.FloorID = 10202001 player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566} player.SavePersistent() } // 4.6.51 LeaveChallengeTierceScRsp (cmd 8983, tag 13 retcode 0) if err := s.SendRaw(&NetPacket{CmdType: 8983, Body: []byte{0x68, 0x00}}); err != nil { return err } _ = s.SendRaw(&NetPacket{CmdType: 8997, Body: []byte{0x38, 0x00}}) _ = s.SendRaw(&NetPacket{CmdType: 8982, Body: []byte{0x68, 0x00}}) // Restore the player back to the open world _, _ = loadScene(s, returnEntry, true, nil, nil) _ = refreshLineup(s) 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 groups []*goproto.ChallengeTierceGroup for _, e := range tierceStages { groups = append(groups, &goproto.ChallengeTierceGroup{ ChallengeId: e.id, IsWin: true, FinishedTargetList: e.targets, }) } rspMsg := &goproto.GetChallengeTierceDataScRsp{ ChallengeTierceGroups: groups, Retcode: 0, } body, err := proto.Marshal(rspMsg) if err == nil { return body } var rsp []byte rsp = append(rsp, 0x40, 0x00) // tag 8: retcode = 0 rsp = append(rsp, 0x60, 0x00) // legacy tag 12: retcode = 0 return rsp }