chingu 0.9rc7 → 0.9rc8

Sign up to get free protection for your applications and to get access to all the features.
@@ -22,18 +22,18 @@
22
22
  module Chingu
23
23
  module GameStates
24
24
  #
25
- # A game state that acts server in a multiplayer game, suitable for smaller/middle sized games.
25
+ # A game state that acts server in a multi-player game, suitable for smaller/middle sized games.
26
26
  # Used in combination with game state NetworkClient.
27
27
  #
28
- # Uses nonblocking polling TCP and YAML to communicate.
28
+ # Uses non-blocking polling TCP and marshal to communicate.
29
29
  # If your game state inherits from NetworkClient you'll have the following methods available:
30
30
  #
31
- # start(ip, port) # Start server listening on ip:port
32
- # send_data(socket, data) # Send raw data on the network, nonblocking
33
- # send_msg(socket, whatever ruby data) # Will get YAML'd and sent to server
31
+ # start(address, port) # Start server listening on address:port
32
+ # send_data(socket, data) # Send raw data on the network, non-blocking
33
+ # send_msg(socket, whatever ruby data) # Will get Marshalled and sent to server
34
34
  # broadcast_msg(whatever ruby data) # Send stuff to all connected clients, buffered and dispatched each gametick
35
- # handle_incoming_connections # Nonblocking accept of incoming connections from clients
36
- # handle_incoming_data(max_size) # Nonblocking read of incoming server data
35
+ # handle_incoming_connections # Non-blocking accept of incoming connections from clients
36
+ # handle_incoming_data(max_size) # Non-blocking read of incoming server data
37
37
 
38
38
  #
39
39
  # The following callbacks can be overwritten to add your game logic:
@@ -60,82 +60,36 @@ module Chingu
60
60
  # end
61
61
  # end
62
62
  #
63
- # push_game_state ServerState.new(:ip => "127.0.0.1", :port => 7778).start
63
+ # push_game_state NetworkServer.new(:address => "127.0.0.1", :port => 7778).start
64
64
  #
65
65
  # NetworkServer works mostly like NetworkClient with a few differences
66
66
  # - since a server handles many sockets (1 for each connected client) all callbacks first argument is 'socket'
67
- # - same with outgoing packets, send_data and send_msgs first argument is socket.
67
+ # - same with outgoing packets, #send_data and #send_msg, first argument is a socket.
68
68
  #
69
69
  # A good idea is to have a socket-ivar in your Player-model and a Player.find_by_socket(socket)
70
70
  #
71
- class NetworkServer < Chingu::GameState
72
- PACKET_HEADER_LENGTH = 4
73
- PACKET_HEADER_FORMAT = "N"
74
- DEFAULT_PORT = 7778
75
-
76
- class PacketBuffer
77
- def initialize
78
- @data = '' # Buffered data.
79
- @length = nil # Length of the next packet. nil if header not read yet.
80
- end
81
-
82
- # Add data string to the buffer.
83
- def buffer_data(data)
84
- @data << data
85
- end
86
-
87
- # Call after adding data with #buffer_data until there are no more packets left.
88
- def next_packet
89
- # Read the header to find out the length of the next packet.
90
- unless @length
91
- if @data.length >= PACKET_HEADER_LENGTH
92
- @length = @data[0...PACKET_HEADER_LENGTH].unpack(PACKET_HEADER_FORMAT)[0]
93
- @data[0...PACKET_HEADER_LENGTH] = ''
94
- end
95
- end
96
-
97
- # If there is enough data after the header for the full packet, return it.
98
- if @length and @length <= @data.length
99
- begin
100
- packet = @data[0...@length]
101
- @data[0...@length] = ''
102
- @length = nil
103
- return packet
104
- rescue TypeError => ex
105
- puts "Bad data received:\n#{@data.inspect}"
106
- raise ex
107
- end
108
- else
109
- return nil
110
- end
111
- end
112
- end
113
-
114
- attr_reader :socket, :sockets, :ip, :port, :max_connections
115
- alias_method :address, :ip
71
+ class NetworkServer < NetworkState
72
+ attr_reader :socket, :sockets, :max_connections
116
73
 
117
74
  def initialize(options = {})
118
- super
119
-
120
- @ip = options[:ip] || "0.0.0.0"
121
- @port = options[:port] || DEFAULT_PORT
122
- @debug = options[:debug]
75
+ super(options)
76
+
123
77
  @max_read_per_update = options[:max_read_per_update] || 20000
124
78
  @max_connections = options[:max_connections] || 256
125
-
79
+
126
80
  @socket = nil
127
81
  @sockets = []
128
82
  @packet_buffers = Hash.new
129
83
  end
130
-
84
+
131
85
  #
132
86
  # Start server
133
87
  #
134
- def start(ip=nil, port=nil)
135
- @ip = ip if ip
88
+ def start(address = nil, port = nil)
89
+ @address = address if address
136
90
  @port = port if port
137
91
  begin
138
- @socket = TCPServer.new(@ip, @port)
92
+ @socket = TCPServer.new(@address, @port)
139
93
  on_start
140
94
  rescue
141
95
  on_start_error($!)
@@ -148,7 +102,7 @@ module Chingu
148
102
  # Callback for when Socket listens correctly on given host/port
149
103
  #
150
104
  def on_start
151
- puts "* Server listening on #{ip}:#{port}" if @debug
105
+ puts "* Server listening on #{address}:#{port}" if @debug
152
106
  end
153
107
 
154
108
  #
@@ -156,12 +110,11 @@ module Chingu
156
110
  #
157
111
  def on_start_error(msg)
158
112
  if @debug
159
- puts "Can't start server on #{ip}:#{port}:\n"
113
+ puts "Can't start server on #{address}:#{port}:\n"
160
114
  puts msg
161
115
  end
162
116
  end
163
-
164
-
117
+
165
118
 
166
119
  #
167
120
  # Default network loop:
@@ -196,16 +149,12 @@ module Chingu
196
149
 
197
150
  def handle_incoming_connections
198
151
  begin
199
- while socket = @socket.accept_nonblock
200
- if @sockets.size < @max_connections
201
- @sockets << socket
202
- @packet_buffers[socket] = PacketBuffer.new
203
- on_connect(socket)
204
- else
205
- socket.close
206
- end
152
+ while @sockets.size < @max_connections and @socket and socket = @socket.accept_nonblock
153
+ @sockets << socket
154
+ @packet_buffers[socket] = PacketBuffer.new
155
+ on_connect(socket)
207
156
  end
208
- rescue IO::WaitReadable, Errno::EINTR
157
+ rescue IO::WaitReadable, Errno::EAGAIN, Errno::EINTR
209
158
  end
210
159
  end
211
160
 
@@ -220,9 +169,7 @@ module Chingu
220
169
  packet, sender = socket.recvfrom(max_size)
221
170
  on_data(socket, packet)
222
171
  rescue Errno::ECONNABORTED, Errno::ECONNRESET, IOError
223
- @packet_buffers[socket] = nil
224
-
225
- on_disconnect(socket)
172
+ disconnect_client(socket)
226
173
  end
227
174
  end
228
175
  end
@@ -236,38 +183,51 @@ module Chingu
236
183
 
237
184
  buffer.buffer_data data
238
185
 
186
+ @bytes_received += data.length
187
+
239
188
  while packet = buffer.next_packet
240
- on_msg(socket, Marshal.load(packet))
189
+ @packets_received += 1
190
+ begin
191
+ on_msg(socket, Marshal.load(packet))
192
+ rescue TypeError
193
+ disconnect_client(socket)
194
+ break
195
+ end
241
196
  end
242
197
  end
198
+
199
+ # Handler when message packets are received. Should be overriden in your code.
200
+ def on_msg(socket, packet)
201
+ # should be overridden.
202
+ end
243
203
 
244
204
  #
245
- # Broadcast 'msg' to all connected clients
246
- # Output is buffered and dispatched once each server-loop
247
- #
205
+ # Broadcast 'msg' to all connected clients.
206
+ # Returns amount of data sent.
248
207
  def broadcast_msg(msg)
249
208
  data = Marshal.dump(msg)
250
- @sockets.each {|s| send_data(s, data) }
209
+ @sockets.inject(0) {|tot, s| tot + send_data(s, data) }
251
210
  end
252
-
253
- #
254
- # Send 'msg' to 'socket'.
255
- # 'msg' must responds to #to_yaml
211
+
256
212
  #
213
+ # Send 'msg' to a specific client 'socket'.
214
+ # Returns amount of data sent.
257
215
  def send_msg(socket, msg)
258
216
  send_data(socket, Marshal.dump(msg))
259
217
  end
260
218
 
261
219
  #
262
220
  # Send raw 'data' to the 'socket'
263
- #
221
+ # Returns amount of data sent, including headers.
264
222
  def send_data(socket, data)
265
- begin
266
- socket.write([data.length].pack(PACKET_HEADER_FORMAT))
267
- socket.write(data)
268
- rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN
269
- on_disconnect(socket)
270
- end
223
+ length = socket.write([data.length].pack(PACKET_HEADER_FORMAT))
224
+ length += socket.write(data)
225
+ @packets_sent += 1
226
+ @bytes_sent += length
227
+ length
228
+ rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN
229
+ disconnect_client(socket)
230
+ 0
271
231
  end
272
232
 
273
233
  #
@@ -275,10 +235,11 @@ module Chingu
275
235
  #
276
236
  def disconnect_client(socket)
277
237
  socket.close
238
+ rescue Errno::ENOTCONN
239
+ ensure
278
240
  @sockets.delete socket
279
241
  @packet_buffers.delete socket
280
242
  on_disconnect(socket)
281
- rescue Errno::ENOTCONN
282
243
  end
283
244
 
284
245
  # Ensure that the buffer is cleared of data to write (call at the end of update or, at least after all sends).
@@ -296,14 +257,16 @@ module Chingu
296
257
  # Stops server
297
258
  #
298
259
  def stop
299
- return unless @socket
300
-
301
- @sockets.each {|socket| disconnect_client(socket) }
302
- @sockets = []
303
- @socket.close
304
- @socket = nil
260
+ begin
261
+ @socket.close if @socket and not @socket.closed?
305
262
  rescue Errno::ENOTCONN
263
+ end
264
+
265
+ @socket = nil
266
+ @sockets.each {|socket| disconnect_client(socket) }
267
+ @sockets = []
306
268
  end
269
+
307
270
  alias close stop
308
271
 
309
272
  end
@@ -0,0 +1,71 @@
1
+ module Chingu
2
+ module GameStates
3
+ # Abstract state, parent of NetworkClient and NetworkServer.
4
+ class NetworkState < GameState
5
+ PACKET_HEADER_LENGTH = 4
6
+ PACKET_HEADER_FORMAT = "N"
7
+ DEFAULT_PORT = 7778
8
+
9
+ class PacketBuffer
10
+ def initialize
11
+ @data = '' # Buffered data.
12
+ @length = nil # Length of the next packet. nil if header not read yet.
13
+ end
14
+
15
+ # Add data string to the buffer.
16
+ def buffer_data(data)
17
+ @data << data
18
+ end
19
+
20
+ # Call after adding data with #buffer_data until there are no more packets left.
21
+ def next_packet
22
+ # Read the header to find out the length of the next packet.
23
+ unless @length
24
+ if @data.length >= PACKET_HEADER_LENGTH
25
+ @length = @data[0...PACKET_HEADER_LENGTH].unpack(PACKET_HEADER_FORMAT)[0]
26
+ @data[0...PACKET_HEADER_LENGTH] = ''
27
+ end
28
+ end
29
+
30
+ # If there is enough data after the header for the full packet, return it.
31
+ if @length and @length <= @data.length
32
+ begin
33
+ packet = @data[0...@length]
34
+ @data[0...@length] = ''
35
+ @length = nil
36
+ return packet
37
+ rescue TypeError => ex
38
+ puts "Bad data received:\n#{@data.inspect}"
39
+ raise ex
40
+ end
41
+ else
42
+ return nil
43
+ end
44
+ end
45
+ end
46
+
47
+ attr_reader :address, :port
48
+ attr_reader :bytes_sent, :bytes_received
49
+ attr_reader :packets_sent, :packets_received
50
+
51
+ def initialize(options = {})
52
+ raise "Can't instantiate abstract class" if self.class == NetworkState
53
+
54
+ super(options)
55
+
56
+ reset_counters
57
+
58
+ @address = options[:address] || "0.0.0.0"
59
+ @port = options[:port] || DEFAULT_PORT
60
+ @debug = options[:debug]
61
+ end
62
+
63
+ # Resets #bytes_sent, #bytes_received, #packets_sent and #packets_received to zero.
64
+ def reset_counters
65
+ @bytes_sent = @bytes_received = 0
66
+ @packets_sent = @packets_received = 0
67
+ 0
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,79 @@
1
+ module Gosu
2
+ class Sample
3
+ DEFAULT_VOLUME = 1.0 # Default volume of new samples.
4
+
5
+ class << self
6
+ # Volume of all Samples.
7
+ attr_reader :volume
8
+
9
+ public
10
+ # Volume of Samples, affected by Sample.volume and Window#volume and muting.
11
+ def effective_volume
12
+ @volume * $window.effective_volume
13
+ end
14
+
15
+ public
16
+ # Set the global volume of Samples.
17
+ def volume=(value)
18
+ raise "Bad volume setting" unless value.is_a? Numeric
19
+
20
+ @volume = [[value, 1.0].min, 0.0].max.to_f
21
+ end
22
+
23
+ public
24
+ def init_sound
25
+ @volume = DEFAULT_VOLUME
26
+ nil
27
+ end
28
+ end
29
+
30
+ init_sound
31
+
32
+ # Volume of this Sample. This is multiplied by the volume in #play.
33
+ attr_reader :volume
34
+
35
+ alias_method :old_initialize, :initialize
36
+ protected :old_initialize
37
+ public
38
+ # Accepts :volume (0.0..1.0) option, defaulting to 1.0.
39
+ def initialize(filename, options = {})
40
+ options = {
41
+ volume: DEFAULT_VOLUME,
42
+ }.merge! options
43
+
44
+ @volume = options[:volume]
45
+
46
+ old_initialize(filename)
47
+ end
48
+
49
+ public
50
+ # Set the volume of this Sample. This is multiplied by the volume in #play.
51
+ def volume=(value)
52
+ raise "Bad volume setting" unless value.is_a? Numeric
53
+
54
+ @volume = [[value, 1.0].min, 0.0].max.to_f
55
+ end
56
+
57
+ public
58
+ # Volume the Sample will actually be played at, affected by Sample.volume and Window#volume.
59
+ def effective_volume
60
+ @volume * self.class.effective_volume
61
+ end
62
+
63
+ alias_method :old_play, :play
64
+ protected :old_play
65
+ public
66
+ def play(volume = 1, speed = 1, looping = false)
67
+ volume *= effective_volume
68
+ old_play(volume, speed, looping) if volume > 0.0
69
+ end
70
+
71
+ alias_method :old_play_pan, :play_pan
72
+ protected :old_play_pan
73
+ public
74
+ def play_pan(pan = 0, volume = 1, speed = 1, looping = false)
75
+ volume *= effective_volume
76
+ old_play_pan(pan, volume, speed, looping) if volume > 0.0
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,96 @@
1
+ module Gosu
2
+ class Song
3
+ DEFAULT_VOLUME = 1.0
4
+
5
+ class << self
6
+ attr_reader :resources
7
+ protected :resources
8
+
9
+ # Volume of Songs, not allowing for global volume settings.
10
+ attr_reader :volume
11
+
12
+ # Volume the song is played at, affected by Song.volume and Window#volume/muting.
13
+ def effective_volume
14
+ @volume * $window.effective_volume
15
+ end
16
+
17
+ # Volume of Songs, not allowing for global volume settings.
18
+ def volume=(value)
19
+ raise "Bad volume setting" unless value.is_a? Numeric
20
+
21
+ old_volume = @volume
22
+ @volume = [[value, 1.0].min, 0.0].max.to_f
23
+
24
+ recalculate_volumes(old_volume, @volume)
25
+
26
+ @volume
27
+ end
28
+
29
+ def init_sound
30
+ @volume = DEFAULT_VOLUME
31
+ nil
32
+ end
33
+
34
+ protected
35
+ # Recalculate all song volumes, after a global volume (Window#volume or Song.volume) has updated.
36
+ def recalculate_volumes(old_volume, new_volume)
37
+ # Avoid divide-by-zero when working out how much to alter the value by.
38
+ multiplier = if old_volume == 0.0
39
+ (new_volume > 0) ? 1.0 : 0.0
40
+ else
41
+ new_volume / old_volume
42
+ end
43
+ resources.each_value {|song| song.send(:effective_volume=, song.volume * effective_volume * multiplier) }
44
+ end
45
+ end
46
+
47
+ init_sound
48
+
49
+ alias_method :old_initialize, :initialize
50
+ protected :old_initialize
51
+
52
+ # Volume, as played.
53
+ alias_method :effective_volume, :volume
54
+
55
+ # Set the volume, as played.
56
+ alias_method :effective_volume=, :volume=
57
+ protected :effective_volume=
58
+
59
+ # Accepts :volume (0.0..1.0) option, defaulting to 1.0.
60
+ def initialize(filename, options = {})
61
+ options = {
62
+ volume: DEFAULT_VOLUME,
63
+ }.merge! options
64
+
65
+ old_initialize(filename)
66
+
67
+ @muted = false
68
+ self.volume = options[:volume]
69
+ end
70
+
71
+ # Volume, not affected by Song volume or the Window volume/muted.
72
+ def volume
73
+ @true_volume
74
+ end
75
+
76
+ def volume=(value)
77
+ @true_volume = [[value, 0.0].max, 1.0].min
78
+
79
+ self.effective_volume = @true_volume * self.class.effective_volume unless @muted
80
+
81
+ volume
82
+ end
83
+
84
+ protected
85
+ def mute
86
+ self.effective_volume = 0.0
87
+ self
88
+ end
89
+
90
+ protected
91
+ def unmute
92
+ self.effective_volume = @true_volume * self.class.effective_volume
93
+ self
94
+ end
95
+ end
96
+ end