Mercurial > hg > rlgwebd
view rlgterm.js @ 127:e54018b26ed8
RLG-Web client: store login key in DOM Storage.
Keep the login key in sessionStorage. This lets the user navigate
away and return without needing to log in again.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Mon, 27 Aug 2012 13:43:12 -0700 |
parents | 54979d35611a |
children | a613380ffdc2 |
line wrap: on
line source
/* rlgterm.js: Roguelike Gallery's driver for termemu.js */ // A state machine that keeps track of polling the server. var ajaxstate = { state: 0, timerID: null, clear: function () { if (this.timerID != null) { window.clearTimeout(this.timerID); this.timerID = null; } }, set: function (ms) { this.clear(); this.timerID = window.setTimeout(getData, ms); }, gotdata: function () { this.set(1000); this.state = 1; }, gotnothing: function () { if (this.state == 0) { this.set(1000); this.state = 1; } else if (this.state < 4) { this.set(4000); this.state++; } else if (session.playing) { if (this.state < 8) { this.set(15000); this.state++; } else { /* It's been over a minute. Stop polling. */ this.clear(); } } else { /* If watching, it can't stop polling entirely, because there * are no POST events to start it up again. */ this.set(10000); } }, posted: function (wasdata) { if (wasdata) { this.set(1000); this.state = 1; } else { this.set(200); this.state = 0; } } }; /* Data on the available games. */ var games = { "rogue3": { "name": "Rogue V3", "uname": "rogue3" }, "rogue4": { "name": "Rogue V4", "uname": "rogue4" }, "rogue5": { "name": "Rogue V5", "uname": "rogue5" }, "srogue": { "name": "Super-Rogue", "uname": "srogue" }, "arogue5": { "name": "Advanced Rogue 5", "uname": "arogue5" } }; var session = { /* The session id assigned by the server. */ id: null, /* Login name and key are now in sessionStorage. */ /* Whether the game is being played or just watched. */ playing: false, /* WebSocket for communication */ sock: null }; /* The interval ID for checking the status of current games. */ var statInterval = null; /* How frequently to check. */ var statDelta = 8000; /* A WebSocket to listen for status events. */ var statsock = null; function writeData(hexstr) { var codenum; var codes = []; var nc; var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */ var expect = 0; /* The number of 10------ bytes expected. */ /* UTF-8 translation. */ for (var i = 0; i < hexstr.length; i += 2) { nc = Number("0x" + hexstr.substr(i, 2)); if (nc < 0x7F) { /* 0------- */ codes.push(nc); /* Any incomplete sequence will be discarded. */ u8wait = 0; expect = 0; } else if (nc < 0xC0) { /* 10------ : part of a multibyte sequence */ if (expect > 0) { u8wait <<= 6; u8wait += (nc & 0x3F); expect--; if (expect == 0) { codes.push(u8wait); u8wait = 0; } } else { /* Assume an initial byte was missed. */ u8wait = 0; } } /* These will all discard any incomplete sequence. */ else if (nc < 0xE0) { /* 110----- : introduces 2-byte sequence */ u8wait = (nc & 0x1F); expect = 1; } else if (nc < 0xF0) { /* 1110---- : introduces 3-byte sequence */ u8wait = (nc & 0x0F); expect = 2; } else if (nc < 0xF8) { /* 11110--- : introduces 4-byte sequence */ u8wait = (nc & 0x07); expect = 3; } else if (nc < 0xFC) { /* 111110-- : introduces 5-byte sequence */ u8wait = (nc & 0x03); expect = 4; } else if (nc < 0xFE) { /* 1111110- : introduces 6-byte sequence */ u8wait = (nc & 0x01); expect = 5; } else { /* 1111111- : should never appear */ u8wait = 0; expect = 0; } /* Supporting all 31 bits is probably overkill... */ } termemu.write(codes); return; } /* State for sending and receiving messages. */ var nsend = 0; // The number of the next packet to send. var nrecv = 0; // The next packet expected. var msgQ = []; // Queue for out-of-order messages. /* Processes a message from the server, returning true or false if it was a * data message with or without data, null if not data. * All non-special responseTexts should be handed directly to this function. */ function processMsg(msg) { var msgDicts; var havedata = null; // eventual return value try { msgDicts = JSON.parse(msg); } catch (e) { if (e instanceof SyntaxError) return null; } if (msgDicts.length === 0) return false; for (var j = 0; j < msgDicts.length; j++) { if (!msgDicts[j].t) continue; else if (msgDicts[j].t == "E") { if (msgDicts[j].c == 1 || msgDicts[j].c == 6 || msgDicts[j].c == 7) { gameover(); if (msgDicts[j].c == 1) { logout(); } } debug(1, "Server error: " + msgDicts[j].s); } // A data message else if (msgDicts[j].t == "d") { if (msgDicts[j].n === nrecv) { writeData(msgDicts[j].d); nrecv++; /* Process anything in the queue that's now ready. */ var next; while ((next = msgQ.shift()) !== undefined) { writeData(next.d); nrecv++; } } else if (msgDicts[j].n > nrecv) { /* The current message comes after one still missing. Queue this one * for later use. */ debug(1, "Got packet " + msgDicts[j].n + ", expected " + nrecv); msgQ[msgDicts[j].n - nrecv - 1] = msgDicts[j]; } else { /* This message's number was encountered previously. */ debug(1, "Discarding packet " + msgDicts[j].n + ", expected " + nrecv); } havedata = true; } else if (msgDicts[j].t == "T") { setTitle(msgDicts[j].d); } else if (msgDicts[j].t == "q") { gameover(); } else { debug(1, "Unrecognized server message " + msg); } } return havedata; } function getData() { if (session.id == null) return; var datareq = new XMLHttpRequest(); var msg = JSON.stringify({"id": session.id, "t": "n"}); datareq.onerror = errHandler; datareq.onreadystatechange = function () { if (datareq.readyState == 4 && datareq.status == 200) { var wasdata = processMsg(datareq.responseText); if (wasdata != null) { if (wasdata) ajaxstate.gotdata(); else ajaxstate.gotnothing(); } return; } }; datareq.open('POST', '/feed', true); datareq.send(msg); return; } function postResponseHandler() { if (this.readyState == 4 && this.status == 200) { // We might want to do something with wasdata someday. var wasdata = processMsg(this.responseText); ajaxstate.posted(); return; } } function errHandler() { message("Unable to connect to the server.", "warn"); } function sendback(str) { /* For responding to terminal queries. */ var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": str}; var datareq = new XMLHttpRequest(); datareq.onerror = errHandler; datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); datareq.send(JSON.stringify(msgDict)); return; } function sendkey(ev) { if (!session.playing) return; var keynum = ev.keyCode; var code; if (keynum >= 65 && keynum <= 90) { /* Letters. */ if (ev.ctrlKey) keynum -= 64; else if (!ev.shiftKey) keynum += 32; code = keynum.toString(16); if (code.length < 2) code = "0" + code; } else if (keynum >= 48 && keynum <= 57) { /* The number row, NOT the numpad. */ if (ev.shiftKey) { code = numShifts[keynum - 48].toString(16); } else { code = keynum.toString(16); } } else if (keynum in keyHexCodes) { if (ev.shiftKey) code = keyHexCodes[keynum][1]; else code = keyHexCodes[keynum][0]; } else if (keynum >= 16 && keynum <= 20) { /* Shift, Cntl, Alt, CAPSLOCK */ return; } else { debug(1, "Ignoring keycode " + keynum); return; } ev.preventDefault(); if (session.sock) { session.sock.send(JSON.stringify({"t": "d", "d": code})); } else { var datareq = new XMLHttpRequest(); var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": code}; datareq.onerror = errHandler; datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); datareq.send(JSON.stringify(msgDict)); } return; } var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c", ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e" } var kpkeys = { "KP1": "1b4f46", "KP2": "1b4f42", "KP3": "1b5b367e", "KP4": "1b4f44", "KP5": "1b5b45", "KP6": "1b4f43", "KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" }; function vkey(c) { if (!session.playing) return; var keystr; if (c.match(/^[a-z]$/)) { if (termemu.ctrlp()) { var n = c.charCodeAt(0) - 96; keystr = n.toString(16); if (keystr.length < 2) keystr = "0" + keystr; } else if (termemu.shiftp()) keystr = c.toUpperCase().charCodeAt(0).toString(16); else keystr = c.charCodeAt(0).toString(16); } else if (c.match(/^[0-9]$/)) { if (termemu.shiftp()) keystr = numShifts[c.charCodeAt(0) - 48].toString(16); else keystr = c.charCodeAt(0).toString(16); } else if (c == '\n') keystr = "0a"; else if (c == '\t') keystr = "09"; else if (c == '\b') keystr = "08"; else if (c == ' ') keystr = "20"; else if (c in charshifts) { if (termemu.shiftp()) keystr = charshifts[c]; else keystr = c.charCodeAt(0).toString(16); } else if (c in kpkeys) { keystr = kpkeys[c]; } else return; if (session.sock) { session.sock.send(JSON.stringify({"t": "d", "d": keystr})); } else { var datareq = new XMLHttpRequest(); var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": keystr}; datareq.onerror = errHandler; datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); datareq.send(JSON.stringify(msgDict)); } return; } function setup() { keyHexCodes.init(); termemu.init("termwrap", 24, 80); /* Is someone already logged in? */ if ("lcred" in sessionStorage) { setmode("choose"); message("You are logged in as " + sessionStorage.getItem("lname") + "."); } else setmode("login"); /* Set up the text size. */ var cssSize = termemu.view.style.fontSize; var match = cssSize.match(/\d*/); if (!match) { return; } var csize = Number(match[0]); var allscreen = document.getElementById("termwrap"); while (csize > 9 && csize < 48) { if (allscreen.scrollWidth * 1.2 > window.innerWidth) { csize = textsize(false); } else if (allscreen.scrollWidth * 2 < window.innerWidth) { csize = textsize(true); } else break; } if (!window.WebSocket) { message("Your browser does not support WebSockets. You can still play, " + "but it will be slower, and may not work in the future.", "warn"); } return; } function toggleshift() { termemu.toggleshift(); keydiv = document.getElementById("shiftkey"); if (termemu.shiftp()) keydiv.className = "keysel"; else keydiv.className = "key"; return; } function togglectrl() { termemu.togglectrl(); keydiv = document.getElementById("ctrlkey"); if (termemu.ctrlp()) keydiv.className = "keysel"; else keydiv.className = "key"; return; } function formlogin(ev) { ev.preventDefault(); if (session.id != null) return; var loginmsg = {}; loginmsg["name"] = document.getElementById("input_name").value; loginmsg["pw"] = document.getElementById("input_pw").value; var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 'l') { /* Success */ sessionStorage.setItem("lcred", reply.k); sessionStorage.setItem("lname", reply.u); message("You are now logged in as " + reply.u + "."); setmode("choose"); } else if (reply.t == 'E') { var failmsg = "Logging in failed. "; if (reply.c == 2) failmsg += reply.s.match(/Invalid data: (.*)/)[1]; else if (reply.c == 3) failmsg += "The username or password was incorrect."; else if (reply.c == 6) failmsg += "The server is shutting down."; message(failmsg, "warn"); document.getElementById("input_name").value = ""; document.getElementById("input_pw").value = ""; } }; req.open('POST', '/login', true); req.send(JSON.stringify(loginmsg)); return; } function tableCurrent(gamelist) { var gamediv = document.getElementById("gametable"); while (gamediv.children.length > 2) gamediv.removeChild(gamediv.children[2]); if (gamelist.length === 0) { gamediv.style.display = "none"; document.getElementById("nogames").style.display = "block"; } else { gamediv.style.display = "table"; document.getElementById("nogames").style.display = "none"; } for (var i = 0; i < gamelist.length; i++) { var row = document.createElement("div"); var cell1 = document.createElement("div"); var cell2 = document.createElement("div"); var cell3 = document.createElement("div"); var cell4 = document.createElement("div"); cell1.appendChild(document.createTextNode(gamelist[i].p)); var uname = gamelist[i].g; if (uname in games) cell2.appendChild(document.createTextNode(games[uname].name)); else { continue; } cell3.appendChild(document.createTextNode(idlestr(gamelist[i].i))); var button = document.createElement("span"); button.appendChild(document.createTextNode("Watch")); button.onclick = makeWatcher(gamelist[i].n); button.className = "ibutton"; cell4.appendChild(button); row.appendChild(cell1); row.appendChild(cell2); row.appendChild(cell3); row.appendChild(cell4); gamediv.appendChild(row); } } /* Handles the status socket, opening and closing it when necessary. */ function wsCurrent() { if (!window.WebSocket) return; if (session.id) { /* Don't bother with status if already playing/watching. */ if (statsock) { statsock.close(); statsock = null; } return; } if ("lcred" in sessionStorage) { /* When starting the socket, the choices list might not be initialized. */ getchoices(); } if (statsock) return; statsock = new WebSocket("ws://" + window.location.host + "/status"); statsock.onmessage = function (ev) { var msg; try { msg = JSON.parse(ev.data); } catch (e) { if (e instanceof SyntaxError) return; } if (msg.t == "t") { tableCurrent(msg.g); } else if ((msg.t == "b" || msg.t == "e") && msg.p == sessionStorage.getItem("lname")) { getchoices(); } }; statsock.onclose = function (ev) { statsock = null; } } function getcurrent(clear) { if (window.WebSocket) { return; } if (session.id || clear) { if (statInterval) { window.clearInterval(statInterval); statInterval = null; } return; } if (!statInterval) { statInterval = window.setInterval(getcurrent, statDelta); } if ("lcred" in sessionStorage) getchoices(); var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply; try { reply = JSON.parse(req.responseText); } catch (e) { if (e instanceof SyntaxError) return; } if (!reply.s) { return; } tableCurrent(reply.g); }; req.open('GET', '/status', true); req.send(); return; } function getchoices() { if (session.id != null || !("lcred" in sessionStorage)) return; var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply; try { reply = JSON.parse(req.responseText); } catch (e) { if (e instanceof SyntaxError) return; } if (!("name" in reply) || reply["name"] != sessionStorage.getItem("lname") || !("stat" in reply)) return; var optdiv = document.getElementById("opttable"); /* Don't remove the first child, it's the header. */ while (optdiv.childNodes.length > 1) optdiv.removeChild(optdiv.childNodes[1]); for (var gname in games) { if (!(gname in reply.stat)) continue; var acttext; if (reply.stat[gname] == "s") acttext = "Resume your game"; else if (reply.stat[gname] == "0") acttext = "Start a game"; else if (reply.stat[gname] == "p" || reply.stat[gname] == "d") acttext = "Force save"; else continue; var button = document.createElement("span"); button.appendChild(document.createTextNode(acttext)); if ("s0".indexOf(reply.stat[gname]) >= 0) { button.onclick = makeStarter(gname); button.className = "ibutton"; } else { button.onclick = makeStopper(gname); button.className = "ibutton"; } var actdiv = document.createElement("div"); actdiv.appendChild(button); var gamediv = document.createElement("div"); gamediv.appendChild(document.createTextNode(games[gname].name)); var rowdiv = document.createElement("div"); rowdiv.appendChild(gamediv); rowdiv.appendChild(actdiv); optdiv.appendChild(rowdiv); } }; req.open('GET', '/pstatus/' + sessionStorage.getItem("lname"), true); req.send(); return; } /* This can't be in the loop in getchoices(), or the closure's scope will * get overwritten on the next iteration, and then all the games end up * being Super-Rogue. */ function makeStarter(gname) { if (!(gname in games)) return null; var game = games[gname]; function starter(ev) { startgame(game); } return starter; } function startgame(game) { if (session.id != null || !("lcred" in sessionStorage)) return; if (window.WebSocket) { wsStart(game); return; } var smsg = {}; smsg["key"] = sessionStorage.getItem("lcred"); smsg["game"] = game.uname; smsg["h"] = 24; smsg["w"] = 80; var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 's') { /* Success */ session.id = reply.id; session.playing = true; termemu.resize(reply.h, reply.w); message("You are now playing " + game.name + "."); setmode("play"); getData(); } else if (reply.t == 'E') { if (reply.c == 1) { logout(); message("The server forgot about you, please log in again.", "warn"); } else if (reply.c == 4) { if (reply.s == "dgamelaunch") { message("You are already playing " + game.name + " over SSH.", "warn"); } else { message("You are already playing " + game.name + " in another browser window.", "warn"); } } else if (reply.c == 7) { message("The game is being saved, try again in a few seconds."); } else { message("The server says it can't start your game because \"" + reply.s + "\". This is probably a bug.", "warn"); } } }; req.open('POST', '/play', true); req.send(JSON.stringify(smsg)); return; } function makeStopper(gname) { if (!(gname in games)) return null; var game = games[gname]; function stopper(ev) { stopgame(game); } return stopper; } function stopgame(game) { if (!("lcred" in sessionStorage)) return; var stopmsg = {"key": sessionStorage.getItem("lcred"), "g": game.uname}; var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 'E') { if (reply.c == 7) message("That game has already stopped."); else if (reply.c == 1) { logout(); message("The server forgot about you, please log in again.", "warn"); } else { message("That game could not be stopped because: " + reply.s + "This might be a bug.", "warn"); } } } req.open('POST', '/quit', true); req.send(JSON.stringify(stopmsg)); return; } function wsStart(game) { var sockurl = "ws://" + window.location.host + "/play/" + game.uname; sockurl += "?key=" + sessionStorage.getItem("lcred") + "&w=80&h=24"; ws = new WebSocket(sockurl); ws.onopen = function (event) { session.id = true; session.playing = true; session.sock = ws; setmode("play"); }; ws.onmessage = function (event) { var msgObject = JSON.parse(event.data); if (msgObject.t == 's') { termemu.resize(msgObject.h, msgObject.w); message("You are now playing " + games[msgObject.g].name + "."); } else if (msgObject.t == 'd') { writeData(msgObject.d); } }; ws.onclose = function (event) { session.sock = null; gameover(); }; } function startwatching(gamenumber) { if (session.id != null) return; if (window.WebSocket) { wsWatch(gamenumber); return; } var wmsg = {"n": Number(gamenumber)}; var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 'w') { /* Success */ session.id = reply.id; session.playing = false; termemu.resize(reply.h, reply.w); termemu.reset(); termemu.toAltBuf(); var pname = reply.p; var gname = games[reply.g].name; message("You are now watching " + pname + " play " + gname + "."); setmode("watch"); getData(); } else if (reply.t == 'E') { message("The game could not be watched: " + reply.s, "warn"); getcurrent(); } }; req.open('POST', '/watch', true); req.send(JSON.stringify(wmsg)); return; } function makeWatcher(n) { function watcher(ev) { startwatching(n); } return watcher; } function wsWatch(gamenumber) { var sockurl = "ws://" + window.location.host + "/watch/" + String(gamenumber);