Strong control over lib using Context objects

This commit is contained in:
ChillerDragon 2022-11-05 10:35:40 +01:00
parent 1d3076e34b
commit 7689d2725a
5 changed files with 171 additions and 56 deletions

View file

@ -20,11 +20,57 @@
### @message [Integer]
### @author [[Player](#player)]
## Context
This class is the callback context.
When you hook into methods using a ``on_*`` method you can access its context.
This gives you the ability to read and modify the data before the default behavior processes it.
Or skip the default behavior and implement your own logic.
### #cancle
Call the ``cancle()`` on the context object to not run any default code for that event.
```ruby
client.on_map_change do |context|
# do nothing when a map change packet comes in
# skips the send ready packet code
context.cancle
end
```
### @data [Hash]
This hash holds all the current data. They keys might vary depending on the current context.
You can read and write those values. If you set an unused key the program will panic.
Here an example to see what keys you are given for a client info event.
```ruby
client = TeeworldsClient.new
client.on_client_info do |context|
p context.keys
# [:player, :chunk]
end
```
Here an example to modify all incoming player info to rename all player objects to yee.
Which is a bit weird but shows the power of the modding api.
```ruby
client = TeeworldsClient.new
client.on_client_info do |context|
context.data[:player].name = 'yee'
end
```
## TeeworldsClient
### #on_chat(&block)
**Parameter: block [Block]**
**Parameter: block [Block |[ChatMessage](#chatmessage)|]**
Takes a block that will be called when the client receives a chat message.
The block takes one parameter of type [ChatMessage](#chatmessage).
@ -40,6 +86,28 @@ end
client.connect('localhost', 8303, detach: true)
```
### #on_map_change(&block)
**Parameter: block [Block |[context](#context)|]**
Takes a block that will be called when the client receives a map change packet.
**Example:**
```ruby
client = TeeworldsClient.new
client.on_map_change do |context|
puts "Got new map!"
# skip default behavior
# in this case do not send the ready packet
context.cancle
end
client.connect('localhost', 8303, detach: true)
```
### connect(ip, port, options)

View file

@ -2,6 +2,33 @@ require_relative 'player'
require_relative 'packer'
require_relative 'chat_message'
class Context
attr_reader :old_data
attr_accessor :data
def initialize(keys = {})
@cancle = false
@old_data = keys
@data = keys
end
def verify
@data.each do |key, value|
next if @old_data.key? key
raise "Error: invalid data key '#{key}'\n valid keys: #{@old_data.keys}"
end
end
def cancled?
@cancle
end
def cancle
@cancle = true
end
end
class GameClient
attr_accessor :players
@ -10,7 +37,12 @@ class GameClient
@players = {}
end
def on_player_join(chunk)
def on_client_enter(chunk)
puts "ON CLIENT ENTER"
puts chunk
end
def on_client_info(chunk)
# puts "Got playerinfo flags: #{chunk.flags}"
u = Unpacker.new(chunk.data[1..])
player = Player.new(
@ -23,10 +55,41 @@ class GameClient
# skinparts and the silent flag
# are currently ignored
context = Context.new(
player: player,
chunk: chunk
)
if @client.hooks[:client_info]
@client.hooks[:client_info].call(context)
context.verify
return if context.cancled?
end
player = context.data[:player]
@players[player.id] = player
puts "'#{player.name}' joined the game"
end
def on_ready_to_enter(chunk)
@client.send_enter_game
end
def on_connected
@client.send_msg_startinfo
end
def on_map_change(chunk)
context = Context.new(chunk: chunk)
if @client.hooks[:map_change]
@client.hooks[:map_change].call(context)
context.verify
return if context.cancled?
end
# ignore mapdownload at all times
# and claim to have the map
@client.send_msg_ready
end
def on_chat(chunk)
# 06 01 00 40 41 00
# msg mode cl_id trgt A nullbyte?

View file

@ -1,7 +1,7 @@
class Player
attr_reader :id, :local, :team, :name, :clan
attr_reader :country
attr_reader :skin_parts, :skin_custom_colors, :skin_colors
attr_accessor :id, :local, :team, :name, :clan
attr_accessor :country
attr_accessor :skin_parts, :skin_custom_colors, :skin_colors
def initialize(data = {})
@id = data[:id] || -1

View file

@ -56,6 +56,14 @@ class TeeworldsClient
@hooks[:chat] = block
end
def on_map_change(&block)
@hooks[:map_change] = block
end
def on_client_info(&block)
@hooks[:client_info] = block
end
def send_chat(str)
@netbase.send_packet(
NetChunk.create_vital_header({vital: true}, 4 + str.length) +
@ -128,18 +136,6 @@ class TeeworldsClient
end
end
private
def connection_loop
until @signal_disconnect
tick
# todo: proper tick speed sleep
sleep 0.001
end
@thread_running = false
@signal_disconnect = false
end
def send_msg(data)
@netbase.send_packet(data)
end
@ -246,36 +242,7 @@ class TeeworldsClient
puts "got NET_CTRLMSG_CLOSE"
end
def get_strings(data)
strings = []
str = ""
data.chars.each do |b|
# use a bunch of control characters as delimiters
# https://en.wikipedia.org/wiki/Control_character
if (0x00..0x0F).to_a.include?(b.unpack('C*').first)
strings.push(str) unless str.length.zero?
str = ""
next
end
str += b
end
strings
end
def on_msg_map_change(data)
mapname = get_strings(data).first
puts "map: #{mapname}"
send_msg_ready()
end
def on_motd(data)
puts "motd: #{get_strings(data)}"
end
def on_playerinfo(data)
puts "playerinfo: #{get_strings(data).join(', ')}"
end
private
# CClient::ProcessConnlessPacket
def on_ctrl_message(msg, data)
@ -290,15 +257,12 @@ class TeeworldsClient
end
end
def on_emoticon(chunk)
# puts "Got emoticon flags: #{chunk.flags}"
end
def on_message(chunk)
case chunk.msg
when NETMSGTYPE_SV_READYTOENTER then send_enter_game
when NETMSGTYPE_SV_CLIENTINFO then @game_client.on_player_join(chunk)
when NETMSGTYPE_SV_EMOTICON then on_emoticon(chunk)
when NETMSGTYPE_SV_READYTOENTER then @game_client.on_ready_to_enter(chunk)
when NETMSGTYPE_SV_CLIENTINFO then @game_client.on_client_info(chunk)
when NETMSGTYPE_DE_CLIENTENTER then @game_client.on_client_enter(chunk)
when NETMSGTYPE_SV_EMOTICON then @game_client.on_emoticon(chunk)
when NETMSGTYPE_SV_CHAT then @game_client.on_chat(chunk)
else
if @verbose
@ -315,11 +279,11 @@ class TeeworldsClient
puts "proccess chunk with msg: #{chunk.msg}"
case chunk.msg
when NETMSG_MAP_CHANGE
send_msg_ready
@game_client.on_map_change(chunk)
when NETMSG_SERVERINFO
puts "ignore server info for now"
when NETMSG_CON_READY
send_msg_startinfo
@game_client.on_connected
when NETMSG_NULL
# should we be in alert here?
else
@ -388,5 +352,16 @@ class TeeworldsClient
send_chat("hello world")
end
end
def connection_loop
until @signal_disconnect
tick
# todo: proper tick speed sleep
sleep 0.001
end
@thread_running = false
@signal_disconnect = false
end
end

View file

@ -34,6 +34,15 @@ client.on_chat do |msg|
puts "[chat] #{msg}"
end
client.on_map_change do |ctx|
puts "GOT MAP CHANGOS"
end
client.on_client_info do |ctx|
puts "player info: #{ctx.data[:player]}"
ctx.data[:player].name = "yee"
end
Signal.trap('INT') do
client.disconnect
end