Mercurial > hg > rlgwebd
view rlgterm.js @ 161:a2a25b7631f1
RLGWebD client: fix watching.
author | John "Elwin" Edwards |
---|---|
date | Sat, 03 Jan 2015 20:03:49 -0500 |
parents | a613380ffdc2 |
children | 0f6da35b27a0 |
line wrap: on
line source
/* rlgterm.js: Roguelike Gallery's driver for termemu.js */ /* 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. */ connect: false, /* 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; } function errHandler() { message("Unable to connect to the server.", "warn"); } function sendback(str) { /* For responding to terminal queries. */ if (session.sock) { session.sock.send(JSON.stringify({"t": "d", "d": str})); } 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})); } /* Otherwise it is disconnected */ 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})); } 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. " + "This Web app will not work.", "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(); /* What to do if logged in already? */ 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; } /* FIXME game list API has changed */ 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(uname + "/" + gamelist[i].p); 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.connect) { /* 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; } } /* FIXME gamelist API has changed */ function getcurrent(clear) { if (window.WebSocket) { return; } if (session.connect || 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.connect || !("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 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 startgame(game) { if (!("lcred" in sessionStorage) || session.connect) return; if (!window.WebSocket) { return; } 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.connect = 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 makeWatcher(t) { function watcher(ev) { startwatching(t); } return watcher; } function startwatching(tag) { if (session.connect) return; var sockurl = "ws://" + window.location.host + "/watch/" + tag; var ws = new WebSocket(sockurl); ws.onopen = function (event) { session.connect = true; session.sock = ws; setmode("watch"); }; ws.onmessage = function (event) { var msgObject = JSON.parse(event.data); if (msgObject.t == 'w') { termemu.resize(msgObject.h, msgObject.w); termemu.reset(); termemu.toAltBuf(); var pname = msgObject.p; var gname = games[msgObject.g].name; message("You are now watching " + pname + " play " + gname + "."); } else if (msgObject.t == 'd') { writeData(msgObject.d); } }; ws.onclose = function (event) { session.sock = null; gameover(); }; } function formreg(ev) { ev.preventDefault(); /* This ought to check for being logged in instead. */ if (session.connect) 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."); 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 = "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.connect) 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.connect = false; session.playing = false; termemu.toNormBuf(); if ("lcred" in sessionStorage) setmode("choose"); else setmode("login"); return; } function logout() { sessionStorage.removeItem("lcred"); sessionStorage.removeItem("lname"); setmode("login"); } /* TODO determine whether this is needed */ function stop() { if (!session.connect) return; if (session.sock) { session.sock.close(); 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"; document.getElementById("playctl").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"; } wsCurrent(); } function toggleBlock(id) { var element = document.getElementById(id); if (!element) return; if (element.style.display != "block") element.style.display = "block"; else element.style.display = "none"; } function message(msg, mtype) { var msgdiv = document.createElement("div"); var msgtext = document.createTextNode(msg); msgdiv.appendChild(msgtext); if (mtype) { msgdiv.className = mtype; } var msgcontainer = document.getElementById("messages"); msgcontainer.insertBefore(msgdiv, msgcontainer.firstChild); } 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 setTitle(tstr) { message(tstr); } function textsize(larger) { var cssSize = termemu.view.style.fontSize; if (!cssSize) { return null; } var match = cssSize.match(/\d*/); if (!match) { return null; } 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; } var cssvalstr = nsize.toString() + "px"; document.getElementById("term").style.fontSize = cssvalstr; document.getElementById("keys").style.fontSize = cssvalstr; termemu.fixsize(); debug(1, "Changing font size to " + nsize.toString()); return nsize; } function idlestr(ms) {