chichilku3 5.0.0

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/bin/chichilku3 +3 -0
  3. data/bin/chichilku3-server +3 -0
  4. data/client.json +6 -0
  5. data/lib/client/chichilku3.rb +18 -0
  6. data/lib/client/client.rb +289 -0
  7. data/lib/client/client_cfg.rb +15 -0
  8. data/lib/client/gui.rb +492 -0
  9. data/lib/client/img/background1024x512.png +0 -0
  10. data/lib/client/img/battle1024x576.png +0 -0
  11. data/lib/client/img/connecting1024x512.png +0 -0
  12. data/lib/client/img/grass1024x512.png +0 -0
  13. data/lib/client/img/grey128.png +0 -0
  14. data/lib/client/img/menu1920x1080.png +0 -0
  15. data/lib/client/img/stick128/stick0.png +0 -0
  16. data/lib/client/img/stick128/stick1.png +0 -0
  17. data/lib/client/img/stick128/stick2.png +0 -0
  18. data/lib/client/img/stick128/stick3.png +0 -0
  19. data/lib/client/img/stick128/stick4.png +0 -0
  20. data/lib/client/img/stick128/stick5.png +0 -0
  21. data/lib/client/img/stick128/stick_crouching0.png +0 -0
  22. data/lib/client/img/stick128/stick_crouching1.png +0 -0
  23. data/lib/client/img/stick128/stick_crouching2.png +0 -0
  24. data/lib/client/img/stick128/stick_crouching3.png +0 -0
  25. data/lib/client/img/stick128/stick_crouching4.png +0 -0
  26. data/lib/client/img/stick128/stick_crouching5.png +0 -0
  27. data/lib/client/scoreboard.rb +25 -0
  28. data/lib/client/test.rb +39 -0
  29. data/lib/client/text.rb +86 -0
  30. data/lib/server/chichilku3_server.rb +293 -0
  31. data/lib/server/gamelogic.rb +121 -0
  32. data/lib/server/server_cfg.rb +6 -0
  33. data/lib/share/config.rb +53 -0
  34. data/lib/share/console.rb +18 -0
  35. data/lib/share/network.rb +145 -0
  36. data/lib/share/player.rb +286 -0
  37. data/server.json +4 -0
  38. metadata +81 -0
@@ -0,0 +1,293 @@
1
+ require 'socket'
2
+
3
+ require_relative '../share/console'
4
+ require_relative '../share/network'
5
+ require_relative '../share/player'
6
+
7
+ require_relative 'gamelogic'
8
+ require_relative 'server_cfg'
9
+
10
+ $next_tick = Time.now
11
+
12
+ NET_CLIENT = 0
13
+ PLAYER_ID = 1
14
+
15
+ # ServerCore: should only contain the networking
16
+ # and no gamelogic
17
+ class ServerCore
18
+ def initialize
19
+ # single dimension array holding player objects
20
+ @players = []
21
+ # multi dimensional array
22
+ # 0 - client network socket
23
+ # 1 - player id
24
+ @clients = []
25
+ @current_id = 0
26
+ @tick = 0
27
+ @last_alive_pck_by_client = Time.now
28
+ @console = Console.new
29
+ @cfg = ServerCfg.new(@console, "server.json")
30
+ @gamelogic = GameLogic.new(@console)
31
+ @global_pack = nil
32
+ end
33
+
34
+ def parse_client_version(data)
35
+ return if data.nil?
36
+
37
+ id = data[0].to_i
38
+ version = data[1..-1]
39
+ player = Player.get_player_by_id(@players, id)
40
+ if player
41
+ @console.log "name req id='#{id}' vrs='#{version}' name='#{player.name}'"
42
+ player.set_version(version)
43
+ else
44
+ @console.log "error parsing version data=#{data}"
45
+ end
46
+ player
47
+ end
48
+
49
+ def create_name_package(data)
50
+ # protocol 3 name prot
51
+ # also includes client version
52
+ player = parse_client_version(data)
53
+ # gamestate
54
+ # |
55
+ pck = "3l#{@players.count}g"
56
+ # pck = format('3l%02d', @players.count) # old 2 digit player count
57
+ @players.each do |p|
58
+ pck += p.to_n_pck
59
+ @console.dbg "pname='#{p.name}'"
60
+ end
61
+ unless player.nil?
62
+ if player.version.to_i < GAME_VERSION.to_i
63
+ return "0l#{NET_ERR_CLIENT_OUTDATED}#{GAME_VERSION} "
64
+ elsif player.version.to_i > GAME_VERSION.to_i
65
+ return "0l#{NET_ERR_SERVER_OUTDATED}#{GAME_VERSION} "
66
+ end
67
+ end
68
+ pck.ljust(SERVER_PACKAGE_LEN, '0')
69
+ end
70
+
71
+ def get_free_id
72
+ # TODO: do this smarter
73
+ used_ids = @clients.map{ |c| c[1] }
74
+ id = 0
75
+ while id < MAX_CLIENTS do
76
+ id += 1
77
+ return id unless used_ids.include? id
78
+ end
79
+ -1
80
+ end
81
+
82
+ def add_player(name, client, ip)
83
+ @current_id = get_free_id()
84
+ return -1 if @current_id > MAX_CLIENTS || @current_id < 1
85
+
86
+ @console.log "Added player id='#{@current_id}' ip='#{ip}'"
87
+ @players << Player.new(@current_id, 0, nil, nil, name, ip)
88
+ client[PLAYER_ID] = @current_id
89
+ @current_id # implicit return
90
+ end
91
+
92
+ def delete_player(id)
93
+ @console.log "Deleted player id='#{id}'"
94
+ @players.delete(Player.get_player_by_id(@players, id))
95
+ end
96
+
97
+ def players_to_packet
98
+ # old 2 digit player count
99
+ # packet = @players.empty? ? '00' : format('%02d', @players.count)
100
+ packet = @players.empty? ? '0' : @players.count.to_s
101
+ packet += 'g' # gamestate
102
+ @players.each do |player|
103
+ packet += player.to_s
104
+ end
105
+ # fill with zeros if less than 3 players online
106
+ packet.ljust(SERVER_PACKAGE_LEN - 2, '0') # implicit return
107
+ end
108
+
109
+ def update_pck(data, dt)
110
+ id = data[0].to_i
111
+ @console.dbg "got player with id: #{id}"
112
+ @players = @gamelogic.handle_client_requests(data[1..-1], id, @players, dt)
113
+ nil # defaults to normal update pck
114
+ end
115
+
116
+ def id_pck(data, client, ip)
117
+ name = data[0..5]
118
+ id = add_player(name, client, ip)
119
+ if id == -1
120
+ @console.log "'#{name}' failed to connect (server full)"
121
+ # protocol 0 (error) code=404 slot not found
122
+ return "0l#{NET_ERR_FULL} "
123
+ end
124
+ @console.log "id='#{id}' name='#{name}' joined the game"
125
+ @global_pack = "true"
126
+ # protocol 2 (id)
127
+ format('2l00%02d0000000000000000000000', id).to_s
128
+ end
129
+
130
+ def command_package(data, client)
131
+ id = data[0..1].to_i
132
+ cmd = data[1..-1]
133
+ @console.log "[chat] ID=#{id} command='#{cmd}'"
134
+ msg = "server_recived_cmd: #{cmd}"
135
+ msg = msg.ljust(SERVER_PACKAGE_LEN - 2, '0')
136
+ msg = msg[0..SERVER_PACKAGE_LEN - 3]
137
+ if cmd == "test"
138
+ # return "0l#{NET_ERR_DISCONNECT} SAMPLE MESSAGE "
139
+ msg = "id=#{client[PLAYER_ID]}"
140
+ end
141
+ msg = msg.ljust(SERVER_PACKAGE_LEN - 2, ' ')
142
+ msg = msg[0..SERVER_PACKAGE_LEN - 3]
143
+ "4l#{msg}"
144
+ end
145
+
146
+ def handle_protocol(client, protocol, p_status, data, ip, dt)
147
+ @console.dbg "HANDLE PROTOCOL=#{protocol} status=#{p_status}"
148
+ if protocol.zero? # error pck
149
+ @console.log "Error pck=#{data}"
150
+ elsif protocol == 1 # id pck
151
+ return id_pck(data, client, ip)
152
+ else
153
+ # all other types require id
154
+ id = data[0].to_i
155
+ if id != client[PLAYER_ID]
156
+ @console.log("id=#{client[PLAYER_ID]} tried to spoof id=#{id} ip=#{ip}")
157
+ disconnect_client(client, "0l#{NET_ERR_DISCONNECT}invalid player id ")
158
+ return nil
159
+ end
160
+ if protocol == 2 # update pck
161
+ return update_pck(data, dt)
162
+ elsif protocol == 3 # initial request names
163
+ return create_name_package(data)
164
+ elsif protocol == 4 # command
165
+ return command_package(data, client)
166
+ else
167
+ @console.log "ERROR unkown protocol=#{protocol} data=#{data}"
168
+ end
169
+ end
170
+ end
171
+
172
+ def handle_client_data(client, data, ip, dt)
173
+ response = handle_protocol(client, data[0].to_i, data[1], data[2..-1], ip, dt)
174
+ # the response is a direct respond to an protocol
175
+ # everything above this could override important responds
176
+ # like id assignment
177
+ # every think that is after this guad case just overrides update pcks
178
+ return response unless response.nil?
179
+
180
+ if (@tick % 100).zero?
181
+ # return '3l0301hello02x0x0x03hax0r000'
182
+ return create_name_package(nil)
183
+ end
184
+
185
+ # some debug suff for class vars
186
+ # if (@tick % 50).zero?
187
+ # puts ""
188
+ # @console.log "id=#{data[0].to_i} currentid=#{@current_id}"
189
+ # end
190
+
191
+ # if @global_pack.nil?
192
+ # @global_pack = nil
193
+ # @console.log "sending an global pck"
194
+ # return "5l#{players_to_packet}"
195
+ # end
196
+
197
+ # if error occurs or something unexpected
198
+ # just send regular update pck
199
+ # protocol 1 (update)
200
+ "1l#{players_to_packet}" # implicit return
201
+ end
202
+
203
+ # TODO: this func and it dependencies should be new file
204
+ # Handles each client
205
+ def client_tick(cli, dt)
206
+ client_data = save_read(cli[NET_CLIENT], CLIENT_PACKAGE_LEN)
207
+ if client_data == ''
208
+ # diff = Time.now - @last_alive_pck_by_client
209
+ # if (diff > MAX_TIMEOUT)
210
+ # @console.log "sombody timed out"
211
+ # end
212
+ return
213
+ end
214
+
215
+ @console.dbg "recv: #{client_data}"
216
+ @last_alive_pck_by_client = Time.now
217
+ port, ip = Socket.unpack_sockaddr_in(cli[NET_CLIENT].getpeername)
218
+ server_response = handle_client_data(cli, client_data, ip, dt)
219
+ # server_response = '1l03011001010220020203300303'
220
+ pck_type = server_response[0]
221
+ if pck_type == SERVER_PCK_TYPE[:error]
222
+ disconnect_client(cli, server_response)
223
+ else
224
+ net_write(server_response, cli[NET_CLIENT])
225
+ end
226
+ end
227
+
228
+ def disconnect_client(client, server_response = nil)
229
+ player_id = client[PLAYER_ID]
230
+ @console.log "player id=#{player_id} left the game." if player_id != -1
231
+ @console.dbg "client disconnected.#{" (" + server_response + ")" unless server_response.nil?}"
232
+ net_write(server_response, client[NET_CLIENT]) unless server_response.nil?
233
+ client[NET_CLIENT].close
234
+ delete_player(player_id)
235
+ @clients.delete(client)
236
+ @current_id -= 1
237
+ end
238
+
239
+ def client_by_playerid(player_id)
240
+ @clients.find do |client|
241
+ client[PLAYER_ID] == player_id
242
+ end
243
+ end
244
+
245
+ def run
246
+ server = TCPServer.open(@cfg.data['port'])
247
+ server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) # nagle's algorithm
248
+ Thread.new do
249
+ accept(server)
250
+ end
251
+ loop do
252
+ diff = 0 # TODO: unused lmao traced it through the half source
253
+ t = Time.now
254
+ sleep $next_tick - t if $next_tick > t
255
+ @tick += 1
256
+ $next_tick = Time.now + MAX_TICK_SPEED
257
+ @players = @gamelogic.tick(@players, diff)
258
+ # there is no gurantee the client will tick here
259
+ # there might be 2 gamelogic ticks and posticks
260
+ # before the server recieves client data
261
+ # since it is a nonblocking read and server/client are not in perfect sync
262
+ @clients.each do |client|
263
+ begin
264
+ client_tick(client, diff)
265
+ rescue Errno::ECONNRESET, EOFError, IOError
266
+ disconnect_client(client)
267
+ end
268
+ end
269
+ @players = @gamelogic.posttick(@players, diff)
270
+ end
271
+ end
272
+
273
+ private
274
+
275
+ def accept(server)
276
+ Socket.accept_loop(server) do |client|
277
+ @clients << [client, -1]
278
+ @console.log "client connected. clients connected: #{@clients.count}"
279
+ end
280
+ end
281
+
282
+ def net_write(data, cli)
283
+ if data.length != SERVER_PACKAGE_LEN
284
+ @console.log "ERROR pack len: #{data.length}/#{SERVER_PACKAGE_LEN} pck: #{data}"
285
+ exit
286
+ end
287
+ @console.dbg("sending: #{data}")
288
+ cli.write(data)
289
+ end
290
+ end
291
+
292
+ srv = ServerCore.new
293
+ srv.run
@@ -0,0 +1,121 @@
1
+ class GameLogic
2
+ def initialize(console)
3
+ @console = console
4
+ @alive_players = 0
5
+ end
6
+
7
+ def check_collide(players, player)
8
+ players.each do |other|
9
+ next if other == player
10
+ next if !player.collide[:down]
11
+
12
+ x_force = player.check_player_collide(other)
13
+ player.apply_force(x_force, -8) if !x_force.zero?
14
+ end
15
+ end
16
+
17
+ def tick(players, dt)
18
+ players.each do |player|
19
+ # reset values (should stay first)
20
+ player.reset_collide
21
+
22
+ gravity(player, dt)
23
+ player.tick
24
+ # player collsions works
25
+ # but it eats performance and delays jumping
26
+ check_collide(players, player)
27
+ end
28
+ end
29
+
30
+ def handle_client_requests(data, id, players, dt)
31
+ player = Player.get_player_by_id(players, id)
32
+ if player.nil?
33
+ @console.log "WARNING failed to update nil player with id=#{id}"
34
+ if players.count > 0
35
+ @console.log "connected players:"
36
+ else
37
+ @console.log "no players currently connected!"
38
+ end
39
+ players.each do |p|
40
+ @console.log "id=#{p.id} name='#{p.name}'"
41
+ end
42
+ return players
43
+ end
44
+
45
+ # reset values (should stay first)
46
+ player.state[:crouching] = false
47
+
48
+ # move request
49
+ if data[0] == '1'
50
+ @console.dbg "player=#{id} wants to crouch"
51
+ player.state[:crouching] = true
52
+ player.x -= TILE_SIZE / 4 unless player.was_crouching
53
+ player.was_crouching = true
54
+ end
55
+ if data[1] == '1'
56
+ @console.dbg "player=#{id} wants to walk left"
57
+ player.move_left
58
+ end
59
+ if data[2] == '1'
60
+ @console.dbg "player=#{id} wants to walk right"
61
+ player.move_right
62
+ end
63
+ if data[3] == '1'
64
+ @console.dbg "player=#{id} wants to jump"
65
+ player.do_jump
66
+ end
67
+
68
+ player.check_out_of_world
69
+
70
+ # return updated players
71
+ players
72
+ end
73
+
74
+ def posttick(players, dt)
75
+ players.each do |player|
76
+ # stopped crouching -> stand up
77
+ if player.was_crouching && player.state[:crouching] == false
78
+ player.y -= TILE_SIZE
79
+ player.x += TILE_SIZE / 4
80
+ player.was_crouching = false
81
+ end
82
+ end
83
+ end
84
+
85
+ def gravity(player, dt)
86
+ if player.dead
87
+ player.dead_ticks += 1
88
+ player.state[:bleeding] = true
89
+ if player.dead_ticks > 3
90
+ player.dead = false
91
+ player.state[:bleeding] = false
92
+ player.die
93
+ end
94
+ else
95
+ if player.y > 320 # too far down --> die
96
+ player.dead = true
97
+ player.dead_ticks = 0
98
+ end
99
+ end
100
+
101
+ # outside of the save zone
102
+ if player.x < 214 || player.x > 800 || player.dead
103
+ if player.y + player.h > 484
104
+ # player.collide[:down] = true
105
+ player.do_collide(:down, true)
106
+ return
107
+ end
108
+ else # on the save zone
109
+ if player.y + player.h > 324
110
+ # player.collide[:down] = true
111
+ player.do_collide(:down, true)
112
+ return
113
+ end
114
+ end
115
+
116
+ # grav = 100000 * dt
117
+ # @console.log "grav: #{grav}"
118
+ # player.y += grav
119
+ player.dy += 2 if player.dy < 16
120
+ end
121
+ end
@@ -0,0 +1,6 @@
1
+ require 'json'
2
+ require_relative '../share/config'
3
+
4
+ # The server side repository usign JSON
5
+ class ServerCfg < Config
6
+ end
@@ -0,0 +1,53 @@
1
+ require 'json'
2
+ require 'os'
3
+ require 'fileutils'
4
+
5
+ # chichilku3 config base used by client and server
6
+ class Config
7
+ attr_reader :data
8
+
9
+ def initialize(console, file)
10
+ chichilku3_dir = ""
11
+ if OS.linux?
12
+ chichilku3_dir = "#{ENV['HOME']}/.chichilku/chichilku3/"
13
+ elsif OS.mac?
14
+ chichilku3_dir = "#{ENV['HOME']}/Library/Application Support/chichilku/chichilku3/"
15
+ # elsif OS.windows?
16
+ # chichilku3_dir = "%APPDATA%\\chichilku\\chichilku3\\"
17
+ else
18
+ puts "os not supported."
19
+ exit
20
+ end
21
+ puts "path: " + chichilku3_dir
22
+ FileUtils.mkdir_p chichilku3_dir
23
+ create_default_cfg(file, "#{chichilku3_dir}/#{file}")
24
+ @file = chichilku3_dir + file
25
+ @console = console
26
+ @data = load
27
+ end
28
+
29
+ def create_default_cfg(from, to)
30
+ return if File.file?(to)
31
+
32
+ tmp = JSON.parse(File.read(from))
33
+ File.open(to,"w") do |f|
34
+ f.write(tmp.to_json)
35
+ end
36
+ end
37
+
38
+ def sanitize_data(data)
39
+ data
40
+ end
41
+
42
+ def load
43
+ data = JSON.parse(File.read(@file))
44
+ data = sanitize_data(data)
45
+ data
46
+ end
47
+
48
+ def save
49
+ File.open(@file, "w") do |f|
50
+ f.write(JSON.pretty_generate(data))
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ DEBUG = false
2
+
3
+ # Console used by Client and Server
4
+ class Console
5
+ def log(message)
6
+ t = Time.now
7
+ puts "[#{t.hour}:#{t.min}:#{t.sec}][log] #{message}"
8
+ end
9
+
10
+ def dbg(message)
11
+ return unless DEBUG
12
+
13
+ t = Time.now
14
+ puts "[#{t.hour}:#{t.min}:#{t.sec}][debug] #{message}"
15
+ end
16
+ end
17
+
18
+ $console = Console.new
@@ -0,0 +1,145 @@
1
+ require 'socket'
2
+ # check doc_network.rb for documentation
3
+
4
+ # update GAME_VERSION on network protocol changes
5
+ GAME_VERSION = '0005'
6
+
7
+ # game
8
+
9
+ TILE_SIZE = 64
10
+ WINDOW_SIZE_X = TILE_SIZE * 16
11
+ WINDOW_SIZE_Y = TILE_SIZE * 9
12
+ FULLHD_X = 1920
13
+ UI_SCALE = WINDOW_SIZE_X.to_f / FULLHD_X.to_f
14
+ SPEED = TILE_SIZE
15
+
16
+ # networking
17
+
18
+ NAME_LEN = 5
19
+ MAX_CLIENTS = 3
20
+ CLIENT_PACKAGE_LEN = 7 # used by server
21
+ SERVER_PACKAGE_LEN = MAX_CLIENTS * 8 + 4 # used by client
22
+
23
+ MAX_TIMEOUT = 5
24
+ MAX_TICK_SPEED = 0.01 # the lower the faster client and server tick
25
+ # MAX_TICK_SPEED = 0.005
26
+
27
+ NET_ERR_FULL = "404"
28
+ NET_ERR_DISCONNECT = "001"
29
+ NET_ERR_KICK = "002"
30
+ NET_ERR_BAN = "003"
31
+ NET_ERR_SERVER_OUTDATED = "004"
32
+ NET_ERR_CLIENT_OUTDATED = "005"
33
+
34
+ NET_ERR = {
35
+ "404" => "SERVER FULL",
36
+ "001" => "DISCONNECTED",
37
+ "002" => "KICKED",
38
+ "003" => "BANNED",
39
+ "004" => "SERVER OUTDATED",
40
+ "005" => "CLIENT OUTDATED"
41
+ }
42
+
43
+ CLIENT_PCK_TYPE = {
44
+ :error => "0",
45
+ :join => "1",
46
+ :move => "2",
47
+ :info => "3",
48
+ :cmd => "4"
49
+ }
50
+
51
+ SERVER_PCK_TYPE = {
52
+ :error => "0",
53
+ :update => "1",
54
+ # TODO: find a good name here
55
+ :info => "3",
56
+ :cmd => "4",
57
+ :event => "5"
58
+ }
59
+
60
+ NET_INT_OFFSET = 33
61
+ NET_INT_BASE = 93
62
+ NET_MAX_INT = NET_INT_BASE
63
+ NET_MIN_INT = 0
64
+
65
+ ##
66
+ # Converts a integer to single character network string
67
+ #
68
+ # the base of the network is NET_INT_BASE
69
+ # so the number 93 is the last single character number represented as '~'
70
+ #
71
+ # @param [Integer, #chr] int decimal based number
72
+ # @return [String] the int converted to base NET_INT_BASE
73
+
74
+ def net_pack_int(int)
75
+ net_error "#{__method__}: '#{int}' is too low allowed range #{NET_MIN_INT}-#{NET_MAX_INT}" if int < NET_MIN_INT
76
+ net_error "#{__method__}: '#{int}' is too high allowed range #{NET_MIN_INT}-#{NET_MAX_INT}" if int > NET_MAX_INT
77
+ int = int + NET_INT_OFFSET
78
+ int.chr
79
+ end
80
+
81
+ ##
82
+ # Converts a single character network string to integer
83
+ #
84
+ # the base of the network is NET_INT_BASE
85
+ # so the number 93 is the last single character number represented as '~'
86
+ #
87
+ # @param [String, #ord] net_int network packed string
88
+ # @return [Integer] the net_int converted to decimal based number
89
+
90
+ def net_unpack_int(net_int)
91
+ net_int.ord - NET_INT_OFFSET
92
+ end
93
+
94
+ ##
95
+ # Converts a integer to multi character network string
96
+ #
97
+ # @param [Integer, #net_pack_int] int decimal based number
98
+ # @param [Integer] size max length of the network string
99
+ # @return [String] the int converted to base NET_INT_BASE
100
+
101
+ def net_pack_bigint(int, size)
102
+ sum = ""
103
+ div = size - 1
104
+ (size - 1).times do
105
+ buf = int / ((NET_MAX_INT+1) ** div)
106
+ sum += net_pack_int(buf)
107
+ int = int % ((NET_MAX_INT+1) ** div)
108
+ end
109
+ sum += net_pack_int(int)
110
+ # TODO: check reminder and so on
111
+ # throw and error when int is too big for size
112
+ int = int / NET_MAX_INT
113
+ sum
114
+ end
115
+
116
+ ##
117
+ # Converts a multi character network string to integer
118
+ #
119
+ # @param [String, #net_unpack_int] net_int network packed int
120
+ # @return [Integer] the net_int converted to decimal based number
121
+
122
+ def net_unpack_bigint(net_int)
123
+ sum = 0
124
+ net_int.chars.reverse.each_with_index do |c, i|
125
+ if i.zero?
126
+ sum = net_unpack_int(c)
127
+ else
128
+ sum += net_unpack_int(c) * i * (NET_MAX_INT+1)
129
+ end
130
+ end
131
+ sum
132
+ end
133
+
134
+ def save_read(socket, size)
135
+ begin
136
+ return socket.read_nonblock(size)
137
+ rescue IO::WaitReadable
138
+ return ''
139
+ end
140
+ end
141
+
142
+ def net_error(err)
143
+ raise "NetError: #{err}"
144
+ exit 1
145
+ end