steam-condenser 0.12.0 → 0.13.0

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 (42) hide show
  1. data/README.md +3 -1
  2. data/lib/exceptions/web_api_exception.rb +49 -0
  3. data/lib/steam/community/app_news.rb +69 -0
  4. data/lib/steam/community/game_achievement.rb +29 -0
  5. data/lib/steam/community/steam_id.rb +1 -1
  6. data/lib/steam/community/tf2/tf2_golden_wrench.rb +43 -0
  7. data/lib/steam/community/tf2/tf2_inventory.rb +51 -0
  8. data/lib/steam/community/tf2/tf2_item.rb +106 -0
  9. data/lib/steam/community/tf2/tf2_stats.rb +11 -0
  10. data/lib/steam/community/web_api.rb +96 -0
  11. data/lib/steam/packets/a2s_player_packet.rb +1 -1
  12. data/lib/steam/packets/a2s_rules_packet.rb +3 -3
  13. data/lib/steam/packets/c2m_checkmd5_packet.rb +24 -0
  14. data/lib/steam/packets/m2c_isvalidmd5_packet.rb +25 -0
  15. data/lib/steam/packets/m2s_requestrestart_packet.rb +24 -0
  16. data/lib/steam/packets/rcon/rcon_packet.rb +1 -1
  17. data/lib/steam/packets/rcon/rcon_packet_factory.rb +0 -1
  18. data/lib/steam/packets/rcon/rcon_terminator.rb +20 -0
  19. data/lib/steam/packets/s2a_info2_packet.rb +3 -3
  20. data/lib/steam/packets/s2a_logstring_packet.rb +24 -0
  21. data/lib/steam/packets/s2a_player_packet.rb +1 -1
  22. data/lib/steam/packets/s2a_rules_packet.rb +2 -2
  23. data/lib/steam/packets/s2c_challenge_packet.rb +2 -2
  24. data/lib/steam/packets/s2m_heartbeat2_packet.rb +57 -0
  25. data/lib/steam/packets/steam_packet.rb +6 -3
  26. data/lib/steam/packets/steam_packet_factory.rb +12 -9
  27. data/lib/steam/servers/game_server.rb +57 -16
  28. data/lib/steam/servers/goldsrc_server.rb +7 -12
  29. data/lib/steam/servers/master_server.rb +63 -15
  30. data/lib/steam/servers/server.rb +66 -0
  31. data/lib/steam/servers/source_server.rb +14 -20
  32. data/lib/steam/sockets/goldsrc_socket.rb +13 -9
  33. data/lib/steam/sockets/rcon_socket.rb +35 -24
  34. data/lib/steam/sockets/steam_socket.rb +27 -21
  35. data/lib/steam/steam_player.rb +30 -15
  36. data/lib/steam-condenser/community.rb +17 -16
  37. data/lib/steam-condenser/version.rb +2 -2
  38. metadata +39 -15
  39. data/lib/datagram_channel.rb +0 -67
  40. data/lib/socket_channel.rb +0 -55
  41. data/lib/steam/packets/a2a_ack_packet.rb +0 -24
  42. data/lib/steam/packets/a2a_ping_packet.rb +0 -19
@@ -1,9 +1,12 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under the
2
2
  # terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2008-2010, Sebastian Staudt
4
+ # Copyright (c) 2008-2011, Sebastian Staudt
5
+
6
+ require 'ipaddr'
7
+ require 'socket'
8
+ require 'timeout'
5
9
 
6
- require 'socket_channel'
7
10
  require 'exceptions/rcon_ban_exception'
8
11
  require 'steam/packets/rcon/rcon_packet'
9
12
  require 'steam/packets/rcon/rcon_packet_factory'
@@ -13,37 +16,45 @@ class RCONSocket
13
16
 
14
17
  include SteamSocket
15
18
 
16
- def initialize(ip_address, port_number)
17
- super ip_address, port_number
18
- @channel = SocketChannel.open
19
+ def initialize(ip, port)
20
+ ip = IPSocket.getaddress(ip) unless ip.is_a? IPAddr
21
+
22
+ @ip = ip
23
+ @port = port
24
+ @socket = nil
25
+ end
26
+
27
+ # Closes the underlying socket if it exists
28
+ def close
29
+ super unless @socket.nil?
30
+ end
31
+
32
+ def connect
33
+ begin
34
+ timeout(@@timeout / 1000.0) { @socket = TCPSocket.new @ip, @port }
35
+ rescue Timeout::Error
36
+ raise TimeoutException
37
+ end
19
38
  end
20
39
 
21
40
  def send(data_packet)
22
- @channel.connect @remote_socket unless @channel.connected?
41
+ connect if @socket.nil? || @socket.closed?
23
42
 
24
- @buffer = StringIO.new data_packet.bytes
25
- @channel.write @buffer
43
+ super
26
44
  end
27
45
 
28
46
  def reply
29
- raise RCONBanException if receive_packet(1440) == 0
47
+ raise RCONBanException if receive_packet(4) == 0
30
48
 
31
- packet_data = @buffer.get
32
49
  @buffer.rewind
33
- packet_size = @buffer.long + 4
34
-
35
- if packet_size > 1440
36
- remaining_bytes = packet_size - @buffer.size
37
- begin
38
- if remaining_bytes < 1440
39
- receive_packet remaining_bytes
40
- else
41
- receive_packet 1440
42
- end
43
- packet_data << @buffer.get
44
- remaining_bytes -= @buffer.size
45
- end while remaining_bytes > 0
46
- end
50
+ remaining_bytes = @buffer.long
51
+
52
+ packet_data = ''
53
+ begin
54
+ received_bytes = receive_packet remaining_bytes
55
+ remaining_bytes -= received_bytes
56
+ packet_data << @buffer.get
57
+ end while remaining_bytes > 0
47
58
 
48
59
  RCONPacketFactory.packet_from_data(packet_data)
49
60
  end
@@ -1,17 +1,27 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under the
2
2
  # terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2008-2010, Sebastian Staudt
4
+ # Copyright (c) 2008-2011, Sebastian Staudt
5
5
 
6
- require 'datagram_channel'
7
6
  require 'ipaddr'
7
+ require 'socket'
8
+
8
9
  require 'stringio_additions'
9
10
  require 'exceptions/timeout_exception'
10
11
 
11
- # Defines common methods for sockets used to connect to game and master
12
- # servers.
12
+ # This module defines common methods for sockets used to connect to game and
13
+ # master servers.
14
+ #
15
+ # @author Sebastian Staudt
16
+ # @see GoldSrcSocket
17
+ # @see MasterServerSocket
18
+ # @see RCONSocket
19
+ # @see SourceSocket
20
+ # @since 0.5.0
13
21
  module SteamSocket
14
22
 
23
+ # The default timeout
24
+ # @since 0.11.0
15
25
  @@timeout = 1000
16
26
 
17
27
  # Sets the timeout for socket operations. This usually only affects timeouts,
@@ -27,21 +37,20 @@ module SteamSocket
27
37
  @@timeout = timeout
28
38
  end
29
39
 
30
- def initialize(*args)
31
- @channel = DatagramChannel.open
32
- @channel.connect(*args)
33
- @channel.configure_blocking false
34
-
35
- @remote_socket = Socket.getaddrinfo args[0].to_s, args[1]
40
+ def initialize(ip_address, port = 27015)
41
+ @socket = UDPSocket.new
42
+ @socket.connect ip_address, port
36
43
  end
37
44
 
38
- # Abstract +reply+ method
39
- def reply
40
- raise NotImplementedError
45
+ # Closes the underlying socket
46
+ def close
47
+ @socket.close
41
48
  end
42
49
 
43
50
  def receive_packet(buffer_length = 0)
44
- raise TimeoutException if select([@channel.socket], nil, nil, @@timeout / 1000).nil?
51
+ if select([@socket], nil, nil, @@timeout / 1000.0).nil?
52
+ raise TimeoutException
53
+ end
45
54
 
46
55
  if buffer_length == 0
47
56
  @buffer.rewind
@@ -49,7 +58,9 @@ module SteamSocket
49
58
  @buffer = StringIO.allocate buffer_length
50
59
  end
51
60
 
52
- bytes_read = @channel.read @buffer
61
+ data = @socket.recv @buffer.remaining
62
+ bytes_read = @buffer.write data
63
+ @buffer.truncate bytes_read
53
64
  @buffer.rewind
54
65
 
55
66
  bytes_read
@@ -58,12 +69,7 @@ module SteamSocket
58
69
  def send(data_packet)
59
70
  puts "Sending data packet of type \"#{data_packet.class.to_s}\"." if $DEBUG
60
71
 
61
- @buffer = StringIO.new data_packet.to_s
62
- @channel.write @buffer
63
- end
64
-
65
- def finalize
66
- @channel.close
72
+ @socket.send data_packet.to_s, 0
67
73
  end
68
74
 
69
75
  end
@@ -1,7 +1,7 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under the
2
2
  # terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2008-2009, Sebastian Staudt
4
+ # Copyright (c) 2008-2011, Sebastian Staudt
5
5
 
6
6
  require 'exceptions/steam_condenser_exception'
7
7
 
@@ -9,7 +9,7 @@ require 'exceptions/steam_condenser_exception'
9
9
  class SteamPlayer
10
10
 
11
11
  attr_reader :client_port, :connect_time, :id, :ip_address, :name, :loss,
12
- :ping, :real_id, :score, :state, :steam_id
12
+ :ping, :rate, :real_id, :score, :state, :steam_id
13
13
 
14
14
  # Creates a new SteamPlayer object based on the given information
15
15
  def initialize(id, name, score, connect_time)
@@ -22,25 +22,40 @@ class SteamPlayer
22
22
 
23
23
  # Extends this player object with additional information acquired using RCON
24
24
  # status
25
- def add_info(real_id, name, steam_id, *player_data)
26
- @extended = true
27
-
28
- unless name == @name
25
+ def add_info(player_data)
26
+ unless player_data[:name] == @name
29
27
  raise SteamCondenserException.new('Information to add belongs to a different player.')
30
28
  end
31
29
 
32
- @real_id = real_id
33
- @steam_id = steam_id
34
- if steam_id == 'BOT'
35
- @state = player_data[0]
36
- else
37
- @ip_address, @client_port = player_data[4].split(':')
38
- @loss = player_data[2]
39
- @ping = player_data[1]
40
- @state = player_data[3]
30
+ @extended = true
31
+
32
+ @real_id = player_data[:userid].to_i
33
+ @steam_id = player_data[:uniqueid]
34
+ @state = player_data[:state] if player_data.key? :state
35
+
36
+ if !bot?
37
+ @loss = player_data[:loss].to_i
38
+ @ping = player_data[:ping].to_i
39
+
40
+ if player_data.key? :adr
41
+ @ip_address, @client_port = player_data[:adr].split(':')
42
+ @client_port = @client_port.to_i
43
+ end
44
+
45
+ @rate = player_data[:rate].to_i if player_data.key? :rate
41
46
  end
42
47
  end
43
48
 
49
+ # Returns whether this player is a bot
50
+ def bot?
51
+ @steam_id == 'BOT'
52
+ end
53
+
54
+ # Returns whether this player object has extended information
55
+ def extended?
56
+ @extended
57
+ end
58
+
44
59
  # Returns a String representation of this player
45
60
  def to_s
46
61
  if @extended
@@ -1,16 +1,17 @@
1
- # This code is free software; you can redistribute it and/or modify it under the
2
- # terms of the new BSD License.
3
- #
4
- # Copyright (c) 2010, Sebastian Staudt
5
-
6
- module SteamCondenser
7
-
8
- require 'steam-condenser/version'
9
-
10
- module Community
11
-
12
- require 'steam/community/steam_id'
13
-
14
- end
15
-
16
- end
1
+ # This code is free software; you can redistribute it and/or modify it under the
2
+ # terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2010, Sebastian Staudt
5
+
6
+ module SteamCondenser
7
+
8
+ require 'steam-condenser/version'
9
+
10
+ module Community
11
+
12
+ require 'steam/community/steam_id'
13
+ require 'steam/community/web_api'
14
+
15
+ end
16
+
17
+ end
@@ -1,10 +1,10 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under the
2
2
  # terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2010, Sebastian Staudt
4
+ # Copyright (c) 2010-2011, Sebastian Staudt
5
5
 
6
6
  module SteamCondenser
7
7
 
8
- VERSION = '0.12.0'
8
+ VERSION = '0.13.0'
9
9
 
10
10
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: steam-condenser
3
3
  version: !ruby/object:Gem::Version
4
- hash: 47
5
- prerelease: false
4
+ hash: 43
5
+ prerelease:
6
6
  segments:
7
7
  - 0
8
- - 12
8
+ - 13
9
9
  - 0
10
- version: 0.12.0
10
+ version: 0.13.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Sebastian Staudt
@@ -15,8 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-29 00:00:00 +01:00
19
- default_executable:
18
+ date: 2011-04-12 00:00:00 Z
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
21
  name: bzip2-ruby
@@ -35,9 +34,25 @@ dependencies:
35
34
  type: :runtime
36
35
  version_requirements: *id001
37
36
  - !ruby/object:Gem::Dependency
38
- name: ore-tasks
37
+ name: json
39
38
  prerelease: false
40
39
  requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 1
45
+ segments:
46
+ - 1
47
+ - 5
48
+ - 1
49
+ version: 1.5.1
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: ore-tasks
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
41
56
  none: false
42
57
  requirements:
43
58
  - - ~>
@@ -49,9 +64,10 @@ dependencies:
49
64
  - 0
50
65
  version: 0.3.0
51
66
  type: :development
52
- version_requirements: *id002
67
+ version_requirements: *id003
53
68
  description: A multi-language library for querying the Steam Community, Source, GoldSrc servers and Steam master servers
54
- email: koraktor@gmail.com
69
+ email:
70
+ - koraktor@gmail.com
55
71
  executables: []
56
72
 
57
73
  extensions: []
@@ -60,16 +76,16 @@ extra_rdoc_files:
60
76
  - LICENSE
61
77
  - README.md
62
78
  files:
63
- - lib/datagram_channel.rb
64
79
  - lib/exceptions/packet_format_exception.rb
65
80
  - lib/exceptions/rcon_ban_exception.rb
66
81
  - lib/exceptions/rcon_no_auth_exception.rb
67
82
  - lib/exceptions/steam_condenser_exception.rb
68
83
  - lib/exceptions/timeout_exception.rb
69
- - lib/socket_channel.rb
84
+ - lib/exceptions/web_api_exception.rb
70
85
  - lib/steam/community/alien_swarm/alien_swarm_mission.rb
71
86
  - lib/steam/community/alien_swarm/alien_swarm_stats.rb
72
87
  - lib/steam/community/alien_swarm/alien_swarm_weapon.rb
88
+ - lib/steam/community/app_news.rb
73
89
  - lib/steam/community/cacheable.rb
74
90
  - lib/steam/community/css/css_map.rb
75
91
  - lib/steam/community/css/css_stats.rb
@@ -96,18 +112,23 @@ files:
96
112
  - lib/steam/community/tf2/tf2_class.rb
97
113
  - lib/steam/community/tf2/tf2_class_factory.rb
98
114
  - lib/steam/community/tf2/tf2_engineer.rb
115
+ - lib/steam/community/tf2/tf2_golden_wrench.rb
116
+ - lib/steam/community/tf2/tf2_inventory.rb
117
+ - lib/steam/community/tf2/tf2_item.rb
99
118
  - lib/steam/community/tf2/tf2_medic.rb
100
119
  - lib/steam/community/tf2/tf2_sniper.rb
101
120
  - lib/steam/community/tf2/tf2_spy.rb
102
121
  - lib/steam/community/tf2/tf2_stats.rb
103
- - lib/steam/packets/a2a_ack_packet.rb
104
- - lib/steam/packets/a2a_ping_packet.rb
122
+ - lib/steam/community/web_api.rb
105
123
  - lib/steam/packets/a2m_get_servers_batch2_packet.rb
106
124
  - lib/steam/packets/a2s_info_packet.rb
107
125
  - lib/steam/packets/a2s_player_packet.rb
108
126
  - lib/steam/packets/a2s_rules_packet.rb
109
127
  - lib/steam/packets/a2s_serverquery_getchallenge_packet.rb
128
+ - lib/steam/packets/c2m_checkmd5_packet.rb
110
129
  - lib/steam/packets/m2a_server_batch_packet.rb
130
+ - lib/steam/packets/m2c_isvalidmd5_packet.rb
131
+ - lib/steam/packets/m2s_requestrestart_packet.rb
111
132
  - lib/steam/packets/rcon/rcon_auth_request.rb
112
133
  - lib/steam/packets/rcon/rcon_auth_response.rb
113
134
  - lib/steam/packets/rcon/rcon_exec_request.rb
@@ -116,18 +137,22 @@ files:
116
137
  - lib/steam/packets/rcon/rcon_goldsrc_response.rb
117
138
  - lib/steam/packets/rcon/rcon_packet.rb
118
139
  - lib/steam/packets/rcon/rcon_packet_factory.rb
140
+ - lib/steam/packets/rcon/rcon_terminator.rb
119
141
  - lib/steam/packets/request_with_challenge.rb
120
142
  - lib/steam/packets/s2a_info2_packet.rb
121
143
  - lib/steam/packets/s2a_info_base_packet.rb
122
144
  - lib/steam/packets/s2a_info_detailed_packet.rb
145
+ - lib/steam/packets/s2a_logstring_packet.rb
123
146
  - lib/steam/packets/s2a_player_packet.rb
124
147
  - lib/steam/packets/s2a_rules_packet.rb
125
148
  - lib/steam/packets/s2c_challenge_packet.rb
149
+ - lib/steam/packets/s2m_heartbeat2_packet.rb
126
150
  - lib/steam/packets/steam_packet.rb
127
151
  - lib/steam/packets/steam_packet_factory.rb
128
152
  - lib/steam/servers/game_server.rb
129
153
  - lib/steam/servers/goldsrc_server.rb
130
154
  - lib/steam/servers/master_server.rb
155
+ - lib/steam/servers/server.rb
131
156
  - lib/steam/servers/source_server.rb
132
157
  - lib/steam/sockets/goldsrc_socket.rb
133
158
  - lib/steam/sockets/master_server_socket.rb
@@ -150,7 +175,6 @@ files:
150
175
  - test/steam/communtiy/steam_id_tests.rb
151
176
  - test/steam_community_tests.rb
152
177
  - test/stringio_additions_tests.rb
153
- has_rdoc: true
154
178
  homepage: http://koraktor.github.com/steam-condenser
155
179
  licenses:
156
180
  - BSD
@@ -180,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
204
  requirements: []
181
205
 
182
206
  rubyforge_project: steam-condenser
183
- rubygems_version: 1.3.7
207
+ rubygems_version: 1.7.2
184
208
  signing_key:
185
209
  specification_version: 3
186
210
  summary: Steam Condenser - A Steam query library
@@ -1,67 +0,0 @@
1
- # This code is free software; you can redistribute it and/or modify it under the
2
- # terms of the new BSD License.
3
- #
4
- # Copyright (c) 2008-2010, Sebastian Staudt
5
-
6
- require 'ipaddr'
7
- require 'socket'
8
-
9
- require 'stringio_additions'
10
-
11
- class DatagramChannel
12
-
13
- attr_reader :socket
14
-
15
- def self.open
16
- DatagramChannel.new
17
- end
18
-
19
- def close
20
- @socket.close
21
- end
22
-
23
- def connect(*args)
24
- if args[0].is_a? IPAddr
25
- ip_address = args[0].to_s
26
- elsif args[0].is_a? String
27
- ip_address = IPSocket.getaddress args[0]
28
- else
29
- raise ArgumentError
30
- end
31
-
32
- if args[1].is_a? Numeric
33
- port_number = args[1]
34
- else
35
- port_number = args[1].to_i
36
- end
37
-
38
- @socket.connect ip_address, port_number
39
-
40
- self
41
- end
42
-
43
- def configure_blocking(do_block)
44
- end
45
-
46
- def read(destination_io)
47
- raise ArgumentError unless destination_io.is_a? StringIO
48
-
49
- data = @socket.recv destination_io.remaining
50
- length = destination_io.write data
51
- destination_io.truncate length
52
- end
53
-
54
- def write(source_io)
55
- raise ArgumentError unless source_io.is_a? StringIO
56
-
57
- @socket.send(source_io.get, 0)
58
- end
59
-
60
- protected
61
-
62
- def initialize
63
- @socket = UDPSocket.new
64
- configure_blocking true
65
- end
66
-
67
- end
@@ -1,55 +0,0 @@
1
- # This code is free software; you can redistribute it and/or modify it under the
2
- # terms of the new BSD License.
3
- #
4
- # Copyright (c) 2008-2010, Sebastian Staudt
5
-
6
- require 'ipaddr'
7
- require 'socket'
8
- require 'timeout'
9
-
10
- require 'stringio_additions'
11
-
12
- class SocketChannel
13
-
14
- attr_reader :socket
15
-
16
- def self.open
17
- SocketChannel.new
18
- end
19
-
20
- def close
21
- @socket.close
22
- end
23
-
24
- def connect(*args)
25
- timeout(1) do
26
- @socket = TCPSocket.new args[0][0][3], args[0][0][1]
27
- @connected = true
28
- end
29
-
30
- self
31
- end
32
-
33
- def initialize
34
- @connected = false
35
- end
36
-
37
- def connected?
38
- @connected
39
- end
40
-
41
- def read(destination_buffer)
42
- raise ArgumentError unless destination_buffer.is_a? StringIO
43
-
44
- data = @socket.recv destination_buffer.remaining
45
- length = destination_buffer.write data
46
- destination_buffer.truncate length
47
- end
48
-
49
- def write(source_buffer)
50
- raise ArgumentError unless source_buffer.is_a? StringIO
51
-
52
- @socket.send(source_buffer.get, 0)
53
- end
54
-
55
- end
@@ -1,24 +0,0 @@
1
- # This code is free software; you can redistribute it and/or modify it under the
2
- # terms of the new BSD License.
3
- #
4
- # Copyright (c) 2008-2010, Sebastian Staudt
5
-
6
- require 'exceptions/packet_format_exception'
7
- require 'steam/packets/steam_packet'
8
-
9
- # The A2A_ACK_Packet class represents the response to a A2A_PING
10
- # request send to the server.
11
- class A2A_ACK_Packet
12
-
13
- include SteamPacket
14
-
15
- # Creates a A2A_ACK response object based on the data received.
16
- def initialize(data)
17
- if data != "\0" && data != "00000000000000\0"
18
- raise PacketFormatException.new('Wrong formatted A2A_ACK packet.')
19
- end
20
-
21
- super A2A_ACK_HEADER, data
22
- end
23
-
24
- end
@@ -1,19 +0,0 @@
1
- # This code is free software; you can redistribute it and/or modify it under the
2
- # terms of the new BSD License.
3
- #
4
- # Copyright (c) 2008-2010, Sebastian Staudt
5
-
6
- require 'steam/packets/steam_packet'
7
-
8
- # The A2A_PING_Packet class represents a A2A_PING request send to the
9
- # server.
10
- class A2A_PING_Packet
11
-
12
- include SteamPacket
13
-
14
- # Creates a new A2A_PING request object
15
- def initialize
16
- super SteamPacket::A2A_PING_HEADER
17
- end
18
-
19
- end