Skip to content

cluster.lic Documentation

Ryan P. McKinnon edited this page Nov 18, 2025 · 5 revisions

Cluster System Documentation

Table of Contents

  1. Overview
  2. Setup
  3. Communication Patterns
  4. Contracts System
  5. Registry (Key-Value Storage)
  6. Complete Examples
  7. Best Practices

Overview

This documentation was created by Claude.AI. There may be slight nuances of code that may need adjusting due to this. Please feel free to update the wiki to adjust for this if errors found.

The Cluster system enables distributed communication between multiple Lich5 scripts running on different characters using Redis pub/sub. This allows characters to coordinate actions, share information, and collaborate on tasks.

Key Features

  • Broadcasts: Send messages to all connected characters
  • Casts: Send messages to a specific character
  • Requests: Send messages and wait for a response
  • Contracts: Auction-style system where characters bid to perform tasks
  • Registry: Shared key-value storage across characters

Setup

Installation

You need to first have a redis server you are going to connect to, either self hosted or in the cloud. This documentation does not cover that, there's plenty of resources on the internet to assist with how to host a redis server locally or pay to use an online one.

You will need to also have the redis and connection_pool Ruby gems installed on your local machine you are looking to run cluster.lic from. Currently the connection_pool gem is not utilized, but may be updated in the near future to assist with thread safety, resource management, and performance.

To install these gems, open a terminal and install them via:

gem install redis connection_pool

Starting the Script

# Start the cluster script
;cluster

# With a remote Redis server
;cluster --url=redis://your-server:6379

# With debug logging
;cluster --debug

Creating Your Callbacks File

Create a file called cluster_callbacks.rb in your scripts/ directory. This file will be automatically loaded by the cluster script.

# scripts/cluster_callbacks.rb

# Your callbacks go here
Cluster.on_broadcast(:hello) do |_, req|
  echo "#{req.from} says hello!"
end

Communication Patterns

1. Broadcasts (One-to-Many)

Send a message to all connected characters (except yourself).

Sending a Broadcast

# Send a broadcast
Cluster.broadcast(:begin_hunt)

# With data
Cluster.broadcast(:command, val: '?')

Receiving a Broadcast

# In cluster_callbacks.rb
Cluster.on_broadcast(:begin_hunt) do |_, req|
  echo "Starting hunt routine from #{req.from}"
  Script.start('hunter')
end

Cluster.on_broadcast(:command) do |_, req|
  # Execute command on all characters
  do_client req.val
end

Use Cases:

  • Start/stop scripts on all characters
  • Announce events (hunt start, boss spawn, etc.)
  • Synchronized actions

2. Casts (One-to-One)

Send a message to a specific character.

Sending a Cast

# Send to specific character
Cluster.cast('John', channel: :command, val: '?')

# More complex example
Cluster.cast(Settings[:force_skinner], 
  channel: :skin_room,
  roomId: Room.current.id
)

Receiving a Cast

# In cluster_callbacks.rb
Cluster.on_cast(:command) do |_, req|
  do_client req.val
  echo "Executed command from #{req.from}"
end

Cluster.on_cast(:skin_room) do |_, req|
  # Only act if we're in the same room
  next unless Room.current.id.eql?(req.roomId)
  ELoot::Loot.skin()
end

Use Cases:

  • Direct commands to specific characters
  • Room-specific actions
  • Character-specific tasks

3. Requests (Request-Response)

Send a message and wait for a response.

Sending a Request

# Send request and wait for response (blocks)
response = Cluster.request('John', channel: :ready_to_move)

if response.ready
  echo "John is ready!"
else
  echo "John is not ready"
end

# With custom timeout (default is 5 seconds)
response = Cluster.request('John', 
  channel: :ready_to_move,
  timeout: 10
)

# Check for errors
if response.is_a?(Exception)
  echo "Request failed: #{response.message}"
end

Async Requests (Non-blocking)

# Start request in background
req_thread = Cluster.async_request('John', channel: :ready_to_move)

# Do other stuff...
do_something_else()

# Get the result when ready
response = req_thread.value

Map Requests (Multiple Characters)

# Request from multiple characters at once
characters = ['John', 'Paul', 'Ringo']
responses = Cluster.map(characters, channel: :ready_to_move)

responses.each do |resp|
  if resp.is_a?(Exception)
    echo "Failed: #{resp.message}"
  else
    echo "#{resp.from} ready: #{resp.ready}"
  end
end

Handling Requests

# In cluster_callbacks.rb
Cluster.on_request(:ready_to_move) do |_, req|
  {
    ready: checkrt.eql?(0) && standing?,
    health: percenthealth,
    mana: percentmana
  }
end

Cluster.on_request(:roundtime) do |_, req|
  {
    hard: checkrt,
    soft: checkcastrt
  }
end

Cluster.on_request(:stats) do |_, req|
  {
    name: Char.name,
    room: Room.current.id,
    exp: Exp.exp,
    mana: checkmana
  }
end

Use Cases:

  • Check character status before coordinated actions
  • Query information from other characters
  • Synchronization checks

Contracts System

The Contracts system is an auction mechanism where characters bid to perform tasks. The character with the highest bid wins the contract.

How Contracts Work

  1. A character opens a contract (announces a task)
  2. Valid bidders evaluate if they can/want to do the task
  3. Each bidder submits a bid (0.0 to 1.0, higher is better)
  4. The highest bidder wins and performs the task

Defining a Contract Handler

# In cluster_callbacks.rb
Contracts.on_contract(:poisoned, { 
  # Called when someone opens this contract
  contract_open: -> req {
    # Return -1 or don't bid if you can't/won't do it
    return -1 unless Spell[114].known?
    return -1 unless Spell[114].affordable?
    
    # Return a bid between 0.0 and 1.0
    # Higher mana = higher bid
    percentmana / 100.0
  },
  
  # Called if you win the contract
  contract_win: -> req {
    echo "Casting Unpoison on #{req.from}"
    Spell[114].cast(req.from)
  }
})

Opening a Contract

# Simple contract
Contracts.collect_bids(:poisoned, 
  valid_bidders: GameObj.pcs.map(&:noun)
)

# With minimum bid requirement
Contracts.collect_bids(:heal_me,
  valid_bidders: ['John', 'Paul', 'Ringo'],
  min_bid: 0.5
) do |contract|
  # This block runs if no valid bids received
  echo "No one could heal me :("
end

# With additional data
Contracts.collect_bids(:box,
  valid_bidders: GameObj.pcs.map(&:noun),
  box_id: target_box.id,
  difficulty: 'hard'
)

Contract Examples

Healing Contract

Contracts.on_contract(:heal, {
  contract_open: -> req {
    return -1 unless Spell[1101].known?  # Heal
    return -1 unless Spell[1101].affordable?
    return -1 if percentmana < 30
    
    # Bid based on mana available
    percentmana / 100.0
  },
  
  contract_win: -> req {
    target = Player[req.from]
    Spell[1101].cast(target)
  }
})

# To use it:
Contracts.collect_bids(:heal, 
  valid_bidders: room_pcs.map(&:noun)
)

Lockpicking Contract

Contracts.on_contract(:box, {
  contract_open: -> req {
    return -1 unless Skill["Picking Locks"].rank > 50
    return -1 if busy?
    
    # Bid based on skill and availability
    (Skill["Picking Locks"].rank / 300.0) * (1.0 - (checkrt / 10.0))
  },
  
  contract_win: -> req {
    box = GameObj[req.box_id]
    fput "open ##{box.id}"
  }
})

Buff Contract

Contracts.on_contract(:buff, {
  contract_open: -> req {
    spell_num = req.spell
    return -1 unless Spell[spell_num].known?
    return -1 unless Spell[spell_num].affordable?
    
    percentmana / 100.0
  },
  
  contract_win: -> req {
    target = Player[req.from]
    Spell[req.spell].cast(target)
  }
})

# To use it:
Contracts.collect_bids(:buff,
  valid_bidders: room_pcs.map(&:noun),
  spell: 1605  # Self Control
)

Contract Best Practices:

  • Always validate you can perform the task before bidding
  • Bid between 0.0 and 1.0 (higher = more likely to win)
  • Return -1 or don't return a bid if you can't/won't do the task
  • Use min_bid to ensure minimum capability
  • Include relevant data in the contract (target, item IDs, etc.)

Registry (Key-Value Storage)

The Registry provides shared key-value storage across all connected characters.

Basic Usage

# Create a registry (namespace defaults to character name)
reg = Cluster.registry

# Store data
reg.put('last_hunt_room', Room.current.id)
reg.put('party_leader', Char.name)
reg.put('loot_count', { boxes: 5, skins: 12 })

# Retrieve data
room_id = reg.get('last_hunt_room')
leader = reg.get('party_leader')

# Check if key exists
if reg.exists?('party_leader')
  echo "Party leader is #{reg.get('party_leader')}"
end

# Delete data
reg.delete('old_data')

Namespaced Registry

# Create a shared registry for all characters
party_reg = Cluster.registry('party')

# All characters can access this data
party_reg.put('meeting_room', 12345)
party_reg.put('hunt_target', 'trolls')

# On any character:
meeting = party_reg.get('meeting_room')

Registry Examples

Hunt Coordination

# Leader sets hunt info
hunt_reg = Cluster.registry('hunt')
hunt_reg.put('active', true)
hunt_reg.put('area', 'Trollfang')
hunt_reg.put('leader', Char.name)

# Followers check hunt status
if hunt_reg.get('active')
  area = hunt_reg.get('area')
  echo "Active hunt in #{area}"
end

Loot Tracking

loot_reg = Cluster.registry('loot')

# Track total loot
current = loot_reg.get('total_boxes') || 0
loot_reg.put('total_boxes', current + 1)

# Track per-character stats
my_stats = loot_reg.get("stats_#{Char.name}") || {boxes: 0, skins: 0}
my_stats[:boxes] += 1
loot_reg.put("stats_#{Char.name}", my_stats)

Complete Examples

Example 1: Coordinated Hunting

# In cluster_callbacks.rb

# Start hunt on all characters
Cluster.on_broadcast(:start_hunt) do |_, req|
  Script.start('hunting', [req.area])
end

# Stop hunt on all characters
Cluster.on_broadcast(:stop_hunt) do |_, req|
  Script.kill('hunting')
end

# Check if everyone is ready
Cluster.on_request(:hunt_ready) do |_, req|
  {
    ready: checkrt.eql?(0) && standing?,
    health: percenthealth,
    mana: percentmana,
    room: Room.current.id
  }
end

# Usage:
# Leader broadcasts start
Cluster.broadcast(:start_hunt, area: 'trolls')

# Leader checks readiness
party = ['John', 'Paul', 'Ringo']
statuses = Cluster.map(party, channel: :hunt_ready)

all_ready = statuses.all? { |s| !s.is_a?(Exception) && s.ready }
if all_ready
  echo "Everyone is ready!"
else
  echo "Waiting for party members..."
end

Example 2: Automated Healing System

# In cluster_callbacks.rb

# Request healing when needed
def request_heal
  return if percenthealth > 70
  
  healers = room_pcs.select { |name| 
    Cluster.connected.include?(name) && name != Char.name
  }
  
  Contracts.collect_bids(:emergency_heal,
    valid_bidders: healers,
    urgency: 100 - percenthealth
  ) do |contract|
    echo "No healers available!"
    # Fallback: use herb or retreat
  end
end

# Respond to healing requests
Contracts.on_contract(:emergency_heal, {
  contract_open: -> req {
    return -1 unless Spell[1101].known?
    return -1 unless Spell[1101].affordable?
    return -1 if percentmana < 20
    
    # Higher urgency + more mana = higher bid
    urgency_factor = req.urgency / 100.0
    mana_factor = percentmana / 100.0
    
    (urgency_factor * 0.7) + (mana_factor * 0.3)
  },
  
  contract_win: -> req {
    target = Player[req.from]
    unless target
      echo "Can't find #{req.from}!"
      return
    end
    
    echo "Healing #{req.from} (urgency: #{req.urgency})"
    Spell[1101].cast(target)
  }
})

Example 3: Box Pickup System

# In cluster_callbacks.rb

# Announce when box is ready
Cluster.on_cast(:box_ready) do |_, req|
  next unless Room.current.id.eql?(req.room_id)
  
  box = GameObj[req.box_id]
  if box
    fput "get ##{box.id}"
    echo "Picked up box from #{req.from}"
  end
end

# Request box pickup
def request_box_pickup(box)
  # Find available characters in room
  available = room_pcs.select { |name|
    Cluster.connected.include?(name) && name != Char.name
  }
  
  # Cast to first available
  if available.any?
    Cluster.cast(available.first,
      channel: :box_ready,
      box_id: box.id,
      room_id: Room.current.id
    )
  end
end

Example 4: Skinning Rotation

# In cluster_callbacks.rb

# Track who's turn it is to skin
@skin_rotation = []
@current_skinner = nil

Cluster.on_broadcast(:update_skinners) do |_, req|
  @skin_rotation = req.skinners
  @current_skinner = req.current
end

Cluster.on_cast(:skin_corpse) do |_, req|
  next unless Room.current.id.eql?(req.room_id)
  next unless Char.name.eql?(@current_skinner)
  
  corpse = GameObj[req.corpse_id]
  if corpse
    fput "skin ##{corpse.id}"
    
    # Rotate to next skinner
    next_skinner = @skin_rotation[(@skin_rotation.index(@current_skinner) + 1) % @skin_rotation.size]
    Cluster.broadcast(:update_skinners, 
      skinners: @skin_rotation,
      current: next_skinner
    )
  end
end

# Initialize rotation (run on leader)
skinners = Cluster.connected.select { |name| 
  # Check if they have skinning skill
  response = Cluster.request(name, channel: :has_skinning)
  !response.is_a?(Exception) && response.can_skin
}

Cluster.broadcast(:update_skinners,
  skinners: skinners,
  current: skinners.first
)

Best Practices

1. Connection Management

# Check who's connected before sending
if Cluster.connected.include?('John')
  Cluster.cast('John', channel: :command, val: 'inv')
end

# Get connected characters minus yourself
others = Cluster.connected - [Char.name]

# Get PCs in room who are also connected
connected_in_room = room_pcs & Cluster.connected

2. Error Handling

# Always check request responses
response = Cluster.request('John', channel: :stats)

if response.is_a?(Exception)
  echo "Request failed: #{response.message}"
elsif response.is_a?(TimeoutError)
  echo "Request timed out - is John connected?"
else
  echo "Stats: #{response.inspect}"
end

3. Defensive Programming

# Check prerequisites before acting
Cluster.on_cast(:buff_me) do |_, req|
  # Validate target exists
  target = Player[req.from]
  next unless target
  
  # Validate spell
  spell = Spell[req.spell_num]
  next unless spell && spell.known? && spell.affordable?
  
  # Validate we're in same room
  next unless target.room_id == Room.current.id
  
  spell.cast(target)
end

4. Using next vs return

# In callbacks, use 'next' to skip processing
Cluster.on_cast(:room_action) do |_, req|
  next unless Room.current.id.eql?(req.room_id)
  # Do action
end

# Use 'return' in contract bids
Contracts.on_contract(:task, {
  contract_open: -> req {
    return -1 unless can_do_task?
    calculate_bid()
  }
})

5. Timeouts

# Use appropriate timeouts for different operations
quick = Cluster.request('John', 
  channel: :simple_check, 
  timeout: 2
)

complex = Cluster.request('John',
  channel: :complex_task,
  timeout: 30
)

6. Data Validation

Cluster.on_cast(:teleport_to) do |_, req|
  # Validate required fields
  unless req.respond_to?(:target_room)
    echo "Missing target_room in request"
    next
  end
  
  room_id = req.target_room.to_i
  if room_id <= 0
    echo "Invalid room ID: #{req.target_room}"
    next
  end
  
  # Proceed with validated data
  Script.run('go2', room_id.to_s)
end

Debugging

Enable Debug Mode

# Start with debug logging
;cluster --debug

Check Connected Characters

# See who's connected
;e echo Cluster.connected.inspect

# Check if specific character is alive
;e echo Cluster.alive?('John')

Test Communication

# Test broadcast
;e Cluster.broadcast(:test, message: 'hello')

# Test cast
;e Cluster.cast('John', channel: :test, data: 123)

# Test request
;e p Cluster.request('John', channel: :test)

Common Patterns

Pattern: Room Coordination

# Only act if in the same room as sender
Cluster.on_cast(:room_action) do |_, req|
  next unless Room.current.id.eql?(req.room_id)
  perform_action()
end

Pattern: State Checking

# Check multiple states before acting
Cluster.on_request(:ready_check) do |_, req|
  {
    standing: standing?,
    rt_clear: checkrt.eql?(0),
    mana_ok: percentmana > 30,
    health_ok: percenthealth > 50
  }
end

Pattern: Resource Pooling

# Characters contribute resources to a pool
Contracts.on_contract(:need_mana, {
  contract_open: -> req {
    return -1 if percentmana < 50
    (percentmana - 50) / 50.0  # Bid based on excess mana
  },
  
  contract_win: -> req {
    target = Player[req.from]
    Spell[1711].cast(target)  # Mana Transfer
  }
})

Troubleshooting

Problem: Characters not seeing each other

  • Check Redis is running: redis-cli ping
  • Verify all characters using same Redis server
  • Check firewall settings

Problem: Requests timing out

  • Increase timeout parameter
  • Check target character has callback defined
  • Verify target is actually connected

Problem: Contract bids not working

  • Ensure contract_open returns a number (0.0-1.0) or -1
  • Check valid_bidders includes character name
  • Verify both callbacks (open and win) are defined

Problem: Registry data not persisting

  • Redis data is in-memory by default
  • Configure Redis persistence if needed
  • Check Redis memory limits

Additional Resources

Support

For issues or questions:

  1. Check if Redis is running: redis-cli ping
  2. Enable debug mode: ;cluster --debug
  3. Review callback definitions
  4. Test with simple examples first