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
1079 lines
27 KiB
Go
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
|
|
}
|