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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../snap_item_base'
4
+
5
+ class NetObj
6
+ class Laser < SnapItemBase
7
+ attr_accessor :x, :y, :from_x, :from_y, :start_tick
8
+
9
+ def initialize(hash_or_raw)
10
+ @field_names = %i[
11
+ x
12
+ y
13
+ from_x
14
+ from_y
15
+ start_tick
16
+ ]
17
+ super
18
+ end
19
+
20
+ def self.match_type?(type)
21
+ type == NETOBJTYPE_LASER
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../snap_item_base'
4
+
5
+ class NetObj
6
+ class Pickup < SnapItemBase
7
+ attr_accessor :x, :y, :type
8
+
9
+ def initialize(hash_or_raw)
10
+ @field_names = %i[
11
+ x
12
+ y
13
+ type
14
+ ]
15
+ super
16
+ end
17
+
18
+ def self.match_type?(type)
19
+ type == NETOBJTYPE_PICKUP
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../snap_item_base'
4
+
5
+ class NetObj
6
+ class PlayerInfo < SnapItemBase
7
+ attr_accessor :player_flags, :score, :latency
8
+
9
+ def initialize(hash_or_raw)
10
+ @field_names = %i[
11
+ player_flags
12
+ score
13
+ latency
14
+ ]
15
+ super
16
+ end
17
+
18
+ def self.match_type?(type)
19
+ type == NETOBJTYPE_PLAYERINFO
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../snap_item_base'
4
+
5
+ class NetObj
6
+ class PlayerInput < SnapItemBase
7
+ attr_accessor :direction, :target_x, :target_y,
8
+ :jump, :fire, :hook,
9
+ :player_flags, :wanted_weapon,
10
+ :next_weapon, :prev_weapon
11
+
12
+ def initialize(hash_or_raw)
13
+ @field_names = %i[
14
+ direction
15
+ target_x
16
+ target_y
17
+ jump
18
+ fire
19
+ hook
20
+ player_flags
21
+ wanted_weapon
22
+ next_weapon
23
+ prev_weapon
24
+ ]
25
+ super
26
+ end
27
+
28
+ def self.match_type?(type)
29
+ type == NETOBJTYPE_PLAYERINPUT
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../snap_item_base'
4
+
5
+ class NetObj
6
+ class Projectile < SnapItemBase
7
+ attr_accessor :x, :y, :vel_x, :vel_y, :type, :start_tick
8
+
9
+ def initialize(hash_or_raw)
10
+ @field_names = %i[
11
+ x
12
+ y
13
+ vel_x
14
+ vel_y
15
+ type
16
+ start_tick
17
+ ]
18
+ super
19
+ end
20
+
21
+ def self.match_type?(type)
22
+ type == NETOBJTYPE_PROJECTILE
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../snap_item_base'
4
+
5
+ class NetObj
6
+ class SpectatorInfo < SnapItemBase
7
+ attr_accessor :spec_mode, :spectator_id, :x, :y
8
+
9
+ def initialize(hash_or_raw)
10
+ @field_names = %i[
11
+ spec_mode
12
+ spectator_id
13
+ x
14
+ y
15
+ ]
16
+ super
17
+ end
18
+
19
+ def self.match_type?(type)
20
+ type == NETOBJTYPE_SPECTATORINFO
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../packer'
4
+
5
+ class NetObj
6
+ class TuneParams
7
+ attr_accessor :tune_params
8
+ attr_reader :notes, :name, :id
9
+
10
+ def initialize(hash_or_raw)
11
+ @field_names = [:tune_params]
12
+ @fields = @field_names.map do |_|
13
+ 0
14
+ end
15
+ @size = @fields.count
16
+ @name = self.class.name
17
+ @notes = [] # hexdump annotation notes
18
+ if hash_or_raw.instance_of?(Hash)
19
+ init_hash(hash_or_raw)
20
+ elsif hash_or_raw.instance_of?(Unpacker)
21
+ init_unpacker(hash_or_raw)
22
+ else
23
+ init_raw(hash_or_raw)
24
+ end
25
+ end
26
+
27
+ def validate
28
+ @fields.select(&:nil?).empty?
29
+ end
30
+
31
+ def init_unpacker(u)
32
+ @id = u.get_int
33
+ p = u.parsed.last
34
+ @notes.push([:cyan, p[:pos], p[:len], "id=#{@id}"])
35
+ i = 0
36
+ @fields.map! do |_|
37
+ # TODO: as of right now it can get nil values here
38
+ # the fix would be "u.get_int || 0"
39
+ # but fixing it would probably make it harder
40
+ # to debug invalid data
41
+ #
42
+ # but do rethink this in a later point please :)
43
+ # for now call .validate() everywhere
44
+ val = u.get_int
45
+
46
+ p = u.parsed.last
47
+ color = (i % 2).zero? ? :yellow : :pink
48
+ desc = @field_names[i]
49
+ @notes.push([color, p[:pos], p[:len], "#{desc}=#{val}"])
50
+ i += 1
51
+
52
+ val
53
+ end
54
+ end
55
+
56
+ def init_raw(data)
57
+ u = Unpacker.new(data)
58
+ init_unpacker(u)
59
+ end
60
+
61
+ def init_hash(attr)
62
+ @fields_names.each do |name|
63
+ instance_variable_set("@#{name}", attr[name] || 0)
64
+ end
65
+ end
66
+
67
+ def to_h
68
+ hash = {}
69
+ hash[:id] = @id
70
+ @field_names.each_with_index do |name, index|
71
+ hash[name] = @fields[index]
72
+ end
73
+ hash
74
+ end
75
+
76
+ # basically to_network
77
+ # int array the server sends to the client
78
+ def to_a
79
+ arr = []
80
+ @fields.each do |value|
81
+ arr += Packer.pack_int(value)
82
+ end
83
+ arr
84
+ end
85
+
86
+ def to_s
87
+ to_h
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+ require_relative 'snap_item_base'
5
+
6
+ class SnapEventBase < SnapItemBase
7
+ attr_reader :x, :y
8
+
9
+ def initialize(hash_or_raw)
10
+ @field_names.prepend(:x)
11
+ @field_names.prepend(:y)
12
+ super
13
+ end
14
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ class SnapItemBase
6
+ attr_reader :notes, :name, :id
7
+
8
+ def initialize(hash_or_raw)
9
+ @fields = @field_names.map do |_|
10
+ 0
11
+ end
12
+ @size = @fields.count
13
+ @name = self.class.name
14
+ @notes = [] # hexdump annotation notes
15
+ if hash_or_raw.instance_of?(Hash)
16
+ init_hash(hash_or_raw)
17
+ elsif hash_or_raw.instance_of?(Unpacker)
18
+ init_unpacker(hash_or_raw)
19
+ else
20
+ init_raw(hash_or_raw)
21
+ end
22
+ end
23
+
24
+ def validate
25
+ @fields.select(&:nil?).empty?
26
+ end
27
+
28
+ def init_unpacker(u)
29
+ @id = u.get_int
30
+ p = u.parsed.last
31
+ @notes.push([:cyan, p[:pos], p[:len], "id=#{@id}"])
32
+ i = 0
33
+ @fields.map! do |_|
34
+ # TODO: as of right now it can get nil values here
35
+ # the fix would be "u.get_int || 0"
36
+ # but fixing it would probably make it harder
37
+ # to debug invalid data
38
+ #
39
+ # but do rethink this in a later point please :)
40
+ # for now call .validate() everywhere
41
+ val = u.get_int
42
+
43
+ p = u.parsed.last
44
+ color = (i % 2).zero? ? :yellow : :pink
45
+ desc = @field_names[i]
46
+ @notes.push([color, p[:pos], p[:len], "#{desc}=#{val}"])
47
+ i += 1
48
+
49
+ val
50
+ end
51
+ end
52
+
53
+ def init_raw(data)
54
+ u = Unpacker.new(data)
55
+ init_unpacker(u)
56
+ end
57
+
58
+ def init_hash(attr)
59
+ @fields_names.each do |name|
60
+ instance_variable_set("@#{name}", attr[name] || 0)
61
+ end
62
+ end
63
+
64
+ def to_h
65
+ hash = {}
66
+ hash[:id] = @id
67
+ @field_names.each_with_index do |name, index|
68
+ hash[name] = @fields[index]
69
+ end
70
+ hash
71
+ end
72
+
73
+ # basically to_network
74
+ # int array the server sends to the client
75
+ def to_a
76
+ arr = []
77
+ @fields.each do |value|
78
+ arr += Packer.pack_int(value)
79
+ end
80
+ arr
81
+ end
82
+
83
+ def to_s
84
+ to_h
85
+ end
86
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'items/game_data'
4
+ require_relative 'items/character'
5
+ require_relative 'items/projectile'
6
+ require_relative 'items/pickup'
7
+ require_relative 'items/flag'
8
+ require_relative 'items/game_data_team'
9
+ require_relative 'items/game_data_flag'
10
+ require_relative 'items/player_input'
11
+ require_relative 'items/laser'
12
+ require_relative 'items/player_info'
13
+ require_relative 'items/spectator_info'
14
+ require_relative 'items/client_info'
15
+ require_relative 'events/sound_world'
16
+ require_relative 'events/explosion'
17
+ require_relative 'events/spawn'
18
+ require_relative 'events/damage'
19
+ require_relative 'events/death'
20
+ require_relative 'events/hammer_hit'
21
+ require_relative '../packer'
22
+
23
+ class Snapshot
24
+ attr_accessor :game_tick, :items
25
+
26
+ def initialize(items)
27
+ @game_tick = 0
28
+ @items = items
29
+ end
30
+ end
31
+
32
+ class DDNetSnapItem
33
+ attr_accessor :notes, :name
34
+
35
+ @@registered_types = []
36
+
37
+ # TODO: rename to register uuid?!
38
+ def initialize(u, id)
39
+ @name = 'ddnet_uuid'
40
+ @notes = []
41
+ len = u.get_int
42
+ p = u.parsed.last
43
+ @notes.push([:green, p[:pos], p[:len], "len=#{len}"])
44
+ (0...len).each do |i|
45
+ val = u.get_int
46
+ p = u.parsed.last
47
+ col = (i % 2).zero? ? :bg_pink : :bg_yellow
48
+ @notes.push([col, p[:pos], p[:len], "val=#{val}"])
49
+ end
50
+ @@registered_types.push(id)
51
+ end
52
+
53
+ # parses registered ddnet items
54
+ def self.parse(u, _item_type)
55
+ id = u.get_int
56
+ p = u.parsed.last
57
+ notes = []
58
+ notes.push([:cyan, p[:pos], p[:len], "id=#{id}"])
59
+ len = u.get_int
60
+ p = u.parsed.last
61
+ notes.push([:green, p[:pos], p[:len], "len=#{len}"])
62
+ (0...len).each do |i|
63
+ val = u.get_int
64
+ p = u.parsed.last
65
+ col = (i % 2).zero? ? :bg_pink : :bg_yellow
66
+ notes.push([col, p[:pos], p[:len], "val=#{val}"])
67
+ end
68
+ notes
69
+ end
70
+
71
+ def self.valid_type?(type)
72
+ @@registered_types.include?(type)
73
+ end
74
+ end
75
+
76
+ class SnapshotUnpacker
77
+ def initialize(client)
78
+ @client = client
79
+ end
80
+
81
+ def unpack_ddnet_item(u, notes)
82
+ id = u.get_int
83
+ p = u.parsed.last
84
+ notes.push([:cyan, p[:pos], p[:len], "id=#{id}"])
85
+ return nil if id < 0x4000 # ddnet offset uuid type
86
+
87
+ DDNetSnapItem.new(u, id)
88
+ end
89
+
90
+ ##
91
+ # Given a NetChunk this method
92
+ # dissects the snapshot header
93
+ # and its payload (snap items)
94
+ #
95
+ # @return [Snapshot]
96
+ def snap_single(chunk)
97
+ u = Unpacker.new(chunk.data)
98
+ msg_id = u.get_int
99
+ msg_id >>= 1
100
+
101
+ num_parts = 1
102
+ part = 0
103
+ game_tick = u.get_int
104
+ delta_tick = u.get_int
105
+ part_size = 0
106
+ crc = 0
107
+ # complete_size = 0
108
+ # data = nil
109
+
110
+ # TODO: state check
111
+
112
+ if msg_id == NETMSG_SNAP
113
+ num_parts = u.get_int
114
+ part = u.get_int
115
+ end
116
+
117
+ unless msg_id == NETMSG_SNAPEMPTY
118
+ crc = u.get_int
119
+ part_size = u.get_int
120
+ end
121
+
122
+ snap_name = 'SNAP_INVALID'
123
+ case msg_id
124
+ when NETMSG_SNAP then snap_name = 'NETMSG_SNAP'
125
+ when NETMSG_SNAPSINGLE then snap_name = 'NETMSG_SNAPSINGLE'
126
+ when NETMSG_SNAPEMPTY then snap_name = 'NETMSG_SNAPEMPTY'
127
+ end
128
+
129
+ return unless msg_id == NETMSG_SNAPSINGLE
130
+
131
+ if @verbose
132
+ puts ">>> snap #{snap_name} (#{msg_id})"
133
+ puts " id=#{msg_id} game_tick=#{game_tick} delta_tick=#{delta_tick}"
134
+ puts " num_parts=#{num_parts} part=#{part} crc=#{crc} part_size=#{part_size}"
135
+ puts "\n header:"
136
+ end
137
+
138
+ header = []
139
+ notes = []
140
+ u.parsed.each_with_index do |parsed, index|
141
+ color = (index % 2).zero? ? :green : :pink
142
+ txt = "#{parsed[:type]} #{parsed[:value]}"
143
+ txt += " >> 1 = #{parsed[:value] >> 1}" if header.empty?
144
+ notes.push([color, parsed[:pos], parsed[:len], txt])
145
+ header += parsed[:raw]
146
+ end
147
+
148
+ if @verbose
149
+ hexdump_lines(header.pack('C*'), 1, notes, legend: :inline).each do |hex|
150
+ puts " #{hex}"
151
+ end
152
+ puts "\n payload:"
153
+ end
154
+
155
+ notes = []
156
+
157
+ data = u.get_raw
158
+
159
+ # tw decompresses all bytes at once
160
+ # and pads it with zeros to get the 4 byte aligned ints
161
+ # we just grab one int at a time cuz yolo
162
+ u = Unpacker.new(data)
163
+ num_removed_items = u.get_int
164
+ p = u.parsed.last
165
+ notes.push([:red, p[:pos], p[:len], "removed_items=#{num_removed_items}"])
166
+
167
+ num_item_deltas = u.get_int
168
+ p = u.parsed.last
169
+ notes.push([:blue, p[:pos], p[:len], "num_item_deltas=#{num_item_deltas}"])
170
+
171
+ zero = u.get_int
172
+ p = u.parsed.last
173
+ notes.push([:cyan, p[:pos], p[:len], "_zero=#{zero}"])
174
+
175
+ (0...num_removed_items).each do |i|
176
+ deleted = u.get_int
177
+ notes.push([:red, p[:pos], p[:len], "del[#{i}]=#{deleted}"])
178
+ end
179
+
180
+ invalid = false
181
+ item_type = u.get_int
182
+ id_parsed = u.parsed.last
183
+ snap_items = []
184
+ while item_type
185
+ obj = nil
186
+ if NetObj::PlayerInput.match_type?(item_type)
187
+ obj = NetObj::PlayerInput.new(u)
188
+ elsif NetObj::Projectile.match_type?(item_type)
189
+ obj = NetObj::Projectile.new(u)
190
+ elsif NetObj::Laser.match_type?(item_type)
191
+ obj = NetObj::Laser.new(u)
192
+ elsif NetObj::Pickup.match_type?(item_type)
193
+ obj = NetObj::Pickup.new(u)
194
+ elsif NetObj::Flag.match_type?(item_type)
195
+ obj = NetObj::Flag.new(u)
196
+ elsif NetObj::GameData.match_type?(item_type)
197
+ obj = NetObj::GameData.new(u)
198
+ elsif NetObj::GameDataTeam.match_type?(item_type)
199
+ obj = NetObj::GameDataTeam.new(u)
200
+ elsif NetObj::GameDataFlag.match_type?(item_type)
201
+ obj = NetObj::GameDataFlag.new(u)
202
+ elsif NetObj::Character.match_type?(item_type)
203
+ obj = NetObj::Character.new(u)
204
+ elsif NetObj::PlayerInfo.match_type?(item_type)
205
+ obj = NetObj::PlayerInfo.new(u)
206
+ elsif NetObj::SpectatorInfo.match_type?(item_type)
207
+ obj = NetObj::SpectatorInfo.new(u)
208
+ elsif NetObj::ClientInfo.match_type?(item_type)
209
+ obj = NetObj::ClientInfo.new(u)
210
+ elsif NetEvent::Explosion.match_type?(item_type)
211
+ obj = NetEvent::Explosion.new(u)
212
+ elsif NetEvent::SoundWorld.match_type?(item_type)
213
+ obj = NetEvent::SoundWorld.new(u)
214
+ elsif NetEvent::Spawn.match_type?(item_type)
215
+ obj = NetEvent::Spawn.new(u)
216
+ elsif NetEvent::Damage.match_type?(item_type)
217
+ obj = NetEvent::Damage.new(u)
218
+ elsif NetEvent::Death.match_type?(item_type)
219
+ obj = NetEvent::Death.new(u)
220
+ elsif NetEvent::HammerHit.match_type?(item_type)
221
+ obj = NetEvent::HammerHit.new(u)
222
+ elsif @verbose
223
+ puts "no match #{item_type}"
224
+ end
225
+ obj = unpack_ddnet_item(u, notes) if !obj && item_type.zero?
226
+ if obj
227
+ snap_items.push(obj)
228
+ notes += obj.notes
229
+ notes.push([
230
+ :green,
231
+ id_parsed[:pos],
232
+ id_parsed[:len],
233
+ "type=#{item_type} #{obj.name}"
234
+ ])
235
+ elsif DDNetSnapItem.valid_type?(item_type)
236
+ notes.push([
237
+ :green,
238
+ id_parsed[:pos],
239
+ id_parsed[:len],
240
+ "type=#{item_type} ddnet_ex_reg"
241
+ ])
242
+ notes += DDNetSnapItem.parse(u, item_type)
243
+ elsif item_type < 50 # TODO: i made up this magic number xd
244
+ # figure out what a sane
245
+ # limit for the type is
246
+ # something that is a bit
247
+ # future proof
248
+ # and also strict enough
249
+ # to alert when something
250
+ # goes wrong
251
+ # item with non pre-agreed size
252
+ # first int of the payload is the size of the payload
253
+ id = u.get_int
254
+ p = u.parsed.last
255
+ notes.push([:cyan, p[:pos], p[:len], "id=#{id}"])
256
+ len = u.get_int
257
+ p = u.parsed.last
258
+ notes.push([:green, p[:pos], p[:len], "len=#{len}"])
259
+ (0...len).each do |i|
260
+ val = u.get_int
261
+ p = u.parsed.last
262
+ col = (i % 2).zero? ? :bg_pink : :bg_yellow
263
+ notes.push([col, p[:pos], p[:len], "val=#{val}"])
264
+ end
265
+ else
266
+ invalid = true
267
+ notes.push([
268
+ :bg_red,
269
+ id_parsed[:pos],
270
+ id_parsed[:len],
271
+ "invalid_type=#{item_type}"
272
+ ])
273
+ end
274
+ item_type = u.get_int
275
+ id_parsed = u.parsed.last
276
+ end
277
+
278
+ if @verbose
279
+ hexdump_lines(data.pack('C*'), 1, notes, legend: :inline).each do |hex|
280
+ puts " #{hex}"
281
+ end
282
+ end
283
+
284
+ if invalid
285
+ # make sure if we did not print the hex already
286
+ # to print it now as error message
287
+ unless @verbose
288
+ hexdump_lines(data.pack('C*'), 1, notes, legend: :inline).each do |hex|
289
+ puts " #{hex}"
290
+ end
291
+ end
292
+ puts 'Error: got invalid snap item'
293
+ @client.disconnect
294
+ exit 1
295
+ end
296
+
297
+ snapshot = Snapshot.new(snap_items)
298
+ snapshot.game_tick = game_tick
299
+ snapshot
300
+ end
301
+ end