gamespy_query 0.1.5 → 0.2.0pre
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.
- data/.gitignore +5 -0
- data/README.md +7 -4
- data/Rakefile +14 -0
- data/bin/gamespy_query +8 -0
- data/gamespy_query.gemspec +3 -3
- data/lib/gamespy_query.rb +20 -5
- data/lib/gamespy_query/base.rb +45 -24
- data/lib/gamespy_query/master.rb +40 -13
- data/lib/gamespy_query/options.rb +140 -0
- data/lib/gamespy_query/parser.rb +29 -18
- data/lib/gamespy_query/socket.rb +81 -45
- data/lib/gamespy_query/socket_master.rb +49 -3
- data/lib/gamespy_query/version.rb +2 -1
- data/test/teststrap.rb +4 -0
- data/test/units/base_test.rb +43 -0
- data/test/units/master_test.rb +17 -0
- data/test/units/options_test.rb +27 -0
- data/test/units/parser_test.rb +47 -0
- data/test/units/socket_master_test.rb +17 -0
- data/test/units/socket_test.rb +73 -0
- metadata +51 -7
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
GamespyQuery
|
2
|
-
=============
|
3
|
-
|
4
|
-
This library provides access to GameSpy master server (through gslist utility) and to GameSpy enabled game servers directly through UDPSocket.
|
1
|
+
GamespyQuery
|
2
|
+
=============
|
3
|
+
|
4
|
+
This library provides access to GameSpy master server (through gslist utility) and to GameSpy enabled game servers directly through UDPSocket.
|
5
|
+
|
6
|
+
Requires the gslist utility for GamespyMaster operations: http://aluigi.org/papers.htm#gslist
|
7
|
+
|
data/Rakefile
CHANGED
@@ -1 +1,15 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
desc "Run all our tests"
|
7
|
+
task :test do
|
8
|
+
Rake::TestTask.new do |t|
|
9
|
+
t.libs << "test"
|
10
|
+
t.pattern = "test/**/*_test.rb"
|
11
|
+
t.verbose = false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :test
|
data/bin/gamespy_query
ADDED
data/gamespy_query.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
s.add_runtime_dependency "cri"
|
22
|
+
s.add_development_dependency "riot"
|
23
|
+
s.add_development_dependency "yard"
|
24
24
|
end
|
data/lib/gamespy_query.rb
CHANGED
@@ -1,11 +1,26 @@
|
|
1
1
|
require_relative "gamespy_query/version"
|
2
|
-
require_relative "gamespy_query/base"
|
3
|
-
require_relative "gamespy_query/socket"
|
4
|
-
require_relative "gamespy_query/socket_master"
|
5
|
-
require_relative "gamespy_query/master"
|
6
2
|
|
3
|
+
# GamespyQuery provides access to GameSpy master server (through gslist utility)
|
4
|
+
# and to GameSpy enabled game servers directly through UDPSocket.
|
7
5
|
module GamespyQuery
|
8
|
-
|
6
|
+
autoload :Base, "gamespy_query/base"
|
7
|
+
autoload :Funcs, "gamespy_query/base"
|
8
|
+
autoload :Tools, "gamespy_query/base"
|
9
|
+
|
10
|
+
autoload :Options, "gamespy_query/options"
|
11
|
+
|
12
|
+
autoload :Parser, "gamespy_query/parser"
|
13
|
+
autoload :Socket, "gamespy_query/socket"
|
14
|
+
autoload :MultiSocket, "gamespy_query/socket"
|
15
|
+
autoload :SocketMaster, "gamespy_query/socket_master"
|
16
|
+
autoload :Master, "gamespy_query/master"
|
17
|
+
|
18
|
+
module_function
|
19
|
+
|
20
|
+
# Retrieve full product version string
|
21
|
+
def product_version
|
22
|
+
"GamespyQuery version #{VERSION}"
|
23
|
+
end
|
9
24
|
end
|
10
25
|
|
11
26
|
|
data/lib/gamespy_query/base.rb
CHANGED
@@ -9,52 +9,75 @@ module GamespyQuery
|
|
9
9
|
|
10
10
|
DEBUG = false
|
11
11
|
|
12
|
+
# Contains basic Tools set to work with logging, debugging, etc.
|
12
13
|
module Tools
|
13
14
|
STR_EMPTY = ""
|
14
15
|
CHAR_N = "\n"
|
15
16
|
|
16
17
|
module_function
|
18
|
+
# Provides access to the logger object
|
19
|
+
# Will use ActionController::Base.logger if available
|
17
20
|
def logger
|
18
21
|
@logger ||= if defined?(::Tools); ::Tools.logger; else; defined?(ActionController) ? ActionController::Base.logger || Logger.new("logger.log") : Logger.new("logger.log"); end
|
19
22
|
end
|
20
23
|
|
24
|
+
# Create debug message from Exception
|
25
|
+
# @param [Exception] e Exception to create debug message from
|
21
26
|
def dbg_msg(e)
|
22
|
-
|
23
|
-
|
27
|
+
<<STR
|
28
|
+
#{e.class}: #{e.message if e.respond_to?(:backtrace)}
|
29
|
+
BackTrace: #{e.backtrace.join(CHAR_N) unless !e.respond_to?(:backtrace) || e.backtrace.nil?}
|
30
|
+
STR
|
24
31
|
end
|
25
32
|
|
26
|
-
|
33
|
+
# Log exception
|
34
|
+
# @param [Exception] e Exception to log
|
35
|
+
# @param [Boolean] as_error Log the exception as error in the log
|
36
|
+
# @param [String] msg Include custom error message
|
27
37
|
def log_exception(e, as_error = true, msg = "")
|
28
38
|
if defined?(::Tools)
|
29
39
|
::Tools.log_exception(e, as_error, msg)
|
30
40
|
else
|
31
|
-
puts "Error: #{e.class} #{e.message}, #{e.backtrace.join("\n")}"
|
41
|
+
puts "Error: #{e.class} #{e.message}, #{e.backtrace.join("\n") unless e.backtrace.nil? }"
|
32
42
|
logger.error "#{"#{msg}:" unless msg.empty?}#{e.class} #{e.message}" if as_error
|
33
43
|
logger.debug dbg_msg(e)
|
34
44
|
end
|
35
45
|
end
|
36
46
|
|
47
|
+
# Log to debug log if DEBUG enabled
|
48
|
+
# @param [Block] block Block to yield string from
|
37
49
|
def debug(&block)
|
38
50
|
return unless DEBUG
|
39
|
-
|
51
|
+
out = yield
|
52
|
+
logger.debug out
|
53
|
+
puts out
|
40
54
|
rescue Exception => e
|
41
55
|
puts "Error: #{e.class} #{e.message}, #{e.backtrace.join("\n")}"
|
42
56
|
end
|
43
57
|
end
|
44
58
|
|
59
|
+
# Contains basic Funcs used throughout the classes
|
45
60
|
module Funcs
|
61
|
+
# Provides TimeOutError exception
|
46
62
|
class TimeOutError < StandardError
|
47
63
|
end
|
48
64
|
|
65
|
+
# Strips tags from string
|
66
|
+
# @param [String] str
|
49
67
|
def strip_tags(str)
|
50
68
|
# TODO: Strip tags!!
|
51
69
|
str
|
52
70
|
end
|
53
71
|
|
54
|
-
|
55
|
-
|
72
|
+
# Float Regex
|
73
|
+
RX_F = /\A\-?[0-9]+\.[0-9]*\Z/
|
74
|
+
# Integer Regex
|
75
|
+
RX_I = /\A\-?[0-9]+\Z/
|
76
|
+
# Integer / Float actually String Regex
|
56
77
|
RX_S = /\A\-?0[0-9]+.*\Z/
|
57
78
|
|
79
|
+
# Clean value, convert if possible
|
80
|
+
# @param [String] value String to convert
|
58
81
|
def clean(value) # TODO: Force String, Integer, Float etc?
|
59
82
|
case value
|
60
83
|
when STR_X0
|
@@ -68,39 +91,37 @@ BackTrace: #{e.backtrace.join(CHAR_N) unless !e.respond_to?(:backtrace) || e.bac
|
|
68
91
|
end
|
69
92
|
end
|
70
93
|
|
71
|
-
|
72
|
-
|
73
|
-
end
|
74
|
-
|
94
|
+
# Handle char
|
95
|
+
# @param [Integer] number Integer to convert
|
75
96
|
def handle_chr(number)
|
76
97
|
number = ((number % 256)+256) if number < 0
|
77
98
|
number = number % 256 if number > 255
|
78
99
|
number
|
79
100
|
end
|
80
101
|
|
81
|
-
|
82
|
-
|
83
|
-
|
102
|
+
# Convert string to UTF-8, stripping out all invalid/undefined characters
|
103
|
+
# @param [String] str String to convert
|
104
|
+
def encode_string(str)
|
105
|
+
#Tools.debug {"Getting string #{str}"}
|
106
|
+
_encode_string str
|
84
107
|
end
|
85
108
|
|
86
109
|
if RUBY_PLATFORM =~ /mswin32/
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
def get_string(str)
|
110
|
+
# Get UTF-8 string from string
|
111
|
+
# @param [String] str
|
112
|
+
def _encode_string(str)
|
91
113
|
System::Text::Encoding.UTF8.GetString(System::Array.of(System::Byte).new(str.bytes.to_a)).to_s # # begin; System::Text::Encoding.USASCII.GetString(reply[0]).to_s; rescue nil, Exception => e; Tools.log_exception(e); reply[0].map {|e| e.chr}.join; end
|
92
114
|
end
|
93
115
|
else
|
94
|
-
|
95
|
-
|
96
|
-
def
|
97
|
-
|
98
|
-
str
|
116
|
+
# Get UTF-8 string from string
|
117
|
+
# @param [String] str
|
118
|
+
def _encode_string(str)
|
119
|
+
(str + ' ').encode("UTF-8", invalid: :replace, undef: :replace)[0..-3]
|
99
120
|
end
|
100
121
|
end
|
101
122
|
end
|
102
123
|
|
103
|
-
|
124
|
+
# Base class from which all others derrive
|
104
125
|
class Base
|
105
126
|
include Funcs
|
106
127
|
end
|
data/lib/gamespy_query/master.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
require_relative 'base'
|
2
|
-
|
3
1
|
module GamespyQuery
|
2
|
+
# Provides access to the Gamespy Master browser
|
4
3
|
class Master < Base
|
5
4
|
PARAMS = [:hostname, :gamever, :gametype, :gamemode, :numplayers, :maxplayers, :password, :equalModRequired, :mission, :mapname,
|
6
5
|
:mod, :signatures, :verifysignatures, :gamestate, :dedicated, :platform, :sv_battleeye, :language, :difficulty]
|
@@ -14,29 +13,33 @@ module GamespyQuery
|
|
14
13
|
"\\\\"
|
15
14
|
end
|
16
15
|
|
17
|
-
|
18
|
-
|
16
|
+
# Geo settings
|
17
|
+
attr_reader :geo
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
File.join(Rails.root, "config").gsub("/", "\\")
|
23
|
-
else
|
24
|
-
File.join(Rails.root, "config")
|
25
|
-
end
|
26
|
-
end
|
19
|
+
# Game
|
20
|
+
attr_reader :game
|
27
21
|
|
22
|
+
# Initializes the instance
|
23
|
+
# @param [String] geo Geo string
|
24
|
+
# @param [String] game Game string
|
28
25
|
def initialize(geo = nil, game = "arma2oapc")
|
29
26
|
@geo, @game = geo, game
|
30
27
|
end
|
31
28
|
|
29
|
+
# Convert the master browser data to hash
|
32
30
|
def process list = self.read
|
33
31
|
self.to_hash list
|
34
32
|
end
|
35
33
|
|
34
|
+
# Gets list of PARAMS, delimited by {DELIMIT}
|
36
35
|
def get_params
|
37
36
|
PARAMS.clone.map{|e| "#{DELIMIT}#{e}"}.join("")
|
38
37
|
end
|
39
38
|
|
39
|
+
# Gets list of server addressses and optionally data
|
40
|
+
# @param [String] list Specify list or nil to fetch the list
|
41
|
+
# @param [Boolean] include_data Should server info data from the master browser be included
|
42
|
+
# @param [String] geo Geo String
|
40
43
|
def get_server_list list = nil, include_data = false, geo = nil
|
41
44
|
addrs = []
|
42
45
|
list = %x[gslist -p "#{geoip_path}"#{" #{geo}-X #{get_params}" if include_data} -n #{@game}] if list.nil?
|
@@ -50,6 +53,7 @@ module GamespyQuery
|
|
50
53
|
addrs
|
51
54
|
end
|
52
55
|
|
56
|
+
# Read the server list from gamespy
|
53
57
|
def read
|
54
58
|
geo = @geo ? @geo : "-Q 11 "
|
55
59
|
unless geo.nil? || geo.empty? || File.exists?(File.join(geoip_path, "GeoIP.dat"))
|
@@ -59,13 +63,22 @@ module GamespyQuery
|
|
59
63
|
get_server_list(nil, true, geo)
|
60
64
|
end
|
61
65
|
|
66
|
+
# Handle reply data from gamespy master browser
|
67
|
+
# @param [String] reply Reply from gamespy
|
68
|
+
# @param [String] geo Geo String
|
62
69
|
def handle_data(reply, geo = nil)
|
63
70
|
reply = reply.gsub("\\\\\\", "") if geo
|
64
71
|
reply.split("\n").select{|line| line =~ RX_ADDR_LINE }
|
65
72
|
end
|
66
73
|
|
74
|
+
|
75
|
+
# Address and Data regex
|
67
76
|
RX_H = /\A([\.0-9]*):([0-9]*) *\\(.*)/
|
77
|
+
# Split string
|
68
78
|
STR_SPLIT = "\\"
|
79
|
+
|
80
|
+
# Convert array of data to hash
|
81
|
+
# @param [Array] ar Array to convert
|
69
82
|
def to_hash(ar)
|
70
83
|
list = Hash.new
|
71
84
|
ar.each_with_index do |entry, index|
|
@@ -77,7 +90,7 @@ module GamespyQuery
|
|
77
90
|
i = 0
|
78
91
|
content.map! do |e|
|
79
92
|
i += 1
|
80
|
-
i % 2 == 0 ? e :
|
93
|
+
i % 2 == 0 ? e : encode_string(e)
|
81
94
|
end
|
82
95
|
addr = "#{ip}:#{port}"
|
83
96
|
if list.has_key?(addr)
|
@@ -86,6 +99,7 @@ module GamespyQuery
|
|
86
99
|
e = Hash.new
|
87
100
|
e[:ip] = ip
|
88
101
|
e[:port] = port
|
102
|
+
e[:gamename] = @game
|
89
103
|
list[addr] = e
|
90
104
|
end
|
91
105
|
if e[:gamedata]
|
@@ -96,6 +110,19 @@ module GamespyQuery
|
|
96
110
|
end
|
97
111
|
list
|
98
112
|
end
|
113
|
+
|
114
|
+
|
115
|
+
# Get geoip_path
|
116
|
+
def geoip_path
|
117
|
+
return File.join(Dir.pwd, "config") unless defined?(Rails)
|
118
|
+
|
119
|
+
case RUBY_PLATFORM
|
120
|
+
when /-mingw32$/, /-mswin32$/
|
121
|
+
File.join(Rails.root, "config").gsub("/", "\\")
|
122
|
+
else
|
123
|
+
File.join(Rails.root, "config")
|
124
|
+
end
|
125
|
+
end
|
99
126
|
end
|
100
127
|
end
|
101
128
|
|
@@ -103,4 +130,4 @@ if $0 == __FILE__
|
|
103
130
|
master = GamespyQuery::Master.new
|
104
131
|
r = master.read
|
105
132
|
puts r
|
106
|
-
end
|
133
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'cri'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module GamespyQuery
|
5
|
+
# Handles commandline parameters for the Main tool
|
6
|
+
class Options
|
7
|
+
class <<self
|
8
|
+
# Parse given args
|
9
|
+
# @param [Array] args Parse given args
|
10
|
+
def parse args = ARGV
|
11
|
+
_parse.run(args)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Defaults for options
|
15
|
+
# @param [Hash] opts Options
|
16
|
+
def setup_master_opts opts
|
17
|
+
opts = opts.clone
|
18
|
+
opts[:geo] ||= ""
|
19
|
+
opts[:game] ||= "arma2oapc"
|
20
|
+
opts
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
# Parser definition
|
25
|
+
def _parse
|
26
|
+
#root_command = Cri::Command.new_basic_root # Bug with h self.help -> cmd.help etc
|
27
|
+
root_command = Cri::Command.define do
|
28
|
+
name 'gamespy_query'
|
29
|
+
usage 'gamespy_query [options]'
|
30
|
+
summary 'Gamespy Protocol'
|
31
|
+
description 'This command provides the basic functionality'
|
32
|
+
|
33
|
+
|
34
|
+
option :h, :help, 'show help for this command' do |value, cmd|
|
35
|
+
puts cmd.help
|
36
|
+
exit 0
|
37
|
+
end
|
38
|
+
|
39
|
+
subcommand Cri::Command.new_basic_help
|
40
|
+
|
41
|
+
option nil, :version, 'Show version' do |value, cmd|
|
42
|
+
puts GamespyQuery.product_version
|
43
|
+
exit 0
|
44
|
+
end
|
45
|
+
|
46
|
+
flag :v, :verbose, 'Verbose'
|
47
|
+
end
|
48
|
+
|
49
|
+
root_command.define_command do
|
50
|
+
name 'sync'
|
51
|
+
usage 'sync ip:port [options]'
|
52
|
+
summary 'Sync data'
|
53
|
+
aliases :s
|
54
|
+
|
55
|
+
run do |opts, args, cmd|
|
56
|
+
puts "Running Sync, #{opts}, #{args}, #{cmd}"
|
57
|
+
if args.empty?
|
58
|
+
puts "Missing ip:port"
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
host, port = if args.size > 1
|
62
|
+
args
|
63
|
+
else
|
64
|
+
args[0].split(":")
|
65
|
+
end
|
66
|
+
time_start = Time.now
|
67
|
+
g = GamespyQuery::Socket.new("#{host}:#{port}")
|
68
|
+
r = g.sync
|
69
|
+
time_taken = Time.now - time_start
|
70
|
+
puts "Took: #{time_taken}s"
|
71
|
+
exit unless r
|
72
|
+
puts r.to_yaml
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
root_command.add_command _parse_master_command
|
77
|
+
|
78
|
+
root_command
|
79
|
+
end
|
80
|
+
|
81
|
+
def _parse_master_command
|
82
|
+
master_command = Cri::Command.define do
|
83
|
+
name 'master'
|
84
|
+
usage 'master COMMAND [options]'
|
85
|
+
aliases :m
|
86
|
+
|
87
|
+
option :g, :game, 'Specify game', :argument => :required
|
88
|
+
option nil, :geo, 'Specify geo', :argument => :required
|
89
|
+
|
90
|
+
subcommand Cri::Command.new_basic_help
|
91
|
+
|
92
|
+
run do |opts, args, cmd|
|
93
|
+
puts "Running Master, #{opts}, #{args}, #{cmd}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
master_command.define_command do
|
98
|
+
name 'list'
|
99
|
+
usage 'list [options]'
|
100
|
+
aliases :l
|
101
|
+
|
102
|
+
run do |opts, args, cmd|
|
103
|
+
opts = GamespyQuery::Options.setup_master_opts opts
|
104
|
+
master = GamespyQuery::Master.new(opts[:geo], opts[:game])
|
105
|
+
list = master.read
|
106
|
+
puts list
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
master_command.define_command do
|
111
|
+
name 'process'
|
112
|
+
usage 'process [options]'
|
113
|
+
aliases :p
|
114
|
+
|
115
|
+
run do |opts, args, cmd|
|
116
|
+
opts = GamespyQuery::Options.setup_master_opts opts
|
117
|
+
|
118
|
+
master = GamespyQuery::Master.new(opts[:geo], opts[:game])
|
119
|
+
process = master.process
|
120
|
+
puts process
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
master_command.define_command do
|
125
|
+
name 'process_master'
|
126
|
+
usage 'process_master [options]'
|
127
|
+
aliases :m
|
128
|
+
|
129
|
+
run do |opts, args, cmd|
|
130
|
+
opts = GamespyQuery::Options.setup_master_opts opts
|
131
|
+
process = GamespyQuery::SocketMaster.process_master(opts[:game], opts[:geo])
|
132
|
+
puts process
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
master_command
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|