| | 1 | /* FrobTADS main entrypoint. |
| | 2 | * |
| | 3 | * We don't use the default command line parsing that TADS provides |
| | 4 | * because a) it's clumsy, b) we need our own set of options (colors, |
| | 5 | * scrolling, etc) and c) Unix users expect certain standards |
| | 6 | * (--version, --help, bug-report email address, long vs. short options, |
| | 7 | * etc). |
| | 8 | */ |
| | 9 | #include "common.h" |
| | 10 | |
| | 11 | #include <stdio.h> |
| | 12 | #include <string.h> |
| | 13 | #ifdef HAVE_LOCALE_H |
| | 14 | #include <locale.h> |
| | 15 | #endif |
| | 16 | |
| | 17 | #include "frobtadsappcurses.h" |
| | 18 | #include "frobtadsappplain.h" |
| | 19 | #include "colors.h" |
| | 20 | #include "options.h" |
| | 21 | |
| | 22 | #include "os.h" |
| | 23 | #include "trd.h" |
| | 24 | #include "t3std.h" |
| | 25 | #include "vmmain.h" |
| | 26 | #include "vmvsn.h" |
| | 27 | #include "vmmaincn.h" |
| | 28 | #include "vmhostsi.h" |
| | 29 | extern "C" |
| | 30 | { |
| | 31 | #include "osgen.h" |
| | 32 | } |
| | 33 | |
| | 34 | |
| | 35 | const char versionOutput[] = |
| | 36 | "FrobTADS " PACKAGE_VERSION "\n" |
| | 37 | "TADS 2 virtual machive v" TADS_RUNTIME_VERSION "\n" |
| | 38 | "TADS 3 virtual machine v" T3VM_VSN_STRING " (" T3VM_IDENTIFICATION ")\n" |
| | 39 | "Copyright (C) 2005 Nikos Chantziaras.\n" |
| | 40 | "TADS copyright (C) 2005 Michael J. Roberts.\n" |
| | 41 | "FrobTADS comes with NO WARRANTY, to the extent permitted by law.\n" |
| | 42 | "You may redistribute copies of FrobTADS under certain terms and conditions.\n" |
| | 43 | "See the file named COPYING for more information."; |
| | 44 | |
| | 45 | |
| | 46 | const char helpOutput[] = |
| | 47 | "Options:\n" |
| | 48 | " -h, --help This help text\n" |
| | 49 | " -v, --version Version information\n" |
| | 50 | " -n, --no-colors Disable colors\n" |
| | 51 | " -f, --force-colors Try to enable colors even if the system claims that\n" |
| | 52 | " colors aren't available\n" |
| | 53 | " -o, --no-defcolors Don't use the terminal's default colors\n" |
| | 54 | " -t, --tcolor Text color (default white)\n" |
| | 55 | " -b, --bcolor Background color (default black)\n" |
| | 56 | " -l, --stat-tcolor Statusline text color (default is -b)\n" |
| | 57 | " -g, --stat-bcolor Statusline background color (default is -t)\n" |
| | 58 | " -c, --no-scrolling Disable soft (line by line) scrolling\n" |
| | 59 | " -p, --no-pause Don't pause prior to quiting\n" |
| | 60 | " -d, --no-chdir Don't change the current directory\n" |
| | 61 | " -s, --safety-level File I/O safety level (default is 2)\n" |
| | 62 | " -e, --scroll-buffer Size of the scroll-back buffer. Must be between 8 and\n" |
| | 63 | " 8192kB (default is 512kB)\n" |
| | 64 | " -r, --restore Load a saved game position\n" |
| | 65 | " -u, --t3undo Multiply the availabe T3VM undo buffer by n. Must be\n" |
| | 66 | " between 1 and 64 (default is 16; about 100 UNDOs)\n" |
| | 67 | " -k, --character-set Use given charset as the keyboard and display\n" |
| | 68 | " character set.\n" |
| | 69 | " -i, --interface Use given screen interface (curses or plain). Default\n" |
| | 70 | " is curses. (Advanced features like statusline, banners\n" |
| | 71 | " and colors are not available when using the plain\n" |
| | 72 | " interface.)\n" |
| | 73 | "Color codes:\n" |
| | 74 | " 0:black 1:red 2:green 3:yellow 4:blue 5:magenta 6:cyan 7:white\n" |
| | 75 | "(Note that yellow is actually brown on some hardware, mostly PCs.)\n" |
| | 76 | "\n" |
| | 77 | "File I/O safety levels:\n" |
| | 78 | " 0: Safety mechanism disabled (unlimited read/write access)\n" |
| | 79 | " 1: Read everywhere, write in current directory only\n" |
| | 80 | " 2: Read/write in current directory only\n" |
| | 81 | " 3: Read in current directory only, no writing allowed\n" |
| | 82 | " 4: File I/O disabled (no reading or writing allowed)\n" |
| | 83 | "\n" |
| | 84 | "Short options are case-sensitive, long options are not. Options may be given\n" |
| | 85 | "in any order and may be specified both before and after the filename. If an\n" |
| | 86 | "option that takes an argument is given multiple times the last value is used.\n" |
| | 87 | "Options may be abbreviated: --no-scr will be recognized as --no-scrolling.\n" |
| | 88 | "\n" |
| | 89 | "Report bugs to <" PACKAGE_BUGREPORT ">.\n" |
| | 90 | "Please include the output of the --version option with the report."; |
| | 91 | |
| | 92 | |
| | 93 | int main( int argc, char** argv ) |
| | 94 | { |
| | 95 | // These are the command-line options we recognize. See |
| | 96 | // options.h for details on the format. We keep this list |
| | 97 | // sorted alphabetically, in order to easily detect duplicate |
| | 98 | // short options. |
| | 99 | const char* optv[] = { |
| | 100 | "b:bcolor <0..7>", |
| | 101 | "c|no-scrolling", |
| | 102 | "d|no-chdir", |
| | 103 | "e:scroll-buffer <8..8192>", |
| | 104 | "f|force-colors", |
| | 105 | "g:stat-bcolor <0..7>", |
| | 106 | "h|help", |
| | 107 | "i:interface <curses|plain>", |
| | 108 | "k:character-set <charset>", |
| | 109 | "l:stat-tcolor <0..7>", |
| | 110 | "n|no-colors", |
| | 111 | "o|no-defcolors", |
| | 112 | "p|no-pause", |
| | 113 | "r:restore <filename>", |
| | 114 | "s:safety-level <0..4>", |
| | 115 | "t:tcolor <0..7>", |
| | 116 | "u:undo-size <1..64>", |
| | 117 | "v|version", |
| | 118 | 0 |
| | 119 | }; |
| | 120 | |
| | 121 | #ifdef HAVE_SETLOCALE |
| | 122 | // First, initialise locale, if available. We need only |
| | 123 | // LC_CTYPE. Don't try to mess with numeric formats! |
| | 124 | setlocale(LC_CTYPE, ""); |
| | 125 | #endif |
| | 126 | |
| | 127 | // These are used for actual command-line parsing. |
| | 128 | int optChar; |
| | 129 | const char* optArg; |
| | 130 | Options opts(argv[0], optv); |
| | 131 | OptArgvIter iter(argc - 1, &argv[1]); |
| | 132 | |
| | 133 | // Allow the filename to be specified between options. |
| | 134 | // TODO: Maybe we shouldn't do this. We could ignore any |
| | 135 | // options that come after the filename and just pass them to |
| | 136 | // the main() function of the game (TADS 3). But this would |
| | 137 | // make no sense for TADS 2. Another solution is to pass only |
| | 138 | // the explicitly ignored options to TADS 3 (that means, any |
| | 139 | // options specified after a "-- " in the command-line. |
| | 140 | opts.ctrls(Options::PARSE_POS); |
| | 141 | |
| | 142 | // We set this to true if an invalid option was given (unknown |
| | 143 | // or ambigous), so that we print an error message only the |
| | 144 | // first time an error occurs. |
| | 145 | bool optionError = false; |
| | 146 | |
| | 147 | // A semi-functional, portable ostream as defined in options.h. |
| | 148 | ostream cerr(stderr); |
| | 149 | ostream cout(stdout); |
| | 150 | |
| | 151 | // Available screen interfaces. |
| | 152 | enum screenInterface {cursesInterface, plainInterface}; |
| | 153 | // We will use curses by default. |
| | 154 | screenInterface interface = cursesInterface; |
| | 155 | |
| | 156 | // Increase the T3VM undo-size 16 times by default. |
| | 157 | frobVmUndoMaxRecords = defaultVmUndoMaxRecords * 16; |
| | 158 | FrobTadsApplication::FrobOptions frobOpts = { |
| | 159 | // We assume some defaults. They might change while |
| | 160 | // parsing the command line. |
| | 161 | true, // Use colors. |
| | 162 | false, // Don't force colors. |
| | 163 | true, // Use terminal's defaults for color pair 0. |
| | 164 | true, // Enable soft-scrolling. |
| | 165 | true, // Pause prior to exit. |
| | 166 | true, // Change to the game's directory. |
| | 167 | FROB_WHITE, // Text. |
| | 168 | FROB_BLACK, // Background. |
| | 169 | -1, // Statusline text; none yet. |
| | 170 | -1, // Statusline background; none yet. |
| | 171 | 512*1024, // Scroll-back buffer size. |
| | 172 | // Default file I/O safety level is read/write access |
| | 173 | // in current directory only. |
| | 174 | VM_IO_SAFETY_READWRITE_CUR, |
| | 175 | // TODO: Revert the default back to "\0" when Unicode output |
| | 176 | // is finally implemented. |
| | 177 | "us-ascii" // Character set. |
| | 178 | }; |
| | 179 | |
| | 180 | // Name of the game to run. |
| | 181 | const char* filename = 0; |
| | 182 | |
| | 183 | // Saved game position to restore (optional). |
| | 184 | const char* savedPosFilename = 0; |
| | 185 | |
| | 186 | // Start parsing. Stop when there are no more options to parse. |
| | 187 | while ((optChar = opts(iter, optArg)) != Options::ENDOPTS) switch (optChar) { |
| | 188 | // --version |
| | 189 | case 'v': |
| | 190 | cout << versionOutput << "\n\n"; |
| | 191 | return 0; |
| | 192 | |
| | 193 | // --help |
| | 194 | case 'h': |
| | 195 | cout << "Usage: " << opts.name() << " [options] file\n"; |
| | 196 | cout << helpOutput << "\n\n"; |
| | 197 | return 0; |
| | 198 | break; |
| | 199 | |
| | 200 | // --no-colors |
| | 201 | case 'n': |
| | 202 | frobOpts.useColors = false; |
| | 203 | break; |
| | 204 | |
| | 205 | // --force-colors |
| | 206 | case 'f': |
| | 207 | frobOpts.forceColors = true; |
| | 208 | break; |
| | 209 | |
| | 210 | // --no-defcolors |
| | 211 | case 'o': |
| | 212 | frobOpts.defColors = false; |
| | 213 | break; |
| | 214 | |
| | 215 | // --no-scrolling |
| | 216 | case 'c': |
| | 217 | frobOpts.softScroll = false; |
| | 218 | break; |
| | 219 | |
| | 220 | // --no-pause |
| | 221 | case 'p': |
| | 222 | frobOpts.exitPause = false; |
| | 223 | break; |
| | 224 | |
| | 225 | // --no-chdir |
| | 226 | case 'd': |
| | 227 | frobOpts.changeDir = false; |
| | 228 | break; |
| | 229 | |
| | 230 | case 't': // --tcolor |
| | 231 | case 'b': // --bcolor |
| | 232 | case 'l': // --stat-tcolor |
| | 233 | case 'g': // --stat-bcolor |
| | 234 | { |
| | 235 | if (optionError) break; |
| | 236 | // We'll convert the string argument to a number. |
| | 237 | int tmp; |
| | 238 | if (optArg == 0) { |
| | 239 | // Argument is missing. |
| | 240 | optionError = true; |
| | 241 | break; |
| | 242 | } |
| | 243 | // Convert string to number. |
| | 244 | if (sscanf(optArg, "%d", &tmp) == 0) { |
| | 245 | // Conversion failed; it was not a number. |
| | 246 | cerr << opts.name() << ": colors must be numerical.\n"; |
| | 247 | optionError = true; |
| | 248 | break; |
| | 249 | } |
| | 250 | if (tmp < 0 or tmp > 7) { |
| | 251 | // Invalid color. |
| | 252 | cerr << opts.name() << ": a color must be between 0 and 7.\n"; |
| | 253 | optionError = true; |
| | 254 | break; |
| | 255 | } |
| | 256 | switch (optChar) { |
| | 257 | case 't': frobOpts.textColor = tmp; break; |
| | 258 | case 'b': frobOpts.bgColor = tmp; break; |
| | 259 | case 'l': frobOpts.statTextColor = tmp; break; |
| | 260 | case 'g': frobOpts.statBgColor = tmp; break; |
| | 261 | } |
| | 262 | break; |
| | 263 | } |
| | 264 | |
| | 265 | // --safety-level |
| | 266 | case 's': { |
| | 267 | if (optionError) break; |
| | 268 | int tmp; |
| | 269 | if (optArg == 0) { |
| | 270 | // Argument is missing. |
| | 271 | optionError = true; |
| | 272 | break; |
| | 273 | } |
| | 274 | if (sscanf(optArg, "%d", &tmp) == 0) { |
| | 275 | // The argument was not a number. |
| | 276 | cerr << opts.name() << ": safety level must be numerical.\n"; |
| | 277 | optionError = true; |
| | 278 | break; |
| | 279 | } |
| | 280 | if (tmp < 0 or tmp > 4) { |
| | 281 | // Out of range. |
| | 282 | cerr << opts.name() << ": safety level must be between 0-4.\n"; |
| | 283 | optionError = true; |
| | 284 | break; |
| | 285 | } |
| | 286 | frobOpts.safetyLevel = tmp; |
| | 287 | break; |
| | 288 | } |
| | 289 | |
| | 290 | // --scroll-buffer |
| | 291 | case 'e': { |
| | 292 | if (optionError) break; |
| | 293 | unsigned int tmp; |
| | 294 | if (optArg == 0) { |
| | 295 | // Argument is missing. |
| | 296 | optionError = true; |
| | 297 | break; |
| | 298 | } |
| | 299 | if (sscanf(optArg, "%u", &tmp) == 0) { |
| | 300 | // The argument was not a number. |
| | 301 | cerr << opts.name() << ": buffer size must be numerical.\n"; |
| | 302 | optionError = true; |
| | 303 | break; |
| | 304 | } |
| | 305 | if (tmp < 8 or tmp > 8192) { |
| | 306 | // Buffer is out of range. |
| | 307 | cerr << opts.name() << ": buffer size must be between 8-8192 kB.\n"; |
| | 308 | optionError = true; |
| | 309 | break; |
| | 310 | } |
| | 311 | // Adjust from kB to bytes. |
| | 312 | tmp *= 1024; |
| | 313 | frobOpts.scrollBufSize = tmp; |
| | 314 | break; |
| | 315 | } |
| | 316 | |
| | 317 | // --restore |
| | 318 | case 'r': { |
| | 319 | if (optionError) break; |
| | 320 | if (optArg == 0) { |
| | 321 | // Argument is missing. |
| | 322 | optionError = true; |
| | 323 | break; |
| | 324 | } |
| | 325 | savedPosFilename = optArg; |
| | 326 | break; |
| | 327 | } |
| | 328 | |
| | 329 | // --undo-size |
| | 330 | case 'u': { |
| | 331 | if (optionError) break; |
| | 332 | int tmp; |
| | 333 | if (optArg == 0) { |
| | 334 | // Argument is missing. |
| | 335 | optionError = true; |
| | 336 | break; |
| | 337 | } |
| | 338 | if (sscanf(optArg, "%d", &tmp) == 0) { |
| | 339 | // The argument was not a number. |
| | 340 | cerr << opts.name() << ": undo multiplicator must be numerical.\n"; |
| | 341 | optionError = true; |
| | 342 | break; |
| | 343 | } |
| | 344 | if (tmp < 1 or tmp > 64) { |
| | 345 | // Out of range. |
| | 346 | cerr << opts.name() << ": undo multiplicator must be between 1-64.\n"; |
| | 347 | optionError = true; |
| | 348 | break; |
| | 349 | } |
| | 350 | frobVmUndoMaxRecords = defaultVmUndoMaxRecords * tmp; |
| | 351 | break; |
| | 352 | } |
| | 353 | |
| | 354 | // --character-set |
| | 355 | case 'k': |
| | 356 | if (optionError) break; |
| | 357 | if (optArg == 0) { |
| | 358 | // Argument is missing. |
| | 359 | optionError = true; |
| | 360 | break; |
| | 361 | } |
| | 362 | strncpy(frobOpts.characterSet, optArg, 16); |
| | 363 | frobOpts.characterSet[15] = '\0'; |
| | 364 | break; |
| | 365 | |
| | 366 | // --interface |
| | 367 | case 'i': |
| | 368 | if (optionError) break; |
| | 369 | if (optArg == 0) { |
| | 370 | optionError = true; |
| | 371 | break; |
| | 372 | } |
| | 373 | if (strcmp(optArg, "curses") == 0) { |
| | 374 | interface = cursesInterface; |
| | 375 | } else if (strcmp(optArg, "plain") == 0) { |
| | 376 | interface = plainInterface; |
| | 377 | } else { |
| | 378 | cerr << opts.name() << ": available interfaces: curses, plain.\n"; |
| | 379 | optionError = true; |
| | 380 | } |
| | 381 | break; |
| | 382 | |
| | 383 | // This occurs when the argument is something other than an |
| | 384 | // option. We treat it as the filename of the game to run. |
| | 385 | case Options::POSITIONAL: |
| | 386 | if (optionError) break; |
| | 387 | if (filename != 0) { |
| | 388 | // User already specified a filename. |
| | 389 | cerr << opts.name() << ": more than one filename given.\n"; |
| | 390 | optionError = true; |
| | 391 | break; |
| | 392 | } |
| | 393 | filename = optArg; |
| | 394 | break; |
| | 395 | |
| | 396 | // Common errors below. |
| | 397 | case Options::BADCHAR: |
| | 398 | case Options::BADKWD: |
| | 399 | optionError = true; |
| | 400 | break; |
| | 401 | |
| | 402 | case Options::AMBIGUOUS: |
| | 403 | optionError = true; |
| | 404 | break; |
| | 405 | } |
| | 406 | |
| | 407 | if (filename == 0 and not optionError) { |
| | 408 | if (savedPosFilename != 0) { |
| | 409 | // No filename given, but a saved game position |
| | 410 | // was specified. Ask TADS to find out the |
| | 411 | // filename of the game that the savefile is |
| | 412 | // associated with. We make the buffer static |
| | 413 | // since we need to store a pointer to it even |
| | 414 | // after exiting this if-block. |
| | 415 | static char detectedFilename[OSFNMAX + 1]; |
| | 416 | const char* const argvDummy[] = {"frob", "-r", savedPosFilename}; |
| | 417 | if (vm_get_game_arg(3, argvDummy, detectedFilename, OSFNMAX + 1)) { |
| | 418 | // Success. Store the filename. |
| | 419 | filename = detectedFilename; |
| | 420 | } else { |
| | 421 | // Failed. |
| | 422 | optionError = true; |
| | 423 | } |
| | 424 | } else { |
| | 425 | optionError = true; |
| | 426 | } |
| | 427 | if (optionError) cerr << opts.name() << ": no filename given.\n"; |
| | 428 | } |
| | 429 | |
| | 430 | if (optionError) { |
| | 431 | opts.usage(cerr, "filename[.gam|.t3]"); |
| | 432 | return 1; |
| | 433 | } |
| | 434 | |
| | 435 | // Set up the statusline colors (if the user didn't change the |
| | 436 | // defaults). We'll simply use the normal colors but with |
| | 437 | // foreground/background reversed. |
| | 438 | if (frobOpts.statTextColor == -1) frobOpts.statTextColor = frobOpts.bgColor; |
| | 439 | if (frobOpts.statBgColor == -1) frobOpts.statBgColor = frobOpts.textColor; |
| | 440 | |
| | 441 | // If the filename the user specified lacks an extension, try |
| | 442 | // these. |
| | 443 | const char* defExts[] = {"gam", "t3"}; |
| | 444 | // The filename TADS detects (probably after trying the above |
| | 445 | // extensions). |
| | 446 | char actualFilename[OSFNMAX + 1]; |
| | 447 | |
| | 448 | // Ask TADS to find out what the specified file is supposed to |
| | 449 | // be. |
| | 450 | switch (vm_get_game_type(filename, actualFilename, OSFNMAX, defExts, 2)) { |
| | 451 | case VM_GGT_TADS2: |
| | 452 | // It's a Tads 2 game. Fire-up the T2VM. |
| | 453 | switch (interface) { |
| | 454 | case cursesInterface: return FrobTadsApplicationCurses(frobOpts).runTads(actualFilename, 0); |
| | 455 | case plainInterface: return FrobTadsApplicationPlain(frobOpts).runTads(actualFilename, 0); |
| | 456 | } |
| | 457 | |
| | 458 | case VM_GGT_TADS3: { |
| | 459 | // It's Tads 3. |
| | 460 | int t3vmRet; |
| | 461 | switch (interface) { |
| | 462 | case cursesInterface: |
| | 463 | t3vmRet = FrobTadsApplicationCurses(frobOpts).runTads(actualFilename, 1); |
| | 464 | break; |
| | 465 | case plainInterface: |
| | 466 | t3vmRet = FrobTadsApplicationPlain(frobOpts).runTads(actualFilename, 1); |
| | 467 | break; |
| | 468 | } |
| | 469 | // Show any unfreed memory blocks. This does nothing if |
| | 470 | // Tads 3 has not been compiled with debugging support. |
| | 471 | t3_list_memory_blocks(0); |
| | 472 | return t3vmRet; |
| | 473 | } |
| | 474 | |
| | 475 | case VM_GGT_INVALID: |
| | 476 | // It's not a Tads game. |
| | 477 | cerr << opts.name() << ": " << filename << ": not a Tads game.\n"; |
| | 478 | break; |
| | 479 | |
| | 480 | case VM_GGT_NOT_FOUND: |
| | 481 | // No such file. |
| | 482 | cerr << opts.name() << ": " << filename << ": file not found.\n"; |
| | 483 | break; |
| | 484 | |
| | 485 | case VM_GGT_AMBIG: |
| | 486 | // Filename is ambiguous. |
| | 487 | cerr << opts.name() << ": " << filename |
| | 488 | << ": ambiguous filename; please include the file suffix.\n"; |
| | 489 | break; |
| | 490 | } |
| | 491 | // If we reached this point, an error occured. |
| | 492 | return 1; |
| | 493 | } |