fix: resolve Calyx NullReferenceException by omitting WaveParam, capping wave count, and setting default monster HP
This commit is contained in:
1 parent
57714379de
commit
5b33715c1b
2 files changed
+162
-29
No files matched your search
@@ -105,12 +105,56 @@ func (a *AvatarJson) ToAvatarProto(lightcone *Lightcone, mcID, marchID uint32) *
|
||||
}
|
||||
}
|
||||
|
||||
// EnhanceAvatarID contains avatars that get enhanced mode enabled by default (March 7th Hunt, SP forms, etc.)
|
||||
var EnhanceAvatarID = map[uint32]bool{
|
||||
1004: true, 1005: true, 1006: true, 1102: true, 1205: true,
|
||||
1212: true, 1217: true, 1306: true, 1307: true, 1310: true,
|
||||
}
|
||||
|
||||
// AvatarExRankMap maps AvatarID to its corresponding Ex-Eidolon (Affinitive Eidolon) ID
|
||||
var AvatarExRankMap = map[uint32]uint32{
|
||||
1204: 120401, // Jing Yuan
|
||||
1213: 121301, // Dan Heng • IL
|
||||
1308: 130801, // Acheron
|
||||
}
|
||||
|
||||
// ExRankAvatarID matches pearl-sr 4.6.53 (Jing Yuan 120401, Dan Heng IL 121301, Acheron 130801)
|
||||
var ExRankAvatarID = []uint32{120401, 121301, 130801}
|
||||
|
||||
// DefaultAvatarSkinMap defines default skins if not explicitly set
|
||||
var DefaultAvatarSkinMap = map[uint32]uint32{
|
||||
1303: 1130301,
|
||||
1001: 1100101,
|
||||
1310: 1131001,
|
||||
1415: 1141501,
|
||||
1407: 1140701,
|
||||
1501: 1150101,
|
||||
1505: 1150501,
|
||||
1409: 1140901,
|
||||
}
|
||||
|
||||
func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic, dressedSkinID uint32) *goproto.AvatarPathData {
|
||||
enhancedID := derefU32(a.EnhancedID)
|
||||
if enhancedID == 0 && EnhanceAvatarID[a.AvatarID] {
|
||||
enhancedID = 1
|
||||
}
|
||||
if dressedSkinID == 0 {
|
||||
dressedSkinID = DefaultAvatarSkinMap[a.AvatarID]
|
||||
}
|
||||
|
||||
var exRanks []uint32
|
||||
if a.ExRankUnlocked {
|
||||
if exID, ok := AvatarExRankMap[a.AvatarID]; ok {
|
||||
exRanks = []uint32{exID}
|
||||
}
|
||||
}
|
||||
|
||||
out := &goproto.AvatarPathData{
|
||||
AvatarId: a.AvatarID,
|
||||
Rank: a.Data.Rank,
|
||||
EnhancedId: derefU32(a.EnhancedID),
|
||||
EnhancedId: enhancedID,
|
||||
DressedSkinId: dressedSkinID,
|
||||
IGDEHGIKFJA: exRanks,
|
||||
}
|
||||
|
||||
for _, r := range relics {
|
||||
@@ -137,16 +181,29 @@ func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic
|
||||
}
|
||||
|
||||
func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, relics []*Relic) (*goproto.BattleAvatar, []*goproto.BattleBuff) {
|
||||
enhancedID := derefU32(a.EnhancedID)
|
||||
if enhancedID == 0 && EnhanceAvatarID[a.AvatarID] {
|
||||
enhancedID = 1
|
||||
}
|
||||
|
||||
var exRanks []uint32
|
||||
if a.ExRankUnlocked {
|
||||
if exID, ok := AvatarExRankMap[a.AvatarID]; ok {
|
||||
exRanks = []uint32{exID}
|
||||
}
|
||||
}
|
||||
|
||||
ba := &goproto.BattleAvatar{
|
||||
Index: index,
|
||||
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
||||
Id: a.AvatarID,
|
||||
Level: a.Level,
|
||||
Rank: a.Data.Rank,
|
||||
Hp: 10000,
|
||||
Promotion: a.Promotion,
|
||||
WorldLevel: 6,
|
||||
EnhancedId: derefU32(a.EnhancedID),
|
||||
Index: index,
|
||||
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
||||
Id: a.AvatarID,
|
||||
Level: a.Level,
|
||||
Rank: a.Data.Rank,
|
||||
Hp: 10000,
|
||||
Promotion: a.Promotion,
|
||||
WorldLevel: 6,
|
||||
EnhancedId: enhancedID,
|
||||
IGDEHGIKFJA: exRanks,
|
||||
SpBar: &goproto.SpBarInfo{
|
||||
CurSp: a.SPCur(),
|
||||
MaxSp: a.SPMaxVal(),
|
||||
@@ -252,10 +309,14 @@ func ToLineupInfo(lineups map[uint32]uint32) *goproto.LineupInfo {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (m *Monster) ToSceneMonster() *goproto.SceneMonster {
|
||||
hp := m.MaxHP
|
||||
if hp == 0 {
|
||||
hp = 10000
|
||||
}
|
||||
return &goproto.SceneMonster{
|
||||
MonsterId: m.MonsterID,
|
||||
MaxHp: m.MaxHP,
|
||||
CurHp: m.MaxHP,
|
||||
MaxHp: hp,
|
||||
CurHp: hp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,27 +324,15 @@ func monsterWave(waveID uint32, monsters []Monster) *goproto.SceneMonsterWave {
|
||||
wave := &goproto.SceneMonsterWave{
|
||||
WaveId: waveID,
|
||||
MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)),
|
||||
WaveParam: &goproto.SceneMonsterWaveParam{},
|
||||
// NOTE: WaveParam MUST be nil! In client 4.6.53 GameAssembly.dll
|
||||
// AdventureModule.CreateBattleServerCheckResult (RVA 0x166b2ef6),
|
||||
// if WaveParam is non-nil it tries to dereference [wave_param + 0x10]
|
||||
// and throws NullReferenceException. When nil, it safely jumps over.
|
||||
WaveParam: nil,
|
||||
}
|
||||
maxLevel := uint32(95)
|
||||
for i := range monsters {
|
||||
if monsters[i].Level > maxLevel || wave.WaveParam.Level == 0 {
|
||||
if monsters[i].Level > maxLevel {
|
||||
maxLevel = monsters[i].Level
|
||||
}
|
||||
}
|
||||
wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster())
|
||||
}
|
||||
// Rust: monsters.iter().map(level).max().unwrap_or(95)
|
||||
if len(monsters) > 0 {
|
||||
maxLevel = 0
|
||||
for i := range monsters {
|
||||
if monsters[i].Level > maxLevel {
|
||||
maxLevel = monsters[i].Level
|
||||
}
|
||||
}
|
||||
}
|
||||
wave.WaveParam.Level = maxLevel
|
||||
return wave
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
@@ -8,6 +12,20 @@ import (
|
||||
func init() {
|
||||
handle("StartCocoonStage", func(s *Session, req *goproto.StartCocoonStageCsReq, rsp *goproto.StartCocoonStageScRsp) error {
|
||||
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
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
rsp.PropEntityId = req.PropEntityId
|
||||
@@ -21,6 +39,20 @@ func init() {
|
||||
// (the Rust code referenced an older QuickStartCocoonStage* name).
|
||||
handleNamed("StartQuickCocoonStage", "StartQuickCocoonStageRsp", func(s *Session, req *goproto.StartQuickCocoonStageCsReq, rsp *goproto.StartQuickCocoonStageRsp) error {
|
||||
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
|
||||
}
|
||||
CURRENT_BATTLE.Store(s.Token, battleInfo)
|
||||
|
||||
rsp.CocoonId = req.CocoonId
|
||||
@@ -378,6 +410,27 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
})
|
||||
}
|
||||
|
||||
// Stage invasion buff (pearl-sr 4.6.53: e.g. Peak 11 stage 4 30511021/30511022 -> buff 3034004)
|
||||
if invasionID := getStageInvasionID(battleInfo.StageId); invasionID != 0 {
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 3034000 + invasionID,
|
||||
Level: 1,
|
||||
OwnerId: firstAvatarIdx,
|
||||
WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{0},
|
||||
DynamicValues: map[string]float32{"SkillIndex": 1},
|
||||
})
|
||||
// Aha Invasion support buff
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 3034013,
|
||||
Level: 1,
|
||||
OwnerId: firstAvatarIdx,
|
||||
WaveFlag: 0xffffffff,
|
||||
TargetIndexList: []uint32{0},
|
||||
DynamicValues: map[string]float32{"SkillIndex": 1},
|
||||
})
|
||||
}
|
||||
|
||||
// Aha Global Passive (151104) — always on, same as the Rust build
|
||||
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
|
||||
Id: 151104, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
|
||||
@@ -386,6 +439,37 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
|
||||
return battleInfo
|
||||
}
|
||||
|
||||
type stageInvasionConfigData struct {
|
||||
StageInvasionConfig []struct {
|
||||
StageID uint32 `json:"StageID"`
|
||||
InvasionID uint32 `json:"InvasionID"`
|
||||
} `json:"stage_invasion_config"`
|
||||
}
|
||||
|
||||
var (
|
||||
stageInvasionOnce sync.Once
|
||||
stageInvasionMap = map[uint32]uint32{}
|
||||
)
|
||||
|
||||
func getStageInvasionID(stageID uint32) uint32 {
|
||||
stageInvasionOnce.Do(func() {
|
||||
for _, path := range []string{"resources/StageInvasionConfig.json", "StageInvasionConfig.json"} {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var cfg stageInvasionConfigData
|
||||
if err := json.Unmarshal(raw, &cfg); err == nil {
|
||||
for _, entry := range cfg.StageInvasionConfig {
|
||||
stageInvasionMap[entry.StageID] = entry.InvasionID
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
return stageInvasionMap[stageID]
|
||||
}
|
||||
|
||||
func sortedLineup(m map[uint32]uint32) []uint32 {
|
||||
keys := make([]uint32, 0, len(m))
|
||||
for k := range m {
|
||||
|
||||
Reference in new issue
Block a user