Files
scummvm-cursorfix/engines/glk/adrift/sxscript.cpp
2026-02-02 04:50:13 +01:00

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