pre cw with probe and a bunch script to read game behavior
This commit is contained in:
1 parent
5120219ae2
commit
da8a32f60d
310 files changed
+219693
-872
No files matched your search
@@ -0,0 +1,269 @@
|
||||
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
|
||||
return &config, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user