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,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'models/map'
4
+ require_relative 'models/chat_message'
5
+ require_relative 'messages/game_info'
6
+ require_relative 'messages/server_info'
7
+ require_relative 'messages/server_settings'
8
+ require_relative 'messages/start_info'
9
+ require_relative 'messages/cl_say'
10
+ require_relative 'messages/cl_emoticon'
11
+
12
+ class GameServer
13
+ attr_accessor :pred_game_tick, :ack_game_tick, :map
14
+
15
+ def initialize(server)
16
+ @server = server
17
+ @ack_game_tick = -1
18
+ @pred_game_tick = 0
19
+ @map = Map.new(
20
+ name: 'dm1',
21
+ crc: '98a0a4c50c', # decimal 64548818
22
+ size: 6793,
23
+ sha256: '491af17a510214506270904f147a4c30ae0a85b91bb854395bef8c397fc078c3'
24
+ )
25
+ end
26
+
27
+ def on_emoticon(chunk, _packet)
28
+ message = ClEmoticon.new(chunk.data[1..])
29
+ p message
30
+ end
31
+
32
+ def on_info(chunk, packet)
33
+ u = Unpacker.new(chunk.data[1..])
34
+ net_version = u.get_string
35
+ password = u.get_string
36
+ client_version = u.get_int
37
+ puts "vers=#{net_version} vers=#{client_version} pass=#{password}"
38
+
39
+ # TODO: check version and password
40
+
41
+ @server.send_map(packet.client)
42
+ end
43
+
44
+ def on_ready(_chunk, packet)
45
+ # vanilla server sends 3 chunks here usually
46
+ # - motd
47
+ # - server settings
48
+ # - ready
49
+ #
50
+ @server.send_server_settings(packet.client, ServerSettings.new.to_a)
51
+ @server.send_ready(packet.client)
52
+ end
53
+
54
+ def on_start_info(chunk, packet)
55
+ # vanilla server sends 3 chunks here usually
56
+ # - vote clear options
57
+ # - tune params
58
+ # - ready to enter
59
+ #
60
+ # We only send ready to enter for now
61
+ info = StartInfo.new(chunk.data[1..])
62
+ packet.client.player.set_start_info(info)
63
+ info_str = info.to_s
64
+ puts "got start info: #{info_str}" if @verbose
65
+ @server.send_ready_to_enter(packet.client)
66
+ end
67
+
68
+ def on_say(chunk, packet)
69
+ say = ClSay.new(chunk.data[1..])
70
+ author = packet.client.player
71
+ msg = ChatMesage.new(say.to_h.merge(client_id: author.id, author:))
72
+ puts msg.to_s
73
+ end
74
+
75
+ def on_enter_game(_chunk, packet)
76
+ # vanilla server responds to enter game with two packets
77
+ # first:
78
+ # - server info
79
+ # second:
80
+ # - game info
81
+ # - client info
82
+ # - snap single
83
+ packet.client.in_game = true
84
+ @server.send_server_info(packet.client, ServerInfo.new.to_a)
85
+ @server.send_game_info(packet.client, GameInfo.new.to_a)
86
+
87
+ puts "'#{packet.client.player.name}' joined the game"
88
+ end
89
+
90
+ def on_rcon_cmd(chunk, _packet)
91
+ u = Unpacker.new(chunk.data[1..])
92
+ cmd = u.get_string
93
+ puts "got rcon_cmd=#{cmd}"
94
+ end
95
+
96
+ def on_input(chunk, packet)
97
+ # vanilla server responds to input with 2 chunks
98
+ # - input_timing
99
+ # - snap (empty)
100
+
101
+ # we do nothing for now
102
+ # TODO: do something
103
+ end
104
+
105
+ def on_client_drop(client, reason = nil)
106
+ reason = reason.nil? ? '' : " (#{reason})"
107
+ puts "'#{client.player.name}' left the game#{reason}"
108
+ end
109
+
110
+ def on_tick
111
+ now = Time.now
112
+ timeout_ids = []
113
+ @server.clients.each do |id, client|
114
+ diff = now - client.last_recv_time
115
+ timeout_ids.push(id) if diff > 10
116
+ end
117
+
118
+ timeout_ids.each do |id|
119
+ @server.drop_client(@server.clients[id], 'Timeout')
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # ClEmoticon
7
+ #
8
+ # Client -> Server
9
+ class ClEmoticon
10
+ attr_accessor :emoticon, :name
11
+
12
+ def initialize(hash_or_raw)
13
+ names = [
14
+ 'oop!', # 0
15
+ 'alert', # 1
16
+ 'heart', # 2
17
+ 'tear', # 3
18
+ '...', # 4
19
+ 'music', # 5
20
+ 'sorry', # 6
21
+ 'ghost', # 7
22
+ 'annoyed', # 8
23
+ 'angry', # 9
24
+ 'devil', # 10
25
+ 'swearing', # 11
26
+ 'zzZ', # 12
27
+ 'WTF', # 13
28
+ 'happy', # 14
29
+ '???' # 15
30
+ ]
31
+ if hash_or_raw.instance_of?(Hash)
32
+ init_hash(hash_or_raw)
33
+ else
34
+ init_raw(hash_or_raw)
35
+ end
36
+ @name = names[@emoticon]
37
+ end
38
+
39
+ def init_raw(data)
40
+ u = Unpacker.new(data)
41
+ @emoticon = u.get_int
42
+ end
43
+
44
+ def init_hash(attr)
45
+ @emoticon = attr[:emoticon] || 0
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ emoticon: @emoticon
51
+ }
52
+ end
53
+
54
+ # basically to_network
55
+ # int array the Client sends to the Server
56
+ def to_a
57
+ Packer.pack_int(@emoticon)
58
+ end
59
+
60
+ def to_s
61
+ to_h
62
+ end
63
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # ClSay
7
+ #
8
+ # Client -> Server
9
+ class ClSay
10
+ attr_accessor :mode, :target_id, :message
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
+ @mode = u.get_int
23
+ @target_id = u.get_int
24
+ @message = u.get_string
25
+ end
26
+
27
+ def init_hash(attr)
28
+ @mode = attr[:mode] || 0
29
+ @target_id = attr[:target_id] || 0
30
+ @message = attr[:message] || 0
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ mode: @mode,
36
+ target_id: @target_id,
37
+ message: @message
38
+ }
39
+ end
40
+
41
+ # basically to_network
42
+ # int array the client sends to the server
43
+ def to_a
44
+ Packer.pack_int(@mode) +
45
+ Packer.pack_int(@target_id) +
46
+ Packer.pack_str(@message)
47
+ end
48
+
49
+ def to_s
50
+ to_h
51
+ end
52
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ # TODO: use this on client side too
6
+
7
+ ##
8
+ # ClientInfo
9
+ #
10
+ # Server -> Client
11
+ class ClientInfo
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
+ @local = u.get_int
24
+ @team = u.get_int
25
+ @name = u.get_string
26
+ @clan = u.get_string
27
+ @country = u.get_int
28
+ @body = u.get_string
29
+ @marking = u.get_string
30
+ @decoration = u.get_string
31
+ @hands = u.get_string
32
+ @feet = u.get_string
33
+ @eyes = u.get_string
34
+ @custom_color_body = u.get_int
35
+ @custom_color_marking = u.get_int
36
+ @custom_color_decoration = u.get_int
37
+ @custom_color_hands = u.get_int
38
+ @custom_color_feet = u.get_int
39
+ @custom_color_eyes = u.get_int
40
+ @color_body = u.get_int
41
+ @color_marking = u.get_int
42
+ @color_decoration = u.get_int
43
+ @color_hands = u.get_int
44
+ @color_feet = u.get_int
45
+ @color_eyes = u.get_int
46
+ @silent = u.get_int
47
+ end
48
+
49
+ def init_hash(attr)
50
+ @client_id = attr[:client_id] || -1
51
+ @local = attr[:local] || 0
52
+ @team = attr[:team] || 0
53
+ @name = attr[:name] || 'ruby gamer'
54
+ @clan = attr[:clan] || ''
55
+ @country = attr[:country] || -1
56
+ @body = attr[:body] || 'spiky'
57
+ @marking = attr[:marking] || 'duodonny'
58
+ @decoration = attr[:decoration] || ''
59
+ @hands = attr[:hands] || 'standard'
60
+ @feet = attr[:feet] || 'standard'
61
+ @eyes = attr[:eyes] || 'standard'
62
+ @custom_color_body = attr[:custom_color_body] || 0
63
+ @custom_color_marking = attr[:custom_color_marking] || 0
64
+ @custom_color_decoration = attr[:custom_color_decoration] || 0
65
+ @custom_color_hands = attr[:custom_color_hands] || 0
66
+ @custom_color_feet = attr[:custom_color_feet] || 0
67
+ @custom_color_eyes = attr[:custom_color_eyes] || 0
68
+ @color_body = attr[:color_body] || 0
69
+ @color_marking = attr[:color_marking] || 0
70
+ @color_decoration = attr[:color_decoration] || 0
71
+ @color_hands = attr[:color_hands] || 0
72
+ @color_feet = attr[:color_feet] || 0
73
+ @color_eyes = attr[:color_eyes] || 0
74
+ @silent = attr[:silent] || 0
75
+ end
76
+
77
+ # TODO: do we need this?
78
+ def to_h
79
+ {
80
+ client_id: @client_id,
81
+ local: @local,
82
+ team: @team,
83
+ name: @name,
84
+ clan: @clan,
85
+ country: @country,
86
+ body: @body,
87
+ marking: @marking,
88
+ decoration: @decoration,
89
+ hands: @hands,
90
+ feet: @feet,
91
+ eyes: @eyes,
92
+ custom_color_body: @custom_color_body,
93
+ custom_color_marking: @custom_color_marking,
94
+ custom_color_decoration: @custom_color_decoration,
95
+ custom_color_hands: @custom_color_hands,
96
+ custom_color_feet: @custom_color_feet,
97
+ custom_color_eyes: @custom_color_eyes,
98
+ color_body: @color_body,
99
+ color_marking: @color_marking,
100
+ color_decoration: @color_decoration,
101
+ color_hands: @color_hands,
102
+ color_feet: @color_feet,
103
+ color_eyes: @color_eyes,
104
+ silent: @silent
105
+ }
106
+ end
107
+
108
+ # basically to_network
109
+ # int array the server sends to the client
110
+ def to_a
111
+ Packer.pack_int(@client_id) +
112
+ Packer.pack_int(@local) +
113
+ Packer.pack_int(@team) +
114
+ Packer.pack_str(@name) +
115
+ Packer.pack_str(@clan) +
116
+ Packer.pack_int(@country) +
117
+ Packer.pack_str(@body) +
118
+ Packer.pack_str(@marking) +
119
+ Packer.pack_str(@decoration) +
120
+ Packer.pack_str(@hands) +
121
+ Packer.pack_str(@feet) +
122
+ Packer.pack_str(@eyes) +
123
+ Packer.pack_int(@custom_color_body) +
124
+ Packer.pack_int(@custom_color_marking) +
125
+ Packer.pack_int(@custom_color_decoration) +
126
+ Packer.pack_int(@custom_color_hands) +
127
+ Packer.pack_int(@custom_color_feet) +
128
+ Packer.pack_int(@custom_color_eyes) +
129
+ Packer.pack_int(@color_body) +
130
+ Packer.pack_int(@color_marking) +
131
+ Packer.pack_int(@color_decoration) +
132
+ Packer.pack_int(@color_hands) +
133
+ Packer.pack_int(@color_feet) +
134
+ Packer.pack_int(@color_eyes) +
135
+ Packer.pack_int(@silent)
136
+ end
137
+
138
+ def to_s
139
+ to_h
140
+ end
141
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ class GameInfo
6
+ attr_accessor :game_flags, :score_limit, :time_limit, :match_num, :match_current
7
+
8
+ def initialize(attr = {})
9
+ @game_flags = attr[:game_flags] || 0
10
+ @score_limit = attr[:score_limit] || 0
11
+ @time_limit = attr[:time_limit] || 0
12
+ @match_num = attr[:match_num] || 0
13
+ @match_current = attr[:match_current] || 0
14
+ end
15
+
16
+ # basically to_network
17
+ # int array the server sends to the client
18
+ def to_a
19
+ Packer.pack_int(@game_flags) +
20
+ Packer.pack_int(@score_limit) +
21
+ Packer.pack_int(@time_limit) +
22
+ Packer.pack_int(@match_num) +
23
+ Packer.pack_int(@match_current)
24
+ end
25
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # InputTiming
7
+ #
8
+ # Server -> Client
9
+ class InputTiming
10
+ attr_accessor :intended_tick, :time_left
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
+ @intended_tick = u.get_int
23
+ @time_left = u.get_int
24
+ end
25
+
26
+ def init_hash(attr)
27
+ @intended_tick = attr[:intended_tick] || 0
28
+ @time_left = attr[:time_left] || 0
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ intended_tick: @intended_tick,
34
+ time_left: @time_left
35
+ }
36
+ end
37
+
38
+ # basically to_network
39
+ # int array the Server sends to the Client
40
+ def to_a
41
+ Packer.pack_int(@intended_tick) +
42
+ Packer.pack_int(@time_left)
43
+ end
44
+
45
+ def to_s
46
+ to_h
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # MaplistEntryAdd
7
+ #
8
+ # Server -> Client
9
+ class MaplistEntryAdd
10
+ attr_accessor :name
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
+ @name = u.get_string(SANITIZE_CC)
23
+ end
24
+
25
+ def init_hash(attr)
26
+ @name = attr[:name] || 'TODO: fill default'
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ name: @name
32
+ }
33
+ end
34
+
35
+ # basically to_network
36
+ # int array the Server sends to the Client
37
+ def to_a
38
+ Packer.pack_str(@name)
39
+ end
40
+
41
+ def to_s
42
+ to_h
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # MaplistEntryRem
7
+ #
8
+ # Server -> Client
9
+ class MaplistEntryRem
10
+ attr_accessor :name
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
+ @name = u.get_string(SANITIZE_CC)
23
+ end
24
+
25
+ def init_hash(attr)
26
+ @name = attr[:name] || 'TODO: fill default'
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ name: @name
32
+ }
33
+ end
34
+
35
+ # basically to_network
36
+ # int array the Server sends to the Client
37
+ def to_a
38
+ Packer.pack_str(@name)
39
+ end
40
+
41
+ def to_s
42
+ to_h
43
+ end
44
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # RconCmdAdd
7
+ #
8
+ # Server -> Client
9
+ class RconCmdAdd
10
+ attr_accessor :name, :help, :params
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
+ @name = u.get_string(SANITIZE_CC)
23
+ @help = u.get_string(SANITIZE_CC)
24
+ @params = u.get_string(SANITIZE_CC)
25
+ end
26
+
27
+ def init_hash(attr)
28
+ @name = attr[:name] || ''
29
+ @help = attr[:help] || ''
30
+ @params = attr[:params] || ''
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ name: @name,
36
+ help: @help,
37
+ params: @params
38
+ }
39
+ end
40
+
41
+ # basically to_network
42
+ # int array the Server sends to the Client
43
+ def to_a
44
+ Packer.pack_str(@name) +
45
+ Packer.pack_str(@help) +
46
+ Packer.pack_str(@params)
47
+ end
48
+
49
+ def to_s
50
+ to_h
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # RconCmdRem
7
+ #
8
+ # Server -> Client
9
+ class RconCmdRem
10
+ attr_accessor :name
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
+ @name = u.get_string(SANITIZE_CC)
23
+ end
24
+
25
+ def init_hash(attr)
26
+ @name = attr[:name] || ''
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ name: @name
32
+ }
33
+ end
34
+
35
+ # basically to_network
36
+ # int array the Server sends to the Client
37
+ def to_a
38
+ Packer.pack_str(@name)
39
+ end
40
+
41
+ def to_s
42
+ to_h
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packer'
4
+
5
+ ##
6
+ # RconLine
7
+ #
8
+ # Server -> Client
9
+ class RconLine
10
+ attr_accessor :command
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
+ @command = u.get_string
23
+ end
24
+
25
+ def init_hash(attr)
26
+ @command = attr[:command] || 'hello world'
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ command: @command
32
+ }
33
+ end
34
+
35
+ # basically to_network
36
+ # int array the Server sends to the Client
37
+ def to_a
38
+ Packer.pack_str(@command)
39
+ end
40
+
41
+ def to_s
42
+ to_h
43
+ end
44
+ end