2095 lines
65 KiB
C++
2095 lines
65 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/adrift.h"
|
|
#include "glk/adrift/scprotos.h"
|
|
#include "glk/adrift/scgamest.h"
|
|
#include "glk/adrift/serialization.h"
|
|
|
|
namespace Glk {
|
|
namespace Adrift {
|
|
|
|
/* Assorted definitions and constants. */
|
|
enum { LINE_BUFFER_SIZE = 256 };
|
|
static const sc_char NUL = '\0';
|
|
static const sc_char SPECIAL_PATTERN = '#';
|
|
static const sc_char WILDCARD_PATTERN = '*';
|
|
static const sc_char *const WHITESPACE = "\t\n\v\f\r ";
|
|
static const sc_char *const SEPARATORS = ".,";
|
|
|
|
|
|
/*
|
|
* run_is_task_function()
|
|
*
|
|
* Check for the presence of a command function in the first task command,
|
|
* and action it if found. This is a 4.0.42 compatibility hack -- at
|
|
* present, only getdynfromroom() exists. Returns TRUE if function found
|
|
* and handled.
|
|
*/
|
|
static sc_bool run_is_task_function(const sc_char *pattern, sc_gameref_t game) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
sc_vartype_t vt_key[3];
|
|
sc_int room, object;
|
|
sc_char *argument;
|
|
|
|
/* Simple comparison against the one known task expression. */
|
|
argument = (sc_char *)sc_malloc(strlen(pattern) + 1);
|
|
if (sscanf(pattern, " # %%object%% = getdynfromroom (%[^)])", argument) == 0) {
|
|
sc_free(argument);
|
|
return FALSE;
|
|
}
|
|
|
|
/*
|
|
* Compare the argument read in against known room names.
|
|
*
|
|
* TODO Is this simple room name comparison good enough?
|
|
*/
|
|
vt_key[0].string = "Rooms";
|
|
for (room = 0; room < gs_room_count(game); room++) {
|
|
const sc_char *name;
|
|
|
|
vt_key[1].integer = room;
|
|
vt_key[2].string = "Short";
|
|
name = prop_get_string(bundle, "S<-sis", vt_key);
|
|
if (sc_strcasecmp(name, argument) == 0)
|
|
break;
|
|
}
|
|
sc_free(argument);
|
|
if (room == gs_room_count(game))
|
|
return FALSE;
|
|
|
|
/*
|
|
* Select a dynamic object from the room.
|
|
*
|
|
* TODO What are the selection criteria supposed to be? Here we use "on
|
|
* the floor".
|
|
*/
|
|
vt_key[0].string = "Objects";
|
|
for (object = 0; object < gs_object_count(game); object++) {
|
|
sc_bool bstatic;
|
|
|
|
vt_key[1].integer = object;
|
|
vt_key[2].string = "Static";
|
|
bstatic = prop_get_boolean(bundle, "B<-sis", vt_key);
|
|
if (!bstatic && obj_directly_in_room(game, object, room))
|
|
break;
|
|
}
|
|
if (object == gs_object_count(game))
|
|
return FALSE;
|
|
|
|
/* Set this object reference, unambiguously, as if %object% match. */
|
|
gs_clear_object_references(game);
|
|
game->object_references[object] = TRUE;
|
|
var_set_ref_object(vars, object);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
/* Structure used to associate a pattern with a handler function. */
|
|
struct sc_commands_s {
|
|
const sc_char *const command;
|
|
sc_bool(*const handler)(sc_gameref_t game);
|
|
};
|
|
typedef sc_commands_s sc_commands_t;
|
|
typedef sc_commands_t *sc_commandsref_t;
|
|
|
|
/* Movement commands for the four point compass. */
|
|
static sc_commands_t MOVE_COMMANDS_4[] = {
|
|
{"{go {to {the}}} [north/n]", lib_cmd_go_north},
|
|
{"{go {to {the}}} [east/e]", lib_cmd_go_east},
|
|
{"{go {to {the}}} [south/s]", lib_cmd_go_south},
|
|
{"{go {to {the}}} [west/w]", lib_cmd_go_west},
|
|
{"{go {to {the}}} [up/u]", lib_cmd_go_up},
|
|
{"{go {to {the}}} [down/d]", lib_cmd_go_down},
|
|
{"{go {to {the}}} [in]", lib_cmd_go_in},
|
|
{"{go {to {the}}} [out/o]", lib_cmd_go_out},
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
/* Movement commands for the eight point compass. */
|
|
static sc_commands_t MOVE_COMMANDS_8[] = {
|
|
{"{go {to {the}}} [north/n]", lib_cmd_go_north},
|
|
{"{go {to {the}}} [east/e]", lib_cmd_go_east},
|
|
{"{go {to {the}}} [south/s]", lib_cmd_go_south},
|
|
{"{go {to {the}}} [west/w]", lib_cmd_go_west},
|
|
{"{go {to {the}}} [up/u]", lib_cmd_go_up},
|
|
{"{go {to {the}}} [down/d]", lib_cmd_go_down},
|
|
{"{go {to {the}}} [in]", lib_cmd_go_in},
|
|
{"{go {to {the}}} [out/o]", lib_cmd_go_out},
|
|
{"{go {to {the}}} [northeast/north-east/ne]", lib_cmd_go_northeast},
|
|
{"{go {to {the}}} [southeast/south-east/se]", lib_cmd_go_southeast},
|
|
{"{go {to {the}}} [northwest/north-west/nw]", lib_cmd_go_northwest},
|
|
{"{go {to {the}}} [southwest/south-west/sw]", lib_cmd_go_southwest},
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
/* "Priority" library commands, may take precedence over the game. */
|
|
static sc_commands_t PRIORITY_COMMANDS[] = {
|
|
|
|
/* Acquisition of and disposal of inventory. */
|
|
{
|
|
"[[get/take/remove/extract] [all/everything] from/empty] %object%",
|
|
lib_cmd_take_all_from
|
|
},
|
|
{
|
|
"[[get/take/remove/extract] [all/everything] from/empty] %object%"
|
|
" [[except/but] {for}/apart from] %text%",
|
|
lib_cmd_take_from_except_multiple
|
|
},
|
|
{
|
|
"[get/take/remove/extract] [all/everything]"
|
|
" [[except/but] {for}/apart from] %text% from %object%",
|
|
lib_cmd_take_from_except_multiple
|
|
},
|
|
{
|
|
"[get/take/remove/extract] %text% from %object%",
|
|
lib_cmd_take_from_multiple
|
|
},
|
|
{"[get/take] [all/everything] from %character%", lib_cmd_take_all_from_npc},
|
|
{
|
|
"[get/take] [all/everything] from %character%"
|
|
" [[except/but] {for}/apart from] %text%",
|
|
lib_cmd_take_from_npc_except_multiple
|
|
},
|
|
{
|
|
"[get/take] [all/everything]"
|
|
" [[except/but] {for}/apart from] %text% from %character%",
|
|
lib_cmd_take_from_npc_except_multiple
|
|
},
|
|
{"[get/take] %text% from %character%", lib_cmd_take_from_npc_multiple},
|
|
{
|
|
"[[get/take/pick up] [all/everything]/pick [all/everything] up]",
|
|
lib_cmd_take_all
|
|
},
|
|
{
|
|
"[get/take/pick up] [all/everything] [[except/but] {for}/apart from] %text%",
|
|
lib_cmd_take_except_multiple
|
|
},
|
|
{"[get/take/pick up] %text%", lib_cmd_take_multiple},
|
|
{"pick %text% up", lib_cmd_take_multiple},
|
|
{
|
|
"[[drop/put down] [all/everything]/put [all/everything] down]",
|
|
lib_cmd_drop_all
|
|
},
|
|
{
|
|
"[drop/put down] [all/everything] [[except/but] {for}/apart from] %text%",
|
|
lib_cmd_drop_except_multiple
|
|
},
|
|
{"[drop/put down] %text%", lib_cmd_drop_multiple},
|
|
{"put %text% down", lib_cmd_drop_multiple},
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
/* Standard library commands, other than movement and priority above. */
|
|
static sc_commands_t STANDARD_COMMANDS[] = {
|
|
|
|
/* Inventory, and general investigation of surroundings. */
|
|
{"[inventory/inv/i]", lib_cmd_inventory},
|
|
{"[x/ex/exam/examine/l/look {at}] {{the} [room/location]}", lib_cmd_look},
|
|
{"[x/ex/exam/examine/look {at/in}] %object%", lib_cmd_examine_object},
|
|
{"[x/ex/exam/examine/look {at}] %character%", lib_cmd_examine_npc},
|
|
{"[x/ex/exam/examine/look {at}] [me/self/myself]", lib_cmd_examine_self},
|
|
{"[x/ex/exam/examine/look {at}] all", lib_cmd_examine_all},
|
|
|
|
/* Attempted acquisition of and disposal of NPCs. */
|
|
{"[get/take/pick up] %character%", lib_cmd_take_npc},
|
|
{"pick %character% up", lib_cmd_take_npc},
|
|
|
|
/* Manipulating selected objects. */
|
|
{"put [all/everything] [in/into/inside {of}] %object%", lib_cmd_put_all_in},
|
|
{
|
|
"put [all/everything] [[except/but] {for}/apart from] %text%"
|
|
" [in/into/inside {of}] %object%", lib_cmd_put_in_except_multiple
|
|
},
|
|
{"put %text% [in/into/inside {of}] %object%", lib_cmd_put_in_multiple},
|
|
{"put [all/everything] [on/onto/on top of] %object%", lib_cmd_put_all_on},
|
|
{
|
|
"put [all/everything] [[except/but] {for}/apart from] %text%"
|
|
" [on/onto/on top of] %object%", lib_cmd_put_on_except_multiple
|
|
},
|
|
{"put %text% [on/onto/on top of] %object%", lib_cmd_put_on_multiple},
|
|
{"open %object%", lib_cmd_open_object},
|
|
{"close %object%", lib_cmd_close_object},
|
|
{"unlock %object% with %text%", lib_cmd_unlock_object_with},
|
|
{"lock %object% with %text%", lib_cmd_lock_object_with},
|
|
{"unlock %object%", lib_cmd_unlock_object},
|
|
{"lock %object%", lib_cmd_lock_object},
|
|
{"read %object%", lib_cmd_read_object},
|
|
{"read *", lib_cmd_read_other},
|
|
{"give %object% to %character%", lib_cmd_give_object_npc},
|
|
{"sit {down/up} [on/in] %object%", lib_cmd_sit_on_object},
|
|
{"stand {up/down} [on/in] %object%", lib_cmd_stand_on_object},
|
|
{"[lie/lay] on %object%", lib_cmd_lie_on_object},
|
|
{"get {down/up} off %object%", lib_cmd_get_off_object},
|
|
{"get off", lib_cmd_get_off},
|
|
{"sit {down/up} {[on/in] {the} [ground/floor]}", lib_cmd_sit_on_floor},
|
|
{"stand {up/down} {[on/in] {the} [ground/floor]}", lib_cmd_stand_on_floor},
|
|
{"[lie/lay] {down/up} {[on/in] {the} [ground/floor]}", lib_cmd_lie_on_floor},
|
|
{"eat %object%", lib_cmd_eat_object},
|
|
|
|
/* Dressing up, and dressing down. */
|
|
{
|
|
"[[wear/put on/don] [all/everything]/put [all/everything] on]",
|
|
lib_cmd_wear_all
|
|
},
|
|
{
|
|
"[wear/put on/don] [all/everything] [[except/but] {for}/apart from] %text%",
|
|
lib_cmd_wear_except_multiple
|
|
},
|
|
{"[wear/put on/don] %text%", lib_cmd_wear_multiple},
|
|
{"put %text% on", lib_cmd_wear_multiple},
|
|
{
|
|
"[[remove/take off/doff] [all/everything]/take [all/everything] off/strip]",
|
|
lib_cmd_remove_all
|
|
},
|
|
{
|
|
"[remove/take off/doff] [all/everything]"
|
|
" [[except/but] {for}/apart from] %text%",
|
|
lib_cmd_remove_except_multiple
|
|
},
|
|
{"[remove/take off/doff] %text%", lib_cmd_remove_multiple},
|
|
{"take %text% off", lib_cmd_remove_multiple},
|
|
|
|
/* Selected NPC interactions and conversation. */
|
|
{"ask %character% about %text%", lib_cmd_ask_npc_about},
|
|
{
|
|
"[attack/hit/kick/slap/shoot/stab] %character% with %object%",
|
|
lib_cmd_attack_npc_with
|
|
},
|
|
{"[attack/shoot] %character%", lib_cmd_attack_npc},
|
|
|
|
/* More movement, waiting, and miscellaneous administrative commands. */
|
|
{"[goto/go {to}] %text%", lib_cmd_go_room},
|
|
{"[goto/go {to}] *", lib_cmd_print_room_exits},
|
|
{"[exit/exits/directions/where]", lib_cmd_print_room_exits},
|
|
{"[wait/z] %number%", lib_cmd_wait_number},
|
|
{"[wait/z]", lib_cmd_wait},
|
|
{"save", lib_cmd_save},
|
|
{"[restore/load]", lib_cmd_restore},
|
|
{"restart", lib_cmd_restart},
|
|
{"[again/g]", lib_cmd_again},
|
|
{"[redo /!]%number%", lib_cmd_redo_number},
|
|
{"[redo /!]%text%", lib_cmd_redo_text},
|
|
{"[redo/!]", lib_cmd_redo_last},
|
|
{"[quit/q]", lib_cmd_quit},
|
|
{"turns", lib_cmd_turns},
|
|
{"score", lib_cmd_score},
|
|
{"undo", lib_cmd_undo},
|
|
{"[hist/history] %number%", lib_cmd_history_number},
|
|
{"[hist/history]", lib_cmd_history},
|
|
{"[hint/hints]", lib_cmd_hints},
|
|
{"verbose", lib_cmd_verbose},
|
|
{"brief", lib_cmd_brief},
|
|
{"[notify/notification] %text%", lib_cmd_notify_on_off},
|
|
{"[notify/notification]", lib_cmd_notify},
|
|
{"time", lib_cmd_time},
|
|
{"date", lib_cmd_date},
|
|
{"[help/commands]", lib_cmd_help},
|
|
{"[gpl/license]", lib_cmd_license},
|
|
{"[about/info/information/author]", lib_cmd_information},
|
|
{"[clear/cls/clr]", lib_cmd_clear},
|
|
{"status{line}", lib_cmd_statusline},
|
|
{"version", lib_cmd_version},
|
|
|
|
{"[locate/where {is/are}/find] %object%", lib_cmd_locate_object},
|
|
{"[locate/where {is}/find] %character%", lib_cmd_locate_npc},
|
|
|
|
{"[count/num]", lib_cmd_count},
|
|
|
|
/* Standard response commands; no real action, just output. */
|
|
{"[get/take/pick up] *", lib_cmd_get_what},
|
|
{"open *", lib_cmd_open_what},
|
|
{"close *", lib_cmd_close_other},
|
|
{"give %object% *", lib_cmd_give_object},
|
|
{"give *", lib_cmd_give_what},
|
|
{"lock %text%", lib_cmd_lock_other},
|
|
{"lock", lib_cmd_lock_what},
|
|
{"unlock %text%", lib_cmd_unlock_other},
|
|
{"unlock", lib_cmd_unlock_what},
|
|
{"sit {down/up} [on/in] *", lib_cmd_sit_other},
|
|
{"stand {up/down} [on/in] *", lib_cmd_stand_other},
|
|
{"[lie/lay] {down/up} [on/in] *", lib_cmd_lie_other},
|
|
{"[remove/take off/doff] *", lib_cmd_remove_what},
|
|
{"[drop/put down] *", lib_cmd_drop_what},
|
|
{"[wear/put on/don] *", lib_cmd_wear_what},
|
|
{
|
|
"[shit/fuck/bastard/cunt/crap/hell/shag/bollocks/bollox/bugger] *",
|
|
lib_cmd_profanity
|
|
},
|
|
{"[x/examine/look {at}] *", lib_cmd_examine_other},
|
|
{"[locate/where {is/are}/find] *", lib_cmd_locate_other},
|
|
{"[cp/mv/ln/ls] *", lib_cmd_unix_like},
|
|
{"dir *", lib_cmd_dos_like},
|
|
{"ask %character% *", lib_cmd_ask_npc},
|
|
{"ask %object% *", lib_cmd_ask_object},
|
|
{"ask *", lib_cmd_ask_other},
|
|
{"block %object% *", lib_cmd_block_object},
|
|
{"block %text%", lib_cmd_block_other},
|
|
{"block", lib_cmd_block_what},
|
|
{"[break/destroy/smash] %object% *", lib_cmd_break_object},
|
|
{"[break/destroy/smash] %text%", lib_cmd_break_other},
|
|
{"break", lib_cmd_break_what},
|
|
{"destroy", lib_cmd_destroy_what},
|
|
{"smash", lib_cmd_smash_what},
|
|
{"buy %object% *", lib_cmd_buy_object},
|
|
{"buy %text%", lib_cmd_buy_other},
|
|
{"buy", lib_cmd_buy_what},
|
|
{"clean %object% *", lib_cmd_clean_object},
|
|
{"clean %text%", lib_cmd_clean_other},
|
|
{"clean", lib_cmd_clean_what},
|
|
{"climb %object% *", lib_cmd_climb_object},
|
|
{"climb %text%", lib_cmd_climb_other},
|
|
{"climb", lib_cmd_climb_what},
|
|
{"cry *", lib_cmd_cry},
|
|
{"cut %object% *", lib_cmd_cut_object},
|
|
{"cut %text%", lib_cmd_cut_other},
|
|
{"cut", lib_cmd_cut_what},
|
|
{"dance *", lib_cmd_dance},
|
|
{"drink %object% *", lib_cmd_drink_object},
|
|
{"drink %text%", lib_cmd_drink_other},
|
|
{"drink", lib_cmd_drink_what},
|
|
{"eat *", lib_cmd_eat_other},
|
|
{"feed *", lib_cmd_feed},
|
|
{"feel *", lib_cmd_feel},
|
|
{"fight *", lib_cmd_fight},
|
|
{"fix %object% *", lib_cmd_fix_object},
|
|
{"fix %text%", lib_cmd_fix_other},
|
|
{"fix", lib_cmd_fix_what},
|
|
{"fly *", lib_cmd_fly},
|
|
{"hint *", lib_cmd_hint},
|
|
{"hit %character%", lib_cmd_attack_npc},
|
|
{"hit %object% *", lib_cmd_hit_object},
|
|
{"hit %text%", lib_cmd_hit_other},
|
|
{"hit", lib_cmd_hit_what},
|
|
{"hum *", lib_cmd_hum},
|
|
{"jump *", lib_cmd_jump},
|
|
{"kick %character%", lib_cmd_attack_npc},
|
|
{"kick %object% *", lib_cmd_kick_object},
|
|
{"kick %text%", lib_cmd_kick_other},
|
|
{"kick", lib_cmd_kick_what},
|
|
{"kiss %character% *", lib_cmd_kiss_npc},
|
|
{"kiss %object% *", lib_cmd_kiss_object},
|
|
{"kiss *", lib_cmd_kiss_other},
|
|
{"kill *", lib_cmd_kill_other},
|
|
{"lift %object% *", lib_cmd_lift_object},
|
|
{"lift %text%", lib_cmd_lift_other},
|
|
{"lift", lib_cmd_lift_what},
|
|
{"light %object% *", lib_cmd_light_object},
|
|
{"light %text%", lib_cmd_light_other},
|
|
{"light", lib_cmd_light_what},
|
|
{"listen *", lib_cmd_listen},
|
|
{"mend %object% *", lib_cmd_mend_object},
|
|
{"mend %text%", lib_cmd_mend_other},
|
|
{"mend", lib_cmd_mend_what},
|
|
{"move %object% *", lib_cmd_move_object},
|
|
{"move %text%", lib_cmd_move_other},
|
|
{"move", lib_cmd_move_what},
|
|
{"please *", lib_cmd_please},
|
|
{"press %object% *", lib_cmd_press_object},
|
|
{"press %text%", lib_cmd_press_other},
|
|
{"press", lib_cmd_press_what},
|
|
{"pull %object% *", lib_cmd_pull_object},
|
|
{"pull %text%", lib_cmd_pull_other},
|
|
{"pull", lib_cmd_pull_what},
|
|
{"punch *", lib_cmd_punch},
|
|
{"push %object% *", lib_cmd_push_object},
|
|
{"push %text%", lib_cmd_push_other},
|
|
{"push", lib_cmd_push_what},
|
|
{"repair %object% *", lib_cmd_repair_object},
|
|
{"repair %text%", lib_cmd_repair_other},
|
|
{"repair", lib_cmd_repair_what},
|
|
{"rub %object% *", lib_cmd_rub_object},
|
|
{"rub %text%", lib_cmd_rub_other},
|
|
{"rub", lib_cmd_rub_what},
|
|
{"run *", lib_cmd_run},
|
|
{"say *", lib_cmd_say},
|
|
{"sell %object% *", lib_cmd_sell_object},
|
|
{"sell %text%", lib_cmd_sell_other},
|
|
{"sell", lib_cmd_sell_what},
|
|
{"shake %object% *", lib_cmd_shake_object},
|
|
{"shake %text%", lib_cmd_shake_other},
|
|
{"shake", lib_cmd_shake_what},
|
|
{"shout *", lib_cmd_shout},
|
|
{"sing *", lib_cmd_sing},
|
|
{"sleep *", lib_cmd_sleep},
|
|
{"smell %object% *", lib_cmd_smell_object},
|
|
{"smell *", lib_cmd_smell_other},
|
|
{"stop %object% *", lib_cmd_stop_object},
|
|
{"stop %text%", lib_cmd_stop_other},
|
|
{"stop", lib_cmd_stop_what},
|
|
{"suck %object% *", lib_cmd_suck_object},
|
|
{"suck %text%", lib_cmd_suck_other},
|
|
{"suck", lib_cmd_suck_what},
|
|
{"talk *", lib_cmd_talk},
|
|
{"thank *", lib_cmd_thank},
|
|
{"turn %object% *", lib_cmd_turn_object},
|
|
{"turn %text%", lib_cmd_turn_other},
|
|
{"turn", lib_cmd_turn_what},
|
|
{"touch %object% *", lib_cmd_touch_object},
|
|
{"touch %text%", lib_cmd_touch_other},
|
|
{"touch", lib_cmd_touch_what},
|
|
{"unblock %object% *", lib_cmd_unblock_object},
|
|
{"unblock %text%", lib_cmd_unblock_other},
|
|
{"unblock", lib_cmd_unblock_what},
|
|
{"wash %object% *", lib_cmd_wash_object},
|
|
{"wash %text%", lib_cmd_wash_other},
|
|
{"wash", lib_cmd_wash_what},
|
|
{"whistle *", lib_cmd_whistle},
|
|
{"[why/when/what/can/how] *", lib_cmd_interrogation},
|
|
{"xyzzy *", lib_cmd_xyzzy},
|
|
{"campbell", lib_cmd_egotistic},
|
|
{"[yes/no] *", lib_cmd_yes_or_no},
|
|
{"* %object% *", lib_cmd_verb_object},
|
|
{"* %character% *", lib_cmd_verb_npc},
|
|
|
|
/* SCARE debugger hook command, placed last just in case... */
|
|
{"{#}debug{ger}", debug_cmd_debugger},
|
|
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
|
|
/*
|
|
* run_priority_commands()
|
|
* run_standard_commands()
|
|
*
|
|
* Compare a user input string against commands recognized by the library,
|
|
* and action any command. Returns TRUE if the string matched a command
|
|
* that then ran successfully, FALSE otherwise.
|
|
*
|
|
* "Priority" commands are ones that Adrift seems to action no matter what
|
|
* the game tries to override. For example, a simple game with one "ball"
|
|
* object and a task "* ball *" should, if the task is restricted, override
|
|
* "take ball" such that the ball can never be acquired. Adrift lets the
|
|
* "take" succeed, though (and more curiously, may respond "I don't
|
|
* understand..." to "drop ball"). This could be an Adrift bug. Shrug.
|
|
*
|
|
* For now, I can't find any better way to try to handle it than to make
|
|
* object acquisition take precedence over game commands.
|
|
*/
|
|
static sc_bool run_priority_commands(sc_gameref_t game, const sc_char *string) {
|
|
sc_commandsref_t command;
|
|
|
|
for (command = PRIORITY_COMMANDS; command->command; command++) {
|
|
if (uip_match(command->command, string, game)) {
|
|
if (command->handler(game))
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
/* Nothing matched match the string. Or if it did, its handler failed. */
|
|
return FALSE;
|
|
}
|
|
|
|
static sc_bool run_standard_commands(sc_gameref_t game, const sc_char *string) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
sc_vartype_t vt_key[2];
|
|
sc_bool eightpointcompass;
|
|
sc_commandsref_t command;
|
|
|
|
/* Select the appropriate movement commands. */
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "EightPointCompass";
|
|
eightpointcompass = prop_get_boolean(bundle, "B<-ss", vt_key);
|
|
command = eightpointcompass ? MOVE_COMMANDS_8 : MOVE_COMMANDS_4;
|
|
|
|
/*
|
|
* Search movement commands first, returning TRUE if any matching command
|
|
* handler succeeded. Then repeat for standard library commands.
|
|
*/
|
|
for (; command->command; command++) {
|
|
if (uip_match(command->command, string, game)) {
|
|
if (command->handler(game))
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
for (command = STANDARD_COMMANDS; command->command; command++) {
|
|
if (uip_match(command->command, string, game)) {
|
|
if (command->handler(game))
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
/* Nothing matched match the string. Or if it did, its handler failed. */
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_update_status()
|
|
*
|
|
* Update the game's current room and status line strings.
|
|
*/
|
|
static void run_update_status(sc_gameref_t game) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
sc_vartype_t vt_key[2];
|
|
const sc_char *name, *status;
|
|
sc_char *filtered;
|
|
sc_bool statusbox;
|
|
|
|
/* Get the current room name, and filter and untag it. */
|
|
name = lib_get_room_name(game, gs_playerroom(game));
|
|
filtered = pf_filter(name, vars, bundle);
|
|
pf_strip_tags(filtered);
|
|
|
|
/* Free any existing room name, then save this room name. */
|
|
sc_free(game->current_room_name);
|
|
game->current_room_name = filtered;
|
|
|
|
/* See if the game does a status box. */
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "StatusBox";
|
|
statusbox = prop_get_boolean(bundle, "B<-ss", vt_key);
|
|
if (statusbox) {
|
|
/* Get the status line, and filter and untag it. */
|
|
vt_key[1].string = "StatusBoxText";
|
|
status = prop_get_string(bundle, "S<-ss", vt_key);
|
|
filtered = pf_filter(status, vars, bundle);
|
|
pf_strip_tags(filtered);
|
|
} else
|
|
/* No status line, so use NULL. */
|
|
filtered = nullptr;
|
|
|
|
/* Free any existing status line, then save this status text. */
|
|
sc_free(game->status_line);
|
|
game->status_line = filtered;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_notify_score_change()
|
|
*
|
|
* Print an indication of any score change, if appropriate. The change is
|
|
* detected by comparing against the undo game. Uses if_print_string()
|
|
* directly for printing, rather than the filter, so that it can place its
|
|
* output ahead of buffered printfilter text.
|
|
*/
|
|
static void run_notify_score_change(sc_gameref_t game) {
|
|
const sc_gameref_t undo = game->undo;
|
|
sc_char buffer[32];
|
|
assert(gs_is_game_valid(undo));
|
|
|
|
/*
|
|
* Do nothing if no undo available, or if notification is off, or if we've
|
|
* already done this once this turn.
|
|
*/
|
|
if (!game->undo_available
|
|
|| !game->notify_score_change || game->has_notified)
|
|
return;
|
|
|
|
/* Note any change in the score. */
|
|
if (game->score > undo->score) {
|
|
if_print_string("(Your score has increased by ");
|
|
Common::sprintf_s(buffer, "%ld", game->score - undo->score);
|
|
if_print_string(buffer);
|
|
if_print_string(")\n");
|
|
} else if (game->score < undo->score) {
|
|
if_print_string("(Your score has decreased by ");
|
|
Common::sprintf_s(buffer, "%ld", undo->score - game->score);
|
|
if_print_string(buffer);
|
|
if_print_string(")\n");
|
|
}
|
|
game->has_notified = TRUE;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_match_task_common()
|
|
* run_match_task_commands()
|
|
* run_match_task_functions()
|
|
*
|
|
* Helpers for run_game_commands_common().
|
|
*
|
|
* Search task command for a match to the string passed in, returning TRUE
|
|
* if a task command matches, FALSE otherwise. Ordinary or reverse commands
|
|
* are selected by 'forwards'.
|
|
*/
|
|
static sc_bool run_match_task_common(sc_gameref_t game, sc_int task, const sc_char *string,
|
|
sc_bool forwards, sc_bool is_library, sc_bool is_normal) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
sc_vartype_t vt_key[4];
|
|
sc_int command_count, command;
|
|
sc_bool is_matched;
|
|
|
|
/* Get the count of task commands. */
|
|
vt_key[0].string = "Tasks";
|
|
vt_key[1].integer = task;
|
|
vt_key[2].string = forwards ? "Command" : "ReverseCommand";
|
|
command_count = prop_get_child_count(bundle, "I<-sis", vt_key);
|
|
|
|
/* Iterate over commands, looking for patterns that match string. */
|
|
is_matched = FALSE;
|
|
for (command = 0; command < command_count; command++) {
|
|
const sc_char *pattern;
|
|
sc_int first;
|
|
|
|
/* Retrieve the pattern for this command, find its first character. */
|
|
vt_key[3].integer = command;
|
|
pattern = prop_get_string(bundle, "S<-sisi", vt_key);
|
|
first = strspn(pattern, WHITESPACE);
|
|
|
|
/* Match using either the parser, or the special function matcher. */
|
|
if (is_normal) {
|
|
if (pattern[first] != SPECIAL_PATTERN) {
|
|
/*
|
|
* Make a special case of library calls and commands that begin
|
|
* with a wildcard; these we ignore for this match attempt.
|
|
*/
|
|
if (is_library && pattern[first] == WILDCARD_PATTERN)
|
|
is_matched = FALSE;
|
|
else
|
|
is_matched = uip_match(pattern, string, game);
|
|
}
|
|
} else {
|
|
if (pattern[first] == SPECIAL_PATTERN)
|
|
is_matched = run_is_task_function(pattern, game);
|
|
}
|
|
|
|
/* Stop searching if we find a match. */
|
|
if (is_matched)
|
|
break;
|
|
}
|
|
|
|
/* Return TRUE if we found a pattern match. */
|
|
return is_matched;
|
|
}
|
|
|
|
static sc_bool run_match_task_commands(sc_gameref_t game, sc_int task,
|
|
const sc_char *string, sc_bool forwards, sc_bool is_library) {
|
|
/*
|
|
* Match tasks using the normal pattern matcher, with or without any note
|
|
* about whether the call is from the library.
|
|
*/
|
|
return run_match_task_common(game, task, string, forwards, is_library, TRUE);
|
|
}
|
|
|
|
static sc_bool run_match_task_functions(sc_gameref_t game, sc_int task,
|
|
const sc_char *string, sc_bool forwards) {
|
|
/* Match tasks against "task command functions". */
|
|
return run_match_task_common(game, task, string, forwards, FALSE, FALSE);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_task_is_unrestricted()
|
|
* run_task_is_loudly_restricted()
|
|
*
|
|
* Helpers for run_game_commands_common().
|
|
*
|
|
* Adapters for uncovering task restriction state. The first returns TRUE
|
|
* if the task is unrestricted, and can therefore run unimpeded. The second
|
|
* returns TRUE iff the task is restricted and has a fail message that
|
|
* indicates why it fails; such tasks, if run, produce their failure message
|
|
* and don't change state.
|
|
*/
|
|
static sc_bool run_task_is_unrestricted(sc_gameref_t game, sc_int task) {
|
|
sc_bool restrictions_passed;
|
|
const sc_char *fail_message;
|
|
|
|
/*
|
|
* Evaluate task restrictions, and if they fail to parse for some reason,
|
|
* return as if restrictions did not pass.
|
|
*/
|
|
if (!restr_eval_task_restrictions(game, task,
|
|
&restrictions_passed, &fail_message)) {
|
|
sc_error("run_task_is_unrestricted: restrictions error, %ld\n", task);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Return TRUE if the task is unrestricted. */
|
|
return restrictions_passed;
|
|
}
|
|
|
|
static sc_bool run_task_is_loudly_restricted(sc_gameref_t game, sc_int task) {
|
|
sc_bool restrictions_passed;
|
|
const sc_char *fail_message;
|
|
|
|
/*
|
|
* Evaluate task restrictions, and if they fail to parse for some reason,
|
|
* return as if restrictions did not pass.
|
|
*/
|
|
if (!restr_eval_task_restrictions(game, task,
|
|
&restrictions_passed, &fail_message)) {
|
|
sc_error("run_task_is_loudly_restricted:"
|
|
" restrictions error, %ld\n", task);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Return TRUE if the task is restricted and indicates why. */
|
|
return !restrictions_passed && (fail_message != nullptr);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_game_commands_common()
|
|
* run_game_commands_in_parser_context()
|
|
* run_game_commands_in_library_context()
|
|
*
|
|
* The central handler for running, or at least trying to run, game-defined
|
|
* tasks that have commands that match the input string. Here's the algorithm
|
|
* as currently understood (and it may not be right, so be warned):
|
|
*
|
|
* for each task executable in the current room
|
|
* for direction in forwards, backwards
|
|
* for each command string defined by the task for this direction
|
|
* match against player input
|
|
* if any command string matched player input
|
|
* if task restrictions pass
|
|
* run the task actions in the current direction
|
|
* if the task actions produced output
|
|
* return
|
|
* is_matched := true
|
|
* break out of all loops
|
|
*
|
|
* if not is_matched and we're allowing restrictions to fail tasks
|
|
* for each task executable in the current room
|
|
* for direction in forwards, backwards
|
|
* for each command string defined by the task for this direction
|
|
* match against player input
|
|
* if any command string matched player input
|
|
* if task restrictions fail with an error message
|
|
* run the task, to persuade it to print this error message
|
|
* return
|
|
*
|
|
* Part of the fun and games is that run_game_task_commands() is called by the
|
|
* library to try to run "get " and "drop " game commands for standard get/drop
|
|
* handlers and get_all/drop_all handlers. No pressure, then.
|
|
*/
|
|
static sc_bool run_game_commands_common(sc_gameref_t game, const sc_char *string,
|
|
sc_bool include_restrictions, sc_bool is_library) {
|
|
sc_bool is_matched = FALSE, is_handled = FALSE;
|
|
sc_bool *is_matching;
|
|
sc_int task_count, task, direction;
|
|
|
|
/*
|
|
* Matching is expensive, so it helps to use a cache of results from the
|
|
* first loop in the second. If we're using the second, that is.
|
|
*/
|
|
task_count = gs_task_count(game);
|
|
if (include_restrictions) {
|
|
is_matching = (sc_bool *)sc_malloc(task_count * sizeof(*is_matching));
|
|
memset(is_matching, FALSE, task_count * sizeof(*is_matching));
|
|
} else
|
|
is_matching = nullptr;
|
|
|
|
/*
|
|
* Iterate over every task, ignoring those not runnable. For each runnable
|
|
* task, try matching task commands, and on matches, check restrictions and
|
|
* if they pass, try running the task.
|
|
*/
|
|
for (task = 0; task < task_count; task++) {
|
|
if (!task_can_run_task(game, task))
|
|
continue;
|
|
|
|
/*
|
|
* Try matching forwards and reverse commands. If there's a match for
|
|
* unrestricted tasks, run the task, and if it runs (defined as printing
|
|
* some game output), we're done; otherwise, note the command match but
|
|
* keep searching for other possible matches.
|
|
*/
|
|
for (direction = 0; direction < 2; direction++) {
|
|
const sc_bool is_forwards = !direction;
|
|
|
|
if (task_can_run_task_directional(game, task, is_forwards)
|
|
&& run_match_task_commands(game, task, string,
|
|
is_forwards, is_library)) {
|
|
if (run_task_is_unrestricted(game, task)) {
|
|
if (task_run_task(game, task, is_forwards))
|
|
is_handled = TRUE;
|
|
is_matched = TRUE;
|
|
break;
|
|
}
|
|
|
|
if (is_matching)
|
|
is_matching[task] = TRUE;
|
|
}
|
|
}
|
|
if (is_matched)
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* If no match, and we've been asked to consider failing restrictions, look
|
|
* through all of the runnable tasks again, this time searching for
|
|
* restricted ones with a fail message. Use the cache built above to weed
|
|
* out matches that are certain to fail.
|
|
*/
|
|
if (!is_handled && !is_matched && include_restrictions) {
|
|
for (task = 0; task < task_count; task++) {
|
|
if (!is_matching[task] || !task_can_run_task(game, task))
|
|
continue;
|
|
|
|
/*
|
|
* Check matches of forwards and reverse commands. If there's a
|
|
* match for restricted tasks (ones that have and will print a fail
|
|
* message if we try to run them), run the task to get the print of
|
|
* the fail message, and we're done.
|
|
*/
|
|
for (direction = 0; direction < 2; direction++) {
|
|
const sc_bool is_forwards = !direction;
|
|
|
|
if (task_can_run_task_directional(game, task, is_forwards)
|
|
&& run_match_task_commands(game, task, string,
|
|
is_forwards, is_library)) {
|
|
if (run_task_is_loudly_restricted(game, task)) {
|
|
if (task_run_task(game, task, is_forwards)) {
|
|
is_handled = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (is_handled)
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Return TRUE if any game task handled the command in some way. */
|
|
sc_free(is_matching);
|
|
return is_handled;
|
|
}
|
|
|
|
static sc_bool run_game_commands_in_parser_context(sc_gameref_t game,
|
|
const sc_char *string, sc_bool include_restrictions) {
|
|
/*
|
|
* Try game commands, either with or without restrictions, and all full and
|
|
* complete parse matching (no special case for game commands that begin
|
|
* with a '*' wildcard).
|
|
*/
|
|
return run_game_commands_common(game, string, include_restrictions, FALSE);
|
|
}
|
|
|
|
static sc_bool run_game_commands_in_library_context(sc_gameref_t game, const sc_char *string) {
|
|
/*
|
|
* Try game commands, including restrictions, and noting that this is a
|
|
* library call so that the parse matcher can exclude game commands that
|
|
* begin with a '*' wildcard.
|
|
*/
|
|
return run_game_commands_common(game, string, TRUE, TRUE);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_game_functions()
|
|
*
|
|
* Iterate over every task, ignoring those not runnable, searching just for
|
|
* "task command functions". These seem to happen in addition to any regular
|
|
* command matches, so we try them as a separate action.
|
|
*/
|
|
static void run_game_functions(sc_gameref_t game, const sc_char *string) {
|
|
sc_int task_count, task, direction;
|
|
|
|
/* Iterate over every task, ignoring those not runnable. */
|
|
task_count = gs_task_count(game);
|
|
for (task = 0; task < task_count; task++) {
|
|
if (!task_can_run_task(game, task))
|
|
continue;
|
|
|
|
/*
|
|
* Try matching forwards and reverse commands. I don't know if it's
|
|
* valid to put a function in a reverse command, but nevertheless...
|
|
*/
|
|
for (direction = 0; direction < 2; direction++) {
|
|
const sc_bool is_forwards = !direction;
|
|
|
|
if (task_can_run_task_directional(game, task, is_forwards)
|
|
&& run_match_task_functions(game, task, string, is_forwards)) {
|
|
if (run_task_is_unrestricted(game, task))
|
|
task_run_task(game, task, is_forwards);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* run_all_commands()
|
|
* run_game_task_commands()
|
|
*
|
|
* Alternative facets of run_commands_common(). The first is used by the
|
|
* main user input handling loop; the latter by the library when looking for
|
|
* game commands that override standard actions.
|
|
*/
|
|
static sc_bool run_all_commands(sc_gameref_t game, const sc_char *string) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
sc_bool status;
|
|
|
|
/*
|
|
* Adrift command matching is just weird, perhaps broken. In theory, a
|
|
* game can override system commands with a properly constructed task and
|
|
* set of command matchers. However, the Runner isn't terribly consistent
|
|
* in when this will work and when not, and some games rely on that in-
|
|
* consistency. In particular, a game with a "* object" task that has
|
|
* failing restrictions will not be able to override the system's "take
|
|
* object", whereas a game's "take object", under the same circumstances,
|
|
* will. Yet if the restrictions pass, a game's "* object" overrides the
|
|
* system's "take object" with no apparent difficulty.
|
|
*
|
|
* For example, "The Woods Are Dark" has a "* ball *" task with the
|
|
* restriction "must be holding ball". Without special casing it, there's
|
|
* no way to get the ball in the first place.
|
|
*
|
|
* Trying to find the right way to do things here, then, has been tricky.
|
|
* Here's the current process: First, run game commands, ignoring any
|
|
* cases where restrictions fail to let the task run. Next, try "priority"
|
|
* system commands; ones that move objects to inventory. These system
|
|
* commands will call back into trying game commands for objects taken or
|
|
* dropped, and in those tries, allow overrides only if the game task is
|
|
* explicit about what it's doing (that is, doesn't start with "*"), and
|
|
* handle restrictions in those tries. After that, retry all game commands
|
|
* again with restrictions enabled. And finally, try all other standard
|
|
* library commands.
|
|
*
|
|
* TODO This is the fourth or fifth attempt at getting this to match the
|
|
* Runner, which is surprisingly inconsistent in this area. What on earth
|
|
* is the real behavior supposed to be?
|
|
*/
|
|
status = run_game_commands_in_parser_context(game, string, FALSE);
|
|
if (!status)
|
|
status = run_priority_commands(game, string);
|
|
if (!status)
|
|
status = run_game_commands_in_parser_context(game, string, TRUE);
|
|
if (!status)
|
|
status = run_standard_commands(game, string);
|
|
|
|
/*
|
|
* For version 4.0 games, it seems that if any command succeeded, we need
|
|
* need to scan for and run any matching "task command functions", in
|
|
* addition to anything done above.
|
|
*/
|
|
if (status && !game->is_admin) {
|
|
sc_vartype_t vt_key;
|
|
sc_int version;
|
|
|
|
/* Check "task command functions" for version 4.0 only. */
|
|
vt_key.string = "Version";
|
|
version = prop_get_integer(bundle, "I<-s", &vt_key);
|
|
if (version == TAF_VERSION_400)
|
|
run_game_functions(game, string);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
sc_bool run_game_task_commands(sc_gameref_t game, const sc_char *string) {
|
|
return run_game_commands_in_library_context(game, string);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_player_input()
|
|
*
|
|
* Take a line of player input and buffer it. Split the line into elements
|
|
* separated by periods. For the first element, try to match it to either a
|
|
* task or a standard command, and return TRUE if it matched, FALSE otherwise.
|
|
*
|
|
* On subsequent calls, successively work with the next line element until
|
|
* none remain. In this case, prompt for more player input and continue as
|
|
* above.
|
|
*
|
|
* For the case of "again" or "g", rerun the last successful command element.
|
|
*
|
|
* One extra special special case; if called with a game that is not running,
|
|
* this is a signal to reset all noted line input to initial conditions, and
|
|
* just return. Sorry about the ugliness.
|
|
*/
|
|
static sc_bool run_player_input(sc_gameref_t game) {
|
|
static sc_char line_buffer[LINE_BUFFER_SIZE];
|
|
static sc_char prior_element[LINE_BUFFER_SIZE];
|
|
static sc_char line_element[LINE_BUFFER_SIZE];
|
|
|
|
const sc_filterref_t filter = gs_get_filter(game);
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
const sc_memo_setref_t memento = gs_get_memento(game);
|
|
sc_bool is_rerunning, was_undo_available, status;
|
|
sc_char *filtered, *replaced;
|
|
const sc_char *command;
|
|
|
|
/* Special case; reset statics if the game isn't running. */
|
|
if (!game->is_running) {
|
|
memset(line_buffer, NUL, sizeof(line_buffer));
|
|
memset(prior_element, NUL, sizeof(prior_element));
|
|
memset(line_element, NUL, sizeof(line_element));
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Save the settings of the game's do_again and undo_available flags for
|
|
* later checks.
|
|
*/
|
|
is_rerunning = game->do_again;
|
|
was_undo_available = game->undo_available;
|
|
|
|
/* See if the player asked to rerun a command element. */
|
|
if (game->do_again) {
|
|
game->do_again = FALSE;
|
|
|
|
/* Check there is a last element to repeat. */
|
|
if (prior_element[0] == NUL) {
|
|
pf_buffer_string(filter, "You can hardly repeat that.\n");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Make the last element the current input element. */
|
|
Common::strcpy_s(line_element, prior_element);
|
|
} else {
|
|
sc_int length, extent;
|
|
|
|
/*
|
|
* If there's none buffered, read a new line of player input. Other-
|
|
* wise, separate output so far with a newline.
|
|
*/
|
|
if (line_buffer[0] == NUL)
|
|
if_read_line(line_buffer, sizeof(line_buffer));
|
|
else
|
|
if_print_character('\n');
|
|
|
|
/*
|
|
* Find the length of the next input line element. Unless the line
|
|
* buffer is empty, we always take the first character, even if it's a
|
|
* separator. This catches odd input like "." and turns it into a
|
|
* parser complaint, rather than treating it as two empty commands with
|
|
* a separator between them; this makes it close to what Inform does
|
|
* with similar inputs.
|
|
*/
|
|
length = (line_buffer[0] == NUL) ? 0 : 1;
|
|
while (line_buffer[length] != NUL
|
|
&& strchr(SEPARATORS, line_buffer[length]) == nullptr)
|
|
length++;
|
|
|
|
/*
|
|
* Make this the current input element, and remove it, the separator,
|
|
* and any trailing whitespace, from the front of the line buffer.
|
|
* Removing whitespace prevents "i. ." looking like "i" and ""; it
|
|
* instead looks like "i" and ".", and results in a parser complaint.
|
|
*/
|
|
memcpy(line_element, line_buffer, length);
|
|
line_element[length] = NUL;
|
|
|
|
extent = length;
|
|
extent += (line_buffer[length] == NUL
|
|
|| strchr(SEPARATORS, line_buffer[length]) == nullptr) ? 0 : 1;
|
|
extent += strspn(line_buffer + extent, WHITESPACE);
|
|
memmove(line_buffer,
|
|
line_buffer + extent, strlen(line_buffer) - extent + 1);
|
|
}
|
|
|
|
/* Copy the current game to the temporary undo buffer. */
|
|
gs_copy(game->temporary, game);
|
|
|
|
/* Filter the input element for synonyms, then for pronouns. */
|
|
filtered = pf_filter_input(line_element, bundle);
|
|
replaced = uip_replace_pronouns(game, filtered ? filtered : line_element);
|
|
|
|
/*
|
|
* If filtering didn't replace synonyms, or no pronouns were replaced, use
|
|
* the original line element.
|
|
*/
|
|
command = replaced ? sc_normalize_string(replaced)
|
|
: (filtered ? sc_normalize_string(filtered) : line_element);
|
|
if (command != line_element) {
|
|
if_print_tag(SC_TAG_ITALICS, "");
|
|
if_print_character('[');
|
|
if_print_string(command);
|
|
if_print_character(']');
|
|
if_print_tag(SC_TAG_ENDITALICS, "");
|
|
if_print_character('\n');
|
|
}
|
|
|
|
/* Try the command line element against command matchers. */
|
|
status = run_all_commands(game, command);
|
|
if (!status) {
|
|
/* Only complain on non-empty command input line elements. */
|
|
if (!sc_strempty(command)) {
|
|
sc_vartype_t vt_key[2];
|
|
sc_char *escaped;
|
|
const sc_char *message;
|
|
|
|
/* Command line element not understood. */
|
|
escaped = pf_escape(sc_normalize_string(line_element));
|
|
var_set_ref_text(vars, escaped);
|
|
sc_free(escaped);
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "DontUnderstand";
|
|
message = prop_get_string(bundle, "S<-ss", vt_key);
|
|
pf_buffer_string(filter, message);
|
|
pf_buffer_character(filter, '\n');
|
|
|
|
/*
|
|
* On a line element that's not understood, throw out any remaining
|
|
* input line elements.
|
|
*/
|
|
line_buffer[0] = NUL;
|
|
sc_free(filtered);
|
|
sc_free(replaced);
|
|
return status;
|
|
}
|
|
} else {
|
|
/*
|
|
* Unless administrative, back up any valid undo, copy the temporary
|
|
* game into the undo buffer, flag the undo buffer as available, and
|
|
* assign any pronouns used in the command ready for the next iteration.
|
|
*/
|
|
if (!game->is_admin) {
|
|
if (game->undo_available)
|
|
memo_save_game(memento, game->undo);
|
|
|
|
gs_copy(game->undo, game->temporary);
|
|
game->undo_available = TRUE;
|
|
|
|
uip_assign_pronouns(game, command);
|
|
}
|
|
}
|
|
sc_free(filtered);
|
|
sc_free(replaced);
|
|
|
|
/*
|
|
* If do_again is set, we'll come round with the prior command in line
|
|
* element in a moment, so save nothing for that case. Otherwise save the
|
|
* command in the history.
|
|
*/
|
|
if (!sc_strempty(line_element) && !game->do_again) {
|
|
/*
|
|
* If this is a failed redo, redo_sequence will be set but do_again will
|
|
* be clear. Suppress the save for this special case; otherwise, failed
|
|
* redo commands get into the history, where they can cause problems
|
|
* later on.
|
|
*/
|
|
if (game->redo_sequence == 0) {
|
|
sc_int timestamp;
|
|
|
|
timestamp = var_get_elapsed_seconds(vars);
|
|
memo_save_command(memento, line_element, timestamp, game->turns);
|
|
} else
|
|
game->redo_sequence = 0;
|
|
}
|
|
|
|
/*
|
|
* Special case restart and restore commands; throw out any remaining input
|
|
* and return straight away. Do the same if this was an undo, detected by
|
|
* noting that undo is no longer available, where it was on entry.
|
|
*/
|
|
if (game->do_restart || game->do_restore
|
|
|| (was_undo_available && !game->undo_available)) {
|
|
line_buffer[0] = NUL;
|
|
return status;
|
|
}
|
|
|
|
/* If not empty, consider as saving for "again" calls and in the history. */
|
|
if (!sc_strempty(line_element)) {
|
|
/*
|
|
* Unless "again", note this line element as prior input. "Again" shows
|
|
* up as do_again set in the game, where it wasn't when we entered here.
|
|
*/
|
|
if (!game->do_again && !is_rerunning)
|
|
Common::strcpy_s(prior_element, line_element);
|
|
|
|
/*
|
|
* If this was a request to run a command from the history, copy that
|
|
* command into the prior_element for the next iteration. The library
|
|
* should have verified the value in redo_sequence, so fetching the
|
|
* command string should not fail.
|
|
*/
|
|
if (game->do_again && game->redo_sequence != 0) {
|
|
const sc_char *redo_command;
|
|
|
|
redo_command = memo_find_command(memento, game->redo_sequence);
|
|
if (redo_command)
|
|
Common::strcpy_s(prior_element, redo_command);
|
|
else {
|
|
sc_error("run_player_input: invalid redo sequence request\n");
|
|
game->do_again = FALSE;
|
|
}
|
|
game->redo_sequence = 0;
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_main_loop()
|
|
*
|
|
* Main interpreter loop.
|
|
*/
|
|
static void run_main_loop(CONTEXT, sc_gameref_t game) {
|
|
const sc_filterref_t filter = gs_get_filter(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
|
|
/*
|
|
* This may not be the very first time this game has been used, for example
|
|
* saving a game right at the start, or undo-ing back to the start through
|
|
* memos. Caught by looking to see if the player room is marked as seen.
|
|
*/
|
|
if (!gs_room_seen(game, gs_playerroom(game))) {
|
|
sc_vartype_t vt_key[2];
|
|
const sc_char *gamename, *startuptext;
|
|
sc_bool disp_first_room, battle_system;
|
|
|
|
/* If battle system and no debugger display a warning. */
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "BattleSystem";
|
|
battle_system = prop_get_boolean(bundle, "B<-ss", vt_key);
|
|
if (battle_system && !debug_get_enabled(game)) {
|
|
if_print_tag(SC_TAG_CLS, "");
|
|
lib_warn_battle_system();
|
|
}
|
|
|
|
/* Initial clear screen. */
|
|
pf_buffer_tag(filter, SC_TAG_CLS);
|
|
|
|
/* Print the game name. */
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "GameName";
|
|
gamename = prop_get_string(bundle, "S<-ss", vt_key);
|
|
pf_buffer_string(filter, gamename);
|
|
pf_buffer_character(filter, '\n');
|
|
|
|
/* Print the game header. */
|
|
vt_key[0].string = "Header";
|
|
vt_key[1].string = "StartupText";
|
|
startuptext = prop_get_string(bundle, "S<-ss", vt_key);
|
|
pf_buffer_string(filter, startuptext);
|
|
pf_buffer_character(filter, '\n');
|
|
|
|
/* If flagged, describe the initial room. */
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "DispFirstRoom";
|
|
disp_first_room = prop_get_boolean(bundle, "B<-ss", vt_key);
|
|
if (disp_first_room)
|
|
lib_cmd_look(game);
|
|
|
|
/* Handle any introductory resources. */
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "IntroRes";
|
|
res_handle_resource(game, "ss", vt_key);
|
|
|
|
/* Set initial values for NPC and object states. */
|
|
npc_setup_initial(game);
|
|
obj_setup_initial(game);
|
|
|
|
/* Nudge events and NPCs. */
|
|
evt_tick_events(game);
|
|
npc_tick_npcs(game);
|
|
|
|
/*
|
|
* Notify the debugger that the game has started. This is a chance to
|
|
* set watchpoints to catch game startup actions. Done before setting
|
|
* the initial room visited as this is how the debugger differentiates
|
|
* restarts from restore or undo back to game start.
|
|
*/
|
|
CALL1(debug_game_started, game);
|
|
|
|
/* Note the initial room as visited. */
|
|
gs_set_room_seen(game, gs_playerroom(game), TRUE);
|
|
} else {
|
|
/* Notify the debugger that the game has restarted. */
|
|
CALL1(debug_game_started, game);
|
|
}
|
|
|
|
/*
|
|
* Game loop, exits either when a command parser handler sets the game
|
|
* running flag to FALSE, or by call to run_quit().
|
|
*/
|
|
game->is_running &= !g_vm->shouldQuit();
|
|
while (game->is_running) {
|
|
sc_bool status;
|
|
|
|
/*
|
|
* Synchronize any resources in use; do this before flushing so that any
|
|
* appropriate graphics/sound appear before waits or waitkey tag delays
|
|
* invoked by flushing the printfilter. Also, print any score change
|
|
* notifications.
|
|
*/
|
|
res_sync_resources(game);
|
|
run_notify_score_change(game);
|
|
|
|
/*
|
|
* Flush printfilter of any accumulated output, and clear any prior
|
|
* notion of administrative commands from input.
|
|
*/
|
|
pf_flush(filter, vars, bundle);
|
|
game->is_admin = FALSE;
|
|
|
|
/* If waitcounter is zero, accept and try a command. */
|
|
if (game->waitcounter == 0) {
|
|
/* Not waiting, so handle a player input line. */
|
|
run_update_status(game);
|
|
status = run_player_input(game);
|
|
|
|
/*
|
|
* If waitcounter is now set, decrement it, as this turn counts as
|
|
* one of them.
|
|
*/
|
|
if (game->waitcounter > 0)
|
|
game->waitcounter--;
|
|
} else {
|
|
/*
|
|
* Currently "waiting"; decrement wait turns, then run a turn having
|
|
* taken no input.
|
|
*/
|
|
game->waitcounter--;
|
|
status = TRUE;
|
|
}
|
|
|
|
/*
|
|
* Do usual turn stuff unless either something stopped the game, or the
|
|
* last command didn't match, or the last command did match but was
|
|
* administrative.
|
|
*/
|
|
if (status && !game->is_admin) {
|
|
/* Increment turn counter, and clear notifications done flag. */
|
|
game->turns++;
|
|
game->has_notified = FALSE;
|
|
|
|
if (game->is_running) {
|
|
/* Nudge events and NPCs. */
|
|
evt_tick_events(game);
|
|
npc_tick_npcs(game);
|
|
|
|
/* Update NPC and object states. */
|
|
npc_turn_update(game);
|
|
obj_turn_update(game);
|
|
|
|
/* Note the current room as visited. */
|
|
gs_set_room_seen(game, gs_playerroom(game), TRUE);
|
|
|
|
/* Give the debugger a chance to catch watchpoints. */
|
|
CALL1(debug_turn_update, game);
|
|
}
|
|
}
|
|
|
|
game->is_running &= !g_vm->shouldQuit();
|
|
}
|
|
|
|
/*
|
|
* Final status update, for games that vary it on completion, then notify
|
|
* the debugger that the game has ended, to let it make a last watchpoint
|
|
* scan and offer the dialog if appropriate.
|
|
*/
|
|
run_update_status(game);
|
|
CALL1(debug_game_ended, game);
|
|
|
|
/*
|
|
* Final resource sync, score change notification and printfilter flush
|
|
* on game-instigated loop exit.
|
|
*/
|
|
res_sync_resources(game);
|
|
run_notify_score_change(game);
|
|
pf_flush(filter, vars, bundle);
|
|
|
|
/*
|
|
* Reset static variables inside run_player_input() with a call to it with
|
|
* is_running false; this is a special case.
|
|
*/
|
|
assert(!game->is_running);
|
|
run_player_input(game);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_create()
|
|
*
|
|
* Create a game context from a callback.
|
|
*/
|
|
sc_gameref_t run_create(sc_read_callbackref_t callback, void *opaque) {
|
|
sc_tafref_t taf;
|
|
sc_prop_setref_t bundle;
|
|
sc_var_setref_t vars, temporary_vars, undo_vars;
|
|
sc_filterref_t filter;
|
|
sc_gameref_t game, temporary_game, undo_game;
|
|
assert(callback);
|
|
|
|
/* Create a new TAF using the callback; return NULL if this fails. */
|
|
taf = taf_create(callback, opaque);
|
|
if (!taf)
|
|
return nullptr;
|
|
else if (if_get_trace_flag(SC_DUMP_TAF))
|
|
taf_debug_dump(taf);
|
|
|
|
/* Create a properties bundle, and parse the TAF data into it. */
|
|
bundle = prop_create(taf);
|
|
if (!bundle) {
|
|
sc_error("run_create: error parsing game data\n");
|
|
taf_destroy(taf);
|
|
return nullptr;
|
|
} else if (if_get_trace_flag(SC_DUMP_PROPERTIES))
|
|
prop_debug_dump(bundle);
|
|
|
|
/* Try to set an interpreter locale from the properties bundle. */
|
|
loc_detect_game_locale(bundle);
|
|
if (if_get_trace_flag(SC_DUMP_LOCALE_TABLES))
|
|
loc_debug_dump();
|
|
|
|
/* Create a set of variables from the bundle. */
|
|
vars = var_create(bundle);
|
|
if (if_get_trace_flag(SC_DUMP_VARIABLES))
|
|
var_debug_dump(vars);
|
|
|
|
/* Create a printfilter for the game. */
|
|
filter = pf_create();
|
|
|
|
/*
|
|
* Create an initial game state, and register it with variables. Also,
|
|
* create undo buffers, and initialize them in the same way.
|
|
*/
|
|
game = gs_create(vars, bundle, filter);
|
|
var_register_game(vars, game);
|
|
|
|
temporary_vars = var_create(bundle);
|
|
temporary_game = gs_create(temporary_vars, bundle, filter);
|
|
var_register_game(temporary_vars, temporary_game);
|
|
|
|
undo_vars = var_create(bundle);
|
|
undo_game = gs_create(undo_vars, bundle, filter);
|
|
var_register_game(undo_vars, undo_game);
|
|
|
|
/* Add the undo buffers and memos to the game, and return it. */
|
|
game->temporary = temporary_game;
|
|
game->undo = undo_game;
|
|
game->memento = memo_create();
|
|
return game;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_restart_handler()
|
|
*
|
|
* Return a game context to initial states to restart a game.
|
|
*/
|
|
static void run_restart_handler(sc_gameref_t game) {
|
|
const sc_filterref_t filter = gs_get_filter(game);
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
sc_gameref_t new_game;
|
|
sc_var_setref_t new_vars;
|
|
|
|
/*
|
|
* Create a fresh set of variables from the current game properties,
|
|
* then a new game using these variables and existing properties and
|
|
* printfilter.
|
|
*/
|
|
new_vars = var_create(bundle);
|
|
new_game = gs_create(new_vars, bundle, filter);
|
|
var_register_game(new_vars, new_game);
|
|
|
|
/*
|
|
* Overwrite the dynamic parts of the current game with the new one.
|
|
*/
|
|
new_game->temporary = game->temporary;
|
|
new_game->undo = game->undo;
|
|
gs_copy(game, new_game);
|
|
|
|
/* Destroy invalid game status strings. */
|
|
sc_free(game->current_room_name);
|
|
game->current_room_name = nullptr;
|
|
sc_free(game->status_line);
|
|
game->status_line = nullptr;
|
|
|
|
/*
|
|
* Now it's safely copied, destroy the temporary new game, and its
|
|
* associated variable set.
|
|
*/
|
|
gs_destroy(new_game);
|
|
var_destroy(new_vars);
|
|
|
|
/* Reset resources handling. */
|
|
res_cancel_resources(game);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_restore_handler()
|
|
*
|
|
* Adjust a game context for continuation after restoring a game.
|
|
*/
|
|
static void run_restore_handler(sc_gameref_t game) {
|
|
/* Invalidate the undo buffer. */
|
|
game->undo_available = FALSE;
|
|
|
|
/*
|
|
* Resources handling? Arguably we should re-offer resources active when
|
|
* the game was saved, but I can't see how this can be achieved with Adrift
|
|
* the way it is. Canceling is too broad, so I'll go here with just
|
|
* stopping sounds (in case looping).
|
|
*
|
|
* TODO Rationalize what happens here.
|
|
*/
|
|
game->stop_sound = TRUE;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_quit_handler()
|
|
*
|
|
* Tidy up printfilter and input statics on game quit.
|
|
*/
|
|
static void run_quit_handler(sc_gameref_t game) {
|
|
const sc_filterref_t filter = gs_get_filter(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
|
|
/* Flush printfilter and notifications of any dangling output. */
|
|
run_notify_score_change(game);
|
|
pf_flush(filter, vars, bundle);
|
|
|
|
/* Cancel any active resources. */
|
|
res_cancel_resources(game);
|
|
|
|
/*
|
|
* Make the special call to reset all of the static variables inside
|
|
* run_player_input().
|
|
*/
|
|
assert(!game->is_running);
|
|
run_player_input(game);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_interpret()
|
|
*
|
|
* Intepret the game in a game context.
|
|
*/
|
|
void run_interpret(CONTEXT, sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/* Verify the game is not already running, and is runnable. */
|
|
if (game->is_running) {
|
|
sc_error("run_interpret: game is already running\n");
|
|
return;
|
|
}
|
|
if (game->has_completed) {
|
|
sc_error("run_interpret: game has already completed\n");
|
|
return;
|
|
}
|
|
|
|
/* Refuse to run a game with no rooms. */
|
|
if (gs_room_count(game) == 0) {
|
|
sc_error("run_interpret: game contains no rooms\n");
|
|
return;
|
|
}
|
|
|
|
/* Run the main interpreter loop until no more restarts. */
|
|
game->is_running = TRUE;
|
|
do {
|
|
// Run the game until some form of halt is requested
|
|
CALL1(run_main_loop, game);
|
|
|
|
/*
|
|
* If the halt was a restart or restore, cancel the request, handle
|
|
* restart or restore game adjustments, and set the game running
|
|
* again.
|
|
*/
|
|
if (game->do_restart) {
|
|
game->do_restart = FALSE;
|
|
run_restart_handler(game);
|
|
game->is_running = TRUE;
|
|
}
|
|
|
|
if (game->do_restore) {
|
|
game->do_restore = FALSE;
|
|
run_restore_handler(game);
|
|
game->is_running = TRUE;
|
|
}
|
|
} while (game->is_running);
|
|
|
|
/* Tidy up the printfilter and input statics. */
|
|
run_quit_handler(game);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_destroy()
|
|
*
|
|
* Destroy a game context, and free all resources.
|
|
*/
|
|
void run_destroy(sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/* Can't destroy the context of a running game. */
|
|
if (game->is_running) {
|
|
sc_error("run_destroy: game is running, stop it first\n");
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Cancel any game state debugger -- this frees its resources. Only the
|
|
* primary game may have acquired a debugger.
|
|
*/
|
|
debug_set_enabled(game, FALSE);
|
|
assert(!debug_get_enabled(game->temporary));
|
|
assert(!debug_get_enabled(game->undo));
|
|
|
|
/*
|
|
* Destroy the game state, variables, properties bundle, memos, undo
|
|
* buffers and their variables, and filter. The bundle and printfilter
|
|
* are shared by the main game, the undo game, and the temporary game, so
|
|
* destroy these only once! The main game has a memento, but it is not
|
|
* visible to these other two games, neither of which have one.
|
|
*/
|
|
assert(gs_get_bundle(game->temporary) == gs_get_bundle(game));
|
|
assert(gs_get_filter(game->temporary) == gs_get_filter(game));
|
|
assert(gs_get_vars(game->temporary) != gs_get_vars(game));
|
|
assert(!gs_get_memento(game->temporary));
|
|
var_destroy(gs_get_vars(game->temporary));
|
|
gs_destroy(game->temporary);
|
|
|
|
assert(gs_get_bundle(game->undo) == gs_get_bundle(game));
|
|
assert(gs_get_filter(game->undo) == gs_get_filter(game));
|
|
assert(gs_get_vars(game->undo) != gs_get_vars(game));
|
|
assert(!gs_get_memento(game->undo));
|
|
var_destroy(gs_get_vars(game->undo));
|
|
gs_destroy(game->undo);
|
|
|
|
prop_destroy(gs_get_bundle(game));
|
|
pf_destroy(gs_get_filter(game));
|
|
var_destroy(gs_get_vars(game));
|
|
memo_destroy(gs_get_memento(game));
|
|
|
|
gs_destroy(game);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_quit()
|
|
*
|
|
* Quits a running game. This function calls a longjump to act as if
|
|
* run_main_loop() returned, and so never returns to its caller.
|
|
*/
|
|
void run_quit(CONTEXT, sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
// Disallow quitting a non-running game
|
|
if (!game->is_running) {
|
|
sc_error("run_quit: game is not running\n");
|
|
return;
|
|
}
|
|
|
|
// Exit the main loop
|
|
game->is_running = FALSE;
|
|
LONG_JUMP;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_restart()
|
|
*
|
|
* Restarts either a running or a stopped game. For running games, this
|
|
* function calls a longjump to act as if run_main_loop() returned, and so
|
|
* never returns to its caller. For stopped games, it returns.
|
|
*/
|
|
void run_restart(CONTEXT, sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/*
|
|
* If the game is running, stop it, request a restart, and exit the main
|
|
* loop with a longjump.
|
|
*/
|
|
if (game->is_running) {
|
|
game->is_running = FALSE;
|
|
game->do_restart = TRUE;
|
|
LONG_JUMP;
|
|
}
|
|
|
|
// Restart locally, and ensure that the game remains stopped
|
|
run_restart_handler(game);
|
|
game->is_running = FALSE;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_save()
|
|
* run_save_prompted()
|
|
*
|
|
* Saves either a running or a stopped game.
|
|
*/
|
|
void run_save(sc_gameref_t game, sc_write_callbackref_t callback, void *opaque) {
|
|
assert(gs_is_game_valid(game));
|
|
assert(callback);
|
|
|
|
SaveSerializer ser(game, callback, opaque);
|
|
ser.save();
|
|
}
|
|
|
|
sc_bool run_save_prompted(sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
return g_vm->saveGame().getCode() == Common::kNoError;
|
|
}
|
|
|
|
/*
|
|
* run_restore_common()
|
|
* run_restore()
|
|
* run_restore_prompted()
|
|
*
|
|
* Restores either a running or a stopped game. For running games, on
|
|
* successful restore, these functions call a longjump to act as if
|
|
* run_main_loop() returned, and so never return to their caller. On failed
|
|
* restore, and for stopped games, they will return, with TRUE if successful,
|
|
* FALSE if restore failed.
|
|
*/
|
|
static sc_bool run_restore_common(CONTEXT, sc_gameref_t game, sc_read_callbackref_t callback, void *opaque) {
|
|
sc_bool is_running, status;
|
|
|
|
/*
|
|
* Save the game running flag, and call the restore appropriate for the
|
|
* caller. The indication of a call from run_restore_prompted() is a
|
|
* callback of NULL; callback cannot be NULL for run_restore() calls.
|
|
*/
|
|
is_running = game->is_running;
|
|
LoadSerializer ser(game, callback, opaque);
|
|
status = ser.load();
|
|
if (status) {
|
|
/* Loading a game clears is_running -- restore it here. */
|
|
game->is_running = is_running;
|
|
|
|
/*
|
|
* If the game is (was) running, set flags so that the interpreter
|
|
* loop cycles, and exit the main loop with a longjump.
|
|
*/
|
|
if (game->is_running) {
|
|
game->is_running = FALSE;
|
|
game->do_restore = TRUE;
|
|
LONG_JUMP0;
|
|
}
|
|
}
|
|
|
|
/* Return TRUE on successful restore of a stopped game, FALSE on error. */
|
|
return status;
|
|
}
|
|
|
|
sc_bool run_restore(CONTEXT, sc_gameref_t game, sc_read_callbackref_t callback, void *opaque) {
|
|
assert(gs_is_game_valid(game));
|
|
assert(callback);
|
|
|
|
return run_restore_common(context, game, callback, opaque);
|
|
}
|
|
|
|
sc_bool run_restore_prompted(CONTEXT, sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
return run_restore_common(context, game, nullptr, nullptr);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_undo()
|
|
*
|
|
* Undo a turn in either a running or a stopped game. Returns TRUE on
|
|
* successful undo, FALSE if no undo buffer is available.
|
|
*/
|
|
sc_bool run_undo(CONTEXT, sc_gameref_t game) {
|
|
const sc_memo_setref_t memento = gs_get_memento(game);
|
|
sc_bool is_running;
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/* Save the game's running state, so we can restore it later. */
|
|
is_running = game->is_running;
|
|
|
|
/* If there's an undo buffer available, restore it. */
|
|
if (game->undo_available) {
|
|
/* Restore the undo buffer, and then restore running flag. */
|
|
gs_copy(game, game->undo);
|
|
game->undo_available = FALSE;
|
|
game->is_running = is_running;
|
|
|
|
/* Location may have changed; update status. */
|
|
run_update_status(game);
|
|
|
|
/* Bring resources into line with the revised game. */
|
|
res_sync_resources(game);
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* If there is no undo buffer, try to restore one saved previously in a
|
|
* memo. Handle as if restoring from a file.
|
|
*/
|
|
if (memo_load_game(memento, game)) {
|
|
/* Loading a game clears is_running -- restore it here. */
|
|
game->is_running = is_running;
|
|
|
|
/*
|
|
* If the game is (was) running, set flags so that the interpreter
|
|
* loop cycles, and exit the main loop with a longjump.
|
|
*/
|
|
if (game->is_running) {
|
|
game->is_running = FALSE;
|
|
game->do_restore = TRUE;
|
|
LONG_JUMP0;
|
|
}
|
|
|
|
/* Game undo on non-running game accomplished with memos. */
|
|
return TRUE;
|
|
}
|
|
|
|
/* No undo buffer and no memos available. */
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_is_running()
|
|
*
|
|
* Query the game running state.
|
|
*/
|
|
sc_bool run_is_running(sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
return game->is_running;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_has_completed()
|
|
*
|
|
* Query the game completion state. Completed games cannot be resumed,
|
|
* since they've run the exit task and thus have nowhere to go.
|
|
*/
|
|
sc_bool run_has_completed(sc_gameref_t game) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
return game->has_completed;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_is_undo_available()
|
|
*
|
|
* Query the game turn undo buffer and memo availability.
|
|
*/
|
|
sc_bool run_is_undo_available(sc_gameref_t game) {
|
|
const sc_memo_setref_t memento = gs_get_memento(game);
|
|
assert(gs_is_game_valid(game));
|
|
|
|
return game->undo_available || memo_is_load_available(memento);
|
|
}
|
|
|
|
|
|
/*
|
|
* run_get_attributes()
|
|
* run_set_attributes()
|
|
*
|
|
* Get and set selected game attributes.
|
|
*/
|
|
void run_get_attributes(sc_gameref_t game, const sc_char **game_name, const sc_char **game_author,
|
|
const sc_char **game_compile_date, sc_int *turns, sc_int *score, sc_int *max_score,
|
|
const sc_char **current_room_name, const sc_char **status_line, const sc_char **preferred_font,
|
|
sc_bool *bold_room_names, sc_bool *verbose, sc_bool *notify_score_change) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
sc_vartype_t vt_key[2];
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/* Return the game name, author, and compile date if requested. */
|
|
if (game_name) {
|
|
if (!game->title) {
|
|
const sc_char *gamename;
|
|
sc_char *filtered;
|
|
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "GameName";
|
|
gamename = prop_get_string(bundle, "S<-ss", vt_key);
|
|
|
|
filtered = pf_filter_for_info(gamename, vars);
|
|
pf_strip_tags(filtered);
|
|
game->title = filtered;
|
|
}
|
|
*game_name = game->title;
|
|
}
|
|
if (game_author) {
|
|
if (!game->author) {
|
|
const sc_char *gameauthor;
|
|
sc_char *filtered;
|
|
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "GameAuthor";
|
|
gameauthor = prop_get_string(bundle, "S<-ss", vt_key);
|
|
|
|
filtered = pf_filter_for_info(gameauthor, vars);
|
|
pf_strip_tags(filtered);
|
|
game->author = filtered;
|
|
}
|
|
*game_author = game->author;
|
|
}
|
|
if (game_compile_date) {
|
|
vt_key[0].string = "CompileDate";
|
|
*game_compile_date = prop_get_string(bundle, "S<-s", vt_key);
|
|
}
|
|
|
|
/* Return the current room name and status line if requested. */
|
|
if (current_room_name)
|
|
*current_room_name = game->current_room_name;
|
|
if (status_line)
|
|
*status_line = game->status_line;
|
|
|
|
/* Return any game preferred font, or NULL if none. */
|
|
if (preferred_font) {
|
|
vt_key[0].string = "CustomFont";
|
|
if (prop_get_boolean(bundle, "B<-s", vt_key)) {
|
|
vt_key[0].string = "FontNameSize";
|
|
*preferred_font = prop_get_string(bundle, "S<-s", vt_key);
|
|
} else
|
|
*preferred_font = nullptr;
|
|
}
|
|
|
|
/* Return any other selected game attributes. */
|
|
if (turns)
|
|
*turns = game->turns;
|
|
if (score)
|
|
*score = game->score;
|
|
if (max_score) {
|
|
vt_key[0].string = "Globals";
|
|
vt_key[1].string = "MaxScore";
|
|
*max_score = prop_get_integer(bundle, "I<-ss", vt_key);
|
|
}
|
|
if (bold_room_names)
|
|
*bold_room_names = game->bold_room_names;
|
|
if (verbose)
|
|
*verbose = game->verbose;
|
|
if (notify_score_change)
|
|
*notify_score_change = game->notify_score_change;
|
|
}
|
|
|
|
void run_set_attributes(sc_gameref_t game, sc_bool bold_room_names, sc_bool verbose,
|
|
sc_bool notify_score_change) {
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/* Set game options. */
|
|
game->bold_room_names = bold_room_names;
|
|
game->verbose = verbose;
|
|
game->notify_score_change = notify_score_change;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_hint_iterate()
|
|
*
|
|
* Return the next hint appropriate to the game state, or the first if
|
|
* hint is NULL. Returns NULL if none, or no more hints. This function
|
|
* works with pointers to a task state rather than task indexes so that
|
|
* the token passed in and out is a pointer, and readily made opaque to
|
|
* the client as a void*.
|
|
*/
|
|
sc_hintref_t run_hint_iterate(sc_gameref_t game, sc_hintref_t hint) {
|
|
sc_int task;
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/*
|
|
* Hint is a pointer to a task state; convert to a task index, adding one
|
|
* to move on to the next task, or start at the first task if null.
|
|
*/
|
|
if (!hint)
|
|
task = 0;
|
|
else {
|
|
/* Convert into pointer, and range check. */
|
|
task = hint - game->tasks;
|
|
if (task < 0 || task >= gs_task_count(game)) {
|
|
sc_error("run_hint_iterate: invalid iteration hint\n");
|
|
return nullptr;
|
|
}
|
|
|
|
/* Advance beyond current task. */
|
|
task++;
|
|
}
|
|
|
|
/* Scan for the next runnable task that offers a hint. */
|
|
for (; task < gs_task_count(game); task++) {
|
|
if (task_can_run_task(game, task) && task_has_hints(game, task))
|
|
break;
|
|
}
|
|
|
|
/* Return a pointer to the state of the task identified, or NULL. */
|
|
return task < gs_task_count(game) ? game->tasks + task : nullptr;
|
|
}
|
|
|
|
|
|
/*
|
|
* run_get_hint_common()
|
|
* run_get_hint_question()
|
|
* run_get_subtle_hint()
|
|
* run_get_unsubtle_hint()
|
|
*
|
|
* Return the strings for a hint. Front-ends to task functions. Each
|
|
* converts the hint "address" to a task index through pointer arithmetic,
|
|
* then filters it and returns a temporary, valid only until the next hint
|
|
* call.
|
|
*
|
|
* Hint strings are NULL if empty (not defined by the game).
|
|
*/
|
|
static const sc_char *run_get_hint_common(sc_gameref_t game, sc_hintref_t hint,
|
|
const sc_char * (*handler)(sc_gameref_t, sc_int)) {
|
|
const sc_prop_setref_t bundle = gs_get_bundle(game);
|
|
const sc_var_setref_t vars = gs_get_vars(game);
|
|
sc_int task;
|
|
const sc_char *string;
|
|
assert(gs_is_game_valid(game));
|
|
|
|
/* Verify the caller passed in a valid hint. */
|
|
task = hint - game->tasks;
|
|
if (task < 0 || task >= gs_task_count(game)) {
|
|
sc_error("run_get_hint_common: invalid iteration hint\n");
|
|
return nullptr;
|
|
} else if (!task_has_hints(game, task)) {
|
|
sc_error("run_get_hint_common: task has no hint\n");
|
|
return nullptr;
|
|
}
|
|
|
|
/* Get the required game text by calling the given handler function. */
|
|
string = handler(game, task);
|
|
if (!sc_strempty(string)) {
|
|
sc_char *filtered;
|
|
|
|
/* Filter and strip tags, note in game. */
|
|
filtered = pf_filter(string, vars, bundle);
|
|
pf_strip_tags_for_hints(filtered);
|
|
sc_free(game->hint_text);
|
|
game->hint_text = filtered;
|
|
} else {
|
|
/* Hint text is empty; drop any text noted in game. */
|
|
sc_free(game->hint_text);
|
|
game->hint_text = nullptr;
|
|
}
|
|
|
|
return game->hint_text;
|
|
}
|
|
|
|
const sc_char *run_get_hint_question(sc_gameref_t game, sc_hintref_t hint) {
|
|
return run_get_hint_common(game, hint, task_get_hint_question);
|
|
}
|
|
|
|
const sc_char *run_get_subtle_hint(sc_gameref_t game, sc_hintref_t hint) {
|
|
return run_get_hint_common(game, hint, task_get_hint_subtle);
|
|
}
|
|
|
|
const sc_char *run_get_unsubtle_hint(sc_gameref_t game, sc_hintref_t hint) {
|
|
return run_get_hint_common(game, hint, task_get_hint_unsubtle);
|
|
}
|
|
|
|
} // End of namespace Adrift
|
|
} // End of namespace Glk
|