game_2d 0.0.1 → 0.0.2
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/README.md +18 -5
- data/bin/game_2d_client.rb +3 -2
- data/lib/game_2d/client_connection.rb +75 -20
- data/lib/game_2d/client_engine.rb +35 -34
- data/lib/game_2d/complex_move.rb +1 -1
- data/lib/game_2d/encryption.rb +35 -0
- data/lib/game_2d/entity.rb +47 -31
- data/lib/game_2d/entity/block.rb +2 -6
- data/lib/game_2d/entity/destination.rb +17 -0
- data/lib/game_2d/entity/owned_entity.rb +3 -2
- data/lib/game_2d/entity/pellet.rb +0 -8
- data/lib/game_2d/entity/teleporter.rb +46 -0
- data/lib/game_2d/game.rb +82 -23
- data/lib/game_2d/game_client.rb +360 -0
- data/lib/game_2d/game_space.rb +104 -18
- data/lib/game_2d/game_window.rb +17 -216
- data/lib/game_2d/menu.rb +3 -3
- data/lib/game_2d/message.rb +37 -0
- data/lib/game_2d/move/rise_up.rb +2 -2
- data/lib/game_2d/password_dialog.rb +44 -0
- data/lib/game_2d/player.rb +15 -18
- data/lib/game_2d/registerable.rb +1 -1
- data/lib/game_2d/serializable.rb +2 -13
- data/lib/game_2d/server_connection.rb +56 -24
- data/lib/game_2d/server_port.rb +14 -8
- data/lib/game_2d/transparency.rb +59 -0
- data/lib/game_2d/version.rb +1 -1
- data/lib/game_2d/zorder.rb +1 -1
- data/media/destination.png +0 -0
- data/media/destination.xcf +0 -0
- data/spec/client_engine_spec.rb +162 -71
- data/spec/game_space_spec.rb +8 -9
- metadata +11 -2
data/lib/game_2d/menu.rb
CHANGED
@@ -9,7 +9,7 @@ class Menu
|
|
9
9
|
@right = window.width - 1
|
10
10
|
@choices.each_with_index do |choice, num|
|
11
11
|
choice.x = @right
|
12
|
-
choice.y = (num + 2) *
|
12
|
+
choice.y = (num + 2) * @font.height
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -17,7 +17,7 @@ class Menu
|
|
17
17
|
str = to_s
|
18
18
|
@font.draw_rel(str, @window.width - 1, 0, ZOrder::Text, 1.0, 0.0, 1.0, 1.0,
|
19
19
|
@main_color)
|
20
|
-
x1, x2, y, c = @right - @font.text_width(str), @right,
|
20
|
+
x1, x2, y, c = @right - @font.text_width(str), @right, @font.height, @main_color
|
21
21
|
@window.draw_box_at(x1, y, x2, y+1, @main_color)
|
22
22
|
@choices.each(&:draw)
|
23
23
|
end
|
@@ -53,7 +53,7 @@ class MenuItem
|
|
53
53
|
def left; @x - @font.text_width(to_s); end
|
54
54
|
def right; @x; end
|
55
55
|
def top; @y; end
|
56
|
-
def bottom; @y +
|
56
|
+
def bottom; @y + @font.height; end
|
57
57
|
|
58
58
|
def draw
|
59
59
|
selected = mouse_over?
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'game_2d/zorder'
|
3
|
+
|
4
|
+
class Message
|
5
|
+
def initialize(window, font, lines)
|
6
|
+
@window, @font, @lines = window, font, lines
|
7
|
+
|
8
|
+
@fg_color, @bg_color = Gosu::Color::YELLOW, Gosu::Color::BLACK
|
9
|
+
@drawn = false
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :lines
|
13
|
+
def lines=(new_lines)
|
14
|
+
@lines, @drawn = new_lines, false
|
15
|
+
end
|
16
|
+
|
17
|
+
def draw
|
18
|
+
count = @lines.size
|
19
|
+
line_height = @font.height
|
20
|
+
lines_height = line_height * count
|
21
|
+
lines_top = (@window.height - (count * line_height)) / 2
|
22
|
+
x_center = @window.width / 2
|
23
|
+
@lines.each_with_index do |line, n|
|
24
|
+
@font.draw_rel(line, x_center, lines_top + (line_height * n), ZOrder::Text,
|
25
|
+
0.5, 0.0, 1.0, 1.0, @fg_color)
|
26
|
+
end
|
27
|
+
max_width = @lines.collect {|line| @font.text_width(line)}.max
|
28
|
+
lines_bottom = lines_top + (line_height * count)
|
29
|
+
left = x_center - (max_width / 2)
|
30
|
+
right = x_center + (max_width / 2)
|
31
|
+
@window.draw_box_at(left - 1, lines_top - 1, right + 1, lines_bottom + 1, @fg_color)
|
32
|
+
@window.draw_box_at(left, lines_top, right, lines_bottom, @bg_color)
|
33
|
+
@drawn = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def drawn?; @drawn; end
|
37
|
+
end
|
data/lib/game_2d/move/rise_up.rb
CHANGED
@@ -64,8 +64,8 @@ class RiseUp < ComplexMove
|
|
64
64
|
:distance => @distance
|
65
65
|
end
|
66
66
|
def update_from_json(json)
|
67
|
-
self.stage = json[:stage].to_sym
|
68
|
-
self.distance = json[:distance]
|
67
|
+
self.stage = json[:stage].to_sym if json[:stage]
|
68
|
+
self.distance = json[:distance] if json[:distance]
|
69
69
|
super
|
70
70
|
end
|
71
71
|
def to_s
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require 'game_2d/message'
|
3
|
+
require 'game_2d/encryption'
|
4
|
+
|
5
|
+
class PasswordDialog < Message
|
6
|
+
include Encryption
|
7
|
+
|
8
|
+
PROMPT = 'Enter password:'
|
9
|
+
PRINTABLE_ASCII = (32..126).to_a.pack 'C*'
|
10
|
+
|
11
|
+
def initialize(window, font)
|
12
|
+
super(window, font, [PROMPT, '_'])
|
13
|
+
@text = @window.text_input = Gosu::TextInput.new
|
14
|
+
@draw_count = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def display_text
|
18
|
+
size = password.size
|
19
|
+
return '_' if size.zero?
|
20
|
+
rand_char = PRINTABLE_ASCII[
|
21
|
+
(@draw_count / 10) * 53 % PRINTABLE_ASCII.size
|
22
|
+
]
|
23
|
+
rand_char * size
|
24
|
+
end
|
25
|
+
|
26
|
+
def draw
|
27
|
+
@draw_count += 1
|
28
|
+
self.lines = [PROMPT, display_text]
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def enter
|
33
|
+
@window.text_input = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def password
|
37
|
+
@text.text
|
38
|
+
end
|
39
|
+
private :password
|
40
|
+
|
41
|
+
def password_hash
|
42
|
+
make_password_hash password
|
43
|
+
end
|
44
|
+
end
|
data/lib/game_2d/player.rb
CHANGED
@@ -30,7 +30,6 @@ class Player < Entity
|
|
30
30
|
@score = 0
|
31
31
|
@moves = []
|
32
32
|
@current_move = nil
|
33
|
-
@falling = false
|
34
33
|
@build_block_id = nil
|
35
34
|
@build_level = 0
|
36
35
|
@complex_move = nil
|
@@ -38,7 +37,12 @@ class Player < Entity
|
|
38
37
|
|
39
38
|
def sleep_now?; false; end
|
40
39
|
|
41
|
-
def
|
40
|
+
def underfoot
|
41
|
+
opaque(next_to(self.a + 180))
|
42
|
+
end
|
43
|
+
def falling?
|
44
|
+
underfoot.empty?
|
45
|
+
end
|
42
46
|
|
43
47
|
def build_block_id=(new_id)
|
44
48
|
@build_block_id = new_id.try(:to_sym)
|
@@ -56,13 +60,6 @@ class Player < Entity
|
|
56
60
|
build_block.owner_id = nil if building?
|
57
61
|
end
|
58
62
|
|
59
|
-
# Pellets don't hit the originating player
|
60
|
-
def transparent_to_me?(other)
|
61
|
-
super ||
|
62
|
-
(other == build_block) ||
|
63
|
-
(other.is_a?(Pellet) && other.owner == self)
|
64
|
-
end
|
65
|
-
|
66
63
|
def update
|
67
64
|
fail "No space set for #{self}" unless @space
|
68
65
|
check_for_disown_block
|
@@ -74,8 +71,7 @@ class Player < Entity
|
|
74
71
|
@complex_move = nil
|
75
72
|
end
|
76
73
|
|
77
|
-
|
78
|
-
if @falling = underfoot.empty?
|
74
|
+
if falling = falling?
|
79
75
|
self.a = 0
|
80
76
|
accelerate(0, 1)
|
81
77
|
end
|
@@ -83,7 +79,7 @@ class Player < Entity
|
|
83
79
|
args = @moves.shift
|
84
80
|
case (current_move = args.delete(:move).to_sym)
|
85
81
|
when :slide_left, :slide_right, :brake, :flip, :build, :rise_up
|
86
|
-
send current_move unless
|
82
|
+
send current_move unless falling
|
87
83
|
when :fire
|
88
84
|
fire args[:x_vel], args[:y_vel]
|
89
85
|
else
|
@@ -91,8 +87,9 @@ class Player < Entity
|
|
91
87
|
end if args
|
92
88
|
|
93
89
|
# Only go around corner if sitting on exactly one object
|
94
|
-
|
95
|
-
|
90
|
+
blocks_underfoot = underfoot
|
91
|
+
if blocks_underfoot.size == 1
|
92
|
+
other = blocks_underfoot.first
|
96
93
|
# Figure out where corner is and whether we're about to reach or pass it
|
97
94
|
corner, distance, overshoot, turn = going_past_entity(other.x, other.y)
|
98
95
|
if corner
|
@@ -230,10 +227,10 @@ class Player < Entity
|
|
230
227
|
end
|
231
228
|
|
232
229
|
def update_from_json(json)
|
233
|
-
@player_name = json[:player_name]
|
234
|
-
@score = json[:score]
|
235
|
-
@build_block_id = json[:build_block].try(:to_sym)
|
236
|
-
@complex_move = Serializable.from_json(json[:complex_move])
|
230
|
+
@player_name = json[:player_name] if json[:player_name]
|
231
|
+
@score = json[:score] if json[:score]
|
232
|
+
@build_block_id = json[:build_block].try(:to_sym) if json[:build_block]
|
233
|
+
@complex_move = Serializable.from_json(json[:complex_move]) if json[:complex_move]
|
237
234
|
super
|
238
235
|
end
|
239
236
|
|
data/lib/game_2d/registerable.rb
CHANGED
@@ -19,7 +19,7 @@ module Registerable
|
|
19
19
|
|
20
20
|
def registry_id=(id)
|
21
21
|
raise "#{self}: Already have ID #{@registry_id}, cannot set to #{id}" if @registry_id
|
22
|
-
raise "#{self}:
|
22
|
+
raise "#{self}: Nil ID" unless id
|
23
23
|
@registry_id = id.to_sym
|
24
24
|
end
|
25
25
|
end
|
data/lib/game_2d/serializable.rb
CHANGED
@@ -37,10 +37,9 @@ module Serializable
|
|
37
37
|
self.class.name
|
38
38
|
end
|
39
39
|
|
40
|
-
def self.from_json(json
|
40
|
+
def self.from_json(json)
|
41
41
|
return nil unless json
|
42
42
|
class_name = json[:class]
|
43
|
-
binding.pry unless class_name
|
44
43
|
raise "Suspicious class name: #{class_name}" unless
|
45
44
|
(class_name == 'Player') ||
|
46
45
|
(class_name.start_with? 'Entity::') ||
|
@@ -49,18 +48,8 @@ module Serializable
|
|
49
48
|
clazz = constant(class_name)
|
50
49
|
it = clazz.new
|
51
50
|
|
52
|
-
# A registry ID must be specified either in the JSON or by the caller, but
|
53
|
-
# not both
|
54
51
|
if it.is_a? Registerable
|
55
|
-
if
|
56
|
-
fail("Entity #{it} (from #{json.inspect}) already has " +
|
57
|
-
"ID #{it.registry_id}, cannot generate") if it.registry_id?
|
58
|
-
# Leave it nil - it will be populated when added to a space
|
59
|
-
else
|
60
|
-
it.registry_id = json[:registry_id]
|
61
|
-
end
|
62
|
-
elsif generate_id
|
63
|
-
fail("#{clazz} is not Registerable")
|
52
|
+
it.registry_id = json[:registry_id] if json[:registry_id]
|
64
53
|
end
|
65
54
|
|
66
55
|
it.update_from_json(json)
|
@@ -1,37 +1,59 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
2
4
|
require 'game_2d/hash'
|
5
|
+
require 'game_2d/encryption'
|
3
6
|
|
4
7
|
# An instance of this class is created by ServerPort whenever an
|
5
8
|
# incoming connection is accepted.
|
6
9
|
|
7
10
|
class ServerConnection
|
11
|
+
include Encryption
|
12
|
+
include Base64
|
8
13
|
|
9
14
|
def initialize(port, game, server, id, remote_addr)
|
10
15
|
@port, @game, @server, @id, @remote_addr = port, game, server, id, remote_addr
|
11
16
|
puts "ServerConnection: New connection #{id} from #{remote_addr}"
|
12
17
|
end
|
13
18
|
|
14
|
-
|
15
|
-
player_name = handshake[:player_name]
|
16
|
-
player = @game.add_player(player_name)
|
17
|
-
@player_id = player.registry_id
|
18
|
-
@port.register_player @player_id, self
|
19
|
+
attr_reader :id, :player_id
|
19
20
|
|
21
|
+
def answer_handshake(handshake)
|
22
|
+
@player_name = handshake[:player_name]
|
23
|
+
dh_public_key = handshake[:dh_public_key]
|
24
|
+
client_public_key = handshake[:client_public_key]
|
25
|
+
dh = OpenSSL::PKey::DH.new(dh_public_key)
|
26
|
+
dh.generate_key!
|
27
|
+
self.key = dh.compute_key(OpenSSL::BN.new client_public_key)
|
20
28
|
response = {
|
21
|
-
:
|
22
|
-
:world => {
|
23
|
-
:world_name => @game.world_name,
|
24
|
-
:world_id => @game.world_id,
|
25
|
-
:highest_id => @game.world_highest_id,
|
26
|
-
:cell_width => @game.world_cell_width,
|
27
|
-
:cell_height => @game.world_cell_height,
|
28
|
-
},
|
29
|
-
:add_players => @game.get_all_players,
|
30
|
-
:add_npcs => @game.get_all_npcs,
|
31
|
-
:at_tick => @game.tick,
|
29
|
+
:server_public_key => dh.pub_key.to_s
|
32
30
|
}
|
31
|
+
send_record response, true # answer reliably
|
32
|
+
end
|
33
|
+
|
34
|
+
def answer_login(b64_password_hash, b64_iv)
|
35
|
+
password_hash = decrypt(
|
36
|
+
strict_decode64(b64_password_hash),
|
37
|
+
strict_decode64(b64_iv))
|
38
|
+
player_data = @game.player_data(@player_name)
|
39
|
+
if player_data
|
40
|
+
unless password_hash == player_data[:password_hash]
|
41
|
+
$stderr.puts "Wrong password for #{@player_name} (#{password_hash} != #{player_data[:password_hash]})"
|
42
|
+
disconnect!
|
43
|
+
return
|
44
|
+
end
|
45
|
+
else # new player
|
46
|
+
@game.store_player_data @player_name, :password_hash => password_hash
|
47
|
+
end
|
48
|
+
|
49
|
+
player = @game.add_player(@player_name)
|
50
|
+
@player_id = player.registry_id
|
51
|
+
@port.register_player @player_id, self
|
33
52
|
puts "#{player} logs in from #{@remote_addr} at <#{@game.tick}>"
|
34
|
-
|
53
|
+
|
54
|
+
# We don't send the registry here. The Game will do it after
|
55
|
+
# all logins have been processed and the update has completed.
|
56
|
+
# Otherwise, we're sending an incomplete frame.
|
35
57
|
end
|
36
58
|
|
37
59
|
def player
|
@@ -64,6 +86,7 @@ class ServerConnection
|
|
64
86
|
end
|
65
87
|
|
66
88
|
def close
|
89
|
+
return unless @player_id
|
67
90
|
@port.deregister_player @player_id
|
68
91
|
toast = player
|
69
92
|
puts "#{toast} -- #{@remote_addr} disconnected at <#{@game.tick}>"
|
@@ -73,17 +96,22 @@ class ServerConnection
|
|
73
96
|
def on_packet(data, channel)
|
74
97
|
hash = JSON.parse(data).fix_keys
|
75
98
|
debug_packet('Received', hash)
|
76
|
-
if
|
99
|
+
if handshake = hash.delete(:handshake)
|
77
100
|
answer_handshake(handshake)
|
78
|
-
elsif (
|
101
|
+
elsif password_hash = hash.delete(:password_hash)
|
102
|
+
answer_login(password_hash, hash.delete(:iv))
|
103
|
+
elsif hash.delete(:save)
|
79
104
|
@game.save
|
80
|
-
elsif
|
105
|
+
elsif ping = hash.delete(:ping)
|
81
106
|
answer_ping ping
|
82
107
|
else
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
channel
|
108
|
+
if hash[:player_id] = @player_id
|
109
|
+
@game.add_player_action hash
|
110
|
+
# TODO: Validate
|
111
|
+
@port.broadcast_player_action hash, channel
|
112
|
+
else
|
113
|
+
$stderr.puts "Ignoring move #{hash.inspect}, no player_id for this connection"
|
114
|
+
end
|
87
115
|
end
|
88
116
|
end
|
89
117
|
|
@@ -101,4 +129,8 @@ class ServerConnection
|
|
101
129
|
keys = hash.keys - [:at_tick]
|
102
130
|
puts "#{direction} #{keys.join(', ')} <#{at_tick}>"
|
103
131
|
end
|
132
|
+
|
133
|
+
def disconnect!
|
134
|
+
@server.disconnect_client(@id)
|
135
|
+
end
|
104
136
|
end
|
data/lib/game_2d/server_port.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'set'
|
1
2
|
require 'renet'
|
2
3
|
require 'json'
|
3
4
|
require 'game_2d/server_connection'
|
@@ -10,6 +11,7 @@ class ServerPort
|
|
10
11
|
|
11
12
|
@clients = {}
|
12
13
|
@player_connections = {}
|
14
|
+
@new_players = Set.new
|
13
15
|
|
14
16
|
@server.on_connection method(:on_connection)
|
15
17
|
@server.on_packet_receive method(:on_packet_receive)
|
@@ -36,28 +38,32 @@ class ServerPort
|
|
36
38
|
puts "Remaining connection IDs: #{@clients.keys.sort.join(', ')}"
|
37
39
|
end
|
38
40
|
|
39
|
-
def broadcast(data, reliable=false, channel=1)
|
40
|
-
@server.broadcast_packet data.to_json, reliable, channel
|
41
|
-
@server.flush
|
42
|
-
end
|
43
|
-
|
44
41
|
def register_player(player_id, conn)
|
45
42
|
@player_connections[player_id] = conn
|
43
|
+
@new_players << player_id
|
46
44
|
end
|
47
45
|
|
48
46
|
def deregister_player(player_id)
|
49
47
|
@player_connections.delete player_id
|
50
48
|
end
|
51
49
|
|
50
|
+
def new_players
|
51
|
+
copy = @new_players.dup
|
52
|
+
@new_players.clear
|
53
|
+
copy
|
54
|
+
end
|
55
|
+
|
52
56
|
def player_connection(player_id)
|
53
57
|
@player_connections[player_id]
|
54
58
|
end
|
55
59
|
|
56
60
|
# Re-broadcast to everyone except the original sender
|
57
|
-
def broadcast_player_action(
|
61
|
+
def broadcast_player_action(hash, channel)
|
62
|
+
sender_player_id = hash[:player_id]
|
63
|
+
fail "No player_id in #{hash.inspect}" unless sender_player_id
|
58
64
|
data = hash.to_json
|
59
|
-
@
|
60
|
-
@server.send_packet(id, data, false, channel) unless
|
65
|
+
@player_connections.each do |player_id, conn|
|
66
|
+
@server.send_packet(conn.id, data, false, channel) unless player_id == sender_player_id
|
61
67
|
end
|
62
68
|
@server.flush
|
63
69
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'game_2d/entity'
|
2
|
+
require 'game_2d/entity/block'
|
3
|
+
require 'game_2d/entity/titanium'
|
4
|
+
require 'game_2d/entity/owned_entity'
|
5
|
+
require 'game_2d/entity/teleporter'
|
6
|
+
require 'game_2d/entity/destination'
|
7
|
+
require 'game_2d/wall'
|
8
|
+
|
9
|
+
module Transparency
|
10
|
+
def transparent?(one, two)
|
11
|
+
# Walls and titanium: transparent to absolutely nothing
|
12
|
+
return false if wall?(one) || wall?(two)
|
13
|
+
|
14
|
+
# Teleporter destinations: transparent to everything
|
15
|
+
return true if destination?(one) || destination?(two)
|
16
|
+
|
17
|
+
# Teleporters: transparent to everything except other
|
18
|
+
# teleporters, and destinations
|
19
|
+
return teleporter_ok?(one, two) if teleporter?(one)
|
20
|
+
return teleporter_ok?(two, one) if teleporter?(two)
|
21
|
+
|
22
|
+
# Owned entities are transparent to the owner, and other
|
23
|
+
# objects with the same owner
|
24
|
+
return related_by_owner?(one, two) if owned?(one)
|
25
|
+
return related_by_owner?(two, one) if owned?(two)
|
26
|
+
|
27
|
+
# Should only get here if both objects are players
|
28
|
+
fail("Huh? one=#{one}, two=#{two}") unless
|
29
|
+
one.is_a?(Player) && two.is_a?(Player)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def wall?(entity)
|
35
|
+
entity.is_a?(Wall) || entity.is_a?(Entity::Titanium)
|
36
|
+
end
|
37
|
+
|
38
|
+
def teleporter_ok?(tp, other)
|
39
|
+
!teleporter?(other)
|
40
|
+
end
|
41
|
+
|
42
|
+
def teleporter?(entity)
|
43
|
+
entity.is_a?(Entity::Teleporter)
|
44
|
+
end
|
45
|
+
|
46
|
+
def destination?(entity)
|
47
|
+
entity.is_a?(Entity::Destination)
|
48
|
+
end
|
49
|
+
|
50
|
+
def related_by_owner?(o, other)
|
51
|
+
return false unless o.owner
|
52
|
+
other.registry_id == o.owner_id ||
|
53
|
+
(other.is_a?(Entity::OwnedEntity) && other.owner_id == o.owner_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def owned?(entity)
|
57
|
+
entity.is_a? Entity::OwnedEntity
|
58
|
+
end
|
59
|
+
end
|