cfad47cfa3/tads3/vmmain.cpp

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