This commit is contained in:
Jeena 2014-03-01 23:11:36 +01:00
parent d83376d5c7
commit 810a74a28b
13 changed files with 445 additions and 281 deletions

1
.gitignore vendored
View file

@ -1,5 +1,4 @@
node_modules/
*.log
.DS_Store
pixi.js/
lab/audio/

View file

@ -9,7 +9,7 @@
function (GameController, Nc, User, ProtocolHelper, Options, Settings) {
function Channel (pipeToServer, name, options) {
function Channel (pipeToServer, options) {
var self = this;
@ -17,7 +17,7 @@
levelUids: Settings.DEFAULT_LEVELS
});
this.name = name;
this.name = options.channelName;
this.users = {};
this.pipeToServer = pipeToServer;
@ -34,11 +34,13 @@
Nc.on('broadcastGameCommand', this.broadcastGameCommand, this);
Nc.on('broadcastGameCommandExcept', this.broadcastGameCommandExcept, this);
console.checkpoint('channel ' + name + ' created');
}
console.checkpoint('channel ' + this.name + ' created');
Channel.validateName = function (name) {
return true;
setTimeout(function() {
if(Object.keys(self.users).length < 1) {
self.destroy();
}
}, Settings.CHANNEL_DESTRUCTION_TIME * 1000);
}
@ -81,10 +83,21 @@
Channel.prototype.onReleaseUser = function (userId) {
var user = this.users[userId];
this.broadcastControlCommandExcept("userLeft", user.id, user);
Nc.trigger('user/left', user);
delete this.users[user.id];
Nc.trigger('user/left', userId);
delete this.users[userId];
this.broadcastControlCommand("userLeft", userId);
// FIXME: if this was the last user terminate forked process
if(Object.keys(this.users).length < 1) {
this.destroy();
}
}
Channel.prototype.destroy = function() {
console.checkpoint("channel (" + this.name + ") destroyed");
this.pipeToServer.destroy();
};
// Sending commands

View file

@ -21,7 +21,7 @@ function (Parent, PhysicsEngine, Settings, PlayerController, requestAnimFrame, N
Parent.call(this);
Nc.on('user/joined', this.onUserJoined, this);
Nc.on('user/left', this.onUserLeft, this); // FIXME: refactor this.userLeft -> this.onUserLeft, even in core and client
Nc.on('user/left', this.onUserLeft, this);
Nc.on('user/resetLevel', this.onResetLevel, this);
Nc.on('user/clientReady', this.onClientReady, this);
Nc.on('player/killed', this.onPlayerKilled, this);

View file

@ -17,10 +17,9 @@ function (Nc, Channel) {
process.on('message', function (message, handle) {
if(message.data.hasOwnProperty('CREATE')) {
self.channel = new Channel(this, message.data['CREATE']);
self.channel = new Channel(self, message.data.options);
} else if (message.data.hasOwnProperty('KILL')) {
self.channel.destroy();
process.exit(0);
} else {
self.onMessage(message);
}
@ -41,6 +40,11 @@ function (Nc, Channel) {
Nc.trigger(message.recipient + '/controlCommand', message);
}
PipeToServer.prototype.destroy = function() {
this.send('coordinator', {destroy:this.channel.name});
this.process.exit(0);
};
return PipeToServer;
});

View file

@ -20,7 +20,8 @@ function (ProtocolHelper, GameController, User, Nc, Settings, DomController) {
var self = this;
this.socketLink.on('message', function (message) {
if(Settings.NETWORK_LOG_INCOMING) {
var m = JSON.parse(message)
if(Settings.NETWORK_LOG_INCOMING && !m.gameCommand) {
console.log('INCOMING', message);
}
ProtocolHelper.applyCommand(message, self);
@ -35,8 +36,13 @@ function (ProtocolHelper, GameController, User, Nc, Settings, DomController) {
Networker.prototype.onConnect = function () {
console.log('connected.')
var channel = JSON.parse(localStorage["channel"]);
var player = JSON.parse(localStorage["player"]);
if(channel.name) {
this.sendCommand('join', channel.name);
var options = {
channelName: channel.name,
nickname: player.nickname
}
this.sendCommand('join', options);
} else {
window.location.href = "/";
}
@ -67,6 +73,11 @@ function (ProtocolHelper, GameController, User, Nc, Settings, DomController) {
this.initPing();
}
Networker.prototype.onJoinError = function(options) {
alert(options.message);
window.location.href = "/";
};
Networker.prototype.onLevelLoaded = function() {
for (var userId in this.users) {
this.gameController.createPlayer(this.users[userId]);
@ -116,8 +127,7 @@ function (ProtocolHelper, GameController, User, Nc, Settings, DomController) {
}
Networker.prototype.onUserLeft = function (userId) {
var user = this.users[userId];
this.gameController.onUserLeft(user);
this.gameController.onUserLeft(userId);
delete this.users[userId];
}

View file

@ -62,6 +62,7 @@ define(function() {
// NETWORKING
WORLD_UPDATE_BROADCAST_INTERVAL: 70,
CHANNEL_DESTRUCTION_TIME: 30,
NETWORK_LOG_INCOMING: false,
NETWORK_LOG_OUTGOING: false
}

View file

@ -79,14 +79,18 @@ function (PhysicsEngine, TiledLevel, Player, Nc) {
}
*/
GameController.prototype.onUserLeft = function (user) {
var player = this.players[user.id];
GameController.prototype.onUserLeft = function (userId) {
var player = this.players[userId];
if(!player) {
console.warn("User (", userId ,") left who has not joined");
return;
}
var i = this.gameObjects.animated.indexOf(player);
if(i>=0) this.gameObjects.animated.splice(i, 1);
player.destroy();
delete this.players[user.id];
delete this.players[userId];
}
GameController.prototype.createPlayer = function(user) {

View file

@ -65,11 +65,8 @@ function () {
}
}
},
server: {
pipeToServer: function(v) { return v + "-ns.server.pipeToServer")}
},
lobby: {
channel: {
pipeToServer: function(v) { return v + "-ns.channel.pipeToServer")}
}
};

View file

@ -13,19 +13,26 @@ function (Nc, ProtocolHelper) {
Api.prototype.handleCall = function(queryParameters) {
var command;
var command,
output = null;
try {
var message = JSON.parse(queryParameters);
command = message.command;
} catch(e) {
this.isError = true;
output = "JSON syntax error";
console.error(e)
}
var output = null;
switch(command) {
case "getChannels":
output = this.coordinator.getChannels();
break;
case "createChannel":
// FIXME: sanitize input
output = this.createChannel(message.options);
break;
default:
this.isError = true;
output = "Command not found";
@ -35,6 +42,16 @@ function (Nc, ProtocolHelper) {
this.output = output;
}
Api.prototype.createChannel = function(options) {
var result = this.coordinator.createChannel(options);
if(result !== false) {
return result;
} else {
this.isError = true;
return "Could not create channel, name might already exist.";
}
};
Api.prototype.getOutput = function() {
var output = {};
var key = this.isError ? "error" : "success";

104
app/Server/Coordinator.js Executable file → Normal file
View file

@ -2,96 +2,33 @@ define([
"Server/User",
"Game/Channel/Channel",
"Server/PipeToChannel",
"Lib/Utilities/NotificationCenter"
"Lib/Utilities/NotificationCenter",
"Game/Config/Settings"
],
function (User, Channel, PipeToChannel, Nc) {
function (User, Channel, PipeToChannel, Nc, Settings) {
function Coordinator () {
function Coordinator() {
this.channelPipes = {};
this.lobbyUsers = {};
Nc.on('coordinator/message', this.onMessage, this);
console.checkpoint('create Coordinator');
}
Coordinator.prototype.createUser = function (socketLink) {
var user = new User(socketLink, this);
console.checkpoint('creating user');
this.assignUserToLobby(user);
}
Coordinator.prototype.assignUserToLobby = function (user) {
if(user.channelPipe) {
//user.channel.releaseUser(user); -> generate message
}
this.lobbyUsers[user.id] = user;
console.checkpoint('assign user to lobby');
new User(socketLink, this);
}
Coordinator.prototype.assignUserToChannel = function (user, channelName) {
if(user.channelPipe) {
//user.channel.releaseUser(user); -> generate message
}
if(!Channel.validateName(channelName)) {
//TODO send validation error
return false;
}
var channelPipe = this.channelPipes[channelName];
if(!channelPipe) {
this.createPipe(channelName);
user.setChannelPipe(channelPipe);
}
//channel.addUser(user);
//user.setChannel(channel);
Nc.trigger('user/joined', user);
delete this.lobbyUsers[user.id];
Coordinator.prototype.onDestroyPipe = function(channelName) {
delete this.channelPipes[channelName];
}
Coordinator.prototype.removeUser = function (user) {
Nc.trigger('user/left', user);
//NotificationCenter.off('channel/' + user.channel.channelName + '/user/' + user.id);
delete this.lobbyUsers[user.id];
}
Coordinator.prototype.createPipe = function(channelName) {
var channelPipe = new PipeToChannel(channelName);
this.channelPipes[channelName] = channelPipe;
Nc.on('channel/' + channelName + '/message', function (data) {
channelPipe.send('channel', data);
}, this);
// sending info to user
Nc.on('user/joined', function (user) {
/*
Nc.on('channel/' + channelName + '/user/' + user.id, function (recipient, data) {
channelPipe.send(recipient, data);
}, this);
*/
channelPipe.send('channel', { addUser: user.id });
}, this);
Nc.on('user/left', function (user) {
channelPipe.send('channel', { releaseUser: user.id });
}, this);
Nc.on('user/controlCommand', function (userId, data) {
channelPipe.sendToUser(userId, data);
}, this);
return channelPipe;
};
Coordinator.prototype.getChannels = function(options) {
var list = [];
for (var channelName in this.channelPipes) {
@ -100,8 +37,29 @@ function (User, Channel, PipeToChannel, Nc) {
});
}
return list;
}
Coordinator.prototype.createChannel = function(options) {
if(this.channelPipes[options.channelName]) {
return false;
}
var channelPipe = new PipeToChannel(options);
this.channelPipes[options.channelName] = channelPipe;
return {
channelName: options.channelName,
link: "#" + options.channelName,
timeout: Settings.CHANNEL_DESTRUCTION_TIME
}
};
Coordinator.prototype.onMessage = function(message) {
if(message.destroy) {
delete this.channelPipes[message.destroy];
}
};
return Coordinator;
});

View file

@ -7,21 +7,21 @@ function (Nc, childProcess) {
var fork = childProcess.fork;
function PipeToChannel (channelName) {
function PipeToChannel (options) {
this.channelPipe = null;
this.fork = null;
try {
this.channelPipe = fork('channel.js');
this.fork = fork('channel.js');
} catch (err) {
throw 'Failed to fork channel! (' + err + ')';
}
console.checkpoint('creating channel process for ' + channelName);
console.checkpoint('creating channel process for ' + options.channelName);
this.send('channel/' + channelName, { CREATE: channelName });
this.send('channel/' + options.channelName, { CREATE: true, options: options });
this.channelPipe.on('message', this.onMessage.bind(this));
this.fork.on('message', this.onMessage.bind(this));
var self = this;
}
@ -33,7 +33,7 @@ function (Nc, childProcess) {
data: data
}
this.channelPipe.send(message);
this.fork.send(message);
}
// If user already created
@ -43,7 +43,7 @@ function (Nc, childProcess) {
data: data
}
this.channelPipe.send(message);
this.fork.send(message);
}
PipeToChannel.prototype.onMessage = function (message) {

31
app/Server/User.js Executable file → Normal file
View file

@ -10,8 +10,8 @@ function (Parent, ProtocolHelper, Nc) {
Parent.call(this, socketLink.id);
this.coordinator = coordinator;
this.channelProcess = null;
this.socketLink = socketLink;
this.channelPipe = null;
socketLink.on('message', this.onMessage.bind(this));
socketLink.on('disconnect', this.onDisconnect.bind(this));
@ -21,6 +21,15 @@ function (Parent, ProtocolHelper, Nc) {
User.prototype = Object.create(Parent.prototype);
User.prototype.setChannelPipe = function(channelPipe) {
if(channelPipe) {
this.channelPipe = channelPipe;
} else {
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel not found"});
this.socketLink.send(message);
}
};
// Socket callbacks
@ -29,7 +38,12 @@ function (Parent, ProtocolHelper, Nc) {
}
User.prototype.onDisconnect = function () {
this.coordinator.removeUser(this);
if(!this.channelPipe) {
console.warn("Disconnecting user without a channel.");
return;
}
this.channelPipe.send('channel', { releaseUser: this.id });
}
@ -37,17 +51,20 @@ function (Parent, ProtocolHelper, Nc) {
// Remember: control commands are coordinator relevant commands
User.prototype.onJoin = function(options) {
this.coordinator.assignUserToChannel(this, options);
};
this.coordinator.assignUserToChannel(this, options.channelName);
User.prototype.onLeave = function(options) {
this.coordinator.assignUserToLobby(this);
if(!this.channelPipe) {
console.warn("Can not join user because channel (" + options.channelName + ") does not exist.")
return;
}
this.channelPipe.send('channel', { addUser: this.id });
};
User.prototype.onGameCommand = function(options) {
// repacking for transport via pipe
var message = ProtocolHelper.encodeCommand("gameCommand", options);
Nc.trigger("user/controlCommand", this.id, message);
this.channelPipe.sendToUser(this.id, message);
};
User.prototype.onPing = function(timestamp) {

View file

@ -2,9 +2,21 @@
<html>
<head>
<title>Chuck Lobby</title>
<style type="text/css">
body { background: #222; color: #ccc; font-family: "Lucida Grande", sans-serif; }
input, button { background: black; color: #ccc; border: 0; padding: 0.3em 0.6em; font-size: 1em; }
#createform, #customjoinform { display: none; }
article { margin: 4em auto; background: #1a1a1a; padding: 2em; max-width: 30em; }
table, th, td { border: 1px solid #777; border-collapse: collapse; }
th, td { padding: 0.5em 1em; }
ul { list-style-type: none; padding-left: 0; }
#link { width: 100%; }
.offline { background: #ccc; }
</style>
</head>
<body>
<form action="game.html" method="POST">
<article>
<h1>Chuck</h1>
<p>
<label>
Nickname:<br>
@ -12,32 +24,70 @@
</label>
</p>
<form action="#" id="createform">
<div>
<h2>Create your own!</h2>
<p><label>Name:<br> <input id="customname"></label></p>
<fieldset>
<legend>Maps</legend>
<ul>
<li>
<label><input name="maps" value="debug" type="checkbox" checked> Debug</label>
</li>
<li>
<label><input name="maps" value="stones2" type="checkbox" checked> Stones2</label>
</li>
</ul>
</fieldset>
<p>
<button>Run</button>
<button onclick="show('#listform'); return false;">Cancel</button>
</p>
</div>
</form>
<form action="game.html" method="GET" id="customjoinform">
<p>
<label>
Link to share with your friends<br>
<input id="link">
</label>
</p>
<p>
<button>Join</button>
<span id="timeout"></span>
</p>
</form>
<form action="game.html" method="GET" id="listform">
<h2>Channel list</h2>
<table>
<thead>
<tr><th>Name</th></tr>
</thead>
<tfoot>
<tr>
<td>
<label>
<input type="radio" name="channel" id="radiochannel" checked>
custom channel: <input id="customname">
</label>
</td>
<th>Name</th>
<th>Type</th>
</tr>
</tfoot>
</thead>
<tbody id="list"></tbody>
</table>
<p>
<button>Join</button>
<button id="refresh">Refresh list</button>
<button onclick="show('#createform'); return false;">Create</button>
</p>
</form>
</form>
</article>
<script>
function $(selector) {
return document.querySelector(selector);
}
function $$(selector) {
return document.querySelectorAll(selector);
}
if(localStorage["player"]) {
var player = JSON.parse(localStorage["player"]);
if(player.nickname) {
@ -45,32 +95,27 @@
}
}
$("#customname").onfocus = function() {
$("#radiochannel").checked = true;
}
if(localStorage["channel"]) {
var channel = JSON.parse(localStorage["channel"])
if(channel.customname) {
$("#customname").value = channel.customname;
}
}
var hash = window.location.hash.split("#").join("");
if(hash) {
$("#customname").value = hash;
$("#radiochannel").checked = true;
if(localStorage["customname"]) {
$("#customname").value = localStorage["customname"];
}
var lastRefreshResponse;
function refresh() {
$("#list").innerHTML = "";
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
if(xhr.status == 200) {
populate(JSON.parse(xhr.responseText).success);
var response = xhr.responseText;
if(response != lastRefreshResponse) {
lastRefreshResponse = response;
populate(JSON.parse(response).success);
}
document.body.className = "";
} else {
console.error("Ajax error: " + xhr.status + " " + xhr.statusText)
$("#list").innerHTML = "";
document.body.className = "offline";
}
}
}
@ -81,35 +126,31 @@
function populate(list) {
var html = "";
if(list.length > 0) {
for (var i = 0; i < list.length; i++) {
var channel = list[i];
html += "<tr><td><label>";
html += "<input name='channel' type='radio' value='" + channel.name + "'"
if(!hash && i == 0) html += " checked"
if(i == 0) html += " checked"
html += "> ";
html += channel.name
html += "</label></td></tr>";
html += "</label></td><td></td></tr>";
};
} else {
html += "<tr><td colspan='2'>No channels found.</td></tr>";
}
$("#list").innerHTML = html;
}
$("form").onsubmit = function(e) {
$("form#listform").onsubmit = function(e) {
var nickname = $("#nick").value;
if(!nickname || nickname.length < 3) {
alert("nickname too short")
return false;
}
localStorage["player"] = JSON.stringify({nickname: nickname});
if ($("#radiochannel").checked) {
var name = $("#customname").value;
if(name) {
$("#radiochannel").value = name;
} else {
alert("custom channel empty")
return false;
}
} else {
var radios = document.querySelectorAll("form input[name=channel]");
var radios = document.querySelectorAll("form#listform input[name=channel]");
for (var i = 0; i < radios.length; i++) {
var radio = radios[i];
if(radio.checked) {
@ -117,21 +158,124 @@
break;
}
};
}
if(name) {
localStorage["channel"] = JSON.stringify({
name: name,
customname: $("#customname").value
name: name
});
window.location.href = "/game.html";
return false;
} else {
alert("no channel selected")
alert("No channel selected")
return false;
}
}
$("form#createform").onsubmit = function(e) {
var maps = [];
var checkboxes = document.querySelectorAll("form#createform input[name=maps]");
for (var i = 0; i < checkboxes.length; i++) {
var checkbox = checkboxes[i];
if(checkbox.checked) {
maps.push(checkbox.value);
}
};
if(maps.length == 0) {
alert("Please choose at least one map.")
return false;
}
var name = $("#customname").value;
if(!name) {
alert("Please provide a channel name.")
return false;
}
localStorage["customname"] = name;
var options = {
channelName: name,
maps: maps,
maxUsers: 10,
minUsers: 2
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState == 4) {
if(xhr.status == 200) {
onCreateSuccess(JSON.parse(xhr.responseText).success);
} else {
console.log(xhr.responseText)
alert(JSON.parse(xhr.responseText).error)
}
}
}
xhr.open("POST", "/api", true);
xhr.send(JSON.stringify({command:"createChannel", options: options}));
return false;
}
$("form#customjoinform").onsubmit = function(e) {
var nickname = $("#nick").value;
if(!nickname || nickname.length < 3) {
alert("nickname too short")
return false;
}
localStorage["player"] = JSON.stringify({nickname: nickname});
var name = $("form#createform input[name=channel]").value;
localStorage["channel"] = JSON.stringify({
name: name
});
window.location.href = "/game.html";
return false;
}
function onCreateSuccess(options) {
$("#customname").value = options.channelName;
$("#link").value = window.location.href + options.link;
show("#customjoinform");
startTimer(options.timeout);
}
function show(id) {
$("#createform").style.display = "none";
$("#listform").style.display = "none";
$("#customjoinform").style.display = "none";
$(id).style.display = "block";
}
function startTimer(seconds) {
var now = new Date();
var end = new Date(now.getTime() + seconds * 1000);
setInterval(function() {
now = new Date();
var diff = new Date(end.getTime() - now.getTime());
if(diff.getTime() < 0) {
alert("Your channel has timed out.");
window.location.href = "/";
} else {
$("#timeout").innerHTML = " within " + formatDate(diff) + " minutes";
}
}, 1000);
}
function formatDate(date) {
var minutes = date.getMinutes();
var seconds = date.getSeconds();
if(minutes < 10) minutes = "0" + minutes;
if(seconds < 10) seconds = "0" + seconds;
return minutes + ":" + seconds;
}
$("#refresh").onclick = refresh;
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>