RLGWebD: fix simultaneous player bug.

Multiple games can now run at the same time, and data will be sent to
the proper place.  The interaction of multiple players with watchers
has not yet been tested.
This commit is contained in:
John "Elwin" Edwards 2015-01-09 13:06:41 -05:00
parent ef4cb9ac8d
commit 6373e70361

View file

@ -69,35 +69,32 @@ var gamemux = new events.EventEmitter();
/* Constructor. A TermSession handles a pty and the game running on it. /* Constructor. A TermSession handles a pty and the game running on it.
* gname: (String) Name of the game to launch. * gname: (String) Name of the game to launch.
* pname: (String) The player's name. * pname: (String) The player's name.
* dims: (Array [Number, Number]) Height and width of the pty. * wsReq: (WebSocketRequest) The request from the client.
* handlers: (Object) Key-value pairs, event names and functions to *
* install to handle them.
* Events: * Events:
* "data": Data generated by child. Parameters: buf (Buffer) * "data": Data generated by child. Parameters: buf (Buffer)
* "exit": Child terminated. Parameters: none * "exit": Child terminated. Parameters: none
*/ */
function TermSession(gname, pname, dims, handlers) { function TermSession(gname, pname, wsReq) {
var ss = this; var ss = this;
/* Subclass EventEmitter to do the hard work. */ /* Subclass EventEmitter to do the hard work. */
events.EventEmitter.call(this); events.EventEmitter.call(this);
for (var evname in handlers)
this.on(evname, handlers[evname]);
/* Don't launch anything that's not a real game. */ /* Don't launch anything that's not a real game. */
if (gname in games) { if (gname in games) {
this.game = games[gname]; this.game = games[gname];
} }
else { else {
this.failed = true; this.failed = true;
wsReq.reject(404, errorcodes[2], "No such game");
tslog("Game %s is not available", game);
return; return;
} }
this.pname = pname; this.pname = pname;
/* Grab a spot in the sessions table. */
sessions[this.game.uname + "/" + this.pname] = this;
/* Set up the sizes. */ /* Set up the sizes. */
this.w = Math.floor(Number(dims[1])); this.w = Math.floor(Number(wsReq.resourceURL.query.w));
if (!(this.w > 0 && this.w < 256)) if (!(this.w > 0 && this.w < 256))
this.w = 80; this.w = 80;
this.h = Math.floor(Number(dims[0])); this.h = Math.floor(Number(wsReq.resourceURL.query.h));
if (!(this.h > 0 && this.h < 256)) if (!(this.h > 0 && this.h < 256))
this.h = 24; this.h = 24;
/* Environment. */ /* Environment. */
@ -111,6 +108,7 @@ function TermSession(gname, pname, dims, handlers) {
this.term = pty.spawn(this.game.path, args, spawnopts); this.term = pty.spawn(this.game.path, args, spawnopts);
tslog("%s playing %s (pid %d)", this.pname, this.game.uname, this.term.pid); tslog("%s playing %s (pid %d)", this.pname, this.game.uname, this.term.pid);
this.failed = false; this.failed = false;
sessions[this.game.uname + "/" + this.pname] = this;
gamemux.emit('begin', this.game.uname, this.pname); gamemux.emit('begin', this.game.uname, this.pname);
/* Set up the lockfile and ttyrec */ /* Set up the lockfile and ttyrec */
this.lasttime = new Date(); this.lasttime = new Date();
@ -126,6 +124,7 @@ function TermSession(gname, pname, dims, handlers) {
* with a complete screen. */ * with a complete screen. */
this.framebuf = new Buffer(1024); this.framebuf = new Buffer(1024);
this.frameoff = 0; this.frameoff = 0;
this.playerconn = wsReq.accept(null, wsReq.origin);
/* END setup */ /* END setup */
function ttyrec_chunk(datastr) { function ttyrec_chunk(datastr) {
ss.lasttime = new Date(); ss.lasttime = new Date();
@ -138,6 +137,10 @@ function TermSession(gname, pname, dims, handlers) {
buf.copy(chunk, 12); buf.copy(chunk, 12);
ss.record.write(chunk); ss.record.write(chunk);
ss.framepush(buf); ss.framepush(buf);
/* Send to the player. */
var msg = {"t": "d", "d": buf.toString("hex")};
ss.playerconn.sendUTF(JSON.stringify(msg));
/* For the benefit of watchers. */
ss.emit('data', buf); ss.emit('data', buf);
} }
this.term.on("data", ttyrec_chunk); this.term.on("data", ttyrec_chunk);
@ -171,15 +174,39 @@ function TermSession(gname, pname, dims, handlers) {
var tag = ss.tag(); var tag = ss.tag();
fs.unlink(ss.lock); fs.unlink(ss.lock);
ss.record.end(); ss.record.end();
if (ss.playerconn.connected) {
ss.playerconn.sendUTF(JSON.stringify({"t": "q"}));
ss.playerconn.close();
}
ss.emit('exit'); ss.emit('exit');
gamemux.emit('end', ss.game.uname, ss.pname); gamemux.emit('end', ss.game.uname, ss.pname);
delete sessions[tag]; delete sessions[tag];
tslog("Game %s ended.", tag); tslog("Game %s ended.", tag);
}); });
this.close = function () { this.close = function () {
if (this.tag() in sessions) if (ss.tag() in sessions)
this.term.kill('SIGHUP'); ss.term.kill('SIGHUP');
}; };
/* Send initial data. */
this.playerconn.sendUTF(JSON.stringify({"t": "s", "w": this.w, "h": this.h,
"p": this.pname, "g": this.game.uname}));
/* Attach handlers. */
function messageH(message) {
var parsedMsg = getMsgWS(message);
if (parsedMsg.t == 'q') {
ss.close();
}
else if (parsedMsg.t == 'd') {
var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
if (hexstr.length % 2 != 0) {
hexstr = hexstr.slice(0, -1);
}
var keybuf = new Buffer(hexstr, "hex");
ss.write(keybuf);
}
}
this.playerconn.on('message', messageH);
this.playerconn.on('close', this.close);
} }
TermSession.prototype = new events.EventEmitter(); TermSession.prototype = new events.EventEmitter();
@ -315,64 +342,7 @@ function wsWatcher(conn, session) {
"d": session.framebuf.toString("hex", 0, session.frameoff)})); "d": session.framebuf.toString("hex", 0, session.frameoff)}));
} }
function wsPlay(wsReq, game, pname, dims) { function wsStartGame(wsReq) {
tslog("wsPlay: running for %s/%s", game, pname);
tslog("Request is for %s", logins[wsReq.resourceURL.query["key"]].name);
var conn;
var session;
/* Listeners on the WebSocket */
function messageH(message) {
var parsedMsg = getMsgWS(message);
if (parsedMsg.t == 'q') {
session.close();
}
else if (parsedMsg.t == 'd') {
var hexstr = parsedMsg.d.replace(/[^0-9a-f]/gi, "");
if (hexstr.length % 2 != 0) {
hexstr = hexstr.slice(0, -1);
}
var keybuf = new Buffer(hexstr, "hex");
session.write(keybuf);
}
}
function closeH() {
session.close();
}
/* These listen on the TermSession. */
function dataH(chunk) {
var msg = {};
msg.t = "d";
msg.d = chunk.toString("hex");
conn.sendUTF(JSON.stringify(msg));
}
function exitH() {
if (conn.connected)
conn.sendUTF(JSON.stringify({"t": "q"}));
conn.close();
session.removeListener('data', dataH);
session.removeListener('exit', exitH);
}
var handlers = {'data': dataH, 'exit': exitH};
session = new TermSession(game, pname, dims, handlers);
if (!session.failed) {
var tag = session.game.uname + "/" + session.pname;
var reply = {"t": "s", "tag": tag, "w": session.w, "h": session.h,
"p": session.pname, "g": session.game.uname};
tslog("Accepting for %s", tag);
tslog("Request is for %s", logins[wsReq.resourceURL.query["key"]].name);
tslog("Session is for %s", session.pname);
conn = wsReq.accept(null, wsReq.origin);
conn.sendUTF(JSON.stringify(reply));
conn.on('message', messageH);
conn.on('close', closeH);
}
else {
wsReq.reject(500, errorcodes[5]);
tslog("Unable to allocate TTY for %s", game);
}
}
function wsStart(wsReq) {
var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/); var playmatch = wsReq.resourceURL.pathname.match(/^\/play\/([^\/]*)$/);
if (!playmatch[1] || !(playmatch[1] in games)) { if (!playmatch[1] || !(playmatch[1] in games)) {
wsReq.reject(404, errorcodes[2]); wsReq.reject(404, errorcodes[2]);
@ -393,14 +363,14 @@ function wsStart(wsReq) {
return; return;
} }
var pname = logins[lkey].name; var pname = logins[lkey].name;
var dims = [wsReq.resourceURL.query.h, wsReq.resourceURL.query.w];
function progcallback(err, fname) { function progcallback(err, fname) {
if (fname) { if (fname) {
wsReq.reject(404, errorcodes[4]); wsReq.reject(404, errorcodes[4]);
tslog("%s is already playing %s", pname, gname); tslog("%s is already playing %s", pname, gname);
} }
else else {
wsPlay(wsReq, gname, pname, dims); new TermSession(gname, pname, wsReq);
}
}; };
checkprogress(pname, games[gname], progcallback, []); checkprogress(pname, games[gname], progcallback, []);
} }
@ -1044,7 +1014,7 @@ function wsHandler(wsRequest) {
tslog("Game %s is being watched via WebSockets", tsession.tag()); tslog("Game %s is being watched via WebSockets", tsession.tag());
} }
else if (playmatch !== null) { else if (playmatch !== null) {
wsStart(wsRequest); wsStartGame(wsRequest);
} }
else if (wsRequest.resourceURL.pathname == "/status") { else if (wsRequest.resourceURL.pathname == "/status") {
var conn = wsRequest.accept(null, wsRequest.origin); var conn = wsRequest.accept(null, wsRequest.origin);