2026-10-07 00:21:41 +07:00
|
|
|
package gameserver
|
|
|
|
|
|
|
|
|
|
import (
|
2026-10-16 22:19:52 +07:00
|
|
|
"fmt"
|
2026-10-07 00:21:41 +07:00
|
|
|
"sort"
|
|
|
|
|
|
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
|
|
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
|
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-16 22:19:52 +07:00
|
|
|
// Challenge flows use the generated request and response layouts. Command
|
|
|
|
|
// numbers are resolved through the current protocol table at registration/send.
|
2026-10-07 00:21:41 +07:00
|
|
|
|
|
|
|
|
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 {
|
2026-10-16 20:08:13 +07:00
|
|
|
cd := getChallengeData()
|
|
|
|
|
if _, ok := cd.Challenges[id]; ok {
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
if _, ok := cd.Tierce[id]; ok {
|
|
|
|
|
return id
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
switch {
|
2026-10-16 20:08:13 +07:00
|
|
|
case id >= 5400 && id <= 5700: // MoC 1035/1036 -> 5301..5312
|
2026-10-07 00:21:41 +07:00
|
|
|
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 {
|
2026-10-16 20:08:13 +07:00
|
|
|
var pbReq goproto.StartChallengeCsReq
|
2026-10-16 22:19:52 +07:00
|
|
|
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)
|
2026-10-16 20:08:13 +07:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
}
|
|
|
|
|
if len(req.secondAvatars) == 0 {
|
|
|
|
|
for _, avatar := range pbReq.AvatarLineupSecond {
|
|
|
|
|
if avatar.Id != 0 {
|
|
|
|
|
req.secondAvatars = append(req.secondAvatars, avatar.Id)
|
2026-10-16 20:08:13 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
if pbReq.StageInfo != nil {
|
|
|
|
|
if info := pbReq.StageInfo.BossInfo; info != nil {
|
|
|
|
|
req.buffID = info.BuffOne
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
if info := pbReq.StageInfo.StoryInfo; info != nil && req.buffID == 0 {
|
|
|
|
|
req.buffID = info.BuffOne
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return req
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart {
|
2026-10-16 20:08:13 +07:00
|
|
|
var pbReq goproto.StartChallengeTierceCsReq
|
2026-10-16 22:19:52 +07:00
|
|
|
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)
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 20:08:13 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
req.stageInfoList = append(req.stageInfoList, avatars)
|
|
|
|
|
if uint32(index) == req.stageIndex {
|
|
|
|
|
req.buffID = stage.BuffId
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
if len(req.stageInfoList) > 0 {
|
|
|
|
|
req.firstAvatars = req.stageInfoList[0]
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
if len(req.stageInfoList) > 1 {
|
|
|
|
|
req.secondAvatars = req.stageInfoList[1]
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
if len(req.stageInfoList) > 2 {
|
|
|
|
|
req.thirdAvatars = req.stageInfoList[2]
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
return req
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) {
|
2026-10-16 22:19:52 +07:00
|
|
|
var req goproto.SetChallengeTierceLineupCsReq
|
|
|
|
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
var stages [][]uint32
|
2026-10-16 22:19:52 +07:00
|
|
|
for _, stage := range req.StageInfoList {
|
|
|
|
|
var avatars []uint32
|
|
|
|
|
for _, avatar := range stage.TierceAvatarIdList {
|
|
|
|
|
if avatar.Id != 0 {
|
|
|
|
|
avatars = append(avatars, avatar.Id)
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
stages = append(stages, avatars)
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
return req.ChallengeId, stages
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2026-10-16 22:19:52 +07:00
|
|
|
// config setup shared by start handlers
|
2026-10-07 00:21:41 +07:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
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
|
2026-10-07 02:13:45 +07:00
|
|
|
// retcode(3) + cur_challenge(5) + lineup_list(6) + scene(10) + stage_info(13).
|
2026-10-07 00:21:41 +07:00
|
|
|
func handleStartChallenge(s *Session, payload []byte) error {
|
|
|
|
|
req := decodeStartChallengeReq(payload)
|
2026-10-07 02:13:45 +07:00
|
|
|
if req.challengeID == 0 {
|
2026-10-16 22:19:52 +07:00
|
|
|
return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
|
2026-10-07 02:13:45 +07:00
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
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()
|
2026-10-16 20:08:13 +07:00
|
|
|
entrance, group, monster, event, buff := uint32(3000101), uint32(5), uint32(4033010), uint32(30124011), uint32(0)
|
2026-10-07 00:21:41 +07:00
|
|
|
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
|
|
|
|
|
}
|
2026-10-16 20:08:13 +07:00
|
|
|
} else if t, ok := cd.Tierce[baseID]; ok {
|
|
|
|
|
entrance, group, monster, event = t.Entrance, t.Group, t.Monster, t.Event
|
|
|
|
|
buff = req.buffID
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
2026-10-16 20:08:13 +07:00
|
|
|
if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) {
|
2026-10-07 02:13:45 +07:00
|
|
|
Log("[CHALLENGE] Arena entrance missing in res.json, fallback to 3000101")
|
|
|
|
|
entrance = 3000101
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 20:08:13 +07:00
|
|
|
var lineups []*goproto.LineupInfo
|
|
|
|
|
lineups = append(lineups, lineupInfoFromAvatars(chosenAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 0))
|
|
|
|
|
|
|
|
|
|
secondAvatars := req.secondAvatars
|
|
|
|
|
if len(secondAvatars) == 0 {
|
|
|
|
|
secondAvatars = chosenAvatars
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 20:08:13 +07:00
|
|
|
secondAvatars = dedupeTruncate4(secondAvatars)
|
|
|
|
|
lineups = append(lineups, lineupInfoFromAvatars(secondAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 1))
|
2026-10-07 00:21:41 +07:00
|
|
|
|
|
|
|
|
curChallenge := &goproto.CurChallenge{
|
|
|
|
|
ChallengeId: req.challengeID,
|
2026-10-16 20:08:13 +07:00
|
|
|
Status: goproto.ChallengeStatus_CHALLENGE_DOING,
|
2026-10-07 00:21:41 +07:00
|
|
|
RoundCount: 0,
|
|
|
|
|
ScoreId: scoreIDFor(req.challengeID),
|
|
|
|
|
ScoreTwo: 0,
|
|
|
|
|
ExtraLineupType: goproto.ExtraLineupType(1),
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 20:08:13 +07:00
|
|
|
rsp := &goproto.StartChallengeScRsp{
|
|
|
|
|
Retcode: 0,
|
|
|
|
|
CurChallenge: curChallenge,
|
|
|
|
|
LineupList: lineups,
|
|
|
|
|
Scene: sceneInfo,
|
2026-10-07 02:13:45 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
return s.Send(rsp)
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scoreIDFor(challengeID uint32) uint32 {
|
|
|
|
|
if challengeID >= 20000 && challengeID < 30000 {
|
|
|
|
|
return 40000
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 22:19:52 +07:00
|
|
|
// handleStartChallengeTierce uses the current Tierce request schema.
|
2026-10-07 00:21:41 +07:00
|
|
|
func handleStartChallengeTierce(s *Session, payload []byte) error {
|
|
|
|
|
req := decodeStartChallengeTierceReq(payload)
|
2026-10-16 22:19:52 +07:00
|
|
|
if req.challengeID == 0 {
|
|
|
|
|
return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
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:
|
2026-10-16 20:08:13 +07:00
|
|
|
if len(req.secondAvatars) > 0 {
|
|
|
|
|
chosenAvatars = req.secondAvatars
|
|
|
|
|
} else {
|
|
|
|
|
chosenAvatars = req.firstAvatars
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
case 2:
|
2026-10-16 20:08:13 +07:00
|
|
|
if len(req.thirdAvatars) > 0 {
|
|
|
|
|
chosenAvatars = req.thirdAvatars
|
|
|
|
|
} else {
|
|
|
|
|
chosenAvatars = req.firstAvatars
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
2026-10-16 20:08:13 +07:00
|
|
|
entrance, group, monster, event, buff := uint32(3000101), uint32(5), uint32(4033010), uint32(30124011), uint32(0)
|
|
|
|
|
|
|
|
|
|
if req.stageIndex == 0 {
|
2026-10-07 00:21:41 +07:00
|
|
|
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 {
|
2026-10-16 20:08:13 +07:00
|
|
|
entrance, group, monster, event = c.Entrance, c.Group1, c.Monster1, c.Event1
|
2026-10-07 00:21:41 +07:00
|
|
|
if req.buffID != 0 {
|
|
|
|
|
buff = req.buffID
|
|
|
|
|
} else {
|
|
|
|
|
buff = c.Buff
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-16 20:08:13 +07:00
|
|
|
} else if req.stageIndex == 1 {
|
|
|
|
|
// Stage 2 (Node 2)
|
|
|
|
|
if c, ok := cd.Challenges[baseID-1]; ok && c.Entrance2 != 0 {
|
2026-10-07 00:21:41 +07:00
|
|
|
entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2
|
|
|
|
|
if req.buffID != 0 {
|
|
|
|
|
buff = req.buffID
|
|
|
|
|
} else {
|
|
|
|
|
buff = c.Buff
|
|
|
|
|
}
|
2026-10-16 20:08:13 +07:00
|
|
|
} else if c, ok2 := cd.Challenges[baseID]; ok2 && c.Entrance2 != 0 {
|
|
|
|
|
entrance, group, monster, event = c.Entrance2, c.Group2, c.Monster2, c.Event2
|
2026-10-07 00:21:41 +07:00
|
|
|
if req.buffID != 0 {
|
|
|
|
|
buff = req.buffID
|
|
|
|
|
} else {
|
|
|
|
|
buff = c.Buff
|
|
|
|
|
}
|
2026-10-16 20:08:13 +07:00
|
|
|
} 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
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 20:08:13 +07:00
|
|
|
if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) {
|
2026-10-07 02:13:45 +07:00
|
|
|
Log("[CHALLENGE] Tierce arena entrance missing in res.json, fallback to 3000101")
|
|
|
|
|
entrance = 3000101
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 00:21:41 +07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-16 20:08:13 +07:00
|
|
|
var tierceLineupList []*goproto.LineupInfo
|
2026-10-07 00:21:41 +07:00
|
|
|
for stIdx, stageAvatars := range allLineups {
|
2026-10-16 20:08:13 +07:00
|
|
|
tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx)))
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
2026-10-16 22:19:52 +07:00
|
|
|
// Generated response resolves its command ID through Session.Send.
|
|
|
|
|
rsp := &goproto.StartChallengeTierceScRsp{
|
2026-10-16 20:08:13 +07:00
|
|
|
Retcode: 0,
|
|
|
|
|
Scene: sceneInfo,
|
|
|
|
|
CurTierceChallenge: &goproto.CurTierceChallenge{
|
|
|
|
|
LineupList: tierceLineupList,
|
|
|
|
|
ChallengeId: req.challengeID,
|
|
|
|
|
FINOCEGDBDI: req.stageIndex,
|
|
|
|
|
MPKMIKBFEHG: req.isSingleStage,
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
return s.Send(rsp)
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleSetChallengeTierceLineup(s *Session, payload []byte) error {
|
|
|
|
|
challengeID, stages := decodeSetChallengeTierceLineupReq(payload)
|
2026-10-16 22:19:52 +07:00
|
|
|
if challengeID == 0 {
|
|
|
|
|
return s.Send(&goproto.SetChallengeTierceLineupScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
if len(stages) > 0 {
|
|
|
|
|
tierceLineupsMu.Lock()
|
|
|
|
|
tierceLineups[challengeID] = stages
|
|
|
|
|
tierceLineupsMu.Unlock()
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
return s.Send(&goproto.SetChallengeTierceLineupScRsp{})
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleLeaveChallenge(s *Session, _ []byte) error {
|
2026-10-16 22:19:52 +07:00
|
|
|
return leaveChallengeScene(s, &goproto.LeaveChallengeScRsp{})
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleLeaveChallengeTierce(s *Session, _ []byte) error {
|
2026-10-16 22:19:52 +07:00
|
|
|
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")
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
// 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 {
|
2026-10-07 00:21:41 +07:00
|
|
|
return err
|
|
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
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)
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleTakeChallengeReward(s *Session, payload []byte) error {
|
2026-10-16 22:19:52 +07:00
|
|
|
var req goproto.TakeChallengeRewardCsReq
|
|
|
|
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
|
|
|
|
return err
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
2026-10-16 22:19:52 +07:00
|
|
|
return s.Send(&goproto.TakeChallengeRewardScRsp{GroupId: req.GroupId})
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|
|
|
|
|
|
2026-10-16 22:19:52 +07:00
|
|
|
// buildGetChallengeTierceDataScRspReal builds the generated current response.
|
|
|
|
|
func buildGetChallengeTierceDataScRspReal() *goproto.GetChallengeTierceDataScRsp {
|
2026-10-07 00:21:41 +07:00
|
|
|
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 })
|
|
|
|
|
|
2026-10-16 20:08:13 +07:00
|
|
|
var groups []*goproto.ChallengeTierceGroup
|
2026-10-07 00:21:41 +07:00
|
|
|
for _, e := range tierceStages {
|
2026-10-16 20:08:13 +07:00
|
|
|
groups = append(groups, &goproto.ChallengeTierceGroup{
|
|
|
|
|
ChallengeId: e.id,
|
|
|
|
|
IsWin: true,
|
|
|
|
|
FinishedTargetList: e.targets,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
|
2026-10-16 22:19:52 +07:00
|
|
|
return &goproto.GetChallengeTierceDataScRsp{
|
2026-10-16 20:08:13 +07:00
|
|
|
ChallengeTierceGroups: groups,
|
|
|
|
|
Retcode: 0,
|
|
|
|
|
}
|
2026-10-07 00:21:41 +07:00
|
|
|
}
|