Mercurial > hg > rlgwebd
view rlgterm.js @ 59:00b985b8ba6a
RLG-Web client: implement watching.
It is now possible to watch games currently being played through
RLG-Web, but not dgamelaunch. Also, there are some deficiencies, like
getting chaos until an absolute cursor addressing happens.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Tue, 19 Jun 2012 16:19:50 -0700 |
parents | 7a50b4412fea |
children | 071ec6b1ec03 |
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" } }; 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 }; 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.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": session.id, "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 (!session.playing) 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; } ev.preventDefault(); var datareq = new XMLHttpRequest(); var msgDict = {"id": session.id, "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 (!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 return; var datareq = new XMLHttpRequest(); var msgDict = {"id": session.id, "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 (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.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; 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 getcurrent() { if (session.id) 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 (!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"); cell1.appendChild(document.createTextNode(reply.g[i].p)); cell2.appendChild(document.createTextNode(reply.g[i].g)); var button = document.createElement("span"); button.appendChild(document.createTextNode("Watch")); button.onclick = makeWatcher(reply.g[i].n); button.className = "ibutton"; cell3.appendChild(button); row.appendChild(cell1); row.appendChild(cell2); row.appendChild(cell3); gamediv.appendChild(row); } }; req.open('GET', '/status', true); req.send(); return; } function getchoices() { if (session.id != null || !session.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"] != 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 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/' + 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.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); setTitle("Playing as " + session.lname); debug(1, "Playing with id " + session.id); 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 startwatching(gamenumber) { if (session.id != null) return; var wmsg = {"n": Number(gamenumber)}; var req = new XMLHttpRequest(); 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); setTitle("Watching"); debug(1, "Watching with id " + session.id); setmode("play"); getData(); } else if (reply.t == 'E') { debug(1, "Could not watch game " + gamenumber + ": " + reply.s); } }; 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.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); session.lcred = reply.k; session.lname = reply.u; setTitle("Logged in as " + session.lname); debug(1, "Logged in as " + session.lname + "with id " + session.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 (session.id == null) return; /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */ session.id = null; session.playing = false; ajaxstate.clear(); setTitle("Game over."); 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.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("startgame").style.display = "none"; document.getElementById("login").style.display = "none"; document.getElementById("register").style.display = "none"; document.getElementById("current").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"; document.getElementById("current").style.display = "block"; getchoices(); getcurrent(); } 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"; document.getElementById("current").style.display = "block"; getcurrent(); } 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"; document.getElementById("current").style.display = "none"; } } 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; }