274 lines
8.1 KiB
Go
274 lines
8.1 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
|
)
|
|
|
|
type worldEvent struct {
|
|
EventID uint32 `json:"EventID"`
|
|
WorldLevel uint32 `json:"WorldLevel"`
|
|
StageID uint32 `json:"StageID"`
|
|
}
|
|
type cocoonResource struct {
|
|
ID uint32 `json:"ID"`
|
|
WorldLevel uint32 `json:"WorldLevel"`
|
|
CocoonType uint32 `json:"CocoonType"`
|
|
StageID uint32 `json:"StageID"`
|
|
StageIDs []uint32 `json:"StageIDList"`
|
|
MaxCount uint32 `json:"MaxChallengeCnt"`
|
|
}
|
|
|
|
// CocoonType values from the Client's Cocoon table. Only Calyx entries may
|
|
// use FreeSR custom battles; Stagnant Shadow and Cavern must use Client stages.
|
|
const (
|
|
clientCocoonTypeCalyx uint32 = 1
|
|
clientCocoonTypeStagnantShadow uint32 = 2
|
|
clientCocoonTypeCavernCorrosion uint32 = 3
|
|
)
|
|
|
|
func canUseSrtoolCustomCocoon(row cocoonResource) bool {
|
|
return row.CocoonType == clientCocoonTypeCalyx
|
|
}
|
|
|
|
type worldBattleResources struct {
|
|
ClientVersion string `json:"client_version"`
|
|
WorldLevel uint32 `json:"world_level"`
|
|
Events []worldEvent `json:"events"`
|
|
Cocoons []cocoonResource `json:"cocoons"`
|
|
NPCMonsters map[uint32]struct {
|
|
Rank uint32 `json:"rank"`
|
|
} `json:"npc_monsters"`
|
|
InstantDefeatAvatars []uint32 `json:"instant_defeat_avatars"`
|
|
InstantDefeatRanks []uint32 `json:"instant_defeat_ranks"`
|
|
}
|
|
|
|
type sceneMonsterIdentity struct {
|
|
MonsterID uint32
|
|
GroupID uint32
|
|
}
|
|
|
|
// Match both the technique owner and the actual NPC rank exported from Client.
|
|
// Challenge bosses and tool-controlled encounters always use their battle flow.
|
|
func (s *Session) instantDefeatTargets(req *goproto.SceneCastSkillCsReq, targets []uint32) []uint32 {
|
|
if req.SkillIndex != 1 || s.ChallengeBattleConfig != nil || s.ChallengeStageID != 0 || common.SrtoolCustomBattleActive() {
|
|
return nil
|
|
}
|
|
data, err := loadWorldBattleResources()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
owner := s.techniqueAvatar(req.CasterId)
|
|
allowed := false
|
|
for _, id := range data.InstantDefeatAvatars {
|
|
if id == owner {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
return nil
|
|
}
|
|
var defeated []uint32
|
|
for _, entity := range targets {
|
|
identity, ok := s.SceneMonsterIdentities[entity]
|
|
if !ok {
|
|
continue
|
|
}
|
|
row, ok := data.NPCMonsters[identity.MonsterID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, rank := range data.InstantDefeatRanks {
|
|
if rank == row.Rank {
|
|
defeated = append(defeated, entity)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return defeated
|
|
}
|
|
|
|
func (s *Session) removeSceneMonsters(ids []uint32) error {
|
|
notify := &goproto.SceneGroupRefreshScNotify{FloorId: s.CurrentSceneFloor}
|
|
groups := map[uint32]*goproto.SceneGroupRefreshInfo{}
|
|
for _, id := range ids {
|
|
m, ok := s.SceneMonsterIdentities[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
group := groups[m.GroupID]
|
|
if group == nil {
|
|
group = &goproto.SceneGroupRefreshInfo{GroupId: m.GroupID, State: 1, GroupRefreshType: goproto.SceneGroupRefreshType_SCENE_GROUP_REFRESH_TYPE_LOADED}
|
|
groups[m.GroupID] = group
|
|
}
|
|
group.RefreshEntity = append(group.RefreshEntity, &goproto.SceneEntityRefreshInfo{RefreshType: &goproto.SceneEntityRefreshInfo_DeleteEntity{DeleteEntity: id}})
|
|
delete(s.MonsterEntities, id)
|
|
delete(s.SceneMonsterIdentities, id)
|
|
}
|
|
for _, id := range sortedU32Keys2(groups) {
|
|
notify.GroupRefreshInfo = append(notify.GroupRefreshInfo, groups[id])
|
|
}
|
|
if len(notify.GroupRefreshInfo) == 0 {
|
|
return nil
|
|
}
|
|
return s.Send(notify)
|
|
}
|
|
|
|
var worldBattleOnce sync.Once
|
|
var worldBattleData worldBattleResources
|
|
var worldBattleError error
|
|
|
|
func loadWorldBattleResources() (*worldBattleResources, error) {
|
|
worldBattleOnce.Do(func() {
|
|
var raw []byte
|
|
raw, worldBattleError = common.ReadResourceFile("resources/WorldBattleConfig.json")
|
|
if worldBattleError != nil {
|
|
return
|
|
}
|
|
worldBattleError = json.Unmarshal(raw, &worldBattleData)
|
|
if worldBattleError != nil {
|
|
return
|
|
}
|
|
cfg, err := common.LoadRuntimeConfig()
|
|
if err != nil {
|
|
worldBattleError = err
|
|
return
|
|
}
|
|
if cfg.ClientVersion != worldBattleData.ClientVersion || len(worldBattleData.Events) == 0 || len(worldBattleData.Cocoons) == 0 {
|
|
worldBattleError = fmt.Errorf("world battle resource missing or Client revision mismatch")
|
|
}
|
|
})
|
|
return &worldBattleData, worldBattleError
|
|
}
|
|
|
|
func resourceBattleConfig(stageID uint32) (*common.BattleConfig, error) {
|
|
stage, ok := getChallengeData().Stages[stageID]
|
|
if !ok || stage.Level == 0 || len(stage.Monsters) == 0 {
|
|
return nil, fmt.Errorf("stage %d has no verified waves", stageID)
|
|
}
|
|
rules, err := loadBattleRules()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config := &common.BattleConfig{StageID: stageID, BattleType: common.BattleTypeDefault, CycleCount: rules.ChallengeRoundLimits[common.BattleTypeDefault]}
|
|
for _, wave := range stage.Monsters {
|
|
if len(wave) == 0 {
|
|
return nil, fmt.Errorf("stage %d has an empty wave", stageID)
|
|
}
|
|
var monsters []common.Monster
|
|
for _, id := range wave {
|
|
monsters = append(monsters, common.Monster{MonsterID: id, Level: stage.Level})
|
|
}
|
|
config.Monsters = append(config.Monsters, monsters)
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (s *Session) worldEncounterConfig(targets []uint32) (*common.BattleConfig, error) {
|
|
if s.ChallengeBattleConfig != nil {
|
|
return s.ChallengeBattleConfig, nil
|
|
}
|
|
data, err := loadWorldBattleResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(targets) == 0 {
|
|
return nil, fmt.Errorf("battle has no scene monster")
|
|
}
|
|
var combined *common.BattleConfig
|
|
seen := map[uint32]bool{}
|
|
for _, target := range targets {
|
|
eventID := s.MonsterEntities[target]
|
|
if seen[eventID] {
|
|
continue
|
|
}
|
|
seen[eventID] = true
|
|
config, err := s.eventBattleConfig(eventID, data.WorldLevel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if combined == nil {
|
|
combined = config
|
|
} else {
|
|
combined.Monsters = append(combined.Monsters, config.Monsters...)
|
|
}
|
|
}
|
|
return combined, nil
|
|
}
|
|
func (s *Session) eventBattleConfig(eventID, worldLevel uint32) (*common.BattleConfig, error) {
|
|
data, err := loadWorldBattleResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range data.Events {
|
|
if row.EventID == eventID && row.WorldLevel == worldLevel {
|
|
config, err := resourceBattleConfig(row.StageID)
|
|
if config != nil {
|
|
level := worldLevel
|
|
config.WorldLevel = &level
|
|
}
|
|
return config, err
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("scene event %d world level %d has no Client stage mapping", eventID, worldLevel)
|
|
}
|
|
|
|
func (s *Session) cocoonBattleConfig(id, worldLevel, wave uint32) (*common.BattleConfig, error) {
|
|
data, err := loadWorldBattleResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range data.Cocoons {
|
|
if row.ID != id || row.WorldLevel != worldLevel {
|
|
continue
|
|
}
|
|
if row.MaxCount != 0 && wave > row.MaxCount {
|
|
return nil, fmt.Errorf("cocoon wave exceeds Client limit")
|
|
}
|
|
if common.SrtoolCustomBattleActive() && canUseSrtoolCustomCocoon(row) {
|
|
// The Client's Cocoon table identifies Calyx separately from
|
|
// Stagnant Shadow and Cavern of Corrosion. Custom battles are limited
|
|
// to Calyx; all other cocoon encounters use their Client stage rows.
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil, fmt.Errorf("player data is not loaded")
|
|
}
|
|
config := player.BattleConfig
|
|
if len(config.Monsters) != 0 {
|
|
return &config, nil
|
|
}
|
|
// An empty custom wave list means the user selected “use Client
|
|
// stages” in Srtool. Fall through to the installed Calyx stage rows.
|
|
}
|
|
stages := row.StageIDs
|
|
if len(stages) == 0 && row.StageID != 0 {
|
|
stages = []uint32{row.StageID}
|
|
}
|
|
if len(stages) == 0 {
|
|
return nil, fmt.Errorf("cocoon %d has no stages", id)
|
|
}
|
|
config, err := resourceBattleConfig(stages[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
level := worldLevel
|
|
config.WorldLevel = &level
|
|
// Quick-start sends zero for its default single stage. Explicit counts
|
|
// repeat complete resource stages; never replace their monster lists.
|
|
for i := uint32(1); i < wave; i++ {
|
|
next, err := resourceBattleConfig(stages[int(i)%len(stages)])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.Monsters = append(config.Monsters, next.Monsters...)
|
|
}
|
|
return config, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown cocoon %d at world level %d", id, worldLevel)
|
|
}
|