| | 1 | #charset "us-ascii" |
| | 2 | |
| | 3 | /* |
| | 4 | * Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved. |
| | 5 | * |
| | 6 | * TADS 3 Library: input |
| | 7 | * |
| | 8 | * This modules defines functions and objects related to reading input |
| | 9 | * from the player. |
| | 10 | */ |
| | 11 | |
| | 12 | #include "adv3.h" |
| | 13 | |
| | 14 | |
| | 15 | /* ------------------------------------------------------------------------ */ |
| | 16 | /* |
| | 17 | * Keyboard input parameter definition. |
| | 18 | */ |
| | 19 | class InputDef: object |
| | 20 | /* |
| | 21 | * The prompt function. This is a function pointer (which is |
| | 22 | * frequently given as an anonymous function) or nil; if it's nil, |
| | 23 | * we won't show any prompt at all, otherwise we'll call the |
| | 24 | * function pointer to display a prompt as needed. |
| | 25 | */ |
| | 26 | promptFunc = nil |
| | 27 | |
| | 28 | /* |
| | 29 | * Allow real-time events. If this is true, we'll allow real-time |
| | 30 | * events to interrupt the input; if it's nil, we'll freeze the |
| | 31 | * real-time clock while reading input. |
| | 32 | */ |
| | 33 | allowRealTime = nil |
| | 34 | |
| | 35 | /* |
| | 36 | * Begin the input style. This should do anything required to set |
| | 37 | * the font to the desired attributes for the input text. By |
| | 38 | * default, we'll simply display <.inputline> to set up the default |
| | 39 | * input style. |
| | 40 | */ |
| | 41 | beginInputFont() { "<.inputline>"; } |
| | 42 | |
| | 43 | /* |
| | 44 | * End the input style. By default, we'll close the <.inputline> |
| | 45 | * that we opened in beginInputFont(). |
| | 46 | */ |
| | 47 | endInputFont() { "<./inputline>"; } |
| | 48 | ; |
| | 49 | |
| | 50 | /* |
| | 51 | * Basic keyboard input parameter definition. This class defines |
| | 52 | * keyboard input parameters with the real-time status and prompt |
| | 53 | * function specified via the constructor. |
| | 54 | */ |
| | 55 | class BasicInputDef: InputDef |
| | 56 | construct(allowRealTime, promptFunc) |
| | 57 | { |
| | 58 | self.allowRealTime = allowRealTime; |
| | 59 | self.promptFunc = promptFunc; |
| | 60 | } |
| | 61 | ; |
| | 62 | |
| | 63 | |
| | 64 | /* ------------------------------------------------------------------------ */ |
| | 65 | /* |
| | 66 | * Keyboard input manager. |
| | 67 | */ |
| | 68 | inputManager: PostRestoreObject |
| | 69 | /* |
| | 70 | * Read a line of input from the keyboard. |
| | 71 | * |
| | 72 | * If allowRealTime is true, we'll execute any real-time events that |
| | 73 | * are already due to run, and then we'll allow the input to be |
| | 74 | * interrupted by real-time events, if interrupted input is |
| | 75 | * supported on the local platform. Otherwise, we will not process |
| | 76 | * any real-time events. |
| | 77 | * |
| | 78 | * promptFunc is a callback function to invoke to display the |
| | 79 | * prompt. This is provided as a callback so that we can re-display |
| | 80 | * the prompt as necessary after real-time event interruptions. |
| | 81 | * Note that if real-time interruption is not to be allowed, the |
| | 82 | * caller can simply display the prompt before calling this routine |
| | 83 | * rather than passing in a prompt callback, if desired. |
| | 84 | * |
| | 85 | * If we're in HTML mode, this will switch into the 'tads-input' |
| | 86 | * font while reading the line, so this routine should be used |
| | 87 | * wherever possible rather than calling inputLine() or |
| | 88 | * inputLineTimeout() directly. |
| | 89 | */ |
| | 90 | getInputLine(allowRealTime, promptFunc) |
| | 91 | { |
| | 92 | /* read input using a basic InputDef for the given parameters */ |
| | 93 | return getInputLineExt(new BasicInputDef(allowRealTime, promptFunc)); |
| | 94 | } |
| | 95 | |
| | 96 | /* |
| | 97 | * Read a line of input from the keyboard - extended interface, |
| | 98 | * using the InputDef object to define the input parameters. |
| | 99 | * 'defObj' is an instance of class InputDef, defining how we're to |
| | 100 | * handle the input. |
| | 101 | */ |
| | 102 | getInputLineExt(defObj) |
| | 103 | { |
| | 104 | /* make sure the command transcript is flushed */ |
| | 105 | if (gTranscript != nil) |
| | 106 | gTranscript.flushForInput(); |
| | 107 | |
| | 108 | /* |
| | 109 | * If a previous input was in progress, cancel it - this must be |
| | 110 | * a recursive entry from a real-time event that's interrupting |
| | 111 | * the enclosing input attempt. Simply cancel out the enclosing |
| | 112 | * read attempt entirely in this case; if and when we return to |
| | 113 | * the enclosing reader, that reader will start over with a |
| | 114 | * fresh read attempt at that point. |
| | 115 | */ |
| | 116 | cancelInputInProgress(true); |
| | 117 | |
| | 118 | /* |
| | 119 | * Keep going until we finish reading the command. We might |
| | 120 | * have to try several times, because our attempts might be |
| | 121 | * interrupted by real-time events. |
| | 122 | */ |
| | 123 | for (;;) |
| | 124 | { |
| | 125 | local result; |
| | 126 | local timeout; |
| | 127 | local t0; |
| | 128 | |
| | 129 | /* note the starting time, in case we want to freeze the clock */ |
| | 130 | t0 = realTimeManager.getElapsedTime(); |
| | 131 | |
| | 132 | /* process real-time events, if possible */ |
| | 133 | timeout = processRealTimeEvents(defObj.allowRealTime); |
| | 134 | |
| | 135 | /* show the prompt and any pre-input codes */ |
| | 136 | inputLineBegin(defObj); |
| | 137 | |
| | 138 | getInput: |
| | 139 | /* |
| | 140 | * Read the input. (Note that if our timeout is nil, this |
| | 141 | * will simply act like the ordinary untimed inputLine.) |
| | 142 | */ |
| | 143 | result = inputLineTimeout(timeout); |
| | 144 | |
| | 145 | /* |
| | 146 | * If we're not allowing real-time event processing, freeze |
| | 147 | * the clock during the read - set the elapsed game |
| | 148 | * real-time clock back to the value it had on entry, so |
| | 149 | * that the input effectively consumes no real time. |
| | 150 | */ |
| | 151 | if (!defObj.allowRealTime) |
| | 152 | realTimeManager.setElapsedTime(t0); |
| | 153 | |
| | 154 | /* check the event code from the result list */ |
| | 155 | switch(result[1]) |
| | 156 | { |
| | 157 | case InEvtNoTimeout: |
| | 158 | /* |
| | 159 | * the platform doesn't support timeouts - note it for |
| | 160 | * future reference so that we don't ask for input with |
| | 161 | * timeout again, then go back to try the input again |
| | 162 | * without a timeout |
| | 163 | */ |
| | 164 | noInputTimeout = true; |
| | 165 | timeout = nil; |
| | 166 | goto getInput; |
| | 167 | |
| | 168 | case InEvtLine: |
| | 169 | /* we've finished the current line - end input mode */ |
| | 170 | inputLineEnd(); |
| | 171 | |
| | 172 | /* return the line of text we got */ |
| | 173 | return result[2]; |
| | 174 | |
| | 175 | case InEvtTimeout: |
| | 176 | /* |
| | 177 | * We got a timeout without finishing the input line. |
| | 178 | * This means that we have reached the time when the |
| | 179 | * next real-time event is ready to execute. Simply |
| | 180 | * continue looping; we'll process all real-time events |
| | 181 | * that are ready to go, then we'll resume reading the |
| | 182 | * command. |
| | 183 | * |
| | 184 | * Before we proceed, though, notify the command |
| | 185 | * sequencer (via the command-interrupt pseudo-tag) that |
| | 186 | * we're at the start of output text after an |
| | 187 | * interrupted command line input |
| | 188 | */ |
| | 189 | "<.commandint>"; |
| | 190 | break; |
| | 191 | |
| | 192 | case InEvtEof: |
| | 193 | /* |
| | 194 | * End of file - this indicates that the user has closed |
| | 195 | * down the application, or that the keyboard has become |
| | 196 | * unreadable due to a hardware or OS error. |
| | 197 | * |
| | 198 | * Write a blank line to the display in an attempt to |
| | 199 | * flush any partially-entered command line text, then |
| | 200 | * throw an error to signal the EOF condition. |
| | 201 | */ |
| | 202 | "\b"; |
| | 203 | throw new EndOfFileException(); |
| | 204 | |
| | 205 | case InEvtEndQuietScript: |
| | 206 | /* |
| | 207 | * End of "quiet" script - this indicates that we've |
| | 208 | * been reading input from a script file, but we've now |
| | 209 | * reached the end of that file and are about to return |
| | 210 | * to reading from the keyboard. |
| | 211 | * |
| | 212 | * "Quiet script" mode causes all output to be hidden |
| | 213 | * while the script is being processed. This means that |
| | 214 | * we won't have displayed a prompt for the current |
| | 215 | * line, or updated the status line. We'll |
| | 216 | * automatically display a new prompt when we loop back |
| | 217 | * for another line of input, but we have to mark the |
| | 218 | * current input line as actually ended now for that to |
| | 219 | * happen. |
| | 220 | */ |
| | 221 | inputLineInProgress = nil; |
| | 222 | inProgressDefObj = nil; |
| | 223 | |
| | 224 | /* |
| | 225 | * update the status line, since the quiet script mode |
| | 226 | * will have suppressed all status line updates while we |
| | 227 | * were reading the script, and thus the last update |
| | 228 | * before this prompt won't have been shown |
| | 229 | */ |
| | 230 | statusLine.showStatusLine(); |
| | 231 | |
| | 232 | /* back for more */ |
| | 233 | break; |
| | 234 | } |
| | 235 | } |
| | 236 | } |
| | 237 | |
| | 238 | /* |
| | 239 | * Pause for a MORE prompt. If freezeRealTime is true, we'll stop |
| | 240 | * the real-time clock; otherwise we'll let it keep running. Even if |
| | 241 | * we don't freeze the clock, we won't actually process any real-time |
| | 242 | * events while waiting: the point of the MORE prompt is to allow the |
| | 243 | * player to read and acknowledge the on-screen display before |
| | 244 | * showing anything new, so we don't want to allow any output to |
| | 245 | * result from real-time events that occur while waiting for user |
| | 246 | * input. If any real-time events come due while we're waiting, |
| | 247 | * we'll process them when we're done. |
| | 248 | * |
| | 249 | * In order to ensure that the display makes sense to the user, we |
| | 250 | * flush any captured input in the transcript before pausing. We |
| | 251 | * re-activate transcript capture after the pause if it was active |
| | 252 | * before. Note that in some cases, this could affect the overall |
| | 253 | * output for the command, since some commands wait until the very |
| | 254 | * end of the command to go back and process the entire transcript |
| | 255 | * for the command. Since we interrupt the transcript, flushing any |
| | 256 | * output that occurred before the pause, a command that goes back |
| | 257 | * over its entire output stream at the end of the turn won't be able |
| | 258 | * to see or modify any of the output that occurred prior to the |
| | 259 | * pause, since we will have flushed the output to that point. |
| | 260 | */ |
| | 261 | pauseForMore(freezeRealTime) |
| | 262 | { |
| | 263 | local t0; |
| | 264 | local wasTranscriptActive = nil; |
| | 265 | |
| | 266 | /* |
| | 267 | * flush any command transcript and turn off transcript capture, |
| | 268 | * so that we show any pent-up reports before pausing for the |
| | 269 | * MORE prompt |
| | 270 | */ |
| | 271 | if (gTranscript != nil) |
| | 272 | wasTranscriptActive = gTranscript.flushForInput(); |
| | 273 | |
| | 274 | /* |
| | 275 | * cancel any pending input - we must be interrupting the |
| | 276 | * pending input with a real-time event |
| | 277 | */ |
| | 278 | cancelInputInProgress(true); |
| | 279 | |
| | 280 | /* note the starting time, in case we want to freeze the clock */ |
| | 281 | t0 = realTimeManager.getElapsedTime(); |
| | 282 | |
| | 283 | /* run the MORE prompt */ |
| | 284 | morePrompt(); |
| | 285 | |
| | 286 | /* if the transcript was previously active, re-activate it */ |
| | 287 | if (wasTranscriptActive) |
| | 288 | gTranscript.activate(); |
| | 289 | |
| | 290 | /* |
| | 291 | * if the caller wanted us to freeze the clock, restore the |
| | 292 | * elapsed game real time to what it was when we started, so |
| | 293 | * that the time the player took to acknowledge the MORE prompt |
| | 294 | * won't count against the elapsed game time; otherwise, process |
| | 295 | * any real-time events that came due while we were waiting |
| | 296 | */ |
| | 297 | if (freezeRealTime) |
| | 298 | { |
| | 299 | /* time was frozen - restore the original elapsed time */ |
| | 300 | realTimeManager.setElapsedTime(t0); |
| | 301 | } |
| | 302 | else |
| | 303 | { |
| | 304 | /* |
| | 305 | * time wasn't frozen - check for any events that have come |
| | 306 | * due since we started waiting, and process them |
| | 307 | * immediately |
| | 308 | */ |
| | 309 | processRealTimeEvents(true); |
| | 310 | } |
| | 311 | } |
| | 312 | |
| | 313 | /* |
| | 314 | * Read a keystroke, processing real-time events while waiting, if |
| | 315 | * desired. 'allowRealTime' and 'promptFunc' work the same way they |
| | 316 | * do with getInputLine(). |
| | 317 | */ |
| | 318 | getKey(allowRealTime, promptFunc) |
| | 319 | { |
| | 320 | local evt; |
| | 321 | |
| | 322 | /* get an event */ |
| | 323 | evt = getEventOrKey(allowRealTime, promptFunc, true); |
| | 324 | |
| | 325 | /* |
| | 326 | * the only event that getEventOrKey will return is a keystroke, |
| | 327 | * so return the keystroke from the event record |
| | 328 | */ |
| | 329 | return evt[2]; |
| | 330 | } |
| | 331 | |
| | 332 | /* |
| | 333 | * Read an event, processing real-time events while waiting, if |
| | 334 | * desired. 'allowRealTime' and 'promptFunc' work the same way they |
| | 335 | * do with getInputLine(). |
| | 336 | */ |
| | 337 | getEvent(allowRealTime, promptFunc) |
| | 338 | { |
| | 339 | /* read and return an event */ |
| | 340 | return getEventOrKey(allowRealTime, promptFunc, nil); |
| | 341 | } |
| | 342 | |
| | 343 | /* |
| | 344 | * Read an event or keystroke. 'allowRealTime' and 'promptFunc' work |
| | 345 | * the same way they do in getInputLine(). If 'keyOnly' is true, |
| | 346 | * then we're only interested in keystroke events, and we'll ignore |
| | 347 | * any other events entered. |
| | 348 | * |
| | 349 | * Note that this routine is not generally called directly; callers |
| | 350 | * should usually call the convenience routines getKey() or |
| | 351 | * getEvent(), as needed. |
| | 352 | */ |
| | 353 | getEventOrKey(allowRealTime, promptFunc, keyOnly) |
| | 354 | { |
| | 355 | /* make sure the command transcript is flushed */ |
| | 356 | if (gTranscript != nil) |
| | 357 | gTranscript.flushForInput(); |
| | 358 | |
| | 359 | /* |
| | 360 | * Cancel any in-progress input. If there's an in-progress |
| | 361 | * input, a real-time event must be interrupting the input, |
| | 362 | * which is recursively invoking us to start a new input. |
| | 363 | */ |
| | 364 | cancelInputInProgress(true); |
| | 365 | |
| | 366 | /* keep going until we get a keystroke or other event */ |
| | 367 | for (;;) |
| | 368 | { |
| | 369 | local result; |
| | 370 | local timeout; |
| | 371 | local t0; |
| | 372 | |
| | 373 | /* note the starting time, in case we want to freeze the clock */ |
| | 374 | t0 = realTimeManager.getElapsedTime(); |
| | 375 | |
| | 376 | /* process real-time events, if possible */ |
| | 377 | timeout = processRealTimeEvents(allowRealTime); |
| | 378 | |
| | 379 | /* show the prompt and any pre-input codes */ |
| | 380 | inputEventBegin(promptFunc); |
| | 381 | |
| | 382 | getInput: |
| | 383 | /* |
| | 384 | * Read the input. (Note that if our timeout is nil, this |
| | 385 | * will simply act like the ordinary untimed inputLine.) |
| | 386 | */ |
| | 387 | result = inputEvent(timeout); |
| | 388 | |
| | 389 | /* |
| | 390 | * If we're not allowing real-time event processing, freeze |
| | 391 | * the clock during the read - set the elapsed game |
| | 392 | * real-time clock back to the value it had on entry, so |
| | 393 | * that the input effectively consumes no real time. |
| | 394 | */ |
| | 395 | if (!allowRealTime) |
| | 396 | realTimeManager.setElapsedTime(t0); |
| | 397 | |
| | 398 | /* check the event code from the result list */ |
| | 399 | switch(result[1]) |
| | 400 | { |
| | 401 | case InEvtNoTimeout: |
| | 402 | /* |
| | 403 | * the platform doesn't support timeouts - note it for |
| | 404 | * future reference so that we don't ask for input with |
| | 405 | * timeout again, then go back to try the input again |
| | 406 | * without a timeout |
| | 407 | */ |
| | 408 | noInputTimeout = true; |
| | 409 | timeout = nil; |
| | 410 | goto getInput; |
| | 411 | |
| | 412 | case InEvtTimeout: |
| | 413 | /* |
| | 414 | * We got a timeout without finishing the input line. |
| | 415 | * This means that we have reached the time when the |
| | 416 | * next real-time event is ready to execute. Simply |
| | 417 | * continue looping; we'll process all real-time events |
| | 418 | * that are ready to go, then we'll resume reading the |
| | 419 | * command. |
| | 420 | */ |
| | 421 | break; |
| | 422 | |
| | 423 | case InEvtEof: |
| | 424 | /* |
| | 425 | * End of file - this indicates that the user has closed |
| | 426 | * down the application, or that the keyboard has become |
| | 427 | * unreadable due to a hardware or OS error. |
| | 428 | * |
| | 429 | * Write a blank line to the display in an attempt to |
| | 430 | * flush any partially-entered command line text, then |
| | 431 | * throw an error to signal the EOF condition. |
| | 432 | */ |
| | 433 | "\b"; |
| | 434 | throw new EndOfFileException(); |
| | 435 | |
| | 436 | case InEvtKey: |
| | 437 | /* keystroke - finish the input and return the event */ |
| | 438 | inputEventEnd(); |
| | 439 | return result; |
| | 440 | |
| | 441 | case InEvtHref: |
| | 442 | /* |
| | 443 | * Hyperlink activation - if we're allowed to return |
| | 444 | * events other than keystrokes, finish the input and |
| | 445 | * return the event; otherwise, ignore the event and keep |
| | 446 | * looping. |
| | 447 | */ |
| | 448 | if (!keyOnly) |
| | 449 | { |
| | 450 | inputEventEnd(); |
| | 451 | return result; |
| | 452 | } |
| | 453 | break; |
| | 454 | |
| | 455 | default: |
| | 456 | /* ignore other events */ |
| | 457 | break; |
| | 458 | } |
| | 459 | } |
| | 460 | } |
| | 461 | |
| | 462 | /* |
| | 463 | * Cancel input in progress. |
| | 464 | * |
| | 465 | * If 'reset' is true, we'll clear any input state saved from the |
| | 466 | * interrupted in-progress editing session; otherwise, we'll retain |
| | 467 | * the saved editing state for restoration on the next input. |
| | 468 | * |
| | 469 | * This MUST be called before calling tadsSay(). Games should |
| | 470 | * generally never call tadsSay() directly (call the library |
| | 471 | * function say() instead), so in most cases authors will not need |
| | 472 | * to worry about calling this on output. |
| | 473 | * |
| | 474 | * This MUST ALSO be called before performing any keyboard input. |
| | 475 | * Callers using inputManager methods for keyboard operations won't |
| | 476 | * have to worry about this, because the inputManager methods call |
| | 477 | * this routine when necessary. |
| | 478 | */ |
| | 479 | cancelInputInProgress(reset) |
| | 480 | { |
| | 481 | /* cancel the interpreter's internal input state */ |
| | 482 | inputLineCancel(reset); |
| | 483 | |
| | 484 | /* if we were editing a command line, terminate the editing session */ |
| | 485 | if (inputLineInProgress) |
| | 486 | { |
| | 487 | /* do our normal after-input work */ |
| | 488 | inputLineEnd(); |
| | 489 | } |
| | 490 | |
| | 491 | /* if we were waiting for event input, note that we are no longer */ |
| | 492 | if (inputEventInProgress) |
| | 493 | { |
| | 494 | /* do our normal after-input work */ |
| | 495 | inputEventEnd(); |
| | 496 | } |
| | 497 | } |
| | 498 | |
| | 499 | /* |
| | 500 | * Process any real-time events that are ready to run, and return the |
| | 501 | * timeout until the next real-time event. |
| | 502 | * |
| | 503 | * If allowRealTime is nil, we won't process real-time events at all; |
| | 504 | * we'll merely return nil for the timeout to indicate to the caller |
| | 505 | * that any user input interaction about to be attempted should wait |
| | 506 | * indefinitely. |
| | 507 | */ |
| | 508 | processRealTimeEvents(allowRealTime) |
| | 509 | { |
| | 510 | local timeout; |
| | 511 | |
| | 512 | /* presume we will not use a timeout */ |
| | 513 | timeout = nil; |
| | 514 | |
| | 515 | /* process real-time events, if allowed */ |
| | 516 | if (allowRealTime) |
| | 517 | { |
| | 518 | local tNext; |
| | 519 | |
| | 520 | /* |
| | 521 | * Process any real-time events that are currently ready to |
| | 522 | * execute, and note the amount of time until the next |
| | 523 | * real-time event is ready. |
| | 524 | */ |
| | 525 | tNext = realTimeManager.executeEvents(); |
| | 526 | |
| | 527 | /* |
| | 528 | * If there's an event pending, note the interval between the |
| | 529 | * current time and the event's scheduled time - this will |
| | 530 | * give us the maximum amount of time we want to wait for the |
| | 531 | * user to edit the command line before interrupting to |
| | 532 | * execute the pending event. Ignore this if the platform |
| | 533 | * doesn't support timeouts to begin with. |
| | 534 | */ |
| | 535 | if (tNext != nil && !noInputTimeout) |
| | 536 | timeout = tNext - realTimeManager.getElapsedTime(); |
| | 537 | } |
| | 538 | |
| | 539 | /* return the timeout until the next real-time event */ |
| | 540 | return timeout; |
| | 541 | } |
| | 542 | |
| | 543 | /* |
| | 544 | * Begin reading key/event input. We'll cancel any report gatherer |
| | 545 | * so that prompt text shows immediately, and show the prompt if |
| | 546 | * desired. |
| | 547 | */ |
| | 548 | inputEventBegin(promptFunc) |
| | 549 | { |
| | 550 | /* if we're not continuing previous input, show the prompt */ |
| | 551 | if (!inputEventInProgress) |
| | 552 | { |
| | 553 | inputBegin(promptFunc); |
| | 554 | |
| | 555 | /* note that we're in input mode */ |
| | 556 | inputEventInProgress = true; |
| | 557 | } |
| | 558 | } |
| | 559 | |
| | 560 | /* |
| | 561 | * End keystroke/event input. |
| | 562 | */ |
| | 563 | inputEventEnd() |
| | 564 | { |
| | 565 | /* if input is in progress, terminate it */ |
| | 566 | if (inputEventInProgress) |
| | 567 | { |
| | 568 | /* note that we're no longer reading an event */ |
| | 569 | inputEventInProgress = nil; |
| | 570 | } |
| | 571 | } |
| | 572 | |
| | 573 | /* |
| | 574 | * Begin command line editing. If we're in HTML mode, we'll show |
| | 575 | * the appropriate codes to establish the input font. |
| | 576 | */ |
| | 577 | inputLineBegin(defObj) |
| | 578 | { |
| | 579 | /* notify the command sequencer that we're reading a command */ |
| | 580 | "<.commandbefore>"; |
| | 581 | |
| | 582 | /* if we're not resuming a session, set up a new session */ |
| | 583 | if (!inputLineInProgress) |
| | 584 | { |
| | 585 | /* begin input */ |
| | 586 | inputBegin(defObj.promptFunc); |
| | 587 | |
| | 588 | /* switch to input font */ |
| | 589 | defObj.beginInputFont(); |
| | 590 | |
| | 591 | /* note that we're in input mode */ |
| | 592 | inputLineInProgress = true; |
| | 593 | |
| | 594 | /* remember the parameter object for this input */ |
| | 595 | inProgressDefObj = defObj; |
| | 596 | } |
| | 597 | } |
| | 598 | |
| | 599 | /* |
| | 600 | * End command line editing. If we're in HTML mode, we'll show the |
| | 601 | * appropriate codes to close the input font. |
| | 602 | */ |
| | 603 | inputLineEnd() |
| | 604 | { |
| | 605 | /* if input is in progress, terminate it */ |
| | 606 | if (inputLineInProgress) |
| | 607 | { |
| | 608 | /* note that we're no longer reading a line of input */ |
| | 609 | inputLineInProgress = nil; |
| | 610 | |
| | 611 | /* end input font mode */ |
| | 612 | inProgressDefObj.endInputFont(); |
| | 613 | |
| | 614 | /* notify the command sequencer that we're done reading */ |
| | 615 | "<.commandafter>"; |
| | 616 | |
| | 617 | /* |
| | 618 | * tell the main text area's output stream that we just |
| | 619 | * ended an input line |
| | 620 | */ |
| | 621 | mainOutputStream.inputLineEnd(); |
| | 622 | |
| | 623 | /* forget the parameter object for the input */ |
| | 624 | inProgressDefObj = nil; |
| | 625 | } |
| | 626 | } |
| | 627 | |
| | 628 | /* |
| | 629 | * Begin generic input. Cancels command report list capture, and |
| | 630 | * shows the prompt if given. |
| | 631 | */ |
| | 632 | inputBegin(promptFunc) |
| | 633 | { |
| | 634 | /* |
| | 635 | * Turn off command transcript capture, if it's active. Once |
| | 636 | * we're soliciting input interactively, we can no longer |
| | 637 | * usefully capture the text output of commands, but this is fine |
| | 638 | * because we must be doing something for which capture isn't |
| | 639 | * important anyway. Reporting capture is used for things like |
| | 640 | * selecting the kind of result to show, which clearly isn't a |
| | 641 | * factor for actions involving interactive input. |
| | 642 | */ |
| | 643 | if (gTranscript != nil) |
| | 644 | gTranscript.flushForInput(); |
| | 645 | |
| | 646 | /* if we have a prompt, display it */ |
| | 647 | if (promptFunc != nil) |
| | 648 | (promptFunc)(); |
| | 649 | } |
| | 650 | |
| | 651 | /* receive post-restore notification */ |
| | 652 | execute() |
| | 653 | { |
| | 654 | /* |
| | 655 | * Reset the inputLine state. If we had any previously |
| | 656 | * interrupted input from the current interpreter session, |
| | 657 | * forget it by cancelling and resetting the input line. If we |
| | 658 | * had an interrupted line in the session being restored, forget |
| | 659 | * about that, too. |
| | 660 | */ |
| | 661 | inputLineCancel(true); |
| | 662 | inputLineInProgress = nil; |
| | 663 | inputEventInProgress = nil; |
| | 664 | |
| | 665 | /* |
| | 666 | * Clear the inputLineTimeout disabling flag - we might be |
| | 667 | * restoring the game on a different platform from the one where |
| | 668 | * the game started, so we might be able to use timed command |
| | 669 | * line input even if we didn't when we started the game. By |
| | 670 | * clearing this flag, we'll check again to see if we can |
| | 671 | * perform timed input; if we can't, we'll just set the flag |
| | 672 | * again, so there will be no harm done. |
| | 673 | */ |
| | 674 | noInputTimeout = nil; |
| | 675 | } |
| | 676 | |
| | 677 | /* |
| | 678 | * Flag: command line input is in progress. If this is set, it means |
| | 679 | * that we interrupted command-line editing by a timeout, so we |
| | 680 | * should not show a prompt the next time we go back to the keyboard |
| | 681 | * for input. |
| | 682 | */ |
| | 683 | inputLineInProgress = nil |
| | 684 | |
| | 685 | /* the InputDef object for the input in progress */ |
| | 686 | inProgressDefObj = nil |
| | 687 | |
| | 688 | /* flag: keystroke/event input is in progress */ |
| | 689 | inputEventInProgress = nil |
| | 690 | |
| | 691 | /* |
| | 692 | * Flag: inputLine does not support timeouts on the current platform. |
| | 693 | * We set this when we get an InEvtNoTimeout return code from |
| | 694 | * inputLineTimeout, so that we'll know not to try calling again with |
| | 695 | * a timeout. This applies to the current interpreter only, so we |
| | 696 | * must ignore any value restored from a previously saved game, since |
| | 697 | * the game might have been saved on a different platform. |
| | 698 | * |
| | 699 | * Note that if this value is nil, it means only that we've never |
| | 700 | * seen an InEvtNoTimeout return code from inputLineEvent - it does |
| | 701 | * NOT mean that timeouts are supported locally. |
| | 702 | * |
| | 703 | * We assume that the input functions are uniform in their treatment |
| | 704 | * of timeouts; that is, we assume that if inputLineTimeout supports |
| | 705 | * timeout, then so does inputEvent, and that if one doesn't support |
| | 706 | * timeout, the other won't either. |
| | 707 | */ |
| | 708 | noInputTimeout = nil |
| | 709 | ; |
| | 710 | |
| | 711 | |
| | 712 | /* ------------------------------------------------------------------------ */ |
| | 713 | /* |
| | 714 | * Read a command line from the player. Displays the main command |
| | 715 | * prompt and returns a line of input. |
| | 716 | * |
| | 717 | * We process any pending real-time events before reading the command. |
| | 718 | * If the local platform supports real-time command-line interruptions, |
| | 719 | * we'll continue processing real-time events as they occur in the |
| | 720 | * course of command editing. |
| | 721 | */ |
| | 722 | readMainCommand(which) |
| | 723 | { |
| | 724 | local str; |
| | 725 | |
| | 726 | /* execute any pre-command-prompt daemons */ |
| | 727 | eventManager.executePrompt(); |
| | 728 | |
| | 729 | /* |
| | 730 | * Read a line of input, allowing real-time event processing, and |
| | 731 | * return the line of text we read. Use the appropriate main |
| | 732 | * command prompt for the given prompt mode. |
| | 733 | */ |
| | 734 | str = inputManager.getInputLine( |
| | 735 | true, {: gLibMessages.mainCommandPrompt(which) }); |
| | 736 | |
| | 737 | /* return the string we read */ |
| | 738 | return str; |
| | 739 | } |
| | 740 | |
| | 741 | |
| | 742 | /* ------------------------------------------------------------------------ */ |
| | 743 | /* |
| | 744 | * End-of-file exception - this is thrown when readMainCommand() |
| | 745 | * encounters end of file reading the console input. |
| | 746 | */ |
| | 747 | class EndOfFileException: Exception |
| | 748 | ; |
| | 749 | |
| | 750 | |
| | 751 | /* ------------------------------------------------------------------------ */ |
| | 752 | /* |
| | 753 | * 'Quitting' exception. This isn't an error - it merely indicates that |
| | 754 | * the user has explicitly asked to quit the game. |
| | 755 | */ |
| | 756 | class QuittingException: Exception |
| | 757 | ; |
| | 758 | |
| | 759 | /* ------------------------------------------------------------------------ */ |
| | 760 | /* |
| | 761 | * Base class for command input string preparsers. |
| | 762 | * |
| | 763 | * Preparsers must be registered in order to run. During |
| | 764 | * preinitialization, we will automatically register any existing |
| | 765 | * preparser objects; preparsers that are created dynamically during |
| | 766 | * execution must be registered explicitly, which can be accomplished by |
| | 767 | * inheriting the default constructor from this class. |
| | 768 | */ |
| | 769 | class StringPreParser: PreinitObject |
| | 770 | /* |
| | 771 | * My execution order number. When multiple preparsers are |
| | 772 | * registered, we'll run the preparsers in ascending order of this |
| | 773 | * value (i.e., smallest runOrder goes first). |
| | 774 | */ |
| | 775 | runOrder = 100 |
| | 776 | |
| | 777 | /* |
| | 778 | * Do our parsing. Each instance should override this method to |
| | 779 | * define the parsing that it does. |
| | 780 | * |
| | 781 | * 'str' is the string to parse, and 'which' is the rmcXxx enum |
| | 782 | * giving the type of command we're working with. |
| | 783 | * |
| | 784 | * This method returns a string or nil. If the method returns a |
| | 785 | * string, the caller will forget the original string and work from |
| | 786 | * here on out with the new version returned; this allows the method |
| | 787 | * to rewrite the original input as desired. If the method returns |
| | 788 | * nil, it means that the string has been fully handled and that |
| | 789 | * further parsing of the same string is not desired. |
| | 790 | */ |
| | 791 | doParsing(str, which) |
| | 792 | { |
| | 793 | /* return the original string unchanged */ |
| | 794 | return str; |
| | 795 | } |
| | 796 | |
| | 797 | /* |
| | 798 | * construction - when we dynamically create a preparser, register |
| | 799 | * it by default |
| | 800 | */ |
| | 801 | construct() |
| | 802 | { |
| | 803 | /* register the preparser */ |
| | 804 | StringPreParser.registerPreParser(self); |
| | 805 | } |
| | 806 | |
| | 807 | /* run pre-initialization */ |
| | 808 | execute() |
| | 809 | { |
| | 810 | /* register the preparser if it's not already registered */ |
| | 811 | StringPreParser.registerPreParser(self); |
| | 812 | } |
| | 813 | |
| | 814 | /* register a preparser */ |
| | 815 | registerPreParser(pp) |
| | 816 | { |
| | 817 | /* if the preparser isn't already in our list, add it */ |
| | 818 | if (regList.indexOf(pp) == nil) |
| | 819 | { |
| | 820 | /* append this new item to the list */ |
| | 821 | regList.append(pp); |
| | 822 | |
| | 823 | /* the list is no longer sorted */ |
| | 824 | regListSorted = nil; |
| | 825 | } |
| | 826 | } |
| | 827 | |
| | 828 | /* |
| | 829 | * Class method - Run all preparsers. Returns the result of |
| | 830 | * successively calling each preparser on the given string. |
| | 831 | */ |
| | 832 | runAll(str, which) |
| | 833 | { |
| | 834 | /* |
| | 835 | * if the list of preparsers isn't sorted, sort it in ascending |
| | 836 | * order of execution order number |
| | 837 | */ |
| | 838 | if (!regListSorted) |
| | 839 | { |
| | 840 | /* sort the list */ |
| | 841 | regList.sort(SortAsc, {x, y: x.runOrder - y.runOrder}); |
| | 842 | |
| | 843 | /* the list is now sorted */ |
| | 844 | regListSorted = true; |
| | 845 | } |
| | 846 | |
| | 847 | /* run each preparser */ |
| | 848 | foreach (local cur in regList) |
| | 849 | { |
| | 850 | /* run this preparser */ |
| | 851 | str = cur.doParsing(str, which); |
| | 852 | |
| | 853 | /* |
| | 854 | * if the result is nil, it means that the string has been |
| | 855 | * fully handled, so we need not run any further preparsing |
| | 856 | */ |
| | 857 | if (str == nil) |
| | 858 | return nil; |
| | 859 | } |
| | 860 | |
| | 861 | /* return the result of the series of preparsing steps */ |
| | 862 | return str; |
| | 863 | } |
| | 864 | |
| | 865 | /* class property containing the list of registered parsers */ |
| | 866 | regList = static new Vector(10) |
| | 867 | |
| | 868 | /* class property - the registration list has been sorted */ |
| | 869 | regListSorted = nil |
| | 870 | ; |
| | 871 | |
| | 872 | /* ------------------------------------------------------------------------ */ |
| | 873 | /* |
| | 874 | * The "comment" pre-parser. If the command line starts with a special |
| | 875 | * prefix string (by default, "*", but this can be changed via our |
| | 876 | * commentPrefix property), this pre-parser intercepts the command, |
| | 877 | * treating it as a comment from the player and otherwise ignoring the |
| | 878 | * entire input line. The main purpose is to give players a way to put |
| | 879 | * comments into recorded transcripts, as notes to themselves when later |
| | 880 | * reviewing the transcripts or as notes to the author when submitting |
| | 881 | * play-testing feedback. |
| | 882 | */ |
| | 883 | commentPreParser: StringPreParser |
| | 884 | doParsing(str, which) |
| | 885 | { |
| | 886 | /* get the amount of leading whitespace, so we can ignore it */ |
| | 887 | local sp = rexMatch(leadPat, str); |
| | 888 | |
| | 889 | /* |
| | 890 | * if the command line starts with the comment prefix, treat it |
| | 891 | * as a comment |
| | 892 | */ |
| | 893 | if (str.substr(sp + 1, commentPrefix.length()) == commentPrefix) |
| | 894 | { |
| | 895 | /* |
| | 896 | * It's a comment. |
| | 897 | * |
| | 898 | * If a transcript is being recorded, simply acknowledge the |
| | 899 | * comment; if not, acknowledge it, but with a warning that |
| | 900 | * the comment isn't being saved anywhere |
| | 901 | */ |
| | 902 | if (scriptStatus.scriptFile != nil) |
| | 903 | gLibMessages.noteWithScript; |
| | 904 | else if (warningCount++ == 0) |
| | 905 | gLibMessages.noteWithoutScriptWarning; |
| | 906 | else |
| | 907 | gLibMessages.noteWithoutScript; |
| | 908 | |
| | 909 | /* |
| | 910 | * Otherwise completely ignore the command line. To do this, |
| | 911 | * simply return nil: this tells the parser that the command |
| | 912 | * has been fully handled by the preparser. |
| | 913 | */ |
| | 914 | return nil; |
| | 915 | } |
| | 916 | else |
| | 917 | { |
| | 918 | /* it's not a command - return the string unchanged */ |
| | 919 | return str; |
| | 920 | } |
| | 921 | } |
| | 922 | |
| | 923 | /* |
| | 924 | * The comment prefix. You can change this to any character, or to |
| | 925 | * any sequence of characters (longer sequences, such as '//', will |
| | 926 | * work fine). If a command line starts with this exact string (or |
| | 927 | * starts with whitespace followed by this string), we'll consider |
| | 928 | * the line to be a comment. |
| | 929 | */ |
| | 930 | commentPrefix = '*' |
| | 931 | |
| | 932 | /* |
| | 933 | * The leading-whitespace pattern. We skip any text that matches |
| | 934 | * this pattern at the start of a command line before looking for the |
| | 935 | * comment prefix. |
| | 936 | * |
| | 937 | * If you don't want to allow leading whitespace before the comment |
| | 938 | * prefix, you can simply change this to '' - a pattern consisting of |
| | 939 | * an empty string always matches zero characters, so it will prevent |
| | 940 | * us from skipping any leading charactres in the player's input. |
| | 941 | */ |
| | 942 | leadPat = static new RexPattern('<space>*') |
| | 943 | |
| | 944 | /* warning count for entering comments without SCRIPT in effect */ |
| | 945 | warningCount = 0 |
| | 946 | |
| | 947 | /* |
| | 948 | * Use a lower execution order than the default, so that we run |
| | 949 | * before most other pre-parsers. Most other pre-parsers are written |
| | 950 | * to handle actual commands, so it's usually just a waste of time to |
| | 951 | * have them look at comments at all - and can occasionally be |
| | 952 | * problematic, since the free-form text of a comment could confuse a |
| | 953 | * pre-parser that's expecting a more conventional command format. |
| | 954 | * When the comment pre-parser detects a comment, it halts any |
| | 955 | * further processing of the command - so by running ahead of other |
| | 956 | * pre-parsers, we'll effectively bypass other pre-parsers when we |
| | 957 | * detect a comment. |
| | 958 | */ |
| | 959 | runOrder = 50 |
| | 960 | ; |
| | 961 | |
| | 962 | |
| | 963 | /* ------------------------------------------------------------------------ */ |
| | 964 | /* |
| | 965 | * Read a line of text and return the token list and the original text. |
| | 966 | * We keep going until a non-empty line of text is read. |
| | 967 | * |
| | 968 | * 'which' is one of the rmcXxx enum values specifying what kind of |
| | 969 | * command line we're reading. |
| | 970 | * |
| | 971 | * The return value is a list of two elements. The first element is the |
| | 972 | * string entered, and the second element is the token list. |
| | 973 | */ |
| | 974 | readMainCommandTokens(which) |
| | 975 | { |
| | 976 | local str; |
| | 977 | local toks; |
| | 978 | |
| | 979 | /* keep going until we get a non-empty command line */ |
| | 980 | for (;;) |
| | 981 | { |
| | 982 | /* read a command line */ |
| | 983 | str = readMainCommand(which); |
| | 984 | |
| | 985 | /* run any preparsing desired on the string */ |
| | 986 | str = StringPreParser.runAll(str, which); |
| | 987 | |
| | 988 | /* |
| | 989 | * if preparsing returned nil, it means that the preparser fully |
| | 990 | * handled the string - simply return nil to tell the caller |
| | 991 | * that its work is done |
| | 992 | */ |
| | 993 | if (str == nil) |
| | 994 | return nil; |
| | 995 | |
| | 996 | try |
| | 997 | { |
| | 998 | /* tokenize the command string */ |
| | 999 | toks = cmdTokenizer.tokenize(str); |
| | 1000 | } |
| | 1001 | catch (TokErrorNoMatch tokExc) |
| | 1002 | { |
| | 1003 | /* |
| | 1004 | * Invalid tokens in the response - complain about it. Flag |
| | 1005 | * the error as being in the first character of the |
| | 1006 | * remaining string, since that's the character for which we |
| | 1007 | * could find no match. |
| | 1008 | */ |
| | 1009 | gLibMessages.invalidCommandToken(tokExc.curChar_.htmlify()); |
| | 1010 | |
| | 1011 | /* go back for another input line */ |
| | 1012 | continue; |
| | 1013 | } |
| | 1014 | |
| | 1015 | /* if we got a non-empty token list, return it */ |
| | 1016 | if (toks.length() != 0) |
| | 1017 | return [str, toks]; |
| | 1018 | |
| | 1019 | /* show the empty-command reply */ |
| | 1020 | gLibMessages.emptyCommandResponse(); |
| | 1021 | } |
| | 1022 | } |
| | 1023 | |
| | 1024 | |