Overview
The GameObj class is Lich’s system for tracking all in-game entities: NPCs, loot, player characters, inventory items, and room descriptions. It provides fast lookups and automatic deduplication through a shared identity index.
GameObj Class Structure
# From lib/common/gameobj.rb:19
class GameObj
# Class-level registries
@@loot = []
@@npcs = []
@@pcs = []
@@inv = []
@@contents = {} # Container contents
@@right_hand = nil
@@left_hand = nil
@@room_desc = []
# Shared identity index for deduplication
@@index = {}
attr_reader :id , :noun , :name
attr_accessor :before_name , :after_name
end
GameObj uses class-level registries rather than instance storage. This allows global access to all game entities from any script.
Object Attributes
Each GameObj has:
id - Unique game ID (string from XML)
noun - Single-word reference (e.g., “troll”, “sword”)
name - Full descriptive name (e.g., “a cave troll”, “a steel broadsword”)
before_name - Text before name in inventory
after_name - Text after name in inventory
troll = GameObj . npcs . first
echo troll. id # "123456"
echo troll. noun # "troll"
echo troll. name # "a cave troll"
echo troll. to_s # "troll" (same as noun)
Object Registries
NPCs
Non-player characters (monsters, critters):
# Access all NPCs in the room
GameObj . npcs . each { | npc |
echo " #{ npc. name } (ID: #{ npc. id } )"
}
# Find specific NPC
troll = GameObj . npcs . find { | npc | npc. noun == 'troll' }
# Check NPC status
if troll && troll. status == 'dead'
echo "Troll is dead!"
end
NPCs are created from bold text in room objects:
# From lib/common/gameobj.rb:221
def self.new_npc ( id , noun , name , status = nil )
obj = find_or_create ( @@npcs , id, noun, name)
@@npc_status [obj. id ] = status
obj
end
Loot
Items on the ground (non-bold text):
# Find loot by name
sword = GameObj . loot . find { | item | item. noun == 'sword' }
# Get all gems
gems = GameObj . loot . select { | item | item. type? ( 'gem' ) }
# Count items
echo "Items on ground: #{ GameObj . loot . length } "
Player Characters
Other players in the room:
GameObj . pcs . each { | pc |
echo " #{ pc. name } - #{ pc. status } "
}
# Check for specific player
if GameObj . pcs . any? { | pc | pc. noun == 'Tamsin' }
echo "Tamsin is here!"
end
Inventory
Your character’s inventory:
# List all items
GameObj . inv . each { | item |
echo item. name
}
# Find specific item
backpack = GameObj . inv . find { | item | item. noun == 'backpack' }
# Check for item
has_sword = GameObj . inv . any? { | item | item. noun == 'sword' }
Containers
Items inside containers:
# Get container ID from an inventory item
backpack = GameObj . inv . find { | item | item. noun == 'backpack' }
# Access contents
if backpack && backpack. contents
backpack. contents . each { | item |
echo "Inside backpack: #{ item. name } "
}
end
# All containers
GameObj . containers . each { | container_id , items |
echo "Container #{ container_id } : #{ items. length } items"
}
Hands
What you’re holding:
# Right hand
if GameObj . right_hand
echo "Right hand: #{ GameObj . right_hand . name } "
end
# Left hand
if GameObj . left_hand
echo "Left hand: #{ GameObj . left_hand . name } "
end
# Check if hands are empty
if GameObj . right_hand . nil? && GameObj . left_hand . nil?
echo "Hands are empty"
end
Object Lookup
The [] operator provides flexible lookups:
# Lookup by ID (numeric string)
obj = GameObj [ '123456' ]
# Lookup by noun (single word)
obj = GameObj [ 'troll' ]
# Lookup by full name
obj = GameObj [ 'cave troll' ]
# Lookup with regex
obj = GameObj [ /troll/i ]
Search order:
Inventory
Loot
NPCs
Player characters
Hands
Room descriptions
Container contents
The lookup returns the first match. For multiple matches, iterate the specific registry.
Room Tracking
Room descriptions can contain clickable objects:
# Objects embedded in room description
GameObj . room_desc . each { | obj |
echo "Clickable in room: #{ obj. name } "
}
These are typically exits, levers, doors, etc.
Target Tracking
Lich tracks your current combat target(s):
# All targeted NPCs
GameObj . targets . each { | target |
echo "Targeting: #{ target. name } "
}
# Single target
if GameObj . target
echo "Primary target: #{ GameObj . target . name } "
end
# Dead targets
if GameObj . dead
GameObj . dead . each { | corpse |
fput "loot # #{ corpse. id } "
}
end
Target filtering:
# From lib/common/gameobj.rb:541
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 # Skip animated objects
next if npc. noun =~ /^(?:arm|tentacle)s?$/i # Skip appendages
npc
end
end
Object Status
NPCs and PCs can have status information:
npc = GameObj . npcs . first
case npc. status
when 'dead'
echo " #{ npc. name } is dead"
when 'stunned'
echo " #{ npc. name } is stunned"
when 'prone'
echo " #{ npc. name } is on the ground"
when 'gone'
echo " #{ npc. name } has left"
when nil
echo " #{ npc. name } is in normal state"
end
Type System
GameObj includes a type classification system:
# Check object type
item = GameObj [ 'diamond' ]
if item && item. type? ( 'gem' )
echo "Found a gem!"
end
# Get all types
echo item. type # "gem,valuable"
# Multiple types
item. type . split ( ',' ). each { | t |
echo "Type: #{ t } "
}
Types are loaded from gameobj-data.xml:
< data >
< type name = "gem" >
< name > diamond|ruby|emerald|sapphire </ name >
</ type >
< type name = "skin" >
< name > pelt|hide|skin </ name >
</ type >
</ data >
Sellable Classification
Objects can be marked as sellable:
item = GameObj [ 'troll skin' ]
if item && item. sellable
echo "Can sell to: #{ item. sellable } " # "furrier,gemshop"
end
Identity Index
GameObj uses a shared identity index for efficient object reuse:
# From lib/common/gameobj.rb:989
def self.find_or_create ( registry , 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 )
if ( entry = @@index [key])
existing, _ts = entry
@@index [key] = [existing, now] # Refresh timestamp
registry. push (existing) unless registry. include? (existing)
return existing
end
obj = GameObj . new (id, noun, name, before, after)
@@index [key] = [obj, now]
registry. push (obj)
obj
end
Benefits:
O(1) lookups by composite key
Object reuse when re-entering rooms
Automatic deduplication across registries
TTL-based cleanup via prune_index!
The index persists across registry clears, so objects are reused when you return to a room. This reduces allocations and maintains object identity.
Memory Management
GameObj provides memory management utilities:
# Prune stale entries (default: 15 minute TTL)
GameObj . prune_index!
# Custom TTL (5 minutes)
GameObj . prune_index! ( ttl: 300 )
# With verbose output
GameObj . prune_index! ( ttl: 900 , verbose: true )
# => GameObj.prune_index! - TTL: 900s
# => GameObj object memory: 42.5 KB -> 28.3 KB (14.2 KB freed)
# => Index entries: 50 removed, 10 skipped (live), 200 remaining
Inspect index state:
stats = GameObj . index_stats
echo "Total entries: #{ stats[ :total_entries ] } "
echo "Live objects: #{ stats[ :live_in_registries ] } "
echo "Stale entries: #{ stats[ :stale_entries ] } "
echo "Memory usage: #{ stats[ :gameobj_bytes ] } bytes"
# Verbose output
GameObj . index_stats ( verbose: true )
Registry Lifecycle
Registries are cleared on room changes:
# When entering a new room (via XMLParser)
GameObj . clear_loot
GameObj . clear_npcs
GameObj . clear_pcs
GameObj . clear_room_desc
# Inventory is only cleared on explicit inv commands
GameObj . clear_inv if XMLData . game =~ /^GS/
Do not manually clear registries unless you know what you’re doing. The XMLParser manages this automatically.
Common Patterns
Safe NPC Interaction
def attack_target ( noun )
target = GameObj . npcs . find { | npc |
npc. noun == noun && npc. status != 'dead'
}
if target
fput "attack # #{ target. id } "
return true
else
echo "No #{ noun } found"
return false
end
end
Loot Collection
# Collect all gems
GameObj . loot . each { | item |
if item. type? ( 'gem' )
fput "get # #{ item. id } "
waitrt?
end
}
Container Management
# Find an open container
container = GameObj . inv . find { | item |
item. type? ( 'container' ) && item. contents
}
if container
echo "Using container: #{ container. name } "
container. contents . each { | item |
echo " #{ item. name } "
}
end
Smart Targeting
# Target most dangerous NPC
target = GameObj . npcs
. reject { | npc | npc. status == 'dead' }
. max_by { | npc | threat_level (npc. name ) }
if target
fput "target # #{ target. id } "
end
Familiar Support
GameObj tracks familiar views separately:
# Familiar's perspective
GameObj . fam_loot . each { | item | echo item. name }
GameObj . fam_npcs . each { | npc | echo npc. name }
GameObj . fam_pcs . each { | pc | echo pc. name }
GameObj . fam_room_desc . each { | obj | echo obj. name }
Use specific registries - Don’t iterate all GameObj when you only need NPCs
Cache lookups - Store frequently-accessed objects in variables
Avoid repeated type checks - Check type? once and store result
Clean up periodically - Call prune_index! in long-running scripts
Example: Complete Looter
# looter.lic - Collect valuable items
VALUABLE_TYPES = [ 'gem' , 'skin' , 'jewelry' , 'magic' ]
loop do
# Wait for new room
room_count = XMLData . room_count
wait_while { XMLData . room_count == room_count }
# Collect valuable loot
GameObj . loot . each { | item |
next unless VALUABLE_TYPES . any? { | type | item. type? (type) }
if GameObj . right_hand . nil?
fput "get # #{ item. id } "
waitrt?
else
fput "stow # #{ GameObj . right_hand . id } "
fput "get # #{ item. id } "
waitrt?
end
}
# Loot corpses
if GameObj . dead
GameObj . dead . each { | corpse |
fput "loot # #{ corpse. id } "
waitrt?
}
end
end
Next Steps
XML Parsing See how objects are created from XML
Settings Store script configuration persistently