teeworlds_network 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/lib/array.rb +28 -0
  3. data/lib/bytes.rb +139 -0
  4. data/lib/chunk.rb +177 -0
  5. data/lib/config.rb +44 -0
  6. data/lib/context.rb +29 -0
  7. data/lib/game_client.rb +196 -0
  8. data/lib/game_server.rb +122 -0
  9. data/lib/messages/cl_emoticon.rb +63 -0
  10. data/lib/messages/cl_say.rb +52 -0
  11. data/lib/messages/client_info.rb +141 -0
  12. data/lib/messages/game_info.rb +25 -0
  13. data/lib/messages/input_timing.rb +48 -0
  14. data/lib/messages/maplist_entry_add.rb +44 -0
  15. data/lib/messages/maplist_entry_rem.rb +44 -0
  16. data/lib/messages/rcon_cmd_add.rb +52 -0
  17. data/lib/messages/rcon_cmd_rem.rb +44 -0
  18. data/lib/messages/rcon_line.rb +44 -0
  19. data/lib/messages/server_info.rb +64 -0
  20. data/lib/messages/server_settings.rb +23 -0
  21. data/lib/messages/start_info.rb +129 -0
  22. data/lib/messages/sv_client_drop.rb +57 -0
  23. data/lib/models/chat_message.rb +39 -0
  24. data/lib/models/map.rb +57 -0
  25. data/lib/models/net_addr.rb +18 -0
  26. data/lib/models/packet_flags.rb +42 -0
  27. data/lib/models/player.rb +56 -0
  28. data/lib/models/token.rb +20 -0
  29. data/lib/net_base.rb +106 -0
  30. data/lib/network.rb +148 -0
  31. data/lib/packer.rb +194 -0
  32. data/lib/packet.rb +73 -0
  33. data/lib/snapshot/events/damage.rb +24 -0
  34. data/lib/snapshot/events/death.rb +20 -0
  35. data/lib/snapshot/events/explosion.rb +16 -0
  36. data/lib/snapshot/events/hammer_hit.rb +16 -0
  37. data/lib/snapshot/events/sound_world.rb +20 -0
  38. data/lib/snapshot/events/spawn.rb +16 -0
  39. data/lib/snapshot/items/character.rb +43 -0
  40. data/lib/snapshot/items/client_info.rb +22 -0
  41. data/lib/snapshot/items/flag.rb +22 -0
  42. data/lib/snapshot/items/game_data.rb +22 -0
  43. data/lib/snapshot/items/game_data_flag.rb +24 -0
  44. data/lib/snapshot/items/game_data_team.rb +21 -0
  45. data/lib/snapshot/items/laser.rb +24 -0
  46. data/lib/snapshot/items/pickup.rb +22 -0
  47. data/lib/snapshot/items/player_info.rb +22 -0
  48. data/lib/snapshot/items/player_input.rb +32 -0
  49. data/lib/snapshot/items/projectile.rb +25 -0
  50. data/lib/snapshot/items/spectator_info.rb +23 -0
  51. data/lib/snapshot/items/tune_params.rb +90 -0
  52. data/lib/snapshot/snap_event_base.rb +14 -0
  53. data/lib/snapshot/snap_item_base.rb +86 -0
  54. data/lib/snapshot/unpacker.rb +301 -0
  55. data/lib/string.rb +81 -0
  56. data/lib/teeworlds_client.rb +506 -0
  57. data/lib/teeworlds_network.rb +4 -0
  58. data/lib/teeworlds_server.rb +363 -0
  59. data/lib/version.rb +3 -0
  60. metadata +132 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 86e6dabf122aa38551ccaff8cc2c7896b2bf303f36fd49edcb722951f1fa6cc7
4
+ data.tar.gz: a627eb3ded5879fb97303f0129adee1599c8b157c306ff8f302f78c6e04297fc
5
+ SHA512:
6
+ metadata.gz: c041984c8b08096e51925edf8821badb24c188253bd43ddfa4db834ffb58a7d8c9cff50bf456a1946f21fbe5effb4bd1d74cfe133bc8d70c502c68a1031a223e
7
+ data.tar.gz: 70641a88c3e6c3bc3a79f435c7823cee16ace9c4bab610b23abe40c5dbcf80ea9268b6b392eec2ca8d98cbedfb03269f8484de62ec27ce303bbe65f3f143cfad
data/lib/array.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # I JUST REALIZED I ALREADY USED .scan(/../)
4
+ # TO GET .groups_of(2)
5
+ # AND DUUUUH .scan() IS BASICALLY ALREADY
6
+ # .groups_of()
7
+ # TODO: get rid of it?!
8
+ # update: no
9
+ # .scan(/../) works on strings and groups_of works on arrays
10
+ # so it has its purpose
11
+ class Array
12
+ def groups_of(max_size)
13
+ return [] if max_size < 1
14
+
15
+ groups = []
16
+ group = []
17
+ each do |item|
18
+ group.push(item)
19
+
20
+ if group.size >= max_size
21
+ groups.push(group)
22
+ group = []
23
+ end
24
+ end
25
+ groups.push(group) unless group.size.zero?
26
+ groups
27
+ end
28
+ end
data/lib/bytes.rb ADDED
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'array'
4
+ require_relative 'string'
5
+
6
+ # turn byte array into hex string
7
+ def str_hex(data)
8
+ data.unpack1('H*').scan(/../).join(' ').upcase
9
+ end
10
+
11
+ def data_to_ascii(data)
12
+ ascii = ''
13
+ data.unpack('C*').each do |c|
14
+ ascii += c < 32 || c > 126 ? '.' : c.chr
15
+ end
16
+ ascii
17
+ end
18
+
19
+ COL_LEN = 9
20
+
21
+ # TODO: make this a gem?!
22
+ #
23
+ # opts
24
+ # legend: :long
25
+ # legend: :short
26
+ # legend: :inline
27
+ def hexdump_lines(data, width = 2, notes = [], opts = { legend: :long })
28
+ byte_groups = data.unpack1('H*').scan(/../).groups_of(4)
29
+ lines = []
30
+ hex = ''
31
+ ascii = ''
32
+ w = 0
33
+ byte = 0
34
+ legend = []
35
+ notes.each do |info|
36
+ color = info.first
37
+ raise "Invalid color '#{color}' valid ones: #{AVAILABLE_COLORS}" unless AVAILABLE_COLORS.include? color
38
+
39
+ legend.push([color, info.last.send(color)])
40
+ end
41
+ unless legend.empty?
42
+ case opts[:legend]
43
+ when :long
44
+ legend.each do |leg|
45
+ lines.push("#{leg.first}: #{leg.last}".send(leg.first))
46
+ end
47
+ when :short
48
+ lines.push(legend.map(&:last).join(' '))
49
+ end
50
+ end
51
+ byte_groups.each do |byte_group|
52
+ hex += ' ' unless hex.empty?
53
+ ascii += data_to_ascii(str_bytes(byte_group.join).pack('C*'))
54
+ w += 1
55
+ note = ''
56
+ colors = 0
57
+ notes.each do |info|
58
+ color = info.first
59
+ # p color
60
+ # p info
61
+ from = info[1]
62
+ to = info[1] + (info[2] - 1)
63
+
64
+ if from > byte + 3
65
+ # puts "a"
66
+ next
67
+ end
68
+ if to < byte
69
+ # puts "to: #{to} < byte: #{byte}"
70
+ next
71
+ end
72
+
73
+ note += " #{info[3]}".send(color) if opts[:legend] == :inline
74
+
75
+ from -= byte
76
+ to -= byte
77
+ from = 0 if from.negative?
78
+ to = 3 if to > 3
79
+
80
+ # puts "from: #{from} to: #{to}"
81
+ (from..to).each do |i|
82
+ next if byte_group[i].nil?
83
+
84
+ byte_group[i] = byte_group[i].send(color)
85
+ colors += 1
86
+ end
87
+ end
88
+ byte += 4
89
+ hex += byte_group.join(' ')
90
+ next unless w >= width
91
+
92
+ w = 0
93
+ hex_pad = hex.ljust((width * 4 * 3) + (colors * COL_LEN), ' ')
94
+ ascii_pad = ascii.ljust(width * 4)
95
+ lines.push("#{hex_pad} #{ascii_pad}#{note}")
96
+ hex = ''
97
+ ascii = ''
98
+ end
99
+ lines.push("#{hex_pad} #{ascii_pad}#{note}") unless hex.empty?
100
+ lines
101
+ end
102
+
103
+ # turn hex string to byte array
104
+ def str_bytes(str)
105
+ str.scan(/../).map { |b| b.to_i(16) }
106
+ end
107
+
108
+ def bytes_to_str(data)
109
+ data.unpack('H*').join
110
+ end
111
+
112
+ # TODO: remove?
113
+ def get_byte(data, start = 0, num = 1)
114
+ data[start...(start + num)].unpack('H*').join.upcase
115
+ end
116
+
117
+ def todo_make_this_a_unit_test
118
+ notes = [
119
+ [:red, 0, 1, 'foo'],
120
+ [:green, 1, 1, 'bar'],
121
+ [:yellow, 2, 1, 'baz'],
122
+ [:pink, 3, 1, 'bang'],
123
+ [:green, 4, 1, 'bär'],
124
+ [:yellow, 6, 6, 'yee']
125
+ ]
126
+
127
+ hexdump_lines("\x01\x41\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\xef", 40, notes).each do |l|
128
+ puts l
129
+ end
130
+
131
+ hexdump_lines("\x01\x41\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\xef", 40, notes, legend: long).each do |l|
132
+ puts l
133
+ end
134
+
135
+ # should not crash when annotating bytes out of range
136
+ hexdump_lines("\x01\x41", 40, notes, legend: :long).each do |l|
137
+ puts l
138
+ end
139
+ end
data/lib/chunk.rb ADDED
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'array'
4
+ require_relative 'network'
5
+ require_relative 'bytes'
6
+
7
+ ##
8
+ # The NetChunk class represents one individual
9
+ # chunk of a teeworlds packet.
10
+ #
11
+ # A teeworlds packet holds multiple game and system messages
12
+ # as its payload, those are called chunks or messages.
13
+ #
14
+ # https://chillerdragon.github.io/teeworlds-protocol/07/packet_layout.html
15
+ class NetChunk
16
+ attr_reader :next, :data, :msg, :sys, :flags, :header_raw, :full_raw
17
+
18
+ @@sent_vital_chunks = 0
19
+
20
+ def initialize(data)
21
+ @next = nil
22
+ @flags = {}
23
+ @size = 0
24
+ parse_header(data[0..2])
25
+ header_size = if flags_vital
26
+ VITAL_CHUNK_HEADER_SIZE
27
+ else
28
+ NON_VITAL_CHUNK_HEADER_SIZE
29
+ end
30
+ @header_raw = data[...header_size]
31
+ chunk_end = header_size + @size
32
+ # puts "data[0]: " + str_hex(data[0])
33
+ @data = data[header_size...chunk_end]
34
+ @msg = @data[0].unpack1('C*')
35
+ @sys = @msg & 1 == 1
36
+ @msg >>= 1
37
+ @next = data[chunk_end..] if data.size > chunk_end
38
+ @full_raw = data[..chunk_end]
39
+ end
40
+
41
+ def self.reset
42
+ @@sent_vital_chunks = 0
43
+ end
44
+
45
+ def to_s
46
+ "NetChunk\n" \
47
+ " msg=#{msg} sys=#{sys}\n" \
48
+ " #{@flags}\n" \
49
+ " header: #{str_hex(header_raw)}\n" \
50
+ " data: #{str_hex(data)}"
51
+ end
52
+
53
+ def self._create_non_vital_header(data = { size: 0 })
54
+ flag_bits = '00'
55
+ unused_bits = '00'
56
+
57
+ size_bits = data[:size].to_s(2).rjust(12, '0')
58
+ header_bits =
59
+ flag_bits +
60
+ size_bits[0..5] +
61
+ unused_bits +
62
+ size_bits[6..]
63
+ header_bits.chars.groups_of(8).map do |eigth_bits|
64
+ eigth_bits.join.to_i(2)
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Create int array ready to be send over the network
70
+ #
71
+ # Given the flags hash (vital/resend)
72
+ # the size
73
+ # the sequence number
74
+ #
75
+ # It will create a 3 byte chunk header
76
+ # represented as an Array of 3 integers
77
+ def self.create_header(opts = { resend: false, vital: false, size: nil, seq: nil, client: nil })
78
+ raise 'Chunk.create_header :size option can not be nil' if opts[:size].nil?
79
+ return _create_non_vital_header(opts) unless opts[:vital]
80
+
81
+ # client only counts this class var
82
+ @@sent_vital_chunks += 1
83
+ seq = opts[:seq].nil? ? @@sent_vital_chunks : opts[:seq]
84
+
85
+ # server counts per client
86
+ unless opts[:client].nil?
87
+ opts[:client].vital_sent += 1
88
+ seq = opts[:client].vital_sent
89
+ end
90
+
91
+ flag_bits = '00'.dup
92
+ flag_bits[0] = opts[:resend] ? '1' : '0'
93
+ flag_bits[1] = opts[:vital] ? '1' : '0'
94
+
95
+ size_bits = opts[:size].to_s(2).rjust(12, '0')
96
+ # size_bits[0..5]
97
+ # size_bits[6..]
98
+
99
+ seq_bits = seq.to_s(2).rjust(10, '0')
100
+ # seq_bits[0..1]
101
+ # seq_bits[2..]
102
+
103
+ # The vital chunk header is 3 bytes
104
+ # containing flags, size and sequence
105
+ # in the following format
106
+ #
107
+ # f=flag
108
+ # s=size
109
+ # q=sequence
110
+ #
111
+ # ffss ssss qqss ssss qqqq qqqq
112
+ header_bits =
113
+ flag_bits +
114
+ size_bits[0..5] +
115
+ seq_bits[0..1] +
116
+ size_bits[6..] +
117
+ seq_bits[2..]
118
+ header_bits.chars.groups_of(8).map do |eigth_bits|
119
+ eigth_bits.join.to_i(2)
120
+ end
121
+ end
122
+
123
+ def parse_header(data)
124
+ # flags
125
+ flags = data[0].unpack1('B*')
126
+ flags = flags[0..1]
127
+ @flags[:resend] = flags[0] == '1'
128
+ @flags[:vital] = flags[1] == '1'
129
+
130
+ # size
131
+ size = data[0..1].unpack1('B*')
132
+ size_bytes = size.chars.groups_of(8)
133
+ # trim first 2 bits of both bytes
134
+ # Size: 2 bytes (..00 0000 ..00 0010)
135
+ size_bytes.map! { |b| b[2..].join }
136
+ @size = size_bytes.join.to_i(2)
137
+
138
+ # sequence number
139
+ # in da third byte but who needs seq?!
140
+ end
141
+
142
+ # @return [Boolean]
143
+ def flags_vital
144
+ @flags[:vital]
145
+ end
146
+
147
+ # @return [Boolean]
148
+ def flags_resend
149
+ @flags[:resend]
150
+ end
151
+ end
152
+
153
+ MAX_NUM_CHUNKS = 1024
154
+
155
+ class BigChungusTheChunkGetter
156
+ ##
157
+ # given a raw payload of a teeworlds packet
158
+ # it splits it into the indivudal chunks
159
+ # also known as messages
160
+ #
161
+ # @return [Array<NetChunk>]
162
+ def self.get_chunks(data)
163
+ chunks = []
164
+ chunk = NetChunk.new(data)
165
+ chunks.push(chunk)
166
+ while chunk.next
167
+ chunk = NetChunk.new(chunk.next)
168
+ chunks.push(chunk)
169
+ next unless chunks.size > MAX_NUM_CHUNKS
170
+
171
+ # inf loop guard case
172
+ puts 'Warning: abort due to max num chunks bein reached'
173
+ break
174
+ end
175
+ chunks
176
+ end
177
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Config
4
+ def initialize(options = {})
5
+ filepath = options[:file] || 'autoexec.cfg'
6
+ init_configs
7
+ load_cfg(filepath)
8
+ end
9
+
10
+ def init_configs
11
+ @configs = {
12
+ password: { help: 'Password to the server', default: '' }
13
+ }
14
+ @commands = {
15
+ echo: { help: 'Echo the text', callback: proc { |arg| puts arg } },
16
+ quit: { help: 'Quit', callback: proc { |_| exit } }
17
+ }
18
+ @configs.each do |cfg, data|
19
+ self.class.send(:attr_accessor, cfg)
20
+ instance_variable_set("@#{cfg}", data[:default])
21
+ end
22
+ end
23
+
24
+ def load_cfg(file)
25
+ return unless File.exist?(file)
26
+
27
+ File.readlines(file).each_with_index do |line, line_num|
28
+ line.strip!
29
+ next if line.start_with? '#'
30
+ next if line.empty?
31
+
32
+ words = line.split
33
+ cmd = words.shift.to_sym
34
+ arg = words.join(' ')
35
+ if @configs[cmd]
36
+ instance_variable_set("@#{cmd}", arg)
37
+ elsif @commands[cmd]
38
+ @commands[cmd][:callback].call(arg)
39
+ else
40
+ puts "Warning: unsupported config '#{cmd}' #{file}:#{line_num}"
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/context.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Context
4
+ attr_reader :old_data
5
+ attr_accessor :data, :message
6
+
7
+ def initialize(message, keys = {})
8
+ @message = message # the obj holding the parsed chunk
9
+ @cancel = false
10
+ @old_data = keys
11
+ @data = keys
12
+ end
13
+
14
+ def verify
15
+ @data.each do |key, _value|
16
+ next if @old_data.key? key
17
+
18
+ raise "Error: invalid data key '#{key}'\n valid keys: #{@old_data.keys}"
19
+ end
20
+ end
21
+
22
+ def canceld?
23
+ @cancel
24
+ end
25
+
26
+ def cancel
27
+ @cancel = true
28
+ end
29
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'models/player'
4
+ require_relative 'models/chat_message'
5
+ require_relative 'messages/input_timing'
6
+ require_relative 'messages/rcon_line'
7
+ require_relative 'messages/sv_client_drop'
8
+ require_relative 'messages/rcon_cmd_add'
9
+ require_relative 'messages/rcon_cmd_rem'
10
+ require_relative 'messages/maplist_entry_add'
11
+ require_relative 'messages/maplist_entry_rem'
12
+ require_relative 'packer'
13
+ require_relative 'context'
14
+ require_relative 'snapshot/unpacker'
15
+
16
+ class GameClient
17
+ attr_accessor :players, :pred_game_tick, :ack_game_tick
18
+
19
+ def initialize(client)
20
+ @client = client
21
+ @players = {}
22
+ @ack_game_tick = -1
23
+ @pred_game_tick = 0
24
+ end
25
+
26
+ ##
27
+ # call_hook
28
+ #
29
+ # @param: hook_sym [Symbol] name of the symbol to call
30
+ # @param: context [Context] context object to pass on data
31
+ # @param: optional [Any] optional 2nd parameter passed to the callback
32
+ def call_hook(hook_sym, context, optional = nil)
33
+ @client.hooks[hook_sym].each do |hook|
34
+ hook.call(context, optional)
35
+ context.verify
36
+ return nil if context.canceld?
37
+ end
38
+ context
39
+ end
40
+
41
+ def on_auth_on
42
+ return if call_hook(:auth_on, Context.new(nil)).nil?
43
+
44
+ @client.rcon_authed = true
45
+ puts 'rcon logged in'
46
+ end
47
+
48
+ def on_auth_off
49
+ return if call_hook(:auth_off, Context.new(nil)).nil?
50
+
51
+ @client.rcon_authed = false
52
+ puts 'rcon logged out'
53
+ end
54
+
55
+ def on_rcon_cmd_add(chunk)
56
+ message = RconCmdAdd.new(chunk.data[1..])
57
+ context = Context.new(message)
58
+ call_hook(:rcon_cmd_add, context)
59
+ end
60
+
61
+ def on_rcon_cmd_rem(chunk)
62
+ message = RconCmdRem.new(chunk.data[1..])
63
+ context = Context.new(message)
64
+ call_hook(:rcon_cmd_rem, context)
65
+ end
66
+
67
+ def on_maplist_entry_add(chunk)
68
+ message = MaplistEntryAdd.new(chunk.data[1..])
69
+ context = Context.new(message)
70
+ call_hook(:maplist_entry_add, context)
71
+ end
72
+
73
+ def on_maplist_entry_rem(chunk)
74
+ message = MaplistEntryRem.new(chunk.data[1..])
75
+ context = Context.new(message)
76
+ call_hook(:maplist_entry_rem, context)
77
+ end
78
+
79
+ def on_client_info(chunk)
80
+ # puts "Got playerinfo flags: #{chunk.flags}"
81
+ u = Unpacker.new(chunk.data[1..])
82
+ player = Player.new(
83
+ id: u.get_int,
84
+ local: u.get_int,
85
+ team: u.get_int,
86
+ name: u.get_string,
87
+ clan: u.get_string,
88
+ country: u.get_int
89
+ )
90
+ # skinparts and the silent flag
91
+ # are currently ignored
92
+
93
+ context = Context.new(
94
+ nil,
95
+ player:,
96
+ chunk:
97
+ )
98
+ return if call_hook(:client_info, context).nil?
99
+
100
+ player = context.data[:player]
101
+ if player.local?
102
+ @client.local_client_id = player.id
103
+ puts "Our client id is #{@client.local_client_id}"
104
+ end
105
+ @players[player.id] = player
106
+ end
107
+
108
+ def on_input_timing(chunk)
109
+ message = InputTiming.new(chunk.data[1..])
110
+ context = Context.new(message, chunk:)
111
+ call_hook(:input_timing, context)
112
+ end
113
+
114
+ def on_client_drop(chunk)
115
+ message = SvClientDrop.new(chunk.data[1..])
116
+ context = Context.new(
117
+ nil,
118
+ player: @players[message.client_id],
119
+ chunk:,
120
+ client_id: message.client_id,
121
+ reason: message.reason,
122
+ silent: message.silent?
123
+ )
124
+ return if call_hook(:client_drop, context).nil?
125
+
126
+ @players.delete(context.data[:client_id])
127
+ end
128
+
129
+ def on_ready_to_enter(_chunk)
130
+ @client.send_enter_game
131
+ end
132
+
133
+ def on_connected
134
+ context = Context.new(nil)
135
+ return if call_hook(:connected, context).nil?
136
+
137
+ @client.send_msg_start_info
138
+ end
139
+
140
+ def on_disconnect(data)
141
+ context = Context.new(nil, reason: data)
142
+ return if call_hook(:disconnect, context).nil?
143
+
144
+ puts "got disconnect. reason='#{context.data[:reason]}'"
145
+ end
146
+
147
+ def on_rcon_line(chunk)
148
+ message = RconLine.new(chunk.data[1..])
149
+ context = Context.new(message)
150
+ return if call_hook(:rcon_line, context).nil?
151
+
152
+ puts "[rcon] #{context.message.command}"
153
+ end
154
+
155
+ def on_snapshot(chunk)
156
+ u = SnapshotUnpacker.new(@client)
157
+ snapshot = u.snap_single(chunk)
158
+
159
+ return if snapshot.nil?
160
+
161
+ context = Context.new(nil, chunk:)
162
+ return if call_hook(:snapshot, context, snapshot).nil?
163
+
164
+ # ack every snapshot no matter how broken
165
+ @ack_game_tick = snapshot.game_tick
166
+ return unless (@pred_game_tick - @ack_game_tick).abs > 10
167
+
168
+ @pred_game_tick = @ack_game_tick + 1
169
+ end
170
+
171
+ def on_emoticon(chunk); end
172
+
173
+ def on_map_change(chunk)
174
+ context = Context.new(nil, chunk:)
175
+ return if call_hook(:map_change, context).nil?
176
+
177
+ # ignore mapdownload at all times
178
+ # and claim to have the map
179
+ @client.send_msg_ready
180
+ end
181
+
182
+ def on_chat(chunk)
183
+ u = Unpacker.new(chunk.data[1..])
184
+ data = {
185
+ mode: u.get_int,
186
+ client_id: u.get_int,
187
+ target_id: u.get_int,
188
+ message: u.get_string
189
+ }
190
+ data[:author] = @players[data[:client_id]]
191
+ msg = ChatMesage.new(data)
192
+
193
+ context = Context.new(nil, chunk:)
194
+ call_hook(:chat, context, msg)
195
+ end
196
+ end