Skip to main content

Overview

Hooks allow scripts to intercept and modify data as it flows through Lich. There are two types:
  • UpstreamHook: Intercepts commands going from the client to the game server
  • DownstreamHook: Intercepts data coming from the game server to the client
Both hook types use a similar API and allow multiple named hooks to be chained together. Location: lib/common/upstreamhook.rb, lib/common/downstreamhook.rb

UpstreamHook

Intercepts and modifies commands before they’re sent to the game server.

Class Methods

add

Register a new upstream hook with a unique name.
UpstreamHook.add(name, action) → true or false
name
String | Symbol
required
Unique identifier for this hook
action
Proc
required
Proc that takes a string (the command) and returns a modified string or nil to block the command
return
Boolean
Returns false if action is not a Proc, otherwise implicitly true
Behavior:
  • The Proc receives the command string as its argument
  • Return a modified string to change the command
  • Return nil to block the command entirely
  • Hook is associated with the current script
  • Overwrites any existing hook with the same name

remove

Remove a hook by name.
UpstreamHook.remove(name) → Object
name
String | Symbol
required
Name of the hook to remove
return
Object
The removed hook Proc, or nil if not found

run

Execute all registered hooks on a command string (called by framework).
UpstreamHook.run(client_string) → String or nil
client_string
String
required
The command string to process through all hooks
return
String | nil
The modified command string, or nil if any hook blocked it
Behavior:
  • Runs hooks in order of registration
  • Each hook receives the output of the previous hook
  • If any hook returns nil, processing stops and nil is returned
  • If a hook raises an exception, it’s automatically removed and an error is logged

list

Get names of all registered hooks.
UpstreamHook.listArray<String | Symbol>
return
Array
Array of hook names

sources

Display a formatted table of hooks and their source scripts.
UpstreamHook.sourcesnil
Behavior:
  • Displays a table with columns “Hook” and “Source”
  • Output is sent to the game client via Lich::Messaging.mono

hook_sources

Get the raw mapping of hook names to source scripts.
UpstreamHook.hook_sourcesHash
return
Hash
Hash mapping hook names to script names

DownstreamHook

Intercepts and modifies game server data before it reaches the client.

Class Methods

add

Register a new downstream hook with a unique name.
DownstreamHook.add(name, action) → true or false
name
String | Symbol
required
Unique identifier for this hook
action
Proc
required
Proc that takes a string (server data) and returns a modified string or nil to block it
return
Boolean
Returns false if action is not a Proc, otherwise implicitly true
Behavior:
  • The Proc receives a duplicate of the server string
  • Return a modified string to change what the client sees
  • Return nil to block the data from reaching the client
  • Hook is associated with the current script
  • Overwrites any existing hook with the same name

remove

Remove a hook by name.
DownstreamHook.remove(name) → Object
name
String | Symbol
required
Name of the hook to remove
return
Object
The removed hook Proc, or nil if not found

run

Execute all registered hooks on server data (called by framework).
DownstreamHook.run(server_string) → String or nil
server_string
String
required
The server data to process through all hooks
return
String | nil
The modified server string, or nil if any hook blocked it
Behavior:
  • Runs hooks in order of registration
  • Each hook receives the output of the previous hook (as a duplicate)
  • If any hook returns nil at any point, processing stops and nil is returned
  • If a hook raises an exception, it’s automatically removed and an error is logged
  • Only processes string data (skips non-string server messages)

list

Get names of all registered hooks.
DownstreamHook.listArray<String | Symbol>
return
Array
Array of hook names

sources

Display a formatted table of hooks and their source scripts.
DownstreamHook.sourcesnil
Behavior:
  • Displays a table with columns “Hook” and “Source”
  • Output is sent to the game client via Lich::Messaging.mono

hook_sources

Get the raw mapping of hook names to source scripts.
DownstreamHook.hook_sourcesHash
return
Hash
Hash mapping hook names to script names

Examples

Upstream Hook: Command Logging

UpstreamHook.add(:logger, proc { |command|
  File.open('commands.log', 'a') { |f| f.puts "#{Time.now}: #{command}" }
  command  # Pass through unchanged
})

Upstream Hook: Command Aliasing

UpstreamHook.add(:aliases, proc { |command|
  case command
  when /^lk$/
    'look'
  when /^inv$/
    'inventory'
  else
    command
  end
})

Upstream Hook: Blocking Commands

# Prevent accidentally attacking friendlies
UpstreamHook.add(:friendly_fire, proc { |command|
  if command =~ /^attack (Alice|Bob|Charlie)/i
    echo "Blocked: Cannot attack friendly!"
    nil  # Block the command
  else
    command
  end
})

Downstream Hook: Highlighting

DownstreamHook.add(:highlight, proc { |line|
  # Highlight player names in yellow
  line.gsub(/(Alice|Bob|Charlie)/, '<pushBold/>\1<popBold/>')
})

Downstream Hook: Filtering

# Hide spam messages
DownstreamHook.add(:spam_filter, proc { |line|
  if line =~ /^\[Advertisement\]/
    nil  # Block this line
  else
    line
  end
})

Downstream Hook: Trigger Actions

DownstreamHook.add(:auto_heal, proc { |line|
  if line =~ /Your wounds are severe/
    fput 'cast 1101'  # Auto-cast healing
  end
  line  # Pass through unchanged
})

Managing Hooks

# List all upstream hooks
puts UpstreamHook.list.inspect
# => [:logger, :aliases, :friendly_fire]

# Display hook sources
UpstreamHook.sources
# ┌──────────────┬────────────┐
# │ Hook         │ Source     │
# ├──────────────┼────────────┤
# │ logger       │ mylogger   │
# │ aliases      │ myaliases  │
# └──────────────┴────────────┘

# Remove a hook
UpstreamHook.remove(:aliases)

# Check what remains
puts UpstreamHook.list.inspect
# => [:logger, :friendly_fire]

Chaining Multiple Hooks

# First hook: expand aliases
UpstreamHook.add(:aliases, proc { |cmd|
  cmd.sub(/^lk$/, 'look')
})

# Second hook: add timestamp prefix
UpstreamHook.add(:timestamp, proc { |cmd|
  "[#{Time.now.strftime('%H:%M')}] #{cmd}"
})

# Input: "lk"
# After :aliases: "look"
# After :timestamp: "[14:30] look"
# Final output: "[14:30] look"

Hook Lifecycle

Registration

When you call add, the hook is:
  1. Validated (must be a Proc)
  2. Associated with the current script name
  3. Stored with the given name (overwrites if exists)

Execution

When data flows through:
  1. Framework calls .run() with the data
  2. Each hook processes the data in registration order
  3. Output of one hook becomes input to the next
  4. If any hook returns nil, the chain stops

Error Handling

If a hook raises an exception:
  1. The hook is automatically removed
  2. An error message is logged with the exception and first backtrace line
  3. Processing continues without that hook

Cleanup

Hooks should be removed when your script exits:
before_dying {
  UpstreamHook.remove(:my_hook)
  DownstreamHook.remove(:my_other_hook)
}

Best Practices

Use Descriptive Names

# Good
UpstreamHook.add(:highlight_player_names, proc { ... })

# Bad
UpstreamHook.add(:hook1, proc { ... })

Always Return or Explicitly Block

# Good: explicit return
UpstreamHook.add(:filter, proc { |cmd|
  cmd =~ /bad/ ? nil : cmd
})

# Bad: might return nil accidentally
UpstreamHook.add(:filter, proc { |cmd|
  if cmd =~ /bad/
    echo "Blocked!"
    # Forgot to return cmd!
  end
})

Clean Up on Exit

UpstreamHook.add(:my_hook, proc { |cmd| cmd })

before_dying {
  UpstreamHook.remove(:my_hook)
}

Don’t Modify the Original (Downstream)

DownstreamHook automatically passes a duplicate to your Proc, but avoid relying on mutation:
# Good: return modified string
DownstreamHook.add(:caps, proc { |line| line.upcase })

# Bad: mutate and return (works but unclear)
DownstreamHook.add(:caps, proc { |line| line.upcase! })

Differences Between Upstream and Downstream

AspectUpstreamHookDownstreamHook
Data sourceClient commandsGame server output
Data typeAlways stringsUsually strings (skips non-strings)
DuplicationReceives originalReceives .dup
nil behaviorStops chain immediatelyChecks for nil before each hook
Use casesCommand aliasing, blocking, loggingFiltering, highlighting, triggers

Build docs developers (and LLMs) love