568 lines
17 KiB
C++
568 lines
17 KiB
C++
/* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#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
|