Skip to main content

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:
  1. Inventory
  2. Loot
  3. NPCs
  4. Player characters
  5. Hands
  6. Room descriptions
  7. 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 }

Performance Tips

  1. Use specific registries - Don’t iterate all GameObj when you only need NPCs
  2. Cache lookups - Store frequently-accessed objects in variables
  3. Avoid repeated type checks - Check type? once and store result
  4. 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

Build docs developers (and LLMs) love