/* options = { onPlayerConnect(callback), onPlayerDisconnect(callback), onPlayerReconnect(callback), onPlayerUpdate(callback(state)), onEntityUpdate(callback(state)), onPlayerRemove(callback(id)), onEntityRemove(callback(id)), onEvent(callback(data)), onWorldState(callback(state)), onPing(callback(pingDelay)), onUpdateClientPredictionReady(callback(playerId, playerCurrentState, entityCurrentStates, inputs, deltaTime)), onInterpolation(callback(previousState, targetState, amount) : newState), onReady(callback), logging: true } api methods initializeGarageServer(path, options) addInput(input) getPlayerStates : [, playerState] getEntityStates : [, entityState] updatePlayerState(id, state) addEntity(id, state) updateEntityState(id, state) removeEntity(id) getId() : playerid sendServerEvent(data) */ var GarageServerIO = (function (socketio) { "use strict"; if (!socketio) { throw new Error("GarageServer.IO: Socket.IO not found. Please ensure socket.io.js is referenced before the garageserver.io.js file."); } function StateController() { this.clientTime = 0; this.renderTime = 0; this.physicsDelta = 0.0; this.id = ''; this.pingDelay = 100; this.interpolationDelay = 100; this.interpolation = false; this.pingInterval = 2000; this.clientSidePrediction = false; this.smoothingFactor = 0.3; this.worldState = {}; } StateController.prototype = { setTime: function (serverTime) { this.clientTime = serverTime; this.renderTime = this.clientTime - this.interpolationDelay; } }; function Input(input, seq) { this.input = input; this.seq = seq; } function InputController() { this.inputs = []; this.sequenceNumber = 1; } InputController.prototype = { any: function () { return this.inputs.length > 0; }, add: function (input) { this.sequenceNumber += 1; this.inputs.push(new Input(input, this.sequenceNumber)); }, remove: function (seq) { for (var i = 0; i < this.inputs.length; i ++) { if (this.inputs[i].seq === seq) { this.inputs.splice(0, i + 1); break; } } } }; function Update(state, seq, time) { this.state = state; this.seq = seq; this.time = time; } function Entity(id, referrerId, referrerSeq, maxUpdateBuffer) { this.updates = []; this.maxUpdateBuffer = maxUpdateBuffer; this.id = id; this.referrerId = referrerId; this.referrerSeq = referrerSeq; this.state = {}; this.inputController = new InputController(); } Entity.prototype = { addUpdate: function (state, seq, time) { var newUpdate = new Update(state, seq, time); if (this.updates.length === 0) { this.state = newUpdate.state; } this.updates.push(newUpdate); if (this.updates.length > this.maxUpdateBuffer) { this.updates.splice(0, 1); } }, updateState: function (state, seq, time) { var updateFound = false; this.updates.some(function (update) { if (update.seq === seq) { update.state = state; updateFound = true; return true; } }); if (!updateFound) { this.addUpdate(state, seq, time); } }, anyUpdates: function () { return this.updates.length > 0; }, latestUpdate: function () { return this.updates[this.updates.length - 1]; }, surroundingPositions: function (time) { var positions = {}; for (var i = 0; i < this.updates.length; i ++) { var previous = this.updates[i], target = this.updates[i + 1]; if (previous && target && time > previous.time && time < target.time) { positions.previous = previous; positions.target = target; break; } } return positions; } }; function Player(id, maxUpdateBuffer) { Entity.call(this, id, null, null, maxUpdateBuffer); } Player.prototype = Object.create(Entity.prototype); function EntityController(maxUpdateBuffer) { this.entities = []; this.maxUpdateBuffer = maxUpdateBuffer; } EntityController.prototype = { add: function (id, referrerId) { var referrerSeq = this.entities.filter(function (value) { return value.referrerId === referrerId; }).length; var entity = new Entity(id, referrerId, referrerSeq, this.maxUpdateBuffer); this.entities.push(entity); return entity; }, remove: function (id) { for (var i = 0; i < this.entities.length; i ++) { if (this.entities[i].id === id) { this.entities.splice(i, 1); return; } } } }; function PlayerController(maxUpdateBuffer) { EntityController.call(this, maxUpdateBuffer); } PlayerController.prototype = Object.create(EntityController.prototype); PlayerController.prototype.add = function (id) { var player = new Player(id, this.maxUpdateBuffer); this.entities.push(player); return player; }; var _io = socketio, _socket = null, _options = null, _stateController = new StateController(), _playerController = null, _entityController = null, initializeGarageServer = function (path, options) { _options = options; if(path == null || path.length <= 0) { throw new Error('GarageServer.IO client is missing the server path - please verify the path argument passed to GarageServerIO.initializeGarageServer.'); } _socket = _io.connect(path + '/garageserver.io'); registerSocketEvents(); }, registerSocketEvents = function () { _socket.on('connect', function () { _stateController.id = _socket.io.engine.id; if (_options.logging) { console.log('garageserver.io:: socket connect'); } if (_options.onPlayerConnect) { _options.onPlayerConnect(); } }); _socket.on('s', function (data) { if (_options.onWorldState) { _options.onWorldState(data.worldState); } _stateController.maxUpdateBuffer = data.maxUpdateBuffer; _playerController = new PlayerController(_stateController.maxUpdateBuffer); _entityController = new EntityController(_stateController.maxUpdateBuffer); _stateController.physicsDelta = data.physicsDelta; _stateController.interpolation = data.interpolation; _stateController.interpolationDelay = data.interpolationDelay; _stateController.pingInterval = data.pingInterval; _stateController.clientSidePrediction = data.clientSidePrediction; _stateController.smoothingFactor = data.smoothingFactor; _stateController.worldState = data.worldState; if(_options.onReady) { _options.onReady(); } setInterval(function (){ _socket.emit('p', new Date().getTime()); }, _stateController.pingInterval); }); _socket.on('disconnect', function () { if (_options.logging) { console.log('garageserver.io:: socket disconnect'); } if (_options.onPlayerDisconnect) { _options.onPlayerDisconnect(); } }); _socket.on('reconnect', function () { if (_options.logging) { console.log('garageserver.io:: socket reconnect'); } if (_options.onPlayerReconnect) { _options.onPlayerReconnect(); } }); _socket.on('u', function (data) { update(data); }); _socket.on('p', function (data) { _stateController.pingDelay = new Date().getTime() - data; if (_options.logging) { console.log('garageserver.io:: socket ping delay ' + _stateController.pingDelay); } if (_options.onPing) { _options.onPing(_stateController.pingDelay); } }); _socket.on('rp', function (id) { removePlayer(id); if (_options.logging) { console.log('garageserver.io:: socket removePlayer ' + id); } if (_options.onPlayerRemove) { _options.onPlayerRemove(id); } }); _socket.on('re', function (id) { removeEntity(id); if (_options.logging) { console.log('garageserver.io:: socket removeEntity ' + id); } if (_options.onEntityRemove) { _options.onEntityRemove(id); } }); _socket.on('e', function(data) { if (_options.logging) { console.log('garageserver.io:: socket event ' + data); } if (_options.onEvent) { _options.onEvent(data); } }); }, getId = function () { return _stateController.id; }, sendServerEvent = function (data) { _socket.emit('e', data); }, addInput = function (clientInput) { _playerController.entities.some(function (player) { if (player.id === _stateController.id) { if (_stateController.clientSidePrediction && _options.onUpdateClientPredictionReady) { var entityCurrentStates = []; _entityController.entities.forEach(function(entity) { if (entity.referrerId === player.id) { entityCurrentStates.push({ id: entity.id, state: entity.state }); } }); player.inputController.add(clientInput); _options.onUpdateClientPredictionReady(player.id, player.state, entityCurrentStates, [{ input: clientInput }], _stateController.physicsDelta); } _socket.emit('i', [ clientInput, player.inputController.sequenceNumber, _stateController.renderTime ]); } }); }, getPlayerStates = function () { var playerStates = []; if (_stateController.interpolation && _options.onInterpolation) { processEntityStatesInterpolated(_playerController); } else { processEntityStatesCurrent(_playerController); } _playerController.entities.forEach(function(player) { playerStates.push({ id: player.id, state: player.state }); }); return playerStates; }, getEntityStates = function () { var entityStates = []; if (_stateController.interpolation && _options.onInterpolation) { processEntityStatesInterpolated(_entityController); } else { processEntityStatesCurrent(_entityController); } _entityController.entities.forEach(function(entity) { entityStates.push({ id: entity.id, state: entity.state }); }); return entityStates; }, updatePlayerState = function (id, state) { _playerController.entities.some(function(player) { if (player.id === id) { player.state = state; return true; } }); }, removePlayer = function (id) { _playerController.remove(id); }, addEntity = function (id, state) { _entityController.add(id, _stateController.id).state = state; }, updateEntityState = function (id, state) { _entityController.entities.some(function(entity) { if (entity.id === id) { entity.state = state; return true; } }); }, removeEntity = function (id) { _entityController.remove(id); }, update = function (data) { _stateController.setTime(data.t); updatePlayers(data); updateEntities(data); }, updatePlayers = function (data) { data.ps.forEach(function (playerState) { updateEntity(_playerController, playerState, data.t); if (_options.onPlayerUpdate) { _options.onPlayerUpdate(playerState[1]); } }); }, updateEntities = function (data) { data.es.forEach(function (entityState) { updateEntity(_entityController, entityState, data.t); if (_options.onEntityUpdate) { _options.onEntityUpdate(entityState[1]); } }); }, updateEntity = function (entityController, entityState, time) { var entityFound = false; entityController.entities.some(function (entity) { if (entity.id === entityState[0]) { entityFound = true; entity.updateState(entityState[1], entityState[2], time); return true; } }); if (!entityFound) { var newEntity = entityController.add(entityState[0]); newEntity.addUpdate(entityState[1], entityState[2], time); } }, processEntityStatesCurrent = function (entityController) { entityController.entities.forEach(function (entity) { if (entity.anyUpdates() && !entity.inputController.any()) { entity.state = entity.latestUpdate().state; } }); }, processEntityStatesInterpolated = function (entityController) { var positions, amount, newState; entityController.entities.forEach(function (entity) { if (entity.anyUpdates() && !entity.inputController.any()) { positions = entity.surroundingPositions(_stateController.renderTime); if (positions.previous && positions.target) { amount = getInterpolatedAmount(positions.previous.time, positions.target.time); newState = _options.onInterpolation(positions.previous.state, positions.target.state, amount); entity.state = newState = _options.onInterpolation(entity.state, newState, _stateController.smoothingFactor); } else if (entity.inputController.sequenceNumber === 1) { entity.state = entity.latestUpdate().state; } } }); }, getInterpolatedAmount = function (previousTime, targetTime) { var range = targetTime - previousTime, difference = _stateController.renderTime - previousTime, amount = parseFloat((difference / range).toFixed(3)); return amount; }; return { initializeGarageServer: initializeGarageServer, addInput: addInput, getPlayerStates: getPlayerStates, getEntityStates: getEntityStates, updatePlayerState: updatePlayerState, addEntity: addEntity, updateEntityState: updateEntityState, removeEntity: removeEntity, getId: getId, sendServerEvent: sendServerEvent }; }) (window.io);