package gameserver import ( "fmt" "sort" "google.golang.org/protobuf/proto" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) // Challenge flows use the generated request and response layouts. Command // numbers are resolved through the current protocol table at registration/send. 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 decodeStartChallengeReq(payload []byte) decodedChallengeStart { var pbReq goproto.StartChallengeCsReq if err := proto.Unmarshal(payload, &pbReq); err != nil { return decodedChallengeStart{} } req := decodedChallengeStart{challengeID: pbReq.ChallengeId, firstAvatars: pbReq.FirstLineup, secondAvatars: pbReq.SecondLineup} if len(req.firstAvatars) == 0 { for _, avatar := range pbReq.AvatarLineupFirst { if avatar.Id != 0 { req.firstAvatars = append(req.firstAvatars, avatar.Id) } } } if len(req.secondAvatars) == 0 { for _, avatar := range pbReq.AvatarLineupSecond { if avatar.Id != 0 { req.secondAvatars = append(req.secondAvatars, avatar.Id) } } } if pbReq.StageInfo != nil { if info := pbReq.StageInfo.BossInfo; info != nil { req.buffID = info.BuffOne } if info := pbReq.StageInfo.StoryInfo; info != nil && req.buffID == 0 { req.buffID = info.BuffOne } } return req } func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart { var pbReq goproto.StartChallengeTierceCsReq if err := proto.Unmarshal(payload, &pbReq); err != nil { return decodedChallengeStart{} } req := decodedChallengeStart{challengeID: pbReq.ChallengeId, stageIndex: pbReq.FINOCEGDBDI, isSingleStage: pbReq.MPKMIKBFEHG} for index, stage := range pbReq.LineupTierceList { var avatars []uint32 for _, avatar := range stage.TierceAvatarIdList { if avatar.Id != 0 { avatars = append(avatars, avatar.Id) } } req.stageInfoList = append(req.stageInfoList, avatars) if uint32(index) == req.stageIndex { req.buffID = stage.BuffId } } if len(req.stageInfoList) > 0 { req.firstAvatars = req.stageInfoList[0] } if len(req.stageInfoList) > 1 { req.secondAvatars = req.stageInfoList[1] } if len(req.stageInfoList) > 2 { req.thirdAvatars = req.stageInfoList[2] } return req } func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) { var req goproto.SetChallengeTierceLineupCsReq if err := proto.Unmarshal(payload, &req); err != nil { return 0, nil } var stages [][]uint32 for _, stage := range req.StageInfoList { var avatars []uint32 for _, avatar := range stage.TierceAvatarIdList { if avatar.Id != 0 { avatars = append(avatars, avatar.Id) } } stages = append(stages, avatars) } return req.ChallengeId, stages } // --------------------------------------------------------------------------- // 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 } // 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 { return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } 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, } return s.Send(rsp) } func scoreIDFor(challengeID uint32) uint32 { if challengeID >= 20000 && challengeID < 30000 { return 40000 } return 0 } // handleStartChallengeTierce uses the current Tierce request schema. func handleStartChallengeTierce(s *Session, payload []byte) error { req := decodeStartChallengeTierceReq(payload) if req.challengeID == 0 { return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } 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))) } // Generated response resolves its command ID through Session.Send. rsp := &goproto.StartChallengeTierceScRsp{ Retcode: 0, Scene: sceneInfo, CurTierceChallenge: &goproto.CurTierceChallenge{ LineupList: tierceLineupList, ChallengeId: req.challengeID, FINOCEGDBDI: req.stageIndex, MPKMIKBFEHG: req.isSingleStage, }, } return s.Send(rsp) } func handleSetChallengeTierceLineup(s *Session, payload []byte) error { challengeID, stages := decodeSetChallengeTierceLineupReq(payload) if challengeID == 0 { return s.Send(&goproto.SetChallengeTierceLineupScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } if len(stages) > 0 { tierceLineupsMu.Lock() tierceLineups[challengeID] = stages tierceLineupsMu.Unlock() } return s.Send(&goproto.SetChallengeTierceLineupScRsp{}) } func handleLeaveChallenge(s *Session, _ []byte) error { return leaveChallengeScene(s, &goproto.LeaveChallengeScRsp{}) } func handleLeaveChallengeTierce(s *Session, _ []byte) error { return leaveChallengeScene(s, &goproto.LeaveChallengeTierceScRsp{}) } func leaveChallengeScene(s *Session, response proto.Message) error { player := s.JSON() if player == nil || player.Scene.EntryID == 0 { return fmt.Errorf("challenge return scene is unavailable") } // loadChallengeScene preserves the player's saved overworld scene. entryID := player.Scene.EntryID player.BattleConfig.CustomBattleLineup = nil CURRENT_BATTLE.Delete(s.Token) if err := s.Send(&goproto.QuitBattleScNotify{}); err != nil { return err } if _, err := loadScene(s, entryID, true, true, nil, nil); err != nil { return err } if err := refreshLineup(s); err != nil { return err } return s.Send(response) } func handleTakeChallengeReward(s *Session, payload []byte) error { var req goproto.TakeChallengeRewardCsReq if err := proto.Unmarshal(payload, &req); err != nil { return err } return s.Send(&goproto.TakeChallengeRewardScRsp{GroupId: req.GroupId}) } // buildGetChallengeTierceDataScRspReal builds the generated current response. func buildGetChallengeTierceDataScRspReal() *goproto.GetChallengeTierceDataScRsp { 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, }) } return &goproto.GetChallengeTierceDataScRsp{ ChallengeTierceGroups: groups, Retcode: 0, } }