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
data/lib/string.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ AVAILABLE_COLORS = %i[
4
+ red green yellow pink magenta blue cyan white
5
+ bg_red bg_green bg_yellow bg_pink bg_magenta bg_blue bg_cyan bg_white
6
+ ].freeze
7
+
8
+ # String color
9
+ class String
10
+ def colorize(color_code)
11
+ "\e[#{color_code}m#{self}\e[0m"
12
+ end
13
+
14
+ # foreground
15
+ def red
16
+ colorize(31)
17
+ end
18
+
19
+ def green
20
+ colorize(32)
21
+ end
22
+
23
+ def yellow
24
+ colorize(33)
25
+ end
26
+
27
+ def blue
28
+ colorize(34)
29
+ end
30
+
31
+ def pink
32
+ colorize(35)
33
+ end
34
+
35
+ # keklul pink alias
36
+ def magenta
37
+ colorize(35)
38
+ end
39
+
40
+ def cyan
41
+ colorize(36)
42
+ end
43
+
44
+ def white
45
+ colorize(37)
46
+ end
47
+
48
+ # background
49
+ def bg_red
50
+ colorize(41)
51
+ end
52
+
53
+ def bg_green
54
+ colorize(42)
55
+ end
56
+
57
+ def bg_yellow
58
+ colorize(43)
59
+ end
60
+
61
+ def bg_blue
62
+ colorize(44)
63
+ end
64
+
65
+ def bg_pink
66
+ colorize(45)
67
+ end
68
+
69
+ # keklul pink alias
70
+ def bg_magenta
71
+ colorize(45)
72
+ end
73
+
74
+ def bg_cyan
75
+ colorize(46)
76
+ end
77
+
78
+ def bg_white
79
+ colorize(47)
80
+ end
81
+ end
@@ -0,0 +1,506 @@
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 'messages/server_info'
12
+ require_relative 'net_base'
13
+ require_relative 'packer'
14
+ require_relative 'models/player'
15
+ require_relative 'game_client'
16
+ require_relative 'config'
17
+
18
+ class TeeworldsClient
19
+ attr_reader :state, :hooks, :game_client
20
+ attr_accessor :rcon_authed, :local_client_id
21
+
22
+ def initialize(options = {})
23
+ @verbose = options[:verbose] || false
24
+ @state = NET_CONNSTATE_OFFLINE
25
+ @ip = 'localhost'
26
+ @port = 8303
27
+ @local_client_id = 0
28
+ @config = Config.new(file: options[:config])
29
+ @hooks = {
30
+ chat: [],
31
+ map_change: [],
32
+ client_info: [],
33
+ client_drop: [],
34
+ connected: [],
35
+ disconnect: [],
36
+ rcon_line: [],
37
+ snapshot: [],
38
+ input_timing: [],
39
+ auth_on: [],
40
+ auth_off: [],
41
+ rcon_cmd_add: [],
42
+ rcon_cmd_rem: [],
43
+ maplist_entry_add: [],
44
+ maplist_entry_rem: []
45
+ }
46
+ @thread_running = false
47
+ @signal_disconnect = false
48
+ @game_client = GameClient.new(self)
49
+ @start_info = {
50
+ name: 'ruby gamer',
51
+ clan: '',
52
+ country: -1,
53
+ body: 'spiky',
54
+ marking: 'duodonny',
55
+ decoration: '',
56
+ hands: 'standard',
57
+ feet: 'standard',
58
+ eyes: 'standard',
59
+ custom_color_body: 0,
60
+ custom_color_marking: 0,
61
+ custom_color_decoration: 0,
62
+ custom_color_hands: 0,
63
+ custom_color_feet: 0,
64
+ custom_color_eyes: 0,
65
+ color_body: 0,
66
+ color_marking: 0,
67
+ color_decoration: 0,
68
+ color_hands: 0,
69
+ color_feet: 0,
70
+ color_eyes: 0
71
+ }
72
+ @rcon_authed = false
73
+ end
74
+
75
+ def rcon_authed?
76
+ @rcon_authed
77
+ end
78
+
79
+ def on_auth_on(&block)
80
+ @hooks[:auth_on].push(block)
81
+ end
82
+
83
+ def on_auth_off(&block)
84
+ @hooks[:auth_off].push(block)
85
+ end
86
+
87
+ def on_rcon_cmd_add(&block)
88
+ @hooks[:rcon_cmd_add].push(block)
89
+ end
90
+
91
+ def on_rcon_cmd_rem(&block)
92
+ @hooks[:rcon_cmd_rem].push(block)
93
+ end
94
+
95
+ def on_maplist_entry_add(&block)
96
+ @hooks[:maplist_entry_add].push(block)
97
+ end
98
+
99
+ def on_maplist_entry_rem(&block)
100
+ @hooks[:maplist_entry_rem].push(block)
101
+ end
102
+
103
+ def on_chat(&block)
104
+ @hooks[:chat].push(block)
105
+ end
106
+
107
+ def on_map_change(&block)
108
+ @hooks[:map_change].push(block)
109
+ end
110
+
111
+ def on_client_info(&block)
112
+ @hooks[:client_info].push(block)
113
+ end
114
+
115
+ def on_client_drop(&block)
116
+ @hooks[:client_drop].push(block)
117
+ end
118
+
119
+ def on_connected(&block)
120
+ @hooks[:connected].push(block)
121
+ end
122
+
123
+ def on_disconnect(&block)
124
+ @hooks[:disconnect].push(block)
125
+ end
126
+
127
+ def on_rcon_line(&block)
128
+ @hooks[:rcon_line].push(block)
129
+ end
130
+
131
+ def on_snapshot(&block)
132
+ @hooks[:snapshot].push(block)
133
+ end
134
+
135
+ def on_input_timing(&block)
136
+ @hooks[:input_timing].push(block)
137
+ end
138
+
139
+ def send_chat(str)
140
+ @netbase.send_packet(
141
+ NetChunk.create_header(vital: true, size: 4 + str.length) +
142
+ [
143
+ pack_msg_id(NETMSGTYPE_CL_SAY),
144
+ CHAT_ALL,
145
+ 64 # should use TARGET_SERVER (-1) instead of hacking 64 in here
146
+ ] +
147
+ Packer.pack_str(str)
148
+ )
149
+ end
150
+
151
+ def connect(ip, port, options = {})
152
+ options[:detach] = options[:detach] || false
153
+ if options[:detach] && @thread_running
154
+ puts 'Error: connection thread already running call disconnect() first'
155
+ return
156
+ end
157
+ disconnect
158
+ @signal_disconnect = false
159
+ @ticks = 0
160
+ @game_client = GameClient.new(self)
161
+ # me trying to write cool code
162
+ @client_token = (1..4).to_a.map { |_| rand(0..255) }
163
+ @client_token = @client_token.map { |b| b.to_s(16).rjust(2, '0') }.join
164
+ puts "client token #{@client_token}"
165
+ @netbase = NetBase.new(verbose: @verbose)
166
+ NetChunk.reset
167
+ @ip = ip
168
+ @port = port
169
+ puts "connecting to #{@ip}:#{@port} .."
170
+ @s = UDPSocket.new
171
+ @s.connect(ip, port)
172
+ puts "client port: #{@s.addr[1]}"
173
+ @netbase.connect(@s, @ip, @port)
174
+ @token = nil
175
+ send_ctrl_with_token
176
+ if options[:detach]
177
+ @thread_running = true
178
+ Thread.new do
179
+ connection_loop
180
+ end
181
+ else
182
+ connection_loop
183
+ end
184
+ end
185
+
186
+ # TODO: this is same in client and server
187
+ # move to NetBase???
188
+ def send_ctrl_close
189
+ @netbase&.send_packet([NET_CTRLMSG_CLOSE], chunks: 0, control: true)
190
+ end
191
+
192
+ def disconnect
193
+ puts 'disconnecting.'
194
+ send_ctrl_close
195
+ @s&.close
196
+ @signal_disconnect = true
197
+ end
198
+
199
+ def set_startinfo(info)
200
+ info.each do |key, value|
201
+ unless @start_info.key?(key)
202
+ puts "Error: invalid start info key '#{key}'"
203
+ puts " valid keys: #{@start_info.keys}"
204
+ exit 1
205
+ end
206
+ @start_info[key] = value
207
+ end
208
+ end
209
+
210
+ def send_msg(data)
211
+ @netbase.send_packet(data)
212
+ end
213
+
214
+ def send_ctrl_keepalive
215
+ @netbase.send_packet([NET_CTRLMSG_KEEPALIVE], chunks: 0, control: true)
216
+ end
217
+
218
+ def send_msg_connect
219
+ msg = [NET_CTRLMSG_CONNECT] + str_bytes(@client_token) + Array.new(501, 0x00)
220
+ @netbase.send_packet(msg, chunks: 0, control: true)
221
+ end
222
+
223
+ def send_ctrl_with_token
224
+ @state = NET_CONNSTATE_TOKEN
225
+ msg = [NET_CTRLMSG_TOKEN] + str_bytes(@client_token) + Array.new(512, 0x00)
226
+ @netbase.send_packet(msg, chunks: 0, control: true)
227
+ end
228
+
229
+ def send_info
230
+ data = []
231
+ data += Packer.pack_str(GAME_NETVERSION)
232
+ data += Packer.pack_str(@config.password)
233
+ data += Packer.pack_int(CLIENT_VERSION)
234
+ msg = NetChunk.create_header(vital: true, size: data.size + 1) +
235
+ [pack_msg_id(NETMSG_INFO, system: true)] +
236
+ data
237
+
238
+ @netbase.send_packet(msg)
239
+ end
240
+
241
+ def rcon_auth(name, password = nil)
242
+ if name.instance_of?(Hash)
243
+ password = name[:password]
244
+ name = name[:name]
245
+ end
246
+ if password.nil?
247
+ raise "Error: password can not be empty\n" \
248
+ " provide two strings: name, password\n" \
249
+ " or a hash with the key :password\n" \
250
+ "\n" \
251
+ " rcon_auth('', '123')\n" \
252
+ " rcon_auth(password: '123')\n"
253
+ end
254
+ data = []
255
+ if name.nil? || name == ''
256
+ data += Packer.pack_str(password)
257
+ else # ddnet auth using name, password and some int?
258
+ data += Packer.pack_str(name)
259
+ data += Packer.pack_str(password)
260
+ data += Packer.pack_int(1)
261
+ end
262
+ msg = NetChunk.create_header(vital: true, size: data.size + 1) +
263
+ [pack_msg_id(NETMSG_RCON_AUTH, system: true)] +
264
+ data
265
+ @netbase.send_packet(msg)
266
+ end
267
+
268
+ def rcon(command)
269
+ data = []
270
+ data += Packer.pack_str(command)
271
+ msg = NetChunk.create_header(vital: true, size: data.size + 1) +
272
+ [pack_msg_id(NETMSG_RCON_CMD, system: true)] +
273
+ data
274
+ @netbase.send_packet(msg)
275
+ end
276
+
277
+ def send_msg_start_info
278
+ data = []
279
+
280
+ @start_info.each do |key, value|
281
+ if value.instance_of?(String)
282
+ data += Packer.pack_str(value)
283
+ elsif value.instance_of?(Integer)
284
+ data += Packer.pack_int(value)
285
+ else
286
+ puts "Error: invalid start info #{key}: #{value}"
287
+ exit 1
288
+ end
289
+ end
290
+
291
+ @netbase.send_packet(
292
+ NetChunk.create_header(vital: true, size: data.size + 1) +
293
+ [pack_msg_id(NETMSGTYPE_CL_STARTINFO, system: false)] +
294
+ data
295
+ )
296
+ end
297
+
298
+ def send_msg_ready
299
+ @netbase.send_packet(
300
+ NetChunk.create_header(vital: true, size: 1) +
301
+ [pack_msg_id(NETMSG_READY, system: true)]
302
+ )
303
+ end
304
+
305
+ def send_enter_game
306
+ @netbase.send_packet(
307
+ NetChunk.create_header(vital: true, size: 1) +
308
+ [pack_msg_id(NETMSG_ENTERGAME, system: true)]
309
+ )
310
+ end
311
+
312
+ def send_input
313
+ inp = {
314
+ direction: -1,
315
+ target_x: 10,
316
+ target_y: 10,
317
+ jump: rand(0..1),
318
+ fire: 0,
319
+ hook: 0,
320
+ player_flags: 0,
321
+ wanted_weapon: 0,
322
+ next_weapon: 0,
323
+ prev_weapon: 0
324
+ }
325
+
326
+ data = []
327
+ data += Packer.pack_int(@game_client.ack_game_tick)
328
+ data += Packer.pack_int(@game_client.pred_game_tick)
329
+ data += Packer.pack_int(40) # magic size 40
330
+ data += Packer.pack_int(inp[:direction])
331
+ data += Packer.pack_int(inp[:target_x])
332
+ data += Packer.pack_int(inp[:target_y])
333
+ data += Packer.pack_int(inp[:jump])
334
+ data += Packer.pack_int(inp[:fire])
335
+ data += Packer.pack_int(inp[:hook])
336
+ data += Packer.pack_int(inp[:player_flags])
337
+ data += Packer.pack_int(inp[:wanted_weapon])
338
+ data += Packer.pack_int(inp[:next_weapon])
339
+ data += Packer.pack_int(inp[:prev_weapon])
340
+ msg = NetChunk.create_header(vital: false, size: data.size + 1) +
341
+ [pack_msg_id(NETMSG_INPUT, system: true)] +
342
+ data
343
+ @netbase.send_packet(msg)
344
+ end
345
+
346
+ private
347
+
348
+ def on_msg_token(data)
349
+ # TODO: add hook
350
+ @token = bytes_to_str(data)
351
+ @netbase.set_peer_token(@token)
352
+ puts "Got token #{@token}"
353
+ send_msg_connect
354
+ end
355
+
356
+ def on_msg_accept
357
+ # TODO: add hook
358
+ puts 'got accept. connection online'
359
+ @state = NET_CONNSTATE_ONLINE
360
+ send_info
361
+ end
362
+
363
+ def on_msg_close(data)
364
+ @game_client.on_disconnect(data)
365
+ end
366
+
367
+ # CClient::ProcessConnlessPacket
368
+ def on_ctrl_message(msg, data)
369
+ case msg
370
+ when NET_CTRLMSG_TOKEN then on_msg_token(data)
371
+ when NET_CTRLMSG_ACCEPT then on_msg_accept
372
+ when NET_CTRLMSG_CLOSE then on_msg_close(data)
373
+ when NET_CTRLMSG_KEEPALIVE # silently ignore keepalive
374
+ else
375
+ puts "Uknown control message #{msg}"
376
+ exit(1)
377
+ end
378
+ end
379
+
380
+ def on_message(chunk)
381
+ case chunk.msg
382
+ when NETMSGTYPE_SV_READYTOENTER then @game_client.on_ready_to_enter(chunk)
383
+ when NETMSGTYPE_SV_CLIENTINFO then @game_client.on_client_info(chunk)
384
+ when NETMSGTYPE_SV_CLIENTDROP then @game_client.on_client_drop(chunk)
385
+ when NETMSGTYPE_SV_EMOTICON then @game_client.on_emoticon(chunk)
386
+ when NETMSGTYPE_SV_CHAT then @game_client.on_chat(chunk)
387
+ else
388
+ puts "todo non sys chunks. skipped msg: #{chunk.msg}" if @verbose
389
+ end
390
+ end
391
+
392
+ def process_chunk(chunk)
393
+ unless chunk.sys
394
+ on_message(chunk)
395
+ return
396
+ end
397
+ puts "proccess chunk with msg: #{chunk.msg}" if @verbose
398
+ case chunk.msg
399
+ when NETMSG_MAP_CHANGE
400
+ @game_client.on_map_change(chunk)
401
+ when NETMSG_SERVERINFO
402
+ puts 'ignore server info for now'
403
+ when NETMSG_CON_READY
404
+ @game_client.on_connected
405
+ when NETMSG_NULL
406
+ # should we be in alert here?
407
+ when NETMSG_RCON_LINE
408
+ @game_client.on_rcon_line(chunk)
409
+ when NETMSG_SNAP, NETMSG_SNAPSINGLE, NETMSG_SNAPEMPTY
410
+ @game_client.on_snapshot(chunk)
411
+ when NETMSG_INPUTTIMING
412
+ @game_client.on_input_timing(chunk)
413
+ when NETMSG_RCON_AUTH_ON
414
+ @game_client.on_auth_on
415
+ when NETMSG_RCON_AUTH_OFF
416
+ @game_client.on_auth_off
417
+ when NETMSG_RCON_CMD_ADD
418
+ @game_client.on_rcon_cmd_add(chunk)
419
+ when NETMSG_RCON_CMD_REM
420
+ @game_client.on_rcon_cmd_rem(chunk)
421
+ when NETMSG_MAPLIST_ENTRY_ADD
422
+ @game_client.on_maplist_entry_add(chunk)
423
+ when NETMSG_MAPLIST_ENTRY_REM
424
+ @game_client.on_maplist_entry_rem(chunk)
425
+ else
426
+ puts "Unsupported system msg: #{chunk.msg}"
427
+ p str_hex(chunk.full_raw)
428
+ exit(1)
429
+ end
430
+ end
431
+
432
+ def process_server_packet(packet)
433
+ data = packet.payload
434
+ if data.size.zero?
435
+ puts 'Error: packet payload is empty'
436
+ puts packet.to_s
437
+ return
438
+ end
439
+ chunks = BigChungusTheChunkGetter.get_chunks(data)
440
+ chunks.each do |chunk|
441
+ if chunk.flags_vital && !chunk.flags_resend && chunk.msg != NETMSG_NULL
442
+ @netbase.ack = (@netbase.ack + 1) % NET_MAX_SEQUENCE
443
+ puts "got ack: #{@netbase.ack}" if @verbose
444
+ end
445
+ process_chunk(chunk)
446
+ end
447
+ end
448
+
449
+ def tick
450
+ # puts "tick"
451
+ begin
452
+ pck = @s.recvfrom_nonblock(1400)
453
+ rescue Errno::ECONNREFUSED
454
+ puts 'connection problems ...'
455
+ pck = nil
456
+ rescue IO::EAGAINWaitReadable
457
+ pck = nil
458
+ end
459
+ if pck.nil? && @token.nil?
460
+ @wait_for_token ||= 0
461
+ @wait_for_token += 1
462
+ if @wait_for_token > 6
463
+ @token = nil
464
+ send_ctrl_with_token
465
+ puts 'retrying connection ...'
466
+ end
467
+ end
468
+ return unless pck
469
+
470
+ data = pck.first
471
+
472
+ packet = Packet.new(data, '<')
473
+ puts packet.to_s if @verbose
474
+
475
+ # process connless packets data
476
+ if packet.flags_control
477
+ msg = data[PACKET_HEADER_SIZE].unpack1('C*')
478
+ on_ctrl_message(msg, data[(PACKET_HEADER_SIZE + 1)..])
479
+ else # process non-connless packets
480
+ process_server_packet(packet)
481
+ end
482
+
483
+ send_ctrl_keepalive if (@ticks % 8).zero?
484
+ if @game_client.ack_game_tick.positive?
485
+ now = Time.now
486
+ @@last_pred ||= now
487
+ diff = now - @@last_pred
488
+ # @Swarfey does js setInterval(20) in his lib
489
+ # not sure if it makes sense to do a diff > 0.2 here then xd
490
+ @game_client.pred_game_tick += 1 if diff > 0.2
491
+ end
492
+
493
+ @ticks += 1
494
+ send_input if (@ticks % 20).zero?
495
+ end
496
+
497
+ def connection_loop
498
+ until @signal_disconnect
499
+ tick
500
+ # TODO: proper tick speed sleep
501
+ sleep 0.001
502
+ end
503
+ @thread_running = false
504
+ @signal_disconnect = false
505
+ end
506
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'teeworlds_server'
4
+ require_relative 'teeworlds_client'