fix(scene): unlock all teleports across maps, merge resource configs, fix scenary not load error, and add quick farm battle handlers
This commit is contained in:
1 parent
9aab62414f
commit
163d4433e6
47 files changed
+2025067
-6082
No files matched your search
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
@@ -33,15 +34,7 @@ func protoMarshal(msg proto.Message) ([]byte, error) {
|
||||
// BASE_AVATAR_IDS — 89 avatars (Rust: handlers/avatar.rs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var BaseAvatarIDs = []uint32{
|
||||
8001, 1001,
|
||||
1002, 1003, 1004, 1005, 1006, 1008, 1009, 1013, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108,
|
||||
1109, 1110, 1111, 1112, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212,
|
||||
1213, 1214, 1215, 1217, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1312, 1315, 1310,
|
||||
1314, 1218, 1221, 1220, 1222, 1223, 1317, 1313, 1225, 1402, 1401, 1404, 1403, 1405, 1407, 1406,
|
||||
1409, 1014, 1015, 1408, 1410, 1412, 1413, 1414, 1415, 1321, 1501, 1502, 1503, 1504, 1505, 1506,
|
||||
1507, 1508, 1509, 1510, 1511, 1512, 1513,
|
||||
}
|
||||
var BaseAvatarIDs = common.BaseAvatarIDs
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch — Go port of the trait_handler! macro + on_message special cases.
|
||||
@@ -109,7 +102,7 @@ func dummy(name string) {
|
||||
// never auto-answer these, drop them silently instead.
|
||||
var dangerFamilies = []string{
|
||||
"GridFight", "ChessRogue", "EvolveBuild", "ChimeraDuel",
|
||||
"Jukebox", "CycleScore", "MusicAlbum",
|
||||
"Jukebox", "CycleScore", "MusicAlbum", "DrinkMaker",
|
||||
}
|
||||
|
||||
func isDangerCmd(cmdID uint16) bool {
|
||||
@@ -121,7 +114,7 @@ func isDangerCmd(cmdID uint16) bool {
|
||||
}
|
||||
// CycleScore cmd ids (4143/4146) and GridFight range carry generic names
|
||||
switch cmdID {
|
||||
case 4143, 4146, 8541, 5410, 5449, 7142, 3174, 9252, 8175:
|
||||
case 4143, 4146, 8541, 5410, 5449, 7142, 3174, 9252, 8175, 6995, 6988:
|
||||
return true
|
||||
}
|
||||
// GetMapRotationData: an empty reply NREs the client's map init
|
||||
|
||||
@@ -183,12 +183,12 @@ func (g *Gateway) processKCPPayload(data []byte, addr *net.UDPAddr) {
|
||||
}
|
||||
}
|
||||
|
||||
// watchFreesrData polls freesr-data.json mtime every second (the Rust version
|
||||
// used a notify debouncer) and re-syncs the player on change.
|
||||
// watchFreesrData polls freesr-data.json mtime every second
|
||||
// and re-syncs the player in real-time when changed.
|
||||
func watchFreesrData(session *Session) {
|
||||
var lastMod time.Time
|
||||
var lastModFreesr time.Time
|
||||
if st, err := statFile("freesr-data.json"); err == nil {
|
||||
lastMod = st
|
||||
lastModFreesr = st
|
||||
}
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -197,18 +197,19 @@ func watchFreesrData(session *Session) {
|
||||
case <-session.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
st, err := statFile("freesr-data.json")
|
||||
if err != nil || !st.After(lastMod) {
|
||||
stFreesr, _ := statFile("freesr-data.json")
|
||||
if stFreesr.IsZero() || !stFreesr.After(lastModFreesr) {
|
||||
continue
|
||||
}
|
||||
lastMod = st
|
||||
lastModFreesr = stFreesr
|
||||
|
||||
if data, err := common.LoadFreesrData(); err == nil {
|
||||
session.SetJSON(data)
|
||||
if err := session.SyncPlayer(); err == nil {
|
||||
Log("json updated")
|
||||
Log("player data updated & live synced to client")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ func init() {
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, id := range []uint32{1130301, 1100101, 1131001, 1141501, 1140701, 1150101, 1150501, 1140901} {
|
||||
if !seen[id] {
|
||||
seen[id] = true
|
||||
rsp.SkinList = append(rsp.SkinList, id)
|
||||
}
|
||||
}
|
||||
sort.Slice(rsp.SkinList, func(i, j int) bool { return rsp.SkinList[i] < rsp.SkinList[j] })
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package gameserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
@@ -11,56 +12,88 @@ import (
|
||||
|
||||
func init() {
|
||||
handle("StartCocoonStage", func(s *Session, req *goproto.StartCocoonStageCsReq, rsp *goproto.StartCocoonStageScRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] StartCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d PropEntityId=%d", req.CocoonId, req.Wave, req.WorldLevel, req.PropEntityId))
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
if req.WorldLevel > 0 {
|
||||
battleInfo.WorldLevel = req.WorldLevel
|
||||
}
|
||||
if req.Wave > 0 && int(req.Wave) <= len(battleInfo.MonsterWaveList) {
|
||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:req.Wave]
|
||||
} else if len(battleInfo.MonsterWaveList) > 1 {
|
||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:1]
|
||||
}
|
||||
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
||||
for _, w := range battleInfo.MonsterWaveList {
|
||||
w.WaveId = 1
|
||||
w.WaveParam = nil
|
||||
w.BattleStageId = battleInfo.StageId
|
||||
}
|
||||
normalizeCalyxBattleInfo(battleInfo, req.Wave, req.WorldLevel)
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
challengeCnt := req.Wave
|
||||
if challengeCnt == 0 {
|
||||
challengeCnt = 1
|
||||
}
|
||||
rsp.Retcode = 0
|
||||
rsp.PropEntityId = req.PropEntityId
|
||||
rsp.CocoonId = req.CocoonId
|
||||
rsp.Wave = req.Wave
|
||||
rsp.ChallengeCnt = challengeCnt
|
||||
rsp.BattleInfo = battleInfo
|
||||
return nil
|
||||
})
|
||||
|
||||
// NOTE: 4.6.51 proto names this message StartQuickCocoonStageCsReq/Rsp
|
||||
handleNamed("StartQuickCocoonStage", "StartQuickCocoonStageRsp", func(s *Session, req *goproto.StartQuickCocoonStageCsReq, rsp *goproto.StartQuickCocoonStageRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] StartQuickCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d", req.CocoonId, req.Wave, req.WorldLevel))
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
normalizeCalyxBattleInfo(battleInfo, req.Wave, req.WorldLevel)
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
rsp.Retcode = 0
|
||||
rsp.CocoonId = req.CocoonId
|
||||
rsp.Wave = req.Wave
|
||||
rsp.BattleInfo = battleInfo
|
||||
return nil
|
||||
})
|
||||
|
||||
// NOTE: 4.6.51 proto names this message StartQuickCocoonStageCsReq/Rsp
|
||||
// (the Rust code referenced an older QuickStartCocoonStage* name).
|
||||
handleNamed("StartQuickCocoonStage", "StartQuickCocoonStageRsp", func(s *Session, req *goproto.StartQuickCocoonStageCsReq, rsp *goproto.StartQuickCocoonStageRsp) error {
|
||||
handleNamed("StartQuickFarmElement", "StartQuickFarmElementRsp", func(s *Session, req *goproto.StartQuickFarmElementCsReq, rsp *goproto.StartQuickFarmElementRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] StartQuickFarmElement: FarmID=%d WorldLevel=%d", req.PAOFHFLFFHD, req.WorldLevel))
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
if req.WorldLevel > 0 {
|
||||
battleInfo.WorldLevel = req.WorldLevel
|
||||
}
|
||||
if req.Wave > 0 && int(req.Wave) <= len(battleInfo.MonsterWaveList) {
|
||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:req.Wave]
|
||||
} else if len(battleInfo.MonsterWaveList) > 1 {
|
||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:1]
|
||||
}
|
||||
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
||||
for _, w := range battleInfo.MonsterWaveList {
|
||||
w.WaveId = 1
|
||||
w.WaveParam = nil
|
||||
w.BattleStageId = battleInfo.StageId
|
||||
}
|
||||
normalizeCalyxBattleInfo(battleInfo, 1, req.WorldLevel)
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
rsp.CocoonId = req.CocoonId
|
||||
rsp.Wave = req.Wave
|
||||
rsp.Retcode = 0
|
||||
rsp.PAOFHFLFFHD = req.PAOFHFLFFHD
|
||||
rsp.WorldLevel = req.WorldLevel
|
||||
rsp.BattleInfo = battleInfo
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("ActiveFarmElement", func(s *Session, req *goproto.ActiveFarmElementCsReq, rsp *goproto.ActiveFarmElementScRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] ActiveFarmElement: EntityId=%d WorldLevel=%d", req.EntityId, req.WorldLevel))
|
||||
rsp.Retcode = 0
|
||||
rsp.EntityId = req.EntityId
|
||||
rsp.WorldLevel = req.WorldLevel
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("DeactivateFarmElement", func(s *Session, req *goproto.DeactivateFarmElementCsReq, rsp *goproto.DeactivateFarmElementScRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] DeactivateFarmElement: EntityId=%d", req.EntityId))
|
||||
rsp.Retcode = 0
|
||||
rsp.EntityId = req.EntityId
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("CocoonSweep", func(s *Session, req *goproto.CocoonSweepCsReq, rsp *goproto.CocoonSweepScRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] CocoonSweep: CocoonId=%d WorldLevel=%d", req.CocoonId, req.WorldLevel))
|
||||
rsp.Retcode = 0
|
||||
rsp.CocoonId = req.CocoonId
|
||||
rsp.DropData = &goproto.ItemList{}
|
||||
rsp.MultipleDropData = &goproto.ItemList{}
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("FarmElementSweep", func(s *Session, req *goproto.FarmElementSweepCsReq, rsp *goproto.FarmElementSweepScRsp) error {
|
||||
Log(fmt.Sprintf("[BATTLE] FarmElementSweep: FarmID=%d", req.PAOFHFLFFHD))
|
||||
rsp.Retcode = 0
|
||||
rsp.PAOFHFLFFHD = req.PAOFHFLFFHD
|
||||
rsp.MultipleDropData = &goproto.ItemList{}
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("GetFarmStageGachaInfo", func(s *Session, req *goproto.GetFarmStageGachaInfoCsReq, rsp *goproto.GetFarmStageGachaInfoScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
return nil
|
||||
})
|
||||
|
||||
handle("SceneEnterStage", func(s *Session, req *goproto.SceneEnterStageCsReq, rsp *goproto.SceneEnterStageScRsp) error {
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
@@ -152,19 +185,32 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if player := s.JSON(); player != nil && len(player.BattleConfig.Monsters) > 0 {
|
||||
battleInfo := createBattleInfo(s, 0, 0)
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
rsp.Retcode = 0
|
||||
rsp.BattleInfo = battleInfo
|
||||
return nil
|
||||
}
|
||||
|
||||
rsp.Retcode = 2000 // RET_BATTLE_STAGE_NOT_MATCH
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeCalyxBattleInfo(battleInfo *goproto.SceneBattleInfo, reqWave uint32, worldLevel uint32) {
|
||||
if worldLevel > 0 {
|
||||
battleInfo.WorldLevel = worldLevel
|
||||
}
|
||||
if reqWave > 0 && int(reqWave) <= len(battleInfo.MonsterWaveList) {
|
||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:reqWave]
|
||||
} else if len(battleInfo.MonsterWaveList) > 1 {
|
||||
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:1]
|
||||
}
|
||||
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
|
||||
for _, w := range battleInfo.MonsterWaveList {
|
||||
// NOTE: In client 4.6.53 GameAssembly.dll 0x166b2fdd, client verifies:
|
||||
// (wave.WaveId & ~8) == 1. If wave_id != 1, it jumps to 0x166b3161
|
||||
// and throws NullReferenceException in AdventureModule.CreateBattleServerCheckResult.
|
||||
// Therefore every wave in Calyx / FarmElement MUST have WaveId = 1 and WaveParam = nil.
|
||||
w.WaveId = 1
|
||||
w.WaveParam = nil
|
||||
w.BattleStageId = battleInfo.StageId
|
||||
}
|
||||
}
|
||||
|
||||
// createBattleInfo is the Go port of battle.rs create_battle_info.
|
||||
func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBattleInfo {
|
||||
player := s.JSON()
|
||||
|
||||
@@ -62,8 +62,15 @@ func skipWireField(wireType uint32, buf *[]byte) {
|
||||
}
|
||||
|
||||
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 <= 5600: // MoC 1035/1036 -> 5301..5312
|
||||
case id >= 5400 && id <= 5700: // MoC 1035/1036 -> 5301..5312
|
||||
rem := id % 100
|
||||
if rem < 1 {
|
||||
rem = 1
|
||||
@@ -91,7 +98,6 @@ func mapChallengeStageID(id uint32) uint32 {
|
||||
}
|
||||
return 30200 + rem
|
||||
case id > 1:
|
||||
cd := getChallengeData()
|
||||
if _, ok := cd.Challenges[id]; !ok {
|
||||
return id - 1
|
||||
}
|
||||
@@ -100,10 +106,59 @@ func mapChallengeStageID(id uint32) uint32 {
|
||||
return id
|
||||
}
|
||||
|
||||
func readLenDelimAndAdvance(buf *[]byte) ([]byte, bool) {
|
||||
b := *buf
|
||||
l, n := readUvarint(b)
|
||||
if n == 0 || int(l) > len(b[n:]) {
|
||||
*buf = nil
|
||||
return nil, false
|
||||
}
|
||||
sub := b[n : n+int(l)]
|
||||
*buf = b[n+int(l):]
|
||||
return sub, true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
var req decodedChallengeStart
|
||||
buf := payload
|
||||
|
||||
var pbReq goproto.StartChallengeCsReq
|
||||
if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 {
|
||||
req.challengeID = pbReq.ChallengeId
|
||||
req.firstAvatars = pbReq.FirstLineup
|
||||
if len(req.firstAvatars) == 0 && len(pbReq.AvatarLineupFirst) > 0 {
|
||||
for _, av := range pbReq.AvatarLineupFirst {
|
||||
if av.Id != 0 {
|
||||
req.firstAvatars = append(req.firstAvatars, av.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
req.secondAvatars = pbReq.SecondLineup
|
||||
if len(req.secondAvatars) == 0 && len(pbReq.AvatarLineupSecond) > 0 {
|
||||
for _, av := range pbReq.AvatarLineupSecond {
|
||||
if av.Id != 0 {
|
||||
req.secondAvatars = append(req.secondAvatars, av.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if pbReq.StageInfo != nil {
|
||||
if pbReq.StageInfo.BossInfo != nil && pbReq.StageInfo.BossInfo.BuffOne != 0 {
|
||||
req.buffID = pbReq.StageInfo.BossInfo.BuffOne
|
||||
} else if pbReq.StageInfo.StoryInfo != nil && pbReq.StageInfo.StoryInfo.BuffOne != 0 {
|
||||
req.buffID = pbReq.StageInfo.StoryInfo.BuffOne
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
buf := payload
|
||||
for len(buf) > 0 {
|
||||
tag, n := readUvarint(buf)
|
||||
if n == 0 {
|
||||
@@ -133,13 +188,10 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
buf = buf[n2:]
|
||||
req.firstAvatars = append(req.firstAvatars, uint32(v))
|
||||
case fieldNumber == 6 && wireType == 2:
|
||||
sub, ok := readLenDelim(buf)
|
||||
sub, ok := readLenDelimAndAdvance(&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 {
|
||||
@@ -157,12 +209,10 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
buf = buf[n2:]
|
||||
req.secondAvatars = append(req.secondAvatars, uint32(v))
|
||||
case fieldNumber == 8 && wireType == 2:
|
||||
sub, ok := readLenDelim(buf)
|
||||
sub, ok := readLenDelimAndAdvance(&buf)
|
||||
if !ok {
|
||||
buf = nil
|
||||
break
|
||||
}
|
||||
buf = buf[len(sub):]
|
||||
for len(sub) > 0 {
|
||||
v, n3 := readUvarint(sub)
|
||||
if n3 == 0 {
|
||||
@@ -171,14 +221,11 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
sub = sub[n3:]
|
||||
req.secondAvatars = append(req.secondAvatars, uint32(v))
|
||||
}
|
||||
case fieldNumber == 4 && wireType == 2:
|
||||
// AvatarLineup: id = tag 1
|
||||
sub, ok := readLenDelim(buf)
|
||||
case (fieldNumber == 4 || fieldNumber == 15) && wireType == 2:
|
||||
sub, ok := readLenDelimAndAdvance(&buf)
|
||||
if !ok {
|
||||
buf = nil
|
||||
break
|
||||
}
|
||||
buf = buf[len(sub):]
|
||||
for len(sub) > 0 {
|
||||
stag, n3 := readUvarint(sub)
|
||||
if n3 == 0 {
|
||||
@@ -192,19 +239,20 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
break
|
||||
}
|
||||
sub = sub[n3+n4:]
|
||||
req.firstAvatars = append(req.firstAvatars, uint32(v))
|
||||
if fieldNumber == 4 {
|
||||
req.firstAvatars = append(req.firstAvatars, uint32(v))
|
||||
} else {
|
||||
req.secondAvatars = append(req.secondAvatars, uint32(v))
|
||||
}
|
||||
} else {
|
||||
skipWireField(swt, &sub)
|
||||
}
|
||||
}
|
||||
case fieldNumber == 10 && wireType == 2:
|
||||
// ChallengeBuffInfo { story_info = 9, boss_info = 10 }
|
||||
sub, ok := readLenDelim(buf)
|
||||
sub, ok := readLenDelimAndAdvance(&buf)
|
||||
if !ok {
|
||||
buf = nil
|
||||
break
|
||||
}
|
||||
buf = buf[len(sub):]
|
||||
for len(sub) > 0 {
|
||||
stag, n3 := readUvarint(sub)
|
||||
if n3 == 0 {
|
||||
@@ -212,9 +260,9 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
}
|
||||
sfn := uint32(stag >> 3)
|
||||
swt := uint32(stag & 0x7)
|
||||
sub2 := sub[n3:]
|
||||
sub = sub[n3:]
|
||||
if swt == 2 && (sfn == 9 || sfn == 10) {
|
||||
inner, ok2 := readLenDelim(sub2)
|
||||
inner, ok2 := readLenDelimAndAdvance(&sub)
|
||||
if !ok2 {
|
||||
break
|
||||
}
|
||||
@@ -225,12 +273,13 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
}
|
||||
f2 := uint32(t2 >> 3)
|
||||
w2 := uint32(t2 & 0x7)
|
||||
inner = inner[n4:]
|
||||
if w2 == 0 && (f2 == 10 || f2 == 15) {
|
||||
v, n5 := readUvarint(inner[n4:])
|
||||
v, n5 := readUvarint(inner)
|
||||
if n5 == 0 {
|
||||
break
|
||||
}
|
||||
inner = inner[n4+n5:]
|
||||
inner = inner[n5:]
|
||||
if req.buffID == 0 {
|
||||
req.buffID = uint32(v)
|
||||
}
|
||||
@@ -238,14 +287,6 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
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)
|
||||
}
|
||||
@@ -258,13 +299,6 @@ func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
|
||||
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
|
||||
@@ -273,105 +307,122 @@ type tierceStageEntry struct {
|
||||
|
||||
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
|
||||
var pbReq goproto.StartChallengeTierceCsReq
|
||||
if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 {
|
||||
req.challengeID = pbReq.ChallengeId
|
||||
req.stageIndex = pbReq.FINOCEGDBDI
|
||||
req.isSingleStage = pbReq.MPKMIKBFEHG
|
||||
for _, lt := range pbReq.LineupTierceList {
|
||||
var l []uint32
|
||||
for _, av := range lt.TierceAvatarIdList {
|
||||
if av.Id != 0 {
|
||||
l = append(l, av.Id)
|
||||
}
|
||||
}
|
||||
stagesAvatars = append(stagesAvatars, tierceStageEntry{
|
||||
buff: lt.BuffId,
|
||||
lineup: l,
|
||||
})
|
||||
}
|
||||
buf = buf[n:]
|
||||
fieldNumber := uint32(tag >> 3)
|
||||
wireType := uint32(tag & 0x7)
|
||||
} else {
|
||||
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 == 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)
|
||||
switch {
|
||||
case (fieldNumber == 10 || fieldNumber == 1 || fieldNumber == 5) && wireType == 0:
|
||||
v, n2 := readUvarint(buf)
|
||||
if n2 == 0 {
|
||||
buf = nil
|
||||
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
|
||||
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 == 2 || fieldNumber == 13) && wireType == 0:
|
||||
v, n2 := readUvarint(buf)
|
||||
if n2 == 0 {
|
||||
buf = nil
|
||||
break
|
||||
}
|
||||
buf = buf[n2:]
|
||||
req.isSingleStage = v != 0
|
||||
case (fieldNumber == 9 || fieldNumber == 12 || fieldNumber == 14) && wireType == 2:
|
||||
sub, ok := readLenDelimAndAdvance(&buf)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
var stageBuff uint32
|
||||
var stageLineup []uint32
|
||||
for len(sub) > 0 {
|
||||
subTag, n2 := readUvarint(sub)
|
||||
if n2 == 0 {
|
||||
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 {
|
||||
subFn := uint32(subTag >> 3)
|
||||
subWt := uint32(subTag & 0x7)
|
||||
sub = sub[n2:]
|
||||
switch {
|
||||
case (subFn == 10 || subFn == 6 || subFn == 5) && subWt == 0:
|
||||
v, n3 := readUvarint(sub)
|
||||
if n3 == 0 {
|
||||
sub = nil
|
||||
break
|
||||
}
|
||||
afn := uint32(atag >> 3)
|
||||
awt := uint32(atag & 0x7)
|
||||
if (afn == 7 || afn == 9) && awt == 0 {
|
||||
v, n5 := readUvarint(abuf[n4:])
|
||||
if n5 == 0 {
|
||||
sub = sub[n3:]
|
||||
stageBuff = uint32(v)
|
||||
case (subFn == 9 || subFn == 4) && subWt == 2:
|
||||
abuf, ok2 := readLenDelimAndAdvance(&sub)
|
||||
if !ok2 {
|
||||
break
|
||||
}
|
||||
var aid uint32
|
||||
for len(abuf) > 0 {
|
||||
atag, n4 := readUvarint(abuf)
|
||||
if n4 == 0 {
|
||||
break
|
||||
}
|
||||
abuf = abuf[n4+n5:]
|
||||
aid = uint32(v)
|
||||
} else {
|
||||
skipWireField(awt, &abuf)
|
||||
afn := uint32(atag >> 3)
|
||||
awt := uint32(atag & 0x7)
|
||||
abuf = abuf[n4:]
|
||||
if (afn == 1 || afn == 7 || afn == 9) && awt == 0 {
|
||||
v, n5 := readUvarint(abuf)
|
||||
if n5 == 0 {
|
||||
break
|
||||
}
|
||||
abuf = abuf[n5:]
|
||||
aid = uint32(v)
|
||||
} else {
|
||||
skipWireField(awt, &abuf)
|
||||
}
|
||||
}
|
||||
if aid != 0 {
|
||||
stageLineup = append(stageLineup, aid)
|
||||
}
|
||||
default:
|
||||
skipWireField(subWt, &sub)
|
||||
}
|
||||
if aid != 0 {
|
||||
stageLineup = append(stageLineup, aid)
|
||||
}
|
||||
default:
|
||||
skipWireField(subWt, &sub)
|
||||
}
|
||||
stagesAvatars = append(stagesAvatars, tierceStageEntry{buff: stageBuff, lineup: stageLineup})
|
||||
default:
|
||||
skipWireField(wireType, &buf)
|
||||
}
|
||||
stagesAvatars = append(stagesAvatars, tierceStageEntry{buff: stageBuff, lineup: stageLineup})
|
||||
default:
|
||||
skipWireField(wireType, &buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +687,7 @@ func handleStartChallenge(s *Session, payload []byte) error {
|
||||
|
||||
baseID := mapChallengeStageID(req.challengeID)
|
||||
cd := getChallengeData()
|
||||
entrance, group, monster, event, buff := uint32(3014101), uint32(5), uint32(4033010), uint32(30124011), uint32(0)
|
||||
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 {
|
||||
@@ -644,13 +695,12 @@ func handleStartChallenge(s *Session, payload []byte) error {
|
||||
} 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) {
|
||||
Log("[CHALLENGE] Arena entrance missing in res.json, fallback to 3000101")
|
||||
entrance = 3000101
|
||||
}
|
||||
@@ -667,64 +717,37 @@ func handleStartChallenge(s *Session, payload []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
custom := map[uint32]uint32{}
|
||||
for i, aid := range chosenAvatars {
|
||||
custom[uint32(i)] = aid
|
||||
var lineups []*goproto.LineupInfo
|
||||
lineups = append(lineups, lineupInfoFromAvatars(chosenAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 0))
|
||||
|
||||
secondAvatars := req.secondAvatars
|
||||
if len(secondAvatars) == 0 {
|
||||
secondAvatars = chosenAvatars
|
||||
}
|
||||
lineupInfo := common.ToLineupInfo(custom)
|
||||
secondAvatars = dedupeTruncate4(secondAvatars)
|
||||
lineups = append(lineups, lineupInfoFromAvatars(secondAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), 1))
|
||||
|
||||
curChallenge := &goproto.CurChallenge{
|
||||
ChallengeId: req.challengeID,
|
||||
Status: 1, // CHALLENGE_DOING
|
||||
Status: goproto.ChallengeStatus_CHALLENGE_DOING,
|
||||
RoundCount: 0,
|
||||
ScoreId: scoreIDFor(req.challengeID),
|
||||
ScoreTwo: 0,
|
||||
ExtraLineupType: goproto.ExtraLineupType(1),
|
||||
}
|
||||
|
||||
chalBuf, err := proto.Marshal(curChallenge)
|
||||
if err != nil {
|
||||
return err
|
||||
rsp := &goproto.StartChallengeScRsp{
|
||||
Retcode: 0,
|
||||
CurChallenge: curChallenge,
|
||||
LineupList: lineups,
|
||||
Scene: sceneInfo,
|
||||
}
|
||||
lineupBuf, err := proto.Marshal(lineupInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sceneBuf, err := proto.Marshal(sceneInfo)
|
||||
rspBuf, err := proto.Marshal(rsp)
|
||||
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})
|
||||
return s.SendRaw(&NetPacket{CmdType: 1787, Body: rspBuf})
|
||||
}
|
||||
|
||||
func scoreIDFor(challengeID uint32) uint32 {
|
||||
@@ -734,7 +757,7 @@ func scoreIDFor(challengeID uint32) uint32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// handleStartChallengeTierce ports handle_start_challenge_tierce -> cmd 8973.
|
||||
// handleStartChallengeTierce ports handle_start_challenge_tierce -> cmd 8998 (4.6.51).
|
||||
func handleStartChallengeTierce(s *Session, payload []byte) error {
|
||||
req := decodeStartChallengeTierceReq(payload)
|
||||
baseID := mapChallengeStageID(req.challengeID)
|
||||
@@ -751,9 +774,17 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
|
||||
case 0:
|
||||
chosenAvatars = req.firstAvatars
|
||||
case 1:
|
||||
chosenAvatars = req.secondAvatars
|
||||
if len(req.secondAvatars) > 0 {
|
||||
chosenAvatars = req.secondAvatars
|
||||
} else {
|
||||
chosenAvatars = req.firstAvatars
|
||||
}
|
||||
case 2:
|
||||
chosenAvatars = req.thirdAvatars
|
||||
if len(req.thirdAvatars) > 0 {
|
||||
chosenAvatars = req.thirdAvatars
|
||||
} else {
|
||||
chosenAvatars = req.firstAvatars
|
||||
}
|
||||
}
|
||||
|
||||
if len(chosenAvatars) == 0 {
|
||||
@@ -768,32 +799,12 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
|
||||
}
|
||||
|
||||
cd := getChallengeData()
|
||||
entrance, group, monster, event, buff := uint32(3014101), uint32(5), uint32(4033010), uint32(30124011), uint32(0)
|
||||
switch {
|
||||
case req.stageIndex == 2:
|
||||
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.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
|
||||
@@ -801,13 +812,32 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if res, err := common.GameRes(); err == nil && !mapHasEntry(res, entrance) {
|
||||
Log("[CHALLENGE] Tierce arena entrance missing in res.json, fallback to 3000101")
|
||||
entrance = 3000101
|
||||
}
|
||||
@@ -875,43 +905,27 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var tierceLineupList []*goproto.LineupInfo
|
||||
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...)
|
||||
tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx)))
|
||||
}
|
||||
|
||||
sceneBuf, err := proto.Marshal(sceneInfo)
|
||||
// 4.6.51 StartChallengeTierceScRsp (CmdID 8998)
|
||||
rsp46 := &goproto.StartChallengeTierceScRsp{
|
||||
Retcode: 0,
|
||||
Scene: sceneInfo,
|
||||
CurTierceChallenge: &goproto.CurTierceChallenge{
|
||||
LineupList: tierceLineupList,
|
||||
ChallengeId: req.challengeID,
|
||||
FINOCEGDBDI: req.stageIndex,
|
||||
MPKMIKBFEHG: req.isSingleStage,
|
||||
},
|
||||
}
|
||||
rspBuf, err := proto.Marshal(rsp46)
|
||||
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})
|
||||
return s.SendRaw(&NetPacket{CmdType: 8998, Body: rspBuf})
|
||||
}
|
||||
|
||||
func handleSetChallengeTierceLineup(s *Session, payload []byte) error {
|
||||
@@ -921,34 +935,60 @@ func handleSetChallengeTierceLineup(s *Session, payload []byte) error {
|
||||
tierceLineups[challengeID] = stages
|
||||
tierceLineupsMu.Unlock()
|
||||
}
|
||||
if err := s.SendRaw(&NetPacket{CmdType: 8999, Body: []byte{0x08, 0x00}}); err != nil {
|
||||
// 4.6.51 SetChallengeTierceLineupScRsp (cmd 8985, retcode tag 1 = 0)
|
||||
if err := s.SendRaw(&NetPacket{CmdType: 8985, Body: []byte{0x08, 0x00}}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.SendRaw(&NetPacket{CmdType: 8999, Body: []byte{0x08, 0x00}})
|
||||
_ = s.SendRaw(&NetPacket{CmdType: 8995, Body: []byte{0x60, 0x00}})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleLeaveChallenge(s *Session, _ []byte) error {
|
||||
var returnEntry uint32 = 102020107
|
||||
if player := s.JSON(); player != nil {
|
||||
player.BattleConfig.CustomBattleLineup = nil
|
||||
// Always return safely to Belobog Admin District 102020107
|
||||
player.Scene.EntryID = 102020107
|
||||
player.Scene.PlaneID = 10202
|
||||
player.Scene.FloorID = 10202001
|
||||
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
|
||||
player.SavePersistent()
|
||||
}
|
||||
if err := s.SendRaw(&NetPacket{CmdType: 1716, Body: []byte{0x20, 0x00}}); err != nil {
|
||||
// 4.6.51 LeaveChallengeScRsp (cmd 1771, tag 10 retcode 0)
|
||||
if err := s.SendRaw(&NetPacket{CmdType: 1771, Body: []byte{0x50, 0x00}}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.SendRaw(&NetPacket{CmdType: 1716, Body: []byte{0x20, 0x00}})
|
||||
_ = s.SendRaw(&NetPacket{CmdType: 1781, Body: []byte{0x40, 0x00}})
|
||||
|
||||
// Restore the player back to the open world
|
||||
_, _ = loadScene(s, returnEntry, true, nil, nil)
|
||||
_ = refreshLineup(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleLeaveChallengeTierce(s *Session, _ []byte) error {
|
||||
var returnEntry uint32 = 102020107
|
||||
if player := s.JSON(); player != nil {
|
||||
player.BattleConfig.CustomBattleLineup = nil
|
||||
// Always return safely to Belobog Admin District 102020107
|
||||
player.Scene.EntryID = 102020107
|
||||
player.Scene.PlaneID = 10202
|
||||
player.Scene.FloorID = 10202001
|
||||
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
|
||||
player.SavePersistent()
|
||||
}
|
||||
if err := s.SendRaw(&NetPacket{CmdType: 8997, Body: []byte{0x38, 0x00}}); err != nil {
|
||||
// 4.6.51 LeaveChallengeTierceScRsp (cmd 8983, tag 13 retcode 0)
|
||||
if err := s.SendRaw(&NetPacket{CmdType: 8983, Body: []byte{0x68, 0x00}}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.SendRaw(&NetPacket{CmdType: 8997, Body: []byte{0x38, 0x00}})
|
||||
_ = s.SendRaw(&NetPacket{CmdType: 8982, Body: []byte{0x68, 0x00}})
|
||||
|
||||
// Restore the player back to the open world
|
||||
_, _ = loadScene(s, returnEntry, true, nil, nil)
|
||||
_ = refreshLineup(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1017,62 +1057,26 @@ func buildGetChallengeTierceDataScRspReal() []byte {
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
rspMsg := &goproto.GetChallengeTierceDataScRsp{
|
||||
ChallengeTierceGroups: groups,
|
||||
Retcode: 0,
|
||||
}
|
||||
body, err := proto.Marshal(rspMsg)
|
||||
if err == nil {
|
||||
return body
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -67,6 +67,29 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u
|
||||
}
|
||||
|
||||
// 2. position the player safely between boss and arena center
|
||||
// 2. detect arena floor platform height from props in scene
|
||||
// In some challenge arenas (e.g. entrance 3012602 / AS Starward 30225),
|
||||
// the boss monster pos in config is placed at Y = -193081 (22m below the floor),
|
||||
// while the arena stage prop (102259) is at Y = -170770.
|
||||
// Spawning at monsPos.Y puts the player under the map into the void.
|
||||
hasFloorProp := false
|
||||
var floorY int32 = -2147483648
|
||||
for _, g := range scene.Scenes {
|
||||
for _, p := range g.Props {
|
||||
dx := float64(p.Pos.X)
|
||||
dz := float64(p.Pos.Z)
|
||||
if math.Sqrt(dx*dx+dz*dz) < 150000.0 { // within 150 meters of center
|
||||
if p.Pos.Y > floorY {
|
||||
floorY = p.Pos.Y
|
||||
hasFloorProp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasFloorProp && floorY > monsPos.Y {
|
||||
monsPos.Y = floorY
|
||||
}
|
||||
|
||||
centerVX := float64(-monsPos.X)
|
||||
centerVZ := float64(-monsPos.Z)
|
||||
distToCenter := math.Sqrt(centerVX*centerVX + centerVZ*centerVZ)
|
||||
@@ -99,10 +122,7 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u
|
||||
Pos: &goproto.Vector{X: spawnX, Y: spawnY, Z: spawnZ},
|
||||
Rot: &goproto.Vector{Y: spawnRotY},
|
||||
}
|
||||
|
||||
player.Scene.EntryID = entryID
|
||||
player.Scene.FloorID = floorID
|
||||
player.Scene.PlaneID = planeID
|
||||
// NOTE: keep player.Scene intact so returning to overworld or relogging spawns in overworld
|
||||
|
||||
sceneInfo := &goproto.SceneInfo{
|
||||
FloorId: floorID,
|
||||
@@ -119,13 +139,10 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u
|
||||
}
|
||||
sceneInfo.LightenSectionList = scene.Sections
|
||||
|
||||
// load props for group 1, target group, and common prop groups (7/8)
|
||||
// load all props in the arena scene (do not filter out prop groups like group 2 / group 3)
|
||||
propEntityID := uint32(1000)
|
||||
s.PropEntities = map[uint32]PropEntry{}
|
||||
for _, gid := range sortedU32Keys2(scene.Scenes) {
|
||||
if gid != 1 && gid != targetGroupID && gid != 7 && gid != 8 {
|
||||
continue
|
||||
}
|
||||
group := scene.Scenes[gid]
|
||||
groupInfo := &goproto.SceneEntityGroupInfo{GroupId: gid}
|
||||
for i := range group.Props {
|
||||
|
||||
@@ -50,9 +50,17 @@ func init() {
|
||||
rsp.RelicList = append(rsp.RelicList, player.Relics[i].ToProtoRelic())
|
||||
}
|
||||
rsp.MaterialList = []*goproto.Material{
|
||||
{Tid: 1, Num: 9999999}, // Stellar Jade
|
||||
{Tid: 2, Num: 99999999}, // Credit
|
||||
{Tid: 101, Num: 999999}, // Normal Pass
|
||||
{Tid: 102, Num: 999999}, // Special Pass
|
||||
}
|
||||
for _, aid := range BaseAvatarIDs {
|
||||
rsp.MaterialList = append(rsp.MaterialList, &goproto.Material{
|
||||
Tid: 130000 + aid,
|
||||
Num: 10,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -99,7 +107,30 @@ func init() {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
avatar.Data.Rank = req.Rank
|
||||
|
||||
isExRank := false
|
||||
if req.CostData != nil {
|
||||
for _, item := range req.CostData.ItemList {
|
||||
if pile := item.GetPileItem(); pile != nil {
|
||||
if (pile.ItemId >= 130000 && pile.ItemId < 140000) || pile.ItemId == 131204 || pile.ItemId == 131213 || pile.ItemId == 131308 {
|
||||
isExRank = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Rank > 6 {
|
||||
isExRank = true
|
||||
}
|
||||
|
||||
if isExRank || (avatar.Data.Rank >= 6 && common.AvatarExRankMap[req.AvatarId] != 0) {
|
||||
avatar.ExRankUnlocked = true
|
||||
if req.Rank > 0 && req.Rank <= 6 {
|
||||
avatar.Data.Rank = req.Rank
|
||||
}
|
||||
} else {
|
||||
avatar.Data.Rank = req.Rank
|
||||
}
|
||||
|
||||
ret := &goproto.PlayerSyncScNotify{}
|
||||
buildSync(player, ret, []uint32{avatar.AvatarID}, nil, nil)
|
||||
|
||||
@@ -2,6 +2,9 @@ package gameserver
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
@@ -37,7 +40,23 @@ func init() {
|
||||
|
||||
var finalData []byte
|
||||
finalData = append(finalData, heartbeatLua...)
|
||||
if freecam, err := os.ReadFile("scripts/freecam.lua"); err == nil {
|
||||
|
||||
// Load all .lua files from scripts/ directory in deterministic order
|
||||
if entries, err := os.ReadDir("scripts"); err == nil {
|
||||
var luaFiles []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".lua") {
|
||||
luaFiles = append(luaFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(luaFiles)
|
||||
for _, name := range luaFiles {
|
||||
if content, err := os.ReadFile(filepath.Join("scripts", name)); err == nil {
|
||||
finalData = append(finalData, '\n')
|
||||
finalData = append(finalData, content...)
|
||||
}
|
||||
}
|
||||
} else if freecam, err := os.ReadFile("scripts/freecam.lua"); err == nil {
|
||||
finalData = append(finalData, '\n')
|
||||
finalData = append(finalData, freecam...)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
@@ -57,6 +62,7 @@ func init() {
|
||||
simpleReply([]uint16{2966}, 2955, []byte{0x30, 0x00}) // tag 6 retcode 0
|
||||
simpleReply([]uint16{2916}, 2906, []byte{0x18, 0x00}) // tag 3 retcode 0
|
||||
simpleReply([]uint16{2919}, 2928, []byte{0x48, 0x00}) // tag 9 retcode 0
|
||||
simpleReply([]uint16{2974}, 2919, []byte{0x18, 0x00}) // GetFriendListInfo tag 3 retcode 0
|
||||
simpleReply([]uint16{740}, 733, []byte{0x58, 0x00}) // tag 11 retcode 0
|
||||
simpleReply([]uint16{1748}, 1745, []byte{0x08, 0x00}) // tag 1 retcode 0
|
||||
simpleReply([]uint16{1765}, 1730, []byte{0x08, 0x00}) // tag 1 retcode 0
|
||||
@@ -64,7 +70,7 @@ func init() {
|
||||
simpleReply([]uint16{1738}, 1748, nil) // empty body
|
||||
simpleReply([]uint16{188}, 181, []byte{0x50, 0x00}) // tag 10 retcode 0
|
||||
simpleReply([]uint16{4130}, 4124, []byte{0x70, 0x00}) // tag 14 retcode 0
|
||||
simpleReply([]uint16{8110, 8115}, 8104, []byte{0x18, 0x00}) // GetSwitchHandData tag 3 retcode 0
|
||||
simpleReply([]uint16{8110, 8115}, 8108, []byte{0x18, 0x00}) // GetSwitchHandData tag 3 retcode 0 (4.6.51 CmdID: 8108)
|
||||
simpleReply([]uint16{8993, 8991}, 8981, []byte{0x70, 0x00}) // 4.6.51: tag 14 retcode 0
|
||||
simpleReply([]uint16{8987}, 8986, []byte{0x38, 0x00}) // ConfirmChallengeTierceStageSettle tag 7 retcode 0
|
||||
|
||||
@@ -81,21 +87,24 @@ func init() {
|
||||
rawHandlers[1731] = handleStartChallenge
|
||||
rawHandlers[1793] = handleStartChallenge
|
||||
rawHandlers[8986] = handleStartChallengeTierce
|
||||
rawHandlers[8983] = handleStartChallengeTierce
|
||||
rawHandlers[8988] = handleStartChallengeTierce
|
||||
rawHandlers[8978] = handleSetChallengeTierceLineup
|
||||
rawHandlers[9000] = handleSetChallengeTierceLineup
|
||||
rawHandlers[1705] = handleLeaveChallenge
|
||||
rawHandlers[1760] = handleLeaveChallenge
|
||||
rawHandlers[1788] = handleLeaveChallenge
|
||||
rawHandlers[8989] = handleLeaveChallengeTierce
|
||||
rawHandlers[8998] = handleLeaveChallengeTierce
|
||||
rawHandlers[1739] = handleTakeChallengeReward
|
||||
|
||||
// ---- 400 -> Ex-Rank / Affinitive Eidolon Unlock ----
|
||||
rawHandlers[400] = handleExRankUnlock
|
||||
}
|
||||
|
||||
// challengeGroupStatsReply ports the 1775/1711 arm: pull group_id (field 12
|
||||
// varint) from the request and answer cmd 1708 with per-mode statistics.
|
||||
func challengeGroupStatsReply(s *Session, payload []byte) error {
|
||||
groupID := uint32(100)
|
||||
// handleExRankUnlock handles CmdID 400 (Affinitive Eidolon / Ex-Rank unlock request from client).
|
||||
func handleExRankUnlock(s *Session, payload []byte) error {
|
||||
Log(fmt.Sprintf("[EX-RANK] Received cmd 400 (len: %d) payload: %x", len(payload), payload))
|
||||
|
||||
var parsedVal uint32
|
||||
buf := payload
|
||||
for len(buf) > 0 {
|
||||
tag, n := readUvarint(buf)
|
||||
@@ -103,91 +112,131 @@ func challengeGroupStatsReply(s *Session, payload []byte) error {
|
||||
break
|
||||
}
|
||||
buf = buf[n:]
|
||||
fieldNumber := tag >> 3
|
||||
wireType := tag & 0x7
|
||||
if fieldNumber == 12 && wireType == 0 {
|
||||
fieldNum := tag >> 3
|
||||
if wireType == 0 {
|
||||
v, n2 := readUvarint(buf)
|
||||
if n2 == 0 {
|
||||
break
|
||||
if n2 > 0 {
|
||||
buf = buf[n2:]
|
||||
Log(fmt.Sprintf("[EX-RANK] field %d = %d", fieldNum, v))
|
||||
if v >= 100000 || (v >= 1000 && v < 10000 && parsedVal == 0) {
|
||||
parsedVal = uint32(v)
|
||||
}
|
||||
continue
|
||||
}
|
||||
buf = buf[n2:]
|
||||
groupID = uint32(v)
|
||||
break
|
||||
} else if wireType == 0 {
|
||||
_, n2 := readUvarint(buf)
|
||||
if n2 == 0 {
|
||||
break
|
||||
}
|
||||
skipWireField(uint32(wireType), &buf)
|
||||
}
|
||||
|
||||
var avatarID uint32
|
||||
if parsedVal >= 100000 {
|
||||
avatarID = parsedVal / 100 // e.g. 120401 -> 1204, 121301 -> 1213, 130801 -> 1308
|
||||
} else if parsedVal >= 1000 && parsedVal < 10000 {
|
||||
avatarID = parsedVal
|
||||
}
|
||||
|
||||
Log(fmt.Sprintf("[EX-RANK] Target avatarID: %d (from val: %d)", avatarID, parsedVal))
|
||||
|
||||
player := s.JSON()
|
||||
if player != nil {
|
||||
if avatarID == 0 {
|
||||
// Fallback: unlock for all avatars that have ExRank defined (1204, 1213, 1308)
|
||||
for aid := range common.AvatarExRankMap {
|
||||
if av, ok := player.Avatars[aid]; ok {
|
||||
av.ExRankUnlocked = true
|
||||
Log(fmt.Sprintf("[EX-RANK] Fallback set ExRankUnlocked=true for avatar %d", aid))
|
||||
syncNotify := &goproto.PlayerSyncScNotify{}
|
||||
buildSync(player, syncNotify, []uint32{aid}, nil, nil)
|
||||
_ = s.Send(syncNotify)
|
||||
}
|
||||
}
|
||||
buf = buf[n2:]
|
||||
} else {
|
||||
break
|
||||
if av, ok := player.Avatars[avatarID]; ok {
|
||||
av.ExRankUnlocked = true
|
||||
Log(fmt.Sprintf("[EX-RANK] Set ExRankUnlocked=true for avatar %d", avatarID))
|
||||
}
|
||||
syncNotify := &goproto.PlayerSyncScNotify{}
|
||||
buildSync(player, syncNotify, []uint32{avatarID}, nil, nil)
|
||||
_ = s.Send(syncNotify)
|
||||
}
|
||||
player.SavePersistent()
|
||||
}
|
||||
|
||||
// Send candidate response packets so client RPC callback resolves without spinning
|
||||
// Candidates in CmdAvatarType: 395 (CmdAvatarType_ENKEIJOLCNO), 354, 355, 364, 341
|
||||
for _, rspID := range []uint16{395, 354, 355, 364, 341} {
|
||||
_ = s.SendRaw(&NetPacket{CmdType: rspID, Body: []byte{}})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// challengeGroupStatsReply handles CmdGetChallengeGroupStatisticsCsReq (1775/1711)
|
||||
// and returns GetChallengeGroupStatisticsScRsp (1708) using standard 4.6.51 proto.
|
||||
func challengeGroupStatsReply(s *Session, payload []byte) error {
|
||||
groupID := uint32(100)
|
||||
var req goproto.GetChallengeGroupStatisticsCsReq
|
||||
if err := proto.Unmarshal(payload, &req); err == nil && req.GroupId != 0 {
|
||||
groupID = req.GroupId
|
||||
} else {
|
||||
buf := payload
|
||||
for len(buf) > 0 {
|
||||
tag, n := readUvarint(buf)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
buf = buf[n:]
|
||||
fieldNumber := tag >> 3
|
||||
wireType := tag & 0x7
|
||||
if (fieldNumber == 5 || fieldNumber == 12) && wireType == 0 {
|
||||
v, n2 := readUvarint(buf)
|
||||
if n2 > 0 {
|
||||
groupID = uint32(v)
|
||||
break
|
||||
}
|
||||
}
|
||||
skipWireField(uint32(wireType), &buf)
|
||||
}
|
||||
}
|
||||
|
||||
var body []byte
|
||||
// tag 9: group_id
|
||||
body = append(body, 0x48)
|
||||
body = appendUvarint(body, uint64(groupID))
|
||||
// tag 15: retcode 0
|
||||
body = append(body, 0x78, 0x00)
|
||||
|
||||
switch {
|
||||
case groupID >= 2000 && groupID < 3000:
|
||||
// Pure Fiction: tag 8 = challenge_story (ChallengeStoryStatistics)
|
||||
var eik []byte
|
||||
eik = append(eik, 0x50) // tag 10: score_id 80000
|
||||
eik = appendUvarint(eik, 80000)
|
||||
eik = append(eik, 0x58) // tag 11: level 4
|
||||
eik = appendUvarint(eik, 4)
|
||||
|
||||
var css []byte
|
||||
css = append(css, 0x32) // tag 6
|
||||
css = appendUvarint(css, uint64(len(eik)))
|
||||
css = append(css, eik...)
|
||||
css = append(css, 0x38) // tag 7: record_id 1
|
||||
css = appendUvarint(css, 1)
|
||||
|
||||
body = append(body, 0x42) // tag 8
|
||||
body = appendUvarint(body, uint64(len(css)))
|
||||
body = append(body, css...)
|
||||
case groupID >= 1000 && groupID < 2000:
|
||||
// Memory of Chaos: tag 2 = challenge_default (ChallengeStatistics)
|
||||
var adk []byte
|
||||
adk = append(adk, 0x10) // tag 2: round_count 20
|
||||
adk = appendUvarint(adk, 20)
|
||||
adk = append(adk, 0x18) // tag 3: level 12
|
||||
adk = appendUvarint(adk, 12)
|
||||
|
||||
var cs []byte
|
||||
cs = append(cs, 0x32) // tag 6
|
||||
cs = appendUvarint(cs, uint64(len(adk)))
|
||||
cs = append(cs, adk...)
|
||||
cs = append(cs, 0x50) // tag 10: record_id 1
|
||||
cs = appendUvarint(cs, 1)
|
||||
|
||||
body = append(body, 0x12) // tag 2
|
||||
body = appendUvarint(body, uint64(len(cs)))
|
||||
body = append(body, cs...)
|
||||
case groupID >= 3000:
|
||||
// Apocalyptic Shadow: tag 11 = challenge_boss (ChallengeBossStatistics)
|
||||
var aan []byte
|
||||
aan = append(aan, 0x50) // tag 10: score_id 8000
|
||||
aan = appendUvarint(aan, 8000)
|
||||
aan = append(aan, 0x18) // tag 3: level 4
|
||||
aan = appendUvarint(aan, 4)
|
||||
|
||||
var cbs []byte
|
||||
cbs = append(cbs, 0x0A) // tag 1
|
||||
cbs = appendUvarint(cbs, uint64(len(aan)))
|
||||
cbs = append(cbs, aan...)
|
||||
cbs = append(cbs, 0x70) // tag 14: record_id 1
|
||||
cbs = appendUvarint(cbs, 1)
|
||||
|
||||
body = append(body, 0x5A) // tag 11
|
||||
body = appendUvarint(body, uint64(len(cbs)))
|
||||
body = append(body, cbs...)
|
||||
rsp := &goproto.GetChallengeGroupStatisticsScRsp{
|
||||
GroupId: groupID,
|
||||
Retcode: 0,
|
||||
}
|
||||
|
||||
switch {
|
||||
case groupID >= 3000:
|
||||
// Apocalyptic Shadow: challenge_boss (field 1)
|
||||
rsp.ChallengeBoss = &goproto.ChallengeBossStatistics{
|
||||
RecordId: 1,
|
||||
PPBHLLOJNEK: &goproto.AANLJBLOOFO{
|
||||
Level: 4,
|
||||
ScoreId: 8000,
|
||||
},
|
||||
}
|
||||
case groupID >= 2000 && groupID < 3000:
|
||||
// Pure Fiction: challenge_story (field 3)
|
||||
rsp.ChallengeStory = &goproto.ChallengeStoryStatistics{
|
||||
RecordId: 1,
|
||||
PPBHLLOJNEK: &goproto.EIKPHEMHIOH{
|
||||
ScoreId: 80000,
|
||||
},
|
||||
}
|
||||
case groupID >= 1000 && groupID < 2000:
|
||||
// Memory of Chaos: challenge_default (field 11)
|
||||
rsp.ChallengeDefault = &goproto.ChallengeStatistics{
|
||||
RecordId: 1,
|
||||
PPBHLLOJNEK: &goproto.ADKJKMKBFDC{
|
||||
RoundCount: 20,
|
||||
Level: 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
body, err := proto.Marshal(rsp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.SendRaw(&NetPacket{CmdType: 1708, Body: body})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,16 +20,36 @@ import (
|
||||
// Rust iterated a HashMap arbitrarily; we pick the lexicographically first
|
||||
// name for determinism.
|
||||
func firstSceneOfEntry(res *common.JsonConfig, entryID uint32) (string, *common.LevelOutputConfig, bool) {
|
||||
m, ok := res.LevelOutputConfigs[entryID]
|
||||
if !ok || len(m) == 0 {
|
||||
return "", nil, false
|
||||
if m, ok := res.LevelOutputConfigs[entryID]; ok && len(m) > 0 {
|
||||
names := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names[0], m[names[0]], true
|
||||
}
|
||||
names := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
names = append(names, k)
|
||||
if entryID != 0 {
|
||||
name := fmt.Sprintf("P%d_F%d", entryID/100, entryID*10+1)
|
||||
cfg := &common.LevelOutputConfig{
|
||||
IsEnteredSceneInfo: true,
|
||||
Scenes: map[uint32]common.SceneData{
|
||||
1: {
|
||||
Teleports: map[uint32]common.TeleportInfo{
|
||||
entryID: {Pos: common.Vector{X: 0, Y: 0, Z: 0}},
|
||||
1: {Pos: common.Vector{X: 0, Y: 0, Z: 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if res.LevelOutputConfigs == nil {
|
||||
res.LevelOutputConfigs = map[uint32]map[string]*common.LevelOutputConfig{}
|
||||
}
|
||||
res.LevelOutputConfigs[entryID] = map[string]*common.LevelOutputConfig{
|
||||
name: cfg,
|
||||
}
|
||||
return name, cfg, true
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names[0], m[names[0]], true
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// parsePlaneFloor splits "P<plane>_F<floor>".
|
||||
@@ -65,19 +85,24 @@ func init() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// recover from a stale challenge arena back to the Parlor Car
|
||||
// recover from a stale challenge arena, broken map, or void coordinate back to Belobog Admin District
|
||||
entryID := player.Scene.EntryID
|
||||
if entryID >= 3000000 && entryID < 4000000 {
|
||||
entryID = 100000104
|
||||
if (entryID >= 3000000 && entryID < 4000000) || entryID == 100000104 || entryID == 100000304 || entryID == 1030402 || entryID == 0 || player.Position.Y < -50000 {
|
||||
entryID = 102020107
|
||||
player.Scene.EntryID = 102020107
|
||||
player.Scene.PlaneID = 10202
|
||||
player.Scene.FloorID = 10202001
|
||||
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
|
||||
player.SavePersistent()
|
||||
}
|
||||
|
||||
defaultScene := &goproto.SceneInfo{
|
||||
GameModeType: 1,
|
||||
EntryId: entryID,
|
||||
}
|
||||
if entryID == 100000104 {
|
||||
defaultScene.PlaneId = 10000
|
||||
defaultScene.FloorId = 10000000
|
||||
if entryID == 102020107 {
|
||||
defaultScene.PlaneId = 10202
|
||||
defaultScene.FloorId = 10202001
|
||||
} else {
|
||||
defaultScene.PlaneId = player.Scene.PlaneID
|
||||
defaultScene.FloorId = player.Scene.FloorID
|
||||
@@ -103,10 +128,10 @@ func init() {
|
||||
}
|
||||
player := s.JSON()
|
||||
currentEntry := func() uint32 {
|
||||
if player != nil {
|
||||
if player != nil && player.Scene.EntryID != 0 && player.Scene.EntryID != 100000104 && player.Scene.EntryID != 100000304 {
|
||||
return player.Scene.EntryID
|
||||
}
|
||||
return 100000104
|
||||
return 102020107
|
||||
}
|
||||
|
||||
interactID := uint32(req.InteractId)
|
||||
@@ -115,17 +140,17 @@ func init() {
|
||||
sceneIdent = proto.Clone(req.SceneIdentifier).(*goproto.SceneIdentifier)
|
||||
}
|
||||
|
||||
// resolve target entry (mirrors the Rust resolution chain)
|
||||
// resolve target entry (mirrors the Rust resolution chain, with cascading fallbacks)
|
||||
resolvedEntry := uint32(0)
|
||||
switch {
|
||||
case req.EntryId != 0 && mapHasEntry(res, req.EntryId):
|
||||
if req.EntryId != 0 && mapHasEntry(res, req.EntryId) {
|
||||
resolvedEntry = req.EntryId
|
||||
case sceneIdent != nil && sceneIdent.FloorId != 0:
|
||||
} else if sceneIdent != nil && sceneIdent.FloorId != 0 {
|
||||
resolvedEntry = findEntryForFloor(res, sceneIdent.FloorId)
|
||||
case req.EntryId != 0 && findEntryForFloor(res, req.EntryId) != 0:
|
||||
// req.EntryId doubles as a floor id in some map flows
|
||||
}
|
||||
if resolvedEntry == 0 && req.EntryId != 0 {
|
||||
resolvedEntry = findEntryForFloor(res, req.EntryId)
|
||||
case req.TeleportId != 0:
|
||||
}
|
||||
if resolvedEntry == 0 && req.TeleportId != 0 {
|
||||
floorID := uint32(0)
|
||||
if sceneIdent != nil {
|
||||
floorID = sceneIdent.FloorId
|
||||
@@ -133,21 +158,26 @@ func init() {
|
||||
if entry := findEntryByTeleportInFloor(res, req.TeleportId, floorID); entry != 0 {
|
||||
resolvedEntry = entry
|
||||
} else {
|
||||
resolvedEntry = findEntryByTeleport(res, req.TeleportId, currentEntry())
|
||||
resolvedEntry = findEntryByTeleport(res, req.TeleportId, 0)
|
||||
}
|
||||
case sceneIdent != nil && sceneIdent.TeleportNigger != nil:
|
||||
}
|
||||
if resolvedEntry == 0 && sceneIdent != nil && sceneIdent.TeleportNigger != nil {
|
||||
if t, okc := sceneIdent.TeleportNigger.(*goproto.SceneIdentifier_MDAIDPPKOPO); okc && t.MDAIDPPKOPO != nil && t.MDAIDPPKOPO.PLIDNBMCIJH != 0 {
|
||||
// map teleport click: teleport id + floor arrive via scene_identifier
|
||||
tpID := t.MDAIDPPKOPO.PLIDNBMCIJH
|
||||
if entry := findEntryByTeleportInFloor(res, tpID, sceneIdent.FloorId); entry != 0 {
|
||||
resolvedEntry = entry
|
||||
} else {
|
||||
resolvedEntry = findEntryByTeleport(res, tpID, currentEntry())
|
||||
resolvedEntry = findEntryByTeleport(res, tpID, 0)
|
||||
}
|
||||
}
|
||||
case interactID != 0:
|
||||
resolvedEntry = findEntryByInteract(res, interactID, currentEntry())
|
||||
default:
|
||||
}
|
||||
if resolvedEntry == 0 && interactID != 0 {
|
||||
resolvedEntry = findEntryByInteract(res, interactID, 0)
|
||||
}
|
||||
if resolvedEntry == 0 && req.EntryId != 0 {
|
||||
resolvedEntry = req.EntryId
|
||||
}
|
||||
if resolvedEntry == 0 {
|
||||
resolvedEntry = currentEntry()
|
||||
}
|
||||
|
||||
@@ -215,6 +245,10 @@ func init() {
|
||||
// a wrong state (like 1100) breaks interactions and UI entry points
|
||||
state := uint32(1)
|
||||
if p, ok := s.PropEntities[req.PropEntityId]; ok {
|
||||
if p.State == 0 {
|
||||
p.State = 1
|
||||
s.PropEntities[req.PropEntityId] = p
|
||||
}
|
||||
state = p.State
|
||||
}
|
||||
|
||||
@@ -354,7 +388,6 @@ func init() {
|
||||
}
|
||||
}
|
||||
}
|
||||
player.SavePersistent()
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -396,31 +429,106 @@ func init() {
|
||||
// cmd 1432 -> 1421: unlock every teleport in the game
|
||||
handle("GetUnlockTeleport", func(s *Session, req *goproto.GetUnlockTeleportCsReq, rsp *goproto.GetUnlockTeleportScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
unlocks := getMissionUnlocks()
|
||||
seen := map[uint32]bool{}
|
||||
var tps []uint32
|
||||
|
||||
// 1. All IDs from mission_unlocks.json
|
||||
unlocks := getMissionUnlocks()
|
||||
for _, id := range unlocks.TeleportIDs {
|
||||
if id != 0 && !seen[id] {
|
||||
seen[id] = true
|
||||
tps = append(tps, id)
|
||||
}
|
||||
}
|
||||
if len(tps) == 0 {
|
||||
for _, m := range common.Teleports() {
|
||||
for id := range m {
|
||||
if id != 0 && !seen[id] {
|
||||
seen[id] = true
|
||||
tps = append(tps, id)
|
||||
|
||||
// 2. All IDs from teleports DB across all floors
|
||||
for _, m := range common.Teleports() {
|
||||
for id := range m {
|
||||
if id != 0 && !seen[id] {
|
||||
seen[id] = true
|
||||
tps = append(tps, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. All teleport IDs from LevelOutputConfigs
|
||||
if res, err := common.GameRes(); err == nil {
|
||||
for _, m := range res.LevelOutputConfigs {
|
||||
for _, cfg := range m {
|
||||
for _, sc := range cfg.Scenes {
|
||||
for tid := range sc.Teleports {
|
||||
if tid != 0 && !seen[tid] {
|
||||
seen[tid] = true
|
||||
tps = append(tps, tid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Any entry IDs requested by client or in EntranceIDs
|
||||
for _, eid := range req.EntryIdList {
|
||||
if eid != 0 && !seen[eid] {
|
||||
seen[eid] = true
|
||||
tps = append(tps, eid)
|
||||
}
|
||||
}
|
||||
for _, id := range unlocks.EntranceIDs {
|
||||
if id != 0 && !seen[id] {
|
||||
seen[id] = true
|
||||
tps = append(tps, id)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(tps, func(i, j int) bool { return tps[i] < tps[j] })
|
||||
rsp.UnlockedTeleportList = tps
|
||||
|
||||
SceneDebugLog("GET_UNLOCK_TELEPORT req_entries=%d teleports_sent=%d", len(req.EntryIdList), len(rsp.UnlockedTeleportList))
|
||||
return nil
|
||||
})
|
||||
|
||||
// cmd 1438 -> 1409: handle entity teleport within scene
|
||||
handle("SceneEntityTeleport", func(s *Session, req *goproto.SceneEntityTeleportCsReq, rsp *goproto.SceneEntityTeleportScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
rsp.EntityMotion = req.EntityMotion
|
||||
if req.EntityMotion != nil && req.EntityMotion.Motion != nil {
|
||||
motion := req.EntityMotion.Motion
|
||||
player := s.JSON()
|
||||
if player != nil {
|
||||
if motion.Pos != nil {
|
||||
player.Position.X = motion.Pos.X
|
||||
player.Position.Y = motion.Pos.Y
|
||||
player.Position.Z = motion.Pos.Z
|
||||
}
|
||||
if motion.Rot != nil {
|
||||
player.Position.RotY = motion.Rot.Y
|
||||
}
|
||||
player.SavePersistent()
|
||||
}
|
||||
}
|
||||
SceneDebugLog("SCENE_ENTITY_TELEPORT entry_id=%d", req.EntryId)
|
||||
return nil
|
||||
})
|
||||
|
||||
// cmd 1267 -> 1263: teleport to mission reset point
|
||||
handle("TeleportToMissionResetPoint", func(s *Session, req *goproto.TeleportToMissionResetPointCsReq, rsp *goproto.TeleportToMissionResetPointScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
player := s.JSON()
|
||||
if player != nil {
|
||||
rsp.Motion = &goproto.MotionInfo{
|
||||
Pos: &goproto.Vector{
|
||||
X: player.Position.X,
|
||||
Y: player.Position.Y,
|
||||
Z: player.Position.Z,
|
||||
},
|
||||
Rot: &goproto.Vector{
|
||||
Y: player.Position.RotY,
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// findEntryForFloor resolves an entry id for a floor WITHOUT relying on
|
||||
|
||||
Reference in new issue
Block a user