gamespy_query 0.1.5 → 0.2.0pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|