diff --git a/internal/common/proto_convert.go b/internal/common/proto_convert.go index db07a67..88c8c11 100644 --- a/internal/common/proto_convert.go +++ b/internal/common/proto_convert.go @@ -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 } diff --git a/internal/gameserver/handlers_battle.go b/internal/gameserver/handlers_battle.go index 8e5e096..2c54b97 100644 --- a/internal/gameserver/handlers_battle.go +++ b/internal/gameserver/handlers_battle.go @@ -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 {