Mercurial > hg > rlgwebd
view rlgterm.js @ 213:08665c56c6a0 default tip
Fix race condition related to watching DGL games.
It's possible for a dgamelaunch game to end and cause rlgwebd to stop watching
it before rlgwebd has started watching it.
author | John "Elwin" Edwards |
---|---|
date | Sun, 16 Aug 2020 20:56:18 -0400 |
parents | f3843245a35e |
children |
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" }, "arogue7": { "name": "Advanced Rogue 7", "uname": "arogue7" }, "xrogue": { "name": "XRogue", "uname": "xrogue" } }; 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, /* If watching, the name of the player. */ playername: null, /* Whether the Stop button was pushed. */ leaving: 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; /* List of currently active games. */ var currentList = []; /* Last time the list was updated. */ var currentTS = 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; } 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"); var cell5 = 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; } var srcstr = "VR"; if (gamelist[i].c == "rlg") srcstr = "Web"; else if (gamelist[i].c == "dgl") srcstr = "SSH"; cell3.appendChild(document.createTextNode(srcstr)); cell4.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"; cell5.appendChild(button); row.appendChild(cell1); row.appendChild(cell2); row.appendChild(cell3); row.appendChild(cell4); row.appendChild(cell5); 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; var wsproto = "ws://"; if (window.location.protocol == "https:") wsproto = "wss://"; statsock = new WebSocket(wsproto + 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") { currentList = msg.g; currentTS = new Date(); tableCurrent(currentList); } else if (msg.t == "p") { var now = new Date(); var idletimes = {}; for (var i = 0; i < msg.g.length; i++) { var tag = msg.g[i].g + "/" + msg.g[i].p; idletimes[tag] = msg.g[i].i; } for (var i = 0; i < currentList.length; i++) { var tag = currentList[i].g + "/" + currentList[i].p; if (tag in idletimes) { currentList[i].i = idletimes[tag]; } else { currentList[i].i += now - currentTS; } } currentTS = now; tableCurrent(currentList); } else if (msg.t == "b") { var justbegun = {}; justbegun.g = msg.g; justbegun.p = msg.p; justbegun.c = msg.c; justbegun.i = 0; currentList.push(justbegun); tableCurrent(currentList); if (msg.p == sessionStorage.getItem("lname")) { getchoices(); } } else if (msg.t == "e") { var i = 0; while (i < currentList.length) { if (currentList[i].g == msg.g && currentList[i].p == msg.p) break; i++; } if (i < currentList.length) { currentList.splice(i, 1); } tableCurrent(currentList); if (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 wsproto = "ws://"; if (window.location.protocol == "https:") wsproto = "wss://"; var sockurl = wsproto + 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 wsproto = "ws://"; if (window.location.protocol == "https:") wsproto = "wss://"; var sockurl = wsproto + 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; session.playername = pname; 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 { if (session.leaving) { /* Client-initiated: the user stopped watching. */ message("Finished watching " + session.playername + "."); } else { /* Server-initiated: end of game. */ message(session.playername + " has finished playing."); } session.playername = null; } session.connect = false; session.playing = false; session.leaving = false; termemu.toNormBuf(); if ("lcred" in sessionStorage) setmode("choose"); else setmode("login"); return; } function logout() { sessionStorage.removeItem("lcred"); sessionStorage.removeItem("lname"); setmode("login"); } function stop() { if (!session.connect) return; if (session.sock) { session.leaving = true; 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();