fix: restore battle entry and character profile initialization

This commit is contained in:
CAtlas0784 committed 2026-10-17 11:58:20 +07:00
1 parent cfbbc7b970
commit 5c4d0d4149
16 files changed
+888 -228

No files matched your search

+27 -9
View File
@@ -199,6 +199,23 @@ func (a *AvatarJson) SPMaxVal() uint32 {
return 10000
}
// Battle SP is a ratio in ten-thousandths (PearlSR createBattleAvatar).
// FreeSR imports may instead carry absolute energy and its capacity; use
// their ratio so both absolute and already scaled imports serialize alike.
const battleRatioScale uint32 = 10000
func (a *AvatarJson) BattleSPCur() uint32 {
maximum := a.SPMaxVal()
if maximum == 0 {
return 0
}
current := a.SPCur()
if current > maximum {
current = maximum
}
return uint32(uint64(current) * uint64(battleRatioScale) / uint64(maximum))
}
// ---------------------------------------------------------------------------
// Battle config (freesr-data.json)
// ---------------------------------------------------------------------------
@@ -253,15 +270,16 @@ type RogueMagicScepter struct {
}
type BattleConfig struct {
BattleType BattleType `json:"battle_type"`
Monsters [][]Monster `json:"monsters"`
Blessings []BattleBuffJson `json:"blessings"`
StageID uint32 `json:"stage_id"`
CycleCount uint32 `json:"cycle_count"`
PathResonanceID uint32 `json:"path_resonance_id"`
CustomStats []SubAffix `json:"custom_stats"`
Scepters []RogueMagicScepter `json:"scepters"`
CustomBattleLineup map[uint32]uint32 `json:"custom_battle_lineup"`
EnableInvasionSupport bool `json:"enable_invasion_support"`
BattleType BattleType `json:"battle_type"`
Monsters [][]Monster `json:"monsters"`
Blessings []BattleBuffJson `json:"blessings"`
StageID uint32 `json:"stage_id"`
CycleCount uint32 `json:"cycle_count"`
PathResonanceID uint32 `json:"path_resonance_id"`
CustomStats []SubAffix `json:"custom_stats"`
Scepters []RogueMagicScepter `json:"scepters"`
CustomBattleLineup map[uint32]uint32 `json:"custom_battle_lineup"`
}
func DefaultBattleConfig() BattleConfig {
+10 -12
View File
@@ -206,8 +206,8 @@ func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, rel
EnhancedId: enhancedID,
IGDEHGIKFJA: exRanks,
SpBar: &goproto.SpBarInfo{
CurSp: a.SPCur(),
MaxSp: a.SPMaxVal(),
CurSp: a.BattleSPCur(),
MaxSp: battleRatioScale,
},
}
@@ -227,11 +227,12 @@ func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, rel
var buffs []*goproto.BattleBuff
for _, buffID := range a.Techniques {
buffs = append(buffs, &goproto.BattleBuff{
Id: buffID,
Level: 1,
OwnerId: index,
WaveFlag: 0xffffffff,
DynamicValues: map[string]float32{"SkillIndex": 2.0},
Id: buffID,
Level: 1,
OwnerId: index,
WaveFlag: 0xffffffff,
TargetIndexList: []uint32{0},
DynamicValues: map[string]float32{"SkillIndex": 0},
})
}
@@ -310,13 +311,10 @@ 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: hp,
// Zero requests the stage's normal HP, as in PearlSR addMonsterWaves.
MaxHp: m.MaxHP,
}
}
+18 -1
View File
@@ -75,6 +75,7 @@ type LevelOutputConfig struct {
type AvatarConfig struct {
WeaknessBuffID uint32 `json:"weaknessBuffId"`
DamageType string `json:"damageType"`
}
type JsonConfig struct {
@@ -185,6 +186,23 @@ func GameRes() (*JsonConfig, error) {
}
func loadSupplementaryResources(res *JsonConfig) {
// PearlSR's resource export keeps avatar data in AvatarConfig.json;
// res.json may legitimately contain an empty avatarConfigs object.
if raw, err := readResourceFile("resources/AvatarConfig.json"); err == nil {
var avatars struct {
Entries []struct {
ID uint32 `json:"AvatarID"`
DamageType string `json:"DamageType"`
} `json:"avatar_config"`
}
if err := json.Unmarshal(raw, &avatars); err == nil {
for _, avatar := range avatars.Entries {
cfg := res.AvatarConfigs[avatar.ID]
cfg.DamageType = avatar.DamageType
res.AvatarConfigs[avatar.ID] = cfg
}
}
}
entrancesByEntry := map[uint32]MapEntranceEntry{}
if raw, err := readResourceFile("resources/MapEntrance.json", "MapEntrance.json"); err == nil {
var list []MapEntranceEntry
@@ -369,4 +387,3 @@ func Teleports() map[uint32]map[uint32]TeleportInfo {
})
return teleportDB
}
+191
View File
@@ -0,0 +1,191 @@
package gameserver
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"gac-hkrpg-go/internal/common"
goproto "gac-hkrpg-go/protocol/proto"
)
// Game IDs and character exceptions are resource data with an upstream revision,
// not executable defaults. Missing or invalid rules must not silently produce a
// different battle.
type battleRules struct {
EntryAttackBuffs map[string]uint32 `json:"entry_attack_buffs"`
Source struct {
Revision string `json:"revision"`
} `json:"source"`
TechniqueBuffs map[uint32][]struct {
ID uint32 `json:"id"`
Owner string `json:"owner"`
DynamicValues map[string]float32 `json:"dynamic_values"`
} `json:"technique_buffs"`
GlobalPassives []struct {
Setting string `json:"setting"`
BuffID uint32 `json:"buff_id"`
OwnerSlot uint32 `json:"owner_slot"`
} `json:"global_passives"`
IgnoreToughness struct {
Avatars []uint32 `json:"avatars"`
BuffID uint32 `json:"buff_id"`
} `json:"ignore_toughness"`
BattleTargets []struct {
BattleType string `json:"battle_type"`
Targets map[uint32][]struct {
ID uint32 `json:"id"`
Progress uint32 `json:"progress"`
TotalProgress uint32 `json:"total_progress"`
} `json:"targets"`
} `json:"battle_targets"`
InvasionBuffs map[uint32]uint32 `json:"invasion_buffs"`
InvasionSupportBuff uint32 `json:"invasion_support_buff"`
}
var battleRulesOnce sync.Once
var loadedBattleRules battleRules
var battleRulesErr error
func loadBattleRules() (*battleRules, error) {
battleRulesOnce.Do(func() {
raw, err := os.ReadFile("resources/BattleRules.json")
if err != nil {
battleRulesErr = err
return
}
if err := json.Unmarshal(raw, &loadedBattleRules); err != nil {
battleRulesErr = err
return
}
if loadedBattleRules.Source.Revision == "" {
battleRulesErr = fmt.Errorf("battle rules need a source revision")
return
}
for _, rules := range loadedBattleRules.TechniqueBuffs {
for _, rule := range rules {
if rule.ID == 0 || (rule.Owner != "avatar" && rule.Owner != "leader") {
battleRulesErr = fmt.Errorf("invalid technique buff rule: %+v", rule)
return
}
}
}
for _, rule := range loadedBattleRules.GlobalPassives {
if rule.BuffID == 0 || rule.OwnerSlot >= 4 {
battleRulesErr = fmt.Errorf("invalid global passive rule: %+v", rule)
return
}
switch rule.Setting {
case "enable_sw_global", "enable_castorice_global", "enable_aha_global":
default:
battleRulesErr = fmt.Errorf("unknown global passive setting %q", rule.Setting)
return
}
}
})
if battleRulesErr != nil {
return nil, fmt.Errorf("load resources/BattleRules.json: %w", battleRulesErr)
}
return &loadedBattleRules, nil
}
func battleBuff(id, owner uint32, dynamic map[string]float32) *goproto.BattleBuff {
values := map[string]float32{"SkillIndex": 0}
for key, value := range dynamic {
values[key] = value
}
return &goproto.BattleBuff{Id: id, Level: 1, OwnerId: owner,
WaveFlag: ^uint32(0), TargetIndexList: []uint32{0}, DynamicValues: values}
}
func (r *battleRules) techniqueBuffs(a *common.AvatarJson, slot, leader uint32, imported []*goproto.BattleBuff) []*goproto.BattleBuff {
if len(a.Techniques) == 0 {
return nil
}
buffs := imported
// Explicit character rules in PearlSR replace the generic technique. The
// user's empty techniques array still disables techniques for that avatar.
if rules, ok := r.TechniqueBuffs[a.AvatarID]; ok {
buffs = nil
for _, rule := range rules {
owner := slot
if rule.Owner == "leader" {
owner = leader
}
buffs = append(buffs, battleBuff(rule.ID, owner, rule.DynamicValues))
}
}
if containsU32(r.IgnoreToughness.Avatars, a.AvatarID) {
found := false
for _, buff := range buffs {
if buff.Id == r.IgnoreToughness.BuffID {
found = true
}
}
if !found {
buffs = append(buffs, battleBuff(r.IgnoreToughness.BuffID, slot, nil))
}
}
// Global passives are handled once, with their configured owner and setting.
result := make([]*goproto.BattleBuff, 0, len(buffs))
for _, buff := range buffs {
global := false
for _, rule := range r.GlobalPassives {
if buff.Id == rule.BuffID {
global = true
break
}
}
if !global {
result = append(result, buff)
}
}
return result
}
func (r *battleRules) addGlobalPassives(battle *goproto.SceneBattleInfo, player *common.FreesrData) {
for _, rule := range r.GlobalPassives {
var enabled *bool
switch rule.Setting {
case "enable_sw_global":
enabled = player.EnableSWGlobal
case "enable_castorice_global":
enabled = player.EnableCastoriceGlobal
case "enable_aha_global":
enabled = player.EnableAhaGlobal
}
if enabled != nil && !*enabled {
continue
}
owner := rule.OwnerSlot
// A shorter lineup must not refer to an absent actor.
present := false
for _, avatar := range battle.BattleAvatarList {
if avatar.Index == owner {
present = true
}
}
if !present {
owner = battle.BattleAvatarList[0].Index
}
battle.BuffList = append(battle.BuffList, battleBuff(rule.BuffID, owner, nil))
}
}
func (r *battleRules) addTargets(battle *goproto.SceneBattleInfo, kind common.BattleType) {
for _, rule := range r.BattleTargets {
if !strings.EqualFold(rule.BattleType, string(kind)) {
continue
}
for key, targets := range rule.Targets {
list := &goproto.BattleTargetList{}
for _, target := range targets {
list.BattleTargetList = append(list.BattleTargetList, &goproto.BattleTarget{
Id: target.ID, Progress: target.Progress, TotalProgress: target.TotalProgress})
}
battle.BattleTargetInfo[key] = list
}
}
}
+5
View File
@@ -123,6 +123,11 @@ func rawNamed(request string, fn func(*Session, []byte) error) {
var dangerFamilies = []string{
"GridFight", "ChessRogue", "EvolveBuild", "ChimeraDuel",
"Jukebox", "CycleScore", "MusicAlbum", "DrinkMaker",
// Player.log 2026-10-17: these callbacks dereference state absent from
// the automatic empty response. Leave unsupported modules unanswered.
"RogueTourn", "ChenLing",
// Same failure observed in the fresh verification login.
"SocialPlay", "PetMarble",
}
func isDangerCmd(cmdID uint16) bool {
+113 -186
View File
@@ -10,6 +10,10 @@ import (
goproto "gac-hkrpg-go/protocol/proto"
)
// PearlSR 005d2c1 protocol/src/protocol.pb.zig: MonsterBattleType.
// The Go schema retains obfuscated enum labels; zero is NONE, not TRIGGER.
const monsterBattleTrigger goproto.MonsterBattleType = 1
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))
@@ -17,7 +21,10 @@ func init() {
rsp.PropEntityId = req.PropEntityId
rsp.CocoonId = req.CocoonId
rsp.Wave = req.Wave
battleInfo := createBattleInfo(s, 0, 0)
battleInfo, err := createBattleInfo(s, 0, 0)
if err != nil {
return err
}
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.BattleInfo = battleInfo
return nil
@@ -29,7 +36,10 @@ func init() {
rsp.Retcode = 0
rsp.CocoonId = req.CocoonId
rsp.Wave = req.Wave
battleInfo := createBattleInfo(s, 0, 0)
battleInfo, err := createBattleInfo(s, 0, 0)
if err != nil {
return err
}
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.BattleInfo = battleInfo
@@ -38,7 +48,10 @@ func init() {
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)
battleInfo, err := createBattleInfo(s, 0, 0)
if err != nil {
return err
}
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.Retcode = 0
@@ -86,7 +99,10 @@ func init() {
})
handle("SceneEnterStage", func(s *Session, req *goproto.SceneEnterStageCsReq, rsp *goproto.SceneEnterStageScRsp) error {
battleInfo := createBattleInfo(s, 0, 0)
battleInfo, err := createBattleInfo(s, 0, 0)
if err != nil {
return err
}
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.BattleInfo = battleInfo
return nil
@@ -100,26 +116,8 @@ func init() {
return nil
})
// ================================================================
// SceneCastSkill — THE CAMERA-DETACH BUG FIX
// (บัคกล้องหลุดจากตัวละครตอนกดตี)
//
// อาการ: กดโจมตีใน overworld แล้วกล้องหลุดออกจากตัวละคร
//
// สาเหตุ (จากโค้ด Rust เดิม): handler ตอบ_info + monster_battle_info
// ให้ client เข้าสู่การต่อสู้กับ target ที่ "ผ่าน filter แบบหลวมๆ"
// filter เดิม: id > 30000 (monster ในฉาก) || id < 1000 (avatar entity ของผู้เล่น!)
// ถ้า targets ว่างและอยู่ใน arena (entry >= 3000000) ยัง "หลอก" target
// เป็น 30001 อีกชั้น
// พอ client ได้_info กับ "monster" ที่จริงคือ prop/NPC/ตัวผู้เล่นเอง
// หรือ monster ที่ไม่มี event_id มันจะเริ่ม transition ที่โหลดไม่สำเร็จ
// → camera ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร
//
// FIX: ยอมรับเฉพาะ entity ที่ server spawn เป็น "monster จริง" ในฉากปัจจุบัน
// (เก็บไว้ใน Session.MonsterEntities ตอน load_scene) ถ้าไม่มี monster จริง
// ถูกโจมตี → ตอบ retcode 0 + cast_entity_id เฉยๆ ไม่เข้า
// client จะเล่นแอนิเมชันตีปกติ กล้องค้างกับตัวละคร
// ================================================================
// Consume every target carrier in the current schema. Only entities sent
// as monsters in this scene may start a battle; never fabricate a target.
handle("SceneCastSkill", func(s *Session, req *goproto.SceneCastSkillCsReq, rsp *goproto.SceneCastSkillScRsp) error {
rsp.CastEntityId = req.CastEntityId
@@ -138,6 +136,14 @@ func init() {
for _, id := range req.AssistMonsterEntityIdList {
addTarget(id)
}
for _, group := range req.AssistMonsterEntityInfo {
for _, id := range group.GetEntityIdList() {
addTarget(id)
}
}
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d skill_index=%d hits=%v assists=%v groups=%v scene_monsters=%v matched=%v",
req.CastEntityId, req.CasterId, req.SkillIndex, req.HitTargetEntityIdList,
req.AssistMonsterEntityIdList, req.AssistMonsterEntityInfo, s.MonsterEntities, targets)
if len(targets) == 0 {
// ไม่มี monster จริงถูกตี — ไม่เข้า (นี่คือ fix หลัก)
@@ -150,12 +156,16 @@ func init() {
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> with %d monster(s)",
req.CastEntityId, req.CasterId, len(targets))
battleInfo := createBattleInfo(s, req.CasterId, req.SkillIndex)
battleInfo, err := createBattleInfo(s, req.CasterId, req.SkillIndex)
if err != nil {
return err
}
CURRENT_BATTLE.Store(s.Token, battleInfo)
for _, id := range targets {
rsp.MonsterBattleInfo = append(rsp.MonsterBattleInfo, &goproto.HitMonsterBattleInfo{
TargetMonsterEntityId: id,
MonsterBattleType: monsterBattleTrigger,
})
}
rsp.BattleInfo = battleInfo
@@ -183,23 +193,26 @@ func init() {
}
// createBattleInfo is the Go port of.rs create_battle_info.
func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBattleInfo {
func createBattleInfo(s *Session, casterID, skillIndex uint32) (*goproto.SceneBattleInfo, error) {
player := s.JSON()
if player == nil {
Log("data is not set!")
return &goproto.SceneBattleInfo{}
return nil, fmt.Errorf("battle: player data is not loaded")
}
rules, err := loadBattleRules()
if err != nil {
return nil, err
}
battleInfo := &goproto.SceneBattleInfo{
StageId: player.BattleConfig.StageID,
LogicRandomSeed: randUint32(),
BattleId: 1,
RoundsLimit: player.BattleConfig.CycleCount,
WorldLevel: 6,
StageId: player.BattleConfig.StageID,
LogicRandomSeed: randUint32(),
BattleId: 1,
RoundsLimit: player.BattleConfig.CycleCount,
WorldLevel: 6,
BattleTargetInfo: make(map[uint32]*goproto.BattleTargetList),
}
isCalyx := casterID == 0 && skillIndex == 0
hasDahlia := false
firstAvatarID := uint32(0)
firstAvatarIdx := uint32(9999)
@@ -217,14 +230,6 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
// avatars
lineup(func(avatarIndex, lineupID uint32) {
if firstAvatarID == 0 {
firstAvatarID = lineupID
firstAvatarIdx = avatarIndex
}
if !hasDahlia {
hasDahlia = lineupID == 1321
}
avatarID := lineupID
isTrailblazer := lineupID == 8001
isMarch := lineupID == 1001
@@ -239,81 +244,51 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
return
}
if firstAvatarID == 0 {
firstAvatarID = avatarID
firstAvatarIdx = avatarIndex
}
battleAvatar, techs := avatar.ToBattleAvatarProto(
avatarIndex,
player.FindLightconeFor(avatarID),
player.FindRelicsFor(avatarID),
)
battleInfo.BuffList = append(battleInfo.BuffList, techs...)
res, err := common.GameRes()
if casterID > 0 && avatarIndex == casterID-1 && err == nil {
if avatarConfig, okc := res.AvatarConfigs[avatarID]; okc && !containsU32(avatar.Techniques, 1000119) {
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
Id: avatarConfig.WeaknessBuffID,
Level: 1,
OwnerId: avatarIndex,
WaveFlag: 0xffffffff,
DynamicValues: map[string]float32{"SkillIndex": float32(skillIndex)},
})
}
}
battleInfo.BuffList = append(battleInfo.BuffList, rules.techniqueBuffs(avatar, avatarIndex, firstAvatarIdx, techs)...)
battleInfo.BattleAvatarList = append(battleInfo.BattleAvatarList, battleAvatar)
// hardcoded march buff
if avatar.AvatarID == 1224 {
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
Id: 122401,
Level: 3,
WaveFlag: 0xffffffff,
OwnerId: avatarIndex,
DynamicValues: map[string]float32{"#ADF_1": 3, "#ADF_2": 3},
TargetIndexList: []uint32{0},
})
}
})
// resolve character-specific technique buffs dynamically
firstAvatarAttackID := uint32(0)
if res, err := common.GameRes(); err == nil {
if cfg, ok := res.AvatarConfigs[firstAvatarID]; ok {
firstAvatarAttackID = cfg.WeaknessBuffID
}
if len(battleInfo.BattleAvatarList) == 0 {
return nil, fmt.Errorf("battle: lineup has no available avatars")
}
len0 := len(battleInfo.BuffList)
hasReplaced := false
var dahliaBuffs []*goproto.BattleBuff
for i, buff := range battleInfo.BuffList {
isLast := i == len0-1
if buff.Id == 141202 || buff.Id == 141403 {
buff.OwnerId = firstAvatarIdx
continue
}
if hasDahlia && isCalyx && buff.Id >= 1000111 && buff.Id <= 1000117 && !hasReplaced && firstAvatarAttackID != 0 {
buff.Id = firstAvatarAttackID
buff.OwnerId = firstAvatarIdx
hasReplaced = true
}
if hasDahlia && isCalyx && !hasReplaced && firstAvatarAttackID != 0 && isLast {
dahliaBuffs = append(dahliaBuffs,
&goproto.BattleBuff{
Id: 1000121, Level: 1, WaveFlag: 0xffffffff,
TargetIndexList: []uint32{firstAvatarIdx},
},
&goproto.BattleBuff{
Id: firstAvatarAttackID, Level: 1, WaveFlag: 0xffffffff,
TargetIndexList: []uint32{firstAvatarIdx},
DynamicValues: map[string]float32{"SkillIndex": float32(firstAvatarIdx)},
},
)
// PearlSR always supplies an entry attack, including Calyx. Resolve the
// actor from the scene's actual slot IDs, rather than assuming cast_entity
// identifies a lineup position.
attackSlot, attackID := firstAvatarIdx, firstAvatarID
for _, avatar := range battleInfo.BattleAvatarList {
if casterID != 0 && avatar.Index+1 == casterID {
attackSlot, attackID = avatar.Index, avatar.Id
break
}
}
battleInfo.BuffList = append(battleInfo.BuffList, dahliaBuffs...)
res, err := common.GameRes()
if err != nil {
return nil, fmt.Errorf("battle: load avatar resources: %w", err)
}
cfg := res.AvatarConfigs[attackID]
attackBuffID := rules.EntryAttackBuffs[cfg.DamageType]
if attackBuffID == 0 {
attackBuffID = cfg.WeaknessBuffID
}
if attackBuffID != 0 {
entrySkillIndex := skillIndex
if casterID == 0 {
entrySkillIndex = 1
}
battleInfo.BuffList = append(battleInfo.BuffList, battleBuff(attackBuffID, attackSlot, map[string]float32{"SkillIndex": float32(entrySkillIndex)}))
} else {
return nil, fmt.Errorf("battle: avatar %d has no entry attack in res.json", attackID)
}
// custom stats for avatars
for _, stat := range player.BattleConfig.CustomStats {
@@ -344,15 +319,16 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
// blessings
for _, blessing := range player.BattleConfig.Blessings {
buff := &goproto.BattleBuff{
Id: blessing.ID,
Level: blessing.Level,
WaveFlag: 0xffffffff,
OwnerId: 0xffffffff,
Id: blessing.ID,
Level: blessing.Level,
WaveFlag: 0xffffffff,
OwnerId: 0xffffffff,
TargetIndexList: []uint32{0},
}
if blessing.DynamicKey != nil {
buff.DynamicValues = map[string]float32{blessing.DynamicKey.Key: float32(blessing.DynamicKey.Value)}
} else {
buff.DynamicValues = map[string]float32{}
buff.DynamicValues = map[string]float32{"SkillIndex": 0}
}
for _, dv := range blessing.DynamicValues {
if _, exists := buff.DynamicValues[dv.Key]; exists {
@@ -363,31 +339,7 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
battleInfo.BuffList = append(battleInfo.BuffList, buff)
}
// pf score object
if player.BattleConfig.BattleType == common.BattleTypePF {
if battleInfo.StageId >= 30309011 {
battleInfo.BattleTargetInfo[1] = &goproto.BattleTargetList{
BattleTargetList: []*goproto.BattleTarget{{Id: 10003}},
}
} else {
battleInfo.BattleTargetInfo[1] = &goproto.BattleTargetList{
BattleTargetList: []*goproto.BattleTarget{{Id: 10002}},
}
}
for i := uint32(2); i <= 4; i++ {
battleInfo.BattleTargetInfo[i] = &goproto.BattleTargetList{}
}
battleInfo.BattleTargetInfo[5] = &goproto.BattleTargetList{
BattleTargetList: []*goproto.BattleTarget{{Id: 2001}, {Id: 2002}},
}
}
// Apocalyptic Shadow
if player.BattleConfig.BattleType == common.BattleTypeAS {
battleInfo.BattleTargetInfo[1] = &goproto.BattleTargetList{
BattleTargetList: []*goproto.BattleTarget{{Id: 90005}},
}
}
rules.addTargets(battleInfo, player.BattleConfig.BattleType)
// SU
if player.BattleConfig.BattleType == common.BattleTypeSU {
@@ -403,58 +355,33 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
battleInfo.MonsterWaveList = common.ToSceneMonsterWaves(player.BattleConfig.Monsters)
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
// global buffs
hasCastorice, hasSilverWolf := false, false
hasAha := false
for _, buff := range battleInfo.BuffList {
if buff.Id == 140703 {
hasCastorice = true
}
if buff.Id == 150602 {
hasSilverWolf = true
}
if buff.Id == 151104 {
hasAha = true
}
}
if !hasCastorice && (player.EnableCastoriceGlobal == nil || *player.EnableCastoriceGlobal) {
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
Id: 140703, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
})
}
if !hasSilverWolf && (player.EnableSWGlobal == nil || *player.EnableSWGlobal) {
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
Id: 150602, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
})
}
if !hasAha && (player.EnableAhaGlobal == nil || *player.EnableAhaGlobal) {
battleInfo.BuffList = append(battleInfo.BuffList, &goproto.BattleBuff{
Id: 151104, Level: 1, OwnerId: 0xffffffff, WaveFlag: 0xffffffff,
})
}
rules.addGlobalPassives(battleInfo, player)
// 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},
})
// Invasion support belongs to an active challenge, not a custom battle
// merely reusing the same stage. The scene loader records that state.
if s.ChallengeStageID != 0 {
if invasionID := getStageInvasionID(s.ChallengeStageID); invasionID != 0 {
buffID, ok := rules.InvasionBuffs[invasionID]
if !ok {
return nil, fmt.Errorf("battle: invasion %d has no buff resource", invasionID)
}
battleInfo.BuffList = append(battleInfo.BuffList, battleBuff(buffID, firstAvatarIdx, map[string]float32{"SkillIndex": 1}))
if player.BattleConfig.EnableInvasionSupport {
battleInfo.BuffList = append(battleInfo.BuffList, battleBuff(rules.InvasionSupportBuff, firstAvatarIdx, map[string]float32{"SkillIndex": 1}))
}
}
}
if len(battleInfo.MonsterWaveList) == 0 {
return nil, fmt.Errorf("battle: stage %d has no configured waves", battleInfo.StageId)
}
for i, wave := range battleInfo.MonsterWaveList {
if len(wave.MonsterList) == 0 {
return nil, fmt.Errorf("battle: stage %d wave %d is empty", battleInfo.StageId, i)
}
}
Log(fmt.Sprintf("[BATTLE] prepared stage=%d avatars=%d waves=%d buffs=%d challenge_stage=%d", battleInfo.StageId, len(battleInfo.BattleAvatarList), len(battleInfo.MonsterWaveList), len(battleInfo.BuffList), s.ChallengeStageID))
return battleInfo
return battleInfo, nil
}
type stageInvasionConfigData struct {
@@ -167,6 +167,7 @@ func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID u
// single boss monster (entity 30001, registered for SceneCastSkill battles)
monsterGroup := &goproto.SceneEntityGroupInfo{GroupId: targetGroupID}
s.MonsterEntities = map[uint32]uint32{30001: eventID}
s.ChallengeStageID = eventID
monsterGroup.EntityList = append(monsterGroup.EntityList, &goproto.SceneEntityInfo{
InstId: instID,
GroupId: targetGroupID,
+26 -6
View File
@@ -37,6 +37,25 @@ func (sm *syncMap) Delete(k uint32) { muBattle.Lock(); sm.init(); delete(sm.m, k
func randUint32() uint32 { return rand.Uint32() }
func playerArchive(s *Session) *goproto.ArchiveData {
archive := &goproto.ArchiveData{}
if player := s.JSON(); player != nil {
for id := range player.Avatars {
archive.ArchiveAvatarIdList = append(archive.ArchiveAvatarIdList, id)
}
sortU32Slice(archive.ArchiveAvatarIdList)
seen := map[uint32]bool{}
for _, equipment := range player.Lightcones {
if !seen[equipment.ItemID] {
seen[equipment.ItemID] = true
archive.ArchiveEquipmentIdList = append(archive.ArchiveEquipmentIdList, equipment.ItemID)
}
}
sortU32Slice(archive.ArchiveEquipmentIdList)
}
return archive
}
func init() {
handle("GetBag", func(s *Session, req *goproto.GetBagCsReq, rsp *goproto.GetBagScRsp) error {
player := s.JSON()
@@ -55,17 +74,18 @@ func init() {
{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,
})
}
// Avatar IDs do not imply item IDs. The old 130000+avatar formula
// produced nonexistent materials and broke the client's inventory init.
return nil
})
handle("GetArchiveData", func(s *Session, req *goproto.GetArchiveDataCsReq, rsp *goproto.GetArchiveDataScRsp) error {
rsp.Retcode = 0
rsp.ArchiveData = playerArchive(s)
return nil
})
handle("GetUpdatedArchiveData", func(s *Session, req *goproto.GetUpdatedArchiveDataCsReq, rsp *goproto.GetUpdatedArchiveDataScRsp) error {
rsp.ArchiveData = playerArchive(s)
return nil
})
+35 -10
View File
@@ -95,24 +95,49 @@ func init() {
})
handle("GetPlayerBoardData", func(s *Session, req *goproto.GetPlayerBoardDataCsReq, rsp *goproto.GetPlayerBoardDataScRsp) error {
profile, err := loadProfileResources()
if err != nil {
return err
}
rsp.Retcode = 0
rsp.CurrentHeadIconId = 200001
rsp.CurrentPersonalCardId = 253001
rsp.CurrentHeadIconId = profile.Icons[0]
rsp.CurrentPersonalCardId = profile.CurrentPersonalCard
rsp.HeadFrameInfo = profile.HeadFrame
rsp.NBIEJKLENOB = &goproto.DisplayAvatarVec{IsDisplay: true}
rsp.Signature = "Trailblazer"
rsp.JJDDEKIAOFC = []uint32{200001, 200002, 200101, 200102, 200103, 200104}
rsp.KJAABDABLJJ = []uint32{253001, 253002}
// PearlSR 005d2c1 GetPlayerBoardDataScRsp: field 4 is personal
// cards, field 8 is head icons, and field 9 is assist avatar IDs.
rsp.JJDDEKIAOFC = profile.PersonalCards
for _, id := range profile.Icons {
rsp.KNINLDPFLMA = append(rsp.KNINLDPFLMA, &goproto.HeadIconData{Id: id})
}
if player := s.JSON(); player != nil {
for _, slot := range player.SortedLineupKeys() {
id := player.Lineups[slot]
if _, ok := player.Avatars[id]; !ok {
continue
}
rsp.KJAABDABLJJ = append(rsp.KJAABDABLJJ, id)
rsp.NBIEJKLENOB.DisplayAvatarList = append(rsp.NBIEJKLENOB.DisplayAvatarList,
&goproto.DisplayAvatarData{AvatarId: id, Pos: slot})
}
}
SceneDebugLog("GET_PLAYER_BOARD_DATA sent")
return nil
})
handle("GetPhoneData", func(s *Session, req *goproto.GetPhoneDataCsReq, rsp *goproto.GetPhoneDataScRsp) error {
profile, err := loadProfileResources()
if err != nil {
return err
}
rsp.Retcode = 0
rsp.CurPhoneTheme = 1
rsp.CurPhoneCase = 200001
rsp.CurChatBubble = 220001
rsp.OwnedPhoneThemes = []uint32{1}
rsp.OwnedPhoneCases = []uint32{200001}
rsp.OwnedChatBubbles = []uint32{220001}
rsp.CurPhoneTheme = profile.CurrentPhoneTheme
rsp.CurPhoneCase = profile.CurrentPhoneCase
rsp.CurChatBubble = profile.CurrentChatBubble
rsp.OwnedPhoneThemes = profile.PhoneThemes
rsp.OwnedPhoneCases = profile.PhoneCases
rsp.OwnedChatBubbles = profile.ChatBubbles
SceneDebugLog("GET_PHONE_DATA sent")
return nil
})
+1
View File
@@ -928,6 +928,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, telepo
s.PropEntities = map[uint32]PropEntry{}
s.MonsterEntities = map[uint32]uint32{}
s.ChallengeStageID = 0
for _, groupID := range sortedU32Keys2(scene.Scenes) {
group := scene.Scenes[groupID]
+78
View File
@@ -0,0 +1,78 @@
package gameserver
import (
"encoding/json"
"fmt"
"os"
"sync"
goproto "gac-hkrpg-go/protocol/proto"
)
type profileResources struct {
Source struct {
Revision string `json:"revision"`
} `json:"source"`
PersonalCards []uint32 `json:"personal_cards"`
CurrentPersonalCard uint32 `json:"current_personal_card"`
HeadFrame *goproto.HeadFrameInfo `json:"head_frame"`
PhoneCases []uint32 `json:"phone_cases"`
PhoneThemes []uint32 `json:"phone_themes"`
ChatBubbles []uint32 `json:"chat_bubbles"`
CurrentPhoneCase uint32 `json:"current_phone_case"`
CurrentPhoneTheme uint32 `json:"current_phone_theme"`
CurrentChatBubble uint32 `json:"current_chat_bubble"`
Icons []uint32 `json:"-"`
}
var profileOnce sync.Once
var profileData profileResources
var profileError error
func loadProfileResources() (*profileResources, error) {
profileOnce.Do(func() {
raw, err := os.ReadFile("resources/ProfileConfig.json")
if err != nil {
profileError = err
return
}
if err := json.Unmarshal(raw, &profileData); err != nil {
profileError = err
return
}
if profileData.Source.Revision == "" || profileData.HeadFrame == nil || !containsU32(profileData.PersonalCards, profileData.CurrentPersonalCard) {
profileError = fmt.Errorf("profile source, head frame or selected personal card is invalid")
return
}
if !containsU32(profileData.PhoneCases, profileData.CurrentPhoneCase) {
profileError = fmt.Errorf("selected phone case is not owned")
return
}
raw, err = os.ReadFile("resources/AvatarPlayerIcon.json")
if err != nil {
profileError = err
return
}
var icons struct {
Entries []struct {
ID uint32 `json:"ID"`
} `json:"player_icon_config"`
}
if err := json.Unmarshal(raw, &icons); err != nil {
profileError = err
return
}
for _, icon := range icons.Entries {
if icon.ID != 0 {
profileData.Icons = append(profileData.Icons, icon.ID)
}
}
if len(profileData.Icons) == 0 {
profileError = fmt.Errorf("AvatarPlayerIcon.json has no icons")
}
})
if profileError != nil {
return nil, fmt.Errorf("load profile resources: %w", profileError)
}
return &profileData, nil
}
+2
View File
@@ -44,6 +44,8 @@ type Session struct {
// spawned in the current scene. Used by SceneCastSkill to decide whether an
// attack actually hit a monster (the camera-detach bug fix).
MonsterEntities map[uint32]uint32
// Set by the challenge scene loader, cleared when loading an overworld scene.
ChallengeStageID uint32
closed atomic.Bool
closeOnce sync.Once