280 lines
9.1 KiB
Go
280 lines
9.1 KiB
Go
package common
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
)
|
|
|
|
// MonsterSettingsCatalog is a read-only preview of challenge stages plus the
|
|
// resource-backed ID/level suggestions used by the Custom Enemy editor.
|
|
type MonsterSettingsCatalog struct {
|
|
Modes map[string][]MonsterSettingsGroup `json:"modes"`
|
|
MonsterIDs []uint32 `json:"monster_ids"`
|
|
Monsters map[uint32]MonsterSettingsMonsterMetadata `json:"monsters"`
|
|
Levels []uint32 `json:"levels"`
|
|
Sources []string `json:"sources"`
|
|
}
|
|
|
|
type MonsterSettingsGroup struct {
|
|
ID uint32 `json:"id"`
|
|
Names map[string]string `json:"names,omitempty"`
|
|
Stages []MonsterSettingsEncounter `json:"stages"`
|
|
}
|
|
|
|
type MonsterSettingsEncounter struct {
|
|
ID uint32 `json:"id"`
|
|
IsBoss bool `json:"is_boss,omitempty"`
|
|
DefaultBuffID uint32 `json:"default_buff_id,omitempty"`
|
|
MonsterIDs []uint32 `json:"monster_ids,omitempty"`
|
|
Battles []MonsterSettingsBattle `json:"battles"`
|
|
}
|
|
|
|
type MonsterSettingsMonsterMetadata struct {
|
|
Names map[string]string `json:"names"`
|
|
IconURL string `json:"icon_url,omitempty"`
|
|
}
|
|
|
|
type MonsterSettingsBattle struct {
|
|
StageID uint32 `json:"stage_id"`
|
|
Level uint32 `json:"level"`
|
|
Waves [][]uint32 `json:"waves"`
|
|
}
|
|
|
|
type monsterSettingStageData struct {
|
|
Level uint32 `json:"level"`
|
|
Monsters [][]uint32 `json:"monsters"`
|
|
}
|
|
|
|
type monsterSettingsAccumulator struct {
|
|
groups map[uint32]map[uint32]*MonsterSettingsEncounter
|
|
}
|
|
|
|
// LoadMonsterSettingsCatalog uses the same installed challenge and stage
|
|
// resources as the game-server battle handlers. It never supplies a battle
|
|
// override; custom waves are separately restricted to Client Calyx rows.
|
|
func LoadMonsterSettingsCatalog() (MonsterSettingsCatalog, error) {
|
|
challengeRaw, err := ReadResourceFile("challenge_data.json")
|
|
if err != nil {
|
|
return MonsterSettingsCatalog{}, fmt.Errorf("read challenge catalog: %w", err)
|
|
}
|
|
stageRaw, err := ReadResourceFile("resources/StageConfig.json")
|
|
if err != nil {
|
|
return MonsterSettingsCatalog{}, fmt.Errorf("read Client stage catalog: %w", err)
|
|
}
|
|
|
|
var challenges struct {
|
|
Challenges map[string]struct {
|
|
GroupID uint32 `json:"groupId"`
|
|
Monster1 uint32 `json:"monster1"`
|
|
Monster2 uint32 `json:"monster2"`
|
|
Event1 uint32 `json:"event1"`
|
|
Event2 uint32 `json:"event2"`
|
|
} `json:"challenges"`
|
|
Tierce map[string]struct {
|
|
Group uint32 `json:"group"`
|
|
Event uint32 `json:"event"`
|
|
} `json:"tierce"`
|
|
Stages map[string]monsterSettingStageData `json:"stages"`
|
|
Peak struct {
|
|
Stages map[string]struct {
|
|
EventID uint32 `json:"event_id"`
|
|
HardEventID uint32 `json:"hard_event_id"`
|
|
MonsterID uint32 `json:"monster_id"`
|
|
DefaultBuff uint32 `json:"default_buff"`
|
|
IsBoss bool `json:"is_boss"`
|
|
} `json:"stages"`
|
|
Groups map[string]struct {
|
|
PreLevelIDs []uint32 `json:"pre_level_ids"`
|
|
BossLevelID uint32 `json:"boss_level_id"`
|
|
} `json:"groups"`
|
|
} `json:"peak"`
|
|
}
|
|
if err := json.Unmarshal(challengeRaw, &challenges); err != nil {
|
|
return MonsterSettingsCatalog{}, fmt.Errorf("decode challenge catalog: %w", err)
|
|
}
|
|
var stageTable struct {
|
|
Entries []struct {
|
|
StageID uint32 `json:"StageID"`
|
|
Level uint32 `json:"Level"`
|
|
MonsterList [][]uint32 `json:"MonsterList"`
|
|
} `json:"stage_config"`
|
|
}
|
|
if err := json.Unmarshal(stageRaw, &stageTable); err != nil {
|
|
return MonsterSettingsCatalog{}, fmt.Errorf("decode Client stage catalog: %w", err)
|
|
}
|
|
stageByID := make(map[uint32]monsterSettingStageData, len(stageTable.Entries))
|
|
monsterSet := map[uint32]struct{}{}
|
|
levelSet := map[uint32]struct{}{}
|
|
for _, row := range stageTable.Entries {
|
|
stageByID[row.StageID] = monsterSettingStageData{Level: row.Level, Monsters: row.MonsterList}
|
|
if row.Level != 0 {
|
|
levelSet[row.Level] = struct{}{}
|
|
}
|
|
for _, wave := range row.MonsterList {
|
|
for _, id := range wave {
|
|
if id != 0 {
|
|
monsterSet[id] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(stageByID) == 0 {
|
|
return MonsterSettingsCatalog{}, fmt.Errorf("Client StageConfig has no stage rows")
|
|
}
|
|
|
|
acc := map[string]*monsterSettingsAccumulator{
|
|
"moc": {groups: map[uint32]map[uint32]*MonsterSettingsEncounter{}},
|
|
"pf": {groups: map[uint32]map[uint32]*MonsterSettingsEncounter{}},
|
|
"as": {groups: map[uint32]map[uint32]*MonsterSettingsEncounter{}},
|
|
"aa": {groups: map[uint32]map[uint32]*MonsterSettingsEncounter{}},
|
|
}
|
|
addBattle := func(entry *MonsterSettingsEncounter, eventID uint32) {
|
|
stage, ok := stageByID[eventID]
|
|
if !ok || len(stage.Monsters) == 0 {
|
|
return
|
|
}
|
|
for _, existing := range entry.Battles {
|
|
if existing.StageID == eventID {
|
|
return
|
|
}
|
|
}
|
|
entry.Battles = append(entry.Battles, MonsterSettingsBattle{StageID: eventID, Level: stage.Level, Waves: stage.Monsters})
|
|
}
|
|
addEntry := func(mode string, groupID uint32, entryID uint32, monsterIDs []uint32, events ...uint32) {
|
|
bucket, ok := acc[mode]
|
|
if !ok {
|
|
return
|
|
}
|
|
if bucket.groups[groupID] == nil {
|
|
bucket.groups[groupID] = map[uint32]*MonsterSettingsEncounter{}
|
|
}
|
|
entry := bucket.groups[groupID][entryID]
|
|
if entry == nil {
|
|
entry = &MonsterSettingsEncounter{ID: entryID}
|
|
bucket.groups[groupID][entryID] = entry
|
|
}
|
|
for _, monsterID := range monsterIDs {
|
|
if monsterID == 0 || slices.Contains(entry.MonsterIDs, monsterID) {
|
|
continue
|
|
}
|
|
entry.MonsterIDs = append(entry.MonsterIDs, monsterID)
|
|
}
|
|
for _, eventID := range events {
|
|
if eventID != 0 {
|
|
addBattle(entry, eventID)
|
|
}
|
|
}
|
|
}
|
|
modeForChallengeID := func(id uint32) string {
|
|
// These are the same challenge-ID namespaces used by
|
|
// setChallengeBattleConfigWithLimit in the game server.
|
|
switch {
|
|
case id >= 30000:
|
|
return "as"
|
|
case id >= 20000:
|
|
return "pf"
|
|
default:
|
|
return "moc"
|
|
}
|
|
}
|
|
for rawID, row := range challenges.Challenges {
|
|
id64, err := strconv.ParseUint(rawID, 10, 32)
|
|
if err != nil || id64 == 0 {
|
|
continue
|
|
}
|
|
addEntry(modeForChallengeID(uint32(id64)), row.GroupID, uint32(id64), []uint32{row.Monster1, row.Monster2}, row.Event1, row.Event2)
|
|
}
|
|
for rawID, row := range challenges.Tierce {
|
|
id64, err := strconv.ParseUint(rawID, 10, 32)
|
|
if err != nil || id64 == 0 {
|
|
continue
|
|
}
|
|
addEntry(modeForChallengeID(uint32(id64)), row.Group, uint32(id64), nil, row.Event)
|
|
}
|
|
|
|
peakGroupForStage := map[uint32]uint32{}
|
|
for rawGroupID, group := range challenges.Peak.Groups {
|
|
groupID64, err := strconv.ParseUint(rawGroupID, 10, 32)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
groupID := uint32(groupID64)
|
|
for _, stageID := range group.PreLevelIDs {
|
|
peakGroupForStage[stageID] = groupID
|
|
}
|
|
if group.BossLevelID != 0 {
|
|
peakGroupForStage[group.BossLevelID] = groupID
|
|
}
|
|
}
|
|
for rawStageID, row := range challenges.Peak.Stages {
|
|
stageID64, err := strconv.ParseUint(rawStageID, 10, 32)
|
|
if err != nil || stageID64 == 0 {
|
|
continue
|
|
}
|
|
stageID := uint32(stageID64)
|
|
groupID := peakGroupForStage[stageID]
|
|
if groupID == 0 {
|
|
continue
|
|
}
|
|
bucket := acc["aa"].groups
|
|
if bucket[groupID] == nil {
|
|
bucket[groupID] = map[uint32]*MonsterSettingsEncounter{}
|
|
}
|
|
entry := &MonsterSettingsEncounter{ID: stageID, IsBoss: row.IsBoss, DefaultBuffID: row.DefaultBuff, MonsterIDs: []uint32{row.MonsterID}}
|
|
addBattle(entry, row.EventID)
|
|
addBattle(entry, row.HardEventID)
|
|
if len(entry.Battles) != 0 {
|
|
bucket[groupID][stageID] = entry
|
|
}
|
|
}
|
|
|
|
result := MonsterSettingsCatalog{
|
|
Modes: map[string][]MonsterSettingsGroup{},
|
|
Sources: []string{"challenge_data.json", "resources/StageConfig.json"},
|
|
}
|
|
for _, mode := range []string{"moc", "pf", "as", "aa"} {
|
|
for groupID, stages := range acc[mode].groups {
|
|
group := MonsterSettingsGroup{ID: groupID}
|
|
for _, stage := range stages {
|
|
if len(stage.Battles) == 0 {
|
|
continue
|
|
}
|
|
sort.Slice(stage.Battles, func(i, j int) bool { return stage.Battles[i].StageID < stage.Battles[j].StageID })
|
|
if len(stage.MonsterIDs) == 0 {
|
|
for _, wave := range stage.Battles[0].Waves {
|
|
for _, monsterID := range wave {
|
|
if monsterID != 0 && !slices.Contains(stage.MonsterIDs, monsterID) {
|
|
stage.MonsterIDs = append(stage.MonsterIDs, monsterID)
|
|
if len(stage.MonsterIDs) == 2 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(stage.MonsterIDs) == 2 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
group.Stages = append(group.Stages, *stage)
|
|
}
|
|
if len(group.Stages) != 0 {
|
|
sort.Slice(group.Stages, func(i, j int) bool { return group.Stages[i].ID < group.Stages[j].ID })
|
|
result.Modes[mode] = append(result.Modes[mode], group)
|
|
}
|
|
}
|
|
sort.Slice(result.Modes[mode], func(i, j int) bool { return result.Modes[mode][i].ID < result.Modes[mode][j].ID })
|
|
}
|
|
for id := range monsterSet {
|
|
result.MonsterIDs = append(result.MonsterIDs, id)
|
|
}
|
|
for level := range levelSet {
|
|
result.Levels = append(result.Levels, level)
|
|
}
|
|
sort.Slice(result.MonsterIDs, func(i, j int) bool { return result.MonsterIDs[i] < result.MonsterIDs[j] })
|
|
sort.Slice(result.Levels, func(i, j int) bool { return result.Levels[i] < result.Levels[j] })
|
|
return result, nil
|
|
}
|