/* vi: set ts=2 shiftwidth=2 expandtab: * * Copyright (C) 2003-2007 Simon Baldwin and Mark J. Tilford * * This program is free software; you can redistribute it and/or modify * it under the terms of version 2 of the GNU General Public License * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ /* * Module notes: * * o Event pause and resume tasks need more testing. */ #include #include #include #include #include "scare.h" #include "scprotos.h" #include "scgamest.h" /* Assorted definitions and constants. */ enum { ROOMLIST_NO_ROOMS = 0, ROOMLIST_ONE_ROOM = 1, ROOMLIST_SOME_ROOMS = 2, ROOMLIST_ALL_ROOMS = 3 }; /* Enumeration of TAF file versions. */ enum { TAF_VERSION_400 = 400, TAF_VERSION_390 = 390, TAF_VERSION_380 = 380 }; /* 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; }