aboutsummaryrefslogtreecommitdiff
path: root/engines/glk/adrift/sxscript.cpp
blob: 566aa3485fe8c78a1c586721dca1c99c5063b085 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "glk/adrift/scare.h"
#include "glk/adrift/sxprotos.h"

namespace Glk {
namespace Adrift {

/*
 * Module notes:
 *
 * o The script file format is as follows.  Lines beginning '#' are comments
 *   and empty lines are ignored, otherwise the file is composed of sections.
 *   The first section line is one that starts with either '>' or '~'.  This
 *   is the next command.  The following lines, up to the next '>' or '~'
 *   section start, are concatenated into the expectation for the command.
 *   Expectations are glob patterns.  Commands starting with '>' are sent to
 *   the game; those starting with '~' are sent to the SCARE debugger.  Before
 *   the game is running, debugger commands are valid.  The first non-debugger
 *   command starts the game running.  An empty debugger command ('~') that
 *   follows any introductory debugger commands both starts the game and sets
 *   an expectation for the game's introductory text.  After the game has
 *   completed (or quit), only debugger commands are valid; others are ignored.
 *
 * o The script file structure is intentionally simple, but might be too
 *   simple for some purposes.
 */

/* Assorted definitions and constants. */
static const sc_int LINE_BUFFER_SIZE = 256;
static const sc_char NUL = '\0';
static const sc_char SCRIPT_COMMENT = '#';
static const sc_char GAME_COMMAND = '>';
static const sc_char DEBUG_COMMAND = '~';

/* Verbosity, and references to the game and script being processed. */
static sc_bool scr_is_verbose = FALSE;
static sc_game scr_game = NULL;
static sx_script scr_script = NULL;

/* Script line number, and count of errors registered for the script. */
static sc_int scr_line_number = 0;
static sc_int scr_errors = 0;

/*
 * Current expected output, and game accumulated output, used by the
 * expectation checking function.
 */
static sc_char *scr_expectation = NULL;
static sc_char *scr_game_output = NULL;


/*
 * scr_set_verbose()
 *
 * Set error reporting for expectation errors detected in the script.
 */
void scr_set_verbose(sc_bool flag) {
	scr_is_verbose = flag;
}


/*
 * scr_test_message()
 * scr_test_failed()
 *
 * Simple common message and test case failure handling functions.  The second
 * is used by the serialization helper, so is not static.
 */
static void scr_test_message(const sc_char *format, const sc_char *string) {
	if (scr_is_verbose) {
		sx_trace("--- ");
		sx_trace(format, string);
		sx_trace("\n");
	}
}

void scr_test_failed(const sc_char *format, const sc_char *string) {
	assert(format && string);

	if (scr_is_verbose) {
		if (scr_line_number > 0)
			sx_trace("--- Near line %ld: ", scr_line_number);
		else
			sx_trace("--- ");
		sx_trace(format, string);
		sx_trace("\n");
	}
	scr_errors++;
}


/*
 * scr_is_line_type()
 * scr_is_line_comment_or_empty()
 * scr_is_line_game_command()
 * scr_is_line_debug_command()
 * scr_is_line_command()
 * scr_is_line_empty_debug_command()
 *
 * Line classifiers, return TRUE if line has the given type.
 */
static sc_bool scr_is_line_type(const sc_char *line, sc_char type) {
	return line[0] == type;
}

static sc_bool scr_is_line_comment_or_empty(const sc_char *line) {
	return scr_is_line_type(line, SCRIPT_COMMENT)
	       || strspn(line, "\t\n\v\f\r ") == strlen(line);
}

static sc_bool scr_is_line_game_command(const sc_char *line) {
	return scr_is_line_type(line, GAME_COMMAND);
}

static sc_bool scr_is_line_debug_command(const sc_char *line) {
	return scr_is_line_type(line, DEBUG_COMMAND);
}

static sc_bool scr_is_line_command(const sc_char *line) {
	return scr_is_line_game_command(line) || scr_is_line_debug_command(line);
}

static sc_bool scr_is_line_empty_debug_command(const sc_char *line) {
	return scr_is_line_type(line, DEBUG_COMMAND) && line[1] == NUL;
}


/* Script location, a pair holding the file location and the line number. */
struct sx_scr_location_t {
	size_t position;
	sc_int line_number;
};
typedef sx_scr_location_t *sx_scr_locationref_t;

/*
 * scr_save_location()
 * scr_restore_location()
 *
 * Save and restore the script location in the given structure.
 */
static void scr_save_location(sx_script script, sx_scr_locationref_t location) {
	location->position = script->pos();
	location->line_number = scr_line_number;
}

static void scr_restore_location(sx_script script, sx_scr_locationref_t location) {
	script->seek(location->position);
	scr_line_number = location->line_number;
}


/*
 * scr_get_next_line()
 *
 * Helper for scr_get_next_section().  Returns the next non-comment, non-empty
 * line from the script.  Returns NULL if no more lines, or on file error.  The
 * return string is allocated, and it's the caller's responsibility to free it.
 */
static sc_char *scr_get_next_line(sx_script script) {
	sc_char *buffer, *line = NULL;

	/* Allocate a buffer for line reads. */
	buffer = (sc_char *)sx_malloc(LINE_BUFFER_SIZE);

	/* Read until a significant line is found, or end of file or error. */
	while (adrift_fgets(buffer, LINE_BUFFER_SIZE, script)) {
		scr_line_number++;
		if (!scr_is_line_comment_or_empty(buffer)) {
			line = buffer;
			break;
		}
	}

	/* If no significant line read, free the read buffer. */
	if (!line)
		sx_free(buffer);

	return line;
}


/*
 * scr_concatenate()
 *
 * Helper for scr_get_next_section().  Builds a string formed by concatenating
 * the second argument to the first.  If the first is NULL, acts as strdup()
 * instead.
 */
static sc_char *scr_concatenate(sc_char *string, const sc_char *buffer) {
	/* If string is not null, concatenate buffer, otherwise duplicate. */
	if (string) {
		string = (sc_char *)sx_realloc(string,
		                               strlen(string) + 1 + strlen(buffer) + 1);
		strcat(string, " ");
		strcat(string, buffer);
	} else {
		string = (sc_char *)sx_malloc(strlen(buffer) + 1);
		strcpy(string, buffer);
	}

	return string;
}


/*
 * scr_get_next_section()
 *
 * Retrieve the next command and any expectation from the script file.
 * Returns TRUE if a line is returned, FALSE at end-of-file.  Expectation may
 * be NULL if this paragraph doesn't have one; command may not be (if TRUE is
 * returned).  Command and expectation are allocated, and the caller needs to
 * free them.
 */
static sc_bool scr_get_next_section(sx_script script, sc_char **command, sc_char **expectation) {
	sc_char *line, *first_line, *other_lines;
	sx_scr_location_t location;

	/* Clear initial line accumulation. */
	first_line = other_lines = NULL;

	/* Read the next significant line from the script. */
	scr_save_location(script, &location);
	line = scr_get_next_line(script);
	while (line) {
		/* If already a first line, this is other lines or section end. */
		if (first_line) {
			/*
			 * If we found the start of the next section, reset the script
			 * location that saved on the line read, and we're done.
			 */
			if (scr_is_line_command(line)) {
				scr_restore_location(script, &location);
				sx_free(line);
				break;
			} else
				other_lines = scr_concatenate(other_lines, line);
		} else
			first_line = scr_concatenate(first_line, line);

		sx_free(line);

		/* Read the next significant line from the script. */
		scr_save_location(script, &location);
		line = scr_get_next_line(script);
	}

	/* Clean up and return nothing on file error. */
	if (script->err()) {
		scr_test_failed("Script error: Failed reading script input file", "");
		sx_free(first_line);
		sx_free(other_lines);
		return FALSE;
	}

	/* Return the command and the matching expectation string, if any. */
	if (first_line) {
		*command = sx_normalize_string(first_line);
		*expectation = other_lines ? sx_normalize_string(other_lines) : NULL;
		return TRUE;
	}

	/* End of file, no command section read. */
	return FALSE;
}


/*
 * scr_expect()
 * scr_verify_expectation()
 *
 * Set an expectation, and compare the expectation, if any, with the
 * accumulated game output, using glob matching.  scr_verify_expectation()
 * increments the error count if the expectation isn't met, and reports the
 * error if required.  It then frees both the expectation and accumulated
 * input.
 */
static void scr_expect(sc_char *expectation) {
	/*
	 * Save the expectation, and set up collection of game output if needed.
	 * And if not needed, ensure expectation and game output are cleared.
	 */
	if (expectation) {
		scr_expectation = (sc_char *)sx_malloc(strlen(expectation) + 1);
		strcpy(scr_expectation, expectation);
		scr_game_output = (sc_char *)sx_malloc(1);
		strcpy(scr_game_output, "");
	} else {
		sx_free(scr_expectation);
		scr_expectation = NULL;
		sx_free(scr_game_output);
		scr_game_output = NULL;
	}
}

static void scr_verify_expectation(void) {
	/* Compare expected with actual, and handle any error detected. */
	if (scr_expectation && scr_game_output) {
		scr_game_output = sx_normalize_string(scr_game_output);
		if (!glob_match(scr_expectation, scr_game_output)) {
			scr_test_failed("Expectation error:", "");
			scr_test_message("  Expected: \"%s\"", scr_expectation);
			scr_test_message("  Received: \"%s\"", scr_game_output);
		}
	}

	/* Dispose of the expectation and accumulated game output. */
	sx_free(scr_expectation);
	scr_expectation = NULL;
	sx_free(scr_game_output);
	scr_game_output = NULL;
}


/*
 * scr_execute_debugger_command()
 *
 * Convenience interface for immediate execution of debugger commands.  This
 * function directly calls the debugger interface, and because it's immediate,
 * can also verify the expectation before returning to the caller.
 *
 * Also, it turns on the game debugger, and it's the caller's responsibility
 * to turn it off when it's no longer needed.
 */
static void scr_execute_debugger_command(const sc_char *command, sc_char *expectation) {
	sc_bool status;

	/* Set up the expectation. */
	scr_expect(expectation);

	/*
	 * Execute the command via the debugger interface.  The "+1" on command
	 * skips the leading '~' read in from the game script.
	 */
	sc_set_game_debugger_enabled(scr_game, TRUE);
	status = sc_run_game_debugger_command(scr_game, command + 1);

	if (!status) {
		scr_test_failed("Script error:"
		                " Debug command \"%s\" is not valid", command);
	}

	/* Check expectations immediately. */
	scr_verify_expectation();
}


/*
 * scr_read_line_callback()
 *
 * Check any expectations set for the last line.  Consult the script for the
 * next line to feed to the game, and any expectation for the game output
 * for that line.  If there is an expectation, save it and set scr_game_output
 * to "" so that accumulation begins.  Then pass the next line of data back
 * to the game.
 */
static sc_bool scr_read_line_callback(sc_char *buffer, sc_int length) {
	sc_char *command, *expectation;
	assert(buffer && length > 0);

	/* Check pending expectation, and clear settings for the next line. */
	scr_verify_expectation();

	/* Get the next line-expectation pair from the script stream. */
	if (scr_get_next_section(scr_script, &command, &expectation)) {
		if (scr_is_line_debug_command(command)) {
			/* The debugger persists where debug commands are adjacent. */
			scr_execute_debugger_command(command, expectation);
			sx_free(command);
			sx_free(expectation);

			/*
			 * Returning FALSE here causes the game to re-prompt.  We could
			 * loop (or tail recurse) ourselves, but returning is simpler.
			 */
			return FALSE;
		} else
			sc_set_game_debugger_enabled(scr_game, FALSE);

		if (scr_is_line_game_command(command)) {
			/* Set up the expectation. */
			scr_expect(expectation);

			/* Copy out the line to the return buffer, and free the line. */
			strncpy(buffer, command + 1, length);
			buffer[length - 1] = NUL;
			sx_free(command);
			sx_free(expectation);
			return TRUE;
		}

		/* Neither a '~' nor a '>' command. */
		scr_test_failed("Script error:"
		                " Command \"%s\" is not valid, ignored", command);
		sx_free(command);
		sx_free(expectation);
		return FALSE;
	}

	/* Ensure the game debugger is off after this section. */
	sc_set_game_debugger_enabled(scr_game, FALSE);

	/*
	 * We reached the end of the script without finding a "quit" command.
	 * Supply one here, then.  In the unlikely even that this does not quit
	 * the game, we'll iterate on this.
	 */
	assert(length > 4);
	strcpy(buffer, "quit");
	return TRUE;
}


/*
 * scr_print_string_callback()
 *
 * Handler function for game output.  Accumulates strings received from the
 * game into scr_game_output, unless no expectation is set, in which case
 * the current game output will be NULL, and we can simply save the effort.
 */
static void scr_print_string_callback(const sc_char *string) {
	assert(string);

	if (scr_game_output) {
		scr_game_output = (sc_char *)sx_realloc(scr_game_output,
		                                        strlen(scr_game_output)
		                                        + strlen(string) + 1);
		strcat(scr_game_output, string);
	}
}


/*
 * scr_start_script()
 *
 * Set up game monitoring so that each request for a line from the game
 * enters this module.  For each request, we grab the next "send" and
 * "expect" pair from the script, satisfy the request with the send data,
 * and match against the expectations on next request or on finalization.
 */
void scr_start_script(sc_game game, sx_script script) {
	sc_char *command, *expectation;
	sx_scr_location_t location;
	assert(game && script);

	/* Save the game and stream, and clear the line number and errors count. */
	assert(!scr_game && !scr_script);
	scr_game = game;
	scr_script = script;
	scr_line_number = 0;
	scr_errors = 0;

	/* Set up our callback functions to catch game i/o. */
	stub_attach_handlers(scr_read_line_callback, scr_print_string_callback,
	                     file_open_file_callback, file_read_file_callback,
	                     file_write_file_callback, file_close_file_callback);

	/*
	 * Handle any initial debugging commands, terminating on either a non-
	 * debugging one or an expectation for the game intro.
	 */
	scr_script->seek(0);
	scr_save_location(scr_script, &location);
	while (scr_get_next_section(scr_script, &command, &expectation)) {
		if (scr_is_line_debug_command(command)) {
			if (scr_is_line_empty_debug_command(command)) {
				/* It's an intro expectation - set and break loop. */
				scr_expect(expectation);
				sx_free(command);
				sx_free(expectation);
				break;
			} else {
				/* It's a full debug command - execute it as one. */
				scr_execute_debugger_command(command, expectation);
				sx_free(command);
				sx_free(expectation);
			}
		} else {
			/*
			 * It's an ordinary section - rewind so that it's the first one
			 * handled in the callback, and break loop.
			 */
			scr_restore_location(scr_script, &location);
			sx_free(command);
			sx_free(expectation);
			break;
		}

		/* Note script position before reading the next section. */
		scr_save_location(scr_script, &location);
	}

	/* Ensure the game debugger is off after this section. */
	sc_set_game_debugger_enabled(scr_game, FALSE);
}


/*
 * scr_finalize_script()
 *
 * Match any final received string against a possible expectation, and then
 * clear local records of the game, stream, and error count.  Returns the
 * count of errors detected during the script.
 */
sc_int scr_finalize_script(void) {
	sc_char *command, *expectation;
	sc_int errors;

	/* Check pending expectation, and clear settings. */
	scr_verify_expectation();

	/* Drain the remainder of the script, ignoring non-debugging commands. */
	while (scr_get_next_section(scr_script, &command, &expectation)) {
		if (scr_is_line_debug_command(command)) {
			scr_execute_debugger_command(command, expectation);
			sx_free(command);
			sx_free(expectation);
		} else {
			/* Complain about script entries ignored because the game ended. */
			scr_test_failed("Script error:"
			                " Game completed, command \"%s\" ignored", command);
			sx_free(command);
			sx_free(expectation);
		}
	}

	/* Ensure the game debugger is off after this section. */
	sc_set_game_debugger_enabled(scr_game, FALSE);

	/*
	 * Remove our callback functions from the stubs, and "close" any retained
	 * stream data from game save/load tests.
	 */
	stub_detach_handlers();
	file_cleanup();

	/* Clear local records of game stream, line number, and errors count. */
	errors = scr_errors;
	scr_game = NULL;
	scr_script = NULL;
	scr_line_number = 0;
	scr_errors = 0;

	return errors;
}

} // End of namespace Adrift
} // End of namespace Glk