Mercurial > hg > rlgwebd
view rlgterm.js @ 55:96815eae4ebe
RLG-Web: make multiple watchers possible.
Split the TermSession class into the new TermSession, which handles the
PTY, and client classes, which handle HTTP sessions. These are Player
and Watcher. This allows multiple watchers per game, and other
improvements.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Mon, 18 Jun 2012 13:43:51 -0700 |
parents | 2eda3909f6a3 |
children | 7f3ca16409fe |
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 (this.state < 8) { this.set(15000); this.state++; } else { /* It's been over a minute. Stop polling. */ this.clear(); } }, 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" } }; /* Login name and key */ var lname = null; var lcred = 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 (termemu.sessid == null) return; var datareq = new XMLHttpRequest(); var msg = JSON.stringify({"id": termemu.sessid, "t": "n"}); 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 sendback(str) { /* For responding to terminal queries. */ var msgDict = {"id": termemu.sessid, "t": "d", "n": nsend++, "d": str}; var datareq = new XMLHttpRequest(); datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); datareq.send(JSON.stringify(msgDict)); return; } function sendkey(ev) { if (termemu.sessid == null) return; var keynum = ev.keyCode; var code; if (keynum >= ev.DOM_VK_A && keynum <= ev.DOM_VK_Z) { /* Letters. This assumes the codes are 65-90. */ 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 >= ev.DOM_VK_0 && keynum <= ev.DOM_VK_9) { /* 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 == ev.DOM_VK_SHIFT || keynum == ev.DOM_VK_CONTROL || keynum == ev.DOM_VK_ALT || keynum == ev.DOM_VK_CAPS_LOCK) { return; } else { debug(1, "Ignoring keycode " + keynum); return; } // Isn't this check redundant? if (termemu.sessid != null) ev.preventDefault(); var datareq = new XMLHttpRequest(); var msgDict = {"id": termemu.sessid, "t": "d", "n": nsend++, "d": code}; 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" } function vkey(c) { if (termemu.sessid == null) 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 return; var datareq = new XMLHttpRequest(); var msgDict = {"id": termemu.sessid, "t": "d", "n": nsend++, "d": keystr}; datareq.onreadystatechange = postResponseHandler; datareq.open('POST', '/feed', true); datareq.send(JSON.stringify(msgDict)); return; } function setup() { keyHexCodes.init(); termemu.init("termwrap", 24, 80); setTitle("Not connected."); setmode("login"); 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 (termemu.sessid != null) return; var loginmsg = {}; loginmsg["name"] = document.getElementById("input_name").value; loginmsg["pw"] = document.getElementById("input_pw").value; var req = new XMLHttpRequest(); req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 'l') { /* Success */ lcred = reply.k; lname = reply.u; setTitle("Logged in as " + reply.u); debug(1, "Logged in as " + reply.u + " with id " + reply.k); setmode("choose"); } else if (reply.t == 'E') { debug(1, "Could not log in: " + reply.s); document.getElementById("input_name").value = ""; document.getElementById("input_pw").value = ""; } }; req.open('POST', '/login', true); req.send(JSON.stringify(loginmsg)); return; } function getchoices() { if (termemu.sessid != null || !lcred) return; var req = new XMLHttpRequest(); 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"] != 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 reply.stat) { if (!(gname in games)) 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 = "Game in progress"; 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"; } 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/' + 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 (termemu.sessid != null || !lcred) return; var smsg = {}; smsg["key"] = lcred; smsg["game"] = game.uname; smsg["h"] = 24; smsg["w"] = 80; var req = new XMLHttpRequest(); req.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 's') { /* Success */ termemu.sessid = reply.id; termemu.resize(reply.h, reply.w); setTitle("Playing as " + lname); debug(1, "Playing with id " + termemu.sessid); setmode("play"); getData(); } else if (reply.t == 'E') { debug(1, "Could not start game: " + reply.s); if (reply.c == 1) { logout(); } } }; req.open('POST', '/play', true); req.send(JSON.stringify(smsg)); return; } function formreg(ev) { ev.preventDefault(); if (termemu.sessid != 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.onreadystatechange = function () { if (req.readyState != 4 || req.status != 200) return; var reply = JSON.parse(req.responseText); if (reply.t == 'r') { /* Success */ debug(1, "Registered account: " + reply.d); lcred = reply.k; lname = reply.u; setTitle("Logged in as " + lname); debug(1, "Logged in as " + lname + "with id " + lcred); setmode("choose"); } else if (reply.t == 'E') { debug(1, "Could not register: " + reply.s); 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 (termemu.sessid == null) return; /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */ termemu.sessid = null; ajaxstate.clear(); setTitle("Game over."); nsend = 0; nrecv = 0; msgQ = []; setmode("choose"); return; } function logout() { lcred = null; lname = null; setmode("login"); } function stop() { if (!termemu.sessid) return; var req = new XMLHttpRequest(); req.onreadystatechange = function () { if (req.readyState == 4 && req.status == 200) { processMsg(req.responseText); return; } }; req.open('POST', '/feed', true); req.send(JSON.stringify({"id": termemu.sessid, "t": "q"})); return; } function setmode(mode, ev) { if (ev) ev.preventDefault(); if (mode == "play") { document.getElementById("keyboard").style.display = "block"; document.getElementById("startgame").style.display = "none"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "none"; } if (mode == "choose") { document.getElementById("keyboard").style.display = "none"; document.getElementById("startgame").style.display = "block"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "none"; getchoices(); } else if (mode == "login") { document.getElementById("keyboard").style.display = "none"; document.getElementById("startgame").style.display = "none"; document.getElementById("login").style.display = "block"; document.getElementById("register").style.display = "none"; } else if (mode == "register") { document.getElementById("keyboard").style.display = "none"; document.getElementById("startgame").style.display = "none"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "block"; } } function debug(level, msg) { if (level < debugSuppress) return; var msgdiv = document.createElement("div"); var msgtext = document.createTextNode(msg); msgdiv.appendChild(msgtext); document.getElementById("debug").appendChild(msgdiv); return; } function textsize(larger) { var cssSize = termemu.view.style.fontSize; if (!cssSize) { return; } var match = cssSize.match(/\d*/); if (!match) { return; } var csize = Number(match[0]); var nsize; if (larger) { if (csize >= 48) nsize = 48; else if (csize >= 20) nsize = csize + 4; else if (csize >= 12) nsize = csize + 2; else if (csize >= 8) nsize = csize + 1; else nsize = 8; } else { if (csize <= 8) nsize = 8; else if (csize <= 12) nsize = csize - 1; else if (csize <= 20) nsize = csize - 2; else if (csize <= 48) nsize = csize - 4; else nsize = 48; } document.getElementById("term").style.fontSize = nsize.toString() + "px"; termemu.fixsize(); debug(1, "Changing font size to " + nsize.toString()); return; } function bell(on) { var imgnode = document.getElementById("bell"); if (on) { imgnode.style.visibility = "visible"; window.setTimeout(bell, 1500, false); } else imgnode.style.visibility = "hidden"; return; }