aboutsummaryrefslogtreecommitdiff
path: root/internal/game/core_startup.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 15:40:48 -0400
committerhistoria <[not public]>2026-06-25 15:40:48 -0400
commitabd612c15799f604e671e83dc7c410ed2b44185f (patch)
tree0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/game/core_startup.go
parent2725e2927a1595c7b100d942d1f14146252adeb7 (diff)
downloadthehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz
slop refactor
Diffstat (limited to 'internal/game/core_startup.go')
-rw-r--r--internal/game/core_startup.go887
1 files changed, 0 insertions, 887 deletions
diff --git a/internal/game/core_startup.go b/internal/game/core_startup.go
deleted file mode 100644
index 2e81eac..0000000
--- a/internal/game/core_startup.go
+++ /dev/null
@@ -1,887 +0,0 @@
-package game
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "sort"
- "strings"
-
- "thehouseoficarus/internal/action"
- "thehouseoficarus/internal/world"
-)
-
-type StartupIssue struct {
- Level string
- Type string
- Message string
-}
-
-func (g *Game) ValidateStartup() []StartupIssue {
- var issues []StartupIssue
-
- issues = append(issues, validateIDs(g.DataDir, "items", "item")...)
- issues = append(issues, validateIDs(g.DataDir, "rooms", "room")...)
- issues = append(issues, validateIDs(g.DataDir, "mobs", "mob")...)
- issues = append(issues, validateIDs(g.DataDir, "objects", "object")...)
- issues = append(issues, validateIDs(g.DataDir, "drops", "drop table")...)
- issues = append(issues, validateIDs(g.DataDir, "courses", "course")...)
- issues = append(issues, validateIDs(g.DataDir, "modules", "module")...)
- issues = append(issues, validateIDs(g.DataDir, "techs", "tech")...)
- issues = append(issues, validateIDs(g.DataDir, "help", "help topic")...)
-
- issues = append(issues, validateRooms(g)...)
- issues = append(issues, validateMobs(g)...)
- issues = append(issues, validateHazards(g)...)
- issues = append(issues, validateObjects(g)...)
- issues = append(issues, validateItems(g)...)
- issues = append(issues, validateCraftBlocks(g)...)
- issues = append(issues, validateDropTables(g)...)
- issues = append(issues, validateCourses(g)...)
- issues = append(issues, validateTechs(g)...)
- issues = append(issues, validateRoomWiring(g)...)
-
- return issues
-}
-
-func validateIDs(dataDir, subDir, label string) []StartupIssue {
- dir := filepath.Join(dataDir, subDir)
- dups := action.CheckDuplicateIDs(dir)
- var issues []StartupIssue
- for _, d := range dups {
- short := make([]string, len(d.Paths))
- for i, p := range d.Paths {
- short[i] = strings.TrimPrefix(p, dir+"/")
- }
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "duplicate",
- Message: fmt.Sprintf("Duplicate %s ID %q found in: %s",
- label, d.ID, strings.Join(short, ", ")),
- })
- }
- return issues
-}
-
-func validateRooms(g *Game) []StartupIssue {
- var issues []StartupIssue
- roomIndex := g.World.RoomIndex()
- itemIDs := g.ItemStore.IDSet()
- mobIDs := g.MobStore.AllDefIDs()
- objIDs := g.ObjectStore.IDSet()
- hazardIDs := g.World.HazardIndex()
-
- for id := range roomIndex {
- room, err := g.World.LoadRoom(id)
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: failed to load: %v", id, err),
- })
- continue
- }
- if room.Name == "" {
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: has no name", id),
- })
- }
-
- for dir, exit := range room.Exits {
- switch dir {
- case world.North, world.South, world.East, world.West, world.Up, world.Down:
- default:
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: invalid exit direction %q (only north/south/east/west/up/down supported)", id, dir),
- })
- continue
- }
- if exit.Room <= 0 {
- continue
- }
- if _, ok := roomIndex[exit.Room]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: exit %q targets nonexistent room %d",
- id, dir, exit.Room),
- })
- }
- }
-
- for _, rm := range room.Mobs {
- if rm.ID == "" {
- continue
- }
- if _, ok := mobIDs[rm.ID]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: references nonexistent mob %q",
- id, rm.ID),
- })
- }
- for _, wr := range rm.WanderRooms {
- if _, ok := roomIndex[wr]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: mob %q wander_rooms references nonexistent room %d",
- id, rm.ID, wr),
- })
- }
- }
- }
-
- for _, s := range room.ItemSpawns {
- if s.ID != "" && !itemIDs[s.ID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: spawns nonexistent item %q",
- id, s.ID),
- })
- }
- }
-
- for _, robj := range room.Objects {
- if robj.ID != "" && !objIDs[robj.ID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: references nonexistent object %q",
- id, robj.ID),
- })
- }
- for _, wr := range robj.WanderRooms {
- if _, ok := roomIndex[wr]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: object %q wander_rooms references nonexistent room %d",
- id, robj.ID, wr),
- })
- }
- }
- }
-
- if room.Hazard != "" && !hazardIDs[room.Hazard] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: references nonexistent hazard %q",
- id, room.Hazard),
- })
- }
- }
-
- return issues
-}
-
-func validateMobs(g *Game) []StartupIssue {
- var issues []StartupIssue
- itemIDs := g.ItemStore.IDSet()
- dropIDs := dropTableIDSet(g.DataDir)
- mobIDs := g.MobStore.AllDefIDs()
-
- for id := range mobIDs {
- def, err := g.MobStore.LoadDef(id)
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: failed to load: %v", id, err),
- })
- continue
- }
- if def.Name == "" {
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: has no name", id),
- })
- }
-
- if def.Kind != "" && def.Kind != "combat" && def.Kind != "task" {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: invalid kind %q (must be \"combat\" or \"task\")", id, def.Kind),
- })
- }
-
- if def.Drops.Remains != "" && !itemIDs[def.Drops.Remains] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: drops remains (item %q) does not exist",
- id, def.Drops.Remains),
- })
- }
-
- for _, d := range def.Drops.Loot {
- if d.ItemID != "" && !itemIDs[d.ItemID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: loot item %q does not exist",
- id, d.ItemID),
- })
- }
- if d.Table != "" && !dropIDs[d.Table] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: loot table %q does not exist",
- id, d.Table),
- })
- }
- }
-
- if def.StealTable != "" && !dropIDs[def.StealTable] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: steal_table %q does not exist",
- id, def.StealTable),
- })
- }
-
- if def.FinishingBlow != "" && !itemIDs[def.FinishingBlow] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Mob %q: finishing_blow item %q does not exist",
- id, def.FinishingBlow),
- })
- }
- }
-
- return issues
-}
-
-func validateHazards(g *Game) []StartupIssue {
- var issues []StartupIssue
- itemIDs := g.ItemStore.IDSet()
- validTypes := map[string]bool{"stab": true, "slash": true, "crush": true, "ranged": true, "science": true}
-
- for id := range g.World.HazardIndex() {
- def, err := g.World.LoadHazard(id)
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Hazard %q: failed to load: %v", id, err),
- })
- continue
- }
- if def.AttackType != "" && !validTypes[def.AttackType] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Hazard %q: invalid attack_type %q (must be stab/slash/crush/ranged/science)", id, def.AttackType),
- })
- }
- if def.RequiredItem != "" && !itemIDs[def.RequiredItem] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Hazard %q: required_item %q does not exist", id, def.RequiredItem),
- })
- }
- }
-
- return issues
-}
-
-func validateObjects(g *Game) []StartupIssue {
- var issues []StartupIssue
- objIDs := g.ObjectStore.IDSet()
- itemIDs := g.ItemStore.IDSet()
- mobIDs := g.MobStore.AllDefIDs()
- dropIDs := dropTableIDSet(g.DataDir)
- roomIndex := g.World.RoomIndex()
-
- for id := range objIDs {
- obj, err := g.ObjectStore.Load(id)
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Object %q: failed to load: %v", id, err),
- })
- continue
- }
- if obj.Name == "" {
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "reference",
- Message: fmt.Sprintf("Object %q: has no name", id),
- })
- }
-
- if obj.RemovalItem != "" && !itemIDs[obj.RemovalItem] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Object %q: removal_item %q does not exist",
- id, obj.RemovalItem),
- })
- }
-
- if obj.StealTable != "" && !dropIDs[obj.StealTable] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Object %q: steal_table %q does not exist",
- id, obj.StealTable),
- })
- }
-
- if obj.GuardMob != "" && !mobIDs[obj.GuardMob] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Object %q: guard_mob %q does not exist",
- id, obj.GuardMob),
- })
- }
-
- for _, ui := range obj.UseInteractions {
- if ui.Item != "" && !itemIDs[ui.Item] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Object %q: use_interaction item %q does not exist",
- id, ui.Item),
- })
- }
- if ui.Action != nil {
- issues = append(issues, validateNodeAction(fmt.Sprintf("Object %q: use_interaction", id), ui.Action, itemIDs, roomIndex)...)
- }
- }
-
- if obj.Gather != nil {
- issues = append(issues, validateGather(fmt.Sprintf("Object %q: gather", id), obj.Gather, itemIDs, dropIDs)...)
- }
-
- if obj.Talk != nil {
- issues = append(issues, validateTalkConfig(fmt.Sprintf("Object %q: talk", id), obj.Talk, itemIDs, roomIndex)...)
- }
-
- if obj.Use != nil {
- issues = append(issues, validateUseConfig(fmt.Sprintf("Object %q: use", id), obj.Use, itemIDs)...)
- }
- }
-
- return issues
-}
-
-func validateItems(g *Game) []StartupIssue {
- var issues []StartupIssue
- itemIDs := g.ItemStore.IDSet()
- dropIDs := dropTableIDSet(g.DataDir)
-
- for id := range itemIDs {
- def, err := g.ItemStore.Load(id)
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q: failed to load: %v", id, err),
- })
- continue
- }
- if def.Name == "" {
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "reference",
- Message: fmt.Sprintf("Item %q: has no name", id),
- })
- }
-
- if def.SearchTable != "" && !dropIDs[def.SearchTable] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q: search_table %q does not exist",
- id, def.SearchTable),
- })
- }
-
- if def.SearchMiscTable != "" && !dropIDs[def.SearchMiscTable] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q: search_misc_table %q does not exist",
- id, def.SearchMiscTable),
- })
- }
-
- if def.FarmProduct != "" && !itemIDs[def.FarmProduct] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q (seed): farm_product %q does not exist",
- id, def.FarmProduct),
- })
- }
- }
-
- return issues
-}
-
-func validateCraftBlocks(g *Game) []StartupIssue {
- var issues []StartupIssue
- itemIDs := g.ItemStore.IDSet()
- objIDs := g.ObjectStore.IDSet()
-
- allItems, err := g.ItemStore.LoadAll()
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Failed to load items: %v", err),
- })
- return issues
- }
-
- for _, item := range allItems {
- if len(item.Craft) == 0 {
- continue
- }
- for ci := range item.Craft {
- c := &item.Craft[ci]
-
- for _, e := range c.Consume {
- for _, consumeItem := range e.Items {
- if consumeItem != "" && !itemIDs[consumeItem] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q (craft: %s): consumes nonexistent item %q",
- item.ID, c.Type, consumeItem),
- })
- }
- }
- }
-
- if c.Fail != "" && !itemIDs[c.Fail] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q (craft: %s): fail product %q does not exist",
- item.ID, c.Type, c.Fail),
- })
- }
-
- for _, sid := range c.Station {
- if sid != "" && !objIDs[sid] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Item %q (craft: %s): station object %q does not exist",
- item.ID, c.Type, sid),
- })
- }
- }
- }
- }
-
- return issues
-}
-
-func validateDropTables(g *Game) []StartupIssue {
- var issues []StartupIssue
- dropIDs := dropTableIDSet(g.DataDir)
- itemIDs := g.ItemStore.IDSet()
-
- for id := range dropIDs {
- dt, err := action.LoadDropTable(g.DataDir, id)
- if err != nil {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Drop table %q: failed to load: %v", id, err),
- })
- continue
- }
- for _, d := range dt.Drops {
- if d.ItemID != "" && !itemIDs[d.ItemID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Drop table %q: drops nonexistent item %q",
- id, d.ItemID),
- })
- }
- if d.Table != "" && !dropIDs[d.Table] && d.Table != id {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Drop table %q: references nonexistent sub-table %q",
- id, d.Table),
- })
- }
- }
- }
-
- return issues
-}
-
-func validateCourses(g *Game) []StartupIssue {
- var issues []StartupIssue
- roomIndex := g.World.RoomIndex()
-
- courses := g.CourseStore.AllCourses()
- for id, cfg := range courses {
- if cfg.StartRoom > 0 {
- if _, ok := roomIndex[cfg.StartRoom]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Course %q: start_room %d does not exist",
- id, cfg.StartRoom),
- })
- }
- }
- for _, obs := range cfg.Obstacles {
- if _, ok := roomIndex[obs.RoomID]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Course %q: obstacle room %d does not exist",
- id, obs.RoomID),
- })
- }
- }
- }
-
- return issues
-}
-
-func validateRoomWiring(g *Game) []StartupIssue {
- var issues []StartupIssue
- roomIndex := g.World.RoomIndex()
- cfg := g.ValidationConfig
-
- sourceSet := make(map[int]bool)
- for _, s := range cfg.CheckSources {
- sourceSet[s] = true
- }
-
- ignoreTo := make(map[int]bool)
- for _, r := range cfg.IgnoreUnreachable {
- ignoreTo[r] = true
- }
-
- hasExitTo := make(map[int]bool)
- for id := range roomIndex {
- room, err := g.World.LoadRoom(id)
- if err != nil {
- continue
- }
- for _, exit := range room.Exits {
- if exit.Room > 0 {
- hasExitTo[exit.Room] = true
- }
- }
- }
-
- var orphans []int
- for id := range roomIndex {
- if ignoreTo[id] {
- continue
- }
- if sourceSet[id] {
- continue
- }
- if !hasExitTo[id] {
- orphans = append(orphans, id)
- }
- }
- if len(orphans) > 0 {
- sort.Ints(orphans)
- var strs []string
- for _, id := range orphans {
- strs = append(strs, fmt.Sprintf("%d", id))
- }
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "integrity",
- Message: fmt.Sprintf("Orphan rooms (no exit leads to them): %s",
- strings.Join(strs, ", ")),
- })
- }
-
- return issues
-}
-
-func validateGather(prefix string, cfg *action.GatherConfig, itemIDs map[string]bool, dropIDs map[string]bool) []StartupIssue {
- var issues []StartupIssue
-
- if cfg.Bait != "" && !itemIDs[cfg.Bait] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: bait item %q does not exist",
- prefix, cfg.Bait),
- })
- }
-
- for _, d := range cfg.Drops {
- if d.ItemID != "" && !itemIDs[d.ItemID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: drop item %q does not exist",
- prefix, d.ItemID),
- })
- }
- if d.Table != "" && !dropIDs[d.Table] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: drop table %q does not exist",
- prefix, d.Table),
- })
- }
- }
-
- return issues
-}
-
-func validateTalkConfig(prefix string, cfg *action.TalkConfig, itemIDs map[string]bool, roomIndex map[int]bool) []StartupIssue {
- var issues []StartupIssue
- if cfg == nil {
- return issues
- }
- for nodeID, node := range cfg.Nodes {
- nodePrefix := fmt.Sprintf("%s: node %q", prefix, nodeID)
- if node.Action != nil {
- issues = append(issues, validateNodeAction(nodePrefix, node.Action, itemIDs, roomIndex)...)
- }
- }
- return issues
-}
-
-func validateUseConfig(prefix string, cfg *action.UseConfig, itemIDs map[string]bool) []StartupIssue {
- var issues []StartupIssue
- if cfg == nil {
- return issues
- }
-
- for itemID := range cfg.Consume {
- if itemID != "" && !itemIDs[itemID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: consumes nonexistent item %q",
- prefix, itemID),
- })
- }
- }
-
- if cfg.Reward.ItemID != "" && !itemIDs[cfg.Reward.ItemID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: reward item %q does not exist",
- prefix, cfg.Reward.ItemID),
- })
- }
-
- return issues
-}
-
-func validateNodeAction(prefix string, na *action.NodeAction, itemIDs map[string]bool, roomIndex map[int]bool) []StartupIssue {
- var issues []StartupIssue
- if na == nil {
- return issues
- }
-
- if na.GiveItem != "" && !itemIDs[na.GiveItem] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: give_item %q does not exist",
- prefix, na.GiveItem),
- })
- }
-
- if na.TakeItem != "" && !itemIDs[na.TakeItem] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: take_item %q does not exist",
- prefix, na.TakeItem),
- })
- }
-
- if na.Teleport > 0 {
- if _, ok := roomIndex[na.Teleport]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: teleport to nonexistent room %d",
- prefix, na.Teleport),
- })
- }
- }
-
- if na.Shop != nil {
- for _, si := range na.Shop.Items {
- if si.ItemID != "" && !itemIDs[si.ItemID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: shop item %q does not exist",
- prefix, si.ItemID),
- })
- }
- }
- }
-
- return issues
-}
-
-var knownTechCategories = map[string]bool{
- "accuracy": true,
- "strength": true,
- "defense": true,
- "ranged": true,
- "science": true,
- "protection": true,
- "utility": true,
- "combo": true,
-}
-
-// Reserved tech IDs whose semantics are coupled to code logic —
-// damageAfterTechProtection maps attack types to protect_melee/ranged/science
-// and killPlayer checks for retribution. These IDs must exist in YAML.
-var reservedTechIDs = map[string]bool{
- "protect_melee": true,
- "protect_ranged": true,
- "protect_science": true,
- "retribution": true,
-}
-
-func validateTechs(g *Game) []StartupIssue {
- var issues []StartupIssue
- seen := make(map[string]bool)
- for _, def := range AllTechs {
- if def.ID == "" {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: "Tech has empty id",
- })
- continue
- }
- if seen[def.ID] {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "duplicate",
- Message: fmt.Sprintf("Duplicate tech %q", def.ID),
- })
- }
- seen[def.ID] = true
-
- if def.Name == "" {
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "reference",
- Message: fmt.Sprintf("Tech %q: has no name", def.ID),
- })
- }
- if def.Category != "" && !knownTechCategories[def.Category] {
- issues = append(issues, StartupIssue{
- Level: "WARN",
- Type: "reference",
- Message: fmt.Sprintf("Tech %q: unknown category %q", def.ID, def.Category),
- })
- }
- if def.DrainRate < 0 {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Tech %q: negative drain_rate %.2f", def.ID, def.DrainRate),
- })
- }
- }
-
- for id := range reservedTechIDs {
- if _, ok := techByID[id]; !ok {
- issues = append(issues, StartupIssue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Reserved tech %q does not exist — required by code (damage protection / retribution)", id),
- })
- }
- }
-
- return issues
-}
-
-func dropTableIDSet(dataDir string) map[string]bool {
- dir := filepath.Join(dataDir, "drops")
- ids := make(map[string]bool)
- action.WalkYAMLDir(dir, func(path, id string, data []byte) error {
- ids[id] = true
- return nil
- })
- return ids
-}
-
-func (cs *CourseStore) AllCourses() map[string]*CourseConfig {
- cs.mu.Lock()
- defer cs.mu.Unlock()
- if !cs.loaded {
- cs.loadAllLocked()
- }
- return cs.courses
-}
-
-func (g *Game) ValidateAndLog() {
- LogStartupIssues(g.ValidateStartup())
-}
-
-func LogStartupIssues(issues []StartupIssue) {
- if len(issues) == 0 {
- fmt.Fprintf(os.Stderr, "Startup validation: OK\n")
- return
- }
-
- errors := 0
- warns := 0
- for _, issue := range issues {
- if issue.Level == "ERROR" {
- errors++
- } else {
- warns++
- }
- }
-
- fmt.Fprintf(os.Stderr, "\n=== Startup Validation: %d error(s), %d warning(s) ===\n\n", errors, warns)
-
- sort.Slice(issues, func(i, j int) bool {
- if issues[i].Level != issues[j].Level {
- return issues[i].Level == "ERROR"
- }
- return issues[i].Message < issues[j].Message
- })
-
- for _, issue := range issues {
- tag := fmt.Sprintf("[%s]", issue.Level)
- fmt.Fprintf(os.Stderr, " %-7s %s\n", tag, issue.Message)
- }
- fmt.Fprintln(os.Stderr)
-
- if errors > 0 {
- fmt.Fprintf(os.Stderr, "*** %d startup errors detected. The server may behave unexpectedly. ***\n", errors)
- }
-}