Files
GaCy-GO/internal/common/monster_settings_catalog.go
2026-10-28 18:54:43 +07:00

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
}