gamespy_query 0.2.0pre2 → 0.2.0pre3

Sign up to get free protection for your applications and to get access to all the features.
data/bin/gamespy_query CHANGED
File without changes
@@ -13,7 +13,7 @@ module GamespyQuery
13
13
  module Tools
14
14
  STR_EMPTY = ""
15
15
  CHAR_N = "\n"
16
-
16
+
17
17
  module_function
18
18
  # Provides access to the logger object
19
19
  # Will use ActionController::Base.logger if available
@@ -76,9 +76,9 @@ STR
76
76
  # Integer / Float actually String Regex
77
77
  RX_S = /\A\-?0[0-9]+.*\Z/
78
78
 
79
- # Clean value, convert if possible
79
+ # Convert data type and strip tags
80
80
  # @param [String] value String to convert
81
- def clean(value) # TODO: Force String, Integer, Float etc?
81
+ def convert_type(value) # TODO: Force String, Integer, Float etc?
82
82
  case value
83
83
  when STR_X0
84
84
  nil
@@ -99,25 +99,18 @@ STR
99
99
  number
100
100
  end
101
101
 
102
+ STR_UTF8 = 'UTF-8'
103
+
102
104
  # Convert string to UTF-8, stripping out all invalid/undefined characters
103
105
  # @param [String] str String to convert
104
106
  def encode_string(str)
105
107
  #Tools.debug {"Getting string #{str}"}
106
- _encode_string str
107
- end
108
-
109
- if RUBY_PLATFORM =~ /mswin32/
110
- # Get UTF-8 string from string
111
- # @param [String] str
112
- def _encode_string(str)
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
114
- end
115
- else
116
- # Get UTF-8 string from string
117
- # @param [String] str
118
- def _encode_string(str)
119
- #(str + ' ').encode("US-ASCII", invalid: :replace, undef: :replace)[0..-3]
120
- str.bytes.to_a.pack("U*")
108
+ #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
109
+ begin
110
+ str.bytes.to_a.pack('C*').force_encoding(STR_UTF8)
111
+ rescue nil, Exception => e
112
+ Tools.log_exception e
113
+ str.encode("UTF-8", invalid: :replace, undef: :replace)
121
114
  end
122
115
  end
123
116
  end
@@ -1,8 +1,12 @@
1
1
  module GamespyQuery
2
2
  # Provides access to the Gamespy Master browser
3
3
  class Master < Base
4
+ # TODO: gslist.exe output encoding seems to be a problem.
5
+ # not been able to find a solution yet to get unicode instead of garbelled characters
6
+ # If impossible, perhaps should try custom implementation of master query
7
+
4
8
  PARAMS = [:hostname, :gamever, :gametype, :gamemode, :numplayers, :maxplayers, :password, :equalModRequired, :mission, :mapname,
5
- :mod, :signatures, :verifysignatures, :gamestate, :dedicated, :platform, :sv_battleeye, :language, :difficulty]
9
+ :mod, :signatures, :verifysignatures, :gamestate, :dedicated, :platform, :sv_battleye, :language, :difficulty]
6
10
 
7
11
  RX_ADDR_LINE = /^[\s\t]*([\d\.]+)[\s\t:]*(\d+)[\s\t]*(.*)$/
8
12
 
@@ -13,6 +17,14 @@ module GamespyQuery
13
17
  "\\\\"
14
18
  end
15
19
 
20
+ path = defined?(Rails) ? File.join(Rails.root, "config") : File.join(Dir.pwd, "config")
21
+ DEFAULT_GEOIP_PATH = case RUBY_PLATFORM
22
+ when /-mingw32$/, /-mswin32$/
23
+ path.gsub("/", "\\")
24
+ else
25
+ path
26
+ end
27
+
16
28
  # Geo settings
17
29
  attr_reader :geo
18
30
 
@@ -22,8 +34,8 @@ module GamespyQuery
22
34
  # Initializes the instance
23
35
  # @param [String] geo Geo string
24
36
  # @param [String] game Game string
25
- def initialize(geo = nil, game = "arma2oapc")
26
- @geo, @game = geo, game
37
+ def initialize(geo = nil, game = "arma2oapc", geoip_path = nil)
38
+ @geo, @game, @geoip_path = geo, game, geoip_path
27
39
  end
28
40
 
29
41
  # Convert the master browser data to hash
@@ -118,20 +130,7 @@ module GamespyQuery
118
130
 
119
131
  # Get geoip_path
120
132
  def geoip_path
121
- return File.join(Dir.pwd, "config") unless defined?(Rails)
122
-
123
- case RUBY_PLATFORM
124
- when /-mingw32$/, /-mswin32$/
125
- File.join(Rails.root, "config").gsub("/", "\\")
126
- else
127
- File.join(Rails.root, "config")
128
- end
133
+ @geoip_path || DEFAULT_GEOIP_PATH
129
134
  end
130
135
  end
131
136
  end
132
-
133
- if $0 == __FILE__
134
- master = GamespyQuery::Master.new
135
- r = master.read
136
- puts r
137
- end
@@ -30,13 +30,13 @@ module GamespyQuery
30
30
  # - Array, packetDATA ordered already by packetID
31
31
  def initialize(packets)
32
32
  @packets = case packets
33
- when Hash
34
- packets.keys.sort.map{|key| packets[key] }
35
- when Array
36
- packets
37
- else
38
- raise UnsupportedFormat, "Unsupported format: #{packets.class}"
39
- end
33
+ when Hash
34
+ packets.keys.sort.map{|key| packets[key] }
35
+ when Array
36
+ packets
37
+ else
38
+ raise UnsupportedFormat, "Unsupported format: #{packets.class}"
39
+ end
40
40
  end
41
41
 
42
42
  # Parse game and player data to hash
@@ -48,7 +48,7 @@ module GamespyQuery
48
48
  data[:game] = {} # Key: InfoKey, Value: InfoValue
49
49
  data[:players] = {} # Key: InfoType, Value: Array of Values
50
50
  player_info = false
51
- player_data = "".encode "ASCII-8BIT"
51
+ player_data = ""
52
52
 
53
53
  # Parse the packets
54
54
  @packets.each do |packet|
@@ -70,12 +70,12 @@ module GamespyQuery
70
70
  else
71
71
  # GameData-only
72
72
  data[:game].merge!(parse_game_data(packet))
73
- end
73
+ end
74
74
  end
75
75
  end
76
76
 
77
77
  # Parse player_data
78
- data[:players] = parse_player_data(player_data)
78
+ data[:players] = parse_player_data(encode_string player_data)
79
79
 
80
80
  data
81
81
  end
@@ -168,7 +168,7 @@ module GamespyQuery
168
168
  player_data[player_data.keys[i]] << encode_string(entry.sub(STR_X0, STR_EMPTY))
169
169
  str.sub!(entry, STR_EMPTY)
170
170
  end
171
-
171
+
172
172
  # Search for SIX string to overwrite last entry
173
173
  new_player_data = []
174
174
  overwrite = false
@@ -177,7 +177,6 @@ module GamespyQuery
177
177
  overwrite = true # tag so that the next entry will overwrite the latest entry
178
178
  next # ignore
179
179
  else
180
- info = clean(info) if [2,3].include?(i) # Apply data_type conversion for Score and Deaths
181
180
  if overwrite
182
181
  new_player_data[-1] = info # Overwrite latest entry
183
182
  overwrite = false # done the overwrite
@@ -5,83 +5,6 @@ require 'yaml'
5
5
  require 'socket'
6
6
 
7
7
  module GamespyQuery
8
- # Provides socket functionality on multiple platforms
9
- # TODO
10
- module MultiSocket
11
- # Create socket
12
- def create_socket(*params)
13
- Tools.debug {"Creating socket #{params}"}
14
- _create_socket(*params)
15
- end
16
-
17
- # Write socket
18
- def socket_send(*params)
19
- Tools.debug {"Sending socket #{params}"}
20
- _socket_send(*params)
21
- end
22
-
23
- # Read socket
24
- def socket_receive(*params)
25
- Tools.debug {"Receiving socket #{params}"}
26
- _socket_receive(*params)
27
- end
28
-
29
- # Close socket
30
- def socket_close(*params)
31
- Tools.debug {"Closing socket #{params}"}
32
- @s.close
33
- end
34
-
35
- if RUBY_PLATFORM =~ /mswin32/
36
- include System::Net
37
- include System::Net::Sockets
38
-
39
- # Create socket
40
- def _create_socket(host, port)
41
- @ip_end_point = IPEndPoint.new(IPAddress.Any, 0)
42
- @s = UdpClient.new
43
- @s.client.receive_timeout = DEFAULT_TIMEOUT * 1000
44
- @s.connect(host, port.to_i)
45
- end
46
-
47
- # Write socket
48
- def _socket_send(packet)
49
- @s.Send(packet, packet.length)
50
- end
51
-
52
- # Read socket
53
- def _socket_receive
54
- @s.Receive(@ip_end_point)
55
- end
56
-
57
- else
58
-
59
- # Create socket
60
- def _create_socket(host, port)
61
- @s = UDPSocket.new
62
- @s.connect(host, port)
63
- end
64
-
65
- # Write socket
66
- def _socket_send(packet)
67
- @s.puts(packet)
68
- end
69
-
70
- # Read socket
71
- def _socket_receive
72
- begin
73
- Timeout::timeout(DEFAULT_TIMEOUT) do
74
- @s.recvfrom(RECEIVE_SIZE)
75
- end
76
- rescue Timeout::Error
77
- raise TimeoutError, "TimeOut on #{self}"
78
- ensure
79
- @s.close
80
- end
81
- end
82
- end
83
- end
84
-
85
8
  # Provides direct connection functionality to gamespy enabled game servers
86
9
  # This query contains up to 7x more information than the gamespy master browser query
87
10
  # For example, player lists with info (teams, scores, deaths) are only available by using direct connection
@@ -117,6 +40,8 @@ module GamespyQuery
117
40
  RX_CHALLENGE2 = /[^0-9\-]/si
118
41
  RX_SPLITNUM = /^splitnum\x00(.)/i
119
42
 
43
+ PLATFORM_IR = /-mswin32/
44
+
120
45
  # TODO: Support pings
121
46
  # TODO: Handle .NET native sockets
122
47
  STATE_INIT, STATE_SENT_CHALLENGE, STATE_RECEIVED_CHALLENGE, STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA, STATE_READY = 0, 1, 2, 3, 4, 5
@@ -170,15 +95,11 @@ module GamespyQuery
170
95
  else
171
96
  raise NotInWriteState, "NotInWriteState, #{self}"
172
97
  end
173
- rescue NotInWriteState => e
174
- r = false
175
- self.failed = true
176
- close unless closed?
177
- rescue => e
178
- Tools.log_exception e
98
+ rescue nil, Exception => e
179
99
  self.failed = true
180
100
  r = nil
181
101
  close unless closed?
102
+ raise e
182
103
  end
183
104
 
184
105
  =begin
@@ -192,6 +113,31 @@ module GamespyQuery
192
113
  r
193
114
  end
194
115
 
116
+ # Temp Workaround for IO.select issue on IR
117
+ def _read_non_block
118
+ if RUBY_PLATFORM =~ PLATFORM_IR
119
+ time_end = Time.now + DEFAULT_TIMEOUT
120
+ success = false
121
+ until success || Time.now >= time_end
122
+ begin
123
+ d = self.recvfrom_nonblock(RECEIVE_SIZE)
124
+ success = d
125
+ rescue SocketError
126
+
127
+ rescue nil, Exception => e
128
+ Tools.log_exception(e)
129
+ end
130
+ end
131
+ if success
132
+ success
133
+ else
134
+ raise TimeOutError, "The read operation has timedout"
135
+ end
136
+ else
137
+ self.recvfrom_nonblock(RECEIVE_SIZE)
138
+ end
139
+ end
140
+
195
141
  # Handle the read state
196
142
  def handle_read
197
143
  # Tools.debug {"Read: #{self.inspect}, #{self.state}"}
@@ -200,14 +146,14 @@ module GamespyQuery
200
146
  begin
201
147
  case self.state
202
148
  when STATE_SENT_CHALLENGE
203
- data = self.recvfrom_nonblock(RECEIVE_SIZE)
149
+ data = _read_non_block
204
150
  Tools.debug {"Read (1): #{self.inspect}: #{data}"}
205
151
 
206
152
  handle_challenge data[0]
207
153
 
208
154
  self.state = STATE_RECEIVED_CHALLENGE
209
155
  when STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA
210
- data = self.recvfrom_nonblock(RECEIVE_SIZE)
156
+ data = _read_non_block
211
157
  Tools.debug {"Read (3,4): #{self.inspect}: #{data}"}
212
158
  self.state = STATE_RECEIVE_DATA
213
159
 
@@ -227,16 +173,11 @@ module GamespyQuery
227
173
  else
228
174
  raise NotInReadState, "NotInReadState, #{self}"
229
175
  end
230
- rescue NotInReadState => e
231
- r = false
232
- self.failed = true
233
- close unless closed?
234
- rescue => e
235
- # TODO: Simply raise the exception?
236
- Tools.log_exception(e)
176
+ rescue nil, Exception => e
237
177
  self.failed = true
238
178
  r = nil
239
179
  close unless closed?
180
+ raise e
240
181
  end
241
182
  r
242
183
  end
@@ -290,6 +231,7 @@ module GamespyQuery
290
231
  # @param [String] reply Reply from server
291
232
  def sync reply = self.fetch
292
233
  game_data, key = {}, nil
234
+ Tools.debug {"DATA: #{reply.inspect}"}
293
235
  return game_data if reply.nil? || reply.empty?
294
236
 
295
237
  parser = Parser.new(reply)
@@ -305,18 +247,19 @@ module GamespyQuery
305
247
 
306
248
  # Fetch all packets from socket
307
249
  def fetch
250
+ Tools.debug {"FUCK ME"}
308
251
  pings = []
309
252
  r = self.data
310
253
  begin
311
- until valid?
254
+ until valid? || failed
312
255
  if handle_state
313
- if IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
256
+ if RUBY_PLATFORM =~ PLATFORM_IR || IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
314
257
  handle_write
315
258
  else
316
259
  raise TimeOutError, "TimeOut during write, #{self}"
317
260
  end
318
261
  else
319
- if IO.select([self], nil, nil, DEFAULT_TIMEOUT)
262
+ if RUBY_PLATFORM =~ PLATFORM_IR || IO.select([self], nil, nil, DEFAULT_TIMEOUT)
320
263
  handle_read
321
264
  else
322
265
  raise TimeOutError, "TimeOut during read, #{self}"
@@ -341,4 +284,83 @@ module GamespyQuery
341
284
  r
342
285
  end
343
286
  end
287
+
288
+ # Provides socket functionality on multiple platforms
289
+ # TODO
290
+ =begin
291
+ module MultiSocket
292
+ # Create socket
293
+ def create_socket(*params)
294
+ Tools.debug {"Creating socket #{params}"}
295
+ _create_socket(*params)
296
+ end
297
+
298
+ # Write socket
299
+ def socket_send(*params)
300
+ Tools.debug {"Sending socket #{params}"}
301
+ _socket_send(*params)
302
+ end
303
+
304
+ # Read socket
305
+ def socket_receive(*params)
306
+ Tools.debug {"Receiving socket #{params}"}
307
+ _socket_receive(*params)
308
+ end
309
+
310
+ # Close socket
311
+ def socket_close(*params)
312
+ Tools.debug {"Closing socket #{params}"}
313
+ @s.close
314
+ end
315
+
316
+ if RUBY_PLATFORM =~ /mswin32/
317
+ include System::Net
318
+ include System::Net::Sockets
319
+
320
+ # Create socket
321
+ def _create_socket(host, port)
322
+ @ip_end_point = IPEndPoint.new(IPAddress.Any, 0)
323
+ @s = UdpClient.new
324
+ @s.client.receive_timeout = DEFAULT_TIMEOUT * 1000
325
+ @s.connect(host, port.to_i)
326
+ end
327
+
328
+ # Write socket
329
+ def _socket_send(packet)
330
+ @s.Send(packet, packet.length)
331
+ end
332
+
333
+ # Read socket
334
+ def _socket_receive
335
+ @s.Receive(@ip_end_point)
336
+ end
337
+
338
+ else
339
+
340
+ # Create socket
341
+ def _create_socket(host, port)
342
+ @s = UDPSocket.new
343
+ @s.connect(host, port)
344
+ end
345
+
346
+ # Write socket
347
+ def _socket_send(packet)
348
+ @s.puts(packet)
349
+ end
350
+
351
+ # Read socket
352
+ def _socket_receive
353
+ begin
354
+ Timeout::timeout(DEFAULT_TIMEOUT) do
355
+ @s.recvfrom(RECEIVE_SIZE)
356
+ end
357
+ rescue Timeout::Error
358
+ raise TimeoutError, "TimeOut on #{self}"
359
+ ensure
360
+ @s.close
361
+ end
362
+ end
363
+ end
364
+ end
365
+ =end
344
366
  end
@@ -54,10 +54,10 @@ module GamespyQuery
54
54
  Tools.debug {"Sockets: #{queue.size}, AddrsLeft: #{@addrs.size}, ReadReady: #{"#{ready[0].size} / #{read_sockets.size}, WriteReady: #{ready[1].size} / #{write_sockets.size}, ExcReady: #{ready[2].size} / #{queue.size}" unless ready.nil?}"}
55
55
 
56
56
  # Read
57
- ready[0].each { |s| queue.delete(s) unless s.handle_read() }
57
+ ready[0].each { |s| begin; s.handle_read(); rescue nil, Exception => e; queue.delete(s); end }
58
58
 
59
59
  # Write
60
- ready[1].each { |s| queue.delete(s) unless s.handle_write() }
60
+ ready[1].each { |s| begin; s.handle_write(); rescue nil, Exception => e; queue.delete(s); end }
61
61
 
62
62
  # Exceptions
63
63
  #ready[2].each { |s| queue.delete(s) unless s.handle_exc }
@@ -104,21 +104,3 @@ module GamespyQuery
104
104
  end
105
105
  end
106
106
  end
107
-
108
- if $0 == __FILE__
109
- require_relative 'master'
110
- srv = File.open(ARGV[0] || "servers.txt") { |f| f.read }
111
- master = GamespyQuery::Master.new
112
- addrs = master.get_server_list srv
113
-
114
- time_start = Time.now
115
- sm = GamespyQuery::SocketMaster.new(addrs)
116
- sockets = sm.process!
117
- time_taken = Time.now - time_start
118
-
119
- cool = sockets.count {|v| v.valid? }
120
- dude = sockets.size - cool
121
-
122
- puts "Success: #{cool}, Failed: #{dude}"
123
- puts "Took: #{time_taken}s"
124
- end
@@ -1,4 +1,4 @@
1
1
  module GamespyQuery
2
2
  # Version of the library
3
- VERSION = "0.2.0pre2"
3
+ VERSION = "0.2.0pre3"
4
4
  end
data/lib/gamespy_query.rb CHANGED
@@ -22,19 +22,3 @@ module GamespyQuery
22
22
  "GamespyQuery version #{VERSION}"
23
23
  end
24
24
  end
25
-
26
-
27
- if $0 == __FILE__
28
- host, port = if ARGV.size > 1
29
- ARGV
30
- else
31
- ARGV[0].split(":")
32
- end
33
- time_start = Time.now
34
- g = GamespyQuery::Socket.new("#{host}:#{port}")
35
- r = g.sync
36
- time_taken = Time.now - time_start
37
- puts "Took: #{time_taken}s"
38
- exit unless r
39
- puts r.to_yaml
40
- end
@@ -19,8 +19,8 @@ context "Funcs" do
19
19
  # TODO: This method doesnt do anything atm
20
20
  asserts("strip_tags") { topic.strip_tags "test" }.equals "test"
21
21
 
22
- asserts("clean integer") { topic.clean "1" }.equals 1
23
- asserts("clean float") { topic.clean "1.5" }.equals 1.5
22
+ asserts("convert integer") { topic.convert_type "1" }.equals 1
23
+ asserts("convert float") { topic.convert_type "1.5" }.equals 1.5
24
24
 
25
25
  asserts("encode_string") { topic.encode_string("test encoding").encoding }.equals Encoding.find("UTF-8")
26
26
 
@@ -16,8 +16,8 @@ context "Parser" do
16
16
  setup { topic[:game] }
17
17
  denies("Game data") { topic }.empty
18
18
 
19
- asserts("gamever") { topic["gamever"] }.equals "1.59.79548"
20
- asserts("sv_battleye") { topic["sv_battleye"] }.equals "1"
19
+ asserts("gamever") { topic[:gamever] }.equals "1.59.79548"
20
+ asserts("sv_battleye") { topic[:sv_battleye] }.equals "1"
21
21
  end
22
22
 
23
23
  context "Players data" do
@@ -32,15 +32,15 @@ context "Parser" do
32
32
  context "First element" do
33
33
  asserts("Name") { topic[:names][0] }.equals "Skilllos"
34
34
  asserts("Team") { topic[:teams][0] }.equals ""
35
- asserts("Score") { topic[:scores][0] }.equals 371
36
- asserts("Deaths") { topic[:deaths][0] }.equals 24
35
+ asserts("Score") { topic[:scores][0] }.equals "371"
36
+ asserts("Deaths") { topic[:deaths][0] }.equals "24"
37
37
  end
38
38
 
39
39
  context "Tenth element" do
40
40
  asserts("Name") { topic[:names][10] }.equals "DrHat"
41
41
  asserts("Team") { topic[:teams][10] }.equals ""
42
- asserts("Score") { topic[:scores][10] }.equals 37
43
- asserts("Deaths") { topic[:deaths][10] }.equals 8
42
+ asserts("Score") { topic[:scores][10] }.equals "37"
43
+ asserts("Deaths") { topic[:deaths][10] }.equals "8"
44
44
  end
45
45
  end
46
46
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gamespy_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0pre2
4
+ version: 0.2.0pre3
5
5
  prerelease: 5
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-09 00:00:00.000000000 Z
12
+ date: 2012-03-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cri
16
- requirement: &15149120 !ruby/object:Gem::Requirement
16
+ requirement: &10524320 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *15149120
24
+ version_requirements: *10524320
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: riot
27
- requirement: &15148220 !ruby/object:Gem::Requirement
27
+ requirement: &10523880 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *15148220
35
+ version_requirements: *10523880
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: yard
38
- requirement: &15147420 !ruby/object:Gem::Requirement
38
+ requirement: &10523420 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *15147420
46
+ version_requirements: *10523420
47
47
  description: ''
48
48
  email:
49
49
  - sb@dev-heaven.net