chingu 0.9rc7 → 0.9rc8

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.
@@ -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