1
/* vi: set ts=2 shiftwidth=2 expandtab:
3
* Copyright (C) 2002-2006 Simon Baldwin, simon_baldwin@yahoo.com
4
* Mac portions Copyright (C) 2002 Ben Hines
6
* This program is free software; you can redistribute it and/or modify
7
* it under the terms of version 2 of the GNU General Public License
8
* as published by the Free Software Foundation.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program; if not, write to the Free Software
17
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
22
* Glk interface for Magnetic Scrolls 2.3
23
* --------------------------------------
25
* This module contains the the Glk porting layer for the Magnetic
26
* Scrolls interpreter. It defines the Glk arguments list structure,
27
* the entry points for the Glk library framework to use, and all
28
* platform-abstracted I/O to link to Glk's I/O.
30
* The following items are omitted from this Glk port:
32
* o Glk tries to assert control over _all_ file I/O. It's just too
33
* disruptive to add it to existing code, so for now, the Magnetic
34
* Scrolls interpreter is still dependent on stdio and the like.
53
* True and false definitions -- usually defined in glkstart.h, but we need
54
* them early, so we'll define them here too. We also need NULL, but that's
55
* normally from stdio.h or one of it's cousins.
61
# define TRUE (!FALSE)
65
/*---------------------------------------------------------------------*/
66
/* Module variables, miscellaneous other stuff */
67
/*---------------------------------------------------------------------*/
69
/* Glk Magnetic Scrolls port version number. */
70
static const glui32 GMS_PORT_VERSION = 0x00010601;
73
* We use a maximum of five Glk windows, one for status, one for pictures,
74
* two for hints, and one for everything else. The status and pictures
75
* windows may be NULL, depending on user selections and the capabilities
76
* of the Glk library. The hints windows will normally be NULL, except
77
* when in the hints subsystem.
79
static winid_t gms_main_window = NULL,
80
gms_status_window = NULL,
81
gms_graphics_window = NULL,
82
gms_hint_menu_window = NULL,
83
gms_hint_text_window = NULL;
86
* Transcript stream and input log. These are NULL if there is no current
87
* collection of these strings.
89
static strid_t gms_transcript_stream = NULL,
90
gms_inputlog_stream = NULL;
92
/* Input read log stream, for reading back an input log. */
93
static strid_t gms_readlog_stream = NULL;
95
/* Note about whether graphics is possible, or not. */
96
static int gms_graphics_possible = TRUE;
98
/* Options that may be turned off or set by command line flags. */
99
static int gms_graphics_enabled = TRUE;
101
GAMMA_OFF, GAMMA_NORMAL, GAMMA_HIGH
103
gms_gamma_mode = GAMMA_NORMAL;
104
static int gms_animation_enabled = TRUE,
105
gms_prompt_enabled = TRUE,
106
gms_abbreviations_enabled = TRUE,
107
gms_commands_enabled = TRUE;
109
/* Magnetic Scrolls standard input prompt string. */
110
static const char * const GMS_INPUT_PROMPT = ">";
112
/* Forward declaration of event wait function. */
113
static void gms_event_wait (glui32 wait_type, event_t * event);
116
/*---------------------------------------------------------------------*/
117
/* Glk port utility functions */
118
/*---------------------------------------------------------------------*/
123
* Fatal error handler. The function returns, expecting the caller to
124
* abort() or otherwise handle the error.
127
gms_fatal (const char *string)
130
* If the failure happens too early for us to have a window, print
131
* the message to stderr.
133
if (!gms_main_window)
135
fprintf (stderr, "\n\nINTERNAL ERROR: %s\n", string);
137
fprintf (stderr, "\nPlease record the details of this error, try to"
138
" note down everything you did to cause it, and email"
139
" this information to simon_baldwin@yahoo.com.\n\n");
143
/* Cancel all possible pending window input events. */
144
glk_cancel_line_event (gms_main_window, NULL);
145
glk_cancel_char_event (gms_main_window);
146
if (gms_hint_menu_window)
148
glk_cancel_char_event (gms_hint_menu_window);
149
glk_window_close (gms_hint_menu_window, NULL);
151
if (gms_hint_text_window)
153
glk_cancel_char_event (gms_hint_text_window);
154
glk_window_close (gms_hint_text_window, NULL);
157
/* Print a message indicating the error. */
158
glk_set_window (gms_main_window);
159
glk_set_style (style_Normal);
160
glk_put_string ("\n\nINTERNAL ERROR: ");
161
glk_put_string ((char *) string);
163
glk_put_string ("\n\nPlease record the details of this error, try to"
164
" note down everything you did to cause it, and email"
165
" this information to simon_baldwin@yahoo.com.\n\n");
173
* Non-failing malloc and realloc; call gms_fatal and exit if memory
177
gms_malloc (size_t size)
181
pointer = malloc (size);
184
gms_fatal ("GLK: Out of system memory");
192
gms_realloc (void *ptr, size_t size)
196
pointer = realloc (ptr, size);
199
gms_fatal ("GLK: Out of system memory");
211
* Strncasecmp and strcasecmp are not ANSI functions, so here are local
212
* definitions to do the same jobs.
215
gms_strncasecmp (const char *s1, const char *s2, size_t n)
219
for (index = 0; index < n; index++)
223
diff = glk_char_to_lower (s1[index]) - glk_char_to_lower (s2[index]);
224
if (diff < 0 || diff > 0)
225
return diff < 0 ? -1 : 1;
232
gms_strcasecmp (const char *s1, const char *s2)
240
result = gms_strncasecmp (s1, s2, s1len < s2len ? s1len : s2len);
241
if (result < 0 || result > 0)
244
return s1len < s2len ? -1 : s1len > s2len ? 1 : 0;
248
/*---------------------------------------------------------------------*/
249
/* Glk port stub graphics functions */
250
/*---------------------------------------------------------------------*/
253
* If we're working with a very stripped down, old, or lazy Glk library
254
* that neither offers Glk graphics nor graphics stubs functions, here
255
* we define our own stubs, to avoid link-time errors.
257
#ifndef GLK_MODULE_IMAGE
259
glk_image_draw (winid_t win, glui32 image, glsi32 val1, glsi32 val2)
264
glk_image_draw_scaled (winid_t win, glui32 image, glsi32 val1, glsi32 val2,
265
glui32 width, glui32 height)
270
glk_image_get_info (glui32 image, glui32 * width, glui32 * height)
275
glk_window_flow_break (winid_t win)
279
glk_window_erase_rect (winid_t win, glsi32 left, glsi32 top,
280
glui32 width, glui32 height)
284
glk_window_fill_rect (winid_t win, glui32 color, glsi32 left, glsi32 top,
285
glui32 width, glui32 height)
289
glk_window_set_background_color (winid_t win, glui32 color)
295
/*---------------------------------------------------------------------*/
296
/* Glk port CRC functions */
297
/*---------------------------------------------------------------------*/
299
/* CRC table initialization polynomial. */
300
static const glui32 GMS_CRC_POLYNOMIAL = 0xedb88320;
304
* gms_get_buffer_crc()
306
* Return the CRC of the bytes in buffer[0..length-1].
308
* This algorithm is taken from the PNG specification, version 1.0.
311
gms_get_buffer_crc (const void *void_buffer, size_t length)
313
static int is_initialized = FALSE;
314
static glui32 crc_table[UCHAR_MAX + 1];
316
const char *buffer = (const char *) void_buffer;
320
/* Build the static CRC lookup table on first call. */
323
for (index = 0; index < UCHAR_MAX + 1; index++)
327
crc = (glui32) index;
328
for (bit = 0; bit < CHAR_BIT; bit++)
329
crc = crc & 1 ? GMS_CRC_POLYNOMIAL ^ (crc >> 1) : crc >> 1;
331
crc_table[index] = crc;
334
is_initialized = TRUE;
336
/* CRC lookup table self-test, after is_initialized set -- recursion. */
337
assert (gms_get_buffer_crc ("123456789", 9) == 0xcbf43926);
341
* Start with all ones in the crc, then update using table entries. Xor
342
* with all ones again, finally, before returning.
345
for (index = 0; index < length; index++)
346
crc = crc_table[(crc ^ buffer[index]) & UCHAR_MAX] ^ (crc >> CHAR_BIT);
347
return crc ^ 0xffffffff;
351
/*---------------------------------------------------------------------*/
352
/* Glk port game identification data and identification functions */
353
/*---------------------------------------------------------------------*/
356
* The game's name, suitable for printing out on a status line, or other
357
* location where game information is relevant. Set on game startup, by
358
* identifying the game from its text file header.
360
static const char *gms_gameid_game_name = NULL;
364
* The following game database is built from Generic/games.txt, and is used
365
* to identify the game being run. Magnetic Scrolls games don't generally
366
* supply a status line, so this data can be used instead.
368
typedef const struct {
369
const type32 undo_size; /* Header word at offset 0x22 */
370
const type32 undo_pc; /* Header word at offset 0x26 */
371
const char * const name; /* Game title and platform */
373
typedef gms_game_table_t *gms_game_tableref_t;
375
static gms_game_table_t GMS_GAME_TABLE[] = {
376
{0x2100, 0x427e, "Corruption v1.11 (Amstrad CPC)"},
377
{0x2100, 0x43a0, "Corruption v1.11 (Archimedes)"},
378
{0x2100, 0x43a0, "Corruption v1.11 (DOS)"},
379
{0x2100, 0x4336, "Corruption v1.11 (Commodore 64)"},
380
{0x2100, 0x4222, "Corruption v1.11 (Spectrum +3)"},
381
{0x2100, 0x4350, "Corruption v1.12 (Archimedes)"},
382
{0x2500, 0x6624, "Corruption v1.12 (DOS, Magnetic Windows)"},
384
{0x2300, 0x3fa0, "Fish v1.02 (DOS)"},
385
{0x2400, 0x4364, "Fish v1.03 (Spectrum +3)"},
386
{0x2300, 0x3f72, "Fish v1.07 (Commodore 64)"},
387
{0x2200, 0x3f9c, "Fish v1.08 (Archimedes)"},
388
{0x2a00, 0x583a, "Fish v1.10 (DOS, Magnetic Windows)"},
390
{0x5000, 0x6c30, "Guild v1.0 (Amstrad CPC)"},
391
{0x5000, 0x6cac, "Guild v1.0 (Commodore 64)"},
392
{0x5000, 0x6d5c, "Guild v1.1 (DOS)"},
393
{0x3300, 0x698a, "Guild v1.3 (Archimedes)"},
394
{0x3200, 0x6772, "Guild v1.3 (Spectrum +3)"},
395
{0x3400, 0x6528, "Guild v1.3 (DOS, Magnetic Windows)"},
397
{0x2b00, 0x488c, "Jinxter v1.05 (Commodore 64)"},
398
{0x2c00, 0x4a08, "Jinxter v1.05 (DOS)"},
399
{0x2c00, 0x487a, "Jinxter v1.05 (Spectrum +3)"},
400
{0x2c00, 0x4a56, "Jinxter v1.10 (DOS)"},
401
{0x2b00, 0x4924, "Jinxter v1.22 (Amstrad CPC)"},
402
{0x2c00, 0x4960, "Jinxter v1.30 (Archimedes)"},
404
{0x1600, 0x3940, "Myth v1.0 (Commodore 64)"},
405
{0x1500, 0x3a0a, "Myth v1.0 (DOS)"},
407
{0x3600, 0x42cc, "Pawn v2.3 (Amstrad CPC)"},
408
{0x3600, 0x4420, "Pawn v2.3 (Archimedes)"},
409
{0x3600, 0x3fb0, "Pawn v2.3 (Commodore 64)"},
410
{0x3600, 0x4420, "Pawn v2.3 (DOS)"},
411
{0x3900, 0x42e4, "Pawn v2.3 (Spectrum 128)"},
412
{0x3900, 0x42f4, "Pawn v2.4 (Spectrum +3)"},
414
{0x3900, 0x75f2, "Wonderland v1.21 (DOS, Magnetic Windows)"},
415
{0x3900, 0x75f8, "Wonderland v1.27 (Archimedes)"},
421
* gms_gameid_lookup_game()
423
* Look up and return the game table entry given a game's undo size and
424
* undo pc values. Returns the entry, or NULL if not found.
426
static gms_game_tableref_t
427
gms_gameid_lookup_game (type32 undo_size, type32 undo_pc)
429
gms_game_tableref_t game;
431
for (game = GMS_GAME_TABLE; game->name; game++)
433
if (game->undo_size == undo_size && game->undo_pc == undo_pc)
437
return game->name ? game : NULL;
442
* gms_gameid_read_uint32()
444
* Endian-safe unsigned 32 bit integer read from game text file. Returns
445
* 0 on error, a known unused table value.
448
gms_gameid_read_uint32 (int offset, FILE *stream)
450
unsigned char buffer[4];
452
if (fseek (stream, offset, SEEK_SET) != 0)
454
if (fread (buffer, 1, sizeof (buffer), stream) != sizeof (buffer))
457
return buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3];
462
* gms_gameid_identify_game()
464
* Identify a game from its text file header, and cache the game's name for
465
* later queries. Sets the cache to NULL if not found.
468
gms_gameid_identify_game (const char *text_file)
473
stream = fopen (text_file, "r");
476
type32 undo_size, undo_pc;
477
gms_game_tableref_t game;
479
/* Read the game's signature undo size and undo pc values. */
480
undo_size = gms_gameid_read_uint32 (0x22, stream);
481
undo_pc = gms_gameid_read_uint32 (0x26, stream);
484
/* Search for these values in the table, and set game name if found. */
485
game = gms_gameid_lookup_game (undo_size, undo_pc);
486
gms_gameid_game_name = game ? game->name : NULL;
489
gms_gameid_game_name = NULL;
494
* gms_gameid_get_game_name()
496
* Return the name of the game, or NULL if not identifiable.
499
gms_gameid_get_game_name (void)
501
return gms_gameid_game_name;
505
/*---------------------------------------------------------------------*/
506
/* Glk port picture functions */
507
/*---------------------------------------------------------------------*/
510
* Color conversions lookup tables, and a word about gamma corrections.
512
* When uncorrected, some game pictures can look dark (Corruption, Won-
513
* derland), whereas others look just fine (Guild Of Thieves, Jinxter).
515
* The standard general-purpose gamma correction is around 2.1, with
516
* specific values, normally, of 2.5-2.7 for IBM PC systems, and 1.8 for
517
* Macintosh. However, applying even the low end of this range can make
518
* some pictures look washed out, yet improve others nicely.
520
* To try to solve this, here we'll set up a precalculated table with
521
* discrete gamma values. On displaying a picture, we'll try to find a
522
* gamma correction that seems to offer a reasonable level of contrast
525
* Here's an AWK script to create the gamma table:
527
* BEGIN { max=255.0; step=max/7.0
528
* for (gamma=0.9; gamma<=2.7; gamma+=0.05) {
529
* printf " {\"%2.2f\", {0, ", gamma
530
* for (i=1; i<8; i++) {
531
* printf "%3.0f", (((step*i / max) ^ (1.0/gamma)) * max)
532
* printf "%s", (i<7) ? ", " : ""
535
* printf "%s },\n", (gamma>0.99 && gamma<1.01) ? "FALSE" : "TRUE "
541
const char * const level; /* Gamma correction level. */
542
const unsigned char table[8]; /* Color lookup table. */
543
const int is_corrected; /* Flag if non-linear. */
545
typedef gms_gamma_t *gms_gammaref_t;
547
static gms_gamma_t GMS_GAMMA_TABLE[] = {
548
{"0.90", {0, 29, 63, 99, 137, 175, 215, 255}, TRUE},
549
{"0.95", {0, 33, 68, 105, 141, 179, 217, 255}, TRUE},
550
{"1.00", {0, 36, 73, 109, 146, 182, 219, 255}, FALSE},
551
{"1.05", {0, 40, 77, 114, 150, 185, 220, 255}, TRUE},
552
{"1.10", {0, 43, 82, 118, 153, 188, 222, 255}, TRUE},
553
{"1.15", {0, 47, 86, 122, 157, 190, 223, 255}, TRUE},
554
{"1.20", {0, 50, 90, 126, 160, 193, 224, 255}, TRUE},
555
{"1.25", {0, 54, 94, 129, 163, 195, 225, 255}, TRUE},
556
{"1.30", {0, 57, 97, 133, 166, 197, 226, 255}, TRUE},
557
{"1.35", {0, 60, 101, 136, 168, 199, 227, 255}, TRUE},
558
{"1.40", {0, 64, 104, 139, 171, 201, 228, 255}, TRUE},
559
{"1.45", {0, 67, 107, 142, 173, 202, 229, 255}, TRUE},
560
{"1.50", {0, 70, 111, 145, 176, 204, 230, 255}, TRUE},
561
{"1.55", {0, 73, 114, 148, 178, 205, 231, 255}, TRUE},
562
{"1.60", {0, 76, 117, 150, 180, 207, 232, 255}, TRUE},
563
{"1.65", {0, 78, 119, 153, 182, 208, 232, 255}, TRUE},
564
{"1.70", {0, 81, 122, 155, 183, 209, 233, 255}, TRUE},
565
{"1.75", {0, 84, 125, 157, 185, 210, 233, 255}, TRUE},
566
{"1.80", {0, 87, 127, 159, 187, 212, 234, 255}, TRUE},
567
{"1.85", {0, 89, 130, 161, 188, 213, 235, 255}, TRUE},
568
{"1.90", {0, 92, 132, 163, 190, 214, 235, 255}, TRUE},
569
{"1.95", {0, 94, 134, 165, 191, 215, 236, 255}, TRUE},
570
{"2.00", {0, 96, 136, 167, 193, 216, 236, 255}, TRUE},
571
{"2.05", {0, 99, 138, 169, 194, 216, 237, 255}, TRUE},
572
{"2.10", {0, 101, 140, 170, 195, 217, 237, 255}, TRUE},
573
{"2.15", {0, 103, 142, 172, 197, 218, 237, 255}, TRUE},
574
{"2.20", {0, 105, 144, 173, 198, 219, 238, 255}, TRUE},
575
{"2.25", {0, 107, 146, 175, 199, 220, 238, 255}, TRUE},
576
{"2.30", {0, 109, 148, 176, 200, 220, 238, 255}, TRUE},
577
{"2.35", {0, 111, 150, 178, 201, 221, 239, 255}, TRUE},
578
{"2.40", {0, 113, 151, 179, 202, 222, 239, 255}, TRUE},
579
{"2.45", {0, 115, 153, 180, 203, 222, 239, 255}, TRUE},
580
{"2.50", {0, 117, 154, 182, 204, 223, 240, 255}, TRUE},
581
{"2.55", {0, 119, 156, 183, 205, 223, 240, 255}, TRUE},
582
{"2.60", {0, 121, 158, 184, 206, 224, 240, 255}, TRUE},
583
{"2.65", {0, 122, 159, 185, 206, 225, 241, 255}, TRUE},
584
{"2.70", {0, 124, 160, 186, 207, 225, 241, 255}, TRUE},
585
{NULL, {0, 0, 0, 0, 0, 0, 0, 0}, FALSE}
588
/* R,G,B color triple definition. */
591
int red, green, blue;
593
typedef gms_rgb_t *gms_rgbref_t;
596
* Weighting values for calculating the luminance of a color. There are
597
* two commonly used sets of values for these -- 299,587,114, taken from
598
* NTSC (Never The Same Color) 1953 standards, and 212,716,72, which is the
599
* set that modern CRTs tend to match. The NTSC ones seem to give the best
600
* subjective results.
602
static const gms_rgb_t GMS_LUMINANCE_WEIGHTS = { 299, 587, 114 };
605
* Maximum number of regions to consider in a single repaint pass. A
606
* couple of hundred seems to strike the right balance between not too
607
* sluggardly picture updates, and responsiveness to input during graphics
608
* rendering, when combined with short timeouts.
610
static const int GMS_REPAINT_LIMIT = 256;
613
* Graphics timeout; we like an update call after this period (ms). In
614
* practice, this timeout may actually be shorter than the time taken
615
* to reach the limit on repaint regions, but because Glk guarantees that
616
* user interactions (in this case, line events) take precedence over
617
* timeouts, this should be okay; we'll still see a game that responds to
618
* input each time the background repaint function yields.
620
* Setting this value is tricky. We'd like it to be the shortest possible
621
* consistent with getting other stuff done, say 10ms. However, Xglk has
622
* a granularity of 50ms on checking for timeouts, as it uses a 1/20s
623
* timeout on X select. This means that the shortest timeout we'll ever
624
* get from Xglk will be 50ms, so there's no point in setting this shorter
625
* than that. With luck, other Glk libraries will be more efficient than
626
* this, and can give us higher timer resolution; we'll set 50ms here, and
627
* hope that no other Glk library is worse.
629
static const glui32 GMS_GRAPHICS_TIMEOUT = 50;
632
* Count of timeouts to wait in between animation paints, and to wait on
633
* repaint request. Waiting for 2 timeouts of around 50ms, gets us to the
634
* 100ms recommended animation frame rate. Waiting after a repaint smooths
635
* the display where the frame is being resized, by helping to avoid
636
* graphics output while more resize events are received; around 1/2 second
639
static const int GMS_GRAPHICS_ANIMATION_WAIT = 2,
640
GMS_GRAPHICS_REPAINT_WAIT = 10;
642
/* Pixel size multiplier for image size scaling. */
643
static const int GMS_GRAPHICS_PIXEL = 2;
645
/* Proportion of the display to use for graphics. */
646
static const glui32 GMS_GRAPHICS_PROPORTION = 60;
649
* Border and shading control. For cases where we can't detect the back-
650
* ground color of the main window, there's a default, white, background.
651
* Bordering is black, with a 1 pixel border, 2 pixel shading, and 8 steps
654
static const glui32 GMS_GRAPHICS_DEFAULT_BACKGROUND = 0x00ffffff,
655
GMS_GRAPHICS_BORDER_COLOR = 0x00000000;
656
static const int GMS_GRAPHICS_BORDER = 1,
657
GMS_GRAPHICS_SHADING = 2,
658
GMS_GRAPHICS_SHADE_STEPS = 8;
661
* Guaranteed unused pixel value. This value is used to fill the on-screen
662
* buffer on new pictures or repaints, resulting in a full paint of all
663
* pixels since no off-screen, real picture, pixel will match it.
665
static const int GMS_GRAPHICS_UNUSED_PIXEL = 0xff;
668
* The current picture bitmap being displayed, its width, height, palette,
669
* animation flag, and picture id.
671
enum { GMS_PALETTE_SIZE = 16 };
672
static type8 *gms_graphics_bitmap = NULL;
673
static type16 gms_graphics_width = 0,
674
gms_graphics_height = 0,
675
gms_graphics_palette[GMS_PALETTE_SIZE]; /* = { 0, ... }; */
676
static type8 gms_graphics_animated = FALSE;
677
static type32 gms_graphics_picture = 0;
680
* Flags set on new picture, and on resize or arrange events, and a flag
681
* to indicate whether background repaint is stopped or active.
683
static int gms_graphics_new_picture = FALSE,
684
gms_graphics_repaint = FALSE,
685
gms_graphics_active = FALSE;
687
/* Flag to try to monitor the state of interpreter graphics. */
688
static int gms_graphics_interpreter = FALSE;
691
* Pointer to the two graphics buffers, one the off-screen representation
692
* of pixels, and the other tracking on-screen data. These are temporary
693
* graphics malloc'ed memory, and should be free'd on exit.
695
static type8 *gms_graphics_off_screen = NULL,
696
*gms_graphics_on_screen = NULL;
699
* Pointer to the current active gamma table entry. Because of the way
700
* it's queried, this may not be NULL, otherwise we risk a race, with
701
* admittedly a very low probability, with the updater. So, it's init-
702
* ialized instead to the gamma table. The real value in use is inserted
703
* on the first picture update timeout call for a new picture.
705
static gms_gammaref_t gms_graphics_current_gamma = GMS_GAMMA_TABLE;
708
* The number of colors used in the palette by the current picture. This
709
* value is also at risk of a race with the updater, so it too has a mild
710
* lie for a default value.
712
static int gms_graphics_color_count = GMS_PALETTE_SIZE;
716
* gms_graphics_open()
718
* If it's not open, open the graphics window. Returns TRUE if graphics
719
* was successfully started, or already on.
722
gms_graphics_open (void)
724
if (!gms_graphics_window)
726
gms_graphics_window = glk_window_open (gms_main_window,
728
| winmethod_Proportional,
729
GMS_GRAPHICS_PROPORTION,
730
wintype_Graphics, 0);
733
return gms_graphics_window != NULL;
738
* gms_graphics_close()
740
* If open, close the graphics window and set back to NULL.
743
gms_graphics_close (void)
745
if (gms_graphics_window)
747
glk_window_close (gms_graphics_window, NULL);
748
gms_graphics_window = NULL;
754
* gms_graphics_start()
756
* If graphics enabled, start any background picture update processing.
759
gms_graphics_start (void)
761
if (gms_graphics_enabled)
763
/* If not running, start the updating "thread". */
764
if (!gms_graphics_active)
766
glk_request_timer_events (GMS_GRAPHICS_TIMEOUT);
767
gms_graphics_active = TRUE;
774
* gms_graphics_stop()
776
* Stop any background picture update processing.
779
gms_graphics_stop (void)
781
/* If running, stop the updating "thread". */
782
if (gms_graphics_active)
784
glk_request_timer_events (0);
785
gms_graphics_active = FALSE;
791
* gms_graphics_are_displayed()
793
* Return TRUE if graphics are currently being displayed, FALSE otherwise.
796
gms_graphics_are_displayed (void)
798
return gms_graphics_window != NULL;
803
* gms_graphics_paint()
805
* Set up a complete repaint of the current picture in the graphics window.
806
* This function should be called on the appropriate Glk window resize and
810
gms_graphics_paint (void)
812
if (gms_graphics_enabled && gms_graphics_are_displayed ())
814
/* Set the repaint flag, and start graphics. */
815
gms_graphics_repaint = TRUE;
816
gms_graphics_start ();
822
* gms_graphics_restart()
824
* Restart graphics as if the current picture is a new picture. This
825
* function should be called whenever graphics is re-enabled after being
826
* disabled, on change of gamma color correction policy, and on change
827
* of animation policy.
830
gms_graphics_restart (void)
832
if (gms_graphics_enabled && gms_graphics_are_displayed ())
835
* If the picture is animated, we'll need to be able to re-get the
836
* first animation frame so that the picture can be treated as if
837
* it is a new one. So here, we'll try to re-extract the current
838
* picture to do this. Calling ms_extract() is safe because we
839
* don't get here unless graphics are displayed, and graphics aren't
840
* displayed until there's a valid picture loaded, and ms_showpic
841
* only loads a picture after it's called ms_extract and set the
842
* picture id into gms_graphics_picture.
844
* The bitmap and other picture stuff can be ignored because it's
845
* the precise same stuff as we already have in picture details
846
* variables. If the ms_extract() fails, we'll carry on regardless,
847
* which may, or may not, result in the ideal picture display.
849
* One or two non-animated pictures return NULL from ms_extract()
850
* being re-called, so we'll restrict calls to animations only.
851
* And just to be safe, we'll also call only if we're already
852
* holding a bitmap (and we should be; how else could the graphics
853
* animation flag be set?...).
855
if (gms_graphics_animated && gms_graphics_bitmap)
857
type8 *bitmap, animated;
858
type16 width, height, palette[GMS_PALETTE_SIZE];
860
/* Extract the bitmap into dummy variables. */
861
bitmap = ms_extract (gms_graphics_picture, &width, &height,
865
/* Set the new picture flag, and start graphics. */
866
gms_graphics_new_picture = TRUE;
867
gms_graphics_start ();
873
* gms_graphics_count_colors()
875
* Analyze an image, and return the usage count of each palette color, and
876
* an overall count of how many colors out of the palette are used. NULL
877
* arguments indicate no interest in the return value.
880
gms_graphics_count_colors (type8 bitmap[], type16 width, type16 height,
881
int *color_count, long color_usage[])
884
long usage[GMS_PALETTE_SIZE], index_row;
888
* Traverse the image, counting each pixel usage. For the y iterator,
889
* maintain an index row as an optimization to avoid multiplications in
893
memset (usage, 0, sizeof (usage));
894
for (y = 0, index_row = 0; y < height; y++, index_row += width)
896
for (x = 0; x < width; x++)
900
/* Get the pixel index, and update the count for this color. */
901
index = index_row + x;
902
usage[bitmap[index]]++;
904
/* If color usage is now 1, note new color encountered. */
905
if (usage[bitmap[index]] == 1)
911
*color_count = count;
914
memcpy (color_usage, usage, sizeof (usage));
919
* gms_graphics_game_to_rgb_color()
920
* gms_graphics_split_color()
921
* gms_graphics_combine_color()
922
* gms_graphics_color_luminance()
924
* General graphics helper functions, to convert between Magnetic Scrolls
925
* and RGB color representations, and between RGB and Glk glui32 color
926
* representations, and to calculate color luminance.
929
gms_graphics_game_to_rgb_color (type16 color, gms_gammaref_t gamma,
930
gms_rgbref_t rgb_color)
932
assert (gamma && rgb_color);
935
* Convert Magnetic Scrolls color, through gamma, into RGB. This splits
936
* the color into components based on the 3-bits used in the game palette,
937
* and gamma-corrects and rescales each to the range 0-255, using the given
940
rgb_color->red = gamma->table[(color & 0x700) >> 8];
941
rgb_color->green = gamma->table[(color & 0x070) >> 4];
942
rgb_color->blue = gamma->table[(color & 0x007)];
946
gms_graphics_split_color (glui32 color, gms_rgbref_t rgb_color)
950
rgb_color->red = (color >> 16) & 0xff;
951
rgb_color->green = (color >> 8) & 0xff;
952
rgb_color->blue = color & 0xff;
956
gms_graphics_combine_color (gms_rgbref_t rgb_color)
961
color = (rgb_color->red << 16) | (rgb_color->green << 8) | rgb_color->blue;
966
gms_graphics_color_luminance (gms_rgbref_t rgb_color)
968
static int is_initialized = FALSE;
969
static int weighting = 0;
973
/* On the first call, calculate the overall weighting. */
976
weighting = GMS_LUMINANCE_WEIGHTS.red + GMS_LUMINANCE_WEIGHTS.green
977
+ GMS_LUMINANCE_WEIGHTS.blue;
979
is_initialized = TRUE;
982
/* Calculate the luminance and scale back by 1000 to 0-255 before return. */
983
luminance = ((long) rgb_color->red * (long) GMS_LUMINANCE_WEIGHTS.red
984
+ (long) rgb_color->green * (long) GMS_LUMINANCE_WEIGHTS.green
985
+ (long) rgb_color->blue * (long) GMS_LUMINANCE_WEIGHTS.blue);
987
assert (weighting > 0);
988
return (int) (luminance / weighting);
993
* gms_graphics_compare_luminance()
994
* gms_graphics_constrast_variance()
996
* Calculate the contrast variance of the given palette and color usage, at
997
* the given gamma correction level. Helper functions for automatic gamma
1001
gms_graphics_compare_luminance (const void *void_first,
1002
const void *void_second)
1004
long first = *(long *) void_first;
1005
long second = *(long *) void_second;
1007
return first > second ? 1 : second > first ? -1 : 0;
1011
gms_graphics_contrast_variance (type16 palette[],
1012
long color_usage[], gms_gammaref_t gamma)
1014
int index, count, has_black, mean;
1016
int contrast[GMS_PALETTE_SIZE];
1017
int luminance[GMS_PALETTE_SIZE + 1]; /* Luminance for each color,
1018
plus one extra for black */
1020
/* Calculate the luminance energy of each palette color at this gamma. */
1022
for (index = 0, count = 0; index < GMS_PALETTE_SIZE; index++)
1024
if (color_usage[index] > 0)
1026
gms_rgb_t rgb_color;
1029
* Convert the 16-bit base picture color to RGB using the gamma
1030
* currently under consideration. Calculate luminance for this
1031
* color and store in the next available luminance array entry.
1033
gms_graphics_game_to_rgb_color (palette[index], gamma, &rgb_color);
1034
luminance[count++] = gms_graphics_color_luminance (&rgb_color);
1036
/* Note if black is present in the palette. */
1037
has_black |= luminance[count - 1] == 0;
1042
* For best results, we want to anchor contrast calculations to black, so
1043
* if black is not represented in the palette, add it as an extra luminance.
1046
luminance[count++] = 0;
1048
/* Sort luminance values so that the darkest color is at index 0. */
1049
qsort (luminance, count,
1050
sizeof (*luminance), gms_graphics_compare_luminance);
1053
* Calculate the difference in luminance between adjacent luminances in
1054
* the sorted array, as contrast, and at the same time sum contrasts to
1055
* calculate the mean.
1058
for (index = 0; index < count - 1; index++)
1060
contrast[index] = luminance[index + 1] - luminance[index];
1061
sum += contrast[index];
1063
mean = sum / (count - 1);
1065
/* Calculate and return the variance in contrasts. */
1067
for (index = 0; index < count - 1; index++)
1068
sum += (contrast[index] - mean) * (contrast[index] - mean);
1070
return sum / (count - 1);
1075
* gms_graphics_equal_contrast_gamma()
1077
* Try to find a gamma correction for the given palette and color usage that
1078
* gives relatively equal contrast among the displayed colors.
1080
* To do this, we search the gamma tables, computing color luminance for each
1081
* color in the palette given this gamma. From luminances, we then compute
1082
* the contrasts between the colors, and settle on the gamma correction that
1083
* gives the most even and well-distributed picture contrast. We ignore
1084
* colors not used in the palette.
1086
* Note that we don't consider how often a palette color is used, only whether
1087
* it's represented, or not. Some weighting might improve things, but the
1088
* simple method seems to work adequately. In practice, as there are only 16
1089
* colors in a palette, most pictures use most colors in a relatively well
1090
* distributed manner. This algorithm probably wouldn't work well on real
1091
* photographs, though.
1093
static gms_gammaref_t
1094
gms_graphics_equal_contrast_gamma (type16 palette[], long color_usage[])
1096
gms_gammaref_t gamma, result;
1097
long lowest_variance;
1098
assert (palette && color_usage);
1101
lowest_variance = LONG_MAX;
1103
/* Search the gamma table for the entry with the lowest contrast variance. */
1104
for (gamma = GMS_GAMMA_TABLE; gamma->level; gamma++)
1108
/* Find the color contrast variance of the palette at this gamma. */
1109
variance = gms_graphics_contrast_variance (palette, color_usage, gamma);
1112
* Compare the variance to the lowest so far, and if it is lower, note
1113
* the gamma entry that produced it as being the current best found.
1115
if (variance < lowest_variance)
1118
lowest_variance = variance;
1128
* gms_graphics_select_gamma()
1130
* Select a suitable gamma for the picture, based on the current gamma mode.
1132
* The function returns either the linear gamma, a gamma value half way
1133
* between linear and the gamma that gives the most even contrast, or just
1134
* the gamma that gives the most even contrast.
1136
* In the normal case, a value half way to the extreme case of making color
1137
* contrast equal for all colors is, subjectively, a reasonable value to use.
1138
* The problem cases are the darkest pictures, and selecting this value
1139
* brightens them while at the same time not making them look overbright or
1142
static gms_gammaref_t
1143
gms_graphics_select_gamma (type8 bitmap[], type16 width, type16 height,
1146
static int is_initialized = FALSE;
1147
static gms_gammaref_t linear_gamma = NULL;
1149
long color_usage[GMS_PALETTE_SIZE];
1151
gms_gammaref_t contrast_gamma;
1153
/* On first call, find and cache the uncorrected gamma table entry. */
1154
if (!is_initialized)
1156
gms_gammaref_t gamma;
1158
for (gamma = GMS_GAMMA_TABLE; gamma->level; gamma++)
1160
if (!gamma->is_corrected)
1162
linear_gamma = gamma;
1167
is_initialized = TRUE;
1169
assert (linear_gamma);
1172
* Check to see if automated correction is turned off; if it is, return
1175
if (gms_gamma_mode == GAMMA_OFF)
1176
return linear_gamma;
1179
* Get the color usage and count of total colors represented. For a
1180
* degenerate picture with one color or less, return the linear gamma.
1182
gms_graphics_count_colors (bitmap, width, height, &color_count, color_usage);
1183
if (color_count <= 1)
1184
return linear_gamma;
1187
* Now calculate a gamma setting to give the most equal contrast across the
1188
* picture colors. We'll return either half this gamma, or all of it.
1190
contrast_gamma = gms_graphics_equal_contrast_gamma (palette, color_usage);
1193
* For normal automated correction, return a gamma value half way between
1194
* the linear gamma and the equal contrast gamma.
1196
if (gms_gamma_mode == GAMMA_NORMAL)
1197
return linear_gamma + (contrast_gamma - linear_gamma) / 2;
1199
/* Correction must be high; return the equal contrast gamma. */
1200
assert (gms_gamma_mode == GAMMA_HIGH);
1201
return contrast_gamma;
1206
* gms_graphics_clear_and_border()
1208
* Clear the graphics window, and border and shade the area where the
1209
* picture is going to be rendered. This attempts a small raised effect
1210
* for the picture, in keeping with modern trends.
1213
gms_graphics_clear_and_border (winid_t glk_window, int x_offset, int y_offset,
1214
int pixel_size, type16 width, type16 height)
1216
glui32 background, fade_color, shading_color;
1217
gms_rgb_t rgb_background, rgb_border, rgb_fade;
1219
assert (glk_window);
1222
* Try to detect the background color of the main window, by getting the
1223
* background for Normal style (Glk offers no way to directly get a window's
1224
* background color). If we can get it, we'll match the graphics window
1225
* background to it. If we can't, we'll default the color to white.
1227
if (!glk_style_measure (gms_main_window,
1228
style_Normal, stylehint_BackColor, &background))
1231
* Unable to get the main window background, so assume, and default
1232
* graphics to white.
1234
background = GMS_GRAPHICS_DEFAULT_BACKGROUND;
1238
* Set the graphics window background to match the main window background,
1239
* as best as we can tell, and clear the window.
1241
glk_window_set_background_color (glk_window, background);
1242
glk_window_clear (glk_window);
1245
* For very small pictures, just border them, but don't try and do any
1246
* shading. Failing this check is probably highly unlikely.
1248
if (width < 2 * GMS_GRAPHICS_SHADE_STEPS
1249
|| height < 2 * GMS_GRAPHICS_SHADE_STEPS)
1251
/* Paint a rectangle bigger than the picture by border pixels. */
1252
glk_window_fill_rect (glk_window,
1253
GMS_GRAPHICS_BORDER_COLOR,
1254
x_offset - GMS_GRAPHICS_BORDER,
1255
y_offset - GMS_GRAPHICS_BORDER,
1256
width * pixel_size + GMS_GRAPHICS_BORDER * 2,
1257
height * pixel_size + GMS_GRAPHICS_BORDER * 2);
1262
* Paint a rectangle bigger than the picture by border pixels all round,
1263
* and with additional shading pixels right and below. Some of these
1264
* shading pixels are later overwritten by the fading loop below. The
1265
* picture will sit over this rectangle.
1267
glk_window_fill_rect (glk_window,
1268
GMS_GRAPHICS_BORDER_COLOR,
1269
x_offset - GMS_GRAPHICS_BORDER,
1270
y_offset - GMS_GRAPHICS_BORDER,
1271
width * pixel_size + GMS_GRAPHICS_BORDER * 2
1272
+ GMS_GRAPHICS_SHADING,
1273
height * pixel_size + GMS_GRAPHICS_BORDER * 2
1274
+ GMS_GRAPHICS_SHADING);
1277
* Split the main window background color and the border color into
1280
gms_graphics_split_color (background, &rgb_background);
1281
gms_graphics_split_color (GMS_GRAPHICS_BORDER_COLOR, &rgb_border);
1284
* Generate the incremental color to use in fade steps. Here we're
1285
* assuming that the border is always darker than the main window
1286
* background (currently valid, as we're using black).
1288
rgb_fade.red = (rgb_background.red - rgb_border.red)
1289
/ GMS_GRAPHICS_SHADE_STEPS;
1290
rgb_fade.green = (rgb_background.green - rgb_border.green)
1291
/ GMS_GRAPHICS_SHADE_STEPS;
1292
rgb_fade.blue = (rgb_background.blue - rgb_border.blue)
1293
/ GMS_GRAPHICS_SHADE_STEPS;
1295
/* Combine RGB fade into a single incremental Glk color. */
1296
fade_color = gms_graphics_combine_color (&rgb_fade);
1298
/* Fade in edge, from background to border, shading in stages. */
1299
shading_color = background;
1300
for (index = 0; index < GMS_GRAPHICS_SHADE_STEPS; index++)
1302
/* Shade the two border areas with this color. */
1303
glk_window_fill_rect (glk_window, shading_color,
1304
x_offset + width * pixel_size
1305
+ GMS_GRAPHICS_BORDER,
1306
y_offset + index - GMS_GRAPHICS_BORDER,
1307
GMS_GRAPHICS_SHADING, 1);
1308
glk_window_fill_rect (glk_window, shading_color,
1309
x_offset + index - GMS_GRAPHICS_BORDER,
1310
y_offset + height * pixel_size
1311
+ GMS_GRAPHICS_BORDER,
1312
1, GMS_GRAPHICS_SHADING);
1314
/* Update the shading color for the fade next iteration. */
1315
shading_color -= fade_color;
1321
* gms_graphics_convert_palette()
1323
* Convert a Magnetic Scrolls color palette to a Glk one, using the given
1324
* gamma corrections.
1327
gms_graphics_convert_palette (type16 ms_palette[], gms_gammaref_t gamma,
1328
glui32 glk_palette[])
1331
assert (ms_palette && gamma && glk_palette);
1333
for (index = 0; index < GMS_PALETTE_SIZE; index++)
1335
gms_rgb_t rgb_color;
1338
* Convert the 16-bit base picture color through gamma to a 32-bit
1339
* RGB color, and combine into a Glk color and store in the Glk palette.
1341
gms_graphics_game_to_rgb_color (ms_palette[index], gamma, &rgb_color);
1342
glk_palette[index] = gms_graphics_combine_color (&rgb_color);
1348
* gms_graphics_position_picture()
1350
* Given a picture width and height, return the x and y offsets to center
1351
* this picture in the current graphics window.
1354
gms_graphics_position_picture (winid_t glk_window,
1355
int pixel_size, type16 width, type16 height,
1356
int *x_offset, int *y_offset)
1358
glui32 window_width, window_height;
1359
assert (glk_window && x_offset && y_offset);
1361
/* Measure the current graphics window dimensions. */
1362
glk_window_get_size (glk_window, &window_width, &window_height);
1365
* Calculate and return an x and y offset to use on point plotting, so that
1366
* the image centers inside the graphical window.
1368
*x_offset = ((int) window_width - width * pixel_size) / 2;
1369
*y_offset = ((int) window_height - height * pixel_size) / 2;
1374
* gms_graphics_apply_animation_frame()
1376
* Apply a single animation frame to the given off-screen image buffer, using
1377
* the frame bitmap, width, height and mask, the off-screen buffer, and the
1378
* width and height of the main picture.
1380
* Note that 'mask' may be NULL, implying that no frame pixel is transparent.
1383
gms_graphics_apply_animation_frame (type8 bitmap[],
1384
type16 frame_width, type16 frame_height,
1385
type8 mask[], int frame_x, int frame_y,
1386
type8 off_screen[], type16 width,
1389
int mask_width, x, y;
1391
long frame_row, buffer_row, mask_row;
1392
assert (bitmap && off_screen);
1395
* It turns out that the mask isn't quite as described in defs.h, and thanks
1396
* to Torbjorn Andersson and his Gtk port of Magnetic for illuminating this.
1397
* The mask is made up of lines of 16-bit words, so the mask width is always
1398
* even. Here we'll calculate the real width of a mask, and also set a high
1401
mask_width = (((frame_width - 1) / CHAR_BIT) + 2) & (~1);
1402
mask_hibit = 1 << (CHAR_BIT - 1);
1405
* Initialize row index components; these are optimizations to avoid the
1406
* need for multiplications in the frame iteration loop.
1409
buffer_row = frame_y * width;
1413
* Iterate over each frame row, clipping where y lies outside the main
1416
for (y = 0; y < frame_height; y++)
1418
/* Clip if y is outside the main picture area. */
1419
if (y + frame_y < 0 || y + frame_y >= height)
1421
/* Update optimization variables as if not clipped. */
1422
frame_row += frame_width;
1423
buffer_row += width;
1424
mask_row += mask_width;
1428
/* Iterate over each frame column, clipping again. */
1429
for (x = 0; x < frame_width; x++)
1431
long frame_index, buffer_index;
1433
/* Clip if x is outside the main picture area. */
1434
if (x + frame_x < 0 || x + frame_x >= width)
1438
* If there's a mask, check the bit associated with this x,y, and
1439
* ignore any transparent pixels.
1445
/* Isolate the mask byte, and test the transparency bit. */
1446
mask_byte = mask[mask_row + (x / CHAR_BIT)];
1447
if ((mask_byte & (mask_hibit >> (x % CHAR_BIT))) != 0)
1452
* Calculate indexes for this pixel into the frame, and into the
1453
* main off-screen buffer, and transfer the frame pixel into the
1454
* off-screen buffer.
1456
frame_index = frame_row + x;
1457
buffer_index = buffer_row + x + frame_x;
1458
off_screen[buffer_index] = bitmap[frame_index];
1461
/* Update row index components on change of y. */
1462
frame_row += frame_width;
1463
buffer_row += width;
1464
mask_row += mask_width;
1470
* gms_graphics_animate()
1472
* This function finds and applies the next set of animation frames to the
1473
* given off-screen image buffer. It's handed the width and height of the
1474
* main picture, and the off-screen buffer.
1476
* It returns FALSE if at the end of animations, TRUE if more animations
1480
gms_graphics_animate (type8 off_screen[], type16 width, type16 height)
1482
struct ms_position *positions;
1486
assert (off_screen);
1488
/* Search for more animation frames, and return zero if none. */
1489
status = ms_animate (&positions, &count);
1493
/* Apply each animation frame to the off-screen buffer. */
1494
for (frame = 0; frame < count; frame++)
1496
type8 *bitmap, *mask;
1497
type16 frame_width, frame_height;
1500
* Get the bitmap and other details for this frame. If we can't get
1501
* this animation frame, skip it and see if any others are available.
1503
bitmap = ms_get_anim_frame (positions[frame].number,
1504
&frame_width, &frame_height, &mask);
1507
gms_graphics_apply_animation_frame (bitmap,
1508
frame_width, frame_height, mask,
1511
off_screen, width, height);
1515
/* Return TRUE since more animation frames remain. */
1521
* gms_graphics_is_vertex()
1523
* Given a point, return TRUE if that point is the vertex of a fillable
1524
* region. This is a helper function for layering pictures. When assign-
1525
* ing layers, we want to weight the colors that have the most complex
1526
* shapes, or the largest count of isolated areas, heavier than simpler
1529
* By painting the colors with the largest number of isolated areas or
1530
* the most complex shapes first, we help to minimize the number of fill
1531
* regions needed to render the complete picture.
1534
gms_graphics_is_vertex (type8 off_screen[], type16 width, type16 height,
1538
int above, below, left, right;
1540
assert (off_screen);
1542
/* Use an index row to cut down on multiplications. */
1543
index_row = y * width;
1545
/* Find the color of the reference pixel. */
1546
pixel = off_screen[index_row + x];
1547
assert (pixel < GMS_PALETTE_SIZE);
1550
* Detect differences between the reference pixel and its upper, lower, left
1551
* and right neighbors. Mark as different if the neighbor doesn't exist,
1552
* that is, at the edge of the picture.
1554
above = (y == 0 || off_screen[index_row - width + x] != pixel);
1555
below = (y == height - 1 || off_screen[index_row + width + x] != pixel);
1556
left = (x == 0 || off_screen[index_row + x - 1] != pixel);
1557
right = (x == width - 1 || off_screen[index_row + x + 1] != pixel);
1560
* Return TRUE if this pixel lies at the vertex of a rectangular, fillable,
1561
* area. That is, if two adjacent neighbors aren't the same color (or if
1562
* absent -- at the edge of the picture).
1564
return ((above || below) && (left || right));
1569
* gms_graphics_compare_layering_inverted()
1570
* gms_graphics_assign_layers()
1572
* Given two sets of image bitmaps, and a palette, this function will
1573
* assign layers palette colors.
1575
* Layers are assigned by first counting the number of vertices in the
1576
* color plane, to get a measure of the complexity of shapes displayed in
1577
* this color, and also the raw number of times each palette color is
1578
* used. This is then sorted, so that layers are assigned to colors, with
1579
* the lowest layer being the color with the most complex shapes, and
1580
* within this (or where the count of vertices is zero, as it could be
1581
* in some animation frames) the most used color.
1583
* The function compares pixels in the two image bitmaps given, these
1584
* being the off-screen and on-screen buffers, and generates counts only
1585
* where these bitmaps differ. This ensures that only pixels not yet
1586
* painted are included in layering.
1588
* As well as assigning layers, this function returns a set of layer usage
1589
* flags, to help the rendering loop to terminate as early as possible.
1591
* By painting lower layers first, the paint can take in larger areas if
1592
* it's permitted to include not-yet-validated higher levels. This helps
1593
* minimize the amount of Glk areas fills needed to render a picture.
1596
long complexity; /* Count of vertices for this color. */
1597
long usage; /* Color usage count. */
1598
int color; /* Color index into palette. */
1602
gms_graphics_compare_layering_inverted (const void *void_first,
1603
const void *void_second)
1605
gms_layering_t * first = (gms_layering_t *) void_first;
1606
gms_layering_t * second = (gms_layering_t *) void_second;
1609
* Order by complexity first, then by usage, putting largest first. Some
1610
* colors may have no vertices at all when doing animation frames, but
1611
* rendering optimization relies on the first layer that contains no areas
1612
* to fill halting the rendering loop. So it's important here that we order
1613
* indexes so that colors that render complex shapes come first, non-empty,
1614
* but simpler shaped colors next, and finally all genuinely empty layers.
1616
return second->complexity > first->complexity ? 1 :
1617
first->complexity > second->complexity ? -1 :
1618
second->usage > first->usage ? 1 :
1619
first->usage > second->usage ? -1 : 0;
1623
gms_graphics_assign_layers (type8 off_screen[], type8 on_screen[],
1624
type16 width, type16 height,
1625
int layers[], long layer_usage[])
1629
gms_layering_t layering[GMS_PALETTE_SIZE];
1630
assert (off_screen && on_screen && layers && layer_usage);
1632
/* Clear initial complexity and usage counts, and set initial colors. */
1633
for (index = 0; index < GMS_PALETTE_SIZE; index++)
1635
layering[index].complexity = 0;
1636
layering[index].usage = 0;
1637
layering[index].color = index;
1641
* Traverse the image, counting vertices and pixel usage where the pixels
1642
* differ between the off-screen and on-screen buffers. Optimize by
1643
* maintaining an index row to avoid multiplications.
1645
for (y = 0, index_row = 0; y < height; y++, index_row += width)
1647
for (x = 0; x < width; x++)
1652
* Get the index for this pixel, and update complexity and usage
1653
* if off-screen and on-screen pixels differ.
1655
index = index_row + x;
1656
if (on_screen[index] != off_screen[index])
1658
if (gms_graphics_is_vertex (off_screen, width, height, x, y))
1659
layering[off_screen[index]].complexity++;
1661
layering[off_screen[index]].usage++;
1667
* Sort counts to form color indexes. The primary sort is on the shape
1668
* complexity, and within this, on color usage.
1670
qsort (layering, GMS_PALETTE_SIZE,
1671
sizeof (*layering), gms_graphics_compare_layering_inverted);
1674
* Assign a layer to each palette color, and also return the layer usage
1677
for (index = 0; index < GMS_PALETTE_SIZE; index++)
1679
layers[layering[index].color] = index;
1680
layer_usage[index] = layering[index].usage;
1686
* gms_graphics_paint_region()
1688
* This is a partially optimized point plot. Given a point in the graphics
1689
* bitmap, it tries to extend the point to a color region, and fill a number
1690
* of pixels in a single Glk rectangle fill. The goal here is to reduce the
1691
* number of Glk rectangle fills, which tend to be extremely inefficient
1692
* operations for generalized point plotting.
1694
* The extension works in image layers; each palette color is assigned a
1695
* layer, and we paint each layer individually, starting at the lowest. So,
1696
* the region is free to fill any invalidated pixel in a higher layer, and
1697
* all pixels, invalidated or already validated, in the same layer. In
1698
* practice, it is good enough to look for either invalidated pixels or pixels
1699
* in the same layer, and construct a region as large as possible from these,
1700
* then on marking points as validated, mark only those in the same layer as
1701
* the initial point.
1703
* The optimization here is not the best possible, but is reasonable. What
1704
* we do is to try and stretch the region horizontally first, then vertically.
1705
* In practice, we might find larger areas by stretching vertically and then
1706
* horizontally, or by stretching both dimensions at the same time. In
1707
* mitigation, the number of colors in a picture is small (16), and the
1708
* aspect ratio of pictures makes them generally wider than they are tall.
1710
* Once we've found the region, we render it with a single Glk rectangle fill,
1711
* and mark all the pixels in this region that match the layer of the initial
1712
* given point as validated.
1715
gms_graphics_paint_region (winid_t glk_window, glui32 palette[], int layers[],
1716
type8 off_screen[], type8 on_screen[],
1717
int x, int y, int x_offset, int y_offset,
1718
int pixel_size, type16 width, type16 height)
1721
int layer, x_min, x_max, y_min, y_max, x_index, y_index;
1723
assert (glk_window && palette && layers && off_screen && on_screen);
1725
/* Find the color and layer for the initial pixel. */
1726
pixel = off_screen[y * width + x];
1727
layer = layers[pixel];
1728
assert (pixel < GMS_PALETTE_SIZE);
1731
* Start by finding the extent to which we can pull the x coordinate and
1732
* still find either invalidated pixels, or pixels in this layer.
1734
* Use an index row to remove multiplications from the loops.
1736
index_row = y * width;
1737
for (x_min = x; x_min - 1 >= 0; x_min--)
1739
long index = index_row + x_min - 1;
1741
if (on_screen[index] == off_screen[index]
1742
&& layers[off_screen[index]] != layer)
1745
for (x_max = x; x_max + 1 < width; x_max++)
1747
long index = index_row + x_max + 1;
1749
if (on_screen[index] == off_screen[index]
1750
&& layers[off_screen[index]] != layer)
1755
* Now try to stretch the height of the region, by extending the y
1756
* coordinate as much as possible too. Again, we're looking for pixels
1757
* that are invalidated or ones in the same layer. We need to check
1758
* across the full width of the current region.
1760
* As above, an index row removes multiplications from the loops.
1762
for (y_min = y, index_row = (y - 1) * width;
1763
y_min - 1 >= 0; y_min--, index_row -= width)
1765
for (x_index = x_min; x_index <= x_max; x_index++)
1767
long index = index_row + x_index;
1769
if (on_screen[index] == off_screen[index]
1770
&& layers[off_screen[index]] != layer)
1776
for (y_max = y, index_row = (y + 1) * width;
1777
y_max + 1 < height; y_max++, index_row += width)
1779
for (x_index = x_min; x_index <= x_max; x_index++)
1781
long index = index_row + x_index;
1783
if (on_screen[index] == off_screen[index]
1784
&& layers[off_screen[index]] != layer)
1790
/* Fill the region using Glk's rectangle fill. */
1791
glk_window_fill_rect (glk_window, palette[pixel],
1792
x_min * pixel_size + x_offset,
1793
y_min * pixel_size + y_offset,
1794
(x_max - x_min + 1) * pixel_size,
1795
(y_max - y_min + 1) * pixel_size);
1798
* Validate each pixel in the reference layer that was rendered by the
1799
* rectangle fill. We don't validate pixels that are not in this layer
1800
* (and are by definition in higher layers, as we've validated all lower
1801
* layers), since although we colored them, we did it for optimization
1802
* reasons, and they're not yet colored correctly.
1804
* Maintain an index row as an optimization to avoid multiplication.
1806
index_row = y_min * width;
1807
for (y_index = y_min; y_index <= y_max; y_index++)
1809
for (x_index = x_min; x_index <= x_max; x_index++)
1814
* Get the index for x_index,y_index. If the layers match, update
1815
* the on-screen buffer.
1817
index = index_row + x_index;
1818
if (layers[off_screen[index]] == layer)
1820
assert (off_screen[index] == pixel);
1821
on_screen[index] = off_screen[index];
1825
/* Update row index component on change of y. */
1831
gms_graphics_paint_everything (winid_t glk_window,
1834
int x_offset, int y_offset,
1835
type16 width, type16 height)
1837
type8 pixel; /* Reference pixel color */
1840
for (y = 0; y < height; y++)
1842
for (x = 0; x < width; x ++)
1844
pixel = off_screen[ y * width + x ];
1845
glk_window_fill_rect (glk_window,
1847
x * GMS_GRAPHICS_PIXEL + x_offset,
1848
y * GMS_GRAPHICS_PIXEL + y_offset,
1849
GMS_GRAPHICS_PIXEL, GMS_GRAPHICS_PIXEL);
1855
* gms_graphics_timeout()
1857
* This is a background function, called on Glk timeouts. Its job is to
1858
* repaint some of the current graphics image. On successive calls, it
1859
* does a part of the repaint, then yields to other processing. This is
1860
* useful since the Glk primitive to plot points in graphical windows is
1861
* extremely slow; this way, the repaint doesn't block game play.
1863
* The function should be called on Glk timeout events. When the repaint
1864
* is complete, the function will turn off Glk timers.
1866
* The function uses double-buffering to track how much of the graphics
1867
* buffer has been rendered. This helps to minimize the amount of point
1868
* plots required, as only the differences between the two buffers need
1872
gms_graphics_timeout (void)
1874
static glui32 palette[GMS_PALETTE_SIZE]; /* Precomputed Glk palette */
1875
static int layers[GMS_PALETTE_SIZE]; /* Assigned image layers */
1876
static long layer_usage[GMS_PALETTE_SIZE]; /* Image layer occupancies */
1878
static int deferred_repaint = FALSE; /* Local delayed repaint flag */
1879
static int ignore_counter; /* Count of calls ignored */
1881
static int x_offset, y_offset; /* Point plot offsets */
1882
static int yield_counter; /* Yields in rendering */
1883
static int saved_layer; /* Saved current layer */
1884
static int saved_x, saved_y; /* Saved x,y coord */
1886
static int total_regions; /* Debug statistic */
1888
type8 *on_screen; /* On-screen image buffer */
1889
type8 *off_screen; /* Off-screen image buffer */
1890
long picture_size; /* Picture size in pixels */
1891
int layer; /* Image layer iterator */
1892
int x, y; /* Image iterators */
1893
int regions; /* Count of regions painted */
1895
/* Ignore the call if the current graphics state is inactive. */
1896
if (!gms_graphics_active)
1898
assert (gms_graphics_window);
1901
* On detecting a repaint request, note the flag in a local static variable,
1902
* then set up a graphics delay to wait until, hopefully, the resize, if
1903
* that's what caused it, is complete, and return. This makes resizing the
1904
* window a lot smoother, since it prevents unnecessary region paints where
1905
* we are receiving consecutive Glk arrange or redraw events.
1907
if (gms_graphics_repaint)
1909
deferred_repaint = TRUE;
1910
gms_graphics_repaint = FALSE;
1911
ignore_counter = GMS_GRAPHICS_REPAINT_WAIT - 1;
1916
* If asked to ignore a given number of calls, decrement the ignore counter
1917
* and return having done nothing more. This lets us delay graphics
1918
* operations by a number of timeouts, providing animation timing and
1919
* partial protection from resize event "storms".
1921
* Note -- to wait for N timeouts, set the count of timeouts to be ignored
1924
assert (ignore_counter >= 0);
1925
if (ignore_counter > 0)
1931
/* Calculate the picture size, and synchronize screen buffer pointers. */
1932
picture_size = gms_graphics_width * gms_graphics_height;
1933
off_screen = gms_graphics_off_screen;
1934
on_screen = gms_graphics_on_screen;
1937
* If we received a new picture, set up the local static variables for that
1938
* picture -- decide on gamma correction, convert the color palette, and
1939
* initialize the off_screen buffer to be the base picture.
1941
if (gms_graphics_new_picture)
1943
/* Initialize the off_screen buffer to be a copy of the base picture. */
1945
off_screen = gms_malloc (picture_size * sizeof (*off_screen));
1946
memcpy (off_screen, gms_graphics_bitmap,
1947
picture_size * sizeof (*off_screen));
1949
/* Note the buffer for freeing on cleanup. */
1950
gms_graphics_off_screen = off_screen;
1953
* If the picture is animated, apply the first animation frames now.
1954
* This is important, since they form an intrinsic part of the first
1955
* displayed image (in type2 animation cases, perhaps _all_ of the
1956
* first displayed image).
1958
if (gms_graphics_animated)
1960
gms_graphics_animate (off_screen,
1961
gms_graphics_width, gms_graphics_height);
1965
* Select a suitable gamma for the picture, taking care to use the
1966
* off-screen buffer.
1968
gms_graphics_current_gamma =
1969
gms_graphics_select_gamma (off_screen,
1971
gms_graphics_height,
1972
gms_graphics_palette);
1975
* Pre-convert all the picture palette colors into their corresponding
1978
gms_graphics_convert_palette (gms_graphics_palette,
1979
gms_graphics_current_gamma, palette);
1981
/* Save the color count for possible queries later. */
1982
gms_graphics_count_colors (off_screen,
1983
gms_graphics_width, gms_graphics_height,
1984
&gms_graphics_color_count, NULL);
1988
* For a new picture, or a repaint of a prior one, calculate new values for
1989
* the x and y offsets used to draw image points, and set the on-screen
1990
* buffer to an unused pixel value, in effect invalidating all on-screen
1991
* data. Also, reset the saved image scan coordinates so that we scan for
1992
* unpainted pixels from top left starting at layer zero, and clear the
1995
if (gms_graphics_new_picture || deferred_repaint)
1998
* Calculate the x and y offset to center the picture in the graphics
2001
gms_graphics_position_picture (gms_graphics_window,
2003
gms_graphics_width, gms_graphics_height,
2004
&x_offset, &y_offset);
2007
* Reset all on-screen pixels to an unused value, guaranteed not to
2008
* match any in a real picture. This forces all pixels to be repainted
2009
* on a buffer/on-screen comparison.
2012
on_screen = gms_malloc (picture_size * sizeof (*on_screen));
2013
memset (on_screen, GMS_GRAPHICS_UNUSED_PIXEL,
2014
picture_size * sizeof (*on_screen));
2016
/* Note the buffer for freeing on cleanup. */
2017
gms_graphics_on_screen = on_screen;
2020
* Assign new layers to the current image. This sorts colors by usage
2021
* and puts the most used colors in the lower layers. It also hands us
2022
* a count of pixels in each layer, useful for knowing when to stop
2023
* scanning for layers in the rendering loop.
2026
gms_graphics_assign_layers (off_screen, on_screen,
2027
gms_graphics_width, gms_graphics_height,
2028
layers, layer_usage);
2031
/* Clear the graphics window. */
2032
gms_graphics_clear_and_border (gms_graphics_window,
2035
gms_graphics_width, gms_graphics_height);
2037
/* Start a fresh picture rendering pass. */
2044
/* Clear the new picture and deferred repaint flags. */
2045
gms_graphics_new_picture = FALSE;
2046
deferred_repaint = FALSE;
2051
* Make a portion of an image pass, from lower to higher image layers,
2052
* scanning for invalidated pixels that are in the current image layer we
2053
* are painting. Each invalidated pixel gives rise to a region paint,
2054
* which equates to one Glk rectangle fill.
2056
* When the limit on regions is reached, save the current image pass layer
2057
* and coordinates, and yield control to the main game playing code by
2058
* returning. On the next call, pick up where we left off.
2060
* As an optimization, we can leave the loop on the first empty layer we
2061
* encounter. Since layers are ordered by complexity and color usage, all
2062
* layers higher than the first unused one will also be empty, so we don't
2063
* need to scan them.
2066
for (layer = saved_layer;
2067
layer < GMS_PALETTE_SIZE && layer_usage[layer] > 0; layer++)
2072
* As an optimization to avoid multiplications in the loop, maintain a
2073
* separate index row.
2075
index_row = saved_y * gms_graphics_width;
2076
for (y = saved_y; y < gms_graphics_height; y++)
2078
for (x = saved_x; x < gms_graphics_width; x++)
2082
/* Get the index for this pixel. */
2083
index = index_row + x;
2084
assert (index < picture_size * sizeof (*off_screen));
2087
* Ignore pixels not in the current layer, and pixels not
2088
* currently invalid (that is, ones whose on-screen represen-
2089
* tation matches the off-screen buffer).
2091
if (layers[off_screen[index]] == layer
2092
&& on_screen[index] != off_screen[index])
2095
* Rather than painting just one pixel, here we try to
2096
* paint the maximal region we can for the layer of the
2099
gms_graphics_paint_region (gms_graphics_window,
2101
off_screen, on_screen,
2102
x, y, x_offset, y_offset,
2105
gms_graphics_height);
2108
* Increment count of regions handled, and yield, by
2109
* returning, if the limit on paint regions is reached.
2110
* Before returning, save the current layer and scan
2111
* coordinates, so we can pick up here on the next call.
2114
if (regions >= GMS_REPAINT_LIMIT)
2117
saved_layer = layer;
2120
total_regions += regions;
2126
/* Reset the saved x coordinate on y increment. */
2129
/* Update the index row on change of y. */
2130
index_row += gms_graphics_width;
2133
/* Reset the saved y coordinate on layer change. */
2138
* If we reach this point, then we didn't get to the limit on regions
2139
* painted on this pass. In that case, we've finished rendering the
2142
assert (regions < GMS_REPAINT_LIMIT);
2143
total_regions += regions;
2146
gms_graphics_paint_everything
2147
(gms_graphics_window,
2148
palette, off_screen,
2151
gms_graphics_height);
2155
* If animated, and if animations are enabled, handle further animation
2158
if (gms_animation_enabled && gms_graphics_animated)
2163
* Reset the off-screen buffer to a copy of the base picture. This is
2164
* the correct state for applying animation frames.
2166
memcpy (off_screen, gms_graphics_bitmap,
2167
picture_size * sizeof (*off_screen));
2170
* Apply any further animations. If none, then stop the graphics
2171
* "thread" and return. There's no more to be done until something
2174
more_animation = gms_graphics_animate (off_screen,
2176
gms_graphics_height);
2177
if (!more_animation)
2180
* There's one extra wrinkle here. The base picture we've just put
2181
* into the off-screen buffer isn't really complete (and for type2
2182
* animations, might be pure garbage), so if we happen to get a
2183
* repaint after an animation has ended, the off-screen data we'll
2184
* be painting could well look wrong.
2186
* So... here we want to set the off-screen buffer to contain the
2187
* final animation frame. Fortunately, we still have it in the
2190
memcpy (off_screen, on_screen, picture_size * sizeof (*off_screen));
2191
gms_graphics_stop ();
2196
* Re-assign layers based on animation changes to the off-screen
2200
gms_graphics_assign_layers (off_screen, on_screen,
2201
gms_graphics_width, gms_graphics_height,
2202
layers, layer_usage);
2206
* Set up an animation wait, adjusted here by the number of times we
2207
* had to yield while rendering, as we're now that late with animations,
2208
* and capped at zero, as we can't do anything to compensate for being
2209
* too late. In practice, we're running too close to the edge to have
2210
* much of an effect here, but nevertheless...
2212
ignore_counter = GMS_GRAPHICS_ANIMATION_WAIT - 1;
2213
if (yield_counter > ignore_counter)
2216
ignore_counter -= yield_counter;
2218
/* Start a fresh picture rendering pass. */
2228
* Not an animated picture, so just stop graphics, as again, there's
2229
* no more to be done until something restarts us.
2231
gms_graphics_stop ();
2239
* Called by the main interpreter when it wants us to display a picture.
2240
* The function gets the picture bitmap, palette, and dimensions, and
2241
* saves them, and the picture id, in module variables for the background
2242
* rendering function.
2244
* The graphics window is opened if required, or closed if mode is zero.
2246
* The function checks for changes of actual picture by calculating the
2247
* CRC for picture data; this helps to prevent unnecessary repaints in
2248
* cases where the interpreter passes us the same picture as we're already
2249
* displaying. There is a less than 1 in 4,294,967,296 chance that a new
2250
* picture will be missed. We'll live with that.
2252
* Why use CRCs, rather than simply storing the values of picture passed in
2253
* a static variable? Because some games, typically Magnetic Windows, use
2254
* the picture argument as a form of string pointer, and can pass in the
2255
* same value for several, perhaps all, game pictures. If we just checked
2256
* for a change in the picture argument, we'd never see one. So we must
2257
* instead look for changes in the real picture data.
2260
ms_showpic (type32 picture, type8 mode)
2262
static glui32 current_crc = 0; /* CRC of the current picture */
2264
type8 *bitmap, animated;
2265
type16 width, height, palette[GMS_PALETTE_SIZE];
2269
/* See if the mode indicates no graphics. */
2272
/* Note that the interpreter turned graphics off. */
2273
gms_graphics_interpreter = FALSE;
2276
* If we are currently displaying the graphics window, stop any update
2277
* "thread" and turn off graphics.
2279
if (gms_graphics_enabled && gms_graphics_are_displayed ())
2281
gms_graphics_stop ();
2282
gms_graphics_close ();
2285
/* Nothing more to do now graphics are off. */
2289
/* Note that the interpreter turned graphics on. */
2290
gms_graphics_interpreter = TRUE;
2293
* Obtain the image details for the requested picture. The call returns
2294
* NULL if there's a problem with the picture.
2296
bitmap = ms_extract (picture, &width, &height, palette, &animated);
2300
/* Note the last thing passed to ms_extract, in case of graphics restarts. */
2301
gms_graphics_picture = picture;
2303
/* Calculate the picture size, and the CRC for the bitmap data. */
2304
picture_bytes = width * height * sizeof (*bitmap);
2305
crc = gms_get_buffer_crc (bitmap, picture_bytes);
2308
* If there is no change of picture, we might be able to largely ignore the
2309
* call. Check for a change, and if we don't see one, and if graphics are
2310
* enabled and being displayed, we can safely ignore the call.
2312
if (width == gms_graphics_width
2313
&& height == gms_graphics_height
2314
&& crc == current_crc
2315
&& gms_graphics_enabled && gms_graphics_are_displayed ())
2319
* We know now that this is either a genuine change of picture, or graphics
2320
* were off and have been turned on. So, record picture details, ensure
2321
* graphics is on, set the flags, and start the background graphics update.
2325
* Save the picture details for the update code. Here we take a complete
2326
* local copy of the bitmap, since the interpreter core may reuse part of
2327
* its memory for animations.
2329
free (gms_graphics_bitmap);
2330
gms_graphics_bitmap = gms_malloc (picture_bytes);
2331
memcpy (gms_graphics_bitmap, bitmap, picture_bytes);
2332
gms_graphics_width = width;
2333
gms_graphics_height = height;
2334
memcpy (gms_graphics_palette, palette, sizeof (palette));
2335
gms_graphics_animated = animated;
2337
/* Retain the new picture CRC. */
2341
* If graphics are enabled, ensure the window is displayed, set the
2342
* appropriate flags, and start graphics update. If they're not enabled,
2343
* the picture details will simply stick around in module variables until
2344
* they are required.
2346
if (gms_graphics_enabled)
2349
* Ensure graphics on, then set the new picture flag and start the
2350
* updating "thread".
2352
if (gms_graphics_open ())
2354
gms_graphics_new_picture = TRUE;
2355
gms_graphics_start ();
2362
* gms_graphics_picture_is_available()
2364
* Return TRUE if the graphics module data is loaded with a usable picture,
2365
* FALSE if there is no picture available to display.
2368
gms_graphics_picture_is_available (void)
2370
return gms_graphics_bitmap != NULL;
2375
* gms_graphics_get_picture_details()
2377
* Return the width, height, and animation flag of the currently loaded
2378
* picture. The function returns FALSE if no picture is loaded, otherwise
2379
* TRUE, with picture details in the return arguments.
2382
gms_graphics_get_picture_details (int *width, int *height, int *is_animated)
2384
if (gms_graphics_picture_is_available ())
2387
*width = gms_graphics_width;
2389
*height = gms_graphics_height;
2391
*is_animated = gms_graphics_animated;
2401
* gms_graphics_get_rendering_details()
2403
* Returns the current level of applied gamma correction, as a string, the
2404
* count of colors in the picture, and a flag indicating if graphics is
2405
* active (busy). The function return FALSE if graphics is not enabled or
2406
* if not being displayed, otherwise TRUE with the gamma, color count, and
2407
* active flag in the return arguments.
2409
* This function races with the graphics timeout, as it returns information
2410
* set up by the first timeout following a new picture. There's a very,
2411
* very small chance that it might win the race, in which case out-of-date
2412
* gamma and color count values are returned.
2415
gms_graphics_get_rendering_details (const char **gamma, int *color_count,
2418
if (gms_graphics_enabled && gms_graphics_are_displayed ())
2421
* Return the string representing the gamma correction. If racing
2422
* with timeouts, we might return the gamma for the last picture.
2426
assert (gms_graphics_current_gamma);
2427
*gamma = gms_graphics_current_gamma->level;
2431
* Return the color count noted by timeouts on the first timeout
2432
* following a new picture. Again, we might return the one for
2433
* the prior picture.
2436
*color_count = gms_graphics_color_count;
2438
/* Return graphics active flag. */
2440
*is_active = gms_graphics_active;
2450
* gms_graphics_interpreter_enabled()
2452
* Return TRUE if it looks like interpreter graphics are turned on, FALSE
2456
gms_graphics_interpreter_enabled (void)
2458
return gms_graphics_interpreter;
2463
* gms_graphics_cleanup()
2465
* Free memory resources allocated by graphics functions. Called on game
2469
gms_graphics_cleanup (void)
2471
free (gms_graphics_bitmap);
2472
gms_graphics_bitmap = NULL;
2473
free (gms_graphics_off_screen);
2474
gms_graphics_off_screen = NULL;
2475
free (gms_graphics_on_screen);
2476
gms_graphics_on_screen = NULL;
2478
gms_graphics_animated = FALSE;
2479
gms_graphics_picture = 0;
2483
/*---------------------------------------------------------------------*/
2484
/* Glk port status line functions */
2485
/*---------------------------------------------------------------------*/
2488
* The interpreter feeds us status line characters one at a time, with Tab
2489
* indicating right justify, and CR indicating the line is complete. To get
2490
* this to fit with the Glk event and redraw model, here we'll buffer each
2491
* completed status line, so we have a stable string to output when needed.
2492
* It's also handy to have this buffer for Glk libraries that don't support
2495
enum { GMS_STATBUFFER_LENGTH = 1024 };
2496
static char gms_status_buffer[GMS_STATBUFFER_LENGTH];
2497
static int gms_status_length = 0;
2499
/* Default width used for non-windowing Glk status lines. */
2500
static const int GMS_DEFAULT_STATUS_WIDTH = 74;
2506
* Receive one status character from the interpreter. Characters are
2507
* buffered internally, and on CR, the buffer is copied to the main static
2508
* status buffer for use by the status line printing function.
2511
ms_statuschar (type8 c)
2513
static char buffer[GMS_STATBUFFER_LENGTH];
2514
static int length = 0;
2517
* If the status character is newline, transfer locally buffered data to
2518
* the common buffer, empty the local buffer; otherwise, if space permits,
2519
* buffer the character.
2523
memcpy (gms_status_buffer, buffer, length);
2524
gms_status_length = length;
2530
if (length < sizeof (buffer))
2531
buffer[length++] = c;
2537
* gms_status_update()
2539
* Update the information in the status window with the current contents of
2540
* the completed status line buffer, or a default string if no completed
2544
gms_status_update (void)
2546
glui32 width, height;
2547
assert (gms_status_window);
2549
glk_window_get_size (gms_status_window, &width, &height);
2552
glk_window_clear (gms_status_window);
2553
glk_window_move_cursor (gms_status_window, 0, 0);
2554
glk_set_window (gms_status_window);
2556
if (gms_status_length > 0)
2561
* Output each character from the status line buffer. If the
2562
* character is Tab, position the cursor to eleven characters shy
2563
* of the status window right.
2565
for (index = 0; index < gms_status_length; index++)
2567
if (gms_status_buffer[index] == '\t')
2568
glk_window_move_cursor (gms_status_window, width - 11, 0);
2570
glk_put_char (gms_status_buffer[index]);
2575
const char *game_name;
2578
* We have no status line to display, so print the game's name, or
2579
* a standard message if unable to identify the game. Having no
2580
* status line is common with Magnetic Windows games, which don't,
2581
* in general, seem to use one.
2583
game_name = gms_gameid_get_game_name ();
2584
glk_put_string (game_name ? (char *) game_name
2585
: "Glk Magnetic version 2.3");
2588
glk_set_window (gms_main_window);
2594
* gms_status_print()
2596
* Print the current contents of the completed status line buffer out in the
2597
* main window, if it has changed since the last call. This is for non-
2598
* windowing Glk libraries.
2601
gms_status_print (void)
2603
static char buffer[GMS_STATBUFFER_LENGTH];
2604
static int length = 0;
2609
* Do nothing if there is no status line to print, or if the status
2610
* line hasn't changed since last printed.
2612
if (gms_status_length == 0
2613
|| (gms_status_length == length
2614
&& strncmp (buffer, gms_status_buffer, length)) == 0)
2617
/* Set fixed width font to try to preserve status line formatting. */
2618
glk_set_style (style_Preformatted);
2620
/* Bracket, and output the status line buffer. */
2621
glk_put_string ("[ ");
2623
for (index = 0; index < gms_status_length; index++)
2626
* If the character is Tab, position the cursor to eleven characters
2627
* shy of the right edge. In the absence of the real window dimensions,
2628
* we'll select 74 characters, which gives us a 78 character status
2629
* line; pretty standard.
2631
if (gms_status_buffer[index] == '\t')
2633
while (column <= GMS_DEFAULT_STATUS_WIDTH - 11)
2641
glk_put_char (gms_status_buffer[index]);
2646
while (column <= GMS_DEFAULT_STATUS_WIDTH)
2651
glk_put_string (" ]\n");
2653
/* Save the details of the printed status buffer. */
2654
memcpy (buffer, gms_status_buffer, gms_status_length);
2655
length = gms_status_length;
2660
* gms_status_notify()
2662
* Front end function for updating status. Either updates the status window
2663
* or prints the status line to the main window.
2666
gms_status_notify (void)
2668
if (gms_status_window)
2669
gms_status_update ();
2671
gms_status_print ();
2676
* gms_status_redraw()
2678
* Redraw the contents of any status window with the buffered status string.
2679
* This function should be called on the appropriate Glk window resize and
2683
gms_status_redraw (void)
2685
if (gms_status_window)
2690
* Rearrange the status window, without changing its actual arrangement
2691
* in any way. This is a hack to work round incorrect window repainting
2692
* in Xglk; it forces a complete repaint of affected windows on Glk
2693
* window resize and arrange events, and works in part because Xglk
2694
* doesn't check for actual arrangement changes in any way before
2695
* invalidating its windows. The hack should be harmless to Glk
2696
* libraries other than Xglk, moreover, we're careful to activate it
2697
* only on resize and arrange events.
2699
parent = glk_window_get_parent (gms_status_window);
2700
glk_window_set_arrangement (parent,
2701
winmethod_Above | winmethod_Fixed, 1, NULL);
2703
gms_status_update ();
2708
/*---------------------------------------------------------------------*/
2709
/* Glk port output functions */
2710
/*---------------------------------------------------------------------*/
2713
* Flag for if the user entered "help" as their last input, or if hints have
2714
* been silenced as a result of already using a Glk command.
2716
static int gms_help_requested = FALSE,
2717
gms_help_hints_silenced = FALSE;
2720
* Output buffer. We receive characters one at a time, and it's a bit
2721
* more efficient for everyone if we buffer them, and output a complete
2722
* string on a flush call.
2724
static char *gms_output_buffer = NULL;
2725
static int gms_output_allocation = 0,
2726
gms_output_length = 0;
2729
* Flag to indicate if the last buffer flushed looked like it ended in a
2732
static int gms_output_prompt = FALSE;
2736
* gms_output_register_help_request()
2737
* gms_output_silence_help_hints()
2738
* gms_output_provide_help_hint()
2740
* Register a request for help, and print a note of how to get Glk command
2741
* help from the interpreter unless silenced.
2744
gms_output_register_help_request (void)
2746
gms_help_requested = TRUE;
2750
gms_output_silence_help_hints (void)
2752
gms_help_hints_silenced = TRUE;
2756
gms_output_provide_help_hint (void)
2758
if (gms_help_requested && !gms_help_hints_silenced)
2760
glk_set_style (style_Emphasized);
2761
glk_put_string ("[Try 'glk help' for help on special interpreter"
2764
gms_help_requested = FALSE;
2765
glk_set_style (style_Normal);
2771
* gms_game_prompted()
2773
* Return TRUE if the last game output appears to have been a ">" prompt.
2774
* Once called, the flag is reset to FALSE, and requires more game output
2778
gms_game_prompted (void)
2782
result = gms_output_prompt;
2783
gms_output_prompt = FALSE;
2790
* gms_detect_game_prompt()
2792
* See if the last non-newline-terminated line in the output buffer seems
2793
* to be a prompt, and set the game prompted flag if it does, otherwise
2797
gms_detect_game_prompt (void)
2801
gms_output_prompt = FALSE;
2804
* Search for a prompt across any last unterminated buffered line; a prompt
2805
* is any non-space character on that line.
2807
for (index = gms_output_length - 1;
2808
index >= 0 && gms_output_buffer[index] != '\n'; index--)
2810
if (gms_output_buffer[index] != ' ')
2812
gms_output_prompt = TRUE;
2820
* gms_output_delete()
2822
* Delete all buffered output text. Free all malloc'ed buffer memory, and
2823
* return the buffer variables to their initial values.
2826
gms_output_delete (void)
2828
free (gms_output_buffer);
2829
gms_output_buffer = NULL;
2830
gms_output_allocation = gms_output_length = 0;
2835
* gms_output_flush()
2837
* Flush any buffered output text to the Glk main window, and clear the
2841
gms_output_flush (void)
2843
assert (glk_stream_get_current ());
2845
if (gms_output_length > 0)
2848
* See if the game issued a standard prompt, then print the buffer to
2849
* the main window. If providing a help hint, position that before
2850
* the game's prompt (if any).
2852
gms_detect_game_prompt ();
2853
glk_set_style (style_Normal);
2855
if (gms_output_prompt)
2859
for (index = gms_output_length - 1;
2860
index >= 0 && gms_output_buffer[index] != '\n'; )
2863
glk_put_buffer (gms_output_buffer, index + 1);
2864
gms_output_provide_help_hint ();
2865
glk_put_buffer (gms_output_buffer + index + 1,
2866
gms_output_length - index - 1);
2870
glk_put_buffer (gms_output_buffer, gms_output_length);
2871
gms_output_provide_help_hint ();
2874
gms_output_delete ();
2882
* Buffer a character for eventual printing to the main window.
2885
ms_putchar (type8 c)
2888
assert (gms_output_length <= gms_output_allocation);
2891
* See if the character is a backspace. Magnetic Scrolls games can send
2892
* backspace characters to the display. We'll need to handle such
2893
* characters specially, by taking the last character out of the buffer.
2897
if (gms_output_length > 0)
2898
gms_output_length--;
2903
/* Grow the output buffer if necessary, then add the character. */
2904
for (bytes = gms_output_allocation; bytes < gms_output_length + 1; )
2905
bytes = bytes == 0 ? 1 : bytes << 1;
2907
if (bytes > gms_output_allocation)
2909
gms_output_buffer = gms_realloc (gms_output_buffer, bytes);
2910
gms_output_allocation = bytes;
2913
gms_output_buffer[gms_output_length++] = c;
2918
* gms_styled_string()
2920
* gms_standout_string()
2921
* gms_standout_char()
2922
* gms_normal_string()
2924
* gms_header_string()
2925
* gms_banner_string()
2927
* Convenience functions to print strings in assorted styles. A standout
2928
* string is one that hints that it's from the interpreter, not the game.
2931
gms_styled_string (glui32 style, const char *message)
2935
glk_set_style (style);
2936
glk_put_string ((char *) message);
2937
glk_set_style (style_Normal);
2941
gms_styled_char (glui32 style, char c)
2947
gms_styled_string (style, buffer);
2951
gms_standout_string (const char *message)
2953
gms_styled_string (style_Emphasized, message);
2957
gms_standout_char (char c)
2959
gms_styled_char (style_Emphasized, c);
2963
gms_normal_string (const char *message)
2965
gms_styled_string (style_Normal, message);
2969
gms_normal_char (char c)
2971
gms_styled_char (style_Normal, c);
2975
gms_header_string (const char *message)
2977
gms_styled_string (style_Header, message);
2981
gms_banner_string (const char *message)
2983
gms_styled_string (style_Subheader, message);
2990
* Handle fatal interpreter error message.
2993
ms_fatal (type8s * string)
2995
gms_status_notify ();
2996
gms_output_flush ();
3006
* Handle a core interpreter call to flush the output buffer. Because Glk
3007
* only flushes its buffers and displays text on glk_select(), we can ignore
3008
* these calls as long as we call gms_output_flush() when reading line input.
3010
* Taking ms_flush() at face value can cause game text to appear before status
3011
* line text where we are working with a non-windowing Glk, so it's best
3012
* ignored where we can.
3020
/*---------------------------------------------------------------------*/
3021
/* Glk port hint functions */
3022
/*---------------------------------------------------------------------*/
3024
/* Hint type definitions. */
3026
GMS_HINT_TYPE_FOLDER = 1,
3027
GMS_HINT_TYPE_TEXT = 2
3030
/* Success and fail return codes from hint functions. */
3031
static const type8 GMS_HINT_SUCCESS = 1,
3034
/* Default window sizes for non-windowing Glk libraries. */
3035
static const glui32 GMS_HINT_DEFAULT_WIDTH = 72,
3036
GMS_HINT_DEFAULT_HEIGHT = 25;
3039
* Special hint nodes indicating the root hint node, and a value to signal
3040
* quit from hints subsystem.
3042
static const type16 GMS_HINT_ROOT_NODE = 0,
3043
GMS_HINTS_DONE = USHRT_MAX;
3045
/* Generic hint topic for the root hints node. */
3046
static const char * const GMS_GENERIC_TOPIC = "Hints Menu";
3049
* Note of the interpreter's hints array. Note that keeping its address
3050
* like this assumes that it's either static or heap in the interpreter.
3052
static struct ms_hint *gms_hints = NULL;
3054
/* Details of the current hint node on display from the hints array. */
3055
static type16 gms_current_hint_node = 0;
3058
* Array of cursors for each hint. The cursor indicates the current hint
3059
* position in a folder, and the last hint shown in text hints. Space
3060
* is allocated as needed for a given set of hints, and needs to be freed
3061
* on interpreter exit.
3063
static int *gms_hint_cursor = NULL;
3067
* gms_get_hint_max_node()
3069
* Return the maximum hint node referred to by the tree under the given node.
3070
* The result is the largest index found, or node, if greater. Because the
3071
* interpreter doesn't supply it, we need to uncover it the hard way. The
3072
* function is recursive, and since it is a tree search, assumes that hints
3073
* is a tree, not a graph.
3076
gms_get_hint_max_node (const struct ms_hint hints[], type16 node)
3078
const struct ms_hint *hint;
3083
hint = hints + node;
3086
switch (hint->nodetype)
3088
case GMS_HINT_TYPE_TEXT:
3091
case GMS_HINT_TYPE_FOLDER:
3093
* Recursively find the maximum node reference for each link, and keep
3094
* the largest value found.
3096
for (index = 0; index < hint->elcount; index++)
3100
link_max = gms_get_hint_max_node (hints, hint->links[index]);
3101
if (link_max > max_node)
3102
max_node = link_max;
3107
gms_fatal ("GLK: Invalid hints node type encountered");
3112
* Return the largest node reference found, capped to avoid overlapping the
3113
* special end-hints value.
3115
return max_node < GMS_HINTS_DONE ? max_node : GMS_HINTS_DONE - 1;
3120
* gms_get_hint_content()
3122
* Return the content string for a given hint number within a given node.
3123
* This counts over 'number' ASCII NULs in the node's content, returning
3124
* the address of the string located this way.
3127
gms_get_hint_content (const struct ms_hint hints[], type16 node, int number)
3129
const struct ms_hint *hint;
3133
hint = hints + node;
3135
/* Run through content until 'number' strings found. */
3137
for (index = 0; index < number; index++)
3138
offset += strlen (hint->content + offset) + 1;
3140
/* Return the start of the number'th string encountered. */
3141
return hint->content + offset;
3146
* gms_get_hint_topic()
3148
* Return the topic string for a given hint node. This is found by searching
3149
* the parent node for a link to the node handed in. For the root node, the
3150
* string is defaulted, since the root node has no parent.
3153
gms_get_hint_topic (const struct ms_hint hints[], type16 node)
3157
if (node == GMS_HINT_ROOT_NODE)
3159
/* If the node is the root node, return a generic string. */
3160
return GMS_GENERIC_TOPIC;
3169
* Search the parent for a link to node, and use that as the hint topic;
3170
* NULL if none found.
3172
parent = hints[node].parent;
3175
for (index = 0; index < hints[parent].elcount; index++)
3177
if (hints[parent].links[index] == node)
3179
topic = gms_get_hint_content (hints, parent, index);
3184
return topic ? topic : GMS_GENERIC_TOPIC;
3192
* If not already open, open the hints windows. Returns TRUE if the windows
3193
* opened, or were already open.
3195
* The function creates two hints windows -- a text grid on top, for menus,
3196
* and a text buffer below for hints.
3199
gms_hint_open (void)
3201
if (!gms_hint_menu_window)
3203
assert (!gms_hint_text_window);
3206
* Open the hint menu window. The initial size is two lines, but we'll
3207
* change this later to suit the hint.
3209
gms_hint_menu_window = glk_window_open (gms_main_window,
3210
winmethod_Above | winmethod_Fixed,
3211
2, wintype_TextGrid, 0);
3212
if (!gms_hint_menu_window)
3216
* Now open the hints text window. This is set to be 100% of the size
3217
* of the main window, so should cover what remains of it completely.
3219
gms_hint_text_window = glk_window_open (gms_main_window,
3221
| winmethod_Proportional,
3222
100, wintype_TextBuffer, 0);
3223
if (!gms_hint_text_window)
3225
glk_window_close (gms_hint_menu_window, NULL);
3226
gms_hint_menu_window = NULL;
3238
* If open, close the hints windows.
3241
gms_hint_close (void)
3243
if (gms_hint_menu_window)
3245
assert (gms_hint_text_window);
3247
glk_window_close (gms_hint_menu_window, NULL);
3248
gms_hint_menu_window = NULL;
3249
glk_window_close (gms_hint_text_window, NULL);
3250
gms_hint_text_window = NULL;
3256
* gms_hint_windows_available()
3258
* Return TRUE if hints windows are available. If they're not, the hints
3259
* system will need to use alternative output methods.
3262
gms_hint_windows_available (void)
3264
return (gms_hint_menu_window && gms_hint_text_window);
3269
* gms_hint_menu_print()
3270
* gms_hint_menu_header()
3271
* gms_hint_menu_justify()
3272
* gms_hint_text_print()
3273
* gms_hint_menutext_done()
3274
* gms_hint_menutext_start()
3276
* Output functions for writing hints. These functions will write to hints
3277
* windows where available, and to the main window where not. When writing
3278
* to hints windows, they also take care not to line wrap in the menu window.
3279
* Limited formatting is available.
3282
gms_hint_menu_print (int line, int column, const char *string,
3283
glui32 width, glui32 height)
3287
/* Ignore the call if the text position is outside the window. */
3288
if (!(line > height || column > width))
3290
if (gms_hint_windows_available ())
3294
glk_window_move_cursor (gms_hint_menu_window, column, line);
3295
glk_set_window (gms_hint_menu_window);
3297
/* Write until the end of the string, or the end of the window. */
3298
for (posn = column, index = 0;
3299
posn < width && index < strlen (string); posn++, index++)
3301
glk_put_char (string[index]);
3304
glk_set_window (gms_main_window);
3308
static int current_line = 0; /* Retained line number */
3309
static int current_column = 0; /* Retained col number */
3314
* Check the line number against the last one output. If it is less,
3315
* assume the start of a new block. In this case, perform a hokey
3316
* type of screen clear.
3318
if (line < current_line)
3320
for (index = 0; index < height; index++)
3321
gms_normal_char ('\n');
3327
/* Print blank lines until the target line is reached. */
3328
for (; current_line < line; current_line++)
3330
gms_normal_char ('\n');
3334
/* Now print spaces until the target column is reached. */
3335
for (; current_column < column; current_column++)
3336
gms_normal_char (' ');
3339
* Write characters until the end of the string, or the end of the
3340
* (self-imposed not-really-there) window.
3343
current_column < width && index < strlen (string);
3344
current_column++, index++)
3346
gms_normal_char (string[index]);
3353
gms_hint_menu_header (int line, const char *string,
3354
glui32 width, glui32 height)
3359
/* Output the text in the approximate line center. */
3360
length = strlen (string);
3361
posn = length < width ? (width - length) / 2 : 0;
3362
gms_hint_menu_print (line, posn, string, width, height);
3366
gms_hint_menu_justify (int line,
3367
const char *left_string, const char *right_string,
3368
glui32 width, glui32 height)
3371
assert (left_string && right_string);
3373
/* Write left text normally to window left. */
3374
gms_hint_menu_print (line, 0, left_string, width, height);
3376
/* Output the right text flush with the right of the window. */
3377
length = strlen (right_string);
3378
posn = length < width ? width - length : 0;
3379
gms_hint_menu_print (line, posn, right_string, width, height);
3383
gms_hint_text_print (const char *string)
3387
if (gms_hint_windows_available ())
3389
glk_set_window (gms_hint_text_window);
3390
glk_put_string ((char *) string);
3391
glk_set_window (gms_main_window);
3394
gms_normal_string (string);
3398
gms_hint_menutext_start (void)
3401
* Twiddle for non-windowing libraries; 'clear' the main window by writing
3402
* a null string at line 1, then a null string at line 0. This works
3403
* because we know the current output line in gms_hint_menu_print() is zero,
3404
* since we set it that way with gms_hint_menutext_done(), or if this is
3405
* the first call, then that's its initial value.
3407
if (!gms_hint_windows_available ())
3409
gms_hint_menu_print (1, 0, "",
3410
GMS_HINT_DEFAULT_WIDTH, GMS_HINT_DEFAULT_HEIGHT);
3411
gms_hint_menu_print (0, 0, "",
3412
GMS_HINT_DEFAULT_WIDTH, GMS_HINT_DEFAULT_HEIGHT);
3417
gms_hint_menutext_done (void)
3420
* Twiddle for non-windowing libraries; 'clear' the main window by writing
3421
* an empty string to line zero. For windowing Glk libraries, this function
3424
if (!gms_hint_windows_available ())
3426
gms_hint_menu_print (0, 0, "",
3427
GMS_HINT_DEFAULT_WIDTH, GMS_HINT_DEFAULT_HEIGHT);
3433
* gms_hint_menutext_char_event()
3435
* Request and return a character event from the hints windows. In practice,
3436
* this means either of the hints windows if available, or the main window
3440
gms_hint_menutext_char_event (event_t * event)
3444
if (gms_hint_windows_available ())
3446
glk_request_char_event (gms_hint_menu_window);
3447
glk_request_char_event (gms_hint_text_window);
3449
gms_event_wait (evtype_CharInput, event);
3450
assert (event->win == gms_hint_menu_window
3451
|| event->win == gms_hint_text_window);
3453
glk_cancel_char_event (gms_hint_menu_window);
3454
glk_cancel_char_event (gms_hint_text_window);
3458
glk_request_char_event (gms_main_window);
3459
gms_event_wait (evtype_CharInput, event);
3465
* gms_hint_arrange_windows()
3467
* Arrange the hints windows so that the hint menu window has the requested
3468
* number of lines. Returns the actual hint menu window width and height,
3469
* or defaults if no hints windows are available.
3472
gms_hint_arrange_windows (int requested_lines, glui32 * width, glui32 * height)
3474
if (gms_hint_windows_available ())
3478
/* Resize the hint menu window to fit the current hint. */
3479
parent = glk_window_get_parent (gms_hint_menu_window);
3480
glk_window_set_arrangement (parent,
3481
winmethod_Above | winmethod_Fixed,
3482
requested_lines, NULL);
3484
/* Measure, and return the size of the hint menu window. */
3485
glk_window_get_size (gms_hint_menu_window, width, height);
3487
/* Clear both the hint menu and the hint text window. */
3488
glk_window_clear (gms_hint_menu_window);
3489
glk_window_clear (gms_hint_text_window);
3494
* No hints windows, so default width and height. The hints output
3495
* functions will cope with this.
3498
*width = GMS_HINT_DEFAULT_WIDTH;
3500
*height = GMS_HINT_DEFAULT_HEIGHT;
3506
* gms_hint_display_folder()
3508
* Update the hints windows for the given folder hint node.
3511
gms_hint_display_folder (const struct ms_hint hints[],
3512
const int cursor[], type16 node)
3514
glui32 width, height;
3516
assert (hints && cursor);
3519
* Arrange windows to suit the hint folder. For a folder menu window we
3520
* use one line for each element, three for the controls, and two spacers,
3521
* making a total of five additional lines. Width and height receive the
3522
* actual menu window dimensions.
3524
gms_hint_arrange_windows (hints[node].elcount + 5, &width, &height);
3526
/* Paint in the menu header. */
3528
gms_hint_menu_header (line++,
3529
gms_get_hint_topic (hints, node),
3531
gms_hint_menu_justify (line++,
3532
" N = next subject ", " P = previous ",
3534
gms_hint_menu_justify (line++,
3535
" RETURN = read subject ",
3536
node == GMS_HINT_ROOT_NODE
3537
? " Q = resume game " : " Q = previous menu ",
3541
* Output a blank line, then the menu for the node's folder hint. The folder
3542
* text for the selected hint is preceded by a '>' pointer.
3545
for (index = 0; index < hints[node].elcount; index++)
3547
gms_hint_menu_print (line, 3,
3548
index == cursor[node] ? ">" : " ",
3550
gms_hint_menu_print (line++, 5,
3551
gms_get_hint_content (hints, node, index),
3556
* Terminate with a blank line; using a single space here improves cursor
3557
* positioning for optimized output libraries (for example, without it,
3558
* curses output will leave the cursor at the end of the previous line).
3560
gms_hint_menu_print (line, 0, " ", width, height);
3565
* gms_hint_display_text()
3567
* Update the hints windows for the given text hint node.
3570
gms_hint_display_text (const struct ms_hint hints[],
3571
const int cursor[], type16 node)
3573
glui32 width, height;
3575
assert (hints && cursor);
3578
* Arrange windows to suit the hint text. For a hint menu, we use a simple
3579
* two-line set of controls; everything else is in the hints text window.
3580
* Width and height receive the actual menu window dimensions.
3582
gms_hint_arrange_windows (2, &width, &height);
3584
/* Paint in a short menu header. */
3586
gms_hint_menu_header (line++,
3587
gms_get_hint_topic (hints, node),
3589
gms_hint_menu_justify (line++,
3590
" RETURN = read hint ", " Q = previous menu ",
3594
* Output hints to the hints text window. Hints not yet exposed are
3595
* indicated by the cursor for the hint, and are displayed as a dash.
3597
gms_hint_text_print ("\n");
3598
for (index = 0; index < hints[node].elcount; index++)
3602
sprintf (buffer, "%3d. ", index + 1);
3603
gms_hint_text_print (buffer);
3605
gms_hint_text_print (index < cursor[node]
3606
? gms_get_hint_content (hints, node, index) : "-");
3607
gms_hint_text_print ("\n");
3613
* gms_hint_display()
3615
* Display the given hint using the appropriate display function.
3618
gms_hint_display (const struct ms_hint hints[],
3619
const int cursor[], type16 node)
3621
assert (hints && cursor);
3623
switch (hints[node].nodetype)
3625
case GMS_HINT_TYPE_TEXT:
3626
gms_hint_display_text (hints, cursor, node);
3629
case GMS_HINT_TYPE_FOLDER:
3630
gms_hint_display_folder (hints, cursor, node);
3634
gms_fatal ("GLK: Invalid hints node type encountered");
3641
* gms_hint_handle_folder()
3643
* Handle a Glk keycode for the given folder hint. Return the next node to
3644
* handle, or the special end-hints on Quit at the root node.
3647
gms_hint_handle_folder (const struct ms_hint hints[],
3648
int cursor[], type16 node, glui32 keycode)
3650
unsigned char response;
3652
assert (hints && cursor);
3654
/* Convert key code into a single response character. */
3664
case keycode_Return:
3668
case keycode_Escape:
3672
response = keycode <= UCHAR_MAX ? glk_char_to_upper (keycode) : 0;
3677
* Now handle the response character. We'll default the next node to be
3678
* this node, but a response case can change it.
3684
/* Advance the hint cursor, wrapping at the folder end. */
3685
if (cursor[node] < hints[node].elcount - 1)
3692
/* Regress the hint cursor, wrapping at the folder start. */
3693
if (cursor[node] > 0)
3696
cursor[node] = hints[node].elcount - 1;
3700
/* The next node is the hint node at the cursor position. */
3701
next_node = hints[node].links[cursor[node]];
3705
/* If root, we're done; if not, next node is node's parent. */
3706
next_node = node == GMS_HINT_ROOT_NODE
3707
? GMS_HINTS_DONE : hints[node].parent;
3719
* gms_hint_handle_text()
3721
* Handle a Glk keycode for the given text hint. Return the next node to
3725
gms_hint_handle_text (const struct ms_hint hints[],
3726
int cursor[], type16 node, glui32 keycode)
3728
unsigned char response;
3730
assert (hints && cursor);
3732
/* Convert key code into a single response character. */
3736
case keycode_Return:
3740
case keycode_Escape:
3744
response = keycode <= UCHAR_MAX ? glk_char_to_upper (keycode) : 0;
3749
* Now handle the response character. We'll default the next node to be
3750
* this node, but a response case can change it.
3756
/* If not at end of the hint, advance the hint cursor. */
3757
if (cursor[node] < hints[node].elcount)
3762
/* Done with this hint node, so next node is its parent. */
3763
next_node = hints[node].parent;
3777
* Handle a Glk keycode for the given hint using the appropriate handler
3778
* function. Return the next node to handle.
3781
gms_hint_handle (const struct ms_hint hints[],
3782
int cursor[], type16 node, glui32 keycode)
3785
assert (hints && cursor);
3787
next_node = GMS_HINT_ROOT_NODE;
3788
switch (hints[node].nodetype)
3790
case GMS_HINT_TYPE_TEXT:
3791
next_node = gms_hint_handle_text (hints, cursor, node, keycode);
3794
case GMS_HINT_TYPE_FOLDER:
3795
next_node = gms_hint_handle_folder (hints, cursor, node, keycode);
3799
gms_fatal ("GLK: Invalid hints node type encountered");
3810
* Start game hints. These are modal, though there's no overriding Glk
3811
* reason why. It's just that this matches the way they're implemented by
3812
* most Inform games. This may not be the best way of doing help, but at
3813
* least it's likely to be familiar, and anything more ambitious may be
3814
* beyond the current Glk capabilities.
3816
* This function uses CRCs to detect any change of hints data. Normally,
3817
* we'd expect none, at least within a given game run, but we can probably
3818
* handle it okay if it happens.
3821
ms_showhints (struct ms_hint * hints)
3823
static int is_initialized = FALSE;
3824
static glui32 current_crc = 0;
3831
* Find the number of hints in the array. To do this, we'll visit every
3832
* node in a tree search, starting at the root, to locate the maximum node
3833
* number found, then add one to that. It's a pity that the interpreter
3834
* doesn't hand us this information directly.
3836
hint_count = gms_get_hint_max_node (hints, GMS_HINT_ROOT_NODE) + 1;
3839
* Calculate a CRC for the hints array data. If the CRC has changed, or
3840
* this is the first call, assign a new cursor array.
3842
crc = gms_get_buffer_crc (hints, hint_count * sizeof (*hints));
3843
if (crc != current_crc || !is_initialized)
3847
/* Allocate new cursors, and set all to zero initial state. */
3848
free (gms_hint_cursor);
3849
bytes = hint_count * sizeof (*gms_hint_cursor);
3850
gms_hint_cursor = gms_malloc (bytes);
3851
memset (gms_hint_cursor, 0, bytes);
3854
* Retain the hints CRC, for later comparisons, and set is_initialized
3858
is_initialized = TRUE;
3862
* Save the hints array passed in. This is done here since even if the data
3863
* remains the same (found by the CRC check above), the pointer to it might
3869
* Try to create the hints windows. If they can't be created, perhaps
3870
* because the Glk library doesn't support it, the output functions will
3874
gms_hint_menutext_start ();
3877
* Begin hints display at the root node, and navigate until the user exits
3880
gms_current_hint_node = GMS_HINT_ROOT_NODE;
3881
while (gms_current_hint_node != GMS_HINTS_DONE)
3885
assert (gms_current_hint_node < hint_count);
3886
gms_hint_display (gms_hints, gms_hint_cursor, gms_current_hint_node);
3888
/* Get and handle a character key event for hint navigation. */
3889
gms_hint_menutext_char_event (&event);
3890
assert (event.type == evtype_CharInput);
3891
gms_current_hint_node = gms_hint_handle (gms_hints,
3893
gms_current_hint_node,
3897
/* Done with hint windows. */
3898
gms_hint_menutext_done ();
3901
return GMS_HINT_SUCCESS;
3908
* Update the hints windows for the current hint. This function should be
3909
* called from the event handler on resize events, to repaint the hints
3910
* display. It does nothing if no hints windows have been opened, since
3911
* in this case, there's no resize action required -- either we're not in
3912
* the hints subsystem, or hints are being displayed in the main game
3913
* window, for whatever reason.
3916
gms_hint_redraw (void)
3918
if (gms_hint_windows_available ())
3920
assert (gms_hints && gms_hint_cursor);
3921
gms_hint_display (gms_hints, gms_hint_cursor, gms_current_hint_node);
3927
* gms_hints_cleanup()
3929
* Free memory resources allocated by hints functions. Called on game
3933
gms_hints_cleanup (void)
3935
free (gms_hint_cursor);
3936
gms_hint_cursor = NULL;
3939
gms_current_hint_node = 0;
3943
void ms_playmusic(type8 * midi_data, type32 length, type16 tempo)
3948
/*---------------------------------------------------------------------*/
3949
/* Glk command escape functions */
3950
/*---------------------------------------------------------------------*/
3953
* gms_command_undo()
3955
* Stub function for the undo command. The real work is to return the
3956
* undo code to the input functions.
3959
gms_command_undo (const char *argument)
3966
* gms_command_script()
3968
* Turn game output scripting (logging) on and off.
3971
gms_command_script (const char *argument)
3975
if (gms_strcasecmp (argument, "on") == 0)
3979
if (gms_transcript_stream)
3981
gms_normal_string ("Glk transcript is already on.\n");
3985
fileref = glk_fileref_create_by_prompt (fileusage_Transcript
3986
| fileusage_TextMode,
3987
filemode_WriteAppend, 0);
3990
gms_standout_string ("Glk transcript failed.\n");
3994
gms_transcript_stream = glk_stream_open_file (fileref,
3995
filemode_WriteAppend, 0);
3996
glk_fileref_destroy (fileref);
3997
if (!gms_transcript_stream)
3999
gms_standout_string ("Glk transcript failed.\n");
4003
glk_window_set_echo_stream (gms_main_window, gms_transcript_stream);
4005
gms_normal_string ("Glk transcript is now on.\n");
4008
else if (gms_strcasecmp (argument, "off") == 0)
4010
if (!gms_transcript_stream)
4012
gms_normal_string ("Glk transcript is already off.\n");
4016
glk_stream_close (gms_transcript_stream, NULL);
4017
gms_transcript_stream = NULL;
4019
glk_window_set_echo_stream (gms_main_window, NULL);
4021
gms_normal_string ("Glk transcript is now off.\n");
4024
else if (strlen (argument) == 0)
4026
gms_normal_string ("Glk transcript is ");
4027
gms_normal_string (gms_transcript_stream ? "on" : "off");
4028
gms_normal_string (".\n");
4033
gms_normal_string ("Glk transcript can be ");
4034
gms_standout_string ("on");
4035
gms_normal_string (", or ");
4036
gms_standout_string ("off");
4037
gms_normal_string (".\n");
4043
* gms_command_inputlog()
4045
* Turn game input logging on and off.
4048
gms_command_inputlog (const char *argument)
4052
if (gms_strcasecmp (argument, "on") == 0)
4056
if (gms_inputlog_stream)
4058
gms_normal_string ("Glk input logging is already on.\n");
4062
fileref = glk_fileref_create_by_prompt (fileusage_InputRecord
4063
| fileusage_BinaryMode,
4064
filemode_WriteAppend, 0);
4067
gms_standout_string ("Glk input logging failed.\n");
4071
gms_inputlog_stream = glk_stream_open_file (fileref,
4072
filemode_WriteAppend, 0);
4073
glk_fileref_destroy (fileref);
4074
if (!gms_inputlog_stream)
4076
gms_standout_string ("Glk input logging failed.\n");
4080
gms_normal_string ("Glk input logging is now on.\n");
4083
else if (gms_strcasecmp (argument, "off") == 0)
4085
if (!gms_inputlog_stream)
4087
gms_normal_string ("Glk input logging is already off.\n");
4091
glk_stream_close (gms_inputlog_stream, NULL);
4092
gms_inputlog_stream = NULL;
4094
gms_normal_string ("Glk input log is now off.\n");
4097
else if (strlen (argument) == 0)
4099
gms_normal_string ("Glk input logging is ");
4100
gms_normal_string (gms_inputlog_stream ? "on" : "off");
4101
gms_normal_string (".\n");
4106
gms_normal_string ("Glk input logging can be ");
4107
gms_standout_string ("on");
4108
gms_normal_string (", or ");
4109
gms_standout_string ("off");
4110
gms_normal_string (".\n");
4116
* gms_command_readlog()
4118
* Set the game input log, to read input from a file.
4121
gms_command_readlog (const char *argument)
4125
if (gms_strcasecmp (argument, "on") == 0)
4129
if (gms_readlog_stream)
4131
gms_normal_string ("Glk read log is already on.\n");
4135
fileref = glk_fileref_create_by_prompt (fileusage_InputRecord
4136
| fileusage_BinaryMode,
4140
gms_standout_string ("Glk read log failed.\n");
4144
if (!glk_fileref_does_file_exist (fileref))
4146
glk_fileref_destroy (fileref);
4147
gms_standout_string ("Glk read log failed.\n");
4151
gms_readlog_stream = glk_stream_open_file (fileref, filemode_Read, 0);
4152
glk_fileref_destroy (fileref);
4153
if (!gms_readlog_stream)
4155
gms_standout_string ("Glk read log failed.\n");
4159
gms_normal_string ("Glk read log is now on.\n");
4162
else if (gms_strcasecmp (argument, "off") == 0)
4164
if (!gms_readlog_stream)
4166
gms_normal_string ("Glk read log is already off.\n");
4170
glk_stream_close (gms_readlog_stream, NULL);
4171
gms_readlog_stream = NULL;
4173
gms_normal_string ("Glk read log is now off.\n");
4176
else if (strlen (argument) == 0)
4178
gms_normal_string ("Glk read log is ");
4179
gms_normal_string (gms_readlog_stream ? "on" : "off");
4180
gms_normal_string (".\n");
4185
gms_normal_string ("Glk read log can be ");
4186
gms_standout_string ("on");
4187
gms_normal_string (", or ");
4188
gms_standout_string ("off");
4189
gms_normal_string (".\n");
4195
* gms_command_abbreviations()
4197
* Turn abbreviation expansions on and off.
4200
gms_command_abbreviations (const char *argument)
4204
if (gms_strcasecmp (argument, "on") == 0)
4206
if (gms_abbreviations_enabled)
4208
gms_normal_string ("Glk abbreviation expansions are already on.\n");
4212
gms_abbreviations_enabled = TRUE;
4213
gms_normal_string ("Glk abbreviation expansions are now on.\n");
4216
else if (gms_strcasecmp (argument, "off") == 0)
4218
if (!gms_abbreviations_enabled)
4220
gms_normal_string ("Glk abbreviation expansions are already off.\n");
4224
gms_abbreviations_enabled = FALSE;
4225
gms_normal_string ("Glk abbreviation expansions are now off.\n");
4228
else if (strlen (argument) == 0)
4230
gms_normal_string ("Glk abbreviation expansions are ");
4231
gms_normal_string (gms_abbreviations_enabled ? "on" : "off");
4232
gms_normal_string (".\n");
4237
gms_normal_string ("Glk abbreviation expansions can be ");
4238
gms_standout_string ("on");
4239
gms_normal_string (", or ");
4240
gms_standout_string ("off");
4241
gms_normal_string (".\n");
4247
* gms_command_graphics()
4249
* Enable or disable graphics more permanently than is done by the main
4250
* interpreter. Also, print out a few brief details about the graphics
4251
* state of the program.
4254
gms_command_graphics (const char *argument)
4258
if (!gms_graphics_possible)
4260
gms_normal_string ("Glk graphics are not available.\n");
4264
if (gms_strcasecmp (argument, "on") == 0)
4266
if (gms_graphics_enabled)
4268
gms_normal_string ("Glk graphics are already on.\n");
4272
gms_graphics_enabled = TRUE;
4274
/* If a picture is loaded, call the restart function to repaint it. */
4275
if (gms_graphics_picture_is_available ())
4277
if (!gms_graphics_open ())
4279
gms_normal_string ("Glk graphics error.\n");
4282
gms_graphics_restart ();
4285
gms_normal_string ("Glk graphics are now on.\n");
4288
else if (gms_strcasecmp (argument, "off") == 0)
4290
if (!gms_graphics_enabled)
4292
gms_normal_string ("Glk graphics are already off.\n");
4297
* Set graphics to disabled, and stop any graphics processing. Close
4298
* the graphics window.
4300
gms_graphics_enabled = FALSE;
4301
gms_graphics_stop ();
4302
gms_graphics_close ();
4304
gms_normal_string ("Glk graphics are now off.\n");
4307
else if (strlen (argument) == 0)
4309
gms_normal_string ("Glk graphics are available,");
4310
gms_normal_string (gms_graphics_enabled
4311
? " and enabled.\n" : " but disabled.\n");
4313
if (gms_graphics_picture_is_available ())
4315
int width, height, is_animated;
4317
if (gms_graphics_get_picture_details (&width, &height, &is_animated))
4321
gms_normal_string ("There is ");
4322
gms_normal_string (is_animated ? "an animated" : "a");
4323
gms_normal_string (" picture loaded, ");
4325
sprintf (buffer, "%d", width);
4326
gms_normal_string (buffer);
4327
gms_normal_string (" by ");
4329
sprintf (buffer, "%d", height);
4330
gms_normal_string (buffer);
4332
gms_normal_string (" pixels.\n");
4336
if (!gms_graphics_interpreter_enabled ())
4337
gms_normal_string ("Interpreter graphics are disabled.\n");
4339
if (gms_graphics_enabled && gms_graphics_are_displayed ())
4341
int color_count, is_active;
4344
if (gms_graphics_get_rendering_details (&gamma, &color_count,
4349
gms_normal_string ("Graphics are ");
4350
gms_normal_string (is_active ? "active, " : "displayed, ");
4352
sprintf (buffer, "%d", color_count);
4353
gms_normal_string (buffer);
4354
gms_normal_string (" colours");
4356
if (gms_gamma_mode == GAMMA_OFF)
4357
gms_normal_string (", without gamma correction");
4360
gms_normal_string (", with gamma ");
4361
gms_normal_string (gamma);
4362
gms_normal_string (" correction");
4364
gms_normal_string (".\n");
4367
gms_normal_string ("Graphics are being displayed.\n");
4370
if (gms_graphics_enabled && !gms_graphics_are_displayed ())
4371
gms_normal_string ("Graphics are not being displayed.\n");
4376
gms_normal_string ("Glk graphics can be ");
4377
gms_standout_string ("on");
4378
gms_normal_string (", or ");
4379
gms_standout_string ("off");
4380
gms_normal_string (".\n");
4386
* gms_command_gamma()
4388
* Enable or disable picture gamma corrections.
4391
gms_command_gamma (const char *argument)
4395
if (!gms_graphics_possible)
4397
gms_normal_string ("Glk automatic gamma correction is not available.\n");
4401
if (gms_strcasecmp (argument, "high") == 0)
4403
if (gms_gamma_mode == GAMMA_HIGH)
4405
gms_normal_string ("Glk automatic gamma correction mode is"
4406
" already 'high'.\n");
4410
gms_gamma_mode = GAMMA_HIGH;
4411
gms_graphics_restart ();
4413
gms_normal_string ("Glk automatic gamma correction mode is"
4417
else if (gms_strcasecmp (argument, "normal") == 0
4418
|| gms_strcasecmp (argument, "on") == 0)
4420
if (gms_gamma_mode == GAMMA_NORMAL)
4422
gms_normal_string ("Glk automatic gamma correction mode is"
4423
" already 'normal'.\n");
4427
gms_gamma_mode = GAMMA_NORMAL;
4428
gms_graphics_restart ();
4430
gms_normal_string ("Glk automatic gamma correction mode is"
4431
" now 'normal'.\n");
4434
else if (gms_strcasecmp (argument, "none") == 0
4435
|| gms_strcasecmp (argument, "off") == 0)
4437
if (gms_gamma_mode == GAMMA_OFF)
4439
gms_normal_string ("Glk automatic gamma correction mode is"
4440
" already 'off'.\n");
4444
gms_gamma_mode = GAMMA_OFF;
4445
gms_graphics_restart ();
4447
gms_normal_string ("Glk automatic gamma correction mode is"
4451
else if (strlen (argument) == 0)
4453
gms_normal_string ("Glk automatic gamma correction mode is '");
4454
switch (gms_gamma_mode)
4457
gms_normal_string ("off");
4460
gms_normal_string ("normal");
4463
gms_normal_string ("high");
4466
gms_normal_string ("'.\n");
4471
gms_normal_string ("Glk automatic gamma correction mode can be ");
4472
gms_standout_string ("high");
4473
gms_normal_string (", ");
4474
gms_standout_string ("normal");
4475
gms_normal_string (", or ");
4476
gms_standout_string ("off");
4477
gms_normal_string (".\n");
4483
* gms_command_animations()
4485
* Enable or disable picture animations.
4488
gms_command_animations (const char *argument)
4492
if (!gms_graphics_possible)
4494
gms_normal_string ("Glk graphics animations are not available.\n");
4498
if (gms_strcasecmp (argument, "on") == 0)
4502
if (gms_animation_enabled)
4504
gms_normal_string ("Glk graphics animations are already on.\n");
4509
* Set animation to on, and restart graphics if the current picture
4510
* is animated; if it isn't, we can leave it displayed as is, since
4511
* changing animation mode doesn't affect this picture.
4513
gms_animation_enabled = TRUE;
4514
if (gms_graphics_get_picture_details (NULL, NULL, &is_animated))
4517
gms_graphics_restart ();
4520
gms_normal_string ("Glk graphics animations are now on.\n");
4523
else if (gms_strcasecmp (argument, "off") == 0)
4527
if (!gms_animation_enabled)
4529
gms_normal_string ("Glk graphics animations are already off.\n");
4533
gms_animation_enabled = FALSE;
4534
if (gms_graphics_get_picture_details (NULL, NULL, &is_animated))
4537
gms_graphics_restart ();
4540
gms_normal_string ("Glk graphics animations are now off.\n");
4543
else if (strlen (argument) == 0)
4545
gms_normal_string ("Glk graphics animations are ");
4546
gms_normal_string (gms_animation_enabled ? "on" : "off");
4547
gms_normal_string (".\n");
4552
gms_normal_string ("Glk graphics animations can be ");
4553
gms_standout_string ("on");
4554
gms_normal_string (", or ");
4555
gms_standout_string ("off");
4556
gms_normal_string (".\n");
4562
* gms_command_prompts()
4564
* Turn the extra "> " prompt output on and off.
4567
gms_command_prompts (const char *argument)
4571
if (gms_strcasecmp (argument, "on") == 0)
4573
if (gms_prompt_enabled)
4575
gms_normal_string ("Glk extra prompts are already on.\n");
4579
gms_prompt_enabled = TRUE;
4580
gms_normal_string ("Glk extra prompts are now on.\n");
4582
/* Check for a game prompt to clear the flag. */
4583
gms_game_prompted ();
4586
else if (gms_strcasecmp (argument, "off") == 0)
4588
if (!gms_prompt_enabled)
4590
gms_normal_string ("Glk extra prompts are already off.\n");
4594
gms_prompt_enabled = FALSE;
4595
gms_normal_string ("Glk extra prompts are now off.\n");
4598
else if (strlen (argument) == 0)
4600
gms_normal_string ("Glk extra prompts are ");
4601
gms_normal_string (gms_prompt_enabled ? "on" : "off");
4602
gms_normal_string (".\n");
4607
gms_normal_string ("Glk extra prompts can be ");
4608
gms_standout_string ("on");
4609
gms_normal_string (", or ");
4610
gms_standout_string ("off");
4611
gms_normal_string (".\n");
4617
* gms_command_print_version_number()
4618
* gms_command_version()
4620
* Print out the Glk library version number.
4623
gms_command_print_version_number (glui32 version)
4627
sprintf (buffer, "%lu.%lu.%lu",
4628
(unsigned long) version >> 16,
4629
(unsigned long) (version >> 8) & 0xff,
4630
(unsigned long) version & 0xff);
4631
gms_normal_string (buffer);
4635
gms_command_version (const char *argument)
4640
gms_normal_string ("This is version ");
4641
gms_command_print_version_number (GMS_PORT_VERSION);
4642
gms_normal_string (" of the Glk Magnetic port.\n");
4644
version = glk_gestalt (gestalt_Version, 0);
4645
gms_normal_string ("The Glk library version is ");
4646
gms_command_print_version_number (version);
4647
gms_normal_string (".\n");
4652
* gms_command_commands()
4654
* Turn command escapes off. Once off, there's no way to turn them back on.
4655
* Commands must be on already to enter this function.
4658
gms_command_commands (const char *argument)
4662
if (gms_strcasecmp (argument, "on") == 0)
4664
gms_normal_string ("Glk commands are already on.\n");
4667
else if (gms_strcasecmp (argument, "off") == 0)
4669
gms_commands_enabled = FALSE;
4670
gms_normal_string ("Glk commands are now off.\n");
4673
else if (strlen (argument) == 0)
4675
gms_normal_string ("Glk commands are ");
4676
gms_normal_string (gms_commands_enabled ? "on" : "off");
4677
gms_normal_string (".\n");
4682
gms_normal_string ("Glk commands can be ");
4683
gms_standout_string ("on");
4684
gms_normal_string (", or ");
4685
gms_standout_string ("off");
4686
gms_normal_string (".\n");
4692
* gms_command_license()
4694
* Print licensing terms.
4697
gms_command_license (const char *argument)
4701
gms_normal_string ("This program is free software; you can redistribute it"
4702
" and/or modify it under the terms of version 2 of the"
4703
" GNU General Public License as published by the Free"
4704
" Software Foundation.\n\n");
4706
gms_normal_string ("This program is distributed in the hope that it will be"
4708
gms_standout_string ("WITHOUT ANY WARRANTY");
4709
gms_normal_string ("; without even the implied warranty of ");
4710
gms_standout_string ("MERCHANTABILITY");
4711
gms_normal_string (" or ");
4712
gms_standout_string ("FITNESS FOR A PARTICULAR PURPOSE");
4713
gms_normal_string (". See the GNU General Public License for more"
4716
gms_normal_string ("You should have received a copy of the GNU General"
4717
" Public License along with this program; if not, write"
4718
" to the Free Software Foundation, Inc., 59 Temple"
4719
" Place, Suite 330, Boston, MA 02111-1307 USA\n\n");
4721
gms_normal_string ("Please report any bugs, omissions, or misfeatures to ");
4722
gms_standout_string ("simon_baldwin@yahoo.com");
4723
gms_normal_string (".\n");
4727
/* Glk subcommands and handler functions. */
4728
typedef const struct
4730
const char * const command; /* Glk subcommand. */
4731
void (* const handler) (const char *argument); /* Subcommand handler. */
4732
const int takes_argument; /* Argument flag. */
4733
const int undo_return; /* "Undo" return value. */
4735
typedef gms_command_t *gms_commandref_t;
4737
static void gms_command_summary (const char *argument);
4738
static void gms_command_help (const char *argument);
4740
static gms_command_t GMS_COMMAND_TABLE[] = {
4741
{"summary", gms_command_summary, FALSE, FALSE},
4742
{"undo", gms_command_undo, FALSE, TRUE},
4743
{"script", gms_command_script, TRUE, FALSE},
4744
{"inputlog", gms_command_inputlog, TRUE, FALSE},
4745
{"readlog", gms_command_readlog, TRUE, FALSE},
4746
{"abbreviations", gms_command_abbreviations, TRUE, FALSE},
4747
{"graphics", gms_command_graphics, TRUE, FALSE},
4748
{"gamma", gms_command_gamma, TRUE, FALSE},
4749
{"animations", gms_command_animations, TRUE, FALSE},
4750
{"prompts", gms_command_prompts, TRUE, FALSE},
4751
{"version", gms_command_version, FALSE, FALSE},
4752
{"commands", gms_command_commands, TRUE, FALSE},
4753
{"license", gms_command_license, FALSE, FALSE},
4754
{"help", gms_command_help, TRUE, FALSE},
4755
{NULL, NULL, FALSE, FALSE}
4760
* gms_command_summary()
4762
* Report all current Glk settings.
4765
gms_command_summary (const char *argument)
4767
gms_commandref_t entry;
4771
* Call handlers that have status to report with an empty argument,
4772
* prompting each to print its current setting.
4774
for (entry = GMS_COMMAND_TABLE; entry->command; entry++)
4776
if (entry->handler == gms_command_summary
4777
|| entry->handler == gms_command_undo
4778
|| entry->handler == gms_command_license
4779
|| entry->handler == gms_command_help)
4782
entry->handler ("");
4788
* gms_command_help()
4790
* Document the available Glk commands.
4793
gms_command_help (const char *command)
4795
gms_commandref_t entry, matched;
4798
if (strlen (command) == 0)
4800
gms_normal_string ("Glk commands are");
4801
for (entry = GMS_COMMAND_TABLE; entry->command; entry++)
4803
gms_commandref_t next;
4806
gms_normal_string (next->command ? " " : " and ");
4807
gms_standout_string (entry->command);
4808
gms_normal_string (next->command ? "," : ".\n\n");
4811
gms_normal_string ("Glk commands may be abbreviated, as long as"
4812
" the abbreviation is unambiguous. Use ");
4813
gms_standout_string ("glk help");
4814
gms_normal_string (" followed by a Glk command name for help on that"
4820
for (entry = GMS_COMMAND_TABLE; entry->command; entry++)
4822
if (gms_strncasecmp (command, entry->command, strlen (command)) == 0)
4826
gms_normal_string ("The Glk command ");
4827
gms_standout_string (command);
4828
gms_normal_string (" is ambiguous. Try ");
4829
gms_standout_string ("glk help");
4830
gms_normal_string (" for more information.\n");
4838
gms_normal_string ("The Glk command ");
4839
gms_standout_string (command);
4840
gms_normal_string (" is not valid. Try ");
4841
gms_standout_string ("glk help");
4842
gms_normal_string (" for more information.\n");
4846
if (matched->handler == gms_command_summary)
4848
gms_normal_string ("Prints a summary of all the current Glk Magnetic"
4852
else if (matched->handler == gms_command_undo)
4854
gms_normal_string ("Undoes a single game turn.\n\nEquivalent to the"
4855
" standalone game 'undo' command.\n");
4858
else if (matched->handler == gms_command_script)
4860
gms_normal_string ("Logs the game's output to a file.\n\nUse ");
4861
gms_standout_string ("glk script on");
4862
gms_normal_string (" to begin logging game output, and ");
4863
gms_standout_string ("glk script off");
4864
gms_normal_string (" to end it. Glk Magnetic will ask you for a file"
4865
" when you turn scripts on.\n");
4868
else if (matched->handler == gms_command_inputlog)
4870
gms_normal_string ("Records the commands you type into a game.\n\nUse ");
4871
gms_standout_string ("glk inputlog on");
4872
gms_normal_string (", to begin recording your commands, and ");
4873
gms_standout_string ("glk inputlog off");
4874
gms_normal_string (" to turn off input logs. You can play back"
4875
" recorded commands into a game with the ");
4876
gms_standout_string ("glk readlog");
4877
gms_normal_string (" command.\n");
4880
else if (matched->handler == gms_command_readlog)
4882
gms_normal_string ("Plays back commands recorded with ");
4883
gms_standout_string ("glk inputlog on");
4884
gms_normal_string (".\n\nUse ");
4885
gms_standout_string ("glk readlog on");
4886
gms_normal_string (". Command play back stops at the end of the"
4887
" file. You can also play back commands from a"
4888
" text file created using any standard editor.\n");
4891
else if (matched->handler == gms_command_abbreviations)
4893
gms_normal_string ("Controls abbreviation expansion.\n\nGlk Magnetic"
4894
" automatically expands several standard single"
4895
" letter abbreviations for you; for example, \"x\""
4896
" becomes \"examine\". Use ");
4897
gms_standout_string ("glk abbreviations on");
4898
gms_normal_string (" to turn this feature on, and ");
4899
gms_standout_string ("glk abbreviations off");
4900
gms_normal_string (" to turn it off. While the feature is on, you"
4901
" can bypass abbreviation expansion for an"
4902
" individual game command by prefixing it with a"
4903
" single quote.\n");
4906
else if (matched->handler == gms_command_graphics)
4908
gms_normal_string ("Turns interpreter graphics on and off.\n\nUse ");
4909
gms_standout_string ("glk graphics on");
4910
gms_normal_string (" to enable interpreter graphics, and ");
4911
gms_standout_string ("glk graphics off");
4912
gms_normal_string (" to turn graphics off and close the graphics window."
4913
" This control works slightly differently to the"
4914
" 'graphics' command in Magnetic Windows and Magnetic"
4915
" Scrolls games themselves; the game's 'graphics'"
4916
" command may disable new images, but leave old ones"
4917
" displayed. For graphics to be displayed, they"
4918
" must be turned on in both the game and the"
4922
else if (matched->handler == gms_command_gamma)
4924
gms_normal_string ("Sets the level of automatic gamma correction applied"
4925
" to game graphics.\n\nUse ");
4926
gms_standout_string ("glk gamma normal");
4927
gms_normal_string (" to set moderate automatic colour contrast"
4929
gms_standout_string ("glk gamma high");
4930
gms_normal_string (" to set high automatic colour contrast correction,"
4932
gms_standout_string ("glk gamma off");
4933
gms_normal_string (" to turn off all automatic gamma correction.\n");
4936
else if (matched->handler == gms_command_animations)
4938
gms_normal_string ("Turns graphic animations on and off.\n\nUse ");
4939
gms_standout_string ("glk animation on");
4940
gms_normal_string (" to enable animations, or ");
4941
gms_standout_string ("glk animation off");
4942
gms_normal_string (" to turn animations off. Not all game graphics are"
4943
" animated, so this control works only on graphics"
4944
" that are animated. When animation is off, Glk"
4945
" Magnetic displays only the static portions of a"
4946
" game's pictures.\n");
4949
else if (matched->handler == gms_command_prompts)
4951
gms_normal_string ("Controls extra input prompting.\n\n"
4952
"Glk Magnetic can issue a replacement '>' input"
4953
" prompt if it detects that the game hasn't prompted"
4954
" after, say, an empty input line. Use ");
4955
gms_standout_string ("glk prompts on");
4956
gms_normal_string (" to turn this feature on, and ");
4957
gms_standout_string ("glk prompts off");
4958
gms_normal_string (" to turn it off.\n");
4961
else if (matched->handler == gms_command_version)
4963
gms_normal_string ("Prints the version numbers of the Glk library"
4964
" and the Glk Magnetic port.\n");
4967
else if (matched->handler == gms_command_commands)
4969
gms_normal_string ("Turn off Glk commands.\n\nUse ");
4970
gms_standout_string ("glk commands off");
4971
gms_normal_string (" to disable all Glk commands, including this one."
4972
" Once turned off, there is no way to turn Glk"
4973
" commands back on while inside the game.\n");
4976
else if (matched->handler == gms_command_license)
4978
gms_normal_string ("Prints Glk Magnetic's software license.\n");
4981
else if (matched->handler == gms_command_help)
4982
gms_command_help ("");
4985
gms_normal_string ("There is no help available on that Glk command."
4991
* gms_command_escape()
4993
* This function is handed each input line. If the line contains a specific
4994
* Glk port command, handle it and return TRUE, otherwise return FALSE.
4996
* On unambiguous returns, it will also set the value for undo_command to the
4997
* table undo return value.
5000
gms_command_escape (const char *string, int *undo_command)
5003
char *string_copy, *command, *argument;
5004
assert (string && undo_command);
5007
* Return FALSE if the string doesn't begin with the Glk command escape
5010
posn = strspn (string, "\t ");
5011
if (gms_strncasecmp (string + posn, "glk", strlen ("glk")) != 0)
5014
/* Take a copy of the string, without any leading space or introducer. */
5015
string_copy = gms_malloc (strlen (string + posn) + 1 - strlen ("glk"));
5016
strcpy (string_copy, string + posn + strlen ("glk"));
5019
* Find the subcommand; the first word in the string copy. Find its end,
5020
* and ensure it terminates with a NUL.
5022
posn = strspn (string_copy, "\t ");
5023
command = string_copy + posn;
5024
posn += strcspn (string_copy + posn, "\t ");
5025
if (string_copy[posn] != '\0')
5026
string_copy[posn++] = '\0';
5029
* Now find any argument data for the command, ensuring it too terminates
5032
posn += strspn (string_copy + posn, "\t ");
5033
argument = string_copy + posn;
5034
posn += strcspn (string_copy + posn, "\t ");
5035
string_copy[posn] = '\0';
5038
* Try to handle the command and argument as a Glk subcommand. If it
5039
* doesn't run unambiguously, print command usage. Treat an empty command
5042
if (strlen (command) > 0)
5044
gms_commandref_t entry, matched;
5048
* Search for the first unambiguous table command string matching
5049
* the command passed in.
5053
for (entry = GMS_COMMAND_TABLE; entry->command; entry++)
5055
if (gms_strncasecmp (command, entry->command, strlen (command)) == 0)
5062
/* If the match was unambiguous, call the command handler. */
5065
if (!matched->undo_return)
5066
gms_normal_char ('\n');
5067
matched->handler (argument);
5069
if (!matched->takes_argument && strlen (argument) > 0)
5071
gms_normal_string ("[The ");
5072
gms_standout_string (matched->command);
5073
gms_normal_string (" command ignores arguments.]\n");
5076
*undo_command = matched->undo_return;
5079
/* No match, or the command was ambiguous. */
5082
gms_normal_string ("\nThe Glk command ");
5083
gms_standout_string (command);
5084
gms_normal_string (" is ");
5085
gms_normal_string (matches == 0 ? "not valid" : "ambiguous");
5086
gms_normal_string (". Try ");
5087
gms_standout_string ("glk help");
5088
gms_normal_string (" for more information.\n");
5093
gms_normal_char ('\n');
5094
gms_command_help ("");
5097
/* The string contained a Glk command; return TRUE. */
5104
* gms_command_undo_special()
5106
* This function makes a special case of the input line containing the single
5107
* word "undo", treating it as if it is "glk undo". This makes life a bit
5108
* more convenient for the player, since it's the same behavior that most
5109
* other IF systems have. It returns TRUE if "undo" found, FALSE otherwise.
5112
gms_command_undo_special (const char *string)
5117
/* Find the start and end of the first string word. */
5118
posn = strspn (string, "\t ");
5119
end = posn + strcspn (string + posn, "\t ");
5121
/* See if string contains an "undo" request, with nothing following. */
5122
if (end - posn == strlen ("undo")
5123
&& gms_strncasecmp (string + posn, "undo", end - posn) == 0)
5125
posn = end + strspn (string + end, "\t ");
5126
if (string[posn] == '\0')
5134
/*---------------------------------------------------------------------*/
5135
/* Glk port input functions */
5136
/*---------------------------------------------------------------------*/
5139
* Input buffer allocated for reading input lines. The buffer is filled
5140
* from either an input log, if one is currently being read, or from Glk
5141
* line input. We also need an "undo" notification flag.
5143
enum { GMS_INPUTBUFFER_LENGTH = 256 };
5144
static char gms_input_buffer[GMS_INPUTBUFFER_LENGTH];
5145
static int gms_input_length = 0,
5146
gms_input_cursor = 0,
5147
gms_undo_notification = FALSE;
5149
/* Table of single-character command abbreviations. */
5150
typedef const struct
5152
const char abbreviation; /* Abbreviation character. */
5153
const char * const expansion; /* Expansion string. */
5154
} gms_abbreviation_t;
5155
typedef gms_abbreviation_t *gms_abbreviationref_t;
5157
static gms_abbreviation_t GMS_ABBREVIATIONS[] = {
5158
{'c', "close"}, {'g', "again"}, {'i', "inventory"},
5159
{'k', "attack"}, {'l', "look"}, {'p', "open"},
5160
{'q', "quit"}, {'r', "drop"}, {'t', "take"},
5161
{'x', "examine"}, {'y', "yes"}, {'z', "wait"},
5167
* gms_expand_abbreviations()
5169
* Expand a few common one-character abbreviations commonly found in other
5170
* game systems, but not always normal in Magnetic Scrolls games.
5173
gms_expand_abbreviations (char *buffer, int size)
5175
char *command, abbreviation;
5176
const char *expansion;
5177
gms_abbreviationref_t entry;
5180
/* Ignore anything that isn't a single letter command. */
5181
command = buffer + strspn (buffer, "\t ");
5182
if (!(strlen (command) == 1
5183
|| (strlen (command) > 1 && isspace (command[1]))))
5186
/* Scan the abbreviations table for a match. */
5187
abbreviation = glk_char_to_lower ((unsigned char) command[0]);
5189
for (entry = GMS_ABBREVIATIONS; entry->expansion; entry++)
5191
if (entry->abbreviation == abbreviation)
5193
expansion = entry->expansion;
5199
* If a match found, check for a fit, then replace the character with the
5204
if (strlen (buffer) + strlen (expansion) - 1 >= size)
5207
memmove (command + strlen (expansion) - 1, command, strlen (command) + 1);
5208
memcpy (command, expansion, strlen (expansion));
5211
gms_standout_string ("[");
5212
gms_standout_char (abbreviation);
5213
gms_standout_string (" -> ");
5214
gms_standout_string (expansion);
5215
gms_standout_string ("]\n");
5224
* Read and buffer a line of input. If there is an input log active, then
5225
* data is taken by reading this first. Otherwise, the function gets a
5228
* It also makes special cases of some lines read from the user, either
5229
* handling commands inside them directly, or expanding abbreviations as
5230
* appropriate. This is not reflected in the buffer, which is adjusted as
5231
* required before returning.
5234
gms_buffer_input (void)
5239
* Update the current status line display, and flush any pending buffered
5242
gms_status_notify ();
5243
gms_output_flush ();
5246
* Magnetic Windows games tend not to issue a prompt after reading an empty
5247
* line of input. This can make for a very blank looking screen.
5249
* To slightly improve things, if it looks like we didn't get a prompt from
5250
* the game, do our own.
5252
if (gms_prompt_enabled && !gms_game_prompted ())
5254
gms_normal_char ('\n');
5255
gms_normal_string (GMS_INPUT_PROMPT);
5259
* If we have an input log to read from, use that until it is exhausted. On
5260
* end of file, close the stream and resume input from line requests.
5262
if (gms_readlog_stream)
5266
/* Get the next line from the log stream. */
5267
chars = glk_get_line_stream (gms_readlog_stream,
5268
gms_input_buffer, sizeof (gms_input_buffer));
5271
/* Echo the line just read in input style. */
5272
glk_set_style (style_Input);
5273
glk_put_buffer (gms_input_buffer, chars);
5274
glk_set_style (style_Normal);
5276
/* Note how many characters buffered, and return. */
5277
gms_input_length = chars;
5282
* We're at the end of the log stream. Close it, and then continue
5283
* on to request a line from Glk.
5285
glk_stream_close (gms_readlog_stream, NULL);
5286
gms_readlog_stream = NULL;
5290
* No input log being read, or we just hit the end of file on one. Revert
5291
* to normal line input; start by getting a new line from Glk.
5293
glk_request_line_event (gms_main_window,
5294
gms_input_buffer, sizeof (gms_input_buffer) - 1, 0);
5295
gms_event_wait (evtype_LineInput, &event);
5297
/* Terminate the input line with a NUL. */
5298
assert (event.val1 <= sizeof (gms_input_buffer) - 1);
5299
gms_input_buffer[event.val1] = '\0';
5301
/* Special handling for "undo" commands. */
5302
if (gms_command_undo_special (gms_input_buffer))
5304
/* Write the "undo" to any input log. */
5305
if (gms_inputlog_stream)
5307
glk_put_string_stream (gms_inputlog_stream, gms_input_buffer);
5308
glk_put_char_stream (gms_inputlog_stream, '\n');
5311
/* Overwrite buffer with an empty line if we saw "undo". */
5312
gms_input_buffer[0] = '\n';
5313
gms_input_length = 1;
5315
gms_undo_notification = TRUE;
5320
* If neither abbreviations nor local commands are enabled, use the data
5321
* read above without further massaging.
5323
if (gms_abbreviations_enabled || gms_commands_enabled)
5328
* If the first non-space input character is a quote, bypass all
5329
* abbreviation expansion and local command recognition, and use the
5330
* unadulterated input, less introductory quote.
5332
command = gms_input_buffer + strspn (gms_input_buffer, "\t ");
5333
if (command[0] == '\'')
5335
/* Delete the quote with memmove(). */
5336
memmove (command, command + 1, strlen (command));
5340
/* Check for, and expand, any abbreviated commands. */
5341
if (gms_abbreviations_enabled)
5343
gms_expand_abbreviations (gms_input_buffer,
5344
sizeof (gms_input_buffer));
5348
* Check for standalone "help", then for Glk port special commands;
5349
* suppress the interpreter's use of this input for Glk commands
5350
* by overwriting the line with a single newline character.
5352
if (gms_commands_enabled)
5356
posn = strspn (gms_input_buffer, "\t ");
5357
if (gms_strncasecmp (gms_input_buffer + posn,
5358
"help", strlen ("help"))== 0)
5360
if (strspn (gms_input_buffer + posn + strlen ("help"), "\t ")
5361
== strlen (gms_input_buffer + posn + strlen ("help")))
5363
gms_output_register_help_request ();
5367
if (gms_command_escape (gms_input_buffer,
5368
&gms_undo_notification))
5370
gms_output_silence_help_hints ();
5371
gms_input_buffer[0] = '\n';
5372
gms_input_length = 1;
5380
* If there is an input log active, log this input string to it. Note that
5381
* by logging here we get any abbreviation expansions but we won't log glk
5382
* special commands, nor any input read from a current open input log.
5384
if (gms_inputlog_stream)
5386
glk_put_string_stream (gms_inputlog_stream, gms_input_buffer);
5387
glk_put_char_stream (gms_inputlog_stream, '\n');
5391
* Now append a newline to the buffer, since Glk line input doesn't provide
5392
* one, and in any case, abbreviation expansion may have edited the buffer
5393
* contents (and in particular, changed the length).
5395
gms_input_buffer[strlen (gms_input_buffer) + 1] = '\0';
5396
gms_input_buffer[strlen (gms_input_buffer)] = '\n';
5398
/* Note how many characters are buffered after all of the above. */
5399
gms_input_length = strlen (gms_input_buffer);
5406
* Return the single next character to the interpreter. This function
5407
* extracts characters from the input buffer until empty, when it then
5408
* tries to buffer more data.
5411
ms_getchar (type8 trans)
5413
/* See if we are at the end of the input buffer. */
5414
if (gms_input_cursor == gms_input_length)
5417
* Try to read in more data, and rewind buffer cursor. As well as
5418
* reading input, this may set an undo notification.
5420
gms_buffer_input ();
5421
gms_input_cursor = 0;
5423
if (gms_undo_notification)
5426
* Clear the undo notification, and discard buffered input (usually
5427
* just the '\n' placed there when the undo command was recognized).
5429
gms_undo_notification = FALSE;
5430
gms_input_length = 0;
5433
* Return the special 0, or a blank line if no undo is allowed at
5436
return trans ? 0 : '\n';
5440
/* Return the next character from the input buffer. */
5441
assert (gms_input_cursor < gms_input_length);
5442
return gms_input_buffer[gms_input_cursor++];
5449
* Print a confirmation prompt, and read a single input character, taking
5450
* only [YyNn] input. If the character is 'Y' or 'y', return TRUE.
5453
gms_confirm (const char *prompt)
5456
unsigned char response;
5460
* Print the confirmation prompt, in a style that hints that it's from the
5461
* interpreter, not the game.
5463
gms_standout_string (prompt);
5465
/* Wait for a single 'Y' or 'N' character response. */
5469
glk_request_char_event (gms_main_window);
5470
gms_event_wait (evtype_CharInput, &event);
5472
if (event.val1 <= UCHAR_MAX)
5473
response = glk_char_to_upper (event.val1);
5475
while (!(response == 'Y' || response == 'N'));
5477
/* Echo the confirmation response, and a blank line. */
5478
glk_set_style (style_Input);
5479
glk_put_string (response == 'Y' ? "Yes" : "No");
5480
glk_set_style (style_Normal);
5481
glk_put_string ("\n\n");
5483
return response == 'Y';
5487
/*---------------------------------------------------------------------*/
5488
/* Glk port event functions */
5489
/*---------------------------------------------------------------------*/
5494
* Process Glk events until one of the expected type arrives. Return
5495
* the event of that type.
5498
gms_event_wait (glui32 wait_type, event_t * event)
5506
switch (event->type)
5508
case evtype_Arrange:
5510
/* Refresh any sensitive windows on size events. */
5511
gms_status_redraw ();
5513
gms_graphics_paint ();
5517
/* Do background graphics updates on timeout. */
5518
gms_graphics_timeout ();
5522
while (event->type != wait_type);
5526
/*---------------------------------------------------------------------*/
5527
/* Glk port file functions */
5528
/*---------------------------------------------------------------------*/
5530
/* Success and fail return codes from file functions. */
5531
static const type8 GMS_FILE_SUCCESS = 0,
5539
* Save the current game state to a file, and load a game state.
5542
ms_save_file (type8s * name, type8 * ptr, type16 size)
5546
/* Flush any pending buffered output. */
5547
gms_output_flush ();
5549
/* If there is no name, use Glk to prompt for one, and save. */
5555
fileref = glk_fileref_create_by_prompt (fileusage_SavedGame,
5558
return GMS_FILE_ERROR;
5560
stream = glk_stream_open_file (fileref, filemode_Write, 0);
5563
glk_fileref_destroy (fileref);
5564
return GMS_FILE_ERROR;
5567
/* Write game state. */
5568
glk_put_buffer_stream (stream, ptr, size);
5570
glk_stream_close (stream, NULL);
5571
glk_fileref_destroy (fileref);
5579
* If openable for read, assume the file already exists and confirm
5582
stream = fopen (name, "r");
5588
* Confirm overwrite, clearing the game prompted flag beforehand
5589
* so we're sure to issue a new prompt on the next line input.
5591
gms_game_prompted ();
5592
if (!gms_confirm ("Overwrite existing file? [y/n] "))
5593
return GMS_FILE_ERROR;
5596
/* Open output file directly. There's no Glk way to achieve this. */
5597
stream = fopen (name, "wb");
5599
return GMS_FILE_ERROR;
5601
/* Write game state. */
5602
if (fwrite (ptr, 1, size, stream) != size)
5605
return GMS_FILE_ERROR;
5611
return GMS_FILE_SUCCESS;
5615
ms_load_file (type8s * name, type8 * ptr, type16 size)
5619
/* Flush any pending buffered output. */
5620
gms_output_flush ();
5622
/* If there is no name, use Glk to prompt for one, and load. */
5628
fileref = glk_fileref_create_by_prompt (fileusage_SavedGame,
5631
return GMS_FILE_ERROR;
5634
* Reject the file reference if we're expecting to read from it, and
5635
* the referenced file doesn't exist.
5637
if (!glk_fileref_does_file_exist (fileref))
5639
glk_fileref_destroy (fileref);
5640
return GMS_FILE_ERROR;
5643
stream = glk_stream_open_file (fileref, filemode_Read, 0);
5646
glk_fileref_destroy (fileref);
5647
return GMS_FILE_ERROR;
5650
/* Restore saved game data. */
5651
glk_get_buffer_stream (stream, ptr, size);
5653
glk_stream_close (stream, NULL);
5654
glk_fileref_destroy (fileref);
5662
* Open the input file directly. As above, there's no Glk way to
5665
stream = fopen (name, "rb");
5667
return GMS_FILE_ERROR;
5669
/* Restore saved game data. */
5670
if (fread (ptr, 1, size, stream) != size)
5673
return GMS_FILE_ERROR;
5679
return GMS_FILE_SUCCESS;
5683
/*---------------------------------------------------------------------*/
5684
/* Functions intercepted by link-time wrappers */
5685
/*---------------------------------------------------------------------*/
5691
* Wrapper functions around toupper() and tolower(). The Linux linker's
5692
* --wrap option will convert calls to mumble() to __wrap_mumble() if we
5693
* give it the right options. We'll use this feature to translate all
5694
* toupper() and tolower() calls in the interpreter code into calls to
5695
* Glk's versions of these functions.
5697
* It's not critical that we do this. If a linker, say a non-Linux one,
5698
* won't do --wrap, then just do without it. It's unlikely that there
5699
* will be much noticeable difference.
5702
__wrap_toupper (int ch)
5706
uch = glk_char_to_upper ((unsigned char) ch);
5711
__wrap_tolower (int ch)
5715
lch = glk_char_to_lower ((unsigned char) ch);
5720
/*---------------------------------------------------------------------*/
5721
/* main() and options parsing */
5722
/*---------------------------------------------------------------------*/
5725
* The following values need to be passed between the startup_code and main
5728
static char *gms_gamefile = NULL, /* Name of game file. */
5729
*gms_game_message = NULL; /* Error message. */
5733
* gms_establish_filenames()
5735
* Given a game name, try to establish three filenames from it - the main game
5736
* text file, the (optional) graphics data file, and the (optional) hints
5737
* file. Given an input "file" X, the function looks for X.MAG or X.mag for
5738
* game data, X.GFX or X.gfx for graphics, and X.HNT or X.hnt for hints.
5739
* If the input file already ends with .MAG, .GFX, or .HNT, the extension
5740
* is stripped first.
5742
* The function returns NULL for filenames not available. It's not fatal if
5743
* the graphics filename or hints filename is NULL, but it is if the main game
5744
* filename is NULL. Filenames are malloc'ed, and need to be freed by the
5748
gms_establish_filenames (char *name, char **text, char **graphics, char **hints)
5750
char *base, *text_file, *graphics_file, *hints_file;
5752
assert (name && text && graphics && hints);
5754
/* Take a destroyable copy of the input filename. */
5755
base = gms_malloc (strlen (name) + 1);
5756
strcpy (base, name);
5758
/* If base has an extension .MAG, .GFX, or .HNT, remove it. */
5759
if (strlen (base) > strlen (".XXX"))
5761
if (gms_strcasecmp (base + strlen (base) - strlen (".MAG"), ".MAG") == 0
5762
|| gms_strcasecmp (base + strlen (base) - strlen (".GFX"), ".GFX") == 0
5763
|| gms_strcasecmp (base + strlen (base) - strlen (".HNT"), ".HNT") == 0)
5764
base[strlen (base) - strlen (".XXX")] = '\0';
5767
/* Allocate space for the return text file. */
5768
text_file = gms_malloc (strlen (base) + strlen (".MAG") + 1);
5770
/* Form a candidate text file, by adding a .MAG extension. */
5771
strcpy (text_file, base);
5772
strcat (text_file, ".MAG");
5773
stream = fopen (text_file, "rb");
5776
/* Retry, using a .mag extension instead. */
5777
strcpy (text_file, base);
5778
strcat (text_file, ".mag");
5779
stream = fopen (text_file, "rb");
5783
* No access to a usable game text file. Return immediately,
5784
* without looking for any associated graphics or hints files.
5798
/* Now allocate space for the return graphics file. */
5799
graphics_file = gms_malloc (strlen (base) + strlen (".GFX") + 1);
5801
/* As above, form a candidate graphics file, using a .GFX extension. */
5802
strcpy (graphics_file, base);
5803
strcat (graphics_file, ".GFX");
5804
stream = fopen (graphics_file, "rb");
5807
/* Retry, using a .gfx extension instead. */
5808
strcpy (graphics_file, base);
5809
strcat (graphics_file, ".gfx");
5810
stream = fopen (graphics_file, "rb");
5814
* No access to any graphics file. In this case, free memory and
5815
* reset graphics file to NULL.
5817
free (graphics_file);
5818
graphics_file = NULL;
5824
/* Now allocate space for the return hints file. */
5825
hints_file = gms_malloc (strlen (base) + strlen (".HNT") + 1);
5827
/* As above, form a candidate graphics file, using a .HNT extension. */
5828
strcpy (hints_file, base);
5829
strcat (hints_file, ".HNT");
5830
stream = fopen (hints_file, "rb");
5833
/* Retry, using a .hnt extension instead. */
5834
strcpy (hints_file, base);
5835
strcat (hints_file, ".hnt");
5836
stream = fopen (hints_file, "rb");
5840
* No access to any hints file. In this case, free memory and
5841
* reset hints file to NULL.
5850
/* Return the text file, and graphics and hints, which may be NULL. */
5852
*graphics = graphics_file;
5853
*hints = hints_file;
5860
* gms_startup_code()
5863
* Together, these functions take the place of the original main(). The
5864
* first one is called from glkunix_startup_code(), to parse and generally
5865
* handle options. The second is called from glk_main(), and does the real
5866
* work of running the game.
5869
gms_startup_code (int argc, char *argv[])
5873
/* Handle command line arguments. */
5874
for (argv_index = 1;
5875
argv_index < argc && argv[argv_index][0] == '-'; argv_index++)
5877
if (strcmp (argv[argv_index], "-nc") == 0)
5879
gms_commands_enabled = FALSE;
5882
if (strcmp (argv[argv_index], "-na") == 0)
5884
gms_abbreviations_enabled = FALSE;
5887
if (strcmp (argv[argv_index], "-np") == 0)
5889
gms_graphics_enabled = FALSE;
5892
if (strcmp (argv[argv_index], "-ng") == 0)
5894
gms_gamma_mode = GAMMA_OFF;
5897
if (strcmp (argv[argv_index], "-nx") == 0)
5899
gms_animation_enabled = FALSE;
5902
if (strcmp (argv[argv_index], "-ne") == 0)
5904
gms_prompt_enabled = FALSE;
5911
* Get the name of the game file. Since we need this in our call from
5912
* glk_main, we need to keep it in a module static variable. If the game
5913
* file name is omitted, then here we'll set the pointer to NULL, and
5914
* complain about it later in main. Passing the message string around
5915
* like this is a nuisance...
5917
if (argv_index == argc - 1)
5919
gms_gamefile = argv[argv_index];
5920
gms_game_message = NULL;
5924
s = strrchr(gms_gamefile, '\\');
5925
if (s) garglk_set_story_name(s+1);
5926
s = strrchr(gms_gamefile, '/');
5927
if (s) garglk_set_story_name(s+1);
5933
gms_gamefile = NULL;
5934
if (argv_index < argc - 1)
5935
gms_game_message = "More than one game file was given"
5936
" on the command line.";
5938
gms_game_message = "No game file was given on the command line.";
5941
/* All startup options were handled successfully. */
5948
char *text_file = NULL, *graphics_file = NULL, *hints_file = NULL;
5949
int ms_init_status, is_running;
5951
/* Ensure Magnetic Scrolls internal types have the right sizes. */
5952
if (!(sizeof (type8) == 1 && sizeof (type8s) == 1
5953
&& sizeof (type16) == 2 && sizeof (type16s) == 2
5954
&& sizeof (type32) == 4 && sizeof (type32s) == 4))
5956
gms_fatal ("GLK: Types sized incorrectly, recompilation is needed");
5960
/* Create the main Glk window, and set its stream as current. */
5961
gms_main_window = glk_window_open (0, 0, 0, wintype_TextBuffer, 0);
5962
if (!gms_main_window)
5964
gms_fatal ("GLK: Can't open main window");
5967
glk_window_clear (gms_main_window);
5968
glk_set_window (gms_main_window);
5969
glk_set_style (style_Normal);
5971
/* If there's a problem with the game file, complain now. */
5974
assert (gms_game_message);
5975
gms_header_string ("Glk Magnetic Error\n\n");
5976
gms_normal_string (gms_game_message);
5977
gms_normal_char ('\n');
5982
* Given the basic game name, try to come up with usable text, graphics,
5983
* and hints filenames. The graphics and hints files may be null, but the
5984
* text file may not.
5987
gms_establish_filenames (gms_gamefile,
5988
&text_file, &graphics_file, &hints_file);
5991
assert (!graphics_file && !hints_file);
5992
gms_header_string ("Glk Magnetic Error\n\n");
5993
gms_normal_string ("Can't find or open game '");
5994
gms_normal_string (gms_gamefile);
5995
gms_normal_string ("[.mag|.MAG]'");
5998
gms_normal_string (": ");
5999
gms_normal_string (strerror (errno));
6001
gms_normal_char ('\n');
6005
/* Set the possibility of pictures depending on graphics file. */
6009
* Check Glk library capabilities, and note pictures are impossible if
6010
* the library can't offer both graphics and timers. We need timers to
6011
* create the background "thread" for picture updates.
6013
gms_graphics_possible = glk_gestalt (gestalt_Graphics, 0)
6014
&& glk_gestalt (gestalt_Timer, 0);
6017
gms_graphics_possible = FALSE;
6021
* If pictures are impossible, clear pictures enabled flag. That is, act
6022
* as if -np was given on the command line, even though it may not have
6023
* been. If pictures are impossible, they can never be enabled.
6025
if (!gms_graphics_possible)
6026
gms_graphics_enabled = FALSE;
6028
/* Try to create a one-line status window. We can live without it. */
6029
gms_status_window = glk_window_open (gms_main_window,
6030
winmethod_Above | winmethod_Fixed,
6031
1, wintype_TextGrid, 0);
6033
/* Seed the random number generator. */
6034
ms_seed (time (NULL));
6037
* Load the game. If no graphics are possible, then passing the NULL to
6038
* ms_init() runs a game without graphics.
6041
if (gms_graphics_possible)
6043
assert (graphics_file);
6044
ms_init_status = ms_init (text_file, graphics_file, hints_file, NULL);
6047
ms_init_status = ms_init (text_file, NULL, hints_file, NULL);
6049
/* Look for a complete failure to load the game. */
6050
if (ms_init_status == 0)
6052
if (gms_status_window)
6053
glk_window_close (gms_status_window, NULL);
6054
gms_header_string ("Glk Magnetic Error\n\n");
6055
gms_normal_string ("Can't load game '");
6056
gms_normal_string (gms_gamefile);
6057
gms_normal_char ('\'');
6060
gms_normal_string (": ");
6061
gms_normal_string (strerror (errno));
6063
gms_normal_char ('\n');
6066
* Free the text file path, any graphics/hints file path, and
6067
* interpreter allocated memory.
6070
free (graphics_file);
6076
/* Try to identify the game from its text file header. */
6077
gms_gameid_identify_game (text_file);
6079
/* Print out a short banner. */
6080
gms_header_string ("\nMagnetic Scrolls Interpreter, version 2.3\n");
6081
gms_banner_string ("Written by Niclas Karlsson\n"
6082
"Glk interface by Simon Baldwin\n\n");
6084
/* Look for failure to load just game graphics. */
6085
if (gms_graphics_possible && ms_init_status == 1)
6088
* Output a warning if graphics failed, but the main game text
6091
gms_standout_string ("Error: Unable to open graphics file\n"
6092
"Continuing without pictures...\n\n");
6094
gms_graphics_possible = FALSE;
6097
/* Run the game opcodes -- ms_rungame() returns FALSE on game end. */
6100
is_running = ms_rungame ();
6105
/* Handle any updated status and pending buffered output. */
6106
gms_status_notify ();
6107
gms_output_flush ();
6109
/* Turn off any background graphics "thread". */
6110
gms_graphics_stop ();
6112
/* Free interpreter allocated memory. */
6116
* Free any temporary memory that may have been used by graphics and hints.
6118
gms_graphics_cleanup ();
6119
gms_hints_cleanup ();
6121
/* Close any open transcript, input log, and/or read log. */
6122
if (gms_transcript_stream)
6124
glk_stream_close (gms_transcript_stream, NULL);
6125
gms_transcript_stream = NULL;
6127
if (gms_inputlog_stream)
6129
glk_stream_close (gms_inputlog_stream, NULL);
6130
gms_inputlog_stream = NULL;
6132
if (gms_readlog_stream)
6134
glk_stream_close (gms_readlog_stream, NULL);
6135
gms_readlog_stream = NULL;
6138
/* Free the text file path, and any graphics/hints file path. */
6140
free (graphics_file);
6145
/*---------------------------------------------------------------------*/
6146
/* Linkage between Glk entry/exit calls and the Magnetic interpreter */
6147
/*---------------------------------------------------------------------*/
6150
* Safety flags, to ensure we always get startup before main, and that
6151
* we only get a call to main once.
6153
static int gms_startup_called = FALSE,
6154
gms_main_called = FALSE;
6159
* Main entry point for Glk. Here, all startup is done, and we call our
6160
* function to run the game.
6165
assert (gms_startup_called && !gms_main_called);
6166
gms_main_called = TRUE;
6168
/* Call the interpreter main function. */
6173
/*---------------------------------------------------------------------*/
6174
/* Glk linkage relevant only to the UNIX platform */
6175
/*---------------------------------------------------------------------*/
6178
#include "glkstart.h"
6181
* Glk arguments for UNIX versions of the Glk interpreter.
6183
glkunix_argumentlist_t glkunix_arguments[] = {
6184
{(char *) "-nc", glkunix_arg_NoValue,
6185
(char *) "-nc No local handling for Glk special commands"},
6186
{(char *) "-na", glkunix_arg_NoValue,
6187
(char *) "-na Turn off abbreviation expansions"},
6188
{(char *) "-np", glkunix_arg_NoValue,
6189
(char *) "-np Turn off pictures"},
6190
{(char *) "-ng", glkunix_arg_NoValue,
6191
(char *) "-ng Turn off automatic gamma correction on pictures"},
6192
{(char *) "-nx", glkunix_arg_NoValue,
6193
(char *) "-nx Turn off picture animations"},
6194
{(char *) "-ne", glkunix_arg_NoValue,
6195
(char *) "-ne Turn off additional interpreter prompt"},
6196
{(char *) "", glkunix_arg_ValueCanFollow,
6197
(char *) "filename game to run"},
6198
{NULL, glkunix_arg_End, NULL}
6203
* glkunix_startup_code()
6205
* Startup entry point for UNIX versions of Glk interpreter. Glk will
6206
* call glkunix_startup_code() to pass in arguments. On startup, we call
6207
* our function to parse arguments and generally set stuff up.
6210
glkunix_startup_code (glkunix_startup_t * data)
6212
assert (!gms_startup_called);
6213
gms_startup_called = TRUE;
6216
garglk_set_program_name("Magnetic 2.3");
6217
garglk_set_program_info(
6218
"Magnetic 2.3 by Niclas Karlsson, David Kinder,\n"
6219
"Stefan Meier, and Paul David Doherty.\n"
6220
"Glk port by Simon Baldwin.\n"
6221
"Gargoyle tweaks by Tor Andersson.\n");
6224
return gms_startup_code (data->argc, data->argv);
6229
/*---------------------------------------------------------------------*/
6230
/* Glk linkage relevant only to the Mac platform */
6231
/*---------------------------------------------------------------------*/
6234
#include "macglk_startup.h"
6236
static strid_t gms_mac_gamefile = NULL;
6237
static short gms_savedVRefNum = 0;
6238
static long gms_savedDirID = 0;
6242
* gms_mac_whenselected()
6243
* gms_mac_whenbuiltin()
6244
* macglk_startup_code()
6246
* Startup entry points for Mac versions of Glk interpreter. Glk will call
6247
* macglk_startup_code() for details on what to do when the application is
6248
* selected. On selection, an argv[] vector is built, and passed to the
6249
* normal interpreter startup code, after which, Glk will call glk_main().
6252
gms_mac_whenselected (FSSpec * file, OSType filetype)
6254
static char *argv[2];
6255
assert (!gms_startup_called);
6256
gms_startup_called = TRUE;
6258
/* Set the WD to where the file is, so later fopens work. */
6259
if (HGetVol (0, &gms_savedVRefNum, &gms_savedDirID) != 0)
6261
gms_fatal ("GLK: HGetVol failed");
6264
if (HSetVol (0, file->vRefNum, file->parID) != 0);
6266
gms_fatal ("GLK: HSetVol failed");
6270
/* Put a CString version of the PString name into argv[1]. */
6271
argv[1] = gms_malloc (file->name[0] + 1);
6272
BlockMoveData (file->name + 1, argv[1], file->name[0]);
6273
argv[1][file->name[0]] = '\0';
6276
return gms_startup_code (2, argv);
6280
gms_mac_whenbuiltin (void)
6282
/* Not implemented yet. */
6287
macglk_startup_code (macglk_startup_t * data)
6289
static OSType gms_mac_gamefile_types[] = { 'MaSc' };
6291
data->startup_model = macglk_model_ChooseOrBuiltIn;
6292
data->app_creator = 'cAGL';
6293
data->gamefile_types = gms_mac_gamefile_types;
6294
data->num_gamefile_types = sizeof (gms_mac_gamefile_types)
6295
/ sizeof (*gms_mac_gamefile_types);
6296
data->savefile_type = 'BINA';
6297
data->datafile_type = 0x3f3f3f3f;
6298
data->gamefile = &gms_mac_gamefile;
6299
data->when_selected = gms_mac_whenselected;
6300
data->when_builtin = gms_mac_whenbuiltin;
6301
/* macglk_setprefs(); */
6304
#endif /* TARGET_OS_MAC */