gameoverseer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +34 -0
- data/README.md +34 -0
- data/gameoverseer.gemspec +25 -0
- data/lib/gameoverseer/channels/channel_manager.rb +40 -0
- data/lib/gameoverseer/clients/client_manager.rb +40 -0
- data/lib/gameoverseer/console/console.rb +207 -0
- data/lib/gameoverseer/input_handler/input_handler.rb +30 -0
- data/lib/gameoverseer/messages/message_manager.rb +36 -0
- data/lib/gameoverseer/server/handshake.rb +19 -0
- data/lib/gameoverseer/server/renet_server.rb +84 -0
- data/lib/gameoverseer/services/internal/broadcast.rb +13 -0
- data/lib/gameoverseer/services/internal/chat.rb +9 -0
- data/lib/gameoverseer/services/internal/environment.rb +9 -0
- data/lib/gameoverseer/services/internal/handshake.rb +25 -0
- data/lib/gameoverseer/services/internal/services.rb +3 -0
- data/lib/gameoverseer/services/service.rb +63 -0
- data/lib/gameoverseer/services/services.rb +31 -0
- data/lib/gameoverseer/version.rb +4 -0
- data/lib/gameoverseer.rb +58 -0
- data/license.md +9 -0
- data/test-clients/protocol-lib.rb +13 -0
- data/test-clients/test-client-alpha.rb +36 -0
- data/test-clients/test-client-beta.rb +37 -0
- data/test-clients/test-client-gamma.rb +59 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2dd44389a017c5cae455eddabf417b4bae6a37d2
|
4
|
+
data.tar.gz: a6acb2dc8c928b4c213cd6c564bbd4d1feb443d6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 71e971158f6922753f09215a5d9ab445a070e95debd934094c3c759508b7db265097f26902682133e93abb7cbcbf9dced82215ba86f10b644c489f519f3e49a2
|
7
|
+
data.tar.gz: 85cd07339fc79a9d80a9b73083cb75221cdc128c2c557711e21ccd2ffd3067492042e81cadaf5b756b600feed1c902c09a54090868d6a1c5f962daf26ad5a9c0
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
# gemspec
|
4
|
+
|
5
|
+
gem "gosu"
|
6
|
+
gem "nio4r", git: "https://github.com/MagLev/nio4r.git", branch: "johnnyt/recursive-lock-fix"
|
7
|
+
gem "celluloid"
|
8
|
+
gem "renet"# use rubygems version for now, git: "https://github.com/jvranish/rENet.git"
|
9
|
+
gem "multi_json"
|
10
|
+
gem "net-ssh"
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
GIT
|
2
|
+
remote: https://github.com/MagLev/nio4r.git
|
3
|
+
revision: d22d0b9da828462f0ab14043f476388888430eb2
|
4
|
+
branch: johnnyt/recursive-lock-fix
|
5
|
+
specs:
|
6
|
+
nio4r (1.0.1)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
celluloid (0.16.0)
|
12
|
+
timers (~> 4.0.0)
|
13
|
+
gosu (0.8.7.2)
|
14
|
+
gosu (0.8.7.2-x86-mingw32)
|
15
|
+
hitimes (1.2.2-java)
|
16
|
+
hitimes (1.2.2-x86-mingw32)
|
17
|
+
multi_json (1.10.1)
|
18
|
+
net-ssh (2.9.2)
|
19
|
+
renet (0.1.14)
|
20
|
+
renet (0.1.14-x86-mingw32)
|
21
|
+
timers (4.0.1)
|
22
|
+
hitimes
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
java
|
26
|
+
x86-mingw32
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
celluloid
|
30
|
+
gosu
|
31
|
+
multi_json
|
32
|
+
net-ssh
|
33
|
+
nio4r!
|
34
|
+
renet
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# GameOverseer
|
2
|
+
A game server, designed to be able to play host to up to 4 players in Planet Wars.
|
3
|
+
|
4
|
+
This repo is a complete rewrite of [GameOverseer](https://github.com/cyberarm/gameoverseer).
|
5
|
+
|
6
|
+
# Status
|
7
|
+
In development.
|
8
|
+
Any examples given are subject to be outdated at anytime.
|
9
|
+
|
10
|
+
# Install
|
11
|
+
|
12
|
+
# Usage
|
13
|
+
``` ruby
|
14
|
+
require 'gameoverseer'
|
15
|
+
|
16
|
+
# Write a service for your game
|
17
|
+
class GameWorld < GameOverseer::Service
|
18
|
+
def setup
|
19
|
+
channel_manager.register('game_world')
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(data)
|
23
|
+
# Do stuff with the data hash.
|
24
|
+
end
|
25
|
+
|
26
|
+
def version
|
27
|
+
"1.3.75"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
host = "localhost"
|
32
|
+
port = 56789
|
33
|
+
GameOverseer.activate(host, port)
|
34
|
+
```
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "gameoverseer/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "gameoverseer"
|
7
|
+
s.version = GameOverseer::VERSION
|
8
|
+
s.authors = ["Cyberarm"]
|
9
|
+
s.email = ["matthewlikesrobots@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/cyberarm/rewrite-gameoverseer"
|
11
|
+
s.summary = "Generic game server."
|
12
|
+
s.description = "GameOverseer is designed to simplify the making of multiplayer games by providing a way to simply and easily write a game server."
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib", "bin"]
|
18
|
+
|
19
|
+
s.add_runtime_dependency "gosu"
|
20
|
+
s.add_runtime_dependency "celluloid"
|
21
|
+
s.add_runtime_dependency "renet"
|
22
|
+
s.add_runtime_dependency "multi_json"
|
23
|
+
|
24
|
+
# s.add_development_dependency "simplecov"
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class ChannelManager
|
3
|
+
CHAT = 0
|
4
|
+
WORLD= 1
|
5
|
+
HANDSHAKE = 2
|
6
|
+
FAULT=3
|
7
|
+
def initialize
|
8
|
+
@channels = {}
|
9
|
+
ChannelManager.instance = self # quick and lazy way to remove objectspace
|
10
|
+
# 'chat' => GameOverseer::InternalService::Chat,
|
11
|
+
# 'handshake' => GameOverseer::InternalService::Handshake,
|
12
|
+
# 'broadcast' => GameOverseer::InternalService::Broadcast,
|
13
|
+
# 'environment' => GameOverseer::InternalService::Environment
|
14
|
+
end
|
15
|
+
|
16
|
+
def register_channel(channel, service)
|
17
|
+
_channel = channel.downcase
|
18
|
+
unless @channels[_channel]
|
19
|
+
@channels[_channel] = service
|
20
|
+
GameOverseer::Console.log("ChannelManager> mapped '#{_channel}' to '#{service.class}'.")
|
21
|
+
else
|
22
|
+
raise "Could not map channel '#{_channel}' because '#{@channels[data[_channel]].class}' is already using it."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_to_service(data, client_id)
|
27
|
+
GameOverseer::Console.log("ChannelManager> sent '#{data}' to '#{@channels[data['channel']].class}'.")
|
28
|
+
@channels[data['channel']].client_id = client_id
|
29
|
+
@channels[data['channel']].process(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.instance
|
33
|
+
@instance
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.instance=_instance
|
37
|
+
@instance = _instance
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class ClientManager
|
3
|
+
attr_accessor :clients
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
ClientManager.instance = self
|
7
|
+
@clients = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(client_id, ip_address)
|
11
|
+
@clients << {client_id: client_id, ip_address: ip_address}
|
12
|
+
GameOverseer::Services.client_connected(client_id, ip_address)
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(client_id, key, value)
|
16
|
+
@clients.each_with_index do |hash, index|
|
17
|
+
if hash[:client_id] == client_id
|
18
|
+
hash[key] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove(client_id)
|
24
|
+
@clients.each_with_index do |hash, index|
|
25
|
+
if hash[:client_id] == client_id
|
26
|
+
@clients.delete(hash)
|
27
|
+
GameOverseer::Services.client_disconnected(client_id)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.instance
|
33
|
+
@instance
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.instance=_instance
|
37
|
+
@instance = _instance
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class Console < Gosu::Window
|
3
|
+
include Celluloid
|
4
|
+
# TODO: Use Gosu::Window.record to lower number of objects that need to be updated
|
5
|
+
|
6
|
+
PENDING_LOG = []
|
7
|
+
def initialize
|
8
|
+
GameOverseer::Console.instance = self
|
9
|
+
super(720, 480, false)
|
10
|
+
$window = self
|
11
|
+
$window.caption = "GameOverseer Console"
|
12
|
+
$window.text_input = Gosu::TextInput.new
|
13
|
+
|
14
|
+
@default_text_instance = Gosu::Font.new($window, 'Consolas', 18)
|
15
|
+
@messages = []
|
16
|
+
setup_ui
|
17
|
+
end
|
18
|
+
|
19
|
+
def needs_cursor?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def draw
|
24
|
+
@ui_image.draw(0,0,0) if defined?(@ui_image)
|
25
|
+
end
|
26
|
+
|
27
|
+
def update
|
28
|
+
update_ui
|
29
|
+
@ui_image = $window.record(720, 480) {draw_ui}
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_ui
|
33
|
+
@current_text = text_instance
|
34
|
+
@current_text_x = 4
|
35
|
+
|
36
|
+
# Required first message
|
37
|
+
@messages << {
|
38
|
+
text: '',
|
39
|
+
instance: text_instance,
|
40
|
+
color: Gosu::Color::WHITE,
|
41
|
+
x: 4,
|
42
|
+
y: 480-26-18,
|
43
|
+
z: 1
|
44
|
+
}
|
45
|
+
|
46
|
+
submit_text("#{Time.now.strftime('%c')}", false)
|
47
|
+
end
|
48
|
+
|
49
|
+
def draw_ui
|
50
|
+
draw_rect(0,0, 720, 26, Gosu::Color.rgb(200, 75, 25))
|
51
|
+
draw_rect(0,454, 720, 480, Gosu::Color::WHITE)
|
52
|
+
text_instance.draw("GameOverSeer Console. GameOverseer version #{GameOverSeer::VERSION} #{GameOverSeer::RELEASE_NAME} #{@messages.count}", 4, 4, 3)
|
53
|
+
@current_text.draw("#{$window.text_input.text}", @current_text_x, 458, 3, 1, 1, Gosu::Color::BLACK)
|
54
|
+
draw_rect(@caret+@current_text_x, 456, 2.0+@caret+@current_text_x, 474, Gosu::Color::BLUE, 4) if defined?(@caret) && @render_caret
|
55
|
+
|
56
|
+
@messages.each do |message|
|
57
|
+
message[:instance].draw(message[:text],message[:x],message[:y],message[:z], 1, 1, message[:color])
|
58
|
+
p message[:color] unless message[:color] == Gosu::Color::WHITE
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_ui
|
63
|
+
PENDING_LOG.each do |log_message|
|
64
|
+
submit_text(log_message, false) if log_message.strip.length > 0
|
65
|
+
PENDING_LOG.delete(log_message)
|
66
|
+
end
|
67
|
+
|
68
|
+
@caret = @current_text.text_width($window.text_input.text[0...$window.text_input.caret_pos])
|
69
|
+
|
70
|
+
@caret_tick = 0 unless defined?(@caret_tick)
|
71
|
+
@render_caret = true if @caret_tick < 15
|
72
|
+
@render_caret = false if @caret_tick > 30
|
73
|
+
|
74
|
+
@caret_tick = 0 unless @caret_tick < 45
|
75
|
+
@caret_tick+=1
|
76
|
+
|
77
|
+
value = @current_text.text_width($window.text_input.text)+@current_text_x
|
78
|
+
if value >= 720
|
79
|
+
@current_text_x-=4
|
80
|
+
elsif value <= 715
|
81
|
+
@current_text_x+=4 unless @current_text_x >= 4
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def text_instance
|
86
|
+
@default_text_instance
|
87
|
+
end
|
88
|
+
|
89
|
+
def draw_rect(x1,y1, x2,y2, color = Gosu::Color::GRAY, z = 2)
|
90
|
+
$window.draw_quad(x1, y1, color, x2, y1, color, x1, y2, color, x2, y2, color, z)
|
91
|
+
end
|
92
|
+
|
93
|
+
def scroll(direction)
|
94
|
+
case direction
|
95
|
+
when :down
|
96
|
+
if @messages.last[:y] >= 480 - 26 - 18
|
97
|
+
@messages.each do |message|
|
98
|
+
message[:y]-=18
|
99
|
+
end
|
100
|
+
end
|
101
|
+
when :up
|
102
|
+
if @messages.first[:y] <= 26#<= 480 - 26 - 32
|
103
|
+
@messages.each do |message|
|
104
|
+
message[:y]+=18
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def clean_messages(count)
|
111
|
+
if @messages.count >= count
|
112
|
+
@messages.delete(@messages.first)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def button_up(id)
|
117
|
+
case id
|
118
|
+
when 41 # Escape
|
119
|
+
# Quit?
|
120
|
+
when 40 # Enter
|
121
|
+
submit_text($window.text_input.text)
|
122
|
+
when 88 # Numpad Enter
|
123
|
+
submit_text($window.text_input.text)
|
124
|
+
when 259 # Mouse wheel
|
125
|
+
scroll(:up)
|
126
|
+
when 260 # Mouse wheel
|
127
|
+
scroll(:down)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.instance
|
132
|
+
@instance
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.instance=_instance
|
136
|
+
@instance = _instance
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.log(string)
|
140
|
+
self.log_it(string) if string.strip.length > 0
|
141
|
+
begin
|
142
|
+
GameOverseer::Console.instance.submit_text(string, false)
|
143
|
+
rescue NoMethodError
|
144
|
+
self.defer_log(string)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.log_with_color(string, color = Gosu::Color::WHITE)
|
149
|
+
self.log_it(string) if string.strip.length > 0
|
150
|
+
GameOverseer::Console.instance.submit_text(string, false, color)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.defer_log(string)
|
154
|
+
PENDING_LOG << string
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.log_it(string)
|
158
|
+
puts string
|
159
|
+
retry_limit = 0
|
160
|
+
begin
|
161
|
+
@log_file = File.open("#{Dir.pwd}/logs/log-#{Time.now.strftime('%B-%d-%Y')}.txt", 'a+') unless defined? @log_file
|
162
|
+
@log_file.write "[#{Time.now.strftime('%c')}] #{string}\n"
|
163
|
+
rescue Errno::ENOENT
|
164
|
+
Dir.mkdir("#{Dir.pwd}/logs") unless File.exist?("#{Dir.pwd}/logs") && File.directory?("#{Dir.pwd}/logs")
|
165
|
+
retry_limit+=1
|
166
|
+
retry unless retry_limit >= 2
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
protected
|
171
|
+
def submit_text(text, from_console = true, color = Gosu::Color::WHITE)
|
172
|
+
if text.strip.length > 0
|
173
|
+
clean_messages(300)
|
174
|
+
text = "Console> #{text}" if from_console
|
175
|
+
GameOverseer::Console.log_it(text)
|
176
|
+
if text.length > 83
|
177
|
+
temp_text = text[0..83]
|
178
|
+
@messages.each do |message|
|
179
|
+
message[:y]-=18
|
180
|
+
end
|
181
|
+
@messages << {
|
182
|
+
text: temp_text,
|
183
|
+
instance: text_instance,
|
184
|
+
color: color,
|
185
|
+
x: 4,
|
186
|
+
y: @messages.last[:y] + 18,
|
187
|
+
z: 1
|
188
|
+
}
|
189
|
+
submit_text(text[83..text.length], false)
|
190
|
+
else
|
191
|
+
@messages.each do |message|
|
192
|
+
message[:y]-=18
|
193
|
+
end
|
194
|
+
@messages << {
|
195
|
+
text: text,
|
196
|
+
instance: text_instance,
|
197
|
+
color: color,
|
198
|
+
x: 4,
|
199
|
+
y: @messages.last[:y] + 18,
|
200
|
+
z: 1
|
201
|
+
}
|
202
|
+
end
|
203
|
+
$window.text_input = Gosu::TextInput.new if from_console
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class InputHandler
|
3
|
+
def self.process_data(data, client_id)
|
4
|
+
@data = data
|
5
|
+
@client_id = client_id
|
6
|
+
forward_to_channel_manager if data_valid?
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.data_valid?
|
10
|
+
if @data["channel"]
|
11
|
+
if @data["mode"]
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.forward_to_channel_manager
|
18
|
+
count = 0
|
19
|
+
begin
|
20
|
+
channel_manager = GameOverseer::ChannelManager.instance
|
21
|
+
channel_manager.send_to_service(@data, @client_id)
|
22
|
+
rescue NoMethodError => e
|
23
|
+
GameOverseer::Console.log("InputHandler> #{e.to_s}")
|
24
|
+
raise if count >=2
|
25
|
+
count+=1
|
26
|
+
retry unless count >= 2
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class MessageManager
|
3
|
+
MESSAGES = []
|
4
|
+
BROADCASTS = []
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
MessageManager.instance = self
|
8
|
+
end
|
9
|
+
|
10
|
+
def message(client_id, string, reliable = false, channel = ChannelManager::CHAT)
|
11
|
+
GameOverseer::ENetServer.instance.send(client_id, string, reliable, channel)
|
12
|
+
GameOverseer::Console.log("MessageManager> #{string}-#{client_id}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def broadcast(string, reliable = false, channel = ChannelManager::CHAT)
|
16
|
+
GameOverseer::ENetServer.instance.broadcast(string, reliable, channel)
|
17
|
+
GameOverseer::Console.log("MessageManager> #{string}-#{channel}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def messages
|
21
|
+
MESSAGES
|
22
|
+
end
|
23
|
+
|
24
|
+
def broadcasts
|
25
|
+
BROADCASTS
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.instance
|
29
|
+
@instance
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.instance=_instance
|
33
|
+
@instance = _instance
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class HandShake
|
3
|
+
|
4
|
+
# OpenSSL Public/Private Key Encryption For Initial Handshaking And Authentication Masking.
|
5
|
+
def self.generate
|
6
|
+
@keys = OpenSSL::PKey::RSA.new 512
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.public_key
|
10
|
+
generate unless defined?(@keys)
|
11
|
+
@keys.public_key.to_pem
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.keys
|
15
|
+
generate unless defined?(@keys)
|
16
|
+
@keys
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class ENetServer
|
3
|
+
include Celluloid
|
4
|
+
|
5
|
+
def initialize(host, port)
|
6
|
+
GameOverseer::Console.log("Server> Started on: #{host}:#{port}.")
|
7
|
+
GameOverseer::Services.enable
|
8
|
+
GameOverseer::ENetServer.instance = self
|
9
|
+
|
10
|
+
@message_manager = GameOverseer::MessageManager.instance
|
11
|
+
@channel_manager = GameOverseer::ChannelManager.instance
|
12
|
+
@client_manager = GameOverseer::ClientManager.instance
|
13
|
+
|
14
|
+
@server = ENet::Server.new(port, 4, 4, 0, 0) # Port, max clients, channels, download bandwidth, upload bandwith
|
15
|
+
@server.use_compression(true)
|
16
|
+
|
17
|
+
@server.on_connection(method(:on_connect))
|
18
|
+
@server.on_packet_receive(method(:on_packet))
|
19
|
+
@server.on_disconnection(method(:on_disconnect))
|
20
|
+
|
21
|
+
async.run
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
loop do
|
26
|
+
@server.update(1000)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_packet(client_id, data, channel)
|
31
|
+
p "Packet: #{client_id}-#{data}-#{channel}"
|
32
|
+
handle_connection(client_id, data, channel)
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_connect(client_id, ip_address)
|
36
|
+
p "Connect: #{client_id}-#{ip_address}"
|
37
|
+
@client_manager.add(client_id, ip_address)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_disconnect(client_id)
|
41
|
+
p "Disconnect: #{client_id}"
|
42
|
+
@client_manager.remove(client_id)
|
43
|
+
end
|
44
|
+
|
45
|
+
# send (private packet)
|
46
|
+
def send(client_id, message, reliable = false, channel = ChannelManager::CHAT)
|
47
|
+
@server.send_packet(client_id, message, reliable, channel)
|
48
|
+
end
|
49
|
+
|
50
|
+
# boardcast (global packet)
|
51
|
+
def broadcast(message, reliable = false, channel = ChannelManager::CHAT)
|
52
|
+
@server.broadcast_packet(message, reliable, channel)
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_data(data, client_id)
|
56
|
+
GameOverseer::InputHandler.process_data(data, client_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_connection(client_id, data, channel)
|
60
|
+
begin
|
61
|
+
data = MultiJson.load(data)
|
62
|
+
process_data(data, client_id)
|
63
|
+
rescue MultiJson::ParseError => e
|
64
|
+
send(client_id, " \"channel\": \"__UNDEFINED__\", \"mode\": \"__UNDEFINED__\", \"data\": {\"code\": 400, \"message\": \"Invalid JSON received.\"}}", true, ChannelManager::FAULT)
|
65
|
+
GameOverseer::Console.log("Server> Parse error: '#{e.to_s}'. Bad data: '#{data}' received from client.")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.instance
|
70
|
+
@instance
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.instance=(_instance)
|
74
|
+
@instance = _instance
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class ENetServerRunner
|
79
|
+
attr_reader :supervisor
|
80
|
+
def start(host, port)
|
81
|
+
@supervisor = GameOverseer::ENetServer.new(host, port)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class InternalService
|
3
|
+
class Handshake < GameOverseer::Service
|
4
|
+
def setup
|
5
|
+
channel_manager.register_channel('handshake', self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def process(data)
|
9
|
+
data_to_method(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def extend_hand(data)
|
13
|
+
message = MultiJson.dump({channel: 'handshake', mode: 'public_key', data: {public_key: GameOverseer::HandShake.public_key}})
|
14
|
+
message_manager.message("#{message}", client_id, true, ChannelManager::HANDSHAKE)
|
15
|
+
log("#{self.class}> #{message}.", Gosu::Color::RED)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def version
|
21
|
+
"0.1.0"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
class Service
|
3
|
+
attr_accessor :client_id
|
4
|
+
|
5
|
+
def self.inherited(subclass)
|
6
|
+
GameOverseer::Console.log "Service> added '#{subclass}' to Services::List."
|
7
|
+
Services.register(subclass)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
if defined?(self.setup)
|
12
|
+
@client_id = 0
|
13
|
+
setup
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Called before 'enable' there should be no active code here, only setup variables.
|
18
|
+
def setup
|
19
|
+
end
|
20
|
+
|
21
|
+
# Called when services are first initialized, put active code here.
|
22
|
+
def enable
|
23
|
+
end
|
24
|
+
|
25
|
+
# Called when a message is recieved for this channel.
|
26
|
+
def process(data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def version
|
30
|
+
raise "Method 'version' on class '#{self}' not defined, see '#{__FILE__}#version' in GameOverseer source."
|
31
|
+
# Please use the sematic versioning system,
|
32
|
+
# http://semver.org
|
33
|
+
#
|
34
|
+
# e.g.
|
35
|
+
# "1.5.9"
|
36
|
+
# (Major.Minor.Patch)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
def channel_manager
|
41
|
+
@channel_manager = ChannelManager.instance
|
42
|
+
@channel_manager
|
43
|
+
end
|
44
|
+
|
45
|
+
def message_manager
|
46
|
+
@message_manager = MessageManager.instance# ObjectSpace.each_object(GameOverseer::MessageManager).first unless defined?(@message_manager)
|
47
|
+
@message_manager
|
48
|
+
end
|
49
|
+
|
50
|
+
def data_to_method(data)
|
51
|
+
self.send(data['mode'], data)
|
52
|
+
[self.methods - Class.methods].each do |method|
|
53
|
+
if data['mode'] == method.to_s
|
54
|
+
self.send(data['mode'], data)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def log(string, color = Gosu::Color::RED)
|
60
|
+
GameOverseer::Console.log_with_color(string, color)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GameOverseer
|
2
|
+
module Services
|
3
|
+
LIST = []
|
4
|
+
ACTIVE=[]
|
5
|
+
|
6
|
+
def self.register(klass)
|
7
|
+
LIST << klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.enable
|
11
|
+
LIST.each do |service|
|
12
|
+
_service = service.new
|
13
|
+
_service.enable
|
14
|
+
GameOverseer::Console.log "Services> #{_service.class} #{_service.version}"
|
15
|
+
ACTIVE << _service
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.client_connected(client_id, ip_address)
|
20
|
+
ACTIVE.each do |service|
|
21
|
+
service.client_connected(client_id, ip_address) if defined?(service.client_connected)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.client_disconnected(client_id)
|
26
|
+
ACTIVE.each do |service|
|
27
|
+
service.client_disconnected(client_id) if defined?(service.client_disconnected)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/gameoverseer.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "gosu"
|
4
|
+
require "celluloid"
|
5
|
+
require "renet"
|
6
|
+
require "multi_json"
|
7
|
+
|
8
|
+
require_relative "gamoverseer/version"
|
9
|
+
|
10
|
+
require_relative "gamoverseer/console/console"
|
11
|
+
|
12
|
+
require_relative "gamoverseer/channels/channel_manager"
|
13
|
+
|
14
|
+
require_relative "gamoverseer/messages/message_manager"
|
15
|
+
|
16
|
+
require_relative "gamoverseer/clients/client_manager"
|
17
|
+
|
18
|
+
require_relative "gamoverseer/services/service"
|
19
|
+
require_relative "gamoverseer/services/services"
|
20
|
+
require_relative "gamoverseer/services/internal/services"
|
21
|
+
|
22
|
+
require_relative "gamoverseer/input_handler/input_handler"
|
23
|
+
|
24
|
+
require_relative "gamoverseer/server/renet_server"
|
25
|
+
require_relative "gamoverseer/server/handshake"
|
26
|
+
|
27
|
+
# TEMP
|
28
|
+
Thread.abort_on_exception = true
|
29
|
+
|
30
|
+
# TODO: Move to own file
|
31
|
+
module GameOverseer
|
32
|
+
def self.activate(host, port)
|
33
|
+
GameOverseer::ChannelManager.new
|
34
|
+
GameOverseer::MessageManager.new
|
35
|
+
GameOverseer::ClientManager.new
|
36
|
+
|
37
|
+
# @console = GameOverseer::Console.new
|
38
|
+
@server = GameOverseer::ENetServerRunner.new
|
39
|
+
|
40
|
+
# Thread.new {@console.show}
|
41
|
+
Thread.new {@server.start(host, port)}
|
42
|
+
sleep
|
43
|
+
|
44
|
+
at_exit do
|
45
|
+
# GameOverseer::Console.instance.close
|
46
|
+
@server.supervisor.terminate if defined?(@server.supervisor.terminate)
|
47
|
+
puts "Server Shutdown"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.deactivate
|
52
|
+
puts "ALERT \"CONSOLE CLOSED. LOST CONTROL OF SERVER.\""
|
53
|
+
@server.supervisor.terminate
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Activate self, remove this before a release.
|
58
|
+
# GameOverseer.activate("localhost", 56789)
|
data/license.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2014 cyberarm
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
|
3
|
+
PROTOCOLS = {
|
4
|
+
# Client
|
5
|
+
handshake_extend_hand: MultiJson.dump({'channel' => 'handshake', 'mode' => 'extend_hand'}),
|
6
|
+
handshake_authenticate: "{'channel'=> 'handshake', \"mode\": \"authenticate\", \"data\": {\"auth_key\": \"public_key_encrypted_auth_key\"}}",
|
7
|
+
|
8
|
+
|
9
|
+
# Server
|
10
|
+
handshake_public_key: "{\"channel\": \"handshake\", \"mode\": \"public_key\", \"data\": {\"public_key\": \"really_long_string\"}}",
|
11
|
+
handshake_authenticated: "{\"channel\": \"handshake\", \"mode\": \"authenticated\", \"data\": {\"code\": 200, \"message\": \"Successfully authenticated.\"}}",
|
12
|
+
handshake_bad_authentication: "{\"channel\": \"handshake\", \"mode\": \"bad_authentication\", \"data\": {\"code\": 401, \"message\": \"Could not authenticate.\"}}"
|
13
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "celluloid/io"
|
2
|
+
require "multi_json"
|
3
|
+
require "net/ssh"
|
4
|
+
require_relative "protocol-lib"
|
5
|
+
|
6
|
+
class TestClientAlpha
|
7
|
+
include Celluloid::IO
|
8
|
+
finalizer :finish
|
9
|
+
|
10
|
+
def initialize(host, port)
|
11
|
+
@client = UDPSocket.new
|
12
|
+
@client.connect(host, port)
|
13
|
+
run
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
@client.send("#{PROTOCOLS[:handshake_extend_hand]}", 0)
|
18
|
+
socket = @client.recvfrom(1024)
|
19
|
+
response = MultiJson.load(socket[0])
|
20
|
+
public_key_pem = response['data']['public_key'] if response['mode'] == 'public_key'
|
21
|
+
public_key = OpenSSL::PKey::RSA.new public_key_pem
|
22
|
+
encrypted_auth_key_response = MultiJson.dump({'channel'=> 'handshake', 'mode' => 'authenticate', 'data' => {'auth_key' => "#{public_key.public_encrypt('HELLO_WORLD')}"}})
|
23
|
+
p encrypted_auth_key_response
|
24
|
+
@client.send("#{encrypted_auth_key_response}", 0)
|
25
|
+
end
|
26
|
+
|
27
|
+
def finish
|
28
|
+
@client.close if @client
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
start_time = Time.now
|
33
|
+
|
34
|
+
TestClientAlpha.supervise('localhost', 56789)
|
35
|
+
|
36
|
+
puts "Time to complete: #{Time.now-start_time}"
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "celluloid/io"
|
2
|
+
require "multi_json"
|
3
|
+
require "net/ssh"
|
4
|
+
require 'uri'
|
5
|
+
require_relative "protocol-lib"
|
6
|
+
|
7
|
+
class TestClientBeta
|
8
|
+
include Celluloid::IO
|
9
|
+
finalizer :finish
|
10
|
+
|
11
|
+
def initialize(host, port)
|
12
|
+
@client = Celluloid::IO::TCPSocket.new(host, port)
|
13
|
+
run
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
@client.puts("#{PROTOCOLS[:handshake_extend_hand]}")
|
18
|
+
line = @client.gets
|
19
|
+
p line
|
20
|
+
response = MultiJson.load(line)
|
21
|
+
public_key_pem = response['data']['public_key'] if response['mode'] == 'public_key'
|
22
|
+
public_key = OpenSSL::PKey::RSA.new public_key_pem
|
23
|
+
encrypted_auth_key_response = MultiJson.dump({'channel'=> 'handshake', 'mode' => 'authenticate', 'data' => {'auth_key' => "#{public_key.public_encrypt('HELLO_WORLD')}".force_encoding("utf-8")}})
|
24
|
+
p encrypted_auth_key_response
|
25
|
+
@client.puts("#{encrypted_auth_key_response.inspect}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def finish
|
29
|
+
@client.close if @client
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
start_time = Time.now
|
34
|
+
|
35
|
+
TestClientBeta.supervise("127.0.0.1", 56789)
|
36
|
+
|
37
|
+
puts "Time to complete: #{Time.now-start_time}"
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
require "net/ssh"
|
3
|
+
require 'uri'
|
4
|
+
require "renet"
|
5
|
+
require_relative "protocol-lib"
|
6
|
+
|
7
|
+
class TestClientGamma
|
8
|
+
HANDSHAKE = 2
|
9
|
+
# include Celluloid
|
10
|
+
|
11
|
+
def initialize(host, port)
|
12
|
+
@client = ENet::Connection.new(host, port, 4, 0, 0)
|
13
|
+
|
14
|
+
@client.on_connection(method(:on_connect))
|
15
|
+
@client.on_packet_receive(method(:on_packet))
|
16
|
+
@client.on_disconnection(method(:on_disconnect))
|
17
|
+
|
18
|
+
@client.connect(2000)
|
19
|
+
@client.send_packet("#{PROTOCOLS[:handshake_extend_hand]}", true, HANDSHAKE)
|
20
|
+
run
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
loop do
|
25
|
+
@client.update(0)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_packet(data, channel)
|
30
|
+
p "P: #{data}-#{channel}"
|
31
|
+
handle_connection(data, channel)
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_connect(data = 0, channel = 0)
|
35
|
+
p "C: #{data}-#{channel}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_disconnect(data = 0, channel = 0)
|
39
|
+
p "D: #{data}-#{channel}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_connection(data, channel)
|
43
|
+
response = MultiJson.load(data)
|
44
|
+
case response['channel']
|
45
|
+
when 'handshake'
|
46
|
+
public_key_pem = response['data']['public_key'] if response['mode'] == 'public_key'
|
47
|
+
public_key = OpenSSL::PKey::RSA.new public_key_pem
|
48
|
+
encrypted_auth_key_response = MultiJson.dump({'channel'=> 'handshake', 'mode' => 'authenticate', 'data' => {'auth_key' => "#{public_key.public_encrypt('HELLO_WORLD')}".force_encoding("utf-8")}})
|
49
|
+
@client.send_packet(encrypted_auth_key_response, true, HANDSHAKE)
|
50
|
+
puts "PAST"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
start_time = Time.now
|
56
|
+
|
57
|
+
TestClientGamma.new("localhost", 56789)
|
58
|
+
|
59
|
+
puts "Time to complete: #{Time.now-start_time}"
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gameoverseer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cyberarm
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gosu
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: celluloid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: renet
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: multi_json
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: GameOverseer is designed to simplify the making of multiplayer games
|
70
|
+
by providing a way to simply and easily write a game server.
|
71
|
+
email:
|
72
|
+
- matthewlikesrobots@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- Gemfile.lock
|
80
|
+
- README.md
|
81
|
+
- gameoverseer.gemspec
|
82
|
+
- lib/gameoverseer.rb
|
83
|
+
- lib/gameoverseer/channels/channel_manager.rb
|
84
|
+
- lib/gameoverseer/clients/client_manager.rb
|
85
|
+
- lib/gameoverseer/console/console.rb
|
86
|
+
- lib/gameoverseer/input_handler/input_handler.rb
|
87
|
+
- lib/gameoverseer/messages/message_manager.rb
|
88
|
+
- lib/gameoverseer/server/handshake.rb
|
89
|
+
- lib/gameoverseer/server/renet_server.rb
|
90
|
+
- lib/gameoverseer/services/internal/broadcast.rb
|
91
|
+
- lib/gameoverseer/services/internal/chat.rb
|
92
|
+
- lib/gameoverseer/services/internal/environment.rb
|
93
|
+
- lib/gameoverseer/services/internal/handshake.rb
|
94
|
+
- lib/gameoverseer/services/internal/services.rb
|
95
|
+
- lib/gameoverseer/services/service.rb
|
96
|
+
- lib/gameoverseer/services/services.rb
|
97
|
+
- lib/gameoverseer/version.rb
|
98
|
+
- license.md
|
99
|
+
- test-clients/protocol-lib.rb
|
100
|
+
- test-clients/test-client-alpha.rb
|
101
|
+
- test-clients/test-client-beta.rb
|
102
|
+
- test-clients/test-client-gamma.rb
|
103
|
+
homepage: https://github.com/cyberarm/rewrite-gameoverseer
|
104
|
+
licenses: []
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
- bin
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.2.2
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Generic game server.
|
127
|
+
test_files: []
|
128
|
+
has_rdoc:
|