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
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ require_relative 'string'
6
+ require_relative 'array'
7
+ require_relative 'bytes'
8
+ require_relative 'network'
9
+ require_relative 'packet'
10
+ require_relative 'chunk'
11
+ require_relative 'net_base'
12
+ require_relative 'models/net_addr'
13
+ require_relative 'packer'
14
+ require_relative 'game_server'
15
+ require_relative 'models/token'
16
+
17
+ class Client
18
+ attr_accessor :id, :addr, :vital_sent, :last_recv_time, :token, :player, :in_game
19
+ attr_reader :ack
20
+
21
+ def initialize(attr = {})
22
+ @id = attr[:id]
23
+ @addr = attr[:addr]
24
+ @vital_sent = 0
25
+ @ack = 0
26
+ @in_game = false
27
+ @last_recv_time = Time.now
28
+ @player = Player.new(
29
+ id: @id,
30
+ local: 0,
31
+ team: 0,
32
+ name: '(connecting)',
33
+ clan: '',
34
+ country: -1
35
+ )
36
+ @token = attr[:token]
37
+ SecurityToken.validate(@token)
38
+ end
39
+
40
+ def in_game?
41
+ @in_game
42
+ end
43
+
44
+ def bump_ack
45
+ @ack = (@ack + 1) % NET_MAX_SEQUENCE
46
+ end
47
+
48
+ # TODO: use or remove
49
+ # not sure if its cool
50
+ # one can make vital_sent read only
51
+ # and then the seq reader increments it
52
+ # so everytime a header is created
53
+ # the chunk builder just calls
54
+ # seq = client.seq
55
+ def seq
56
+ @vital_sent + 1
57
+ end
58
+ end
59
+
60
+ class TeeworldsServer
61
+ attr_accessor :clients
62
+
63
+ def initialize(options = {})
64
+ @verbose = options[:verbose] || false
65
+ @ip = '127.0.0.1'
66
+ @port = 8303
67
+ @game_server = GameServer.new(self)
68
+ @clients = {}
69
+ @current_game_tick = 0
70
+ @last_snap_time = Time.now
71
+ end
72
+
73
+ def run(ip, port)
74
+ @server_token = (1..4).to_a.map { |_| rand(0..255) }
75
+ @server_token = @server_token.map { |b| b.to_s(16).rjust(2, '0') }.join
76
+ puts "server token #{@server_token}"
77
+ @netbase = NetBase.new(verbose: @verbose)
78
+ NetChunk.reset
79
+ @ip = ip
80
+ @port = port
81
+ puts "listening on #{@ip}:#{@port} .."
82
+ @s = UDPSocket.new
83
+ @s.bind(@ip, @port)
84
+ @netbase.bind(@s)
85
+ loop do
86
+ tick
87
+ # TODO: proper tick speed sleep
88
+ # replace by blocking network read
89
+ # m_NetServer
90
+ # .Wait(
91
+ # clamp(
92
+ # int((
93
+ # TickStartTime(
94
+ # m_CurrentGameTick+1)-time_get()
95
+ # )*1000/time_freq()),
96
+ # 1,
97
+ # 1000/SERVER_TICK_SPEED/2));
98
+ sleep 0.001
99
+ end
100
+ end
101
+
102
+ def on_message(chunk, packet)
103
+ puts "got game chunk: #{chunk}"
104
+ case chunk.msg
105
+ when NETMSGTYPE_CL_STARTINFO then @game_server.on_start_info(chunk, packet)
106
+ when NETMSGTYPE_CL_SAY then @game_server.on_say(chunk, packet)
107
+ when NETMSGTYPE_CL_EMOTICON then @game_server.on_emoticon(chunk, packet)
108
+ else
109
+ puts "Unsupported game msg: #{chunk.msg}"
110
+ exit(1)
111
+ end
112
+ end
113
+
114
+ def process_chunk(chunk, packet)
115
+ unless chunk.sys
116
+ on_message(chunk, packet)
117
+ return
118
+ end
119
+ puts "proccess chunk with msg: #{chunk.msg}" if @verbose
120
+ case chunk.msg
121
+ when NETMSG_INFO
122
+ @game_server.on_info(chunk, packet)
123
+ when NETMSG_READY
124
+ @game_server.on_ready(chunk, packet)
125
+ when NETMSG_ENTERGAME
126
+ @game_server.on_enter_game(chunk, packet)
127
+ when NETMSG_INPUT
128
+ @game_server.on_input(chunk, packet)
129
+ when NETMSG_RCON_CMD
130
+ @game_server.on_rcon_cmd(chunk, packet)
131
+ else
132
+ puts "Unsupported system msg: #{chunk.msg}"
133
+ exit(1)
134
+ end
135
+ end
136
+
137
+ def on_client_packet(packet)
138
+ client = packet.client
139
+ if client.nil?
140
+ # TODO: turn this into a silent return
141
+ # otherwise bad actors can easily trigger this
142
+ # with handcrafted packets
143
+ puts 'Error: got client packet from unknown client'
144
+ exit 1
145
+ end
146
+ chunks = BigChungusTheChunkGetter.get_chunks(packet.payload)
147
+ chunks.each do |chunk|
148
+ if chunk.flags_vital && !chunk.flags_resend
149
+ packet.client.bump_ack
150
+ puts "got ack: #{packet.client.ack}" if @verbose
151
+ end
152
+ process_chunk(chunk, packet)
153
+ end
154
+ end
155
+
156
+ def on_ctrl_message(packet)
157
+ u = Unpacker.new(packet.payload)
158
+ msg = u.get_int
159
+ puts "got ctrl msg: #{msg}"
160
+ case msg
161
+ when NET_CTRLMSG_TOKEN then on_ctrl_token(packet)
162
+ when NET_CTRLMSG_CONNECT then on_ctrl_connect(packet)
163
+ when NET_CTRLMSG_KEEPALIVE then on_ctrl_keep_alive(packet)
164
+ when NET_CTRLMSG_CLOSE then on_ctrl_close(packet)
165
+ else
166
+ puts "Uknown control message #{msg}"
167
+ exit(1)
168
+ end
169
+ end
170
+
171
+ def send_ctrl_close(client, reason)
172
+ # when clients disconnect
173
+ # during the connection process
174
+ # we do not care
175
+ return if client.nil?
176
+
177
+ msg = [NET_CTRLMSG_CLOSE]
178
+ msg += Packer.pack_str(reason) unless reason.nil?
179
+ @netbase.set_peer_token(client.token)
180
+ @netbase.send_packet(msg, chunks: 0, control: true, client:)
181
+ # @netbase.set_peer_token(@server_token)
182
+ end
183
+
184
+ def send_ctrl_with_token(addr, token)
185
+ msg = [NET_CTRLMSG_TOKEN] + str_bytes(@server_token)
186
+ @netbase.set_peer_token(token)
187
+ @netbase.send_packet(msg, chunks: 0, control: true, addr:)
188
+ # @netbase.set_peer_token(@server_token)
189
+ end
190
+
191
+ def send_map(client)
192
+ data = []
193
+ data += Packer.pack_str(@game_server.map.name)
194
+ data += @game_server.map.crc_arr # poor mans pack_raw()
195
+ data += Packer.pack_int(@game_server.map.size)
196
+ data += Packer.pack_int(8) # chunk num?
197
+ data += Packer.pack_int(MAP_CHUNK_SIZE)
198
+ data += @game_server.map.sha256_arr # poor mans pack_raw()
199
+ msg = NetChunk.create_header(vital: true, size: data.size + 1, client:) +
200
+ [pack_msg_id(NETMSG_MAP_CHANGE, system: true)] +
201
+ data
202
+ @netbase.send_packet(msg, chunks: 1, client:)
203
+ end
204
+
205
+ def send_ready(client)
206
+ msg = NetChunk.create_header(vital: true, size: 1, client:) +
207
+ [pack_msg_id(NETMSG_CON_READY, system: true)]
208
+ @netbase.send_packet(msg, chunks: 1, client:)
209
+ end
210
+
211
+ def send_ready_to_enter(client)
212
+ msg = NetChunk.create_header(vital: true, size: 1, client:) +
213
+ [pack_msg_id(NETMSGTYPE_SV_READYTOENTER, system: false)]
214
+ @netbase.send_packet(msg, chunks: 1, client:)
215
+ end
216
+
217
+ def send_server_settings(client, server_settings)
218
+ msg = NetChunk.create_header(vital: true, size: 1 + server_settings.size, client:) +
219
+ [pack_msg_id(NETMSGTYPE_SV_SERVERSETTINGS, system: false)] +
220
+ server_settings
221
+ @netbase.send_packet(msg, chunks: 1, client:)
222
+ end
223
+
224
+ def send_server_info(client, server_info)
225
+ msg = NetChunk.create_header(vital: true, size: 1 + server_info.size, client:) +
226
+ [pack_msg_id(NETMSG_SERVERINFO, system: true)] +
227
+ server_info
228
+ @netbase.send_packet(msg, chunks: 1, client:)
229
+ end
230
+
231
+ def send_game_info(client, data)
232
+ msg = NetChunk.create_header(vital: true, size: 1 + data.size, client:) +
233
+ [pack_msg_id(NETMSGTYPE_SV_GAMEINFO, system: false)] +
234
+ data
235
+ @netbase.send_packet(msg, chunks: 1, client:)
236
+ end
237
+
238
+ def on_ctrl_token(packet)
239
+ u = Unpacker.new(packet.payload[1..])
240
+ token = u.get_raw(4)
241
+ token_str = token.map { |b| b.to_s(16).rjust(2, '0') }.join
242
+ puts "got token #{token_str}"
243
+ send_ctrl_with_token(packet.addr, token_str)
244
+ end
245
+
246
+ def on_ctrl_keep_alive(packet)
247
+ puts "Got keep alive from #{packet.addr}" if @verbose
248
+ end
249
+
250
+ def on_ctrl_close(packet)
251
+ reason = nil
252
+ if packet.payload[2]
253
+ u = Unpacker.new(packet.payload[1..])
254
+ reason = u.get_string
255
+ end
256
+ drop_client(packet.client, reason)
257
+ end
258
+
259
+ def drop_client(client, reason = nil)
260
+ send_ctrl_close(client, reason)
261
+ return if client.nil?
262
+
263
+ @game_server.on_client_drop(client, reason)
264
+ @clients.delete(client.id)
265
+ end
266
+
267
+ def on_ctrl_connect(packet)
268
+ id = get_next_client_id
269
+ if id == -1
270
+ puts 'server full drop packet. TODO: tell the client'
271
+ return
272
+ end
273
+ token = bytes_to_str(packet.payload[1..4])
274
+ puts "got connection, sending accept (client token: #{token})"
275
+ client = Client.new(id:, addr: packet.addr, token:)
276
+ @clients[id] = client
277
+ @netbase.send_packet([NET_CTRLMSG_ACCEPT], chunks: 0, control: true, client:)
278
+ end
279
+
280
+ def on_packet(packet)
281
+ # process connless packets data
282
+ if packet.flags_control
283
+ on_ctrl_message(packet)
284
+ else # process non-connless packets
285
+ on_client_packet(packet)
286
+ end
287
+ end
288
+
289
+ def get_next_client_id
290
+ (0..MAX_CLIENTS).each do |i|
291
+ next if @clients[i]
292
+
293
+ return i
294
+ end
295
+ -1
296
+ end
297
+
298
+ def tick_start_time(tick)
299
+ # TODO: implement this C++ code
300
+ # m_GameStartTime + (time_freq()*Tick)/SERVER_TICK_SPEED;
301
+ end
302
+
303
+ def do_snapshot
304
+ delta_tick = -1
305
+ # DeltaTick = m_aClients[i].m_LastAckedSnapshot;
306
+ data = []
307
+ data += Packer.pack_int(@current_game_tick)
308
+ data += Packer.pack_int(@current_game_tick - delta_tick)
309
+ msg_snap_empty = NetChunk.create_header(vital: false, size: data.size + 1) +
310
+ [pack_msg_id(NETMSG_SNAPEMPTY, system: true)] +
311
+ data
312
+ # data = []
313
+ # data += Packer.pack_int(@current_game_tick)
314
+ # data += Packer.pack_int(@current_game_tick - delta_tick)
315
+ # msg_snap_single = NetChunk.create_header(vital: false, size: data.size + 1) +
316
+ # [pack_msg_id(NETMSG_SNAPSINGLE, system: true)] +
317
+ # data
318
+ @clients.each do |_id, client|
319
+ next unless client.in_game?
320
+
321
+ @netbase.send_packet(msg_snap_empty, chunks: 1, client:)
322
+ end
323
+ end
324
+
325
+ def get_player_by_id(id)
326
+ @clients[id]&.player
327
+ end
328
+
329
+ def tick
330
+ unless @clients.empty?
331
+ now = Time.now
332
+ diff = now - @last_snap_time
333
+ # TODO: replace snaps every second by something more correct
334
+ if diff > 1
335
+ @current_game_tick += 1
336
+ do_snapshot
337
+ end
338
+ @game_server.on_tick
339
+ end
340
+
341
+ begin
342
+ data, sender_inet_addr = @s.recvfrom_nonblock(1400)
343
+ rescue IO::EAGAINWaitReadable
344
+ data = nil
345
+ sender_inet_addr = nil
346
+ end
347
+ return unless data
348
+
349
+ packet = Packet.new(data, '<')
350
+ packet.addr.ip = sender_inet_addr[2] # or 3 idk bot 127.0.0.1 in my local test case
351
+ packet.addr.port = sender_inet_addr[1]
352
+ @clients.each do |id, client|
353
+ next unless packet.addr.eq(client.addr)
354
+
355
+ client.last_recv_time = Time.now
356
+ packet.client_id = id
357
+ packet.client = client
358
+ end
359
+
360
+ puts packet.to_s if @verbose
361
+ on_packet(packet)
362
+ end
363
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ TEEWORLDS_NETWORK_VERSION = 'v0.0.2'
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: teeworlds_network
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - ChillerDragon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: huffman_tw
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.9.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.9.0
41
+ description: |2
42
+ A library wrapping the network protocol of the game teeworlds.
43
+ Only supporting the version 0.7 of the teeworlds protocol.
44
+ email: ChillerDragon@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - lib/array.rb
50
+ - lib/bytes.rb
51
+ - lib/chunk.rb
52
+ - lib/config.rb
53
+ - lib/context.rb
54
+ - lib/game_client.rb
55
+ - lib/game_server.rb
56
+ - lib/messages/cl_emoticon.rb
57
+ - lib/messages/cl_say.rb
58
+ - lib/messages/client_info.rb
59
+ - lib/messages/game_info.rb
60
+ - lib/messages/input_timing.rb
61
+ - lib/messages/maplist_entry_add.rb
62
+ - lib/messages/maplist_entry_rem.rb
63
+ - lib/messages/rcon_cmd_add.rb
64
+ - lib/messages/rcon_cmd_rem.rb
65
+ - lib/messages/rcon_line.rb
66
+ - lib/messages/server_info.rb
67
+ - lib/messages/server_settings.rb
68
+ - lib/messages/start_info.rb
69
+ - lib/messages/sv_client_drop.rb
70
+ - lib/models/chat_message.rb
71
+ - lib/models/map.rb
72
+ - lib/models/net_addr.rb
73
+ - lib/models/packet_flags.rb
74
+ - lib/models/player.rb
75
+ - lib/models/token.rb
76
+ - lib/net_base.rb
77
+ - lib/network.rb
78
+ - lib/packer.rb
79
+ - lib/packet.rb
80
+ - lib/snapshot/events/damage.rb
81
+ - lib/snapshot/events/death.rb
82
+ - lib/snapshot/events/explosion.rb
83
+ - lib/snapshot/events/hammer_hit.rb
84
+ - lib/snapshot/events/sound_world.rb
85
+ - lib/snapshot/events/spawn.rb
86
+ - lib/snapshot/items/character.rb
87
+ - lib/snapshot/items/client_info.rb
88
+ - lib/snapshot/items/flag.rb
89
+ - lib/snapshot/items/game_data.rb
90
+ - lib/snapshot/items/game_data_flag.rb
91
+ - lib/snapshot/items/game_data_team.rb
92
+ - lib/snapshot/items/laser.rb
93
+ - lib/snapshot/items/pickup.rb
94
+ - lib/snapshot/items/player_info.rb
95
+ - lib/snapshot/items/player_input.rb
96
+ - lib/snapshot/items/projectile.rb
97
+ - lib/snapshot/items/spectator_info.rb
98
+ - lib/snapshot/items/tune_params.rb
99
+ - lib/snapshot/snap_event_base.rb
100
+ - lib/snapshot/snap_item_base.rb
101
+ - lib/snapshot/unpacker.rb
102
+ - lib/string.rb
103
+ - lib/teeworlds_client.rb
104
+ - lib/teeworlds_network.rb
105
+ - lib/teeworlds_server.rb
106
+ - lib/version.rb
107
+ homepage: https://github.com/ChillerDragon/teeworlds_network
108
+ licenses:
109
+ - Unlicense
110
+ metadata:
111
+ rubygems_mfa_required: 'true'
112
+ documentation_uri: https://github.com/ChillerDragon/teeworlds_network/tree/master/docs
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: 3.1.2
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 3.3.7
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: teeworlds 0.7 network protocol (client and server)
132
+ test_files: []