q3_servers 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f82fbeb9b327a11aedb51d1ad6f3dd98d20ff7863d5d1c108e70680cfc4f3ffb
4
+ data.tar.gz: 45fa2991f473f0714823d3574a1965dc56b249fc2a7a63c1a753e15bd752bcd9
5
+ SHA512:
6
+ metadata.gz: 96f27480478d3f3443cd5451cd2876f036949037d2cc22731dc8c39734ec561d6f005430489fb133bdd286153b76289bf7d317f6d7330464ab40a0ded0d0d16b
7
+ data.tar.gz: '01168eac86e3c2bfa307c5c323b204b154e1bab4242387660504300553f9598ee19c732c9d67365c7cda751d74dca55d447c5451c2655fba6ba18b3e1eab3fd0'
data/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.2
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in q3_servers.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ q3_servers (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ minitest (5.14.4)
11
+ parallel (1.20.1)
12
+ parser (3.0.1.1)
13
+ ast (~> 2.4.1)
14
+ rainbow (3.0.0)
15
+ rake (12.3.3)
16
+ regexp_parser (2.1.1)
17
+ rexml (3.2.5)
18
+ rubocop (1.17.0)
19
+ parallel (~> 1.10)
20
+ parser (>= 3.0.0.0)
21
+ rainbow (>= 2.2.2, < 4.0)
22
+ regexp_parser (>= 1.8, < 3.0)
23
+ rexml
24
+ rubocop-ast (>= 1.7.0, < 2.0)
25
+ ruby-progressbar (~> 1.7)
26
+ unicode-display_width (>= 1.4.0, < 3.0)
27
+ rubocop-ast (1.7.0)
28
+ parser (>= 3.0.1.1)
29
+ ruby-progressbar (1.11.0)
30
+ unicode-display_width (2.0.0)
31
+
32
+ PLATFORMS
33
+ x86_64-linux
34
+
35
+ DEPENDENCIES
36
+ minitest (~> 5.0)
37
+ q3_servers!
38
+ rake (~> 12.0)
39
+ rubocop
40
+
41
+ BUNDLED WITH
42
+ 2.2.17
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 jpgarritano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Q3Servers
2
+
3
+ This gem will help you browse through Quake3-style master servers (Protocol 68) (tested only with Urban Terror)
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'q3_servers'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install q3_servers
21
+
22
+
23
+ ## Usage
24
+ ```ruby
25
+ q3_urt_list = Q3Servers::List.new
26
+
27
+ #### Some defaults settings you can change
28
+ # q3_urt_list.master_address
29
+ # q3_urt_list.only_favorites = false
30
+ # q3_urt_list.timeout = 1
31
+ # q3_urt_list.master_cache_secs = 600
32
+ # q3_urt_list.info_cache_secs = 60
33
+ # q3_urt_list.debug = true
34
+ ####
35
+ # q3_urt_list.add_favorite('XXX.XXX.XXX.XXX', '27960')
36
+ # q3_urt_list.add_favorite('YYY.YYY.YYY.YYY', '27961')
37
+
38
+ servers = q3_urt_list.fetch_servers({})
39
+ ```
40
+ ##### Threads
41
+ You can use use_threads keyword, for multi-threaded fetching (one for each server found!)
42
+ ```ruby
43
+ q3_urt_list.fetch_servers({}, use_threads: true)
44
+ ```
45
+
46
+ ##### Filter
47
+ Filter by keys from "info" attribute in Q3Servers::Server
48
+ example:
49
+ ```ruby
50
+ q3_urt_list.fetch_servers({hostname: "Arg"})
51
+ ```
52
+
53
+ #### And now read information from each server
54
+ ##### Examples:
55
+ ```ruby
56
+ # print info from a server
57
+ servers.first.tap do |server|
58
+ puts server.info['hostname'] ## server.name_c_sanitized exclude symbols and colors from hostname field
59
+ puts server.gametype # print gametype mapped in Q3Servers::Server::GT Hash
60
+ puts server.info.keys # for more information you can access
61
+ end
62
+
63
+ # get only servers with clients
64
+ server_with_clients = servers.select { |server| server.info['clients'].to_i > 0 }
65
+
66
+ # show players
67
+ server_with_clients.each do |server|
68
+ server.info['sv_status']['players'].each do |player|
69
+ puts "#{player.name} has ping: #{player.ping} with #{player.kills} kills"
70
+ end
71
+ end
72
+ ```
73
+
74
+ #### More info
75
+
76
+ You can set `only_favorites = true` for fetch only servers added with `add_favorite(host, port)` method
77
+
78
+ #### TODO
79
+ - Tests
80
+ - Pool of threads
81
+
82
+ #### This gem was built only for fun :)
83
+
84
+ ##### Used in `UrbanterrorBot` on Telegram
85
+
86
+ ## Development
87
+
88
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
89
+
90
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
91
+
92
+ ## Contributing
93
+
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jpgarritano/q3_servers.
95
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "q3_servers"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/q3_servers.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "q3_servers/version"
2
+
3
+ module Q3Servers
4
+ class Error < StandardError; end
5
+
6
+ require 'q3_servers/player'
7
+ require 'q3_servers/server_connection'
8
+ require 'q3_servers/server'
9
+ require 'q3_servers/massive_helper'
10
+ require 'q3_servers/list'
11
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'date'
5
+
6
+ module Q3Servers
7
+ class List
8
+ PROTOCOL = 68
9
+ MAX_LENGTH = 65_536
10
+
11
+ attr_reader :master_socket, :threads, :servers, :favorites
12
+ attr_accessor :cache, :timeout, :master_updated_at, :debug, :master_cache_secs, :info_cache_secs, :only_favorites,
13
+ :master_address, :master_port
14
+ alias only_favorites? only_favorites
15
+ alias cache? cache
16
+
17
+ def initialize
18
+ @master_socket = UDPSocket.new
19
+ @servers = []
20
+ @cache = true
21
+ @timeout = 1
22
+ @master_updated_at = nil
23
+ @debug = false
24
+ @master_cache_secs = 600
25
+ @info_cache_secs = 60
26
+ @favorites = []
27
+ @master_port = 27_900
28
+ @master_address = 'master.urbanterror.info' # 51.75.224.242
29
+ @threads = []
30
+ end
31
+
32
+ def fetch_servers(filter = {}, use_threads: false)
33
+ servers_list_from_master if master_server_outdated? && !only_favorites?
34
+ fetch_info_servers(filter, use_threads)
35
+ end
36
+
37
+ def request_server_info(server, filter, use_threads)
38
+ print_debug("INFO Server: Connecting to Server id => #{server.unique_index}")
39
+ if use_threads
40
+ thread_server_info(server, filter)
41
+ else
42
+ server.request_info
43
+ end
44
+ end
45
+
46
+ def cached_info?(server)
47
+ cache? && server_info?(server) && !server_info_outdated?(server)
48
+ end
49
+
50
+ def cached_status?(server)
51
+ cache? && server_status?(server) && !server_status_outdated?(server)
52
+ end
53
+
54
+ def add_server(server)
55
+ servers << server
56
+ end
57
+
58
+ def add_favorite(ip, port)
59
+ favorites << new_favorite = Server.new(ip, port, {})
60
+ add_server(new_favorite)
61
+ end
62
+
63
+ def favorite?(server)
64
+ favorites.any? { |fav| server.unique_index == fav.unique_index }
65
+ end
66
+
67
+ private
68
+
69
+ def thread_server_info(server, filter)
70
+ @threads << Thread.new { get_server_info_status_filter(server, filter) }
71
+ rescue ThreadError => e
72
+ p "Can't create thread! => #{e.inspect}"
73
+ end
74
+
75
+ def get_server_info_status_filter(server, filter)
76
+ server.get_info_connect
77
+ server.request_and_get_status if server.filter_info(filter)
78
+ server.info
79
+ end
80
+
81
+ def server_info?(server)
82
+ !server.info.empty?
83
+ end
84
+
85
+ def server_status?(server)
86
+ server_info?(server) && !server.info_status.empty?
87
+ end
88
+
89
+ def fill_list_favorites
90
+ favorites.each do |fav|
91
+ add_server(fav)
92
+ end
93
+ end
94
+
95
+ def fill_list_master(response)
96
+ response = response.unpack('CCCCA18Ca*')
97
+ response = response[6]
98
+ until response.empty?
99
+ response = response.unpack('NnCa*')
100
+ new_sv = Server.new(to_ip(response[0]), response[1], {}) # response[2] is \\ (EOT)
101
+ add_server(new_sv)
102
+ response = response[3] # prepare for next step
103
+ end
104
+ servers
105
+ end
106
+
107
+ def fetch_info_servers(filter, use_threads)
108
+ servers.each do |server|
109
+ cache_or_request_server_info(server, filter, use_threads)
110
+ end
111
+ # wait for "info servers"
112
+ if use_threads
113
+ @threads.each(&:join) # wait for threads
114
+ else
115
+ massive_read_info_status(servers, filter)
116
+ end
117
+ destroy_socket_servers
118
+ servers.select { |server| server.filter_info(filter) }
119
+ end
120
+
121
+ def cache_or_request_server_info(server, filter, use_threads)
122
+ if cached_info?(server)
123
+ print_debug("INFO Server cached => #{server.unique_index}")
124
+ else
125
+ request_server_info(server, filter, use_threads)
126
+ end
127
+ end
128
+
129
+ def servers_list_from_master
130
+ servers.clear # #clean servers list
131
+ print_debug("Connecting to master server: cache => #{cache} | timeout => #{timeout}")
132
+ connect_request_master_socket
133
+ response_all_servers = []
134
+ loop do
135
+ response_all_servers << master_socket.recvfrom(MAX_LENGTH).first
136
+ size = response_all_servers.last.size
137
+ print_debug(size)
138
+ break if size < 1394
139
+ end
140
+ response_all_servers.each { |r_server| fill_list_master(r_server) } unless only_favorites?
141
+ self.master_updated_at = DateTime.now
142
+ end
143
+
144
+ def print_debug(info)
145
+ p info if @debug
146
+ end
147
+
148
+ def to_ip(decimal)
149
+ [decimal].pack('N').unpack('CCCC').join('.')
150
+ end
151
+
152
+ def master_server_outdated?
153
+ !master_updated_at or (DateTime.now > (master_updated_at + Rational(master_cache_secs, 86_400)))
154
+ end
155
+
156
+ def server_info_outdated?(server)
157
+ (!server_info?(server) or (DateTime.now > (server.updated_at + Rational(info_cache_secs, 86_400))))
158
+ end
159
+
160
+ def server_status_outdated?(server)
161
+ (!server_status?(server) or (DateTime.now > (server.status_updated_at + Rational(info_cache_secs, 86_400))))
162
+ end
163
+
164
+ def massive_read_info_status(servers, filter)
165
+ massive_helper = MassiveHelper.new(servers.select(&:request_status?), self)
166
+ alive_servers = massive_helper.read_info_servers(2, timeout) do |server|
167
+ server.read_info unless cached_info?(server)
168
+ end
169
+
170
+ filtered_servers = alive_servers.select { |server| server.filter_info(filter) }
171
+ filtered_servers.each(&:request_status)
172
+
173
+ massive_helper.read_status_servers(filtered_servers, 2, timeout) do |server|
174
+ server.read_status unless cached_status?(server)
175
+ end
176
+ end
177
+
178
+ def destroy_socket_servers
179
+ servers.each(&:destroy_socket)
180
+ end
181
+
182
+ def connect_request_master_socket
183
+ master_socket.connect(master_address, master_port)
184
+ master_socket.send("#{prepend_oob_data}getservers #{PROTOCOL} full empty", 0)
185
+ end
186
+
187
+ def prepend_oob_data
188
+ "\xFF" * 4
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Q3Servers
4
+ class MassiveHelper
5
+ attr_accessor :sockets, :servers
6
+
7
+ def initialize(servers, context)
8
+ @context = context
9
+ @servers = servers.each_with_object({}) do |server, hsh|
10
+ hsh[server.unique_index] = server
11
+ end
12
+ end
13
+
14
+ def read_info_servers(max_retries, timeout, &block)
15
+ puts '======== Read Info servers ========'
16
+ read_info(servers.map { |_unique_index, server| server.socket }, max_retries, timeout, &Proc.new)
17
+ end
18
+
19
+ def read_status_servers(servers, max_retries, timeout)
20
+ puts '======== Read Status servers ========'
21
+ read_info(servers.map(&:socket), max_retries, timeout, &Proc.new)
22
+ end
23
+
24
+ def read_info(sockets, max_retries, timeout, &block)
25
+ servers_with_info = []
26
+ sockets_completed = 0
27
+ retries = 0
28
+ sockets.size.times do |_i|
29
+ break if (sockets_completed >= sockets.size) || (retries >= max_retries)
30
+
31
+ ready_sockets = IO.select(sockets, nil, nil, timeout)
32
+ if ready_sockets && (ready_sockets = ready_sockets[0])
33
+ retries = 0
34
+ ready_sockets.each do |socket|
35
+ sockets_completed += 1
36
+ server = servers[calculate_index(socket)]
37
+ block.call(server)
38
+ servers_with_info << server
39
+ end
40
+ else
41
+ retries += 1
42
+ puts "Retry n #{retries}"
43
+ end
44
+ end
45
+ servers_with_info
46
+ end
47
+
48
+ def calculate_index(socket)
49
+ # to determine which socket answered
50
+ addr = socket.peeraddr(false)
51
+ ServerConnection.new(addr.last, addr[1]).unique_index
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Q3Servers
4
+ Player = Struct.new(:name, :kills, :ping)
5
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Q3Servers
4
+ class Server
5
+ attr_accessor :connection, :info, :status
6
+
7
+ STATUS = %i[request request_status response response_status destroyed].freeze
8
+ GT = { '0' => 'FFA', '4' => 'TS', '7' => 'CTF', '9' => 'JMP' }.freeze
9
+
10
+ def initialize(ip, port, info)
11
+ @connection = ServerConnection.new(ip, port)
12
+ @info = info || {}
13
+ end
14
+
15
+ def name_c_sanitized
16
+ info['hostname'].gsub(/(\^[0-9]{1})/, '') if info.key?('hostname')
17
+ end
18
+
19
+ def gametype
20
+ GT.fetch(info['gametype'], '')
21
+ end
22
+
23
+ def filter_info(filter)
24
+ return false if info.empty?
25
+ return true if filter.empty?
26
+
27
+ info['hostname'] = name_c_sanitized if info.key?('hostname')
28
+ f = filter.select { |k, v| info.key?(k.to_s) and (info[k.to_s].downcase =~ /#{v.to_s.downcase}/) }
29
+ !f.empty?
30
+ end
31
+
32
+ def get_info_connect
33
+ request_info
34
+ read_info
35
+ end
36
+
37
+ def request_info
38
+ self.status = :request
39
+ puts "Requesting info to #{connection}"
40
+ connection.request_info_server
41
+ end
42
+
43
+ def request_status
44
+ self.status = :request_status
45
+ puts "Requesting status to #{connection}"
46
+ connection.request_status_server
47
+ end
48
+
49
+ def read_info
50
+ self.status = :response
51
+ self.info = connection.read_info_server
52
+ touch!
53
+ info
54
+ end
55
+
56
+ def read_status
57
+ self.status = :response_status
58
+ info['sv_status'] = connection.read_status_server
59
+ status_touch!
60
+ info['sv_status']
61
+ end
62
+
63
+ def request_and_get_status
64
+ info['sv_status'] = connection.request_and_get_server_status # step 2 more info from server
65
+ status_touch!
66
+ info['sv_status']
67
+ end
68
+
69
+ def socket
70
+ connection.socket
71
+ end
72
+
73
+ def unique_index
74
+ connection.unique_index
75
+ end
76
+
77
+ def destroy_socket
78
+ status = :destroyed
79
+ connection.socket.close
80
+ end
81
+
82
+ def updated_at
83
+ info['updated_at']
84
+ end
85
+
86
+ def status_updated_at
87
+ info.dig('sv_status', 'updated_at')
88
+ end
89
+
90
+ def info_status
91
+ info['sv_status'] || {}
92
+ end
93
+
94
+ STATUS.each do |status|
95
+ define_method("#{status}_status?") { self.status == status }
96
+ end
97
+
98
+ private
99
+
100
+ def touch!
101
+ info['updated_at'] = DateTime.now
102
+ end
103
+
104
+ def status_touch!
105
+ info['sv_status']['updated_at'] = DateTime.now
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ module Q3Servers
5
+ class ServerConnection
6
+ attr_accessor :ip, :port, :socket, :timeout, :url_maps
7
+
8
+ MAX_LENGTH = 65_536
9
+
10
+ def initialize(ip, port, timeout = 1)
11
+ @ip = ip
12
+ @port = port
13
+ @timeout = timeout
14
+ @url_maps = 'https://www.urbanterror.info/files/static/images/levels/wide/'
15
+ end
16
+
17
+ def to_s
18
+ "#{ip}:#{port}"
19
+ end
20
+
21
+ def unique_index
22
+ Digest::MD5.hexdigest("#{ip}:#{port}")
23
+ end
24
+
25
+ def server_info_connect
26
+ request_info_server
27
+ read_info_server
28
+ end
29
+
30
+ def connect
31
+ @socket&.close
32
+ @socket = UDPSocket.new
33
+ @socket.connect(ip, port)
34
+ @socket
35
+ end
36
+
37
+ def request_info_server
38
+ connect
39
+ send_data("#{prepend_oob_data}getinfo xxx") # request step 1
40
+ end
41
+
42
+ # INFO 1/2
43
+ def read_info_server
44
+ sv_info = read_data
45
+ sv_info ? parse_sv_info(sv_info) : {} # parse step 1 info
46
+ end
47
+
48
+ # INFO 2/2
49
+ def request_and_get_server_status
50
+ # sv_status = send_and_read(prepend_oob_data + 'getstatus')
51
+ # sv_status ? parse_sv_status(sv_status) : {} # parse step 2
52
+ request_status_server
53
+ read_status_server
54
+ end
55
+
56
+ def request_status_server
57
+ send_data("#{prepend_oob_data}getstatus") # request step 2
58
+ end
59
+
60
+ # INFO 2/2
61
+ def read_status_server
62
+ sv_status = read_data
63
+ sv_status ? parse_sv_status(sv_status) : {} # parse step 2
64
+ end
65
+
66
+ private
67
+
68
+ def send_and_read(data)
69
+ send_data(data)
70
+ read_data
71
+ end
72
+
73
+ def send_data(data)
74
+ socket.send(data, 0)
75
+ end
76
+
77
+ def read_data
78
+ IO.select([socket], nil, nil, timeout)
79
+ response_info = socket.recvfrom_nonblock(MAX_LENGTH)
80
+ rescue IO::WaitReadable, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
81
+ nil
82
+ else
83
+ response_info[0]
84
+ end
85
+
86
+ def parse_sv_info(sv_response)
87
+ response = sv_response.unpack('CCCCA12CCa*') # infoResponse (len 12) string
88
+ response = response[7].split('\\')
89
+ result = Hash[*response]
90
+ result['map_image_url'] = map_url(result['mapname'])
91
+ result
92
+ end
93
+
94
+ def parse_sv_status(sv_response)
95
+ response = sv_response.unpack('CCCCA14CCa*') # statusResponse (len 14) string
96
+ response = response[7].split("\n")
97
+ status = Hash[*response[0].split('\\')]
98
+ players = parse_players(response[1..-1])
99
+ status['players'] = players
100
+ status
101
+ end
102
+
103
+ def parse_players(array_str)
104
+ array_str.map do |player|
105
+ kills, ping, player_name = player.split
106
+ Player.new(player_name[1..-2], kills, ping)
107
+ end
108
+ end
109
+
110
+ def prepend_oob_data
111
+ "\xFF" * 4
112
+ end
113
+
114
+ def map_url(map_name)
115
+ "#{url_maps}#{map_name}.jpg"
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,3 @@
1
+ module Q3Servers
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'lib/q3_servers/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "q3_servers"
5
+ spec.version = Q3Servers::VERSION
6
+ spec.authors = ["Juan Pablo Garritano"]
7
+ spec.email = ["tuny22@gmail.com"]
8
+
9
+ spec.summary = "Browse servers from Quake3/Urban-Terror game"
10
+ spec.homepage = "https://github.com/jpgarritano/q3_servers"
11
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
12
+
13
+ spec.metadata["homepage_uri"] = spec.homepage
14
+ spec.metadata["source_code_uri"] = "https://github.com/jpgarritano/q3_servers"
15
+ spec.metadata["changelog_uri"] = "https://github.com/jpgarritano/q3_servers"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.require_paths = ["lib"]
23
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: q3_servers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Juan Pablo Garritano
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-06-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - tuny22@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".travis.yml"
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - bin/console
28
+ - bin/setup
29
+ - lib/q3_servers.rb
30
+ - lib/q3_servers/list.rb
31
+ - lib/q3_servers/massive_helper.rb
32
+ - lib/q3_servers/player.rb
33
+ - lib/q3_servers/server.rb
34
+ - lib/q3_servers/server_connection.rb
35
+ - lib/q3_servers/version.rb
36
+ - q3_servers.gemspec
37
+ homepage: https://github.com/jpgarritano/q3_servers
38
+ licenses: []
39
+ metadata:
40
+ homepage_uri: https://github.com/jpgarritano/q3_servers
41
+ source_code_uri: https://github.com/jpgarritano/q3_servers
42
+ changelog_uri: https://github.com/jpgarritano/q3_servers
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.3.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.1.4
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Browse servers from Quake3/Urban-Terror game
62
+ test_files: []