-
Notifications
You must be signed in to change notification settings - Fork 65
cluster.lic Documentation
- Overview
- Setup
- Communication Patterns
- Contracts System
- Registry (Key-Value Storage)
- Complete Examples
- Best Practices
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
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