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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../network'
4
+ require_relative '../models/player'
5
+ require_relative '../packer'
6
+
7
+ class ServerInfo
8
+ attr_accessor :version, :name, :hostname, :map, :gametype, :flags, :num_players, :max_players, :num_clients,
9
+ :max_clients, :players
10
+
11
+ def initialize
12
+ # short tokenless version
13
+ @version = GAME_VERSION # '0.7.5'
14
+ @name = 'unnamed ruby server'
15
+ @hostname = 'localhost'
16
+ @map = 'dm1'
17
+ @gametype = 'dm'
18
+ @flags = 0
19
+ @num_players = 1
20
+ @max_players = MAX_PLAYERS
21
+ @num_clients = 1
22
+ @max_clients = MAX_CLIENTS
23
+
24
+ # token only
25
+ @players = [
26
+ Player.new(
27
+ id: 0,
28
+ local: 0,
29
+ team: 0,
30
+ name: 'sample player',
31
+ clan: '',
32
+ country: -1
33
+ )
34
+ ]
35
+ end
36
+
37
+ def to_s
38
+ "version=#{@version} gametype=#{gametype} map=#{map} name=#{name}"
39
+ end
40
+
41
+ # basically to_network
42
+ # int array the server sends to the client
43
+ def to_a
44
+ data = []
45
+ data = Packer.pack_str(@version) +
46
+ Packer.pack_str(@name) +
47
+ Packer.pack_str(@hostname) +
48
+ Packer.pack_str(@map) +
49
+ Packer.pack_str(@gametype) +
50
+ Packer.pack_int(@flags) +
51
+ Packer.pack_int(@num_players) +
52
+ Packer.pack_int(@max_players) +
53
+ Packer.pack_int(@num_clients) +
54
+ Packer.pack_int(@max_clients)
55
+ @players.each do |player|
56
+ data += Packer.pack_str(player.name)
57
+ data += Packer.pack_str(player.clan)
58
+ data += Packer.pack_int(player.country)
59
+ data += Packer.pack_int(player.score)
60
+ data += Packer.pack_int(0) # TODO: bot and spec flag
61
+ end
62
+ data
63
+ end
64
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ServerSettings
4
+ def initialize(attr = {})
5
+ @kick_vote = attr[:kick_vote] || 0
6
+ @kick_min = attr[:kick_min] || 0
7
+ @spec_vote = attr[:spec_vote] || 0
8
+ @team_lock = attr[:team_lock] || 0
9
+ @team_balance = attr[:team_balance] || 0
10
+ @player_slots = attr[:player_slots] || 16
11
+ end
12
+
13
+ # basically to_network
14
+ # int array the server sends to the client
15
+ def to_a
16
+ Packer.pack_int(@kick_vote) +
17
+ Packer.pack_int(@kick_min) +
18
+ Packer.pack_int(@spec_vote) +
19
+ Packer.pack_int(@team_lock) +
20
+ Packer.pack_int(@team_balance) +
21
+ Packer.pack_int(@player_slots)
22
+ end
23
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ # TODO: use this on the client side instead of the other hash
6
+
7
+ ##
8
+ # StartInfo
9
+ #
10
+ # Client -> Server
11
+ class StartInfo
12
+ attr_accessor :name, :clan, :country, :body, :marking, :decoration, :hands, :feet, :eyes,
13
+ :custom_color_body, :custom_color_marking, :custom_color_decoration,
14
+ :custom_color_hands, :custom_color_feet, :custom_color_eyes,
15
+ :color_body, :color_marking, :color_decoration, :color_hands, :color_feet, :color_eyes
16
+
17
+ def initialize(hash_or_raw)
18
+ if hash_or_raw.instance_of?(Hash)
19
+ init_hash(hash_or_raw)
20
+ else
21
+ init_raw(hash_or_raw)
22
+ end
23
+ end
24
+
25
+ def init_raw(data)
26
+ u = Unpacker.new(data)
27
+ @name = u.get_string
28
+ @clan = u.get_string
29
+ @country = u.get_int
30
+ @body = u.get_string
31
+ @marking = u.get_string
32
+ @decoration = u.get_string
33
+ @hands = u.get_string
34
+ @feet = u.get_string
35
+ @eyes = u.get_string
36
+ @custom_color_body = u.get_int
37
+ @custom_color_marking = u.get_int
38
+ @custom_color_decoration = u.get_int
39
+ @custom_color_hands = u.get_int
40
+ @custom_color_feet = u.get_int
41
+ @custom_color_eyes = u.get_int
42
+ @color_body = u.get_int
43
+ @color_marking = u.get_int
44
+ @color_decoration = u.get_int
45
+ @color_hands = u.get_int
46
+ @color_feet = u.get_int
47
+ @color_eyes = u.get_int
48
+ end
49
+
50
+ def init_hash(attr)
51
+ @name = attr[:name] || 'ruby gamer'
52
+ @clan = attr[:clan] || ''
53
+ @country = attr[:country] || -1
54
+ @body = attr[:body] || 'spiky'
55
+ @marking = attr[:marking] || 'duodonny'
56
+ @decoration = attr[:decoration] || ''
57
+ @hands = attr[:hands] || 'standard'
58
+ @feet = attr[:feet] || 'standard'
59
+ @eyes = attr[:eyes] || 'standard'
60
+ @custom_color_body = attr[:custom_color_body] || 0
61
+ @custom_color_marking = attr[:custom_color_marking] || 0
62
+ @custom_color_decoration = attr[:custom_color_decoration] || 0
63
+ @custom_color_hands = attr[:custom_color_hands] || 0
64
+ @custom_color_feet = attr[:custom_color_feet] || 0
65
+ @custom_color_eyes = attr[:custom_color_eyes] || 0
66
+ @color_body = attr[:color_body] || 0
67
+ @color_marking = attr[:color_marking] || 0
68
+ @color_decoration = attr[:color_decoration] || 0
69
+ @color_hands = attr[:color_hands] || 0
70
+ @color_feet = attr[:color_feet] || 0
71
+ @color_eyes = attr[:color_eyes] || 0
72
+ end
73
+
74
+ def to_h
75
+ {
76
+ name: @name,
77
+ clan: @clan,
78
+ country: @country,
79
+ body: @body,
80
+ marking: @marking,
81
+ decoration: @decoration,
82
+ hands: @hands,
83
+ feet: @feet,
84
+ eyes: @eyes,
85
+ custom_color_body: @custom_color_body,
86
+ custom_color_marking: @custom_color_marking,
87
+ custom_color_decoration: @custom_color_decoration,
88
+ custom_color_hands: @custom_color_hands,
89
+ custom_color_feet: @custom_color_feet,
90
+ custom_color_eyes: @custom_color_eyes,
91
+ color_body: @color_body,
92
+ color_marking: @color_marking,
93
+ color_decoration: @color_decoration,
94
+ color_hands: @color_hands,
95
+ color_feet: @color_feet,
96
+ color_eyes: @color_eyes
97
+ }
98
+ end
99
+
100
+ # basically to_network
101
+ # int array the client sends to the server
102
+ def to_a
103
+ Packer.pack_str(@name) +
104
+ Packer.pack_str(@clan) +
105
+ Packer.pack_int(@country) +
106
+ Packer.pack_str(@body) +
107
+ Packer.pack_str(@marking) +
108
+ Packer.pack_str(@decoration) +
109
+ Packer.pack_str(@hands) +
110
+ Packer.pack_str(@feet) +
111
+ Packer.pack_str(@eyes) +
112
+ Packer.pack_int(@custom_color_body) +
113
+ Packer.pack_int(@custom_color_marking) +
114
+ Packer.pack_int(@custom_color_decoration) +
115
+ Packer.pack_int(@custom_color_hands) +
116
+ Packer.pack_int(@custom_color_feet) +
117
+ Packer.pack_int(@custom_color_eyes) +
118
+ Packer.pack_int(@color_body) +
119
+ Packer.pack_int(@color_marking) +
120
+ Packer.pack_int(@color_decoration) +
121
+ Packer.pack_int(@color_hands) +
122
+ Packer.pack_int(@color_feet) +
123
+ Packer.pack_int(@color_eyes)
124
+ end
125
+
126
+ def to_s
127
+ to_h
128
+ end
129
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # SvClientDrop
7
+ #
8
+ # Server -> Client
9
+ class SvClientDrop
10
+ attr_accessor :client_id, :reason, :silent
11
+
12
+ def initialize(hash_or_raw)
13
+ if hash_or_raw.instance_of?(Hash)
14
+ init_hash(hash_or_raw)
15
+ else
16
+ init_raw(hash_or_raw)
17
+ end
18
+ end
19
+
20
+ def init_raw(data)
21
+ u = Unpacker.new(data)
22
+ @client_id = u.get_int
23
+ @reason = u.get_string
24
+ @reason = @reason == '' ? nil : @reason
25
+ @silent = u.get_int
26
+ end
27
+
28
+ def init_hash(attr)
29
+ @client_id = attr[:client_id] || 0
30
+ @reason = attr[:reason] == '' ? nil : attr[:reason]
31
+ @silent = attr[:silent] || false
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ client_id: @client_id,
37
+ reason: @reason,
38
+ silent: @silent
39
+ }
40
+ end
41
+
42
+ def silent?
43
+ !@silent.zero?
44
+ end
45
+
46
+ # basically to_network
47
+ # int array the Server sends to the Client
48
+ def to_a
49
+ Packer.pack_int(@client_id) +
50
+ Packer.pack_str(@reason) +
51
+ Packer.pack_int(@silent)
52
+ end
53
+
54
+ def to_s
55
+ to_h
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChatMesage
4
+ attr_reader :mode, :client_id, :target_id, :message, :author
5
+
6
+ def initialize(data = {})
7
+ # @mode
8
+ # Type: Integer
9
+ @mode = data[:mode]
10
+
11
+ # @client_id
12
+ # Type: Integer
13
+ @client_id = data[:client_id]
14
+
15
+ # @target_id
16
+ # Type: Integer
17
+ @target_id = data[:target_id]
18
+
19
+ # @message
20
+ # Type: String
21
+ @message = data[:message]
22
+
23
+ # @author
24
+ # Type: Player see player.rb
25
+ @author = data[:author]
26
+ end
27
+
28
+ def to_s
29
+ # server message
30
+ return "*** #{@message}" if @client_id == -1
31
+
32
+ # player message
33
+ # should never be from an invalid id
34
+ # but lets not crash if servers send weird stuff
35
+ name = ''
36
+ name = @author.name if @author
37
+ "#{name}: #{@message}"
38
+ end
39
+ end
data/lib/models/map.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../bytes'
4
+
5
+ class Map
6
+ attr_reader :name, :crc, :crc_str, :crc_arr, :size, :sha256, :sha256_str, :sha256_arr
7
+
8
+ def initialize(attr = {})
9
+ # map name as String
10
+ @name = attr[:name]
11
+
12
+ # crc hex encoded string (8 characters / 4 bytes)
13
+ @crc = attr[:crc]
14
+ raise "Error: map crc invalid type: #{@crc.class}" unless @crc.instance_of?(String)
15
+
16
+ unless @crc.match(/[a-fA-F0-9]{8}/) # str encoded hex
17
+ raise "Error: map crc raw string expects size 8 but got #{@crc.size}"
18
+ end
19
+
20
+ @crc_str = @crc
21
+ @crc_arr = str_bytes(@crc)
22
+ @crc = @crc_arr.pack('C*')
23
+
24
+ # size has to be a positive Integer
25
+ @size = attr[:size]
26
+
27
+ # sha256 can be:
28
+ # hex encoded string (64 characters / 32 bytes)
29
+ # '491af17a510214506270904f147a4c30ae0a85b91bb854395bef8c397fc078c3'
30
+ #
31
+ # raw string (32 characters)
32
+ # array of integers representing the bytes (32 elements)
33
+ @sha256 = attr[:sha256]
34
+
35
+ if @sha256.instance_of?(String)
36
+ if @sha256.match(/[a-fA-F0-9]{64}/) # str encoded hex
37
+ @sha256_str = @sha256
38
+ @sha256_arr = str_bytes(@sha256)
39
+ @sha256 = @sha256_arr.pack('C*')
40
+ elsif @sha256.length == 32 # raw byte string
41
+ @sha256_arr = @sha256
42
+ @sha256 = @sha256_arr.pack('C*')
43
+ @sha256_str = str_hex(@sha256).gsub(' ', '')
44
+ else
45
+ raise "Error: map sha256 raw string expects size 64 but got #{@sha256.size}"
46
+ end
47
+ elsif @sha256.instance_of?(Array) # int byte array
48
+ raise "Error: map sha256 array expects size 32 but got #{@sha256.size}" if @sha256.size != 32
49
+
50
+ @sha256_arr = @sha256
51
+ @sha256 = @sha256.pack('C*')
52
+ @sha256_str = @sha256.map { |b| b.to_s(16).rjust(2, '0') }.join
53
+ else
54
+ raise "Error: map sha256 invalid type: #{@sha256.class}"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NetAddr
4
+ attr_accessor :ip, :port
5
+
6
+ def initialize(ip, port)
7
+ @ip = ip
8
+ @port = port
9
+ end
10
+
11
+ def to_s
12
+ "#{@ip}:#{@port}"
13
+ end
14
+
15
+ def eq(addr)
16
+ @ip == addr.ip && @port == addr.port
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PacketFlags
4
+ attr_reader :bits, :hash
5
+
6
+ def initialize(data)
7
+ @hash = {}
8
+ @bits = ''
9
+ if data.instance_of?(Hash)
10
+ @bits = parse_hash(data)
11
+ @hash = data
12
+ elsif data.instance_of?(String)
13
+ @hash = parse_bits(data)
14
+ @bits = data
15
+ else
16
+ raise 'Flags have to be hash or string'
17
+ end
18
+ end
19
+
20
+ def parse_hash(hash)
21
+ bits = ''
22
+ bits += hash[:connection] ? '1' : '0'
23
+ bits += hash[:compressed] ? '1' : '0'
24
+ bits += hash[:resend] ? '1' : '0'
25
+ bits += hash[:control] ? '1' : '0'
26
+ bits
27
+ end
28
+
29
+ def parse_bits(four_bit_str)
30
+ # takes a 4 character string
31
+ # representing the middle of the first byte sent
32
+ # in binary representation
33
+ #
34
+ # and creates a hash out of it
35
+ hash = {}
36
+ hash[:connection] = four_bit_str[0] == '1'
37
+ hash[:compressed] = four_bit_str[1] == '1'
38
+ hash[:resend] = four_bit_str[2] == '1'
39
+ hash[:control] = four_bit_str[3] == '1'
40
+ hash
41
+ end
42
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Player
4
+ attr_accessor :id, :local, :team, :name, :clan, :country, :skin_parts, :skin_custom_colors, :skin_colors, :score
5
+
6
+ def initialize(data = {})
7
+ @id = data[:id] || -1
8
+ @local = data[:local] == 1
9
+ @team = data[:team] || 0
10
+ @name = data[:name] || '(connecting..)'
11
+ @clan = data[:clan] || ''
12
+ @country = data[:country] || -1
13
+ @skin_parts = data[:skin_parts] || Array.new(6, 'standard')
14
+ @skin_custom_colors = data[:skin_custom_colors] || Array.new(6, 0)
15
+ @skin_colors = data[:skin_colors] || Array.new(6, 0)
16
+
17
+ @score = data[:score] || 0
18
+ end
19
+
20
+ def local?
21
+ @local
22
+ end
23
+
24
+ def set_start_info(start_info)
25
+ raise "expected: StartInfo got: #{start_info.class}" unless start_info.instance_of?(StartInfo)
26
+
27
+ start_info = start_info.to_h
28
+ @name = start_info[:name]
29
+ @clan = start_info[:clan]
30
+ @country = start_info[:country]
31
+ @skin_parts = [
32
+ start_info[:body],
33
+ start_info[:marking],
34
+ start_info[:decoration],
35
+ start_info[:hands],
36
+ start_info[:feet],
37
+ start_info[:eyes]
38
+ ]
39
+ @skin_custom_colors = [
40
+ start_info[:custom_color_body],
41
+ start_info[:custom_color_marking],
42
+ start_info[:custom_color_decoration],
43
+ start_info[:custom_color_hands],
44
+ start_info[:custom_color_feet],
45
+ start_info[:custom_color_eyes]
46
+ ]
47
+ @skin_colors = [
48
+ start_info[:color_body],
49
+ start_info[:color_marking],
50
+ start_info[:color_decoration],
51
+ start_info[:color_hands],
52
+ start_info[:color_feet],
53
+ start_info[:color_eyes]
54
+ ]
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../bytes'
4
+
5
+ class SecurityToken
6
+ def self.err_msg(msg, token)
7
+ hex = ''
8
+ hex = "hex: #{str_hex(token)}\n" if token.instance_of?(String)
9
+ "Invalid token! Token should be a human readable hex string!\n" \
10
+ " Good sample token: aabbccdd\n" \
11
+ " #{msg}\n" \
12
+ " token: #{token}:#{token.class}\n" \
13
+ " #{hex}"
14
+ end
15
+
16
+ def self.validate(token)
17
+ raise err_msg("Expected type: String got: #{token.class}", token) unless token.instance_of?(String)
18
+ raise err_msg("Expected size: 8 got: #{token.size}", token) unless token.size == 8
19
+ end
20
+ end
data/lib/net_base.rb ADDED
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'models/token'
4
+
5
+ ##
6
+ # Turns int into network byte
7
+ #
8
+ # Takes a NETMSGTYPE_CL_* integer
9
+ # and returns a byte that can be send over
10
+ # the network
11
+ def pack_msg_id(msg_id, options = { system: false })
12
+ (msg_id << 1) | (options[:system] ? 1 : 0)
13
+ end
14
+
15
+ ##
16
+ # NetBase
17
+ #
18
+ # Lowest network layer logic. Sends packets via udp.
19
+ # Also adding the teeworlds protocol packet header.
20
+ class NetBase
21
+ attr_accessor :ack
22
+ attr_reader :peer_token
23
+
24
+ def initialize(opts = {})
25
+ @verbose = opts[:verbose] || false
26
+ @ip = nil
27
+ @port = nil
28
+ @s = nil
29
+ @ack = 0
30
+ @peer_token = [0xFF, 0xFF, 0xFF, 0xFF].map { |b| b.to_s(16).rjust(2, '0') }.join
31
+ end
32
+
33
+ def bind(socket)
34
+ @s = socket
35
+ end
36
+
37
+ def connect(socket, ip, port)
38
+ @s = socket
39
+ @ip = ip
40
+ @port = port
41
+ @ack = 0
42
+ end
43
+
44
+ def set_peer_token(token)
45
+ SecurityToken.validate(token)
46
+ @peer_token = token
47
+ end
48
+
49
+ ##
50
+ # Sends a packing setting the proper header for you
51
+ #
52
+ # @param payload [Array] The Integer list representing the data after the header
53
+ # @param opts [Hash] :chunks, :client and packet header flags for more details check the class +PacketFlags+
54
+ def send_packet(payload, opts = { chunks: 1, client: nil, addr: nil })
55
+ # unsigned char flags_ack; // 6bit flags, 2bit ack
56
+ # unsigned char ack; // 8bit ack
57
+ # unsigned char numchunks; // 8bit chunks
58
+ # unsigned char token[4]; // 32bit token
59
+ # // ffffffaa
60
+ # // aaaaaaaa
61
+ # // NNNNNNNN
62
+ # // TTTTTTTT
63
+ # // TTTTTTTT
64
+ # // TTTTTTTT
65
+ # // TTTTTTTT
66
+ flags_bits = PacketFlags.new(opts).bits
67
+ ack = @ack
68
+ ip = @ip
69
+ port = @port
70
+ token = @peer_token
71
+ unless opts[:client].nil?
72
+ ack = opts[:client].ack
73
+ ip = opts[:client].addr.ip
74
+ port = opts[:client].addr.port
75
+ token = opts[:client].token
76
+ end
77
+ unless opts[:addr].nil?
78
+ ip = opts[:addr].ip
79
+ port = opts[:addr].port
80
+ end
81
+ # unused flags ack num chunks
82
+ # ff ffff aa aaaa aaaa NNNN NNNN
83
+ header_bits = "00#{flags_bits}#{ack.to_s(2).rjust(10, '0')}#{opts[:chunks].to_s(2).rjust(8, '0')}"
84
+
85
+ header = header_bits.chars.groups_of(8).map do |eight_bits|
86
+ eight_bits.join.to_i(2)
87
+ end
88
+
89
+ header += str_bytes(token)
90
+ data = (header + payload).pack('C*')
91
+ client = opts[:client]
92
+ if @verbose
93
+ if client
94
+ puts "send to #{ip}:#{port} " \
95
+ "client(id=#{client.id} " \
96
+ "token=#{client.token} " \
97
+ "name=#{client.player.name} port=#{client.addr.port})"
98
+ else
99
+ puts "send to #{ip}:#{port}"
100
+ end
101
+ end
102
+ @s.send(data, 0, ip, port)
103
+
104
+ puts Packet.new(data, '>').to_s if @verbose || opts[:test]
105
+ end
106
+ end