Mercurial > hg > rlgwebd
view rlgterm.js @ 98:75a016c49076 beta4
Designate as beta4.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Thu, 12 Jul 2012 07:57:50 -0700 |
parents | 8a748eac7c11 |
children | 3dbfdaf62623 |
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" } }; var session = { /* The session id assigned by the server. */ id: null, /* Login name and key */ lname: null, lcred: null, /* Whether the game is being played or just watched. */ playing: false }; /* The interval ID for checking the status of current games. */ var statInterval = null; /* How frequently to check. */ var statDelta = 8000; 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(); 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; 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); 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; } 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 */ session.lcred = reply.k; session.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 getcurrent(clear) { if (session.id || clear) { if (statInterval) { window.clearInterval(statInterval); statInterval = null; } return; } if (!statInterval) { statInterval = window.setInterval(getcurrent, statDelta); } 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; } var gamediv = document.getElementById("gametable"); while (gamediv.children.length > 2) gamediv.removeChild(gamediv.children[2]); if (reply.g.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 < reply.g.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(reply.g[i].p)); var uname = reply.g[i].g; if (uname in games) cell2.appendChild(document.createTextNode(games[uname].name)); else { debug(1, "Unrecognized game: " + uname); continue; } cell3.appendChild(document.createTextNode(idlestr(reply.g[i].i))); var button = document.createElement("span"); button.appendChild(document.createTextNode("Watch")); button.onclick = makeWatcher(reply.g[i].n); button.className = "ibutton"; cell4.appendChild(button); row.appendChild(cell1); row.appendChild(cell2); row.appendChild(cell3); row.appendChild(cell4); gamediv.appendChild(row); } }; req.open('GET', '/status', true); req.send(); if (session.lcred) getchoices(); return; } function getchoices() { if (session.id != null || !session.lcred) 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"] != session.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") acttext = "Reconnect"; else if (reply.stat[gname] == "d") acttext = "Game in progress (dgl)"; else continue; var button = document.createElement("span"); button.appendChild(document.createTextNode(acttext)); if ("s0p".indexOf(reply.stat[gname]) >= 0) { button.onclick = makeStarter(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/' + session.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 || !session.lcred) return; var smsg = {}; smsg["key"] = session.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) { message("You can't play that game because it is currently being " + "played over SSH.", "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 startwatching(gamenumber) { if (session.id != null) 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 formreg(ev) { ev.preventDefault(); if (session.id != null) return; var regmsg = {}; regmsg["name"] = document.getElementById("regin_name").value; regmsg["pw"] = document.getElementById("regin_pw").value; regmsg["email"] = document.getElementById("regin_email").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 == 'r') { /* Success */ message("Welcome " + reply.u + ", you are now registered."); session.lcred = reply.k; session.lname = reply.u; message("You are now logged in as " + reply.u + "."); setmode("choose"); } else if (reply.t == 'E') { var failmsg = "Registration failed."; if (reply.c == 2) { var errdesc = reply.s.match(/Invalid data: (.*)/)[1]; if (errdesc.match(/No name/)) failmsg += " You need to choose a name."; else if (errdesc.match(/No password/)) failmsg += " You need to choose a password."; else if (errdesc.match(/Invalid/)) { failmsg += " Names must be letters and numbers. E-mail addresses " + "can also contain these characters: @.-_"; } else if (errdesc.match(/Username/)) failmsg += " Someone else is already using that name."; else failmsg += " This is probably a bug."; } else failmsg += " This is probably a bug."; message(failmsg, "warn"); document.getElementById("regin_name").value = ""; document.getElementById("regin_pw").value = ""; document.getElementById("regin_email").value = ""; } }; req.open('POST', '/addacct', true); req.send(JSON.stringify(regmsg)); return; } function gameover() { if (session.id == null) return; /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */ if (session.playing) message("Finished playing."); else message("Finished watching."); session.id = null; session.playing = false; ajaxstate.clear(); termemu.toNormBuf(); nsend = 0; nrecv = 0; msgQ = []; if (session.lcred != null) setmode("choose"); else setmode("login"); return; } function logout() { session.lcred = null; session.lname = null; setmode("login"); } function stop() { if (!session.id) return; var req = new XMLHttpRequest(); req.onerror = errHandler; req.onreadystatechange = function () { if (req.readyState == 4 && req.status == 200) { processMsg(req.responseText); return; } }; req.open('POST', '/feed', true); req.send(JSON.stringify({"id": session.id, "t": "q"})); return; } function setmode(mode, ev) { if (ev) ev.preventDefault(); if (mode == "play") { document.getElementById("keyboard").style.display = "block"; document.getElementById("playctl").style.display = "block"; document.getElementById("startgame").style.display = "none"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "none"; document.getElementById("current").style.display = "none"; } else if (mode == "watch") { document.getElementById("keyboard").style.display = "none"; document.getElementById("playctl").style.display = "block"; document.getElementById("startgame").style.display = "none"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "none"; document.getElementById("current").style.display = "none"; } else if (mode == "choose") { document.getElementById("keyboard").style.display = "none"; document.getElementById("playctl").style.display = "none"; document.getElementById("startgame").style.display = "block"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "none"; document.getElementById("current").style.display = "block"; getcurrent(); } else if (mode == "login") { document.getElementById("keyboard").style.display = "none"; document.getElementById("playctl").style.display = "none"; document.getElementById("startgame").style.display = "none"; document.getElementById("login").style.display = "block"; document.getElementById("register").style.display = "none"; document.getElementById("current").style.display = "block"; getcurrent(); } else if (mode == "register") { document.getElementById("keyboard").style.display = "none";