Permalink
Browse files

Two-step login process, so player can provide a name.

  • Loading branch information...
1 parent 959c01d commit 309bf2125ceef8eeefecd212dcab43a608ca9ee9 @stephank committed Oct 29, 2010
Showing with 143 additions and 63 deletions.
  1. +10 −7 src/client/renderer/base.coffee
  2. +40 −21 src/client/world/client.coffee
  3. +17 −1 src/map.coffee
  4. +1 −0 src/net.coffee
  5. +75 −34 src/server/application.coffee
@@ -20,7 +20,7 @@ class BaseRenderer
@soundkit = @world.soundkit
@canvas = $('<canvas/>').appendTo('body')
- @lastCenter = [0, 0]
+ @lastCenter = @world.map.findCenterCell().getWorldCoordinates()
@mouse = [0, 0]
@canvas.click (e) => @handleClick(e)
@@ -64,8 +64,11 @@ class BaseRenderer
# Draw a single frame.
draw: ->
- {x, y} = @world.player
- {x, y} = @world.player.fireball.$ if @world.player.fireball?
+ if @world.player
+ {x, y} = @world.player
+ {x, y} = @world.player.fireball.$ if @world.player.fireball?
+ else
+ x = y = null
# Remember or restore the last center position. We use this after tank
# death, so as to keep drawing something useful while we fade.
@@ -87,12 +90,12 @@ class BaseRenderer
@drawOverlay()
# Update all DOM HUD elements.
- @updateHud()
+ @updateHud() if @hud
# Play a sound effect.
playSound: (sfx, x, y, owner) ->
mode =
- if owner == @world.player then 'Self'
+ if @world.player and owner == @world.player then 'Self'
else
dx = x - @lastCenter[0]; dy = y - @lastCenter[1]
dist = sqrt(dx*dx + dy*dy)
@@ -159,8 +162,8 @@ class BaseRenderer
# Draw HUD elements that overlay the map. These are elements that need to be drawn in regular
# game coordinates, rather than screen coordinates.
drawOverlay: ->
- unless (player = @world.player).armour == 255
- b = @world.player.builder.$
+ if (player = @world.player) and player.armour != 255
+ b = player.builder.$
unless b.order == b.states.inTank or b.order == b.states.parachuting
@drawBuilderIndicator(b)
@drawReticle()
@@ -56,15 +56,18 @@ class BoloClientWorld extends ClientWorld
$(@ws).bind 'message.bolo', (e) =>
@handleMessage(e.originalEvent)
- # Callback after the welcome message was received.
- receiveWelcome: (tank) ->
- @player = tank
+ # Callback after the server tells us we are synchronized.
+ synchronized: ->
@rebuildMapObjects()
- @renderer.initHud()
@vignette.destroy()
@vignette = null
@loop.start()
+ # Callback after the welcome message was received.
+ receiveWelcome: (tank) ->
+ @player = tank
+ @renderer.initHud()
+
# Send the heartbeat (an empty message) every 10 ticks / 400ms.
tick: ->
super
@@ -106,7 +109,7 @@ class BoloClientWorld extends ClientWorld
#### Input handlers.
handleKeydown: (e) ->
- return unless @ws?
+ return unless @ws and @player
switch e.which
when 32 then @ws.send net.START_SHOOTING
when 37 then @ws.send net.START_TURNING_CCW
@@ -115,7 +118,7 @@ class BoloClientWorld extends ClientWorld
when 40 then @ws.send net.START_BRAKING
handleKeyup: (e) ->
- return unless @ws?
+ return unless @ws and @player
switch e.which
when 32 then @ws.send net.STOP_SHOOTING
when 37 then @ws.send net.STOP_TURNING_CCW
@@ -124,37 +127,46 @@ class BoloClientWorld extends ClientWorld
when 40 then @ws.send net.STOP_BRAKING
buildOrder: (action, trees, cell) ->
- return unless @ws?
+ return unless @ws and @player
trees ||= 0
@ws.send [net.BUILD_ORDER, action, trees, cell.x, cell.y].join(',')
#### Network message handlers.
handleMessage: (e) ->
- @netRestore()
- @processingServerMessages = yes
- data = decodeBase64(e.data)
- pos = 0
- length = data.length
error = null
- while pos < length
- command = data[pos++]
+ if e.data.charAt(0) == '{'
try
- ate = @handleServerCommand command, data, pos
+ @handleJsonCommand JSON.parse(e.data)
+ catch e
+ error = e
+ else
+ @netRestore()
+ try
+ data = decodeBase64(e.data)
+ pos = 0
+ length = data.length
+ @processingServerMessages = yes
+ while pos < length
+ command = data[pos++]
+ ate = @handleBinaryCommand command, data, pos
+ pos += ate
+ @processingServerMessages = no
+ if pos != length
+ error = new Error("Message length mismatch, processed #{pos} out of #{length} bytes")
catch e
error = e
- break
- pos += ate
- if pos != length
- error = new Error("Message length mismatch, processed #{pos} out of #{length} bytes")
if error
@failure 'Connection lost (protocol error)'
console?.log "Following exception occurred while processing message:", data
throw error
- @processingServerMessages = no
- handleServerCommand: (command, data, offset) ->
+ handleBinaryCommand: (command, data, offset) ->
switch command
+ when net.SYNC_MESSAGE
+ @synchronized()
+ 0
+
when net.WELCOME_MESSAGE
[[tank_idx], bytes] = unpack('H', data, offset)
@receiveWelcome @objects[tank_idx]
@@ -190,6 +202,13 @@ class BoloClientWorld extends ClientWorld
else
throw new Error "Bad command '#{command}' from server, at offset #{offset - 1}"
+ handleJsonCommand: (data) ->
+ switch data.command
+ when 'nick'
+ @objects[data.idx].name = data.nick
+ else
+ throw new Error "Bad JSON command '#{data.command}' from server."
+
#### Helpers
# Fill `@map.pills` and `@map.bases` based on the current object list.
View
@@ -3,7 +3,7 @@
# modules that is useful on it's own.
-{floor, min} = Math
+{round, floor, min} = Math
{MAP_SIZE_TILES} = require './constants'
@@ -450,6 +450,22 @@ class Map
cell.retile()
, sx, sy, ex, ey
+ # Find the cell at the center of the 'painted' map area.
+ findCenterCell: ->
+ t = l = MAP_SIZE_TILES - 1
+ b = r = 0
+ @each (c) ->
+ l = c.x if l > c.x
+ r = c.x if r < c.x
+ t = c.y if t > c.y
+ b = c.y if b < c.y
+ if l > r
+ t = l = 0
+ b = r = MAP_SIZE_TILES - 1
+ x = round(l + (r - l) / 2)
+ y = round(t + (b - t) / 2)
+ @cellAtTile(x, y)
+
#### Saving and loading
# Dump the map to an array of octets in BMAP format.
View
@@ -17,6 +17,7 @@
# These are the server message identifiers both sides need to know about.
# The server sends binary data (encoded as base64). So we need to compare character codes.
+exports.SYNC_MESSAGE = 's'.charCodeAt(0)
exports.WELCOME_MESSAGE = 'W'.charCodeAt(0)
exports.CREATE_MESSAGE = 'C'.charCodeAt(0)
exports.DESTROY_MESSAGE = 'D'.charCodeAt(0)
@@ -35,12 +35,13 @@ class BoloServerWorld extends ServerWorld
constructor: (@map) ->
super
@boloInit()
+ @clients = []
@map.world = this
@oddTick = no
@spawnMapObjects()
close: ->
- for {client} in @tanks when client?
+ for client in @clients
client.end()
#### Callbacks
@@ -63,20 +64,14 @@ class BoloServerWorld extends ServerWorld
#### Connection handling.
onConnect: (ws) ->
- tank = @spawn Tank
- packet = @changesPacket(yes)
- packet = new Buffer(packet).toString('base64')
- for {client} in @tanks when client?
- client.sendMessage(packet)
-
# Set-up the websocket parameters.
- tank.client = ws
+ @clients.push ws
ws.setTimeout 10000 # Disconnect after 10s of inactivity.
ws.heartbeatTimer = 0
- ws.on 'message', (message) => @onMessage(tank, message)
- ws.on 'end', => @onEnd(tank)
- ws.on 'error', (exception) => @onError(tank, exception)
- ws.on 'timeout', => @onError(tank, 'Timed out')
+ ws.on 'message', (message) => @onMessage(ws, message)
+ ws.on 'end', => @onEnd(ws)
+ ws.on 'error', (error) => @onError ws, error
+ ws.on 'timeout', => @onError ws, new Error('Connection timed out')
# Send the current map state. We don't send pillboxes and bases, because the client
# receives create messages for those, and then fills the map structure based on those.
@@ -90,31 +85,35 @@ class BoloServerWorld extends ServerWorld
packet = []
for obj in @objects
packet = packet.concat [net.CREATE_MESSAGE, obj._net_type_idx]
- packet = packet.concat [net.UPDATE_MESSAGE], @dumpTick(yes)
- packet = packet.concat pack('BH', net.WELCOME_MESSAGE, tank.idx)
+ packet = packet.concat [net.UPDATE_MESSAGE], @dumpTick(yes), [net.SYNC_MESSAGE]
packet = new Buffer(packet).toString('base64')
ws.sendMessage(packet)
- onEnd: (tank) ->
- return unless ws = tank.client
- tank.client = null
+ onEnd: (ws) ->
ws.end()
- @onDisconnect(tank)
+ @onDisconnect(ws)
- onError: (tank, exception) ->
- return unless ws = tank.client
- tank.client = null
- # FIXME: log exception
+ onError: (ws, error) ->
+ console.log error.toString()
ws.destroy()
- @onDisconnect(tank)
-
- onDisconnect: (tank) ->
- @destroy tank
-
- onMessage: (tank, message) ->
- return unless tank.client?
- switch message.charAt(0)
- when '' then tank.client.heartbeatTimer = 0
+ @onDisconnect(ws)
+
+ onDisconnect: (ws) ->
+ @destroy ws.tank if ws.tank
+ ws.tank = null
+ if (idx = @clients.indexOf(ws)) != -1
+ @clients.splice(idx, 1)
+
+ onMessage: (ws, message) ->
+ if message == '' then ws.heartbeatTimer = 0
+ else if message.charAt(0) == '{' then @onJsonMessage(ws, message)
+ else @onSimpleMessage(ws, message)
+
+ onSimpleMessage: (ws, message) ->
+ unless tank = ws.tank
+ return @onError ws, new Error("Received a game command from a spectator")
+ command = message.charAt(0)
+ switch command
when net.START_TURNING_CCW then tank.turningCounterClockwise = yes
when net.STOP_TURNING_CCW then tank.turningCounterClockwise = no
when net.START_TURNING_CW then tank.turningClockwise = yes
@@ -132,13 +131,55 @@ class BoloServerWorld extends ServerWorld
trees = parseInt(trees); x = parseInt(x); y = parseInt(y)
builder = tank.builder.$
if trees < 0 or not builder.states.actions.hasOwnProperty(action)
- @onError(tank, 'Received invalid build order')
+ @onError ws, new Error("Received invalid build order")
else
builder.performOrder action, trees, @map.cellAtTile(x, y)
- else @onError(tank, 'Received an unknown command')
+ else
+ sanitized = command.replace(/\W+/, '')
+ @onError ws, new Error("Received an unknown command: #{sanitized}")
+
+ onJsonMessage: (ws, message) ->
+ try
+ message = JSON.parse(message)
+ catch e
+ return @onError ws, e
+ switch message.command
+ when 'join'
+ if ws.tank
+ @onError ws, new Error("Client tried to join twice.")
+ else if typeof(message.nick) != 'string' or message.nick.length > 40
+ @onError ws, new Error("Client specified invalid nickname.")
+ else
+ @createPlayer(ws, message.nick)
+ else
+ sanitized = message.command.slice(0, 10).replace(/\W+/, '')
+ @onError ws, new Error("Received an unknown JSON command: #{sanitized}")
#### Helpers
+ # Simple helper to send a message to everyone.
+ broadcast: (message) ->
+ for client in @clients
+ client.sendMessage(message)
+
+ # Creates a tank for a connection and synchronizes it to everyone. Then tells the connection
+ # that this new tank is his.
+ createPlayer: (ws, name) ->
+ ws.tank = @spawn Tank
+ packet = @changesPacket(yes)
+ packet = new Buffer(packet).toString('base64')
+ @broadcast packet
+
+ ws.tank.name = name
+ @broadcast JSON.stringify
+ command: 'nick'
+ idx: ws.tank.idx
+ nick: name
+
+ packet = pack('BH', net.WELCOME_MESSAGE, ws.tank.idx)
+ packet = new Buffer(packet).toString('base64')
+ ws.sendMessage(packet)
+
# We send critical updates every frame, and non-critical updates every other frame. On top of
# that, non-critical updates may be dropped, if the client's hearbeats are interrupted.
sendPackets: ->
@@ -152,7 +193,7 @@ class BoloServerWorld extends ServerWorld
smallPacket = new Buffer(smallPacket).toString('base64')
largePacket = new Buffer(largePacket).toString('base64')
- for {client} in @tanks when client?
+ for client in @clients
if client.heartbeatTimer > 40
client.sendMessage(smallPacket)
else

0 comments on commit 309bf21

Please sign in to comment.