-
Notifications
You must be signed in to change notification settings - Fork 53
cluster.lic Documentation
- Overview
- Setup
- Communication Patterns
- Contracts System
- Registry (Key-Value Storage)
- Complete Examples
- Best Practices
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.
- 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
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# Start the cluster script
;cluster
# With a remote Redis server
;cluster --url=redis://your-server:6379
# With debug logging
;cluster --debugCreate 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!"
endSend a message to all connected characters (except yourself).
# Send a broadcast
Cluster.broadcast(:begin_hunt)
# With data
Cluster.broadcast(:command, val: '?')# 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
endUse Cases:
- Start/stop scripts on all characters
- Announce events (hunt start, boss spawn, etc.)
- Synchronized actions
Send a message to a specific character.
# 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
)# 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()
endUse Cases:
- Direct commands to specific characters
- Room-specific actions
- Character-specific tasks
Send a message and wait for a response.
# 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# 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# 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# 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
}
endUse Cases:
- Check character status before coordinated actions
- Query information from other characters
- Synchronization checks
The Contracts system is an auction mechanism where characters bid to perform tasks. The character with the highest bid wins the contract.
- A character opens a contract (announces a task)
- Valid bidders evaluate if they can/want to do the task
- Each bidder submits a bid (0.0 to 1.0, higher is better)
- The highest bidder wins and performs the task
# 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)
}
})# 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'
)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)
)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}"
}
})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_bidto ensure minimum capability - Include relevant data in the contract (target, item IDs, etc.)
The Registry provides shared key-value storage across all connected characters.
# 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')# 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')# 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}"
endloot_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)# 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# 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)
}
})# 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# 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
)# 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# 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# 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# 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()
}
})# Use appropriate timeouts for different operations
quick = Cluster.request('John',
channel: :simple_check,
timeout: 2
)
complex = Cluster.request('John',
channel: :complex_task,
timeout: 30
)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# Start with debug logging
;cluster --debug# See who's connected
;e echo Cluster.connected.inspect
# Check if specific character is alive
;e echo Cluster.alive?('John')# 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)# 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# 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# 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
}
})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_openreturns a number (0.0-1.0) or -1 - Check
valid_biddersincludes 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
- Redis Documentation: https://redis.io/docs/
- Connection Pool: https://github.com/mperham/connection_pool
- Lich Repository: https://github.com/elanthia-online/lich-5
For issues or questions:
- Check if Redis is running:
redis-cli ping - Enable debug mode:
;cluster --debug - Review callback definitions
- Test with simple examples first