Files
GaCy-GO/internal/gameserver/handlers_challenge_flow.go
T

628 lines
17 KiB
Go
Raw Normal View History

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,
}
}