aboutsummaryrefslogtreecommitdiff
path: root/internal/world/objects.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-19 01:55:34 -0400
committerhistoria <[not public]>2026-09-19 01:55:34 -0400
commitcc33072eced3d5460512c09c42ec4531c807d7af (patch)
treeb1b451fa87fa389cb8343a3645372f3b43f5523e /internal/world/objects.go
parent2c38386484cdb507c4687caceb37e0b963e33594 (diff)
downloadthehouseoficarus-cc33072eced3d5460512c09c42ec4531c807d7af.tar.gz
fix: casino stake preservation, world state reconciliation, casino game registration
Diffstat (limited to 'internal/world/objects.go')
-rw-r--r--internal/world/objects.go182
1 files changed, 155 insertions, 27 deletions
diff --git a/internal/world/objects.go b/internal/world/objects.go
index fa17f08..3c004a5 100644
--- a/internal/world/objects.go
+++ b/internal/world/objects.go
@@ -10,13 +10,23 @@ import (
)
type ObjState struct {
- Depleted bool
- DepleteTimer float64
- DefID string
- Name string
- Aliases []string
- Index int
- RoomID int
+ Depleted bool
+ DepleteTimer float64
+ DefID string
+ Name string
+ Aliases []string
+ Index int
+ // RoomID is where the instance currently is; HomeRoomID is the room that
+ // declares it and never changes as it wanders. Identity (and re-seeding)
+ // is anchored to HomeRoomID so a wandering object cannot be duplicated by
+ // re-entering its home room.
+ RoomID int
+ HomeRoomID int
+ // Dynamic marks an instance created at runtime (AddObjInstance) rather
+ // than declared by the room file. ReconcileRoomState must not prune these:
+ // a live fire is not in the room's objects list and would be extinguished
+ // by any content edit.
+ Dynamic bool
JustRespawned bool
WanderRooms []int
WanderInterval float64
@@ -56,17 +66,37 @@ func (w *World) ensureObjStates(roomID int, defIDs []string) {
idx := counts[defID]
counts[defID]++
key := w.ObjStateKey(roomID, defID, idx)
- if _, exists := w.objStates[key]; !exists {
- w.objStates[key] = &ObjState{
- DefID: defID,
- Name: defID,
- Index: idx,
- RoomID: roomID,
- }
+ if _, exists := w.objStates[key]; exists {
+ continue
+ }
+ // The object may simply be away from its home room (wandering): its
+ // state lives under its current room's key, so re-seeding the home
+ // room must not create a second, independent instance.
+ if w.homeObjStateLocked(roomID, defID, idx) != nil {
+ continue
+ }
+ w.objStates[key] = &ObjState{
+ DefID: defID,
+ Name: defID,
+ Index: idx,
+ RoomID: roomID,
+ HomeRoomID: roomID,
}
}
}
+// homeObjStateLocked returns the instance whose home room is roomID and whose
+// identity is (defID, index), wherever it has currently wandered. It must be
+// called with w.mu held.
+func (w *World) homeObjStateLocked(roomID int, defID string, index int) *ObjState {
+ for _, st := range w.objStates {
+ if st.HomeRoomID == roomID && st.DefID == defID && st.Index == index {
+ return st
+ }
+ }
+ return nil
+}
+
func (w *World) AddObjInstance(roomID int, defID string, quality float64) {
w.mu.Lock()
defer w.mu.Unlock()
@@ -76,13 +106,18 @@ func (w *World) AddObjInstance(roomID int, defID string, quality float64) {
idx := 0
for {
key := w.ObjStateKey(roomID, defID, idx)
- if _, exists := w.objStates[key]; !exists {
+ // An index is free only if no instance currently occupies it AND no
+ // instance from this room has wandered away holding that index; reusing
+ // a wandered index would collide when the wanderer returns.
+ if _, exists := w.objStates[key]; !exists && w.homeObjStateLocked(roomID, defID, idx) == nil {
w.objStates[key] = &ObjState{
- DefID: defID,
- Name: defID,
- Index: idx,
- RoomID: roomID,
- Quality: quality,
+ DefID: defID,
+ Name: defID,
+ Index: idx,
+ RoomID: roomID,
+ HomeRoomID: roomID,
+ Dynamic: true,
+ Quality: quality,
}
return
}
@@ -90,17 +125,108 @@ func (w *World) AddObjInstance(roomID int, defID string, quality float64) {
}
}
-func (w *World) RemoveObjInstance(roomID int, defID string) {
+// ReconcileRoomState brings a room's object instances and item-spawn ground
+// entries in line with a (possibly just-edited) room definition, without a
+// restart. Player-dropped ground items, dynamic object instances (fires), and
+// live reservations are preserved. Mob instances live in MobStore and are
+// reconciled by the game layer, which owns that store.
+func (w *World) ReconcileRoomState(roomID int, room *Room) {
w.mu.Lock()
defer w.mu.Unlock()
- idx := 0
- for {
- key := w.ObjStateKey(roomID, defID, idx)
- if _, exists := w.objStates[key]; !exists {
- return
+ if w.objStates == nil {
+ w.objStates = make(map[string]*ObjState)
+ }
+
+ // Prune room-declared instances the definition no longer declares. Prune
+ // by home room so a wandered instance is reconciled where it belongs, and
+ // never prune a dynamic instance.
+ want := make(map[string]int, len(room.Objects))
+ for _, ro := range room.Objects {
+ want[ro.ID]++
+ }
+ for key, st := range w.objStates {
+ if st.Dynamic || st.HomeRoomID != roomID {
+ continue
+ }
+ if st.Index >= want[st.DefID] {
+ delete(w.objStates, key)
}
+ }
+
+ // Re-create declared instances that are missing (including ones that had
+ // wandered away; ensureObjStates recognises them by HomeRoomID).
+ defIDs := make([]string, 0, len(room.Objects))
+ for _, ro := range room.Objects {
+ defIDs = append(defIDs, ro.ID)
+ }
+ w.ensureObjStates(roomID, defIDs)
+
+ // Ground spawns are only reconciled after the room has been seeded: before
+ // that, the first entry seeds from the definition anyway, and adding
+ // entries here would make SeedGroundItems double the quantity.
+ if w.seeded[roomID] {
+ w.reconcileGroundSpawnsLocked(roomID, room.ItemSpawns)
+ }
+}
+
+// reconcileGroundSpawnsLocked adds entries for newly declared item spawns and
+// drops entries for spawns the room no longer declares, leaving the live
+// quantity of existing spawns (and every dropped item) untouched. It must be
+// called with w.mu held.
+func (w *World) reconcileGroundSpawnsLocked(roomID int, spawns []SpawnDef) {
+ declared := make(map[string]bool, len(spawns))
+ for _, s := range spawns {
+ declared[strings.ToLower(s.ID)] = true
+ }
+ kept := w.groundItems[roomID][:0]
+ for _, e := range w.groundItems[roomID] {
+ if e.isSpawn && !declared[strings.ToLower(e.itemID)] {
+ continue
+ }
+ kept = append(kept, e)
+ }
+ w.groundItems[roomID] = kept
+ for _, s := range spawns {
+ found := false
+ for _, e := range w.groundItems[roomID] {
+ if e.isSpawn && strings.EqualFold(e.itemID, s.ID) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ w.groundItems[roomID] = append(w.groundItems[roomID], &groundEntry{
+ itemID: s.ID,
+ quantity: s.Quantity,
+ isSpawn: true,
+ respawnDelay: spawnRespawnDelay(s.RespawnTicks),
+ respawnQty: s.Quantity,
+ })
+ }
+ }
+}
+
+// RemoveObjInstanceIndex removes exactly one instance by identity
+// (defID, index), looking it up by its current room first (roomID) and
+// falling back to its home room in case it has wandered away. A room may
+// legitimately declare the same object twice, so the index names which one;
+// removing every instance of a defID would destroy a sibling.
+func (w *World) RemoveObjInstanceIndex(roomID int, defID string, index int) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ if w.objStates == nil {
+ return
+ }
+ key := w.ObjStateKey(roomID, defID, index)
+ if st, ok := w.objStates[key]; ok && st.DefID == defID && st.Index == index {
delete(w.objStates, key)
- idx++
+ return
+ }
+ for k, st := range w.objStates {
+ if st.DefID == defID && st.Index == index && st.HomeRoomID == roomID {
+ delete(w.objStates, k)
+ return
+ }
}
}
@@ -170,6 +296,8 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState {
Aliases: st.Aliases,
Index: st.Index,
RoomID: st.RoomID,
+ HomeRoomID: st.HomeRoomID,
+ Dynamic: st.Dynamic,
Depleted: st.Depleted,
DepleteTimer: st.DepleteTimer,
SharedMax: st.SharedMax,