Files
GaCy-GO/internal/gameserver/handlers_challenge_flow.go
T
CAtlas0784 876f20d857 fix(challenge,scene): challenge id auto-detect, real ScRsp ids, teleport ownership
Round-2 fixes from the 01:44-01:53 client session:

- StartChallenge (MoC/PF/AS normal start) decoded challenge_id=0 -> client
  retried and aborted. detectChallengeID scans top-level varints for the
  first plausible id (challenge_data + known mode ranges) when the strict
  tag-14 decode misses; payload hexdump logged when still 0
- StartChallengeScRsp now carries stage_info (tag 13) with boss node buff
  echo, per the 4.6.51 proto
- GetSwitchHandData replies 8108 (the real ScRsp) - 8104 is
  SwitchHandStartCsReq, we were sending a request id back at the client
- GetSceneMapInfo: strict proto parse failed on some payloads (layout
  drift) which stalled the map UI ('no pillars / navigation loading') -
  now a raw handler with a lenient wire walk fallback + hexdump logging
- teleport destination ('AA teleport landed in AP'): teleport ids repeat
  on every floor and teleports.json was scanned globally in random map
  order. Map clicks now resolve floor-first via scene_identifier, and the
  global id scan never touches teleports.json
- tierce: arena 3014101 -> 3000101 fallback (client has no MapEntrance
  3014101), same as StartChallenge
2026-10-07 02:13:45 +07:00

1079 lines
27 KiB
Go

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
}