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:
CAtlas0784 committed 2026-10-16 20:08:13 +07:00
1 parent 9aab62414f
commit 163d4433e6
47 files changed
+2025067 -6082

No files matched your search

+4 -11
View File
@@ -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
+9 -8
View File
@@ -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")
}
}
}
}
}
+6
View File
@@ -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
+87 -41
View File
@@ -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()
+293 -289
View File
@@ -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 {
+32 -1
View File
@@ -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)
+20 -1
View File
@@ -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...)
}
+131 -82
View File
@@ -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})
}
+146 -38
View File
@@ -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