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 CHANGED
@@ -2,3 +2,8 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+
6
+ /.idea/
7
+ /.yardoc/
8
+ /doc/
9
+ *.log
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This binary is mostly intended for testing gamespy_query implementation
4
+
5
+ require 'gamespy_query'
6
+
7
+ # Parse commandline arguments
8
+ options = GamespyQuery::Options.parse
@@ -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
- # specify any dependencies here; for example:
22
- # s.add_development_dependency "rspec"
23
- # s.add_runtime_dependency "rest-client"
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
- # Your code goes here...
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
 
@@ -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
- "#{e.class}: #{e.message if e.respond_to?(:backtrace)}
23
- BackTrace: #{e.backtrace.join(CHAR_N) unless !e.respond_to?(:backtrace) || e.backtrace.nil?}"
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
- logger.debug yield
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
- RX_F = /\A\-?[0-9][0-9]*\.[0-9]*\Z/
55
- RX_I = /\A\-?[0-9][0-9]*\Z/
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
- def clean_string(str)
72
- str.encode("UTF-8", invalid: :replace, undef: :replace)
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
- def get_string(*params)
82
- Tools.debug {"Getting string #{params}"}
83
- _get_string(*params)
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
- include System::Net
88
- include System::Net::Sockets
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
- require 'socket'
95
- require 'timeout'
96
- def get_string(str)
97
- #(str + ' ').encode("UTF-8", :invalid => :replace, :undef => :replace)[0..-2]
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
@@ -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
- def geoip_path
18
- return File.join(Dir.pwd, "config") unless defined?(Rails)
16
+ # Geo settings
17
+ attr_reader :geo
19
18
 
20
- case RUBY_PLATFORM
21
- when /-mingw32$/, /-mswin32$/
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 : clean_string(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