Skip to main content

Overview

Lich scripts run in Ruby threads, allowing you to perform multiple operations concurrently. Understanding threading is essential for creating sophisticated scripts that can:
  • Monitor multiple conditions simultaneously
  • Run background tasks while performing main logic
  • React to game events in real-time
  • Coordinate multiple scripts
Scripts share access to game buffers and resources. Improper thread management can cause race conditions and unexpected behavior.

Basic Threading

Creating Threads

# Create a background thread
thread = Thread.new do
  loop do
    echo "Background task running"
    pause 5
  end
end

# Main script continues
loop do
  echo "Main task running"
  pause 3
end

Thread Lifecycle

# Start a thread
worker = Thread.new do
  # Thread work
  pause 10
  echo "Thread complete"
end

# Check if thread is alive
if worker.alive?
  echo "Worker is still running"
end

# Wait for thread to complete
worker.join
echo "Worker finished"

# Kill a thread
worker.kill

Script Thread Groups

Each script has its own thread group for managing child threads:
script = Script.current

# Add thread to script's group
worker = Thread.new do
  echo "I belong to the script"
end

script.thread_group.add(worker)

# List all threads in script
script.thread_group.list.each do |t|
  echo "Thread: #{t.inspect}"
end

# Check thread membership
if script.has_thread?(Thread.current)
  echo "Current thread is part of script"
end
When a script exits, all threads in its thread group are automatically terminated.

Thread Priority

Manage thread execution priority:
# Default priority is 0
# Higher numbers = higher priority (max 3)
# Lower numbers = lower priority

# Set current thread priority
Thread.current.priority = 2

# Set priority for script threads
setpriority(1)

# Background thread with lower priority
Thread.new do
  Thread.current.priority = -1
  # Low priority work
end
Do not set priority above 3 - this can interfere with Lich’s core send/receive threads.

Practical Threading Patterns

Watchdog Thread

Monitor conditions and react automatically:
# Monitor health and auto-heal
watchdog = Thread.new do
  loop do
    if Char.health < 50
      echo "Low health! Healing..."
      fput "drink my potion"
      waitfor "You feel better"
    end
    pause 2
  end
end

script.thread_group.add(watchdog)

# Main script continues
loop do
  # Main hunting logic
  fput "attack monster"
  waitfor "You swing"
end

Event Handlers

Respond to specific game events:
# Death monitor
death_handler = Thread.new do
  waitfor "You have been slain"
  echo "You died! Stopping hunting."
  stop_script("hunting_script")
end

script.thread_group.add(death_handler)

Periodic Tasks

# Auto-save every 5 minutes
autosave = Thread.new do
  loop do
    pause 300  # 5 minutes
    echo "Auto-saving..."
    Settings.save
  end
end

script.thread_group.add(autosave)

Worker Pool Pattern

workers = []

# Create multiple worker threads
3.times do |i|
  worker = Thread.new do
    loop do
      # Get work from queue
      task = @work_queue.pop
      echo "Worker #{i} processing: #{task}"
      # Process task
    end
  end
  workers << worker
  script.thread_group.add(worker)
end

# Add work to queue
@work_queue = Queue.new
@work_queue << "task1"
@work_queue << "task2"

Thread Communication

Thread-Local Variables

thread = Thread.new do
  Thread.current[:name] = "Worker"
  Thread.current[:counter] = 0
  
  loop do
    Thread.current[:counter] += 1
    pause 1
  end
end

# Access from main thread
pause 3
echo "#{thread[:name]}: #{thread[:counter]}"

Shared State with Mutex

counter = 0
mutex = Mutex.new

threads = 5.times.map do
  Thread.new do
    100.times do
      mutex.synchronize do
        counter += 1
      end
    end
  end
end

threads.each(&:join)
echo "Counter: #{counter}"  # Always 500

Queue for Safe Communication

queue = Queue.new

# Producer thread
producer = Thread.new do
  10.times do |i|
    queue << i
    pause 1
  end
  queue << :done
end

# Consumer thread
consumer = Thread.new do
  loop do
    item = queue.pop
    break if item == :done
    echo "Processed: #{item}"
  end
end

producer.join
consumer.join

Watchfor in Threads

Special hook-based watching:
# Script's watchfor hash
script = Script.current

# Add a watch pattern
script.watchfor[/monster attacks/] = proc {
  echo "Monster attacked!"
  fput "dodge"
}

# Remove watch
script.watchfor.delete(/monster attacks/)

# Clear all watches
script.watchfor.clear
Watchfor patterns create new threads automatically when triggered.

Health Monitoring

Dedicated health monitoring thread:
# Watch health and execute code when low
watchhealth(50) {
  echo "Health below 50!"
  fput "run away"
}

# Equivalent to:
Thread.new do
  wait_while { Char.health > 50 }
  echo "Health below 50!"
  fput "run away"
end

Complete Multi-threaded Example

# Script: multi_threaded_hunter.lic
# version: 1.0.0

script = Script.current

echo "Starting multi-threaded hunter..."

# Health monitor thread
health_monitor = Thread.new do
  loop do
    if Char.health < 30
      echo "DANGER: Low health!"
      fput "stance defensive"
    elsif Char.health > 80 && checkstance("def")
      fput "stance offensive"
    end
    pause 2
  end
end

# Loot collector thread
loot_collector = Thread.new do
  loop do
    loot = checkloot
    if loot && loot.any?
      waitrt?
      echo "Looting: #{loot.join(', ')}"
      loot.each { |item| fput "get #{item}" }
    end
    pause 3
  end
end

# Experience tracker
exp_tracker = Thread.new do
  last_exp = XMLData.player_mind
  loop do
    pause 60
    current_exp = XMLData.player_mind
    if current_exp != last_exp
      echo "Experience update: #{current_exp}"
      last_exp = current_exp
    end
  end
end

# Add all threads to script group
[health_monitor, loot_collector, exp_tracker].each do |t|
  script.thread_group.add(t)
end

# Main hunting loop
loop do
  targets = checknpcs("troll", "ogre")
  
  if targets
    waitrt?
    fput "attack #{targets.first}"
    waitfor "You swing"
  else
    echo "No targets, waiting..."
    pause 5
  end
end

# Cleanup handled automatically when script exits

Thread Safety Considerations

Game Buffer Access

# Scripts share the game buffer
# Use script-specific buffers for isolation

script = Script.current

# Downstream buffer (game output)
script.downstream_buffer

# Upstream buffer (user input)
script.upstream_buffer

# Unique buffer (inter-script)
script.unique_buffer

Shared Resources

# Be careful with shared state
@shared_counter = 0
@mutex = Mutex.new

def increment
  @mutex.synchronize do
    @shared_counter += 1
  end
end

# Use in threads
threads = 10.times.map do
  Thread.new { 100.times { increment } }
end

threads.each(&:join)
echo "Count: #{@shared_counter}"  # Should be 1000

Database Access

# SQLite isn't thread-safe by default
db = Script.db
mutex = Mutex.new

Thread.new do
  mutex.synchronize do
    db.execute("INSERT INTO data VALUES (?)", ["value"])
  end
end

Debugging Threads

# List all threads in script
script.thread_group.list.each_with_index do |t, i|
  echo "Thread #{i}: #{t.status}"
  echo "  Priority: #{t.priority}"
  echo "  Alive: #{t.alive?}"
end

# Thread status values:
# "run"      - currently running
# "sleep"    - sleeping or waiting
# false      - terminated normally
# nil        - terminated with exception
# "aborting" - aborting

Best Practices

Add to Thread Group

Always add child threads to script’s thread group

Handle Cleanup

Use before_dying to clean up thread resources

Synchronize Access

Use Mutex for shared mutable state

Avoid High Priority

Keep thread priority ≤ 3 to avoid system issues

Next Steps

Global Methods

Learn about available script methods

Hooks

Intercept game data with hooks

Build docs developers (and LLMs) love