q3_servers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []