diff options
Diffstat (limited to 'internal/world/objects.go')
| -rw-r--r-- | internal/world/objects.go | 182 |
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, |
