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

772 lines
21 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/scprotos.h"
#include "glk/adrift/scgamest.h"
namespace Glk {
namespace Adrift {
/*
* Module notes:
*
* o Event pause and resume tasks need more testing.
*/
/* Trace flag, set before running. */
static sc_bool evt_trace = FALSE;
/*
* evt_any_task_in_state()
*
* Return TRUE if any task at all matches the given completion state.
*/
static sc_bool evt_any_task_in_state(sc_gameref_t game, sc_bool state) {
sc_int task;
/* Scan tasks for any whose completion matches input. */
for (task = 0; task < gs_task_count(game); task++) {
if (gs_task_done(game, task) == state)
return TRUE;
}
/* No tasks matched. */
return FALSE;
}
/*
* evt_can_see_event()
*
* Return TRUE if player is in the right room for event text.
*/
sc_bool evt_can_see_event(sc_gameref_t game, sc_int event) {
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[5];
sc_int type;
/* Check room list for the event and return it. */
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "Where";
vt_key[3].string = "Type";
type = prop_get_integer(bundle, "I<-siss", vt_key);
switch (type) {
case ROOMLIST_NO_ROOMS:
return FALSE;
case ROOMLIST_ALL_ROOMS:
return TRUE;
case ROOMLIST_ONE_ROOM:
vt_key[3].string = "Room";
return prop_get_integer(bundle, "I<-siss", vt_key)
== gs_playerroom(game);
case ROOMLIST_SOME_ROOMS:
vt_key[3].string = "Rooms";
vt_key[4].integer = gs_playerroom(game);
return prop_get_boolean(bundle, "B<-sissi", vt_key);
default:
sc_fatal("evt_can_see_event: invalid type, %ld\n", type);
return FALSE;
}
}
/*
* evt_move_object()
*
* Move an object from within an event.
*/
static void evt_move_object(sc_gameref_t game, sc_int object, sc_int destination) {
/* Ignore negative values of object. */
if (object >= 0) {
if (evt_trace) {
sc_trace("Event: moving object %ld to room %ld\n",
object, destination);
}
/* Move object depending on destination. */
switch (destination) {
case -1: /* Hidden. */
gs_object_make_hidden(game, object);
break;
case 0: /* Held by player. */
gs_object_player_get(game, object);
break;
case 1: /* Same room as player. */
gs_object_to_room(game, object, gs_playerroom(game));
break;
default:
if (destination < gs_room_count(game) + 2)
gs_object_to_room(game, object, destination - 2);
else {
sc_int roomgroup, room;
roomgroup = destination - gs_room_count(game) - 2;
room = lib_random_roomgroup_member(game, roomgroup);
gs_object_to_room(game, object, room);
}
break;
}
/*
* If static, mark as no longer unmoved.
*
* TODO Is this the only place static objects can be moved? And just
* how static is a static object if it's moveable, anyway?
*/
if (obj_is_static(game, object))
gs_set_object_static_unmoved(game, object, FALSE);
}
}
/*
* evt_fixup_v390_v380_immediate_restart()
*
* Versions 3.9 and 3.8 differ from version 4.0 on immediate restart; they
* "miss" the event start actions and move one step into the event without
* comment. It's arguable if this is a feature or a bug; nevertheless, we
* can do the same thing here, though it's ugly.
*/
static sc_bool evt_fixup_v390_v380_immediate_restart(sc_gameref_t game, sc_int event) {
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[3];
sc_int version;
vt_key[0].string = "Version";
version = prop_get_integer(bundle, "I<-s", vt_key);
if (version < TAF_VERSION_400) {
sc_int time1, time2;
if (evt_trace)
sc_trace("Event: applying 3.9/3.8 restart fixup\n");
/* Set to running state. */
gs_set_event_state(game, event, ES_RUNNING);
/* Set up event time to be one less than a proper start. */
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "Time1";
time1 = prop_get_integer(bundle, "I<-sis", vt_key);
vt_key[2].string = "Time2";
time2 = prop_get_integer(bundle, "I<-sis", vt_key);
gs_set_event_time(game, event, sc_randomint(time1, time2) - 1);
}
/* Return TRUE if we applied the fixup. */
return version < TAF_VERSION_400;
}
/*
* evt_start_event()
*
* Change an event from WAITING to RUNNING.
*/
static void evt_start_event(sc_gameref_t game, sc_int event) {
const sc_filterref_t filter = gs_get_filter(game);
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[4];
sc_int time1, time2, obj1, obj1dest;
if (evt_trace)
sc_trace("Event: starting event %ld\n", event);
/* If event is visible, print its start text. */
if (evt_can_see_event(game, event)) {
const sc_char *starttext;
/* Get and print start text. */
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "StartText";
starttext = prop_get_string(bundle, "S<-sis", vt_key);
if (!sc_strempty(starttext)) {
pf_buffer_string(filter, starttext);
pf_buffer_character(filter, '\n');
}
/* Handle any associated resource. */
vt_key[2].string = "Res";
vt_key[3].integer = 0;
res_handle_resource(game, "sisi", vt_key);
}
/* Move event object to destination. */
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "Obj1";
obj1 = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
vt_key[2].string = "Obj1Dest";
obj1dest = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
evt_move_object(game, obj1, obj1dest);
/* Set the event's state and time. */
gs_set_event_state(game, event, ES_RUNNING);
vt_key[2].string = "Time1";
time1 = prop_get_integer(bundle, "I<-sis", vt_key);
vt_key[2].string = "Time2";
time2 = prop_get_integer(bundle, "I<-sis", vt_key);
gs_set_event_time(game, event, sc_randomint(time1, time2));
if (evt_trace)
sc_trace("Event: start event handling done, %ld\n", event);
}
/*
* evt_get_starter_type()
*
* Return the starter type for an event.
*/
static sc_int evt_get_starter_type(sc_gameref_t game, sc_int event) {
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[3];
sc_int startertype;
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "StarterType";
startertype = prop_get_integer(bundle, "I<-sis", vt_key);
return startertype;
}
/*
* evt_finish_event()
*
* Move an event to FINISHED, or restart it.
*/
static void evt_finish_event(sc_gameref_t game, sc_int event) {
const sc_filterref_t filter = gs_get_filter(game);
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[4];
sc_int obj2, obj2dest, obj3, obj3dest;
sc_int task, startertype, restarttype;
sc_bool taskdir;
if (evt_trace)
sc_trace("Event: finishing event %ld\n", event);
/* Set up invariant parts of the key. */
vt_key[0].string = "Events";
vt_key[1].integer = event;
/* If event is visible, print its finish text. */
if (evt_can_see_event(game, event)) {
const sc_char *finishtext;
/* Get and print finish text. */
vt_key[2].string = "FinishText";
finishtext = prop_get_string(bundle, "S<-sis", vt_key);
if (!sc_strempty(finishtext)) {
pf_buffer_string(filter, finishtext);
pf_buffer_character(filter, '\n');
}
/* Handle any associated resource. */
vt_key[2].string = "Res";
vt_key[3].integer = 4;
res_handle_resource(game, "sisi", vt_key);
}
/* Move event objects to destination. */
vt_key[2].string = "Obj2";
obj2 = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
vt_key[2].string = "Obj2Dest";
obj2dest = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
evt_move_object(game, obj2, obj2dest);
vt_key[2].string = "Obj3";
obj3 = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
vt_key[2].string = "Obj3Dest";
obj3dest = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
evt_move_object(game, obj3, obj3dest);
/* See if there is an affected task. */
vt_key[2].string = "TaskAffected";
task = prop_get_integer(bundle, "I<-sis", vt_key) - 1;
if (task >= 0) {
vt_key[2].string = "TaskFinished";
taskdir = !prop_get_boolean(bundle, "B<-sis", vt_key);
if (task_can_run_task_directional(game, task, taskdir)) {
if (evt_trace) {
sc_trace("Event: event running task %ld, %s\n",
task, taskdir ? "forwards" : "backwards");
}
task_run_task(game, task, taskdir);
} else {
if (evt_trace)
sc_trace("Event: event can't run task %ld\n", task);
}
}
/* Handle possible restart. */
vt_key[2].string = "RestartType";
restarttype = prop_get_integer(bundle, "I<-sis", vt_key);
switch (restarttype) {
case 0: /* Don't restart. */
startertype = evt_get_starter_type(game, event);
switch (startertype) {
case 1: /* Immediate. */
case 2: /* Random delay. */
case 3: /* After task. */
gs_set_event_state(game, event, ES_FINISHED);
gs_set_event_time(game, event, 0);
break;
default:
sc_fatal("evt_finish_event:"
" unknown value for starter type, %ld\n", startertype);
}
break;
case 1: /* Restart immediately. */
if (evt_fixup_v390_v380_immediate_restart(game, event))
break;
else
evt_start_event(game, event);
break;
case 2: /* Restart after delay. */
startertype = evt_get_starter_type(game, event);
switch (startertype) {
case 1: /* Immediate. */
if (evt_fixup_v390_v380_immediate_restart(game, event))
break;
else
evt_start_event(game, event);
break;
case 2: { /* Random delay. */
sc_int start, end;
gs_set_event_state(game, event, ES_WAITING);
vt_key[2].string = "StartTime";
start = prop_get_integer(bundle, "I<-sis", vt_key);
vt_key[2].string = "EndTime";
end = prop_get_integer(bundle, "I<-sis", vt_key);
gs_set_event_time(game, event, sc_randomint(start, end));
break;
}
case 3: /* After task. */
gs_set_event_state(game, event, ES_AWAITING);
gs_set_event_time(game, event, 0);
break;
default:
sc_fatal("evt_finish_event: unknown StarterType\n");
}
break;
default:
sc_fatal("evt_finish_event: unknown RestartType\n");
}
if (evt_trace)
sc_trace("Event: finish event handling done, %ld\n", event);
}
/*
* evt_has_starter_task()
* evt_starter_task_is_complete()
* evt_pauser_task_is_complete()
* evt_resumer_task_is_complete()
*
* Return the status of start, pause and resume states of an event.
*/
static sc_bool evt_has_starter_task(sc_gameref_t game, sc_int event) {
sc_int startertype;
startertype = evt_get_starter_type(game, event);
return startertype == 3;
}
static sc_bool evt_starter_task_is_complete(sc_gameref_t game, sc_int event) {
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[3];
sc_int task;
sc_bool start;
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "TaskNum";
task = prop_get_integer(bundle, "I<-sis", vt_key);
start = FALSE;
if (task == 0) {
if (evt_any_task_in_state(game, TRUE))
start = TRUE;
} else if (task > 0) {
if (gs_task_done(game, task - 1))
start = TRUE;
}
return start;
}
static sc_bool evt_pauser_task_is_complete(sc_gameref_t game, sc_int event) {
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[3];
sc_int pausetask;
sc_bool completed, pause;
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "PauseTask";
pausetask = prop_get_integer(bundle, "I<-sis", vt_key);
vt_key[2].string = "PauserCompleted";
completed = !prop_get_boolean(bundle, "B<-sis", vt_key);
pause = FALSE;
if (pausetask == 1) {
if (evt_any_task_in_state(game, completed))
pause = TRUE;
} else if (pausetask > 1) {
if (completed == gs_task_done(game, pausetask - 2))
pause = TRUE;
}
return pause;
}
static sc_bool evt_resumer_task_is_complete(sc_gameref_t game, sc_int event) {
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[3];
sc_int resumetask;
sc_bool completed, resume;
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "ResumeTask";
resumetask = prop_get_integer(bundle, "I<-sis", vt_key);
vt_key[2].string = "ResumerCompleted";
completed = !prop_get_boolean(bundle, "B<-sis", vt_key);
resume = FALSE;
if (resumetask == 1) {
if (evt_any_task_in_state(game, completed))
resume = TRUE;
} else if (resumetask > 1) {
if (completed == gs_task_done(game, resumetask - 2))
resume = TRUE;
}
return resume;
}
/*
* evt_handle_preftime_notifications()
*
* Print messages and handle resources for the event where we're in mid-event
* and getting close to some number of turns from its end.
*/
static void evt_handle_preftime_notifications(sc_gameref_t game, sc_int event) {
const sc_filterref_t filter = gs_get_filter(game);
const sc_prop_setref_t bundle = gs_get_bundle(game);
sc_vartype_t vt_key[4];
sc_int preftime1, preftime2;
const sc_char *preftext;
vt_key[0].string = "Events";
vt_key[1].integer = event;
vt_key[2].string = "PrefTime1";
preftime1 = prop_get_integer(bundle, "I<-sis", vt_key);
if (preftime1 == gs_event_time(game, event)) {
vt_key[2].string = "PrefText1";
preftext = prop_get_string(bundle, "S<-sis", vt_key);
if (!sc_strempty(preftext)) {
pf_buffer_string(filter, preftext);
pf_buffer_character(filter, '\n');
}
vt_key[2].string = "Res";
vt_key[3].integer = 2;
res_handle_resource(game, "sisi", vt_key);
}
vt_key[2].string = "PrefTime2";
preftime2 = prop_get_integer(bundle, "I<-sis", vt_key);
if (preftime2 == gs_event_time(game, event)) {
vt_key[2].string = "PrefText2";
preftext = prop_get_string(bundle, "S<-sis", vt_key);
if (!sc_strempty(preftext)) {
pf_buffer_string(filter, preftext);
pf_buffer_character(filter, '\n');
}
vt_key[2].string = "Res";
vt_key[3].integer = 3;
res_handle_resource(game, "sisi", vt_key);
}
}
/*
* evt_tick_event()
*
* Attempt to advance an event by one turn.
*/
static void evt_tick_event(sc_gameref_t game, sc_int event) {
if (evt_trace) {
sc_trace("Event: ticking event %ld: state %ld, time %ld\n", event,
gs_event_state(game, event), gs_event_time(game, event));
}
/* Handle call based on current event state. */
switch (gs_event_state(game, event)) {
case ES_WAITING: {
if (evt_trace)
sc_trace("Event: ticking waiting event %ld\n", event);
/*
* Because we also tick an event that goes from waiting to running,
* events started here will tick through RUNNING too, and have their
* time decremented. To get around this, so that the timer for one-
* shot events doesn't look one lower than it should after this
* transition, we need to set the initial time for events that start
* as soon as the game starts to one greater than that set by
* evt_start_time(). Here's the hack to do that; if the event starts
* immediately, its time will already be zero, even before decrement,
* which is how we tell which events to apply this hack to.
*
* TODO This seems to work, but also seems very dodgy.
*/
if (gs_event_time(game, event) == 0) {
evt_start_event(game, event);
/* If the event time was set to zero, finish immediately. */
if (gs_event_time(game, event) <= 0)
evt_finish_event(game, event);
else
gs_set_event_time(game, event, gs_event_time(game, event) + 1);
break;
}
/*
* Decrement the event's time, and if it goes to zero, start running
* the event.
*/
gs_decrement_event_time(game, event);
if (gs_event_time(game, event) <= 0) {
evt_start_event(game, event);
/* If the event time was set to zero, finish immediately. */
if (gs_event_time(game, event) <= 0)
evt_finish_event(game, event);
}
}
break;
case ES_RUNNING: {
if (evt_trace)
sc_trace("Event: ticking running event %ld\n", event);
/*
* Re-check the starter task; if it's no longer completed, we need
* to set the event back to waiting on task.
*/
if (evt_has_starter_task(game, event)) {
if (!evt_starter_task_is_complete(game, event)) {
if (evt_trace)
sc_trace("Event: starter task not complete\n");
gs_set_event_state(game, event, ES_AWAITING);
gs_set_event_time(game, event, 0);
break;
}
}
/* If the pauser has completed, but resumer not, pause this event. */
if (evt_pauser_task_is_complete(game, event)
&& !evt_resumer_task_is_complete(game, event)) {
if (evt_trace)
sc_trace("Event: pause complete\n");
gs_set_event_state(game, event, ES_PAUSED);
break;
}
/*
* Decrement the event's time, and print any notifications for a set
* number of turns from the event end.
*/
gs_decrement_event_time(game, event);
if (evt_can_see_event(game, event))
evt_handle_preftime_notifications(game, event);
/* If the time goes to zero, finish running the event. */
if (gs_event_time(game, event) <= 0)
evt_finish_event(game, event);
}
break;
case ES_AWAITING: {
if (evt_trace)
sc_trace("Event: ticking awaiting event %ld\n", event);
/*
* Check the starter task. If it's completed, start running the
* event.
*/
if (evt_starter_task_is_complete(game, event)) {
evt_start_event(game, event);
/* If the event time was set to zero, finish immediately. */
if (gs_event_time(game, event) <= 0)
evt_finish_event(game, event);
else {
/*
* If the pauser has completed, but resumer not, immediately
* also pause this event.
*/
if (evt_pauser_task_is_complete(game, event)
&& !evt_resumer_task_is_complete(game, event)) {
if (evt_trace)
sc_trace("Event: pause complete, immediate pause\n");
gs_set_event_state(game, event, ES_PAUSED);
}
}
}
}
break;
case ES_FINISHED: {
if (evt_trace)
sc_trace("Event: ticking finished event %ld\n", event);
/*
* Check the starter task; if it's not completed, we need to set the
* event back to waiting on task.
*
* A completed event needs to go back to waiting on its task, but we
* don't want to set it there as soon as the event finishes. We need
* to wait for the starter task to first become undone, otherwise the
* event just cycles endlessly, and they don't in Adrift itself. Here
* is where we wait for starter tasks to become undone.
*/
if (evt_has_starter_task(game, event)) {
if (!evt_starter_task_is_complete(game, event)) {
if (evt_trace)
sc_trace("Event: starter task not complete\n");
gs_set_event_state(game, event, ES_AWAITING);
gs_set_event_time(game, event, 0);
break;
}
}
}
break;
case ES_PAUSED: {
if (evt_trace)
sc_trace("Event: ticking paused event %ld\n", event);
/* If the resumer has completed, resume this event. */
if (evt_resumer_task_is_complete(game, event)) {
if (evt_trace)
sc_trace("Event: resume complete\n");
gs_set_event_state(game, event, ES_RUNNING);
break;
}
}
break;
default:
sc_fatal("evt_tick: invalid event state\n");
}
if (evt_trace) {
sc_trace("Event: after ticking event %ld: state %ld, time %ld\n", event,
gs_event_state(game, event), gs_event_time(game, event));
}
}
/*
* evt_tick_events()
*
* Attempt to advance each event by one turn.
*/
void evt_tick_events(sc_gameref_t game) {
sc_int event;
/*
* Tick all events. If an event transitions into a running state from a
* paused or waiting state, tick that event again.
*/
for (event = 0; event < gs_event_count(game); event++) {
sc_int prior_state, state;
/* Note current state, and tick event forwards. */
prior_state = gs_event_state(game, event);
evt_tick_event(game, event);
/*
* If the event went from paused or waiting to running, tick again.
* This looks dodgy, and probably is, but it does keep timers correct
* by only re-ticking events that have transitioned from non-running
* states to a running one, and not already-running events. This is
* in effect just adding a bit of turn processing to a tick that would
* otherwise change state alone; a bit of laziness, in other words.
*/
state = gs_event_state(game, event);
if (state == ES_RUNNING
&& (prior_state == ES_PAUSED || prior_state == ES_WAITING))
evt_tick_event(game, event);
}
}
/*
* evt_debug_trace()
*
* Set event tracing on/off.
*/
void evt_debug_trace(sc_bool flag) {
evt_trace = flag;
}
} // End of namespace Adrift
} // End of namespace Glk