Class: Lich::Common::GameObj
- Inherits:
-
Object
- Object
- Lich::Common::GameObj
- Defined in:
- documented/common/gameobj.rb
Overview
Represents a game object (NPC, loot, inventory item, room description, etc.) within the Lich game automation framework.
GameObj tracks all in-game entities across categorized class-level registries
(loot, NPCs, PCs, inventory, room descriptions, familiar counterparts, and hands).
Each registry deduplicates entries by the composite key of id, name, and noun.
Direct Known Subclasses
Constant Summary collapse
- @@loot =
Class-level registries
[]
- @@npcs =
[]
- @@npc_status =
{}
- @@pcs =
[]
- @@pc_status =
{}
- @@inv =
[]
- @@reserve =
nil- @@contents =
{}
- @@right_hand =
nil- @@left_hand =
nil- @@room_desc =
[]
- @@fam_loot =
[]
- @@fam_npcs =
[]
- @@fam_pcs =
[]
- @@fam_room_desc =
[]
- @@type_data =
{}
- @@type_cache =
{}
- @@sellable_data =
{}
- @@index =
Shared identity index - single persistent O(1) lookup pool with TTL.
Maps composite key String "id|noun|name" to a two-element array [GameObj, last_seen_at] where
last_seen_atis a Float timestamp (fromProcess.clock_gettime(Process::CLOCK_MONOTONIC)) recording when the entry was last accessed byfind_or_create.All registries share this one index because a GameObj with the same id, noun, and name is the same logical game entity regardless of which registry it belongs to.
The index is intentionally not flushed when a registry is cleared. Room transitions call multiple
clear_*methods in quick succession; flushing on every clear would cause every re-encountered object to be needlessly reallocated. Instead, stale index entries self-heal: whenfind_or_createfinds an entry for an object that was cleared from its registry, it simply re-adds that same instance to the target registry and refreshes itslast_seen_attimestamp.Garbage collection is handled by
prune_index!, which removes entries whoselast_seen_atis older than a given TTL (default 15 minutes). Call it at natural session breakpoints (e.g. after a room transition or from a script's idle loop). It is safe to call frequently - entries that were just accessed will never be pruned regardless of how often it runs.Use
index_statsto inspect the current state of the index at any time.For very long automated sessions an alternative
LruIndexdrop-in is also available - seeLich::Common::LruIndexbelow. {}
- @@index_mutex =
Serializes structural access to @@index (inserts and the delete_if sweeps). The index is mutated from at least two threads - the game parser thread on every object insert, and the MemoryReleaser background worker via
prune_index!- so without this a sweep could iterate the hash while another thread inserts a key, raising "can't add a new key into hash during iteration". Held only around the structural operations, never while computing the live-object set. Mutex.new
- @@staging_inv =
Staging buffers for atomic registry refresh.
While a stream or component is being parsed, incoming objects accumulate in a private staging buffer instead of the published registry. A matching
commit_*swaps the buffer in with a single reference assignment, so a reader never observes an empty or half-filled registry mid-stream - it sees the previous complete snapshot until commit, then the new one.Each buffer is
nilwhen no refresh is in flight and an Array (or Hash, for the paired status maps) while one is open. This mirrors the existing@dr_active_spells_tmpswap-on-prompt pattern in xmlparser.rb.Under MRI the reference swap is atomic and the reader's
dupis consistent, so no lock is required. nil- @@staging_reserve =
nil- @@staging_loot =
nil- @@staging_npcs =
nil- @@staging_npc_status =
nil- @@staging_pcs =
nil- @@staging_pc_status =
nil- @@staging_room_desc =
nil- @@staging_fam_room_desc =
nil- @@staging_fam_loot =
nil- @@staging_fam_npcs =
nil- @@staging_fam_pcs =
nil- @@staging_contents =
{}
- @@staging_all_contents =
Dedicated buffer for a full-replacement container refresh (DR
INV LIST), kept separate from@@staging_contentsso the whole-buffer swap/discard of a full refresh can never clobber an unrelated per-containerbegin_containerrefresh that happens to be in flight at the same time. {}
- @@staging_all_containers =
True while a full-replacement container refresh (DR
INV LIST) is open. While set,new_invroutes container placements not already owned by an open per-container refresh into@@staging_all_contentsinstead of the live@@contents, so an interrupted listing never mutates the published model. See begin_all_containers. false- @@pending_metadata =
Deferred +before_name+/+after_name+ refreshes for objects observed while a staged registry refresh is open.
find_or_createreturns the shared, still-published instance, so mutating its metadata mid-refresh would leak an uncommitted value to readers and could not be rolled back if the refresh is discarded. Instead the change is recorded here and applied by the matchingcommit_*(see apply_pending_metadata), or dropped by every discard path (+abort_*+, delete_container, clear_all_containers, an +begin_inv+/+begin_container+ restart, discard_inv_refresh, and discard_staged_refreshes).Scoped PER staging buffer: the outer hash is keyed by the buffer Array's identity (+compare_by_identity+ -- two empty Arrays are
==, so content keying would collide), the inner by GameObj identity. Buffer scoping is what keeps two concurrently-open refreshes of the same object from overwriting each other's pending values and lets a discard drop exactly its own buffer's entries and no others. {}.compare_by_identity
Instance Attribute Summary collapse
-
#after_name ⇒ String?
Text appended after the name in full display.
-
#before_name ⇒ String?
Text prepended before the name in full display.
-
#id ⇒ String
readonly
The unique string ID of this object.
-
#name ⇒ String?
Full descriptive name (e.g. "a snarling goblin").
-
#noun ⇒ String?
Noun used to refer to this object (e.g. "goblin").
Class Method Summary collapse
-
.[](val) ⇒ GameObj?
Finds a GameObj by ID (numeric string), noun (single word), or name.
-
.abort_container(container_id) ⇒ void
Discards one container's open staging buffer WITHOUT publishing it, leaving the previously published
@@contents[id]visible. -
.abort_inv ⇒ void
Discards an open
@@invstaging buffer WITHOUT publishing it, leaving the previously published@@invvisible. -
.apply_pending_metadata(buffer) ⇒ void
Applies the metadata deferred for one staging
bufferto its objects and clears that buffer's pending entries. -
.begin_all_containers ⇒ void
Opens a full-replacement refresh of ALL container contents, used by DR's
INV LIST(a complete recursive scrape). -
.begin_container(container_id) ⇒ Array
Opens a refresh of a single container's contents.
-
.begin_familiar ⇒ void
Opens a refresh of the four familiar registries.
-
.begin_inv ⇒ Array
Opens a refresh of worn inventory.
- .begin_reserve ⇒ Array
- .begin_room_desc ⇒ Array
-
.begin_room_objs ⇒ void
Opens a refresh of the room object registries (loot + npcs + npc status).
-
.begin_room_players ⇒ void
Opens a refresh of the room player registries (pcs + pc status).
-
.clear_all_containers ⇒ void
Clears all container registries.
-
.clear_container(container_id) ⇒ Array
Resets a single container's contents to an empty array.
- .clear_fam_loot ⇒ void
- .clear_fam_npcs ⇒ void
- .clear_fam_pcs ⇒ void
- .clear_fam_room_desc ⇒ void
- .clear_inv ⇒ void
- .clear_loot ⇒ void
- .clear_npcs ⇒ void
- .clear_pcs ⇒ void
- .clear_reserve ⇒ void
- .clear_room_desc ⇒ void
-
.commit_all_containers ⇒ void
Publishes every open container staging buffer.
-
.commit_all_containers_full ⇒ void
Publishes a full container refresh opened by GameObj.begin_all_containers with a single reference swap and closes it.
- .commit_container(container_id) ⇒ void
- .commit_familiar ⇒ void
- .commit_inv ⇒ void
- .commit_reserve ⇒ void
- .commit_room_desc ⇒ void
- .commit_room_objs ⇒ void
- .commit_room_players ⇒ void
-
.container_refresh_open?(container_id) ⇒ Boolean
Whether a staged refresh for one container's contents is currently open.
-
.containers ⇒ Hash{String => Array<GameObj>}
Returns a snapshot of all container contents: the outer hash and every inner array are duplicated, so a caller iterating
containers[id]is not affected by concurrent in-place mutation of the live registry (e.g. a hand pickup'sremove_inv_itemreject!, or +new_inv+'spush). -
.dead ⇒ Array<GameObj>?
Returns all NPCs with a status of
"dead", ornilif none. -
.delete_container(container_id) ⇒ GameObj?
Removes a container and all its contents from the registry.
-
.discard_inv_refresh ⇒ void
Discards an in-flight INV LIST refresh (worn + full container staging) without publishing it, leaving the previously published model visible.
-
.discard_staged_refreshes ⇒ void
Discards every in-flight staged refresh without publishing it.
-
.drop_pending_metadata(buffer) ⇒ void
Drops the metadata deferred for one staging
bufferwithout applying it -- the abort counterpart to GameObj.apply_pending_metadata. - .fam_loot ⇒ Array<GameObj>?
- .fam_npcs ⇒ Array<GameObj>?
- .fam_pcs ⇒ Array<GameObj>?
- .fam_room_desc ⇒ Array<GameObj>?
-
.hidden_targets ⇒ Array<String>
Returns IDs in the current target list that do not correspond to a known NPC.
-
.index_or_create(id, noun, name, before = nil, after = nil) ⇒ GameObj
Looks up an existing
GameObjin the shared identity index by composite key (+id+,noun,name), or creates and indexes a new one. -
.index_stats(verbose: false) ⇒ Hash
Returns a Hash describing the current memory and age state of the index.
- .inv ⇒ Array<GameObj>?
-
.inv_refresh_open? ⇒ Boolean
Whether a top-level inventory (+@@inv+) staged refresh is currently open.
- .left_hand ⇒ Array<GameObj>?
-
.load_data(filename = nil) ⇒ Boolean
Loads type and sellable classification data from XML.
- .loot ⇒ Array<GameObj>?
-
.merge_data(existing, new_value) ⇒ Regexp
Merges two Regexp values via
Regexp.union, or returns the new value if the existing one is not yet a Regexp. -
.new_fam_loot(id, noun, name) ⇒ GameObj
Creates and registers a new familiar loot object.
-
.new_fam_npc(id, noun, name) ⇒ GameObj
Creates and registers a new familiar NPC.
-
.new_fam_pc(id, noun, name) ⇒ GameObj
Creates and registers a new familiar PC.
-
.new_fam_room_desc(id, noun, name) ⇒ GameObj
Creates and registers a new familiar room description object.
-
.new_inv(id, noun, name, container = nil, before = nil, after = nil) ⇒ GameObj
Creates and registers a new inventory item, optionally in a container.
-
.new_left_hand(id, noun, name) ⇒ GameObj
Sets the left-hand object, replacing any existing one.
-
.new_loot(id, noun, name) ⇒ GameObj
Creates and registers a new loot object.
-
.new_npc(id, noun, name, status = nil) ⇒ GameObj
Creates and registers a new NPC.
-
.new_pc(id, noun, name, status = nil) ⇒ GameObj
Creates and registers a new PC.
-
.new_reserve(id, noun, name) ⇒ GameObj
Creates and registers a new reserve slot item.
-
.new_right_hand(id, noun, name) ⇒ GameObj
Sets the right-hand object, replacing any existing one.
-
.new_room_desc(id, noun, name) ⇒ GameObj
Creates and registers a new room description object.
- .npcs ⇒ Array<GameObj>?
- .pcs ⇒ Array<GameObj>?
-
.prune_index!(ttl: 900, verbose: false) ⇒ Hash
Removes entries from the shared identity index whose
last_seen_attimestamp is older thanttlseconds ago and whose object is not currently present in any active registry, then GC-hints Ruby. -
.reload(filename = nil) ⇒ Boolean
Reloads type and sellable data from disk.
-
.remove_inv_item(id) ⇒ void
Removes every placement of
idfrom the worn inventory (+@@inv+) and from all container contents (+@@contents+). - .reserve ⇒ Array<GameObj>?
- .right_hand ⇒ Array<GameObj>?
- .room_desc ⇒ Array<GameObj>?
-
.sellable_data ⇒ Hash
The loaded sellable classification data.
-
.target ⇒ GameObj?
Returns the single NPC or PC matching
XMLData.current_target_id. -
.targets ⇒ Array<GameObj>
Returns the list of active (non-dead, non-animated, non-appendage) NPCs that are currently targeted via
XMLData.current_target_ids. -
.type_cache ⇒ Hash
The memoized type lookup cache.
-
.type_data ⇒ Hash
The loaded type classification data.
-
.upsert_inv(id, noun, name, container = nil, before = nil, after = nil) ⇒ GameObj
Upserts an inventory item by id for a PARTIAL (filtered) scrape -- e.g.
Instance Method Summary collapse
-
#contents ⇒ Array<GameObj>?
Returns a duplicated snapshot of the object's container contents.
-
#empty? ⇒ false
Always returns
false; GameObj instances are never considered empty. -
#full_name ⇒ String
Returns the full display name, assembling before/name/after parts.
- #GameObj ⇒ String? deprecated Deprecated.
-
#initialize(id, noun, name, before = nil, after = nil) ⇒ GameObj
constructor
Initializes a new GameObj, normalizing certain irregular noun values.
-
#sellable ⇒ String?
Returns a comma-separated string of sellable categories, or
nil. -
#status ⇒ String?
Returns the current status string of this object, or
nilif present but unstated, or"gone"if not found in any registry. -
#status=(val) ⇒ String?
Sets the status of this NPC or PC by ID.
-
#to_s ⇒ String?
Returns a human-readable representation of the object (its noun).
-
#type ⇒ String?
Returns a comma-separated string of matching type tags for this object, or
nilif no types match. -
#type?(type_to_check) ⇒ Boolean
Returns whether this object matches the given type tag.
Constructor Details
#initialize(id, noun, name, before = nil, after = nil) ⇒ GameObj
Initializes a new GameObj, normalizing certain irregular noun values.
189 190 191 192 193 194 195 |
# File 'documented/common/gameobj.rb', line 189 def initialize(id, noun, name, before = nil, after = nil) @id = id.is_a?(Integer) ? id.to_s : id @noun = normalize_noun(noun, name) @name = name @before_name = before @after_name = after end |
Instance Attribute Details
#after_name ⇒ String?
Returns text appended after the name in full display.
180 181 182 |
# File 'documented/common/gameobj.rb', line 180 def after_name @after_name end |
#before_name ⇒ String?
Returns text prepended before the name in full display.
177 178 179 |
# File 'documented/common/gameobj.rb', line 177 def before_name @before_name end |
#id ⇒ String (readonly)
Returns the unique string ID of this object.
168 169 170 |
# File 'documented/common/gameobj.rb', line 168 def id @id end |
#name ⇒ String?
Returns full descriptive name (e.g. "a snarling goblin").
174 175 176 |
# File 'documented/common/gameobj.rb', line 174 def name @name end |
#noun ⇒ String?
Returns noun used to refer to this object (e.g. "goblin").
171 172 173 |
# File 'documented/common/gameobj.rb', line 171 def noun @noun end |
Class Method Details
.[](val) ⇒ GameObj?
Finds a GameObj by ID (numeric string), noun (single word), or name.
Also accepts a Regexp for name-based matching.
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 |
# File 'documented/common/gameobj.rb', line 1115 def self.[](val) unless val.is_a?(String) || val.is_a?(Regexp) respond "--- Lich: error: GameObj[] passed with #{val.class} #{val} via caller: #{caller[0]}" respond "--- Lich: error: GameObj[] supports String or Regexp only" Lich.log "--- Lich: error: GameObj[] passed with #{val.class} #{val} via caller: #{caller[0]}\n\t" Lich.log "--- Lich: error: GameObj[] supports String or Regexp only\n\t" if val.is_a?(Integer) respond "--- Lich: error: GameObj[] converted Integer #{val} to String to continue" val = val.to_s else return nil end end if val.is_a?(Regexp) return search_registries { |o| o.name =~ val } end if val =~ /^\-?[0-9]+$/ # Numeric ID lookup (room_desc excluded from primary, appended last for completeness) search_registries { |o| o.id == val } elsif val.split(' ').length == 1 # Single-word noun lookup search_registries { |o| o.noun == val } else # Name lookup - exact first, then suffix, then fuzzy suffix escaped = Regexp.escape(val.strip) fuzzy = Regexp.escape(val).sub(' ', ' .*') search_registries { |o| o.name == val } || search_registries { |o| o.name =~ /\b#{escaped}$/i } || search_registries { |o| o.name =~ /\b#{fuzzy}$/i } end end |
.abort_container(container_id) ⇒ void
This method returns an undefined value.
Discards one container's open staging buffer WITHOUT publishing it, leaving
the previously published @@contents[id] visible. Rolls back a
begin_container whose fill failed partway; no-op when none is open. Unlike
delete_container it does NOT remove the published contents.
989 990 991 992 |
# File 'documented/common/gameobj.rb', line 989 def self.abort_container(container_id) staged = @@staging_contents.delete(container_id) (staged) if staged end |
.abort_inv ⇒ void
This method returns an undefined value.
Discards an open @@inv staging buffer WITHOUT publishing it, leaving the
previously published @@inv visible. Rolls back a begin_inv whose fill
failed partway (e.g. a second writer's mirror raised mid-registration) so a
later cycle does not see inv_refresh_open? stuck open. No-op when none is
open. Symmetric with commit_inv.
869 870 871 872 873 874 |
# File 'documented/common/gameobj.rb', line 869 def self.abort_inv return if @@staging_inv.nil? (@@staging_inv) @@staging_inv = nil end |
.apply_pending_metadata(buffer) ⇒ void
This method returns an undefined value.
Applies the metadata deferred for one staging buffer to its objects and
clears that buffer's pending entries. Called by each commit_* just before
it publishes the buffer, so the new +before_name+/+after_name+ become
visible together with the new membership rather than mid-refresh. No-op for
a buffer that deferred nothing.
822 823 824 825 826 827 828 829 830 |
# File 'documented/common/gameobj.rb', line 822 def self.(buffer) pending = @@pending_metadata.delete(buffer) return unless pending pending.each do |obj, (before, after)| obj.before_name = before unless before.nil? obj.after_name = after unless after.nil? end end |
.begin_all_containers ⇒ void
This method returns an undefined value.
Opens a full-replacement refresh of ALL container contents, used by DR's
INV LIST (a complete recursive scrape). Staging goes into a dedicated
buffer (+@@staging_all_contents+), separate from the ordinary per-container
@@staging_contents, so an unrelated begin_container refresh in flight
at the same time is never clobbered by this refresh's discard/commit. While
open, new_inv stages every not-already-owned container placement (see
new_inv) and the live @@contents is left visible to readers.
commit_all_containers_full then swaps the whole buffer in one reference
assignment, so containers absent from the listing are dropped -- matching
the old clear-then-fill semantics -- while an interrupted listing that
never commits leaves the previous model intact (see discard_inv_refresh).
Pair with begin_inv for worn items.
1032 1033 1034 1035 1036 1037 1038 |
# File 'documented/common/gameobj.rb', line 1032 def self.begin_all_containers # Restarting an already-open full refresh discards the prior buffers, so # drop their deferred metadata rather than orphan it (mirrors begin_inv). @@staging_all_contents.each_value { |buf| (buf) } @@staging_all_contents = {} @@staging_all_containers = true end |
.begin_container(container_id) ⇒ Array
Opens a refresh of a single container's contents. Restarting an
already-open refresh (a second begin_container before commit) discards
the prior buffer, so drop its deferred metadata too rather than orphan it.
967 968 969 970 |
# File 'documented/common/gameobj.rb', line 967 def self.begin_container(container_id) (@@staging_contents[container_id]) @@staging_contents[container_id] = [] end |
.begin_familiar ⇒ void
This method returns an undefined value.
Opens a refresh of the four familiar registries.
940 941 942 943 944 945 |
# File 'documented/common/gameobj.rb', line 940 def self.begin_familiar @@staging_fam_room_desc = [] @@staging_fam_loot = [] @@staging_fam_npcs = [] @@staging_fam_pcs = [] end |
.begin_inv ⇒ Array
Opens a refresh of worn inventory. Restarting an already-open refresh (a
second begin_inv before commit) discards the prior buffer, so drop its
deferred metadata too rather than orphan it (mirrors begin_container).
848 849 850 851 |
# File 'documented/common/gameobj.rb', line 848 def self.begin_inv (@@staging_inv) @@staging_inv = [] end |
.begin_reserve ⇒ Array
877 |
# File 'documented/common/gameobj.rb', line 877 def self.begin_reserve = @@staging_reserve = [] |
.begin_room_desc ⇒ Array
927 |
# File 'documented/common/gameobj.rb', line 927 def self.begin_room_desc = @@staging_room_desc = [] |
.begin_room_objs ⇒ void
This method returns an undefined value.
Opens a refresh of the room object registries (loot + npcs + npc status).
890 891 892 893 894 |
# File 'documented/common/gameobj.rb', line 890 def self.begin_room_objs @@staging_loot = [] @@staging_npcs = [] @@staging_npc_status = {} end |
.begin_room_players ⇒ void
This method returns an undefined value.
Opens a refresh of the room player registries (pcs + pc status).
911 912 913 914 |
# File 'documented/common/gameobj.rb', line 911 def self.begin_room_players @@staging_pcs = [] @@staging_pc_status = {} end |
.clear_all_containers ⇒ void
This method returns an undefined value.
Clears all container registries. Any in-flight staged container refreshes are aborted as well. The shared identity index is preserved so previously seen objects are reused if re-encountered.
740 741 742 743 744 745 746 747 |
# File 'documented/common/gameobj.rb', line 740 def self.clear_all_containers @@staging_contents.each_value { |staged| (staged) } @@staging_all_contents.each_value { |staged| (staged) } @@staging_contents.clear @@staging_all_contents.clear @@staging_all_containers = false @@contents.clear end |
.clear_container(container_id) ⇒ Array
Resets a single container's contents to an empty array. The shared identity index is preserved.
754 755 756 |
# File 'documented/common/gameobj.rb', line 754 def self.clear_container(container_id) @@contents[container_id] = [] end |
.clear_fam_loot ⇒ void
This method returns an undefined value.
727 |
# File 'documented/common/gameobj.rb', line 727 def self.clear_fam_loot = @@fam_loot.clear |
.clear_fam_npcs ⇒ void
This method returns an undefined value.
730 |
# File 'documented/common/gameobj.rb', line 730 def self.clear_fam_npcs = @@fam_npcs.clear |
.clear_fam_pcs ⇒ void
This method returns an undefined value.
733 |
# File 'documented/common/gameobj.rb', line 733 def self.clear_fam_pcs = @@fam_pcs.clear |
.clear_fam_room_desc ⇒ void
This method returns an undefined value.
724 |
# File 'documented/common/gameobj.rb', line 724 def self.clear_fam_room_desc = @@fam_room_desc.clear |
.clear_inv ⇒ void
This method returns an undefined value.
715 |
# File 'documented/common/gameobj.rb', line 715 def self.clear_inv = @@inv.clear |
.clear_loot ⇒ void
This method returns an undefined value.
706 |
# File 'documented/common/gameobj.rb', line 706 def self.clear_loot = @@loot.clear |
.clear_npcs ⇒ void
This method returns an undefined value.
709 |
# File 'documented/common/gameobj.rb', line 709 def self.clear_npcs = (@@npcs.clear; @@npc_status.clear) |
.clear_pcs ⇒ void
This method returns an undefined value.
712 |
# File 'documented/common/gameobj.rb', line 712 def self.clear_pcs = (@@pcs.clear; @@pc_status.clear) |
.clear_reserve ⇒ void
This method returns an undefined value.
718 |
# File 'documented/common/gameobj.rb', line 718 def self.clear_reserve = (@@reserve = []) |
.clear_room_desc ⇒ void
This method returns an undefined value.
721 |
# File 'documented/common/gameobj.rb', line 721 def self.clear_room_desc = @@room_desc.clear |
.commit_all_containers ⇒ void
This method returns an undefined value.
Publishes every open container staging buffer. Called at the prompt
that terminates a command burst, the reliable close signal for the
clearContainer ... inv fill sequence (which has no closing tag).
No-op when no container refresh is open.
A full INV LIST refresh (begin_all_containers) uses its own dedicated
buffer (+@@staging_all_contents+) and is published only through its clean
path (commit_all_containers_full) or discarded on interruption
(discard_inv_refresh) -- never here. While one is open this defers the
ordinary per-container publish until the listing closes, so the two commit
paths never interleave at an interrupting prompt.
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 |
# File 'documented/common/gameobj.rb', line 1007 def self.commit_all_containers return if @@staging_all_containers return if @@staging_contents.empty? @@staging_contents.each do |id, staged| (staged) @@contents[id] = staged end @@staging_contents.clear end |
.commit_all_containers_full ⇒ void
This method returns an undefined value.
Publishes a full container refresh opened by begin_all_containers with a single reference swap and closes it. No-op if no full refresh is open, so an interrupted listing simply keeps the previous published model.
1045 1046 1047 1048 1049 1050 1051 1052 |
# File 'documented/common/gameobj.rb', line 1045 def self.commit_all_containers_full return unless @@staging_all_containers @@staging_all_contents.each_value { |buf| (buf) } @@contents = @@staging_all_contents @@staging_all_contents = {} @@staging_all_containers = false end |
.commit_container(container_id) ⇒ void
This method returns an undefined value.
974 975 976 977 978 979 980 |
# File 'documented/common/gameobj.rb', line 974 def self.commit_container(container_id) staged = @@staging_contents.delete(container_id) return if staged.nil? (staged) @@contents[container_id] = staged end |
.commit_familiar ⇒ void
This method returns an undefined value.
948 949 950 951 952 953 954 955 956 957 958 959 |
# File 'documented/common/gameobj.rb', line 948 def self.commit_familiar return if @@staging_fam_npcs.nil? @@fam_room_desc = @@staging_fam_room_desc @@fam_loot = @@staging_fam_loot @@fam_npcs = @@staging_fam_npcs @@fam_pcs = @@staging_fam_pcs @@staging_fam_room_desc = nil @@staging_fam_loot = nil @@staging_fam_npcs = nil @@staging_fam_pcs = nil end |
.commit_inv ⇒ void
This method returns an undefined value.
854 855 856 857 858 859 860 |
# File 'documented/common/gameobj.rb', line 854 def self.commit_inv return if @@staging_inv.nil? (@@staging_inv) @@inv = @@staging_inv @@staging_inv = nil end |
.commit_reserve ⇒ void
This method returns an undefined value.
880 881 882 883 884 885 |
# File 'documented/common/gameobj.rb', line 880 def self.commit_reserve return if @@staging_reserve.nil? @@reserve = @@staging_reserve @@staging_reserve = nil end |
.commit_room_desc ⇒ void
This method returns an undefined value.
930 931 932 933 934 935 |
# File 'documented/common/gameobj.rb', line 930 def self.commit_room_desc return if @@staging_room_desc.nil? @@room_desc = @@staging_room_desc @@staging_room_desc = nil end |
.commit_room_objs ⇒ void
This method returns an undefined value.
897 898 899 900 901 902 903 904 905 906 |
# File 'documented/common/gameobj.rb', line 897 def self.commit_room_objs return if @@staging_npcs.nil? @@loot = @@staging_loot @@npcs = @@staging_npcs @@npc_status = @@staging_npc_status @@staging_loot = nil @@staging_npcs = nil @@staging_npc_status = nil end |
.commit_room_players ⇒ void
This method returns an undefined value.
917 918 919 920 921 922 923 924 |
# File 'documented/common/gameobj.rb', line 917 def self.commit_room_players return if @@staging_pcs.nil? @@pcs = @@staging_pcs @@pc_status = @@staging_pc_status @@staging_pcs = nil @@staging_pc_status = nil end |
.container_refresh_open?(container_id) ⇒ Boolean
Whether a staged refresh for one container's contents is currently open. Same purpose as inv_refresh_open?, scoped to a single container id.
A full INV LIST refresh (begin_all_containers) owns every container
the instant it opens, but only allocates a per-container buffer when an
item line for that container arrives. A container not yet seen in the
listing therefore has no key in @@staging_contents and would look
unowned -- letting a second writer publish into or delete it mid-refresh.
Report a full refresh as owning all containers so those writers defer.
791 792 793 |
# File 'documented/common/gameobj.rb', line 791 def self.container_refresh_open?(container_id) @@staging_all_containers || @@staging_contents.key?(container_id) end |
.containers ⇒ Hash{String => Array<GameObj>}
Returns a snapshot of all container contents: the outer hash and every
inner array are duplicated, so a caller iterating containers[id] is not
affected by concurrent in-place mutation of the live registry (e.g. a hand
pickup's remove_inv_item reject!, or +new_inv+'s push). Matches the
dup-on-read contract of the other registry accessors and instance
#contents. The GameObj elements themselves are shared, not copied.
699 |
# File 'documented/common/gameobj.rb', line 699 def self.containers = @@contents.transform_values(&:dup) |
.dead ⇒ Array<GameObj>?
Returns all NPCs with a status of "dead", or nil if none.
1187 1188 1189 1190 |
# File 'documented/common/gameobj.rb', line 1187 def self.dead dead_list = @@npcs.select { |obj| obj.status == 'dead' } dead_list.empty? ? nil : dead_list end |
.delete_container(container_id) ⇒ GameObj?
Removes a container and all its contents from the registry. Any in-flight staged refresh for the same container is aborted so a later commit cannot resurrect the deleted key. The shared identity index is preserved.
765 766 767 768 769 |
# File 'documented/common/gameobj.rb', line 765 def self.delete_container(container_id) staged = @@staging_contents.delete(container_id) (staged) if staged @@contents.delete(container_id) end |
.discard_inv_refresh ⇒ void
This method returns an undefined value.
Discards an in-flight INV LIST refresh (worn + full container staging)
without publishing it, leaving the previously published model visible.
Unlike discard_staged_refreshes this touches only the INV LIST buffers,
so it will not abort an unrelated in-flight room/familiar refresh -- nor an
unrelated per-container begin_container refresh, which lives in the
separate @@staging_contents hash and commits on its own at the next prompt.
1062 1063 1064 1065 1066 1067 1068 |
# File 'documented/common/gameobj.rb', line 1062 def self.discard_inv_refresh (@@staging_inv) @@staging_all_contents.each_value { |buf| (buf) } @@staging_inv = nil @@staging_all_contents = {} @@staging_all_containers = false end |
.discard_staged_refreshes ⇒ void
This method returns an undefined value.
Discards every in-flight staged refresh without publishing it.
Called by XMLParser#reset after a malformed or truncated fragment
forces the parser to resynchronize. Any refresh open at that moment is
known to be incomplete, so its buffer is dropped rather than published:
the previously published snapshot stays visible, which is the same
failure mode as an interrupted stream that never commits.
Without this, an interrupted container fill would be published as
authoritative by the next commit_all_containers at the following
prompt, and objects held only in an abandoned buffer would keep
appearing in live_registry_objects (blocking prune_index!) until the
next refresh of that same registry replaced it.
1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 |
# File 'documented/common/gameobj.rb', line 1085 def self.discard_staged_refreshes @@staging_inv = nil @@staging_reserve = nil @@staging_loot = nil @@staging_npcs = nil @@staging_npc_status = nil @@staging_pcs = nil @@staging_pc_status = nil @@staging_room_desc = nil @@staging_fam_room_desc = nil @@staging_fam_loot = nil @@staging_fam_npcs = nil @@staging_fam_pcs = nil @@staging_contents.clear @@staging_all_contents.clear @@staging_all_containers = false # Every open buffer is being discarded, so no deferred metadata can still # belong to a refresh that will commit; drop all of it. @@pending_metadata.clear end |
.drop_pending_metadata(buffer) ⇒ void
This method returns an undefined value.
Drops the metadata deferred for one staging buffer without applying it --
the abort counterpart to apply_pending_metadata. Called by every path
that discards a buffer so a rolled-back refresh cannot later publish its
staged values. No-op for a buffer that deferred nothing.
839 840 841 |
# File 'documented/common/gameobj.rb', line 839 def self.(buffer) @@pending_metadata.delete(buffer) end |
.fam_loot ⇒ Array<GameObj>?
683 |
# File 'documented/common/gameobj.rb', line 683 def self.fam_loot = registry_or_nil(@@fam_loot) |
.fam_npcs ⇒ Array<GameObj>?
686 |
# File 'documented/common/gameobj.rb', line 686 def self.fam_npcs = registry_or_nil(@@fam_npcs) |
.fam_pcs ⇒ Array<GameObj>?
689 |
# File 'documented/common/gameobj.rb', line 689 def self.fam_pcs = registry_or_nil(@@fam_pcs) |
.fam_room_desc ⇒ Array<GameObj>?
680 |
# File 'documented/common/gameobj.rb', line 680 def self.fam_room_desc = registry_or_nil(@@fam_room_desc) |
.hidden_targets ⇒ Array<String>
Returns IDs in the current target list that do not correspond to a known NPC.
1173 1174 1175 |
# File 'documented/common/gameobj.rb', line 1173 def self.hidden_targets XMLData.current_target_ids.reject { |id| @@npcs.any? { |n| n.id == id } } end |
.index_or_create(id, noun, name, before = nil, after = nil) ⇒ GameObj
Looks up an existing GameObj in the shared identity index by composite
key (+id+, noun, name), or creates and indexes a new one.
Unlike find_or_create, this method does not push the object into any
registry array. It is intended for callers that manage their own storage
slot (e.g. +new_right_hand+/+new_left_hand+) or for external code that
constructs GameObj instances via GameObj.new but wants to participate
in the shared identity index so objects are reused and tracked for TTL-
based garbage collection.
When a matching entry is found, before_name and after_name are
refreshed from a differing non-nil incoming value; a nil observation
never clobbers a known value. (Hand slots pass no before/after, so in
practice this path only ever backfills.)
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'documented/common/gameobj.rb', line 626 def self.index_or_create(id, noun, name, before = nil, after = nil) str_id = id.is_a?(Integer) ? id.to_s : id key = "#{str_id}|#{noun}|#{name}" now = Process.clock_gettime(Process::CLOCK_MONOTONIC) @@index_mutex.synchronize do if (entry = @@index[key]) existing, _ts = entry @@index[key] = [existing, now] # Refresh command metadata whenever a differing non-nil observation # arrives -- e.g. a name-preserving move updates +before_name+ from # "get #id in #old" to "get #id in #new". A nil observation never # clobbers a known value (a sighting from a source that carries no # command, such as the hand slot, must not blank it). existing.before_name = before if !before.nil? && existing.before_name != before existing.after_name = after if !after.nil? && existing.after_name != after existing else new_obj = GameObj.new(id, noun, name, before, after) @@index[key] = [new_obj, now] new_obj end end end |
.index_stats(verbose: false) ⇒ Hash
Returns a Hash describing the current memory and age state of the index.
Useful for diagnosing memory growth in long sessions. The :age_buckets
breakdown shows how many entries fall into each staleness window so you
can tune the TTL passed to prune_index! accordingly.
When verbose: true, prints a formatted report to stdout.
1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 |
# File 'documented/common/gameobj.rb', line 1330 def self.index_stats(verbose: false) require 'objspace' return empty_index_stats if @@index.empty? now = Process.clock_gettime(Process::CLOCK_MONOTONIC) live_objs = live_registry_objects # Snapshot the entries under the mutex so we never iterate the index # while another thread inserts into or prunes it. entries = @@index_mutex.synchronize { @@index.values } total = entries.size buckets = { 'under5m' => 0, '5-15m' => 0, '15-30m' => 0, '30-60m' => 0, 'over60m' => 0 } stale = 0 oldest_age = 0.0 entries.each do |obj, last_seen| age = now - last_seen oldest_age = age if age > oldest_age stale += 1 unless live_objs.include?(obj) buckets[case age when 0...300 then 'under5m' when 300...900 then '5-15m' when 900...1800 then '15-30m' when 1800...3600 then '30-60m' else 'over60m' end] += 1 end obj_mem = gameobj_memory_bytes heap_mem = ruby_heap_bytes result = { total_entries: total, live_in_registries: total - stale, stale_entries: stale, oldest_entry_seconds: oldest_age.round(1), age_buckets: buckets, gameobj_bytes: obj_mem, heap_bytes: heap_mem } if verbose oldest_fmt = if oldest_age < 60 "#{oldest_age.round(1)}s" elsif oldest_age < 3600 "#{(oldest_age / 60).round(1)}m" else "#{(oldest_age / 3600).round(2)}h" end w = 28 puts "=" * 52 puts " GameObj.index_stats" puts "=" * 52 puts format(" %-#{w}s %d", "Total index entries:", total) puts format(" %-#{w}s %d", "Live in registries:", result[:live_in_registries]) puts format(" %-#{w}s %d", "Stale (index-only):", stale) puts format(" %-#{w}s %s", "Oldest entry:", oldest_fmt) puts "-" * 52 puts " Age distribution:" buckets.each do |label, count| = "#" * [count, 30].min puts format(" %-10s %4d %s", label, count, ) end puts "-" * 52 puts format(" %-#{w}s %s", "GameObj object memory:", format_bytes(obj_mem)) puts format(" %-#{w}s %s", "Ruby heap size:", format_bytes(heap_mem)) puts "=" * 52 end result end |
.inv ⇒ Array<GameObj>?
671 |
# File 'documented/common/gameobj.rb', line 671 def self.inv = registry_or_nil(@@inv) |
.inv_refresh_open? ⇒ Boolean
Whether a top-level inventory (+@@inv+) staged refresh is currently open.
Lets a second writer (e.g. the Inventory read-model) skip
its own +begin_inv+/+commit_inv+ cycle so it cannot prematurely publish or
truncate an in-flight classic @@inv refresh.
777 |
# File 'documented/common/gameobj.rb', line 777 def self.inv_refresh_open? = !@@staging_inv.nil? |
.left_hand ⇒ Array<GameObj>?
659 |
# File 'documented/common/gameobj.rb', line 659 def self.left_hand = @@left_hand&.dup |
.load_data(filename = nil) ⇒ Boolean
Loads type and sellable classification data from XML.
Merges custom overrides from gameobj-custom/gameobj-data.xml if present.
1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 |
# File 'documented/common/gameobj.rb', line 1431 def self.load_data(filename = nil) primary = filename || File.join(DATA_DIR, 'gameobj-data.xml') unless File.exist?(primary) @@type_data = @@sellable_data = nil echo "error: GameObj.load_data: file does not exist: #{primary}" return false end begin @@type_data = {} @@sellable_data = {} @@type_cache = {} parse_data_file(primary) rescue => e @@type_data = @@sellable_data = nil echo "error: GameObj.load_data: #{e}" respond e.backtrace[0..1] return false end custom = File.join(DATA_DIR, 'gameobj-custom', 'gameobj-data.xml') if File.exist?(custom) begin parse_data_file(custom, merge: true) rescue => e echo "error: Custom GameObj.load_data: #{e}" respond e.backtrace[0..1] return false end end true end |
.loot ⇒ Array<GameObj>?
665 |
# File 'documented/common/gameobj.rb', line 665 def self.loot = registry_or_nil(@@loot) |
.merge_data(existing, new_value) ⇒ Regexp
Merges two Regexp values via Regexp.union, or returns the new value
if the existing one is not yet a Regexp.
1422 1423 1424 |
# File 'documented/common/gameobj.rb', line 1422 def self.merge_data(existing, new_value) existing.is_a?(Regexp) ? Regexp.union(existing, new_value) : new_value end |
.new_fam_loot(id, noun, name) ⇒ GameObj
Creates and registers a new familiar loot object.
543 544 545 |
# File 'documented/common/gameobj.rb', line 543 def self.new_fam_loot(id, noun, name) find_or_create(@@staging_fam_loot || @@fam_loot, id, noun, name) end |
.new_fam_npc(id, noun, name) ⇒ GameObj
Creates and registers a new familiar NPC.
553 554 555 |
# File 'documented/common/gameobj.rb', line 553 def self.new_fam_npc(id, noun, name) find_or_create(@@staging_fam_npcs || @@fam_npcs, id, noun, name) end |
.new_fam_pc(id, noun, name) ⇒ GameObj
Creates and registers a new familiar PC.
563 564 565 |
# File 'documented/common/gameobj.rb', line 563 def self.new_fam_pc(id, noun, name) find_or_create(@@staging_fam_pcs || @@fam_pcs, id, noun, name) end |
.new_fam_room_desc(id, noun, name) ⇒ GameObj
Creates and registers a new familiar room description object.
533 534 535 |
# File 'documented/common/gameobj.rb', line 533 def self.new_fam_room_desc(id, noun, name) find_or_create(@@staging_fam_room_desc || @@fam_room_desc, id, noun, name) end |
.new_inv(id, noun, name, container = nil, before = nil, after = nil) ⇒ GameObj
Creates and registers a new inventory item, optionally in a container.
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'documented/common/gameobj.rb', line 391 def self.new_inv(id, noun, name, container = nil, before = nil, after = nil) # A write is "staged" when it targets an open staging buffer rather than # the live registry. Metadata refreshes are then deferred into that same # buffer so an uncommitted (or later discarded) value is never published # on the shared instance -- see +@@pending_metadata+. if container staged = if @@staging_contents.key?(container) # An explicit per-container refresh (+begin_container+, e.g. a # +clearContainer+ fill) already owns this container -- stage into # it even during a full INV LIST refresh so the two paths never # share a buffer. @@staging_contents[container] elsif @@staging_all_containers # Full INV LIST refresh: stage every other container into the # dedicated buffer so nothing touches the live model until the # listing commits cleanly. @@staging_all_contents[container] ||= [] end # +staged+ is the target staging buffer (either kind) or nil for a live # write; either way it is both the placement target and the deferral # scope, so an uncommitted metadata change lands in that same buffer. target = staged || (@@contents[container] ||= []) find_or_create(target, id, noun, name, before, after, defer_to: staged) else find_or_create(@@staging_inv || @@inv, id, noun, name, before, after, defer_to: @@staging_inv) end end |
.new_left_hand(id, noun, name) ⇒ GameObj
Sets the left-hand object, replacing any existing one.
Routes through the shared identity index so the same item picked up
again returns the existing GameObj instance rather than allocating
a new one. Replace semantics are preserved - @@left_hand is always
overwritten with the result.
593 594 595 |
# File 'documented/common/gameobj.rb', line 593 def self.new_left_hand(id, noun, name) @@left_hand = index_or_create(id, noun, name) end |
.new_loot(id, noun, name) ⇒ GameObj
Creates and registers a new loot object.
365 366 367 |
# File 'documented/common/gameobj.rb', line 365 def self.new_loot(id, noun, name) find_or_create(@@staging_loot || @@loot, id, noun, name) end |
.new_npc(id, noun, name, status = nil) ⇒ GameObj
Creates and registers a new NPC.
353 354 355 356 357 |
# File 'documented/common/gameobj.rb', line 353 def self.new_npc(id, noun, name, status = nil) obj = find_or_create(@@staging_npcs || @@npcs, id, noun, name) (@@staging_npc_status || @@npc_status)[obj.id] = status obj end |
.new_pc(id, noun, name, status = nil) ⇒ GameObj
Creates and registers a new PC.
376 377 378 379 380 |
# File 'documented/common/gameobj.rb', line 376 def self.new_pc(id, noun, name, status = nil) obj = find_or_create(@@staging_pcs || @@pcs, id, noun, name) (@@staging_pc_status || @@pc_status)[obj.id] = status obj end |
.new_reserve(id, noun, name) ⇒ GameObj
Creates and registers a new reserve slot item.
@@reserve is nil until the first reserve stream is seen; thereafter
it is always an Array (possibly empty).
512 513 514 515 |
# File 'documented/common/gameobj.rb', line 512 def self.new_reserve(id, noun, name) @@reserve ||= [] find_or_create(@@staging_reserve || @@reserve, id, noun, name) end |
.new_right_hand(id, noun, name) ⇒ GameObj
Sets the right-hand object, replacing any existing one.
Routes through the shared identity index so the same item picked up
again returns the existing GameObj instance rather than allocating
a new one. Replace semantics are preserved - @@right_hand is always
overwritten with the result.
578 579 580 |
# File 'documented/common/gameobj.rb', line 578 def self.new_right_hand(id, noun, name) @@right_hand = index_or_create(id, noun, name) end |
.new_room_desc(id, noun, name) ⇒ GameObj
Creates and registers a new room description object.
523 524 525 |
# File 'documented/common/gameobj.rb', line 523 def self.new_room_desc(id, noun, name) find_or_create(@@staging_room_desc || @@room_desc, id, noun, name) end |
.npcs ⇒ Array<GameObj>?
662 |
# File 'documented/common/gameobj.rb', line 662 def self.npcs = registry_or_nil(@@npcs) |
.pcs ⇒ Array<GameObj>?
668 |
# File 'documented/common/gameobj.rb', line 668 def self.pcs = registry_or_nil(@@pcs) |
.prune_index!(ttl: 900, verbose: false) ⇒ Hash
Removes entries from the shared identity index whose last_seen_at
timestamp is older than ttl seconds ago and whose object is not
currently present in any active registry, then GC-hints Ruby.
The live-registry check is the critical guard: an object that is still
held in @@npcs, @@loot, @@inv, or any other registry must never be
pruned regardless of how long ago it was last re-registered. Pruning a
live entry would cause the next find_or_create call for that object to
allocate a brand-new instance, silently breaking the identity guarantee.
An entry is only eligible for pruning when both conditions are true:
1. +last_seen_at+ is older than +ttl+ seconds ago
2. The object's ID is not present in any active registry
Safe to call at any time and as frequently as desired. Entries that are live in registries are always skipped. Entries accessed within the TTL window are always skipped.
Recommended call sites: after a room transition, in a script's idle loop,
or whenever index_stats shows :stale_entries growing large.
When verbose: true, prints a before/after report to stdout showing:
- GameObj count and estimated object memory before and after pruning
- Ruby heap size before and after, with the net change
- Number of entries pruned, skipped (live), and remaining
- Time taken
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 |
# File 'documented/common/gameobj.rb', line 1246 def self.prune_index!(ttl: 900, verbose: false) require 'objspace' t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) cutoff = t_start - ttl obj_before = gameobj_memory_bytes heap_before = ruby_heap_bytes pruned, skipped_live = sweep_stale!(cutoff) GC.start(full_mark: false, immediate_sweep: false) if pruned.positive? obj_after = gameobj_memory_bytes heap_after = ruby_heap_bytes elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000 result = { pruned: pruned, skipped_live: skipped_live, remaining: @@index.size, gameobj_bytes_before: obj_before, gameobj_bytes_after: obj_after, gameobj_bytes_freed: obj_before - obj_after, heap_bytes_before: heap_before, heap_bytes_after: heap_after, heap_bytes_freed: heap_before - heap_after, elapsed_ms: elapsed.round(3) } if verbose w = 28 puts "=" * 52 puts " GameObj.prune_index! - TTL: #{ttl}s" puts "=" * 52 puts format(" %-#{w}s %s -> %s (%s)", "GameObj object memory:", format_bytes(obj_before), format_bytes(obj_after), format_delta(result[:gameobj_bytes_freed])) puts format(" %-#{w}s %s -> %s (%s)", "Ruby heap size:", format_bytes(heap_before), format_bytes(heap_after), format_delta(result[:heap_bytes_freed])) puts format(" %-#{w}s %d removed, %d skipped (live), %d remaining", "Index entries:", pruned, skipped_live, @@index.size) puts format(" %-#{w}s %.3f ms", "Elapsed:", elapsed) puts "=" * 52 end result end |
.reload(filename = nil) ⇒ Boolean
Reloads type and sellable data from disk.
1412 1413 1414 |
# File 'documented/common/gameobj.rb', line 1412 def self.reload(filename = nil) load_data(filename) end |
.remove_inv_item(id) ⇒ void
This method returns an undefined value.
Removes every placement of id from the worn inventory (+@@inv+) and
from all container contents (+@@contents+). Used as the removal half of
upsert_inv and to reconcile the model when an item leaves inventory
for a hand (a held item is neither worn nor in a container). Leaves
+@@right_hand+/+@@left_hand+ and the shared identity index untouched.
A nil or empty id is an explicit no-op: empty hands stream a hand tag
with no exist id, so this is called constantly with nil, and matching
obj.id == nil would wrongly wipe any (future) nil-id entry -- and also
scan the whole inventory for nothing. The reject! runs under
@@index_mutex (the lock find_or_create holds for registry writes) so
it cannot race the off-thread prune sweep that walks these registries.
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 |
# File 'documented/common/gameobj.rb', line 486 def self.remove_inv_item(id) str_id = id.is_a?(Integer) ? id.to_s : id return if str_id.nil? || str_id.empty? @@index_mutex.synchronize do @@inv.reject! { |obj| obj.id == str_id } @@contents.each_value { |list| list.reject! { |obj| obj.id == str_id } } # Scrub any in-flight staging buffers too: a hand pickup during a full # INV LIST refresh must not be resurrected in its old container when the # staged listing commits. @@staging_inv&.reject! { |obj| obj.id == str_id } @@staging_contents.each_value { |list| list.reject! { |obj| obj.id == str_id } } @@staging_all_contents.each_value { |list| list.reject! { |obj| obj.id == str_id } } end nil end |
.reserve ⇒ Array<GameObj>?
674 |
# File 'documented/common/gameobj.rb', line 674 def self.reserve = @@reserve&.dup |
.right_hand ⇒ Array<GameObj>?
656 |
# File 'documented/common/gameobj.rb', line 656 def self.right_hand = @@right_hand&.dup |
.room_desc ⇒ Array<GameObj>?
677 |
# File 'documented/common/gameobj.rb', line 677 def self.room_desc = registry_or_nil(@@room_desc) |
.sellable_data ⇒ Hash
Returns the loaded sellable classification data.
1473 |
# File 'documented/common/gameobj.rb', line 1473 def self.sellable_data = @@sellable_data |
.target ⇒ GameObj?
Returns the single NPC or PC matching XMLData.current_target_id.
1180 1181 1182 |
# File 'documented/common/gameobj.rb', line 1180 def self.target (@@npcs + @@pcs).find { |n| n.id == XMLData.current_target_id } end |
.targets ⇒ Array<GameObj>
Returns the list of active (non-dead, non-animated, non-appendage) NPCs
that are currently targeted via XMLData.current_target_ids.
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 |
# File 'documented/common/gameobj.rb', line 1158 def self.targets XMLData.current_target_ids.filter_map do |id| npc = @@npcs.find { |n| n.id == id } next unless npc next if npc.status.to_s =~ /dead|gone/i next if npc.name =~ /^animated\b/i && npc.name !~ /^animated slush/i next if npc.noun =~ /^(?:arm|appendage|claw|limb|pincer|tentacle)s?$|^(?:palpus|palpi)$/i && npc.name !~ /(?:amaranthine|ghostly|grizzled|ancient) kraken tentacle/i npc end end |
.type_cache ⇒ Hash
Returns the memoized type lookup cache.
1470 |
# File 'documented/common/gameobj.rb', line 1470 def self.type_cache = @@type_cache |
.type_data ⇒ Hash
Returns the loaded type classification data.
1467 |
# File 'documented/common/gameobj.rb', line 1467 def self.type_data = @@type_data |
.upsert_inv(id, noun, name, container = nil, before = nil, after = nil) ⇒ GameObj
Upserts an inventory item by id for a PARTIAL (filtered) scrape -- e.g.
INV SEARCH <word> or INV <category> full, which list only the
matching items and so must not clear unrelated inventory the way a full
INV LIST refresh does. Any prior placement of id is removed from
@@inv and every @@contents list first, so an item that moved
containers (or whose name changed, e.g. gained "(closed)") is refreshed
in exactly one place with neither a duplicate nor a stale copy left
behind. It is then (re)placed via new_inv: worn (+container+ nil) into
@@inv, otherwise into @@contents[container].
Because a filtered scrape only reports matches, it can add or relocate items but can never prove an item is gone -- removals are left to the next full refresh.
The removal runs under @@index_mutex (the same lock find_or_create
holds for registry writes) so it cannot race the off-thread
prune_index! sweep that walks +@@inv+/+@@contents+. It is a separate
acquisition from the new_inv that follows -- Ruby's Mutex is not
reentrant, and new_inv takes the lock itself -- so remove and re-add
are not one atomic step; that is fine because the parser thread is the
sole writer and a reader can at worst momentarily not see the item.
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 |
# File 'documented/common/gameobj.rb', line 449 def self.upsert_inv(id, noun, name, container = nil, before = nil, after = nil) str_id = id.is_a?(Integer) ? id.to_s : id remove_inv_item(str_id) obj = new_inv(str_id, noun, name, container, before, after) # A name change keys a NEW index entry ("id|noun|newname"); the prior # "id|noun|oldname" instance was just pulled from every registry by # remove_inv_item, but its index key lingers until the TTL prune. On the # cold scrape path, evict any index entry for this id whose instance is no # longer live so a frequently-renamed item (e.g. a pouch toggling # "(closed)") can't accumulate orphaned variants between sweeps. Guarded by # object identity via +live_registry_objects+ (computed outside the lock, # matching +sweep_stale!+) so a variant still held anywhere -- the instance # just re-added, or a hand slot -- is kept. live = live_registry_objects @@index_mutex.synchronize do @@index.delete_if { |_key, (indexed, _ts)| indexed.id == str_id && !live.include?(indexed) } end obj end |
Instance Method Details
#contents ⇒ Array<GameObj>?
Returns a duplicated snapshot of the object's container contents.
223 224 225 |
# File 'documented/common/gameobj.rb', line 223 def contents @@contents[@id]&.dup end |
#empty? ⇒ false
Always returns false; GameObj instances are never considered empty.
216 217 218 |
# File 'documented/common/gameobj.rb', line 216 def empty? false end |
#full_name ⇒ String
Returns the full display name, assembling before/name/after parts.
230 231 232 233 |
# File 'documented/common/gameobj.rb', line 230 def full_name parts = [@before_name, @name, @after_name] parts.compact.reject(&:empty?).join(' ') end |
#GameObj ⇒ String?
209 210 211 |
# File 'documented/common/gameobj.rb', line 209 def GameObj @noun end |
#sellable ⇒ String?
Returns a comma-separated string of sellable categories, or nil.
280 281 282 283 284 285 286 287 288 |
# File 'documented/common/gameobj.rb', line 280 def sellable # See +#type+: degrade to +nil+ (loudly once, then quietly) when the data # file is missing/corrupt rather than raising on +nil.empty?+/+nil.keys+. GameObj.load_data if @@sellable_data && @@sellable_data.empty? return nil if @@sellable_data.nil? matches = matching_data_keys(@@sellable_data) matches.empty? ? nil : matches.join(',') end |
#status ⇒ String?
Returns the current status string of this object, or nil if present
but unstated, or "gone" if not found in any registry.
The published status maps are consulted first, then the staging maps.
That order is deliberate: while a refresh is in flight a reader must
still see the previous complete snapshot (see the staging notes above),
so a staged value must never shadow a published one. The staging maps
are only a fallback for an object that has no published entry at all,
which would otherwise be reported as "gone" despite being mid-refresh.
Note that a dedupe hit in find_or_create means a staged object and its
published counterpart are frequently the same instance, so this method
cannot distinguish which of the two a caller holds. Callers that need
the in-flight value (i.e. the parser) must track it themselves and write
through #status= rather than reading it back through here.
The "gone" sentinel is a frozen literal and must not be mutated in
place; build a new String from it instead.
314 315 316 317 318 319 320 321 |
# File 'documented/common/gameobj.rb', line 314 def status return @@npc_status[@id] if @@npc_status.key?(@id) return @@pc_status[@id] if @@pc_status.key?(@id) return @@staging_npc_status[@id] if @@staging_npc_status&.key?(@id) return @@staging_pc_status[@id] if @@staging_pc_status&.key?(@id) present_in_any_registry? ? nil : 'gone' end |
#status=(val) ⇒ String?
Sets the status of this NPC or PC by ID.
When a room-objects or room-players refresh is in flight, the object lives in the staging buffer rather than the published registry, so this checks the staging pools first and writes to the staging status map. When no refresh is open it behaves exactly as before.
332 333 334 335 336 337 338 339 340 |
# File 'documented/common/gameobj.rb', line 332 def status=(val) npc_pool = @@staging_npcs || @@npcs pc_pool = @@staging_pcs || @@pcs if npc_pool.any? { |npc| npc.id == @id } (@@staging_npc_status || @@npc_status)[@id] = val elsif pc_pool.any? { |pc| pc.id == @id } (@@staging_pc_status || @@pc_status)[@id] = val end end |
#to_s ⇒ String?
Returns a human-readable representation of the object (its noun).
200 201 202 |
# File 'documented/common/gameobj.rb', line 200 def to_s @noun end |
#type ⇒ String?
Returns a comma-separated string of matching type tags for this object,
or nil if no types match.
Results are memoized in @@type_cache under a composite key combining
noun, name, and #full_name - i.e. every value #matching_data_keys
inspects. Two objects that differ in any matcher input (for example a
shared full_name but a different noun, or differing
+before_name+/+after_name+) are therefore cached independently rather
than sharing an entry. The +"|"+-delimited form mirrors the composite
keys used by @@index.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'documented/common/gameobj.rb', line 251 def type # +load_data+ nils +@@type_data+ (not +{}+) when the data file is missing # or corrupt, and returns false rather than raising. Only attempt the load # from the pristine +{}+ state (a nil is falsy under +&&+, so a failed load # isn't retried on every call -- it echoes its error once, then degrades), # and guard the lookup so a broken +gameobj-data.xml+ yields +nil+ instead # of crashing every +#type+ caller -- including every Inventory::Item, # which now exposes this as public API. GameObj.load_data if @@type_data && @@type_data.empty? return nil if @@type_data.nil? cache_key = "#{@noun}|#{@name}|#{full_name}" return @@type_cache[cache_key] if @@type_cache.key?(cache_key) matches = matching_data_keys(@@type_data) @@type_cache[cache_key] = matches.empty? ? nil : matches.join(',') end |
#type?(type_to_check) ⇒ Boolean
Returns whether this object matches the given type tag.
273 274 275 |
# File 'documented/common/gameobj.rb', line 273 def type?(type_to_check) type.to_s.split(',').include?(type_to_check) end |