Skip to main content

Overview

Lich 5 provides a comprehensive settings system for persistent storage of script configuration. Settings are automatically saved to the SQLite database and support nested data structures.

Settings Modules

Lich provides three settings interfaces:
  • Settings - Script-specific settings (default scope)
  • CharSettings - Character-specific settings (shared across scripts)
  • GameSettings - Game-specific settings (shared across characters)
# From lib/common/settings.rb
module Settings
  def self.[](name)    # Get setting
  def self.[]=(name, value)  # Set setting (auto-saves)
end
All settings are automatically persisted to the database. There’s no need to call a save method.

Basic Usage

Simple Values

# Store simple values
Settings['last_room'] = 12345
Settings['greeting'] = "Hello, world!"
Settings['enabled'] = true

# Retrieve values
echo Settings['last_room']    # 12345
echo Settings['greeting']     # "Hello, world!"
echo Settings['enabled']      # true

Default Values

Use the || operator for defaults:
# If setting doesn't exist, use default
hunt_target = Settings['hunt_target'] || 'troll'
max_hunts = Settings['max_hunts'] || 10

# Store with default
Settings['hunt_target'] ||= 'troll'
Settings that don’t exist return nil, making the || pattern work naturally.

Nested Structures

Hashes

# Store a hash
Settings['stats'] = {
  'kills' => 0,
  'deaths' => 0,
  'items_found' => 0
}

# Access nested values
kills = Settings['stats']['kills']

# Modify nested values
Settings['stats']['kills'] += 1

# Add new keys
Settings['stats']['sessions'] = 1

Arrays

# Store an array
Settings['targets'] = ['troll', 'orc', 'kobold']

# Access array
targets = Settings['targets']
echo targets[0]  # 'troll'

# Modify array
Settings['targets'].push('goblin')
Settings['targets'].delete('orc')

Deep Nesting

# Complex nested structure
Settings['config'] = {
  'hunting' => {
    'enabled' => true,
    'targets' => ['troll', 'orc'],
    'max_rt' => 5
  },
  'looting' => {
    'enabled' => true,
    'types' => ['gem', 'skin']
  }
}

# Access deep values
enabled = Settings['config']['hunting']['enabled']
targets = Settings['config']['hunting']['targets']

# Modify deep values
Settings['config']['hunting']['max_rt'] = 3
Settings['config']['looting']['types'].push('jewelry')

Settings Scopes

Script Settings (Default)

Script-specific settings isolated per script:
# In script 'hunter.lic'
Settings['target'] = 'troll'  # Stored for 'hunter' only

# In script 'looter.lic'
Settings['target'] = 'gem'    # Different setting, different script

CharSettings

Shared across all scripts for the current character:
# Any script can access
CharSettings['home_room'] = 12345
CharSettings['bank_account'] = 1000000

# In any other script
room = CharSettings['home_room']  # 12345
CharSettings scope:
"#{XMLData.game}:#{XMLData.name}"  # e.g., "GSIV:Tamsin"

GameSettings

Shared across all characters in the same game:
# Store game-wide data
GameSettings['town_square'] = 12345
GameSettings['bank_location'] = 'Town Square, Southeast'

# Any character can access
location = GameSettings['bank_location']
GameSettings scope:
"#{XMLData.game}:"  # e.g., "GSIV:"

SettingsProxy

Nested structures return a SettingsProxy for transparent modification:
Settings['config'] = { 'enabled' => true }

# Returns a SettingsProxy wrapping the hash
config = Settings['config']
echo config.class  # SettingsProxy

# Modifications auto-save
config['enabled'] = false  # Automatically saved
config['new_key'] = 'value'  # Automatically saved
Non-destructive operations (like .sort) on proxies may require explicit handling. Use .to_a or similar to materialize the data first.

Reading Settings

to_hash

Get a snapshot of all settings:
# Get all script settings as hash
all_settings = Settings.to_hash
echo all_settings.inspect

# Get character settings
char_data = CharSettings.to_hash
echo char_data.inspect

Existence Checks

# Check if setting exists
if Settings['my_setting'].nil?
  echo "Setting not configured"
else
  echo "Setting value: #{Settings['my_setting']}"
end

# With default
value = Settings['my_setting'] || 'default'

Common Patterns

Configuration Management

# Initialize defaults on first run
unless Settings['initialized']
  Settings['config'] = {
    'enabled' => true,
    'targets' => ['troll'],
    'max_hunts' => 10,
    'loot_types' => ['gem', 'skin']
  }
  Settings['initialized'] = true
end

# Load config
config = Settings['config']

Session Tracking

# Track sessions
Settings['sessions'] ||= 0
Settings['sessions'] += 1

Settings['last_run'] = Time.now.to_i
Settings['total_time'] ||= 0

# Calculate duration later
start_time = Time.now.to_i
at_exit {
  duration = Time.now.to_i - start_time
  Settings['total_time'] += duration
  echo "Session lasted #{duration} seconds"
  echo "Total time: #{Settings['total_time']} seconds"
}

Statistics Tracking

# Initialize stats
Settings['stats'] ||= {
  'kills' => 0,
  'deaths' => 0,
  'exp_gained' => 0,
  'items_found' => 0
}

# Update stats
def record_kill(creature)
  Settings['stats']['kills'] += 1
  echo "Total kills: #{Settings['stats']['kills']}"
end

def record_death
  Settings['stats']['deaths'] += 1
  echo "Total deaths: #{Settings['stats']['deaths']}"
end

Per-Character Configuration

# Store preferences per character
CharSettings['hunting_area'] = 'River's Rest'
CharSettings['preferred_weapon'] = 'broadsword'
CharSettings['home_room'] = 12345

# Access from any script
area = CharSettings['hunting_area']
fput "go2 #{CharSettings['home_room']}"

Advanced Usage

Direct Scope Access

# Access specific scope
scope = "#{XMLData.game}:#{XMLData.name}"
value = Settings.get_scoped_setting(scope, 'my_key')

# Create root proxy for scope
proxy = Settings.root_proxy_for(scope)
proxy['my_setting'] = 'value'

Custom Scopes

For advanced use cases:
# Create custom scope
custom_scope = "custom:shared"
proxy = Settings.root_proxy_for(custom_scope)

# All scripts can access this scope
proxy['shared_data'] = { 'value' => 123 }

Database Backend

Settings are stored in SQLite:
# From lib/common/settings/database_adapter.rb
class DatabaseAdapter
  def save_settings(script_name, settings_data, scope)
    serialized = Marshal.dump(settings_data)
    db.execute(
      "INSERT OR REPLACE INTO script_auto_settings(script, scope, hash) 
       VALUES(?, ?, ?)",
      [script_name, scope, SQLite3::Blob.new(serialized)]
    )
  end
end
Location: ~/.lich/data/lich.db3 Table: script_auto_settings

Migration from Old Settings

If migrating from Lich 4:
# Old style (deprecated)
Settings.auto = true
Settings.save
Settings.load

# New style (automatic)
Settings['key'] = 'value'  # Auto-saves
value = Settings['key']     # Auto-loads
The old Settings.save and Settings.load methods are now no-ops. Settings are automatically managed.

Error Handling

begin
  Settings['config'] = complex_data
rescue => e
  echo "Error saving settings: #{e.message}"
  respond e.backtrace.first
end

# Settings that fail to load return nil
config = Settings['config']
if config.nil?
  echo "Failed to load config, using defaults"
  config = default_config
end

Performance Considerations

Settings are cached in memory. Reading the same setting multiple times only hits the database once per session.
Settings are immediately written to the database. For frequent updates, batch changes in a single structure.
While deep nesting is supported, very deep structures (10+ levels) may have performance implications.

Debugging Settings

Enable debug logging:
# From lib/common/settings.rb
Settings.set_log_level(:debug)

# Log levels:
# :none   - No logging (default)
# :error  - Errors only
# :info   - Informational messages
# :debug  - Detailed debugging
Inspect settings:
# View all settings
require 'pp'
pp Settings.to_hash

# View specific scope
pp CharSettings.to_hash
pp GameSettings.to_hash

Example: Complete Configuration System

# hunter.lic - Full configuration example

# Initialize default configuration
unless Settings['config']
  Settings['config'] = {
    'hunting' => {
      'enabled' => true,
      'targets' => ['troll', 'orc'],
      'area' => 'River\'s Rest',
      'max_rt' => 5
    },
    'looting' => {
      'enabled' => true,
      'types' => ['gem', 'skin', 'jewelry'],
      'min_value' => 100
    },
    'safety' => {
      'min_health' => 50,
      'min_mana' => 20,
      'flee_on_death' => true
    }
  }
  
  Settings['stats'] = {
    'kills' => 0,
    'deaths' => 0,
    'sessions' => 0,
    'total_time' => 0
  }
end

# Load configuration
config = Settings['config']
stats = Settings['stats']

# Update session count
stats['sessions'] += 1
start_time = Time.now.to_i

# Use configuration
hunting_config = config['hunting']
if hunting_config['enabled']
  echo "Hunting enabled in #{hunting_config['area']}"
  hunting_config['targets'].each { |target|
    echo "Target: #{target}"
  }
end

# Safety check using config
def safe_to_hunt?
  safety = Settings['config']['safety']
  XMLData.health > safety['min_health'] &&
    XMLData.mana > safety['min_mana']
end

# Record stats
def record_kill
  Settings['stats']['kills'] += 1
end

# Cleanup
at_exit {
  duration = Time.now.to_i - start_time
  Settings['stats']['total_time'] += duration
  echo "Session stats:"
  echo "  Duration: #{duration}s"
  echo "  Kills: #{Settings['stats']['kills']}"
  echo "  Deaths: #{Settings['stats']['deaths']}"
}

Next Steps

Architecture

Understand Lich’s overall structure

Scripting

Learn about script execution

Build docs developers (and LLMs) love