From a1b6613c6c6a2f8d6e1ecd47e766bd40f92ac295 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 28 Jun 2026 02:40:30 -0400 Subject: feat: admin accounts, god mode, OLC commands like dig/room, 'inline' objects changed to 'local' objects --- internal/world/mob.go | 17 ++++++++ internal/world/room.go | 94 +++++++++++++++++++++++++++++++++-------- internal/world/room_test.go | 34 +++++++-------- internal/world/trigger_store.go | 9 ++++ internal/world/world.go | 37 ++++++++++++++++ 5 files changed, 156 insertions(+), 35 deletions(-) (limited to 'internal/world') diff --git a/internal/world/mob.go b/internal/world/mob.go index d3ea05b..e02da53 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -289,6 +289,13 @@ func (s *MobStore) AllDefIDs() map[string]bool { return ids } +func (s *MobStore) ReloadDefs(dataDir string) { + s.mu.Lock() + defer s.mu.Unlock() + s.defs = make(map[string]*MobDef) + s.pathIndex = behavior.BuildPathIndex(filepath.Join(dataDir, "mobs")) +} + func (s *MobStore) GetInstance(id string) *MobInstance { s.mu.Lock() defer s.mu.Unlock() @@ -371,6 +378,16 @@ func (s *MobStore) RemoveInstance(instanceID string) { delete(s.instances, instanceID) } +func (s *MobStore) RemoveMobsInRoom(roomID int) { + s.mu.Lock() + defer s.mu.Unlock() + for id, inst := range s.instances { + if inst.RoomID == roomID { + delete(s.instances, id) + } + } +} + func (s *MobStore) SeedMobs(roomID int, entries []RoomMob) { type defWrapper struct { def *MobDef diff --git a/internal/world/room.go b/internal/world/room.go index 8b8830e..21e1277 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -49,10 +49,10 @@ type SpawnDef struct { type ExitDef struct { Room int `yaml:"room"` - Condition *behavior.Condition `yaml:"condition"` - BlockedMessage string `yaml:"blocked_message"` - SetFlags map[string]any `yaml:"set_flags"` - SetPlayerFlags map[string]any `yaml:"set_player_flags"` + Condition *behavior.Condition `yaml:"condition,omitempty"` + BlockedMessage string `yaml:"blocked_message,omitempty"` + SetFlags map[string]any `yaml:"set_flags,omitempty"` + SetPlayerFlags map[string]any `yaml:"set_player_flags,omitempty"` } func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { @@ -85,8 +85,8 @@ type Room struct { type RoomMob struct { ID string `yaml:"id"` - WanderRooms []int `yaml:"wander_rooms"` - WanderInterval float64 `yaml:"wander_interval"` + WanderRooms []int `yaml:"wander_rooms,omitempty"` + WanderInterval float64 `yaml:"wander_interval,omitempty"` } func (rm *RoomMob) UnmarshalYAML(value *yaml.Node) error { @@ -129,16 +129,17 @@ func (e EnterStep) IsTimed() bool { } // RoomObject is either a reference to a file-backed object definition (only -// `id`/wander fields set) or a fully inline object definition (Inline != nil). -// An entry is treated as inline when it carries any passive content field +// `id`/wander fields set) or a fully local object definition (Local != nil). +// An entry is treated as local when it carries any passive content field // (name, description, aliases, inroom_description, color, hidden, on_look). -// Inline objects are room-scoped: their identity (ID, used as the ObjState +// Local objects are room-scoped: their identity (ID, used as the ObjState // DefID) is derived from the normalized name; `id:` is reserved for references. type RoomObject struct { ID string WanderRooms []int WanderInterval float64 - Inline *object.ObjectDef + Local *object.ObjectDef + HiddenOverride bool `yaml:"hidden,omitempty"` } func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error { @@ -146,6 +147,7 @@ func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error { ID string `yaml:"id"` WanderRooms []int `yaml:"wander_rooms"` WanderInterval float64 `yaml:"wander_interval"` + Hidden bool `yaml:"hidden"` } var ref rawRef if err := value.Decode(&ref); err != nil { @@ -158,24 +160,80 @@ func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error { if err := value.Decode(&def); err != nil { return err } - inline := def.Name != "" || len(def.Description) > 0 || def.InRoomDescription != "" || - len(def.Aliases) > 0 || def.Color != "" || def.Hidden || def.OnLook != nil - if inline { - // Identity derives from the name. def.ID still holds any explicit `id:` - // the builder wrote; it is left in place only so validation can flag it - // as a stray field (runtime resolution overrides the copy's ID anyway). + isLocal := def.Name != "" || len(def.Description) > 0 || def.InRoomDescription != "" || + len(def.Aliases) > 0 || def.Color != "" || def.OnLook != nil + if isLocal { ro.ID = NormalizeObjectName(def.Name) - ro.Inline = &def + ro.Local = &def return nil } ro.ID = ref.ID + ro.HiddenOverride = ref.Hidden return nil } +func (ro RoomObject) MarshalYAML() (interface{}, error) { + if ro.Local != nil { + type inlineObj struct { + Name string `yaml:"name"` + Aliases []string `yaml:"aliases,omitempty"` + Color string `yaml:"color,omitempty"` + Hidden bool `yaml:"hidden,omitempty"` + InRoomDescription string `yaml:"inroom_description,omitempty"` + RemovalItem string `yaml:"removal_item,omitempty"` + Description behavior.DescList `yaml:"description,omitempty"` + UseInteractions []object.UseInteraction `yaml:"use_interactions,omitempty"` + StealTable string `yaml:"steal_table,omitempty"` + StealLevel int `yaml:"steal_level,omitempty"` + StealXP int `yaml:"steal_xp,omitempty"` + StealSpeed float64 `yaml:"steal_speed,omitempty"` + GuardMob string `yaml:"guard_mob,omitempty"` + Gather *behavior.GatherConfig `yaml:"gather,omitempty"` + Talk *behavior.TalkConfig `yaml:"talk,omitempty"` + Use *behavior.UseConfig `yaml:"use,omitempty"` + Safespot *object.SafespotConfig `yaml:"safespot,omitempty"` + OnLook *behavior.NodeAction `yaml:"on_look,omitempty"` + } + def := ro.Local + return inlineObj{ + Name: def.Name, + Aliases: def.Aliases, + Color: def.Color, + Hidden: def.Hidden, + InRoomDescription: def.InRoomDescription, + RemovalItem: def.RemovalItem, + Description: def.Description, + UseInteractions: def.UseInteractions, + StealTable: def.StealTable, + StealLevel: def.StealLevel, + StealXP: def.StealXP, + StealSpeed: def.StealSpeed, + GuardMob: def.GuardMob, + Gather: def.Gather, + Talk: def.Talk, + Use: def.Use, + Safespot: def.Safespot, + OnLook: def.OnLook, + }, nil + } + type rawRef struct { + ID string `yaml:"id"` + WanderRooms []int `yaml:"wander_rooms,omitempty"` + WanderInterval float64 `yaml:"wander_interval,omitempty"` + Hidden bool `yaml:"hidden,omitempty"` + } + return rawRef{ + ID: ro.ID, + WanderRooms: ro.WanderRooms, + WanderInterval: ro.WanderInterval, + Hidden: ro.HiddenOverride, + }, nil +} + // NormalizeObjectName lowercases, trims, and collapses internal whitespace, // keeping spaces (not underscores) so the word-prefix matcher treats the name // as a single token in its `_`-split fallback. It is the canonical derivation -// of an inline object's ObjState DefID from its name. +// of a local object's ObjState DefID from its name. func NormalizeObjectName(name string) string { return strings.Join(strings.Fields(strings.ToLower(name)), " ") } diff --git a/internal/world/room_test.go b/internal/world/room_test.go index 472fe4f..e7b1718 100644 --- a/internal/world/room_test.go +++ b/internal/world/room_test.go @@ -18,7 +18,7 @@ func writeRoom(t *testing.T, dir string, body string) { } } -func TestRoomObjectReferenceVsInline(t *testing.T) { +func TestRoomObjectReferenceVsLocal(t *testing.T) { dir := t.TempDir() body := `name: Test Room objects: @@ -44,37 +44,37 @@ objects: } // reference - if ref := room.Objects[0]; ref.Inline != nil || ref.ID != "workbench" { - t.Errorf("object 0 should be a reference to workbench, got %+v (inline=%v)", ref, ref.Inline) + if ref := room.Objects[0]; ref.Local != nil || ref.ID != "workbench" { + t.Errorf("object 0 should be a reference to workbench, got %+v (local=%v)", ref, ref.Local) } - // inline, id derived from the normalized name + // local, id derived from the normalized name in := room.Objects[1] - if in.Inline == nil { - t.Fatalf("object 1 should be inline") + if in.Local == nil { + t.Fatalf("object 1 should be local") } if in.ID != "window" { - t.Errorf("inline id should derive from name to %q, got %q", "window", in.ID) + t.Errorf("local id should derive from name to %q, got %q", "window", in.ID) } - if !in.Inline.Hidden || in.Inline.Name != "window" { - t.Errorf("inline fields not parsed: %+v", in.Inline) + if !in.Local.Hidden || in.Local.Name != "window" { + t.Errorf("local fields not parsed: %+v", in.Local) } - // inline with a multi-word name: id is the normalized name (spaces kept), + // local with a multi-word name: id is the normalized name (spaces kept), // and an explicit `id:` is NOT used as identity (it is preserved on the // def only so validation can flag it). ex := room.Objects[2] - if ex.Inline == nil { - t.Fatalf("object 2 should be inline") + if ex.Local == nil { + t.Fatalf("object 2 should be local") } if ex.ID != "instrument panel" { - t.Errorf("inline id should be normalized name %q, got %q", "instrument panel", ex.ID) + t.Errorf("local id should be normalized name %q, got %q", "instrument panel", ex.ID) } - if ex.Inline.ID != "control" { - t.Errorf("explicit id should be preserved on def for validation, got %q", ex.Inline.ID) + if ex.Local.ID != "control" { + t.Errorf("explicit id should be preserved on def for validation, got %q", ex.Local.ID) } - if len(ex.Inline.Aliases) != 1 || ex.Inline.Aliases[0] != "cockpit" { - t.Errorf("inline aliases not parsed: %+v", ex.Inline.Aliases) + if len(ex.Local.Aliases) != 1 || ex.Local.Aliases[0] != "cockpit" { + t.Errorf("local aliases not parsed: %+v", ex.Local.Aliases) } } diff --git a/internal/world/trigger_store.go b/internal/world/trigger_store.go index f00d256..b5fac5b 100644 --- a/internal/world/trigger_store.go +++ b/internal/world/trigger_store.go @@ -51,6 +51,15 @@ func NewTriggerStore() *TriggerStore { } } +func (ts *TriggerStore) ClearTriggers() { + ts.mu.Lock() + defer ts.mu.Unlock() + ts.globalByPlayerFlag = make(map[string][]*TriggerDef) + ts.globalByWorldFlag = make(map[string][]*TriggerDef) + ts.roomByPlayerFlag = make(map[int]map[string][]*TriggerDef) + ts.roomByWorldFlag = make(map[int]map[string][]*TriggerDef) +} + func (ts *TriggerStore) LoadGlobal(dataDir string) error { dir := filepath.Join(dataDir, "triggers") if _, err := os.Stat(dir); os.IsNotExist(err) { diff --git a/internal/world/world.go b/internal/world/world.go index ee06142..0c992ad 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -126,9 +126,46 @@ func (w *World) RoomIndex() map[int]bool { return ids } +func (w *World) RebuildRoomIndex(dataDir string) { + w.mu.Lock() + defer w.mu.Unlock() + w.roomPathIndex = behavior.BuildRoomIndex(filepath.Join(dataDir, "rooms")) +} + +func (w *World) AddRoomPath(id int, path string) { + w.mu.Lock() + defer w.mu.Unlock() + w.roomPathIndex[id] = path +} + +func (w *World) GetRoomPath(id int) (string, bool) { + path, ok := w.roomPathIndex[id] + return path, ok +} + +func (w *World) ClearHazardCache() { + w.hazardMu.Lock() + defer w.hazardMu.Unlock() + w.hazardPathIndex = nil + w.hazardDefs = make(map[string]*HazardDef) +} + func (w *World) Tick() { w.mu.Lock() defer w.mu.Unlock() w.tickGroundItems() w.tickObjStatesLocked() } + +func (w *World) ClearRoomState(roomID int) { + w.mu.Lock() + defer w.mu.Unlock() + delete(w.groundItems, roomID) + delete(w.seeded, roomID) + prefix := fmt.Sprintf("%d:", roomID) + for k := range w.objStates { + if strings.HasPrefix(k, prefix) { + delete(w.objStates, k) + } + } +} -- cgit v1.2.3