aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-28 02:40:30 -0400
committerhistoria <[not public]>2026-06-28 02:40:30 -0400
commita1b6613c6c6a2f8d6e1ecd47e766bd40f92ac295 (patch)
tree4ff8fefb029c93f2aa3db7361658b0b4e9805b66 /internal/world
parent87dacfbb3dd16e7f55a82361eace98f9a39d75c8 (diff)
downloadthehouseoficarus-a1b6613c6c6a2f8d6e1ecd47e766bd40f92ac295.tar.gz
feat: admin accounts, god mode, OLC commands like dig/room, 'inline' objects changed to 'local' objects
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/mob.go17
-rw-r--r--internal/world/room.go94
-rw-r--r--internal/world/room_test.go34
-rw-r--r--internal/world/trigger_store.go9
-rw-r--r--internal/world/world.go37
5 files changed, 156 insertions, 35 deletions
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)
+ }
+ }
+}