comparison rlgterm.js @ 159:a613380ffdc2

RLGWebD: excise polling. WebSockets are supported nearly everywhere now. Listing current games and watching them are still broken.
author John "Elwin" Edwards
date Sat, 03 Jan 2015 15:23:04 -0500
parents e54018b26ed8
children a2a25b7631f1
comparison
equal deleted inserted replaced
158:9961a538c00e 159:a613380ffdc2
1 /* rlgterm.js: Roguelike Gallery's driver for termemu.js */ 1 /* rlgterm.js: Roguelike Gallery's driver for termemu.js */
2
3 // A state machine that keeps track of polling the server.
4 var ajaxstate = {
5 state: 0,
6 timerID: null,
7 clear: function () {
8 if (this.timerID != null) {
9 window.clearTimeout(this.timerID);
10 this.timerID = null;
11 }
12 },
13 set: function (ms) {
14 this.clear();
15 this.timerID = window.setTimeout(getData, ms);
16 },
17 gotdata: function () {
18 this.set(1000);
19 this.state = 1;
20 },
21 gotnothing: function () {
22 if (this.state == 0) {
23 this.set(1000);
24 this.state = 1;
25 }
26 else if (this.state < 4) {
27 this.set(4000);
28 this.state++;
29 }
30 else if (session.playing) {
31 if (this.state < 8) {
32 this.set(15000);
33 this.state++;
34 }
35 else {
36 /* It's been over a minute. Stop polling. */
37 this.clear();
38 }
39 }
40 else {
41 /* If watching, it can't stop polling entirely, because there
42 * are no POST events to start it up again. */
43 this.set(10000);
44 }
45 },
46 posted: function (wasdata) {
47 if (wasdata) {
48 this.set(1000);
49 this.state = 1;
50 }
51 else {
52 this.set(200);
53 this.state = 0;
54 }
55 }
56 };
57 2
58 /* Data on the available games. */ 3 /* Data on the available games. */
59 var games = { 4 var games = {
60 "rogue3": { 5 "rogue3": {
61 "name": "Rogue V3", 6 "name": "Rogue V3",
79 } 24 }
80 }; 25 };
81 26
82 var session = { 27 var session = {
83 /* The session id assigned by the server. */ 28 /* The session id assigned by the server. */
84 id: null, 29 connect: false,
85 /* Login name and key are now in sessionStorage. */ 30 /* Login name and key are now in sessionStorage. */
86 /* Whether the game is being played or just watched. */ 31 /* Whether the game is being played or just watched. */
87 playing: false, 32 playing: false,
88 /* WebSocket for communication */ 33 /* WebSocket for communication */
89 sock: null 34 sock: null
163 } 108 }
164 termemu.write(codes); 109 termemu.write(codes);
165 return; 110 return;
166 } 111 }
167 112
168 /* State for sending and receiving messages. */
169 var nsend = 0; // The number of the next packet to send.
170 var nrecv = 0; // The next packet expected.
171 var msgQ = []; // Queue for out-of-order messages.
172
173 /* Processes a message from the server, returning true or false if it was a
174 * data message with or without data, null if not data.
175 * All non-special responseTexts should be handed directly to this function.
176 */
177 function processMsg(msg) {
178 var msgDicts;
179 var havedata = null; // eventual return value
180 try {
181 msgDicts = JSON.parse(msg);
182 } catch (e) {
183 if (e instanceof SyntaxError)
184 return null;
185 }
186 if (msgDicts.length === 0)
187 return false;
188 for (var j = 0; j < msgDicts.length; j++) {
189 if (!msgDicts[j].t)
190 continue;
191 else if (msgDicts[j].t == "E") {
192 if (msgDicts[j].c == 1 || msgDicts[j].c == 6 || msgDicts[j].c == 7) {
193 gameover();
194 if (msgDicts[j].c == 1) {
195 logout();
196 }
197 }
198 debug(1, "Server error: " + msgDicts[j].s);
199 }
200 // A data message
201 else if (msgDicts[j].t == "d") {
202 if (msgDicts[j].n === nrecv) {
203 writeData(msgDicts[j].d);
204 nrecv++;
205 /* Process anything in the queue that's now ready. */
206 var next;
207 while ((next = msgQ.shift()) !== undefined) {
208 writeData(next.d);
209 nrecv++;
210 }
211 }
212 else if (msgDicts[j].n > nrecv) {
213 /* The current message comes after one still missing. Queue this one
214 * for later use. */
215 debug(1, "Got packet " + msgDicts[j].n + ", expected " + nrecv);
216 msgQ[msgDicts[j].n - nrecv - 1] = msgDicts[j];
217 }
218 else {
219 /* This message's number was encountered previously. */
220 debug(1, "Discarding packet " + msgDicts[j].n + ", expected " + nrecv);
221 }
222 havedata = true;
223 }
224 else if (msgDicts[j].t == "T") {
225 setTitle(msgDicts[j].d);
226 }
227 else if (msgDicts[j].t == "q") {
228 gameover();
229 }
230 else {
231 debug(1, "Unrecognized server message " + msg);
232 }
233 }
234 return havedata;
235 }
236
237 function getData() {
238 if (session.id == null)
239 return;
240 var datareq = new XMLHttpRequest();
241 var msg = JSON.stringify({"id": session.id, "t": "n"});
242 datareq.onerror = errHandler;
243 datareq.onreadystatechange = function () {
244 if (datareq.readyState == 4 && datareq.status == 200) {
245 var wasdata = processMsg(datareq.responseText);
246 if (wasdata != null) {
247 if (wasdata)
248 ajaxstate.gotdata();
249 else
250 ajaxstate.gotnothing();
251 }
252 return;
253 }
254 };
255 datareq.open('POST', '/feed', true);
256 datareq.send(msg);
257 return;
258 }
259
260 function postResponseHandler() {
261 if (this.readyState == 4 && this.status == 200) {
262 // We might want to do something with wasdata someday.
263 var wasdata = processMsg(this.responseText);
264 ajaxstate.posted();
265 return;
266 }
267 }
268
269 function errHandler() { 113 function errHandler() {
270 message("Unable to connect to the server.", "warn"); 114 message("Unable to connect to the server.", "warn");
271 } 115 }
272 116
273 function sendback(str) { 117 function sendback(str) {
274 /* For responding to terminal queries. */ 118 /* For responding to terminal queries. */
275 var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": str}; 119 if (session.sock) {
276 var datareq = new XMLHttpRequest(); 120 session.sock.send(JSON.stringify({"t": "d", "d": str}));
277 datareq.onerror = errHandler; 121 }
278 datareq.onreadystatechange = postResponseHandler;
279 datareq.open('POST', '/feed', true);
280 datareq.send(JSON.stringify(msgDict));
281 return; 122 return;
282 } 123 }
283 124
284 function sendkey(ev) { 125 function sendkey(ev) {
285 if (!session.playing) 126 if (!session.playing)
321 } 162 }
322 ev.preventDefault(); 163 ev.preventDefault();
323 if (session.sock) { 164 if (session.sock) {
324 session.sock.send(JSON.stringify({"t": "d", "d": code})); 165 session.sock.send(JSON.stringify({"t": "d", "d": code}));
325 } 166 }
326 else { 167 /* Otherwise it is disconnected */
327 var datareq = new XMLHttpRequest();
328 var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": code};
329 datareq.onerror = errHandler;
330 datareq.onreadystatechange = postResponseHandler;
331 datareq.open('POST', '/feed', true);
332 datareq.send(JSON.stringify(msgDict));
333 }
334 return; 168 return;
335 } 169 }
336 170
337 var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c", 171 var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c",
338 ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e" 172 ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e"
384 else 218 else
385 return; 219 return;
386 if (session.sock) { 220 if (session.sock) {
387 session.sock.send(JSON.stringify({"t": "d", "d": keystr})); 221 session.sock.send(JSON.stringify({"t": "d", "d": keystr}));
388 } 222 }
389 else {
390 var datareq = new XMLHttpRequest();
391 var msgDict = {"id": session.id, "t": "d", "n": nsend++, "d": keystr};
392 datareq.onerror = errHandler;
393 datareq.onreadystatechange = postResponseHandler;
394 datareq.open('POST', '/feed', true);
395 datareq.send(JSON.stringify(msgDict));
396 }
397 return; 223 return;
398 } 224 }
399 225
400 function setup() { 226 function setup() {
401 keyHexCodes.init(); 227 keyHexCodes.init();
424 } 250 }
425 else 251 else
426 break; 252 break;
427 } 253 }
428 if (!window.WebSocket) { 254 if (!window.WebSocket) {
429 message("Your browser does not support WebSockets. You can still play, " + 255 message("Your browser does not support WebSockets. " +
430 "but it will be slower, and may not work in the future.", "warn"); 256 "This Web app will not work.", "warn");
431 } 257 }
432 return; 258 return;
433 } 259 }
434 260
435 function toggleshift() { 261 function toggleshift() {
452 return; 278 return;
453 } 279 }
454 280
455 function formlogin(ev) { 281 function formlogin(ev) {
456 ev.preventDefault(); 282 ev.preventDefault();
457 if (session.id != null) 283 /* What to do if logged in already? */
458 return;
459 var loginmsg = {}; 284 var loginmsg = {};
460 loginmsg["name"] = document.getElementById("input_name").value; 285 loginmsg["name"] = document.getElementById("input_name").value;
461 loginmsg["pw"] = document.getElementById("input_pw").value; 286 loginmsg["pw"] = document.getElementById("input_pw").value;
462 var req = new XMLHttpRequest(); 287 var req = new XMLHttpRequest();
463 req.onerror = errHandler; 288 req.onerror = errHandler;
488 req.open('POST', '/login', true); 313 req.open('POST', '/login', true);
489 req.send(JSON.stringify(loginmsg)); 314 req.send(JSON.stringify(loginmsg));
490 return; 315 return;
491 } 316 }
492 317
318 /* FIXME game list API has changed */
493 function tableCurrent(gamelist) { 319 function tableCurrent(gamelist) {
494 var gamediv = document.getElementById("gametable"); 320 var gamediv = document.getElementById("gametable");
495 while (gamediv.children.length > 2) 321 while (gamediv.children.length > 2)
496 gamediv.removeChild(gamediv.children[2]); 322 gamediv.removeChild(gamediv.children[2]);
497 if (gamelist.length === 0) { 323 if (gamelist.length === 0) {
531 357
532 /* Handles the status socket, opening and closing it when necessary. */ 358 /* Handles the status socket, opening and closing it when necessary. */
533 function wsCurrent() { 359 function wsCurrent() {
534 if (!window.WebSocket) 360 if (!window.WebSocket)
535 return; 361 return;
536 if (session.id) { 362 if (session.connect) {
537 /* Don't bother with status if already playing/watching. */ 363 /* Don't bother with status if already playing/watching. */
538 if (statsock) { 364 if (statsock) {
539 statsock.close(); 365 statsock.close();
540 statsock = null; 366 statsock = null;
541 } 367 }
567 statsock.onclose = function (ev) { 393 statsock.onclose = function (ev) {
568 statsock = null; 394 statsock = null;
569 } 395 }
570 } 396 }
571 397
398 /* FIXME gamelist API has changed */
572 function getcurrent(clear) { 399 function getcurrent(clear) {
573 if (window.WebSocket) { 400 if (window.WebSocket) {
574 return; 401 return;
575 } 402 }
576 if (session.id || clear) { 403 if (session.connect || clear) {
577 if (statInterval) { 404 if (statInterval) {
578 window.clearInterval(statInterval); 405 window.clearInterval(statInterval);
579 statInterval = null; 406 statInterval = null;
580 } 407 }
581 return; 408 return;
606 req.send(); 433 req.send();
607 return; 434 return;
608 } 435 }
609 436
610 function getchoices() { 437 function getchoices() {
611 if (session.id != null || !("lcred" in sessionStorage)) 438 if (session.connect || !("lcred" in sessionStorage))
612 return; 439 return;
613 var req = new XMLHttpRequest(); 440 var req = new XMLHttpRequest();
614 req.onerror = errHandler; 441 req.onerror = errHandler;
615 req.onreadystatechange = function () { 442 req.onreadystatechange = function () {
616 if (req.readyState != 4 || req.status != 200) 443 if (req.readyState != 4 || req.status != 200)
678 startgame(game); 505 startgame(game);
679 } 506 }
680 return starter; 507 return starter;
681 } 508 }
682 509
683 function startgame(game) {
684 if (session.id != null || !("lcred" in sessionStorage))
685 return;
686 if (window.WebSocket) {
687 wsStart(game);
688 return;
689 }
690 var smsg = {};
691 smsg["key"] = sessionStorage.getItem("lcred");
692 smsg["game"] = game.uname;
693 smsg["h"] = 24;
694 smsg["w"] = 80;
695 var req = new XMLHttpRequest();
696 req.onerror = errHandler;
697 req.onreadystatechange = function () {
698 if (req.readyState != 4 || req.status != 200)
699 return;
700 var reply = JSON.parse(req.responseText);
701 if (reply.t == 's') {
702 /* Success */
703 session.id = reply.id;
704 session.playing = true;
705 termemu.resize(reply.h, reply.w);
706 message("You are now playing " + game.name + ".");
707 setmode("play");
708 getData();
709 }
710 else if (reply.t == 'E') {
711 if (reply.c == 1) {
712 logout();
713 message("The server forgot about you, please log in again.", "warn");
714 }
715 else if (reply.c == 4) {
716 if (reply.s == "dgamelaunch") {
717 message("You are already playing " + game.name + " over SSH.",
718 "warn");
719 }
720 else {
721 message("You are already playing " + game.name +
722 " in another browser window.", "warn");
723 }
724 }
725 else if (reply.c == 7) {
726 message("The game is being saved, try again in a few seconds.");
727 }
728 else {
729 message("The server says it can't start your game because \"" +
730 reply.s + "\". This is probably a bug.", "warn");
731 }
732 }
733 };
734 req.open('POST', '/play', true);
735 req.send(JSON.stringify(smsg));
736 return;
737 }
738
739 function makeStopper(gname) { 510 function makeStopper(gname) {
740 if (!(gname in games)) 511 if (!(gname in games))
741 return null; 512 return null;
742 var game = games[gname]; 513 var game = games[gname];
743 function stopper(ev) { 514 function stopper(ev) {
772 req.open('POST', '/quit', true); 543 req.open('POST', '/quit', true);
773 req.send(JSON.stringify(stopmsg)); 544 req.send(JSON.stringify(stopmsg));
774 return; 545 return;
775 } 546 }
776 547
777 function wsStart(game) { 548 function startgame(game) {
549 if (!("lcred" in sessionStorage) || session.connect)
550 return;
551 if (!window.WebSocket) {
552 return;
553 }
778 var sockurl = "ws://" + window.location.host + "/play/" + game.uname; 554 var sockurl = "ws://" + window.location.host + "/play/" + game.uname;
779 sockurl += "?key=" + sessionStorage.getItem("lcred") + "&w=80&h=24"; 555 sockurl += "?key=" + sessionStorage.getItem("lcred") + "&w=80&h=24";
780 ws = new WebSocket(sockurl); 556 ws = new WebSocket(sockurl);
781 ws.onopen = function (event) { 557 ws.onopen = function (event) {
782 session.id = true; 558 session.connect = true;
783 session.playing = true; 559 session.playing = true;
784 session.sock = ws; 560 session.sock = ws;
785 setmode("play"); 561 setmode("play");
786 }; 562 };
787 ws.onmessage = function (event) { 563 ws.onmessage = function (event) {
798 session.sock = null; 574 session.sock = null;
799 gameover(); 575 gameover();
800 }; 576 };
801 } 577 }
802 578
803 function startwatching(gamenumber) { 579 function makeWatcher(t) {
804 if (session.id != null)
805 return;
806 if (window.WebSocket) {
807 wsWatch(gamenumber);
808 return;
809 }
810 var wmsg = {"n": Number(gamenumber)};
811 var req = new XMLHttpRequest();
812 req.onerror = errHandler;
813 req.onreadystatechange = function () {
814 if (req.readyState != 4 || req.status != 200)
815 return;
816 var reply = JSON.parse(req.responseText);
817 if (reply.t == 'w') {
818 /* Success */
819 session.id = reply.id;
820 session.playing = false;
821 termemu.resize(reply.h, reply.w);
822 termemu.reset();
823 termemu.toAltBuf();
824 var pname = reply.p;
825 var gname = games[reply.g].name;
826 message("You are now watching " + pname + " play " + gname + ".");
827 setmode("watch");
828 getData();
829 }
830 else if (reply.t == 'E') {
831 message("The game could not be watched: " + reply.s, "warn");
832 getcurrent();
833 }
834 };
835 req.open('POST', '/watch', true);
836 req.send(JSON.stringify(wmsg));
837 return;
838 }
839
840 function makeWatcher(n) {
841 function watcher(ev) { 580 function watcher(ev) {
842 startwatching(n); 581 startwatching(t);
843 } 582 }
844 return watcher; 583 return watcher;
845 } 584 }
846 585
847 function wsWatch(gamenumber) { 586 function startwatching(tag) {
848 var sockurl = "ws://" + window.location.host + "/watch/" + String(gamenumber); 587 if (session.connect)
588 return;
589 var sockurl = "ws://" + window.location.host + "/watch/" + tag;
849 var ws = new WebSocket(sockurl); 590 var ws = new WebSocket(sockurl);
850 ws.onopen = function (event) { 591 ws.onopen = function (event) {
851 session.id = true; 592 session.connect = true;
852 session.sock = ws; 593 session.sock = ws;
853 setmode("watch"); 594 setmode("watch");
854 }; 595 };
855 ws.onmessage = function (event) { 596 ws.onmessage = function (event) {
856 var msgObject = JSON.parse(event.data); 597 var msgObject = JSON.parse(event.data);
872 }; 613 };
873 } 614 }
874 615
875 function formreg(ev) { 616 function formreg(ev) {
876 ev.preventDefault(); 617 ev.preventDefault();
877 if (session.id != null) 618 /* This ought to check for being logged in instead. */
619 if (session.connect)
878 return; 620 return;
879 var regmsg = {}; 621 var regmsg = {};
880 regmsg["name"] = document.getElementById("regin_name").value; 622 regmsg["name"] = document.getElementById("regin_name").value;
881 regmsg["pw"] = document.getElementById("regin_pw").value; 623 regmsg["pw"] = document.getElementById("regin_pw").value;
882 regmsg["email"] = document.getElementById("regin_email").value; 624 regmsg["email"] = document.getElementById("regin_email").value;
923 req.send(JSON.stringify(regmsg)); 665 req.send(JSON.stringify(regmsg));
924 return; 666 return;
925 } 667 }
926 668
927 function gameover() { 669 function gameover() {
928 if (session.id == null) 670 if (!session.connect)
929 return; 671 return;
930 /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */ 672 /* TODO IFACE2 If the end was unexpected, tell player the game was saved. */
931 if (session.playing) 673 if (session.playing)
932 message("Finished playing."); 674 message("Finished playing.");
933 else 675 else
934 message("Finished watching."); 676 message("Finished watching.");
935 session.id = null; 677 session.connect = false;
936 session.playing = false; 678 session.playing = false;
937 ajaxstate.clear();
938 termemu.toNormBuf(); 679 termemu.toNormBuf();
939 nsend = 0;
940 nrecv = 0;
941 msgQ = [];
942 if ("lcred" in sessionStorage) 680 if ("lcred" in sessionStorage)
943 setmode("choose"); 681 setmode("choose");
944 else 682 else
945 setmode("login"); 683 setmode("login");
946 return; 684 return;
950 sessionStorage.removeItem("lcred"); 688 sessionStorage.removeItem("lcred");
951 sessionStorage.removeItem("lname"); 689 sessionStorage.removeItem("lname");
952 setmode("login"); 690 setmode("login");
953 } 691 }
954 692
693 /* TODO determine whether this is needed */
955 function stop() { 694 function stop() {
956 if (!session.id) 695 if (!session.connect)
957 return; 696 return;
958 if (session.sock) { 697 if (session.sock) {
959 session.sock.close(); 698 session.sock.close();
960 return; 699 return;
961 } 700 }
962 var req = new XMLHttpRequest();
963 req.onerror = errHandler;
964 req.onreadystatechange = function () {
965 if (req.readyState == 4 && req.status == 200) {
966 processMsg(req.responseText);
967 return;
968 }
969 };
970 req.open('POST', '/feed', true);
971 req.send(JSON.stringify({"id": session.id, "t": "q"}));
972 return;
973 } 701 }
974 702
975 function setmode(mode, ev) { 703 function setmode(mode, ev) {
976 if (ev) 704 if (ev)
977 ev.preventDefault(); 705 ev.preventDefault();