DAN ZEN EXPO - CODE EXHIBIT -
ROBIN
package samples{
import flash.display.MovieClip;
import flash.display.Sprite;
import flash.events.*;
import flash.utils.Timer;
import flash.filters.*;
import flash.geom.Rectangle;
// see the Robin.as file located at the path below for instructions on installing classes
import com.danzen.utilities.Robin;
// Robin has a simple concept but is very powerful
// to get the most out of Robin, please read every worm - oops, word of this description
// ROBIN INTRODUCTION
// Robin is a custom Flash AS3 class with supporting NodeJS to handle realtime communication
// realtime communication is used for chats and multiuser games with avatars, etc.
// http://robinflash.wordpress.com - by inventor Dan Zen - http://www.danzen.com
// The backend was initially PHP but was switched over to NodeJS for http://droner.mobi
// if you are using Robin for commercial purposes, you are welcome to donate to Dan Zen
// donations can be made to agency@danzen.com at http://www.paypal.com
// ROBIN OVERVIEW
// this is the Flash overview - you have to have set up the NodeJS side too.
// Robin works by distributing properties (strings or numbers)
// you can have any number of properties and you make up their (single word) names
// you create a Robin object and set events for connection and incoming data
// then you set properties and read other people's matching properties
// for instance, you can set your x, y and text properties
// then you can read other people's x, y and text properties
// you use Robin's setProperty() or setProperties() methods to set properties
// you use Robin's getProperties() or getSenderProperty() methods to get properties
// COMPARATIVE NOTE
// Robin is different than the SharedObject class in Flash
// the SharedObject class lets you share the same properties
// Robin lets each person have unique properties
// However, you can use the latest values to emulate SharedObject functionality with Robin
// Union (formerly Unity) at http://www.unionplatform.com provides an alternative
// Union uses Java in the backend and Robin uses NodeJS
// Union has more structure whereas Robin is just setting and getting properties
// ROOMS WITH ROBIN
// there is no official keyword called rooms but it is a good analogy for chats, etc.
// the properties you can set and get with Robin are for your "room" only
// when you create the Robin object, the first three parameters affect the room
// 1. the first parameter is the application name so this in a sense sets up the "house"
// you make up this name and it will probably just be the name of your app
// 2. the second parameter is how many people are allowed in a "room"
// this defaults to 1000000 so if you leave it out then it is like one big room
// setting 3 as the maximum per room means that people are added to "rooms" of no more than 3
// more rooms are created as more people join
// 3. the third parameter tells Robin how to fill the rooms
// sending true means that Robin fills in places where people have left
// sending false means that Robin always adds people to the latest room to be created
// this defaults to true (filling vacated places) which would be the most common way
// but if you had a game where four people are playing and one loses and leaves
// you may not want to fill in a new person before the game has reached conclusion
// rooms are always removed along with the room data when the last person leaves the room
// getProperties() may still have a length even with no room data
// NOTE: you should always call the numPeople property to get the number of people in your room
// SETTING PROPERTIES
// any property that you set on Robin gets sent to the server
// the server updates the property then distributes all the properties to everyone else in the same room
// you can set a single property or you can set multiple properties at once
// both of these will send an update to everyone else in the room
// so be careful - if you update two properties individually, two updates get sent
// if you have assigned the Robin object to a variable called myRobin then use:
// myRobin.setProperty("x", 27);
// this will send your updated x property value to the server
// the server will then distribute an update with all the properties including your new x property
// this update goes to all the people in the room except you
// to send x, y and text properties at the same time use:
// myRobin.setProperties({x:27, y:32, text:"hello worms"});
// the server will then distribute an update with all the properties including your changed ones
// note that we use an Object literal above to specify properties - optionally you could use:
// var myObject:Object = new Object();
// myObject.x = 27;
// myObject.y = 32;
// myObject.text = "hello worms";
// myRobin.setProperties(myObject);
// RECEIVING PROPERTIES
// let's assume that you are setting x, y and text properties on Robin
// you can get the value of your x property, for instance, like so:
// myRobin.getProperty("x");
// Robin's DataEvent.DATA event is triggered when someone else in your room sets a property
// you can then check for current values of other people in the room's properties
// here is the code to get everyone in the room's x property (except yours):
// myRobin.getProperties("x");
// this returns an Array of everyone in your room's x property (except yours)
// you can find out an Array of names of the properties that are available like so:
// myRobin.getPropertyNames();
// someone must have changed a property for your data event to be triggered
// here is how you find out the index number of that someone - the sender:
// myRobin.senderIndex;
// so you could find out the sender's x property like so:
// myRobin.getProperties("x")[myRobin.senderIndex]; // but use below instead
// or you can use the getSenderProperty() method instead like so:
// myRobin.getSenderProperty("x");
// all the properties per room are always received by the data event (except yours)
// this takes up extra bandwidth - but it keeps things simple
// also, you always know that the data is coming from the server and is current
// Always use the numPeople property to find out how many people are in the room
// LATEST VALUES
// with the getLatestValue() method you can find out the last value distributed for a property
// this might include your value
// this value can be used to emulate the Flash remote SharedObject functionality
// for instance, to let everyone control the x and y position of a ball as well as their own x and y
// you could set x and y properties on Robin for yourself and ballX and ballY for the ball
// for the ball, you would always position it to the latest values of ballX and ballY like so:
// ball.x = myRobin.getLatestValue("ballX");
// ball.y = myRobin.getLatestValue("ballY");
// you can get the index of the sender who changed the latest value like so:
// getLatestValueIndex("ballX"); // returns a number
// this returns the index of the sender who updated ballX or -1 if you were the last to update ballX
// you can get an Array of the latest properties that were updated
// getLatestProperties(); // returns an Array of property names as strings
// HISTORY
// you will always get the current properties from the server each data trigger
// but sometimes you want a history of what has transpired
// this could be a leading scorer or the last 20 messages in a chatroom, etc.
// Robin puts the contents of the history file into a history property at connect time
// myRobin.history; // this is only updated at connect time
// You can append to and clear a history file that is shared by the room like so:
// myRobin.appendToHistory("a line of text\n"); // note that you need to add the \n for new lines
// myRobin.clearHistory();
// the clearHistory() clears the history for new people coming to the room
// it does not clear the history property for the people already in the room
// and of course anyone can appendToHistory() after it is cleared
public class AvatarsChoice extends MovieClip {
private var myRobin:Robin;
private var myAvatar:MovieClip;
private var myTimer:Timer;
private var totalAvatars:Number;
private var players:Array=[];
private var myFrame:Number;
private var myX:Number;
private var myY:Number;
public function AvatarsChoice():void {
trace("hi from AvatarsChoice");
myAvatar = new Avatar();
totalAvatars=myAvatar.totalFrames;
var myMenu:MovieClip = new MovieClip();
var choice:Avatar;
var spacing:Number=90;
for (var i:uint=1; i<=totalAvatars; i++) {
choice = new Avatar();
choice.gotoAndStop(i);
choice.x=i*spacing;
choice.y=100;
choice.alpha=.5;
myMenu.addChild(choice);
}
myMenu.buttonMode=true;
myMenu.addEventListener(MouseEvent.CLICK, doChoice);
myMenu.addEventListener(MouseEvent.MOUSE_OVER, function(e:MouseEvent):void {e.target.alpha=.7;});
myMenu.addEventListener(MouseEvent.MOUSE_OUT, function(e:MouseEvent):void {e.target.alpha=.5;});
myMenu.x=-60;
addChild(myMenu);
}
private function doChoice(e:MouseEvent):void {
myFrame=e.currentTarget.getChildIndex(MovieClip(e.target))+1;
myX=e.target.x-60;
myY=e.target.y;
myRobin=new Robin("RobinAvatarsChoice");
myRobin.addEventListener(Event.CONNECT, init);
myRobin.addEventListener(IOErrorEvent.IO_ERROR, doError); // connect problems
}
private function init(e:Event):void {
trace("Connected!");
myRobin.addEventListener(DataEvent.DATA, receiveData);
myRobin.addEventListener(Robin.CLIENT_JOINED, playerJoined);
myRobin.addEventListener(Robin.CLIENT_LEFT, playerLeft);
removeChildAt(1);// Robin icon is child 0
var padding:Number=100;
myAvatar.gotoAndStop(myFrame);
myAvatar.x=myX;
myAvatar.y=myY;
myAvatar.buttonMode=true;
myAvatar.filters=[new DropShadowFilter(4,45,0,.7,10,10,1,3)];
addChild(myAvatar);
// record the avatar frame, x and y in Robin
myRobin.setProperties({avatar:myFrame, x:myX, y:myY});
// place any other avatars
placeOthers();
myAvatar.addEventListener(MouseEvent.MOUSE_DOWN, dragMe);
}
private function dragMe(e:MouseEvent):void {
stage.addEventListener(MouseEvent.MOUSE_MOVE, moveMe);
stage.addEventListener(MouseEvent.MOUSE_UP, dropMe);
myAvatar.startDrag(false,new Rectangle(0,0,stage.stageWidth-myAvatar.width,stage.stageHeight-myAvatar.height));
}
private function moveMe(e:MouseEvent):void {
myRobin.setProperties({x:myAvatar.x, y:myAvatar.y});
}
private function dropMe(e:MouseEvent):void {
stage.removeEventListener(MouseEvent.MOUSE_MOVE, moveMe);
stage.removeEventListener(MouseEvent.MOUSE_UP, dropMe);
myAvatar.stopDrag();
myRobin.setProperties({x:myAvatar.x, y:myAvatar.y});
}
private function receiveData(e:Event):void {
moveOthers();
}
private function playerJoined(e:Event):void {
var i:Number = myRobin.lastIndexToJoin;
players[i] = new Avatar();
players[i].gotoAndStop(Number(myRobin.getSenderProperty("avatar")));
addChild(players[i]);
addChild(myAvatar); // put ours on top
}
private function playerLeft(e:Event):void {
var i:Number = myRobin.lastIndexToLeave;
if (players[i]) {
removeChild(players[i]);
players[i]=null;
}
}
private function placeOthers():void {
if (myRobin.numPeople>0) {
var xProp:String;// x of an avatar
var yProp:String;// y of an avatar
var aProp:String;// frame number of an avatar
// there could be blanks in getProperties from people leaving
// so always cycle through the full length of getProperties
// instead of cycling through numPeople
for (var i:uint=0; i<myRobin.getProperties("x").length; i++) {
trace ("here " + i);
xProp=myRobin.getProperties("x")[i];
yProp=myRobin.getProperties("y")[i];
aProp=myRobin.getProperties("avatar")[i];
trace ("here2 " + i);
if (xProp!="") { // make sure non blank
players[i] = new Avatar();
players[i].x=Number(xProp);
players[i].y=Number(yProp);
players[i].gotoAndStop(Number(aProp));
addChild(players[i]);
}
}
addChild(myAvatar);
}
}
private function moveOthers():void {
if (myRobin.numPeople>0) {
var xProp:String = myRobin.getSenderProperty("x");
var yProp:String = myRobin.getSenderProperty("y");
if (xProp!="") {
players[myRobin.senderIndex].x=Number(xProp);
players[myRobin.senderIndex].y=Number(yProp);
}
}
}
private function doError(e:Event):void {
trace ("error connecting to robinServer");
}
}
}
package com.danzen.utilities {
// ROBIN INTRODUCTION
// Robin is a custom Flash AS3 class with supporting NodeJS to handle realtime communication
// realtime communication is used for chats and multiuser games with avatars, etc.
// http://robinflash.wordpress.com - by inventor Dan Zen - http://www.danzen.com
// The backend was initially PHP but was switched over to NodeJS for http://droner.mobi
// if you are using Robin for commercial purposes, you are welcome to donate to Dan Zen
// donations can be made to agency@danzen.com at http://www.paypal.com
// INSTALLING CLASSES
// suggested installation:
// create a "classes" folder on your hard drive - for example c:\classes
// add the classes folder to your Flash class path:
// Flash menu choose Edit > Preferences > ActionScript - ActionScript 3 Settings
// then use the + sign for the source path box to add the path to your classes folder
// put the provided com/ directory with its folders and files in the classes folder
// the readme has more information if you need it
// USING ROBIN
// please make sure that com/danzen/utilities/Robin.as is in a folder in your class path
// set the configuration below to match the robinServer.js file port
// make sure you run your nodeJS as recommended
// the Flash side will not connect unless you have nodeJS running properly
// there is a list of methods, events and properties below for reference
// see the example files that come with Robin to see how to work with Robin
// CONFIGURATION
// the myServer variable must match the domain that is hosting your robinServer (no http://)
// you can set myServer to "localhost" if you are running nodeJS locally
// the myPort variable needs to match what you set in the robinServer.js for the port
import flash.display.Sprite;
import flash.net.XMLSocket;
import flash.system.Security;
import flash.utils.Timer;
import flash.events.*;
public class Robin extends Sprite {
// just put your server domain without http:// for example www.danzen.com
// see RobinNest.zip for examples to run on your local host NodeJS server
//private var myServer:String = "127.0.0.1";
private var myServer:String = "54.209.193.48";
//private var myServer:String; // now obtained with constructor parameter
private var myPort:Number = 8081;
// see the example files that call the Robin class
// CONSTRUCTOR
// Robin(theApplicationName:String, theMaxPeople:uint = 0, theFill:Boolean = true, theStartProperties:Object = null):void
// constructor to connect your application to the server.js Node socket for multiuser functionality
// server.js must be running for this to work. It works in your Flash authoring tool too (AS3)
// PARAMETERS:
// theApplicationName:String
// you make this up - it should be one word (or camel case) and probably unique for your app
// theMaxPeople:uint
// how many people are allowed per "room" - there are as many rooms as needed - 0 is virtually unlimited
// theFill:Boolean
// if someone leaves a room - set to true to fill in their location with the next person to join
// EXAMPLES:
// at its simplest making a new Robin object might look like this:
// var myRobin:Robin = new Robin("FreezeTag");
// or you could add some limitations:
// var myRobin:Robin = new Robin("FreezeTag", 10, false);
// EVENTS
// IOErrorEvent.IO_ERROR
// trouble connecting - make sure server.js is running and you have the right domain and port (see CONFIGURATION)
// Event.CONNECT
// connected to socket - you can set up your application to submit and receive data, etc.
// DataEvent.DATA
// dispatched when someone in the room makes a change (not including you)
// Event.CLOSE
// the socket is closed - could be that server.js stops for some reason - all data on the server will be lost
// Robin.CLIENT_JOINED
// another client (not you) joined - can find out with lastIndexToJoin property
// Robin.CLIENT_LEFT
// another client (not you) left - can find out with lastIndexToLeave property
// METHODS
// setProperty(propertyName:String, propertyValue:Object):void
// sets your property to the value and sends out change to all in room (distributes)
// setProperties(objectOfPropertiesToSet:Object):void
// pass in an object with properties and values and it sets yours to match and distributes them
// getProperty(propertyName:String):String
// returns your value for the property you pass to it
// getProperties(propertyName:String):Array
// returns an array of everyone in the room's values for the property you pass to it (can include blanks! see numPeople property)
// getPropertyNames():Array
// returns an array of all property names that have been distributed
// getSenderProperty(propertyName:String):String
// returns the value of the property you pass it that belongs to the last person to distribute (not you)
// getLatestValue(propertyName:String):String
// returns the last distributed value for the property you pass to it - could be yours - could be null if person leaving
// getLatestValueIndex(propertyName:String):Number
// returns the index number of the last person to distribute a value for the property you pass to it
// getLatestProperties():Array
// returns an array of the last properties to be distributed (sometimes multiple properties are distributed at once)
// appendToHistory(someText:String=""):void
// adds the text passed to it to the history file for the room (deleted if room is empty)
// clearHistory():void
// deletes the history file for the room
// dispose():void
// removes listeners, closes connection to socket, deletes data objects
// PROPERTIES (READ ONLY) Getter Methods at bottom
// applicationName:String - the name of your application
// server:String - the server you set in the CONFIGURATION
// port:Number - the port you set in the CONFIGURATION
// maxPeople:Number - see CONSTRUCTOR
// numPeople:Number - how many people are in the room - do not go by the length of a getProperties() array
// fill:Boolean - see CONSTRUCTOR
// senderIndex:Number - the index number of the last person to send out data
// history:String - the history text for your room at the time of application start
// lastIndexToJoin:Number - the index of the last client to join (other than you)
// lastIndexToLeave:Number - the index of the last client to leave (other than you)
// PUBLIC CONSTANTS
public static const CLIENT_JOINED:String = "clientJoined";
public static const CLIENT_LEFT:String = "clientLeft";
// PRIVATE VARIABLES
// getter methods allow you to get these properties (less the "my" prefix)
private var myApplicationName:String;
private var myMaxPeople:uint;
private var myFill:Boolean;
private var mySenderIndex:Number;
private var myHistory:String = "";
private var myLastIndexToJoin:Number;
private var myLastIndexToLeave:Number;
private var disposeCheck:Boolean = false;
// public methods are available to get this data
private var myData:Object = new Object();
private var theirData:Object = new Object();
private var latestValues:Object = new Object();
private var latestIndexes:Object = new Object();
private var latestProperties:Array = [];
// internal variables
private var mySocket:XMLSocket;
private var initializationCheck:Boolean = false;
private var myTimer:Timer;
private var connectCheck:Boolean = false;
private var lastIndexes:Array = [];
private var pingTimer:Timer;
private var connectAttempt:Number = 0;
private var errorCheck:Boolean = false;
private var errorTimer:Timer;
public function Robin(
theApplicationName:String,
theMaxPeople:uint = 0,
theFill:Boolean = true,
theServer:String = "127.0.0.1"
) {
trace ("hi from Robin");
trace (theServer);
myApplicationName = theApplicationName;
if (!myApplicationName.match(/^[a-zA-Z0-9_-]+$/)) {
trace ("----------------------------------");
trace ("Robin Application Name: \""+myApplicationName+"\"");
trace ("Sorry - your application name must include only a-z, A-Z, numbers, _ or -");
trace ("----------------------------------");
return;
}
myMaxPeople = (theMaxPeople == 0) ? 1000000 : theMaxPeople;
myFill = theFill;
myServer = theServer;
mySocket = new XMLSocket();
mySocket.timeout = 10;
var fixTimer:Timer = new Timer(100,1); // create a delay for any error events
fixTimer.addEventListener(TimerEvent.TIMER_COMPLETE, fix);
fixTimer.start();
errorTimer = new Timer(500, 1);
errorTimer.addEventListener(TimerEvent.TIMER, doErrorTimer);
pingTimer = new Timer(15*1000); // create ping to defeat server timeOut (set to 15 seconds)
pingTimer.addEventListener(TimerEvent.TIMER, ping);
}
// -------------------- PRIVATE METHODS -------------------------------
private function fix(e:TimerEvent):void {
getPolicy();
makeConnection();
}
private function ping(e:TimerEvent):void {
if (mySocket) {
mySocket.send("p"); // send ping to socket server
}
}
private function getPolicy():void {
Security.loadPolicyFile("xmlsocket://"+myServer+":"+myPort);
mySocket.connect(myServer, myPort);
mySocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, policyDelay);
mySocket.addEventListener(IOErrorEvent.IO_ERROR, function(e:Event):void{});
mySocket.close();
}
private function policyDelay(e:Event):void {
// do not want to try and access socket before policy returns
// so bounce errors to the makeConnection until the policy connects
// Web issue only
// errorCheck monitors how many times this has happened
// as there may be no socket connection to connect
if (!errorCheck) {
makeConnection();
}
}
private function makeConnection():void {
mySocket.connect(myServer, myPort);
mySocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, noConnect);
mySocket.addEventListener(Event.CONNECT, handleMeta);
mySocket.addEventListener(Event.CLOSE, handleMeta);
mySocket.addEventListener(IOErrorEvent.IO_ERROR, handleMeta);
mySocket.addEventListener(DataEvent.DATA, incomingData);
}
private function noConnect(e:Event):void {
connectAttempt++;
if (connectAttempt > 40) {
if (errorCheck) {return;}
errorCheck = true;
dispatchEvent(new Event(IOErrorEvent.IO_ERROR));
// wait to dispose in case delayed security error
errorTimer.start();
}
}
private function doErrorTimer(e:TimerEvent):void {
dispose();
}
private function handleMeta(e:Event):void {
switch(e.type){
case 'ioError':
//trace ("error");
dispatchEvent(new Event(IOErrorEvent.IO_ERROR));
if (pingTimer) {
pingTimer.stop();
}
break;
case 'connect':
if (!initializationCheck) {
initializationCheck = true;
sendData();
pingTimer.start();
}
break;
case 'close':
//trace ("close");
dispatchEvent(new Event(Event.CLOSE));
dispose();
break;
}
}
private function incomingData(e:DataEvent):void {
// GENERAL DATA FORMAT
// HISTORY -~^~- LATEST -~^~- VARIABLES
// HISTORY -~^~- LATEST -~^~- is only sent to new clients once at start
// HISTORY -~^~- LATEST -~^~- is not sent to existing clients
// VARIABLES are sent every time although ignored if client is leaving
// EXAMPLE FORMAT NEW CLIENT
// history as string
// -~^~-\n // delimeter then the latest users for the properties
// _u_^1 // the second user was last to update anything
// text^0|value // the first user was the last to update the text and its value
// x^1|value // the second user was the last to update x and y then its value
// y^1|value
// -~^~- // delimeter then the VARIABLES section - this shows sample for new client
// -1 // mySenderIndex is -1 as the client sent message to themself
// // blank line for variables sent as nobody sent variables - client is new
// _u_^12 // etc. for VARIABLES section (see later below for processing that section)
// SPLIT DATA
var latestData:String
var incomingData:String;
//trace ("DATA:\n-----------\n"+e.data+"\n-----------");
if (!String(e.data)) {return;}
if (!connectCheck) {
var feed:Array = String(e.data).split("-~^~-\n");
myHistory = decode(feed[0]).replace(/\n$/,"");
latestData = feed[1].replace(/\n$/,"");
incomingData = feed[2].replace(/\n$/,"");
} else {
incomingData = String(e.data).replace(/\n$/,"");
}
// VARIABLES FORMAT
// 0 // -1 if new user or -2 if a user is leaving
// x|y // blank return if new user or a user is leaving
// _u_^12|22 // could be blanks in here for users who have left
// text^test|hello // might be blanks in here for no updates from user or they left
// x^27|30
// y^25|200
// PROCESS VARIABLES
var lines:Array = incomingData.split("\n"); // line locked data
mySenderIndex = Number(lines.shift());
var updatedProperties:String = lines.shift();
/* needs to be after we check
if (getProperties("_u_")) {
lastIndexes = getProperties("_u_").slice(0);
}
*/
// for new clients and normal messages - not for leaving room
//if (connectCheck == false || mySenderIndex > -1) {
theirData = {};
var temp:Array;
var prop:String;
var temp2:Array;
for (var i:uint=0; i<lines.length; i++) {
if (lines[i] == "") {continue;}
temp = lines[i].split("^");
prop = temp[0];
if (temp[1] != "") {
temp2 = temp[1].split("|");
for (var j:uint=0; j<temp2.length; j++) {
temp2[j] = decode(temp2[j]);
}
theirData[prop] = temp2;
//theirData[prop] = decode(temp[1]).split("|");
}
}
//}
// PROCESS LATEST
// if -2 values - someone has left
if (connectCheck==false) { // new client get data from latestData
// _u_^1 // the second user was last to update anything
// text^0|value // the first user was the last to update the text and its value
// x^1|value // the second user was the last to update x and y then its value
// y^1|value
latestProperties = [];
var latestLines:Array = latestData.split("\n");
var latestVV:Array;
for (var iii:uint=0; iii<latestLines.length; iii++) {
if (latestLines[iii] != "") {
latestVV = latestLines[iii].split("^"); // [_u_, 0|value] then [text, 0|value]
latestIndexes[latestVV[0]] = Number(latestVV[1].split("|")[0]);
latestValues[latestVV[0]] = String(latestVV[1].split("|")[1]);
}
}
} else { // existing client
if (mySenderIndex > -1) {
if (updatedProperties == "") {
// should always be something for a normal send but just in case
} else {
latestProperties = updatedProperties.split("|");
for (var ii:uint=0; ii<latestProperties.length; ii++) {
// already added all normal sender data to theirData[v]=[value|value|value]
// already set mySenderIndex
// getSenderProperty(property) gets the value at the mySenderIndex for the property
latestValues[latestProperties[ii]] = getSenderProperty(latestProperties[ii]);
latestIndexes[latestProperties[ii]] = mySenderIndex;
}
}
}
}
if (connectCheck == false) { // client joined
//trace("Robin - connect");
dispatchEvent(new Event(Event.CONNECT));
connectCheck = true;
} else if (mySenderIndex == -2) { // sender leaving - figure out who left
if (hasLeft()) {
// set latestIndex belonging to leaving index to -2
for (var vari:String in latestIndexes) {
if (latestIndexes[vari] == lastIndexToLeave) {
latestIndexes[vari] = -2;
// do not update values
// this gives user choice to take a value of someone who left
// for instance in a shared ball position
// or ignore the last value if they check the index first
}
}
}
dispatchEvent(new Event(Robin.CLIENT_LEFT));
} else {
if (hasJoined(mySenderIndex)) { // sender joined
// new client - dispatch CLIENT_JOINED
// trace("Robin - Joined");
dispatchEvent(new Event(Robin.CLIENT_JOINED));
}
dispatchEvent(new DataEvent(DataEvent.DATA, false, false, String(e.data)));
}
if (getProperties("_u_")) {
lastIndexes = getProperties("_u_").slice(0);
}
if (mySenderIndex == -2) {
lastIndexes[lastIndexToLeave] = "";
}
}
private function hasLeft():Boolean {
var list:Array = [];
if (!getProperties("_u_")) {
for (var j:uint=0; j<lastIndexes.length; j++) {
list.push("");
}
} else {
list = getProperties("_u_");
}
for (var i:uint=0; i<lastIndexes.length; i++) {
if (lastIndexes[i] != "" && lastIndexes[i] != list[i]) {
myLastIndexToLeave = i;
return true;
}
}
return false;
}
private function hasJoined(clientIndex:Number):Boolean {
if (clientIndex >= lastIndexes.length || lastIndexes[clientIndex] == "") {
myLastIndexToJoin = clientIndex;
return true;
} else {
return false;
}
}
private function sendData(s:String=""):void {
var f:Number;
if (myFill) {f=1;} else {f=0;}
var prefix:String = "%^%"+myApplicationName+"|"+myMaxPeople+"|"+f;
// %^%sample9|3|1^y=5^x=20
// the %^% makes sure that if there are multiple entries in the socket buffer
// that they can be separated in the server code
if (mySocket.connected) {
mySocket.send(prefix+s);
} else {
//trace ("crash");
dispatchEvent(new Event(Event.CLOSE));
dispose();
}
}
private function encode(w:String):String {
w = w.replace(/\r\n/g,"\n");
w = w.replace(/\n\r/g,"\n");
w = w.replace(/\n/g,"`~`");
w = w.replace(/\^/g,"*`*");
//w = w.replace(/\|/g,"^~^");
w = w.replace(/\|/g,"%`%");
return w;
}
private function decode(w:String):String {
w = w.replace(/`~`/g,"\n");
//w = w.replace(/^~^/g,"|");
w = w.replace(/%`%/g,"|");
w = w.replace(/\*`\*/g,"^");
return w;
}
// -------------------- PUBLIC METHODS -------------------------------
public function setProperty(n:String, v:Object):void {
// set one of my properties at a time
myData[n] = v;
sendData("^" + n + "=" + encode(String(v)));
}
public function setProperties(o:Object):void {
// set a bunch of my properties at once with an object
var vars:String = "";
for (var i:Object in o) {
myData[i] = String(o[i]);
vars += "^" + i + "=" + encode(String(o[i]));
}
sendData(vars);
}
public function getProperty(w:String):String {
// get one of my properties
return myData[w];
}
public function getProperties(w:String):Array {
// get an array of their properties
return theirData[w];
}
public function getPropertyNames():Array {
// get a list of their property names
var props:Array =[];
for (var i:String in theirData) {
props.push(i);
}
return props;
}
public function getSenderProperty(w:String):String {
if (theirData[w] && theirData[w][mySenderIndex]) {
return theirData[w][mySenderIndex];
} else {
return null;
}
}
public function getLatestValue(w:String):String {
if (latestValues[w]) {
return latestValues[w];
} else {
return null;
}
}
public function getLatestValueIndex(w:String):Number {
return Number(latestIndexes[w]);
}
public function getLatestProperties():Array {
return latestProperties;
}
public function appendToHistory(t:String=""):void {
sendData("^_history_=" + encode(String(t)));
}
public function clearHistory():void {
sendData("^_clearhistory_=");
}
public function dispose():void {
// avoid double disposing
if (disposeCheck) {return;}
disposeCheck = true;
mySocket.removeEventListener(Event.CONNECT, handleMeta);
mySocket.removeEventListener(IOErrorEvent.IO_ERROR, handleMeta);
mySocket.removeEventListener(DataEvent.DATA, incomingData);
mySocket.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, policyDelay);
mySocket.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, noConnect);
if (mySocket.connected) {
mySocket.close();
}
mySocket.removeEventListener(Event.CLOSE, handleMeta);
mySocket = null;
pingTimer.stop();
pingTimer.removeEventListener(TimerEvent.TIMER, ping);
pingTimer = null;
myData = null;
theirData = null;
latestValues = null;
latestIndexes = null;
latestProperties = null;
myHistory = null;
trace ("bye from Robin");
}
// ----------------------- PUBLIC READ ONLY PROPERTIES ---------------------
public function get applicationName():String {
return myApplicationName;
}
public function get server():String {
return myServer;
}
public function get port():Number {
return myPort;
}
public function get maxPeople():Number {
return myMaxPeople;
}
public function get numPeople():Number {
var list:Array = getProperties("_u_");
var num:uint = 0;
if (list != null && list.length > 0) {
for (var i:uint=0; i<list.length; i++) {
if (list[i] != "") {
num++;
}
}
}
return num;
}
public function get fill():Boolean {
return myFill;
}
public function get senderIndex():Number {
return mySenderIndex;
}
public function get history():String {
return myHistory;
}
public function get lastIndexToJoin():Number {
return myLastIndexToJoin;
}
public function get lastIndexToLeave():Number {
return myLastIndexToLeave;
}
}
}
/* robin.js
---- DZ commented and refactored -------
then took all old code and comments out and called file robinServer.js
*
* RobinJS
*
* Created for Robin Flash / PHP Multiuser Solution
* https://robinflash.wordpress.com/
*
* @author Andrew Blackbourn blackbourna@gmail.com
* @producer Dan Zen door@danzen.com
*
* */
ROBIN_LOGS = 'logs/debuglog.txt';
function RobinServer() {
// DZ GENERAL SETUP VARIABLES AND FUNCTIONS
var net = require('net');
var fs = require('fs');
var port = 8081;
try {
var env = JSON.parse(fs.readFileSync('/home/dotcloud/environment.json', 'utf-8'));
port = env['PORT_NODE'];
} catch (e) {}
var debug = true;
var splitter = "_______________________";
var dzSplitter = "\n\n***********************";
function debuglog(x) {
if (debug) console.log(x);
}
// .length for property maps
Object.size = function (obj) {
var size = 0,
key;
for (key in obj) {
if (obj.hasOwnProperty(key)) size++;
}
return size;
}
function getKeys(map) {
var keys = [];
for (var k in map)
keys.push(k);
return keys;
}
function getArray(map) {
var values = [];
for (var k in map)
values.push(map[k]);
return values;
}
// can change port using cmd args
// node filename.js -p 1234
if (process.argv.length == 4) {
if (process.argv[2].toUpperCase() == "-P" && !isNaN(process.argv[3])) {
port = process.argv[3];
}
}
// from http://stackoverflow.com/questions/646628/javascript-startswith
if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (str) {
return this.indexOf(str) == 0;
};
}
// append null when writing to socket
net.Socket.prototype.sendMessage = function (x) {
try {
if (this.write) this.write(x + "\0");
} catch (e) {
console.log(e);
}
}
// DZ MAIN CODE
// ApplicationPool holds all the ApplicationInstance objects
// ApplicationInstance objects hold a rooms arrays of Room objects
// Room objects have sockets array of Socket objects
// Room objects have data properties such as currentVariables and who set the data, etc.
// Room objects have a buildMessage() method that makes a relative message to send to each Socket
// Room objects have a broadcast() method that calls buildMessage() and sends the messages
// Socket objects have a sendMessage() method that sends data to the clients
// Socket objects have an clientIndex and a clientID
// Socket objects also store what ApplicationInstance and Room they are in
// Socket objects hold data variables sent to them
applicationPool = new ApplicationPool();
// DZ server has a data event that captures incoming socket data
// DZ after some initialization we run processMessage()
var server = net.createServer(function (socket) {
socket.on('data', function (data) {
//console.log(" INCOMING DATA: " + data.toString());
// Flash initially requests an XML policy file
if (data.toString().match("<policy-file-request/>")) {
var policy = "<?xml version=\"1.0\"?><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>";
this.sendMessage(policy);
return;
} else if (data.toString().startsWith("%^%")) {
// DZ used %^% to separate messages because in PHP
// DZ because would sometimes get two "messages" in data send
var messages = data.toString().split('%^%');
for (var m in messages) {
if (messages[m].length > 0) processMessage(messages[m], socket);
}
} else {
socket.write("INVALID HEADER");
}
});
}); // end net.createServer
function processMessage(data, socket) { // after policy handshake, will begin to recieve messages from client
// %^%AppName|maxInRoom|fillType^property=value^property=value
// e.g.
// %^%RobinBall|10|1^y=147^x=211.95
var message = data.split("|");
// message[0]=RobinBall
// message[1]=10
// message[2]=1^y=147^x=211.95
for (var m in message) { // remove null terminators
message[m] = message[m].replace("\u0000", "");
}
var appName = message[0];
var maxPerRoom = message[1];
// split name/val pairs
var variableMessages = message[2].split("^"); // DZ - refactored below
var fillType = variableMessages.shift(); // DZ - refactored below
//var variableIndex = message[2].indexOf("^");
//var fillType = message[2].substring(0, 1);
//var variableMessages = (variableIndex > -1) ? message[2].substring(variableIndex).split("^").slice(1) : [];
console.log(dzSplitter);
if (!socket.room) { // set up new user (new user will never be last sender, etc.)
console.log("MESSAGE IN: New Socket");
var message = '';
socket.application = applicationPool.getApplication(socket, appName, maxPerRoom, fillType);
socket.variables = {};
if (socket.room.history.length > 0) {
message += socket.room.history + "\n";
}
message += '-~^~-\n';
/* DZ - summary of LAST section (after History and before variables)
-~^~- // delimiter
_u_^1 // the last user index (relative to the receiving client) in the room to update
text^0|last value // the properties shared so far ^ the last user index to update the property and the value
x^1|last value
y^1last value
-~^~-
*/
// DZ convert to relative indexes
// DZ if the client is positioned before the other then they would be taken out of the index count
// DZ and the relative index of the other would be one less as below
function convertToRelative(client,other) {
return (other > client) ? other-1 : other;
}
var relativeIndex = convertToRelative(socket.clientIndex, socket.room.lastBroadcastIndex);
message += '_u_^' + relativeIndex + '\n'; // might be -1 if last client to update left - that's okay
var lastIndex, lastValue;
for (var v in socket.room.lastClientToUpdateVariable) {
lastIndex = socket.room.getLastClientToUpdateVariableIndex(v);
lastValue = socket.room.getLastClientToUpdateVariableValue(v);
relativeIndex = convertToRelative(socket.clientIndex, lastIndex);
message += v + '^' + relativeIndex + "|" + lastValue + '\n';
}
message += '-~^~-\n' + socket.room.buildMessage(socket, socket, -1); //DZ - -1 for new client
console.log('BEGIN INIT MESSAGE OUT');
console.log(splitter);
console.log(message);
console.log(splitter);
console.log('END INIT MESSAGE OUT');
socket.sendMessage(message);
// DZ note that new socket does not broadcast - only sends message back to itself, the new socket
// DZ it is up to client to send a second message with a change of property to trigger broadcast
socket.active = true;
socket.on('end', function () {
socket.room.removeSocket(socket);
if (socket.room.countNonEmptySockets() == 0) {
console.log("Last socket, cleared all sockets - deleted room");
//socket.room.history = '';
//socket.room.sockets = [];
//socket.room.lastBroadcastIndex = -1;
//socket.room.lastClientToUpdateVariable = {}; // DZ - just used to Object literal being a {}
for (var i=0; i<socket.application.rooms.length; i++) {
if (socket.application.rooms[i] == socket.room) {
socket.application.rooms.splice(i, 1);
}
}
console.log("Number of rooms now = " + socket.application.rooms.length);
}
});
} else {
console.log("MESSAGE IN: Data");
console.log('Recieved: ' + data);
var somethingChanged = false;
for (var v in variableMessages) { // update this socket's variable hashmap
var vSplit = variableMessages[v].split("=");
if (vSplit[0].toLowerCase() == '_history_') {
socket.room.history += vSplit[1];
} else if (vSplit[0].toLowerCase() == '_clearhistory_') {
socket.room.history = ''; // DZ changed it to = rather than +=
} else {
somethingChanged = true;
socket.variables[vSplit[0]] = vSplit[1];
}
}
// console.log(socket.variables);
if (somethingChanged) socket.room.broadcast(socket); // broadcast this user's variables
}
}
function ApplicationPool() { // not quite a singleton but will do for now...
this.applicationInstances = {}; // DZ just more used to {} than [] for associative array
this.getApplication = function (socket, name, maxPerRoom, fillType) {
if (!this.applicationInstances[name]) { // ApplicationInstance held in associate array
this.applicationInstances[name] = new ApplicationInstance(maxPerRoom, fillType, name);
}
this.applicationInstances[name].addSocket(socket);
return this.applicationInstances[name];
}
this.closeAllApplications = function () {
for (var a in this.applicationInstances) {
this.applicationInstances[a].closeApplicationInstance();
}
this.applicationInstances = null;
}
} // end ApplicationPool
function ApplicationInstance(maxSize, fillType, name) {
this.name = name;
this.getRoom = function () { // DZ gets last room
return this.rooms[this.rooms.length - 1];
}
this.addSocket = function (socket) {
// 0 = don't refill slot when user leaves room
var socketRoom = null;
var roomAvailable = false;
for (var r in this.rooms) {
if (this.rooms[r].hasRoomForOneMore()) {
socketRoom = this.rooms[r];
roomAvailable = true;
break;
}
}
//console.log("Room available: " + roomAvailable);
if (!roomAvailable) {
this.rooms.push(new Room(fillType, maxSize));
socketRoom = this.rooms[this.rooms.length - 1];
}
// add to empty socket if available, otherwise append to end!
var addedToRoom = false;
for (var s in socketRoom.sockets) {
if (socketRoom.sockets[s].clientID == '') {
// DZ when socket is removed socket is not deleted
// DZ it is set to active=false and clientID removed
// DZ the socket.clientIndex is kept so below works
socket.clientIndex = socketRoom.sockets[s].clientIndex
// DZ replace old socket with new socket
// DZ would it help to set the old socket to null first?
// DZ socketRoom.sockets[s] = null;
socketRoom.sockets[s] = socket;
addedToRoom = true;
//console.log('Added to open slot');
break;
}
}
if (!addedToRoom) {
socketRoom.sockets.push(socket);
socket.clientIndex = socketRoom.sockets.length - 1;
}
socket.clientID = socketRoom.getNewClientID();
socket.room = socketRoom;
debuglog('Added ' + socket.clientID + ' to ' + ((roomAvailable) ? ' existing' : ' newly created') + ' room\n');
}
if (!this.rooms) {
this.rooms = [new Room(fillType, maxSize)];
}
this.closeApplicationInstance = function () {
for (var r in this.rooms) {
this.rooms[r].closeRoom();
}
this.rooms = null;
}
} // end ApplicationInstance
function Room(fillType, maxSize) {
var self = this;
if (!this.sockets) {
this.fillType = fillType;
this.maxSize = maxSize;
this.sockets = [];
this.clientIDs = [];
this.lastBroadcastIndex = -1; // last client that sent data
this.history = "";
this.lastMessage = "";
this.currentVariables = {};
this.lastClientToUpdateVariable = {}; // DZ - now stores client ID and variable value
// DZ - do not think we use this
// this.lastSender = null;
}
this.closeRoom = function () {
for (var s in this.sockets) {
this.sockets[s] = null;
}
this.sockets = null;
}
this.buildMessage = function(sender, recievingSocket, idx) {
// DZ - format of the variables section of a message:
// DZ - does not include the sender receivingSocket's data
/*
relativeLastSenderIndex
lastSentPropertyName|lastSentPropertyName
_u_^clientID|clientID
propertyName^propertyValueClient0|propertyValueClient1
propertyName^propertyValueClient0|propertyValueClient1
// or for example:
0
x|y
_u_^6|22
x^5|10
y^20|30
*/
if (!recievingSocket
|| recievingSocket == sender && idx != -1
|| recievingSocket.clientID == '') return ''; // DZ - okay to be same socket if new socket building message to itself
var message = "";
var relativeIndex; // DZ - refactored a bit here as did not like storing idx as always relativeIndex
if (idx > -1) {
// DZ if the receiver (idx) is positioned before the sender then they would be taken out of the index count
// DZ and the relative index of the sender would be one less as below
relativeIndex = (sender.clientIndex > idx) ? sender.clientIndex - 1 : sender.clientIndex;
relativeIndex = relativeIndex % self.maxSize; // DZ not sure we need but okay (not if -2 % 2)
} else {
relativeIndex = idx; // DZ - either -1 (new) or -2 (leaving)
}
message += relativeIndex + "\n"; // socket relative to receiver's index
// DZ don't think we need below - the changed variables can just be left blank for a new client
// DZ we get all the variable names and the updaters from the history area in the process message
/* DZ
if (idx == -1) {
for (var s in this.sockets) {
console.log('socket')
for (var v in this.sockets[s].variables) {
console.log('variable');
var val = sender.variables[v];
// DZ if (!val && (val !== '') && (val !== 0)) {
// DZ did you mean if there is a val?
if (val && (val !== '') && (val !== 0)) {
console.log('added var');
sender.variables[v] = '';
}
}
}
}
*/
if (getKeys(sender.variables).length) {
message += getKeys(sender.variables).join("|") + "\n";
} else { // DZ - need at least a \n for Robin to parse properly - this section is line locked
message += "\n";
}
message += "_u_^" + self.getOtherClientIDs(recievingSocket).join('|') + '\n';
// DZ need to send all variables back every time - may not be most efficient
// DZ but the spec says it I think to simplify proceedure
// DZ most of the time, all variables are updated anyway - but not all the time
// DZ possible that this was creating some of the anomolies
// DZ for (var v in sender.variables) { // loop through sender's variables
for (var v in self.currentVariables) { // loop through all room variables
if (v.toLowerCase == '_history_' || v.toLowerCase == '_clearhistory_') continue;
// prepare message
message += v + "^";
var others = self.getOthersInRoom(recievingSocket);
var otherVars = [];
for (var o in others) {
if (others[o]) {
if (others[o].variables[v]) {
otherVars.push(others[o].variables[v]);
} else {
otherVars.push('');
}
}
}
message += otherVars.join('|') + "\n";
}
return message;
}
// DZ - refactored what happens when leaving
this.broadcast = function (sender, leavingCheck) {
console.log('BROADCAST CALLED');
console.log(this.countNonEmptySockets() + " users in the room");
if (leavingCheck) {
this.lastBroadcastIndex = -2; // DZ - just to distinguish because leaving
} else {
this.lastBroadcastIndex = sender.clientIndex; //DZ note absolute index
}
debuglog('Sender is: ' + sender.clientID);
debuglog("Sender's variables: ");
if (!leavingCheck) {
for (var v in sender.variables) { // loop through sender's variables
// used for _history messages
if (this.currentVariables[v] != sender.variables[v]) {
this.lastClientToUpdateVariable[v] = sender.clientIndex+"|"+sender.variables[v]; //DZ note absolute index
}
this.currentVariables[v] = sender.variables[v];
// DZ - do not think we use this?
// this.lastSender = sender;
}
}
// DZ below has been replaced by buildMessage()
// countNonEmptySockets counts based on whether clientID.length > 0, but clientID is empty when a socket is removed
/*if (this.countNonEmptySockets() == 1) { // only 1 socket in the room!
return;
var message = sender.clientIndex + '\n';
message += getKeys(sender.variables).join("|") + "\n";
message += "_u_^" + sender.clientID + '\n';
for (var v in sender.variables) {
if (v.toLowerCase == '_history_' || v.toLowerCase == '_clearhistory_') continue;
// prepare message
message += v + "^" + sender.variables[v] + '\n';
}
this.lastMessage = message;
console.log("Only 1 user in the room!");
//sender.sendMessage(message);
for (var s = 0; s < this.sockets.length; s++) {
var lastSocket = this.sockets[s];
lastSocket.room.broadcast(lastSocket);
}
return;
}*/
var sentCount = 0;
for (var s=0; s<this.sockets.length; s++) { // loop through non-sender sockets
var recievingSocket = this.sockets[s];
if (leavingCheck) {
var message = this.buildMessage(sender, recievingSocket, -2); // DZ - -2 for leaving
} else {
var message = this.buildMessage(sender, recievingSocket, s);
}
if (!message) continue; // DZ - buildMessage returns "" if sender and reciever is same
this.lastMessage = message;
//debuglog('SENDING TO: ' + recievingSocket.clientID);
//debuglog(message + splitter);
//console.log(message + splitter);
console.log("START DATA MESSAGE OUT");
console.log(splitter);
console.log(message);
console.log(splitter);
console.log("END DATA MESSAGE OUT");
recievingSocket.sendMessage(message);
// DZ this was already set - this.lastMessage = message;
sentCount++;
}
debuglog('Sent ' + sentCount + ' messages');
}
this.users = function () {
var users = 'List of users: ';
for (var s in this.sockets) {
if (this.sockets[s] == null) continue;
users += this.sockets[s].clientID + ' ';
}
return users;
}
this.removeSocket = function (socket) {
console.log(dzSplitter);
console.log("MESSAGE IN: Client Disconnected");
for (var i = 0; i < this.sockets.length; i++) {
if (this.sockets[i] == socket) {
/*
var s = this.sockets[i];
if (i == this.sockets.length - 1) {
for (var v in s.variables) {
s.variables[v] = '';
}
if (this.lastBroadcastIndex == s.clientIndex) this.lastBroadcastIndex = -1;
// Don't strip off the trailing socket!
//this.sockets.splice(i, 1);
} else {
if (this.lastBroadcastIndex == s.clientIndex) this.lastBroadcastIndex = -1;
for (var v in s.variables) {
s.variables[v] = '';
}
}
s.clientID = '';
s.room.broadcast(s, true);
s.active = false;
break;
*/
// DZ refactored the above code:
// DZ might want to delete socket.variables[v] but no big deal
var s = this.sockets[i];
for (var v in socket.variables) {
socket.variables[v] = '';
}
// DZ - moved handling lastBroadcastIndex to broadcast()
// DZ - if (this.lastBroadcastIndex == socket.clientIndex) this.lastBroadcastIndex = -1;
// DZ - set any lastClientToUpdateVariable with this index to -2, leave the value though
for (var v in this.lastClientToUpdateVariable) {
lastIndex = this.getLastClientToUpdateVariableIndex(v);
lastValue = this.getLastClientToUpdateVariableValue(v);
if (lastIndex == s.clientIndex) {
this.lastClientToUpdateVariable[v] = -2+"|"+lastValue; // DZ - -2 for client left
}
}
s.clientID = '';
this.broadcast(s, true);
s.active = false;
break;
}
}
console.log("Socket removed!");
// DZ - refactored the removing of room to after calling the removeSocket method
}
this.hasRoomForOneMore = function () {
if (this.fillType == 1) {
return this.countNonEmptySockets() < this.maxSize;
} else {
return this.sockets.length < this.maxSize;
}
}
this.countNonEmptySockets = function () {
var count = 0;
for (var s in this.sockets) {
// console.log('ID: '+this.sockets[s].clientID);
if (this.sockets[s].active) count++;
}
// console.log("There are " + count + " sockets currently in this room. MaxSize = " + this.maxSize);
return count;
}
this.getOthersInRoom = function (socket) {
var others = []; // DZ list of sockets (including inactive sockets)
for (var s in this.sockets) {
if (socket != this.sockets[s] && this.sockets[s] != null && this.sockets[s] !== undefined) others.push(this.sockets[s]);
}
return others;
}
this.getOtherClientIDs = function (socket) {
var others = this.getOthersInRoom(socket);
var otherIDs = [];
for (var o in others)
otherIDs.push(others[o].clientID);
return otherIDs;
}
this.getNewClientID = function () {
var id;
while (true) {
id = Math.floor(Math.random() * 1e6);
if (this.clientIDs.indexOf(id) == -1) break;
}
return id;
}
this.getLastClientToUpdateVariableIndex = function (v) {
return this.lastClientToUpdateVariable[v].split("|")[0];
}
this.getLastClientToUpdateVariableValue = function (v) {
return this.lastClientToUpdateVariable[v].split("|")[1];
}
} // end Room
this.startServer = function () {
server.listen(port);
console.log("Robin server started on port " + port + "...");
}
this.stopServer = function () {
applicationPool.closeAllApplications();
server.close();
}
this.setPort = function (customPort) {
this.port = customPort;
}
}
exports.robinServer = new RobinServer();