Mercurial > hg > rlgwebd
comparison rlgterm.js @ 0:bd412f63ce0d
Put this project under version control, finally.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Sun, 06 May 2012 08:45:40 -0700 |
parents | |
children | ee22eb9ab009 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:bd412f63ce0d |
---|---|
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(100); | |
19 this.state = 0; | |
20 }, | |
21 gotnothing: function () { | |
22 if (this.state == 0) { | |
23 this.set(100); | |
24 this.state = 1; | |
25 } | |
26 else if (this.state == 1) { | |
27 this.set(300); | |
28 this.state = 2; | |
29 } | |
30 else if (this.state == 2) { | |
31 this.set(1000); | |
32 this.state = 3; | |
33 } | |
34 else { | |
35 this.set(5000); | |
36 this.state = 3; | |
37 } | |
38 }, | |
39 posted: function () { | |
40 this.set(100); | |
41 this.state = 0; | |
42 } | |
43 }; | |
44 | |
45 function writeData(hexstr) { | |
46 var codenum; | |
47 var codes = []; | |
48 var nc; | |
49 var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */ | |
50 var expect = 0; /* The number of 10------ bytes expected. */ | |
51 /* UTF-8 translation. */ | |
52 for (var i = 0; i < hexstr.length; i += 2) { | |
53 nc = Number("0x" + hexstr.substr(i, 2)); | |
54 if (nc < 0x7F) { | |
55 /* 0------- */ | |
56 codes.push(nc); | |
57 /* Any incomplete sequence will be discarded. */ | |
58 u8wait = 0; | |
59 expect = 0; | |
60 } | |
61 else if (nc < 0xC0) { | |
62 /* 10------ : part of a multibyte sequence */ | |
63 if (expect > 0) { | |
64 u8wait <<= 6; | |
65 u8wait += (nc & 0x3F); | |
66 expect--; | |
67 if (expect == 0) { | |
68 codes.push(u8wait); | |
69 u8wait = 0; | |
70 } | |
71 } | |
72 else { | |
73 /* Assume an initial byte was missed. */ | |
74 u8wait = 0; | |
75 } | |
76 } | |
77 /* These will all discard any incomplete sequence. */ | |
78 else if (nc < 0xE0) { | |
79 /* 110----- : introduces 2-byte sequence */ | |
80 u8wait = (nc & 0x1F); | |
81 expect = 1; | |
82 } | |
83 else if (nc < 0xF0) { | |
84 /* 1110---- : introduces 3-byte sequence */ | |
85 u8wait = (nc & 0x0F); | |
86 expect = 2; | |
87 } | |
88 else if (nc < 0xF8) { | |
89 /* 11110--- : introduces 4-byte sequence */ | |
90 u8wait = (nc & 0x07); | |
91 expect = 3; | |
92 } | |
93 else if (nc < 0xFC) { | |
94 /* 111110-- : introduces 5-byte sequence */ | |
95 u8wait = (nc & 0x03); | |
96 expect = 4; | |
97 } | |
98 else if (nc < 0xFE) { | |
99 /* 1111110- : introduces 6-byte sequence */ | |
100 u8wait = (nc & 0x01); | |
101 expect = 5; | |
102 } | |
103 else { | |
104 /* 1111111- : should never appear */ | |
105 u8wait = 0; | |
106 expect = 0; | |
107 } | |
108 /* Supporting all 31 bits is probably overkill... */ | |
109 } | |
110 termemu.write(codes); | |
111 return; | |
112 } | |
113 | |
114 /* Processes a message from the server, returning true or false if it was a | |
115 * data message with or without data, null if not data. */ | |
116 function processMsg(msg) { | |
117 var msglines = msg.split("\n"); | |
118 var havedata = null; | |
119 if (!msglines[0]) | |
120 return null; | |
121 if (msglines[0].charAt(0) == 'd') { | |
122 if (msglines[1]){ | |
123 writeData(msglines[1]); | |
124 havedata = true; | |
125 } | |
126 else { | |
127 havedata = false; | |
128 } | |
129 } | |
130 else if (msglines[0] == "E1") { | |
131 logout(); | |
132 } | |
133 else if (msglines[0].charAt(0) == "T") { | |
134 setTitle(msglines[1]); | |
135 } | |
136 else if (msglines[0] == "q1") { | |
137 logout(); | |
138 } | |
139 else { | |
140 debug(1, "Unrecognized server message " + msglines[0]); | |
141 } | |
142 return havedata; | |
143 } | |
144 | |
145 function getData() { | |
146 if (termemu.sessid == null) | |
147 return; | |
148 var datareq = new XMLHttpRequest(); | |
149 datareq.onreadystatechange = function () { | |
150 if (datareq.readyState == 4 && datareq.status == 200) { | |
151 var wasdata = processMsg(datareq.responseText); | |
152 if (wasdata != null) { | |
153 if (wasdata) | |
154 ajaxstate.gotdata(); | |
155 else | |
156 ajaxstate.gotnothing(); | |
157 } | |
158 return; | |
159 } | |
160 }; | |
161 datareq.open('POST', '/feed', true); | |
162 datareq.send("id=" + termemu.sessid); | |
163 return; | |
164 } | |
165 | |
166 function postResponseHandler() { | |
167 if (this.readyState == 4 && this.status == 200) { | |
168 // We might want to do something with wasdata someday. | |
169 var wasdata = processMsg(this.responseText); | |
170 ajaxstate.posted(); | |
171 return; | |
172 } | |
173 } | |
174 | |
175 function sendback(str) { | |
176 /* For responding to terminal queries. */ | |
177 var datareq = new XMLHttpRequest(); | |
178 datareq.onreadystatechange = postResponseHandler; | |
179 datareq.open('POST', '/feed', true); | |
180 datareq.send("id=" + termemu.sessid + "&keys=" + str); | |
181 return; | |
182 } | |
183 | |
184 /* ASCII values of keys 0-9. */ | |
185 var numShifts = [41, 33, 64, 35, 36, 37, 94, 38, 42, 40]; | |
186 | |
187 var keyHexCodes = { | |
188 init: function () { | |
189 this[KeyboardEvent.DOM_VK_RETURN] = ["0d", "0d"]; | |
190 this[KeyboardEvent.DOM_VK_SPACE] = ["20", "20"]; | |
191 this[KeyboardEvent.DOM_VK_TAB] = ["09", "09"]; | |
192 this[KeyboardEvent.DOM_VK_BACK_QUOTE] = ["60", "7e"]; | |
193 this[KeyboardEvent.DOM_VK_OPEN_BRACKET] = ["5b", "7b"]; | |
194 this[KeyboardEvent.DOM_VK_CLOSE_BRACKET] = ["5d", "7d"]; | |
195 this[KeyboardEvent.DOM_VK_BACK_SLASH] = ["5c", "7c"]; | |
196 this[KeyboardEvent.DOM_VK_SEMICOLON] = ["3b", "3a"]; | |
197 this[KeyboardEvent.DOM_VK_QUOTE] = ["27", "22"]; | |
198 this[KeyboardEvent.DOM_VK_COMMA] = ["2c", "3c"]; | |
199 this[KeyboardEvent.DOM_VK_PERIOD] = ["2e", "3e"]; | |
200 this[KeyboardEvent.DOM_VK_SLASH] = ["2f", "3f"]; | |
201 this[KeyboardEvent.DOM_VK_EQUALS] = ["3d", "2b"]; | |
202 this[KeyboardEvent.DOM_VK_SUBTRACT] = ["2d", "5f"]; | |
203 this[KeyboardEvent.DOM_VK_BACK_SPACE] = ["08", "08"]; | |
204 this[KeyboardEvent.DOM_VK_ESCAPE] = ["1b", "1b"]; | |
205 /* Multi-char control sequences! Neat! */ | |
206 this[KeyboardEvent.DOM_VK_PAGE_UP] = ["1b5b357e", "1b5b357e"]; | |
207 this[KeyboardEvent.DOM_VK_PAGE_DOWN] = ["1b5b367e", "1b5b367e"]; | |
208 this.appCursor(false); | |
209 this.appKeypad(false); | |
210 }, | |
211 appCursor: function (on) { | |
212 if (on) { | |
213 this[KeyboardEvent.DOM_VK_LEFT] = ["1b4f44", "1b4f44"]; | |
214 this[KeyboardEvent.DOM_VK_RIGHT] = ["1b4f43", "1b4f43"]; | |
215 this[KeyboardEvent.DOM_VK_UP] = ["1b4f41", "1b4f41"]; | |
216 this[KeyboardEvent.DOM_VK_DOWN] = ["1b4f42", "1b4f42"]; | |
217 this[KeyboardEvent.DOM_VK_END] = ["1b4f46", "1b4f46"]; | |
218 this[KeyboardEvent.DOM_VK_HOME] = ["1b4f48", "1b4f48"]; | |
219 } | |
220 else { | |
221 this[KeyboardEvent.DOM_VK_LEFT] = ["1b5b44", "1b5b44"]; | |
222 this[KeyboardEvent.DOM_VK_RIGHT] = ["1b5b43", "1b5b43"]; | |
223 this[KeyboardEvent.DOM_VK_UP] = ["1b5b41", "1b5b41"]; | |
224 this[KeyboardEvent.DOM_VK_DOWN] = ["1b5b42", "1b5b42"]; | |
225 this[KeyboardEvent.DOM_VK_END] = ["1b5b46", "1b5b46"]; | |
226 this[KeyboardEvent.DOM_VK_HOME] = ["1b5b48", "1b5b48"]; | |
227 } | |
228 }, | |
229 appKeypad: function (on) { | |
230 /* In theory, these should produce either numerals or the k[a-c][1-3] | |
231 * sequences. Since we can't count on the terminfo description actually | |
232 * containing those sequences, pretend they're just arrow keys etc. | |
233 */ | |
234 this[KeyboardEvent.DOM_VK_NUMPAD1] = ["1b4f46", "1b4f46"]; | |
235 this[KeyboardEvent.DOM_VK_NUMPAD2] = ["1b4f42", "1b4f42"]; | |
236 this[KeyboardEvent.DOM_VK_NUMPAD3] = ["1b5b367e", "1b5b367e"]; | |
237 this[KeyboardEvent.DOM_VK_NUMPAD4] = ["1b4f44", "1b4f44"]; | |
238 this[KeyboardEvent.DOM_VK_NUMPAD5] = ["1b5b45", "1b5b45"]; | |
239 this[KeyboardEvent.DOM_VK_NUMPAD6] = ["1b4f43", "1b4f43"]; | |
240 this[KeyboardEvent.DOM_VK_NUMPAD7] = ["1b4f48", "1b4f48"]; | |
241 this[KeyboardEvent.DOM_VK_NUMPAD8] = ["1b4f41", "1b4f41"]; | |
242 this[KeyboardEvent.DOM_VK_NUMPAD9] = ["1b5b357e", "1b5b357e"]; | |
243 return; | |
244 } | |
245 }; | |
246 | |
247 function sendkey(ev) { | |
248 if (termemu.sessid == null) | |
249 return; | |
250 var keynum = ev.keyCode; | |
251 var code; | |
252 if (keynum >= ev.DOM_VK_A && keynum <= ev.DOM_VK_Z) { | |
253 /* Letters. This assumes the codes are 65-90. */ | |
254 if (ev.ctrlKey) | |
255 keynum -= 64; | |
256 else if (!ev.shiftKey) | |
257 keynum += 32; | |
258 code = keynum.toString(16); | |
259 if (code.length < 2) | |
260 code = "0" + code; | |
261 } | |
262 else if (keynum >= ev.DOM_VK_0 && keynum <= ev.DOM_VK_9) { | |
263 /* The number row, NOT the numpad. */ | |
264 if (ev.shiftKey) { | |
265 code = numShifts[keynum - 48].toString(16); | |
266 } | |
267 else { | |
268 code = keynum.toString(16); | |
269 } | |
270 } | |
271 else if (keynum in keyHexCodes) { | |
272 if (ev.shiftKey) | |
273 code = keyHexCodes[keynum][1]; | |
274 else | |
275 code = keyHexCodes[keynum][0]; | |
276 } | |
277 else if (keynum == ev.DOM_VK_SHIFT || keynum == ev.DOM_VK_CONTROL || | |
278 keynum == ev.DOM_VK_ALT || keynum == ev.DOM_VK_CAPS_LOCK) { | |
279 return; | |
280 } | |
281 else { | |
282 debug(1, "Ignoring keycode " + keynum); | |
283 return; | |
284 } | |
285 if (termemu.sessid != null) | |
286 ev.preventDefault(); | |
287 var datareq = new XMLHttpRequest(); | |
288 datareq.onreadystatechange = postResponseHandler; | |
289 datareq.open('POST', '/feed', true); | |
290 datareq.send("id=" + termemu.sessid + "&keys=" + code); | |
291 return; | |
292 } | |
293 | |
294 var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c", | |
295 ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e" | |
296 } | |
297 | |
298 function vkey(c) { | |
299 if (termemu.sessid == null) | |
300 return; | |
301 var keystr; | |
302 if (c.match(/^[a-z]$/)) { | |
303 if (termemu.ctrlp()) { | |
304 var n = c.charCodeAt(0) - 96; | |
305 keystr = n.toString(16); | |
306 if (keystr.length < 2) | |
307 keystr = "0" + keystr; | |
308 } | |
309 else if (termemu.shiftp()) | |
310 keystr = c.toUpperCase().charCodeAt(0).toString(16); | |
311 else | |
312 keystr = c.charCodeAt(0).toString(16); | |
313 } | |
314 else if (c.match(/^[0-9]$/)) { | |
315 if (termemu.shiftp()) | |
316 keystr = numShifts[c.charCodeAt(0) - 48].toString(16); | |
317 else | |
318 keystr = c.charCodeAt(0).toString(16); | |
319 } | |
320 else if (c == '\n') | |
321 keystr = "0a"; | |
322 else if (c == '\t') | |
323 keystr = "09"; | |
324 else if (c == '\b') | |
325 keystr = "08"; | |
326 else if (c == ' ') | |
327 keystr = "20"; | |
328 else if (c in charshifts) { | |
329 if (termemu.shiftp()) | |
330 keystr = charshifts[c]; | |
331 else | |
332 keystr = c.charCodeAt(0).toString(16); | |
333 } | |
334 else | |
335 return; | |
336 //writeData("Sending " + keystr); | |
337 var datareq = new XMLHttpRequest(); | |
338 datareq.onreadystatechange = postResponseHandler; | |
339 datareq.open('POST', '/feed', true); | |
340 datareq.send("id=" + termemu.sessid + "&keys=" + keystr); | |
341 return; | |
342 } | |
343 | |
344 function setup() { | |
345 keyHexCodes.init(); | |
346 termemu.init("termwrap"); | |
347 setTitle("Not connected."); | |
348 return; | |
349 } | |
350 | |
351 function toggleshift() { | |
352 termemu.toggleshift(); | |
353 keydiv = document.getElementById("shiftkey"); | |
354 if (termemu.shiftp()) | |
355 keydiv.className = "keysel"; | |
356 else | |
357 keydiv.className = "key"; | |
358 return; | |
359 } | |
360 | |
361 function togglectrl() { | |
362 termemu.togglectrl(); | |
363 keydiv = document.getElementById("ctrlkey"); | |
364 if (termemu.ctrlp()) | |
365 keydiv.className = "keysel"; | |
366 else | |
367 keydiv.className = "key"; | |
368 return; | |
369 } | |
370 | |
371 function formlogin(ev) { | |
372 ev.preventDefault(); | |
373 if (termemu.sessid != null) | |
374 return; | |
375 var formname = document.getElementById("input_name").value; | |
376 var formpass = document.getElementById("input_pw").value; | |
377 var formgame = document.getElementById("input_game").value; | |
378 var formdata = "game=" + encodeURIComponent(formgame) + "&name=" + encodeURIComponent(formname) + "&pw=" + encodeURIComponent(formpass); | |
379 var req = new XMLHttpRequest(); | |
380 req.onreadystatechange = function () { | |
381 if (req.readyState == 4 && req.status == 200) { | |
382 var datalines = req.responseText.split("\n"); | |
383 if (datalines[0] == 'l1') { | |
384 /* Success */ | |
385 termemu.sessid = datalines[1]; | |
386 setTitle("Playing as " + formname); | |
387 debug(1, "Logged in with id " + termemu.sessid); | |
388 document.getElementById("loginform").style.display = "none"; | |
389 getData(); | |
390 } | |
391 else { | |
392 debug(1, "Could not start game: " + datalines[1]); | |
393 document.getElementById("input_name").value = ""; | |
394 document.getElementById("input_pw").value = ""; | |
395 } | |
396 } | |
397 }; | |
398 req.open('POST', '/login', true); | |
399 req.send(formdata); | |
400 return; | |
401 } | |
402 | |
403 function logout() { | |
404 if (termemu.sessid == null) | |
405 return; | |
406 termemu.sessid = null; | |
407 setTitle("Game over."); | |
408 document.getElementById("loginform").style.display = "block"; | |
409 return; | |
410 } | |
411 | |
412 function stop() { | |
413 var req = new XMLHttpRequest(); | |
414 req.onreadystatechange = function () { | |
415 if (req.readyState == 4 && req.status == 200) { | |
416 processMsg(req.responseText); | |
417 return; | |
418 } | |
419 }; | |
420 req.open('POST', '/feed', true); | |
421 req.send("id=" + termemu.sessid + "&quit=quit"); | |
422 return; | |
423 } | |
424 | |
425 function debug(level, msg) { | |
426 if (level < debugSuppress) | |
427 return; | |
428 var msgdiv = document.createElement("div"); | |
429 var msgtext = document.createTextNode(msg); | |
430 msgdiv.appendChild(msgtext); | |
431 document.getElementById("debug").appendChild(msgdiv); | |
432 return; | |
433 } | |
434 | |
435 function textsize(larger) { | |
436 var cssSize = termemu.view.style.fontSize; | |
437 if (!cssSize) { | |
438 return; | |
439 } | |
440 var match = cssSize.match(/\d*/); | |
441 if (!match) { | |
442 return; | |
443 } | |
444 var csize = Number(match[0]); | |
445 var nsize; | |
446 if (larger) { | |
447 if (csize >= 48) | |
448 nsize = 48; | |
449 else if (csize >= 20) | |
450 nsize = csize + 4; | |
451 else if (csize >= 12) | |
452 nsize = csize + 2; | |
453 else if (csize >= 8) | |
454 nsize = csize + 1; | |
455 else | |
456 nsize = 8; | |
457 } | |
458 else { | |
459 if (csize <= 8) | |
460 nsize = 8; | |
461 else if (csize <= 12) | |
462 nsize = csize - 1; | |
463 else if (csize <= 20) | |
464 nsize = csize - 2; | |
465 else if (csize <= 48) | |
466 nsize = csize - 4; | |
467 else | |
468 nsize = 48; | |
469 } | |
470 document.getElementById("term").style.fontSize = nsize.toString() + "px"; | |
471 termemu.resize(); | |
472 debug(1, "Changing font size to " + nsize.toString()); | |
473 return; | |
474 } | |
475 | |
476 function bell(on) { | |
477 var imgnode = document.getElementById("bell"); | |
478 if (on) { | |
479 imgnode.style.visibility = "visible"; | |
480 window.setTimeout(bell, 1500, false); | |
481 } | |
482 else | |
483 imgnode.style.visibility = "hidden"; | |
484 return; | |
485 } |