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.
- checksums.yaml +7 -0
- data/lib/array.rb +28 -0
- data/lib/bytes.rb +139 -0
- data/lib/chunk.rb +177 -0
- data/lib/config.rb +44 -0
- data/lib/context.rb +29 -0
- data/lib/game_client.rb +196 -0
- data/lib/game_server.rb +122 -0
- data/lib/messages/cl_emoticon.rb +63 -0
- data/lib/messages/cl_say.rb +52 -0
- data/lib/messages/client_info.rb +141 -0
- data/lib/messages/game_info.rb +25 -0
- data/lib/messages/input_timing.rb +48 -0
- data/lib/messages/maplist_entry_add.rb +44 -0
- data/lib/messages/maplist_entry_rem.rb +44 -0
- data/lib/messages/rcon_cmd_add.rb +52 -0
- data/lib/messages/rcon_cmd_rem.rb +44 -0
- data/lib/messages/rcon_line.rb +44 -0
- data/lib/messages/server_info.rb +64 -0
- data/lib/messages/server_settings.rb +23 -0
- data/lib/messages/start_info.rb +129 -0
- data/lib/messages/sv_client_drop.rb +57 -0
- data/lib/models/chat_message.rb +39 -0
- data/lib/models/map.rb +57 -0
- data/lib/models/net_addr.rb +18 -0
- data/lib/models/packet_flags.rb +42 -0
- data/lib/models/player.rb +56 -0
- data/lib/models/token.rb +20 -0
- data/lib/net_base.rb +106 -0
- data/lib/network.rb +148 -0
- data/lib/packer.rb +194 -0
- data/lib/packet.rb +73 -0
- data/lib/snapshot/events/damage.rb +24 -0
- data/lib/snapshot/events/death.rb +20 -0
- data/lib/snapshot/events/explosion.rb +16 -0
- data/lib/snapshot/events/hammer_hit.rb +16 -0
- data/lib/snapshot/events/sound_world.rb +20 -0
- data/lib/snapshot/events/spawn.rb +16 -0
- data/lib/snapshot/items/character.rb +43 -0
- data/lib/snapshot/items/client_info.rb +22 -0
- data/lib/snapshot/items/flag.rb +22 -0
- data/lib/snapshot/items/game_data.rb +22 -0
- data/lib/snapshot/items/game_data_flag.rb +24 -0
- data/lib/snapshot/items/game_data_team.rb +21 -0
- data/lib/snapshot/items/laser.rb +24 -0
- data/lib/snapshot/items/pickup.rb +22 -0
- data/lib/snapshot/items/player_info.rb +22 -0
- data/lib/snapshot/items/player_input.rb +32 -0
- data/lib/snapshot/items/projectile.rb +25 -0
- data/lib/snapshot/items/spectator_info.rb +23 -0
- data/lib/snapshot/items/tune_params.rb +90 -0
- data/lib/snapshot/snap_event_base.rb +14 -0
- data/lib/snapshot/snap_item_base.rb +86 -0
- data/lib/snapshot/unpacker.rb +301 -0
- data/lib/string.rb +81 -0
- data/lib/teeworlds_client.rb +506 -0
- data/lib/teeworlds_network.rb +4 -0
- data/lib/teeworlds_server.rb +363 -0
- data/lib/version.rb +3 -0
- 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
|
data/lib/game_client.rb
ADDED
@@ -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
|