1 #ifdef RCSID
2 static char RCSid[] =
3 "$Header$";
4 #endif
5
6 /*
7 * Copyright (c) 1999, 2002 Michael J. Roberts. All Rights Reserved.
8 *
9 * Please see the accompanying license file, LICENSE.TXT, for information
10 * on using and copying this software.
11 */
12 /*
13 Name
14 vmmain.cpp - T3 VM main entrypoint - execute an image file
15 Function
16 Main entrypoint for executing a T3 image file. Loads an image file
17 and begins execution. Returns when execution terminates.
18 Notes
19
20 Modified
21 10/07/99 MJRoberts - Creation
22 */
23
24 #include <stdlib.h>
25 #include <string.h>
26 #include <stdio.h>
27
28 #include "os.h"
29 #include "t3std.h"
30 #include "vmerr.h"
31 #include "vmfile.h"
32 #include "vmimage.h"
33 #include "vmrun.h"
34 #include "vmimgrb.h"
35 #include "vmmain.h"
36 #include "vmhost.h"
37 #include "vmhostsi.h"
38 #include "vminit.h"
39 #include "vmpredef.h"
40 #include "vmobj.h"
41 #include "vmvsn.h"
42 #include "charmap.h"
43 #include "vmsave.h"
44 #include "vmtype.h"
45 #include "vmrunsym.h"
46
47
48 /* ------------------------------------------------------------------------ */
49 /*
50 * Execute an image file. If an exception occurs, we'll display a
51 * message on the console, and we'll return the error code; we'll return
52 * zero on success. If an error occurs, we'll fill in 'errbuf' with a
53 * message describing the problem.
54 */
vm_run_image(CVmMainClientIfc * clientifc,const char * image_file_name,class CVmHostIfc * hostifc,const char * const * prog_argv,int prog_argc,const char * script_file,const char * log_file,const char * cmd_log_file,int load_from_exe,int show_banner,const char * charset,const char * saved_state,const char * res_dir)55 int vm_run_image(CVmMainClientIfc *clientifc,
56 const char *image_file_name,
57 class CVmHostIfc *hostifc,
58 const char *const *prog_argv, int prog_argc,
59 const char *script_file, const char *log_file,
60 const char *cmd_log_file,
61 int load_from_exe, int show_banner, const char *charset,
62 const char *saved_state, const char *res_dir)
63 {
64 CVmFile *fp = 0;
65 CVmImageLoader *volatile loader = 0;
66 CVmImageFile *volatile imagefp = 0;
67 unsigned long image_file_base = 0;
68 int retval;
69 vm_globals *vmg__;
70
71 /* presume we will return success */
72 retval = 0;
73
74 /* create the file object */
75 fp = new CVmFile();
76
77 /* initialize the VM */
78 vm_initialize(&vmg__, hostifc, clientifc, charset);
79
80 /* tell the client system to initialize */
81 clientifc->client_init(VMGLOB_ADDR, script_file, log_file,
82 cmd_log_file,
83 show_banner ? T3VM_BANNER_STRING : 0);
84
85 /* catch any errors that occur during loading and running */
86 err_try
87 {
88 if (load_from_exe)
89 {
90 osfildef *exe_fp;
91
92 /* find the image within the executable */
93 exe_fp = os_exeseek(image_file_name, "TGAM");
94 if (exe_fp == 0)
95 err_throw(VMERR_NO_IMAGE_IN_EXE);
96
97 /*
98 * set up to read from the executable at the location of the
99 * embedded image file that we just found
100 */
101 image_file_base = osfpos(exe_fp);
102 fp->set_file(exe_fp, image_file_base);
103 }
104 else
105 {
106 /* reading from a normal file - open the file */
107 fp->open_read(image_file_name, OSFTT3IMG);
108 }
109
110 /* create the loader */
111 imagefp = new CVmImageFileExt(fp);
112 loader = new CVmImageLoader(imagefp, image_file_name,
113 image_file_base);
114
115 /* load the image */
116 loader->load(vmg0_);
117
118 /* if we have a resource root path, tell the host interface */
119 if (res_dir != 0)
120 hostifc->set_res_dir(res_dir);
121
122 /* let the client prepare for execution */
123 clientifc->pre_exec(VMGLOB_ADDR);
124
125 /* run the program from the main entrypoint */
126 loader->run(vmg_ prog_argv, prog_argc, 0, saved_state);
127
128 /* tell the client we're done with execution */
129 clientifc->post_exec(VMGLOB_ADDR);
130 }
131 err_catch(exc)
132 {
133 char errbuf[512];
134
135 /* tell the client execution failed due to an error */
136 clientifc->post_exec_err(VMGLOB_ADDR);
137
138 /* note the error code for returning to the caller */
139 retval = exc->get_error_code();
140
141 /* get the message for the error */
142 CVmRun::get_exc_message(vmg_ exc, errbuf, sizeof(errbuf), TRUE);
143
144 /* display the message */
145 clientifc->display_error(VMGLOB_ADDR, errbuf, FALSE);
146 }
147 err_end;
148
149 /* unload the image */
150 if (loader != 0)
151 loader->unload(vmg0_);
152
153 /* delete the loader and the image file object */
154 if (loader != 0)
155 delete loader;
156 if (imagefp != 0)
157 delete imagefp;
158
159 /* notify the client */
160 clientifc->client_terminate(VMGLOB_ADDR);
161
162 /* terminate the VM */
163 vm_terminate(vmg__, clientifc);
164
165 /* delete the file */
166 if (fp != 0)
167 delete fp;
168
169 /* return the status code */
170 return retval;
171 }
172
173 /* ------------------------------------------------------------------------ */
174 /*
175 * Read an option argument from an option that allows its arguments to be
176 * either appended directly to the end of the option (as in "-otest") or
177 * to follow as the subsequent vector item (as in "-o test"). optlen
178 * gives the length of the option prefix itself, including the hyphen
179 * prefix (so an option "-o" would have length 2). We'll increment
180 * *curarg if we consume the extra vector position. Returns null if there
181 * isn't an argument for the option.
182 */
get_opt_arg(int argc,char ** argv,int * curarg,int optlen)183 static char *get_opt_arg(int argc, char **argv, int *curarg, int optlen)
184 {
185 /*
186 * if we have an argument (i.e., any non-empty text) appended directly
187 * to the option vector item, we don't need to consume an extra vector
188 * position
189 */
190 if (argv[*curarg][optlen] != '\0')
191 {
192 /* the argument is merely the remainder of this vector item */
193 return &argv[*curarg][optlen];
194 }
195
196 /* advance to the next vector position */
197 ++(*curarg);
198
199 /* if we don't have any vector items left, there's no argument */
200 if (*curarg >= argc)
201 return 0;
202
203 /* this vector item is the next argument */
204 return argv[*curarg];
205 }
206
207 /* ------------------------------------------------------------------------ */
208 /*
209 * Main Entrypoint for command-line invocations. For simplicity, a
210 * normal C main() or equivalent entrypoint can invoke this routine
211 * directly, using the usual argc/argv conventions.
212 *
213 * Returns a status code suitable for use with exit(): OSEXSUCC if we
214 * successfully loaded and ran an executable, OSEXFAIL on failure. If
215 * an error occurs, we'll fill in 'errbuf' with a message describing the
216 * problem.
217 */
vm_run_image_main(CVmMainClientIfc * clientifc,const char * executable_name,int argc,char ** argv,int defext,int test_mode,CVmHostIfc * hostifc)218 int vm_run_image_main(CVmMainClientIfc *clientifc,
219 const char *executable_name,
220 int argc, char **argv, int defext, int test_mode,
221 CVmHostIfc *hostifc)
222 {
223 int curarg;
224 char image_file_name[OSFNMAX];
225 int stat;
226 const char *script_file;
227 const char *log_file;
228 const char *cmd_log_file;
229 const char *res_dir;
230 int load_from_exe;
231 int show_banner;
232 int found_image;
233 int hide_usage;
234 int usage_err;
235 const char *charset;
236 char *saved_state;
237
238 /* we haven't found an image file yet */
239 found_image = FALSE;
240
241 /* presume we'll show usage on error */
242 hide_usage = FALSE;
243
244 /* presume there will be no usage error */
245 usage_err = FALSE;
246
247 /* presume we won't have any console input/output files */
248 script_file = 0;
249 log_file = 0;
250 cmd_log_file = 0;
251
252 /* presume we'll use the default OS character set */
253 charset = 0;
254
255 /* presume we won't show the banner */
256 show_banner = FALSE;
257
258 /* presume we won't load from the .exe file */
259 load_from_exe = FALSE;
260
261 /* presume we won't restore a saved state file */
262 saved_state = 0;
263
264 /* presume we won't have a resource directory specified */
265 res_dir = 0;
266
267 /* scan options */
268 for (curarg = 1 ; curarg < argc && argv[curarg][0] == '-' ; ++curarg)
269 {
270 /* check the argument */
271 switch(argv[curarg][1])
272 {
273 case 'b':
274 if (strcmp(argv[curarg], "-banner") == 0)
275 {
276 /* make a note to show the banner */
277 show_banner = TRUE;
278 }
279 else
280 goto opt_error;
281 break;
282
283 case 'c':
284 if (argv[curarg][2] == 's' && argv[curarg][3] == '\0')
285 {
286 ++curarg;
287 if (curarg < argc)
288 charset = argv[curarg];
289 else
290 goto opt_error;
291 }
292 else
293 goto opt_error;
294 break;
295
296 case 'n':
297 if (strcmp(argv[curarg], "-nobanner") == 0)
298 {
299 /* make a note not to show the banner */
300 show_banner = FALSE;
301 }
302 else
303 goto opt_error;
304 break;
305
306 case 's':
307 /* file safety level - check the range */
308 if (argv[curarg][2] < '0' || argv[curarg][2] > '4'
309 || argv[curarg][3] != '\0')
310 {
311 /* invalid level */
312 goto opt_error;
313 }
314 else
315 {
316 /* set the level in the host application */
317 hostifc->set_io_safety(argv[curarg][2] - '0');
318 }
319 break;
320
321 case 'i':
322 /*
323 * read from a script file - the next argument, or the
324 * remainder of this argument, is the filename
325 */
326 script_file = get_opt_arg(argc, argv, &curarg, 2);
327 if (script_file == 0)
328 goto opt_error;
329 break;
330
331 case 'l':
332 /* log output to file */
333 log_file = get_opt_arg(argc, argv, &curarg, 2);
334 if (log_file == 0)
335 goto opt_error;
336 break;
337
338 case 'o':
339 /* log commands to file */
340 cmd_log_file = get_opt_arg(argc, argv, &curarg, 2);
341 if (cmd_log_file == 0)
342 goto opt_error;
343 break;
344
345 case 'p':
346 /* check what follows */
347 if (strcmp(argv[curarg], "-plain") == 0)
348 {
349 /* tell the client to set plain ASCII mode */
350 clientifc->set_plain_mode();
351 break;
352 }
353 else
354 goto opt_error;
355 break;
356
357 case 'r':
358 /* get the name of the saved state file to restore */
359 saved_state = get_opt_arg(argc, argv, &curarg, 2);
360 if (saved_state == 0)
361 goto opt_error;
362 break;
363
364 case 'R':
365 /* note the resource root directory */
366 res_dir = get_opt_arg(argc, argv, &curarg, 2);
367 if (res_dir == 0)
368 goto opt_error;
369 break;
370
371 default:
372 opt_error:
373 /* discard remaining arguments */
374 curarg = argc;
375
376 /* note the error */
377 usage_err = TRUE;
378 break;
379 }
380 }
381
382 /*
383 * If there was no usage error so far, but we don't have an image
384 * filename argument, try to find the image file some other way.
385 */
386 if (usage_err)
387 {
388 /* there was a usage error - don't bother looking for an image file */
389 }
390 else if (curarg + 1 <= argc)
391 {
392 /* the last argument is the image file name */
393 strcpy(image_file_name, argv[curarg]);
394 found_image = TRUE;
395
396 /*
397 * If the given filename exists, use it as-is; otherwise, if
398 * we're allowed to add an extension, try applying a default
399 * extension of "t3" (formerly "t3x") to the given name.
400 */
401 if (defext && osfacc(image_file_name))
402 {
403 /* the given name doesn't exist - try a default extension */
404 os_defext(image_file_name, "t3"); /* formerly "t3x" */
405 }
406 }
407 else
408 {
409 osfildef *fp;
410
411 /* look for an image file attached to the executable */
412 fp = os_exeseek(argv[0], "TGAM");
413 if (fp != 0)
414 {
415 /* close the file */
416 osfcls(fp);
417
418 /* note that we want to load from the executable */
419 load_from_exe = TRUE;
420
421 /* use the executable filename as the image file */
422 if (os_get_exe_filename(image_file_name,
423 sizeof(image_file_name), argv[0]))
424 found_image = TRUE;
425 }
426
427 /*
428 * if we still haven't found an image file, try to get the image
429 * file from the saved state file, if one was specified
430 */
431 if (!found_image && saved_state != 0)
432 {
433 osfildef *save_fp;
434
435 /* open the saved state file */
436 save_fp = osfoprb(saved_state, OSFTT3SAV);
437 if (save_fp != 0)
438 {
439 /* get the name of the image file */
440 if (CVmSaveFile::restore_get_image(
441 save_fp, image_file_name, sizeof(image_file_name)) == 0)
442 {
443 /* we successfully obtained the filename */
444 found_image = TRUE;
445 }
446
447 /* close the file */
448 osfcls(save_fp);
449 }
450 }
451
452 /*
453 * if we haven't found the image, and the host system provides a
454 * way of asking the user for a filename, try that
455 */
456 if (!found_image)
457 {
458 /* ask the host system for a game name */
459 switch (hostifc->get_image_name(image_file_name,
460 sizeof(image_file_name)))
461 {
462 case VMHOST_GIN_IGNORED:
463 /* no effect - we have no new information */
464 break;
465
466 case VMHOST_GIN_CANCEL:
467 /*
468 * the user cancelled the dialog - we don't have a
469 * filename, but we also don't want to show usage, since
470 * the user chose not to proceed
471 */
472 hide_usage = TRUE;
473 break;
474
475 case VMHOST_GIN_ERROR:
476 /*
477 * an error occurred showing the dialog - there's not
478 * much we can do except show the usage message
479 */
480 break;
481
482 case VMHOST_GIN_SUCCESS:
483 /* that was successful - we have an image file now */
484 found_image = TRUE;
485 break;
486 }
487 }
488 }
489
490 /*
491 * if we don't have an image file name by this point, we can't
492 * proceed - show the usage message and terminate
493 */
494 if (usage_err || !found_image)
495 {
496 char buf[OSFNMAX + 1024];
497
498 /* show the usage message if allowed */
499 if (!hide_usage)
500 {
501 /* build the usage message */
502 sprintf(buf,
503 "%s\n"
504 "usage: %s [options] <image-file-name> [arguments]\n"
505 "options:\n"
506 " -banner - show the version/copyright banner\n"
507 " -cs xxx - use character set 'xxx' for keyboard "
508 "and display\n"
509 " -i file - read command line input from file\n"
510 " -l file - log all console input/output to file\n"
511 " -o file - log console input to file\n"
512 " -plain - run in plain mode (no cursor positioning, "
513 "colors, etc.)\n"
514 " -r file - restore saved state from file\n"
515 " -R dir - set directory for external resources\n"
516 " -s# - set I/O safety level (# in range 0 to 4 - 0 "
517 "is the least\n"
518 " restrictive, 4 allows no file I/O at all)\n"
519 "\n"
520 "If provided, the optional arguments after the image "
521 "file name are passed\n"
522 "to the program's main entrypoint.\n",
523 T3VM_BANNER_STRING, executable_name);
524
525 /* display the message */
526 clientifc->display_error(0, buf, FALSE);
527 }
528
529 /* return failure */
530 return OSEXFAIL;
531 }
532
533 /*
534 * if we're in test mode, replace the first argument to the program
535 * with its root name, so that we don't include any path information
536 * in the argument list
537 */
538 if (test_mode && curarg <= argc && argv[curarg] != 0)
539 argv[curarg] = os_get_root_name(argv[curarg]);
540
541 /* run the program */
542 stat = vm_run_image(clientifc, image_file_name, hostifc,
543 argv + curarg, argc - curarg,
544 script_file, log_file, cmd_log_file,
545 load_from_exe, show_banner, charset,
546 saved_state, res_dir);
547
548 /* return the status code */
549 return stat;
550 }
551
552 /* ------------------------------------------------------------------------ */
553 /*
554 * Copy a filename with a given size limit. This works essentially like
555 * strncpy(), but we guarantee that the result is null-terminated even if
556 * it's truncated.
557 */
strcpy_limit(char * dst,const char * src,size_t limit)558 static void strcpy_limit(char *dst, const char *src, size_t limit)
559 {
560 size_t copy_len;
561
562 /* get the length of the source string */
563 copy_len = strlen(src);
564
565 /*
566 * if it exceeds the available space, leaving space for the null
567 * terminator, limit the copy to the available space
568 */
569 if (copy_len > limit - 1)
570 copy_len = limit - 1;
571
572 /* copy as much as we can */
573 memcpy(dst, src, copy_len);
574
575 /* null-terminate what we managed to copy */
576 dst[copy_len] = '\0';
577 }
578
579 /* ------------------------------------------------------------------------ */
580 /*
581 * Given a saved game file, try to identify the game file that created the
582 * saved game.
583 */
vm_get_game_file_from_savefile(const char * savefile,char * fname,size_t fnamelen)584 static int vm_get_game_file_from_savefile(const char *savefile,
585 char *fname, size_t fnamelen)
586 {
587 osfildef *fp;
588 char buf[128];
589 int ret;
590 size_t len;
591
592 /* open the saved game file */
593 fp = osfoprb(savefile, OSFTBIN);
594
595 /* if that failed, there's no way to read the game file name */
596 if (fp == 0)
597 return FALSE;
598
599 /* read the first few bytes */
600 if (osfrb(fp, buf, 16))
601 {
602 /*
603 * we couldn't even read that much, so it must not really be a
604 * saved game file
605 */
606 ret = FALSE;
607 }
608 else
609 {
610 /* check for a saved game signature we recognize */
611 if (memcmp(buf, "TADS2 save/g\012\015\032\000", 16) == 0)
612 {
613 /*
614 * It's a TADS 2 saved game with embedded .GAM file
615 * information. The filename immediately follows the signature
616 * (the 15 bytes we just matched), with a two-byte length
617 * prefix. Seek to the length prefix and read it.
618 */
619 osfseek(fp, 16, OSFSK_SET);
620 osfrb(fp, buf, 2);
621 len = osrp2(buf);
622
623 /* limit the read length to our caller's available buffer */
624 if (len > fnamelen - 1)
625 len = fnamelen - 1;
626
627 /* read the filename and null-terminate it */
628 osfrb(fp, fname, len);
629 fname[len] = '\0';
630
631 /* success */
632 ret = TRUE;
633 }
634 else if (memcmp(buf, "T3-state-v", 10) == 0)
635 {
636 /*
637 * It's a T3 saved state file. The image filename is always
638 * embedded in this type of file, so seek back to the start of
639 * the file and read the filename.
640 *
641 * Note that restore_get_image() returns zero on success, so we
642 * want to return true if and only if that routine returns
643 * zero.
644 */
645 osfseek(fp, 0, OSFSK_SET);
646 ret = (CVmSaveFile::restore_get_image(fp, fname, fnamelen) == 0);
647 }
648 else
649 {
650 /*
651 * it's not a signature we know, so it must not be a saved
652 * state file (at least not one we can deal with)
653 */
654 ret = FALSE;
655 }
656 }
657
658 /* we're done with the file now, so close it */
659 osfcls(fp);
660
661 /* return the result */
662 return ret;
663 }
664
665
666 /* ------------------------------------------------------------------------ */
667 /*
668 * Parse a command line to determine the name of the game file specified
669 * by the arguments. We'll return the index of the argv element that
670 * specifies the name of the game, or a negative number if there is no
671 * filename argument.
672 *
673 * Note that our parsing will work for TADS 2 or TADS 3 interpreter
674 * command lines, so this routine can be used to extract the filename from
675 * an ambiguous command line in order to check the file for its type and
676 * thereby resolve which interpreter to use.
677 */
vm_get_game_arg(int argc,const char * const * argv,char * buf,size_t buflen)678 int vm_get_game_arg(int argc, const char *const *argv,
679 char *buf, size_t buflen)
680 {
681 int i;
682 const char *restore_file;
683
684 /* presume we won't find a file to restore */
685 restore_file = 0;
686
687 /*
688 * Scan the arguments for the union of the TADS 2 and TADS 3 command
689 * line options. Start at the second element of the argument vector,
690 * since the first element is the executable name. Keep going until
691 * we run out of options, each of which must start with a '-'.
692 *
693 * Note that we don't care about the meanings of the options - we
694 * simply want to skip past them. This is more complicated than
695 * merely looking for the first argument without a '-' prefix, because
696 * some of the options allow arguments, and an option argument can
697 * sometimes - depending on the argument - take the next position in
698 * the argument vector after the option itself. So, we must determine
699 * the meaning of each option well enough that we can tell whether or
700 * not the next argv element after the option is part of the option.
701 */
702 for (i = 1 ; i < argc && argv[i][0] == '-' ; ++i)
703 {
704 /*
705 * check the first character after the hyphen to determine which
706 * option we have
707 */
708 switch(argv[i][1])
709 {
710 case 'b':
711 /*
712 * tads 3 "-banner" (no arguments, so just consume it and
713 * continue)
714 */
715 break;
716
717 case 'c':
718 /* tads 3 "-cs charset"; tads 2 "-ctab-" or "-ctab tab" */
719 if (strcmp(argv[i], "-ctab") == 0
720 || strcmp(argv[i], "-cs") == 0)
721 {
722 /* there's another argument giving the filename */
723 ++i;
724 }
725 break;
726
727 case 'd':
728 /* tads 2 "-double[+-]" (no arguments) */
729 break;
730
731 case 'm':
732 /* tads 2 "-msSize", "-mhSize", "-mSize" */
733 switch(argv[i][2])
734 {
735 case 's':
736 case 'h':
737 /*
738 * argument required - if nothing follows the second
739 * letter, consume the next vector item as the option
740 * argument
741 */
742 if (argv[i][3] == '\0')
743 ++i;
744 break;
745
746 case '\0':
747 /*
748 * argument required, but nothing follows the "-m" -
749 * consume the next vector item
750 */
751 ++i;
752 break;
753
754 default:
755 /* argument required and present */
756 break;
757 }
758 break;
759
760 case 't':
761 /* tads 2 "-tfFile", "-tsSize", "-tp[+-]", "-t[+0]" */
762 switch(argv[i][2])
763 {
764 case 'f':
765 case 's':
766 /*
767 * argument required - consume the next vector item if
768 * nothing is attached to this item
769 */
770 if (argv[i][3] == '\0')
771 ++i;
772 break;
773
774 case 'p':
775 /* no arguments */
776 break;
777
778 default:
779 /* no arguments */
780 break;
781 }
782 break;
783
784 case 'u':
785 /*
786 * tads 2 "-uSize" - argument required, so consume the next
787 * vector item if necessary
788 */
789 if (argv[i][2] == '\0')
790 ++i;
791 break;
792
793 case 'n':
794 /* tads 3 "-nobanner" - no arguments */
795 break;
796
797 case 's':
798 /*
799 * tads 2/3 "-s#" (#=0,1,2,3,4); in tads 2, the # can be
800 * separated by a space from the -s, so we'll allow it this
801 * way in general
802 */
803 if (argv[i][2] == '\0')
804 ++i;
805 break;
806
807 case 'i':
808 /* tads 2/3 "-iFile" - consume an argument */
809 if (argv[i][2] == '\0')
810 ++i;
811 break;
812
813 case 'l':
814 /* tads 2/3 "-lFile" - consume an argument */
815 if (argv[i][2] == '\0')
816 ++i;
817 break;
818
819 case 'o':
820 /* tads 2/3 "-oFile" - consume an argument */
821 if (argv[i][2] == '\0')
822 ++i;
823 break;
824
825 case 'p':
826 /* tads 2/3 "-plain"; tads 2 "-p[+-]" - no arguments */
827 break;
828
829 case 'R':
830 /* tads 3 '-Rdir' - consume an argument */
831 if (argv[i][2] == '\0')
832 ++i;
833 break;
834
835 case 'r':
836 /* tads 2/3 "-rFile" - consume an argument */
837 if (argv[i][2] != '\0')
838 {
839 /* the file to be restored is appended to the "-r" */
840 restore_file = argv[i] + 2;
841 }
842 else
843 {
844 /* the file to be restored is the next argument */
845 ++i;
846 restore_file = argv[i];
847 }
848 break;
849 }
850 }
851
852 /*
853 * We have no more options, so the next argument is the game filename.
854 * If we're out of argv elements, then no game filename was directly
855 * specified, in which case we'll try looking for a file spec in the
856 * restore file, if present.
857 */
858 if (i < argc)
859 {
860 /* there's a game file argument - copy it to the caller's buffer */
861 strcpy_limit(buf, argv[i], buflen);
862
863 /* return success */
864 return TRUE;
865 }
866 else if (restore_file != 0)
867 {
868 /*
869 * There's no game file argument, but there is a restore file
870 * argument. Try identifying the game file from the original game
871 * file specification stored in the saved state file.
872 */
873 return vm_get_game_file_from_savefile(restore_file, buf, buflen);
874 }
875 else
876 {
877 /*
878 * there's no game file or restore file argument, so they've given
879 * us nothing to go on - there's no game file
880 */
881 return FALSE;
882 }
883 }
884
885 /* ------------------------------------------------------------------------ */
886 /*
887 * Given the name of a file, determine the engine (TADS 2 or TADS 3) for
888 * which the file was compiled, based on the file's signature. Returns
889 * one of the VM_GGT_xxx codes.
890 */
vm_get_game_type_for_file(const char * filename)891 static int vm_get_game_type_for_file(const char *filename)
892 {
893 osfildef *fp;
894 char buf[16];
895 int ret;
896
897 /* try opening the filename exactly as given */
898 fp = osfoprb(filename, OSFTBIN);
899
900 /* if the file doesn't exist, tell the caller */
901 if (fp == 0)
902 return VM_GGT_NOT_FOUND;
903
904 /* read the first few bytes of the file, where the signature resides */
905 if (osfrb(fp, buf, 16))
906 {
907 /*
908 * error reading the file - any valid game file is going to be at
909 * least long enough to hold the number of bytes we asked for, so
910 * it must not be a valid file
911 */
912 ret = VM_GGT_INVALID;
913 }
914 else
915 {
916 /* check the signature we read against the known signatures */
917 if (memcmp(buf, "TADS2 bin\012\015\032", 12) == 0)
918 ret = VM_GGT_TADS2;
919 else if (memcmp(buf, "T3-image\015\012\032", 11) == 0)
920 ret = VM_GGT_TADS3;
921 else
922 ret = VM_GGT_INVALID;
923 }
924
925 /* close the file */
926 osfcls(fp);
927
928 /* return the version identifier */
929 return ret;
930 }
931
932 /*
933 * Given a game file argument, determine which engine (TADS 2 or TADS 3)
934 * should be used to run the game.
935 */
vm_get_game_type(const char * filename,char * actual_fname,size_t actual_fname_len,const char * const * defexts,size_t defext_count)936 int vm_get_game_type(const char *filename,
937 char *actual_fname, size_t actual_fname_len,
938 const char *const *defexts, size_t defext_count)
939 {
940 int good_count;
941 int last_good_ver;
942 size_t i;
943
944 /*
945 * If the exact filename as given exists, determine the file type
946 * directly from this file without trying any default extensions.
947 */
948 if (osfacc(filename) == 0)
949 {
950 /* the actual filename is exactly what we were given */
951 if (actual_fname_len != 0)
952 {
953 /* copy the filename, limiting it to the buffer length */
954 strncpy(actual_fname, filename, actual_fname_len);
955 actual_fname[actual_fname_len - 1] = '\0';
956 }
957
958 /* return the type according to the file's signature */
959 return vm_get_game_type_for_file(filename);
960 }
961
962 /* presume we won't find any good files using default extensions */
963 good_count = 0;
964
965 /* try each default extension supplied */
966 for (i = 0 ; i < defext_count ; ++i)
967 {
968 int cur_ver;
969 char cur_fname[OSFNMAX];
970
971 /*
972 * build the default filename from the given filename and the
973 * current default suffix
974 */
975 strcpy(cur_fname, filename);
976 os_defext(cur_fname, defexts[i]);
977
978 /* get the version for this file */
979 cur_ver = vm_get_game_type_for_file(cur_fname);
980
981 /* if it's a valid code, note it and remember it */
982 if (vm_ggt_is_valid(cur_ver))
983 {
984 /* it's a valid file - count it */
985 ++good_count;
986
987 /* remember its version as the last good file's version */
988 last_good_ver = cur_ver;
989
990 /* remember its name as the last good file's name */
991 if (actual_fname_len != 0)
992 {
993 /* copy the filename, limiting it to the buffer length */
994 strncpy(actual_fname, cur_fname, actual_fname_len);
995 actual_fname[actual_fname_len - 1] = '\0';
996 }
997 }
998 }
999
1000 /*
1001 * If we had exactly one good match, return it. We will already have
1002 * filled in actual_fname with the last good filename, so all we need
1003 * to do in this case is return the version ID for the last good file.
1004 */
1005 if (good_count == 1)
1006 return last_good_ver;
1007
1008 /*
1009 * If we didn't find any matches, tell the caller there is no match
1010 * for the given filename.
1011 */
1012 if (good_count == 0)
1013 return VM_GGT_NOT_FOUND;
1014
1015 /* we found more than one match, so the type is ambiguous */
1016 return VM_GGT_AMBIG;
1017 }
1018
1019