/* vi: set ts=2 shiftwidth=2 expandtab:
 *
 * Copyright (C) 2003-2008  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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
 * USA
 */

/*
 * Module notes:
 *
 * o Implements task return FALSE on no output, a slight extension of
 *   current jAsea behavior.
 */

#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include "scare.h"
#include "scprotos.h"
#include "scgamest.h"


/*
 * Tasks can run other tasks, leading to the possibility of an infinite loop
 * in the task calling sequence.  It's a game error, and we'll apply a limit
 * to the task recursion depth to try and catch it more controllably than
 * waiting for memory exhaustion.
 */
enum { TASK_MAXIMUM_RECURSION = 128 };

/* Trace flag, set before running. */
static sc_bool task_trace = FALSE;


/*
 * task_get_hint_common()
 * task_get_hint_question()
 * task_get_hint_subtle()
 * task_get_hint_unsubtle()
 * task_has_hints()
 *
 * Return the assorted hint text strings, and TRUE if the given task offers
 * hints.
 */
static const sc_char *
task_get_hint_common (sc_gameref_t game, sc_int task, const sc_char *hint)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[3];
  const sc_char *retval;

  /* Look up and return the requested hint string. */
  vt_key[0].string = "Tasks";
  vt_key[1].integer = task;
  vt_key[2].string = hint;
  retval = prop_get_string (bundle, "S<-sis", vt_key);
  return retval;
}

const sc_char *
task_get_hint_question (sc_gameref_t game, sc_int task)
{
  return task_get_hint_common (game, task, "Question");
}

const sc_char *
task_get_hint_subtle (sc_gameref_t game, sc_int task)
{
  return task_get_hint_common (game, task, "Hint1");
}

const sc_char *
task_get_hint_unsubtle (sc_gameref_t game, sc_int task)
{
  return task_get_hint_common (game, task, "Hint2");
}

sc_bool
task_has_hints (sc_gameref_t game, sc_int task)
{
  /* A non-empty question implies hints available. */
  return !sc_strempty (task_get_hint_question (game, task));
}


/*
 * task_can_run_task_directional()
 *
 * Return TRUE if player is in a room where the task can be run and the task
 * is runnable in the given direction.
 */
sc_bool
task_can_run_task_directional (sc_gameref_t game,
                               sc_int task, sc_bool forwards)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5];
  sc_int type;

  /* If already run, non-repeatable tasks are not re-runnable forwards. */
  if (forwards && gs_task_done (game, task))
    {
      sc_bool repeatable;
      const sc_char *repeattext;

      vt_key[0].string = "Tasks";
      vt_key[1].integer = task;
      vt_key[2].string = "Repeatable";
      repeatable = prop_get_boolean (bundle, "B<-sis", vt_key);
      if (!repeatable)
        return FALSE;

      vt_key[2].string = "RepeatText";
      repeattext = prop_get_string (bundle, "S<-sis", vt_key);
      if (!sc_strempty (repeattext))
        return FALSE;
    }

  /* If checking for reverse, test the reversibility flag. */
  if (!forwards)
    {
      sc_bool reversible;

      vt_key[0].string = "Tasks";
      vt_key[1].integer = task;
      vt_key[2].string = "Reversible";
      reversible = prop_get_boolean (bundle, "B<-sis", vt_key);
      if (!reversible)
        return FALSE;
    }

  /* Check room list for the task and return it. */
  vt_key[0].string = "Tasks";
  vt_key[1].integer = task;
  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 ("task_can_run_task_directional: invalid type, %ld\n", type);
      return FALSE;
    }
}


/*
 * task_can_run_task()
 *
 * Returns TRUE if the task can be run in either direction.
 */
sc_bool
task_can_run_task (sc_gameref_t game, sc_int task)
{
  /*
   * Testing reversible tasks first may be a little more efficient if they
   * aren't common in games.  There is, though, probably a little bit of
   * redundant work going on here.
   */
  return task_can_run_task_directional (game, task, FALSE)
         || task_can_run_task_directional (game, task, TRUE);
}


/*
 * task_move_object()
 *
 * Move an object to a place.
 */
static void
task_move_object (sc_gameref_t game, sc_int object, sc_int var2, sc_int var3)
{
  const sc_var_setref_t vars = gs_get_vars (game);

  /* Select action depending on var2. */
  switch (var2)
    {
    case 0:                    /* To room */
      if (var3 == 0)
        {
          if (task_trace)
            sc_trace ("Task: moving object %ld to hidden\n", object);

          gs_object_make_hidden (game, object);
        }
      else
        {
          if (task_trace)
            {
              sc_trace ("Task: moving object %ld to room %ld\n",
                        object, var3 - 1);
            }

          if (var3 == 0)
            gs_object_player_get (game, object);
          else
            gs_object_to_room (game, object, var3 - 1);
        }
      break;

    case 1:                    /* To roomgroup part */
      if (task_trace)
        {
          sc_trace ("Task: moving object %ld to random room in group %ld\n",
                    object, var3);
        }

      gs_object_to_room (game, object,
                         lib_random_roomgroup_member (game, var3));
      break;

    case 2:                    /* Into object */
      if (task_trace)
        sc_trace ("Task: moving object %ld into %ld\n", object, var3);

      gs_object_move_into (game, object, obj_container_object (game, var3));
      break;

    case 3:                    /* Onto object */
      if (task_trace)
        sc_trace ("Task: moving object %ld onto %ld\n", object, var3);

      gs_object_move_onto (game, object, obj_surface_object (game, var3));
      break;

    case 4:                    /* Held by */
      if (task_trace)
        sc_trace ("Task: moving object %ld to held by %ld\n", object, var3);

      if (var3 == 0)            /* Player */
        gs_object_player_get (game, object);
      else if (var3 == 1)       /* Ref character */
        gs_object_npc_get (game, object, var_get_ref_character (vars));
      else                      /* NPC id */
        gs_object_npc_get (game, object, var3 - 2);
      break;

    case 5:                    /* Worn by */
      if (task_trace)
        sc_trace ("Task: moving object %ld to worn by %ld\n", object, var3);

      if (var3 == 0)            /* Player */
        gs_object_player_wear (game, object);
      else if (var3 == 1)       /* Ref character */
        gs_object_npc_wear (game, object, var_get_ref_character (vars));
      else                      /* NPC id */
        gs_object_npc_wear (game, object, var3 - 2);
      break;

    case 6:                    /* Same room as */
      {
        sc_int room, npc;

        if (task_trace)
          {
            sc_trace ("Task: moving object %ld to same room as %ld\n",
                      object, var3);
          }

        if (var3 == 0)          /* Player */
          room = gs_playerroom (game);
        else if (var3 == 1)     /* Ref character */
          {
            npc = var_get_ref_character (vars);
            room = gs_npc_location (game, npc) - 1;
          }
        else                    /* NPC id */
          {
            npc = var3 - 2;
            room = gs_npc_location (game, npc) - 1;
          }
        gs_object_to_room (game, object, room);
        break;
      }

    default:
      sc_fatal ("task_move_object: unknown move type, %ld\n", var2);
    }
}


/*
 * task_run_move_object_action()
 *
 * Demultiplex an object move action and execute it.
 */
static void
task_run_move_object_action (sc_gameref_t game,
                             sc_int var1, sc_int var2, sc_int var3)
{
  const sc_var_setref_t vars = gs_get_vars (game);
  sc_int object;

  /* Select depending on value in var1. */
  switch (var1)
    {
    case 0:                    /* All held */
      for (object = 0; object < gs_object_count (game); object++)
        {
          if (gs_object_position (game, object) == OBJ_HELD_PLAYER)
            task_move_object (game, object, var2, var3);
        }
      break;

    case 1:                    /* All worn */
      for (object = 0; object < gs_object_count (game); object++)
        {
          if (gs_object_position (game, object) == OBJ_WORN_PLAYER)
            task_move_object (game, object, var2, var3);
        }
      break;

    case 2:                    /* Ref object */
      object = var_get_ref_object (vars);
      task_move_object (game, object, var2, var3);
      break;

    default:                   /* Dynamic object */
      object = obj_dynamic_object (game, var1 - 3);
      task_move_object (game, object, var2, var3);
      break;
    }
}


/*
 * task_move_npc_to_room()
 *
 * Move an NPC to a given room.
 */
static void
task_move_npc_to_room (sc_gameref_t game, sc_int npc, sc_int room)
{
  if (task_trace)
    sc_trace ("Task: moving NPC %ld to room %ld\n", npc, room);

  /* Update the NPC's state. */
  if (room < gs_room_count (game))
    gs_set_npc_location (game, npc, room + 1);
  else
    gs_set_npc_location (game, npc,
                         lib_random_roomgroup_member (game,
                                              room - gs_room_count (game)) + 1);

  gs_set_npc_parent (game, npc, -1);
  gs_set_npc_position (game, npc, 0);
}


/*
 * task_run_move_npc_action()
 *
 * Move player or NPC.
 */
static void
task_run_move_npc_action (sc_gameref_t game,
                          sc_int var1, sc_int var2, sc_int var3)
{
  const sc_var_setref_t vars = gs_get_vars (game);
  sc_int npc, room, ref_npc = -1;

  /* Player or NPC? */
  if (var1 == 0)
    {
      /* Player -- decide where to move player to. */
      switch (var2)
        {
        case 0:                /* To room */
          gs_move_player_to_room (game, var3);
          return;

        case 1:                /* To roomgroup part */
          if (task_trace)
            {
              sc_trace ("Task: moving player to random room in group %ld\n",
                        var3);
            }

          gs_move_player_to_room (game,
                                  lib_random_roomgroup_member (game, var3));
          return;

        case 2:                /* To same room as... */
          switch (var3)
            {
            case 0:            /* ...player! */
              return;
            case 1:            /* ...referenced NPC */
              npc = var_get_ref_character (vars);
              break;
            default:           /* ...specified NPC */
              npc = var3 - 2;
              break;
            }

          if (task_trace)
            sc_trace ("Task: moving player to same room as NPC %ld\n", npc);

          room = gs_npc_location (game, npc) - 1;
          if (room < 0)
            {
              if (task_trace)
                sc_trace ("Task: silently suppressed player move to hidden\n");
            }
          else
            gs_move_player_to_room (game, room);
          return;

        case 3:                /* To standing on */
          gs_set_playerposition (game, 0);
          gs_set_playerparent (game, obj_standable_object (game, var3 - 1));
          return;

        case 4:                /* To sitting on */
          gs_set_playerposition (game, 1);
          gs_set_playerparent (game, obj_standable_object (game, var3 - 1));
          return;

        case 5:                /* To lying on */
          gs_set_playerposition (game, 2);
          gs_set_playerparent (game, obj_lieable_object (game, var3 - 1));
          return;

        default:
          sc_fatal ("task_run_move_npc_action:"
                    " unknown player move type, %ld\n", var2);
          return;
        }
    }
  else
    {
      /* NPC -- first find which NPC to move about. */
      if (var1 == 1)
        npc = var_get_ref_character (vars);
      else
        npc = var1 - 2;

      /* Decide where to move the NPC to. */
      switch (var2)
        {
        case 0:                /* To room */
          task_move_npc_to_room (game, npc, var3 - 1);
          return;

        case 1:                /* To roomgroup part */
          if (task_trace)
            {
              sc_trace ("Task: moving NPC %ld to random room in group %ld\n",
                        npc, var3);
            }

          task_move_npc_to_room (game, npc,
                                 lib_random_roomgroup_member (game, var3));
          return;

        case 2:                /* To same room as... */
          switch (var3)
            {
            case 0:            /* ...player */
              if (task_trace)
                {
                  sc_trace ("Task: moving NPC %ld to same room as player\n",
                            npc);
                }

              task_move_npc_to_room (game, npc, gs_playerroom (game));
              break;
            case 1:            /* ...referenced NPC */
              ref_npc = var_get_ref_character (vars);
              if (task_trace)
                {
                  sc_trace ("Task: moving NPC %ld to"
                            " same room as referenced NPC %ld\n", npc, ref_npc);
                }

              room = gs_npc_location (game, ref_npc) - 1;
              task_move_npc_to_room (game, npc, room);
              break;
            default:           /* ...specified NPC */
              ref_npc = var3 - 2;
              if (task_trace)
                {
                  sc_trace ("Task: moving NPC %ld to"
                            " same room as NPC %ld\n", npc, ref_npc);
                }

              room = gs_npc_location (game, ref_npc) - 1;
              task_move_npc_to_room (game, npc, room);
              break;
            }
          return;

        case 3:                /* To standing on */
          gs_set_npc_position (game, npc, 0);
          gs_set_npc_parent (game, npc, obj_standable_object (game, var3));
          return;

        case 4:                /* To sitting on */
          gs_set_npc_position (game, npc, 1);
          gs_set_npc_parent (game, npc, obj_standable_object (game, var3));
          return;

        case 5:                /* To lying on */
          gs_set_npc_position (game, npc, 2);
          gs_set_npc_parent (game, npc, obj_lieable_object (game, var3));
          return;

        default:
          sc_fatal ("task_run_move_npc_action:"
                    " unknown NPC move type, %ld\n", var2);
          return;
        }
    }
}


/*
 * task_run_change_object_status()
 *
 * Change the status of an object.
 */
static void
task_run_change_object_status (sc_gameref_t game, sc_int var1, sc_int var2)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[3];
  sc_int object, openable, lockable;

  if (task_trace)
    {
      sc_trace ("Task: setting status of stateful object %ld to %ld\n",
                var1, var2);
    }

  /* Identify the target object. */
  object = obj_stateful_object (game, var1);

  /* See if openable. */
  vt_key[0].string = "Objects";
  vt_key[1].integer = object;
  vt_key[2].string = "Openable";
  openable = prop_get_integer (bundle, "I<-sis", vt_key);
  if (openable > 0)
    {
      /* See if lockable. */
      vt_key[2].string = "Key";
      lockable = prop_get_integer (bundle, "I<-sis", vt_key);
      if (lockable >= 0)
        {
          /* Lockable. */
          if (var2 <= 2)
            gs_set_object_openness (game, object, var2 + 5);
          else
            gs_set_object_state (game, object, var2 - 2);
        }
      else
        {
          /* Not lockable, though openable. */
          if (var2 <= 1)
            gs_set_object_openness (game, object, var2 + 5);
          else
            gs_set_object_state (game, object, var2 - 1);
        }
    }
  else
    /* Not openable. */
    gs_set_object_state (game, object, var2 + 1);

  if (task_trace)
    {
      sc_trace ("Task: openness of object %ld is now %ld\n",
                object, gs_object_openness (game, object));
      sc_trace ("Task: state of object %ld is now %ld\n",
                object, gs_object_state (game, object));
    }
}


/*
 * task_run_change_variable_action()
 *
 * Change a variable's value in inscrutable ways.
 */
static void
task_run_change_variable_action (sc_gameref_t game,
                                 sc_int var1, sc_int var2, sc_int var3,
                                 const sc_char *expr, sc_int var5)
{
  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);
  sc_vartype_t vt_key[3];
  const sc_char *name, *string;
  sc_char *mutable_string;
  sc_int type, value;

  /*
   * At this point, we need to checkpoint the filter.  We're about to change
   * a variable value, so interpolating here before doing that ensures that
   * any currently buffered text gets the values that were set when the text
   * was buffered.
   */
  pf_checkpoint (filter, vars, bundle);

  /* Get the name and type of the variable being addressed. */
  vt_key[0].string = "Variables";
  vt_key[1].integer = var1;
  vt_key[2].string = "Name";
  name = prop_get_string (bundle, "S<-sis", vt_key);
  vt_key[2].string = "Type";
  type = prop_get_integer (bundle, "I<-sis", vt_key);

  /* Select first based on variable type. */
  switch (type)
    {
    case TAFVAR_NUMERIC:       /* Integer */

      /* Select again based on action type. */
      switch (var2)
        {
        case 0:                /* Var = */
          if (task_trace)
            sc_trace ("Task: variable %ld (%s) = %ld\n", var1, name, var3);

          var_put_integer (vars, name, var3);
          return;

        case 1:                /* Var += */
          if (task_trace)
            sc_trace ("Task: variable %ld (%s) += %ld\n", var1, name, var3);

          value = var_get_integer (vars, name) + var3;
          var_put_integer (vars, name, value);
          return;

        case 2:                /* Var = rnd(range) */
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) = random(%ld,%ld)\n",
                        var1, name, var3, var5);
            }

          value = sc_randomint (var3, var5);
          var_put_integer (vars, name, value);
          return;

        case 3:                /* Var += rnd(range) */
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) += random(%ld,%ld)\n",
                        var1, name, var3, var5);
            }

          value = var_get_integer (vars, name) + sc_randomint (var3, var5);
          var_put_integer (vars, name, value);
          return;

        case 4:                /* Var = ref */
          value = var_get_ref_number (vars);
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) = ref, %ld\n",
                        var1, name, value);
            }

          var_put_integer (vars, name, value);
          return;

        case 5:                /* Var = expr */
          if (!expr_eval_numeric_expression (expr, vars, &value))
            {
              sc_error ("task_run_change_variable_action:"
                        " invalid expression, %s\n", expr);
              value = 0;
            }
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) = %s, %ld\n",
                        var1, name, expr, value);
            }

          var_put_integer (vars, name, value);
          return;

        default:
          sc_fatal ("task_run_change_variable_action:"
                    " unknown integer change type, %ld\n", var2);
        }

    case TAFVAR_STRING:        /* String */

      /* Select again based on action type. */
      switch (var2)
        {
        case 0:                /* Var = text literal */
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) = \"%s\"\n",
                        var1, name, expr);
            }

          var_put_string (vars, name, expr);
          return;

        case 1:                /* Var = ref */
          string = var_get_ref_text (vars);
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) = ref, \"%s\"\n",
                        var1, name, string);
            }

          var_put_string (vars, name, string);
          return;

        case 2:                /* Var = expr */
          if (!expr_eval_string_expression (expr, vars, &mutable_string))
            {
              sc_error ("task_run_change_variable_action:"
                        " invalid string expression, %s\n", expr);
              mutable_string = sc_malloc (strlen ("[expr error]") + 1);
              strcpy (mutable_string, "[expr error]");
            }
          if (task_trace)
            {
              sc_trace ("Task: variable %ld (%s) = %s, %s\n",
                        var1, name, expr, mutable_string);
            }

          var_put_string (vars, name, mutable_string);
          sc_free (mutable_string);
          return;

        default:
          sc_fatal ("task_run_change_variable_action:"
                    " unknown string change type, %ld\n", var2);
        }

    default:
      sc_fatal ("task_run_change_variable_action:"
                " invalid variable type, %ld\n", type);
    }
}


/*
 * task_run_change_score_action()
 *
 * Change game score.
 */
static void
task_run_change_score_action (sc_gameref_t game, sc_int task, sc_int var1)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);

  /* Increasing or decreasing the score? */
  if (var1 > 0)
    {
      sc_bool increase_score;

      /* See if this task is already scored. */
      increase_score = !gs_task_scored (game, task);
      if (!increase_score)
        {
          sc_vartype_t vt_key[3];
          sc_int version;

          if (task_trace)
            sc_trace ("Task: already scored task %ld\n", var1);

          /* Version 3.8 games permit tasks to rescore. */
          vt_key[0].string = "Version";
          version = prop_get_integer (bundle, "I<-s", vt_key);
          if (version == TAF_VERSION_380)
            {
              vt_key[0].string = "Tasks";
              vt_key[1].integer = task;
              vt_key[2].string = "SingleScore";
              increase_score = !prop_get_boolean (bundle, "B<-sis", vt_key);

              if (increase_score)
                {
                  if (task_trace)
                    sc_trace ("Task: rescoring version 3.8 task anyway\n");
                }
            }
        }

      /*
       * Increase the score if not yet scored or a version 3.8 multiple
       * scoring task, and note as a scored task.
       */
      if (increase_score)
        {
          if (task_trace)
            sc_trace ("Task: increased score by %ld\n", var1);

          game->score += var1;
          gs_set_task_scored (game, task, TRUE);
        }
    }
  else if (var1 < 0)
    {
      /* Decrease the score. */
      if (task_trace)
        sc_trace ("Task: decreased score by %ld\n", -(var1));

      game->score += var1;
    }
}


/*
 * task_run_set_task_action()
 *
 * Redirect to another task.
 */
static sc_bool
task_run_set_task_action (sc_gameref_t game, sc_int var1, sc_int var2)
{
  sc_bool status = FALSE;

  /* Select based on var1. */
  if (var1 == 0)
    {
      /* Redirect forwards. */
      if (task_can_run_task_directional (game, var2, TRUE))
        {
          if (task_trace)
            sc_trace ("Task: redirecting to task %ld\n", var2);

          status = task_run_task (game, var2, TRUE);
        }
      else
        {
          if (task_trace)
            sc_trace ("Task: can't redirect to task %ld\n", var2);
        }
    }
  else
    {
      /* Undo task. */
      gs_set_task_done (game, var2, FALSE);
      if (task_trace)
        sc_trace ("Task: reversing task %ld\n", var2);
    }

  return status;
}


/*
 * task_run_end_game_action()
 *
 * End of game task action.
 */
static sc_bool
task_run_end_game_action (sc_gameref_t game, sc_int var1)
{
  const sc_filterref_t filter = gs_get_filter (game);
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_bool status = FALSE;

  /* Print a message based on var1. */
  switch (var1)
    {
    case 0:
      {
        sc_vartype_t vt_key[2];
        const sc_char *wintext;

        /* Get game WinText. */
        vt_key[0].string = "Header";
        vt_key[1].string = "WinText";
        wintext = prop_get_string (bundle, "S<-ss", vt_key);

        /* Print WinText, if any defined, otherwise a default. */
        if (!sc_strempty (wintext))
          {
            pf_buffer_string (filter, wintext);
            pf_buffer_character (filter, '\n');
          }
        else
          pf_buffer_string (filter, "Congratulations!\n");

        /* Handle any associated WinRes resource. */
        vt_key[0].string = "Globals";
        vt_key[1].string = "WinRes";
        res_handle_resource (game, "ss", vt_key);

        status = TRUE;
        break;
      }

    case 1:
      pf_buffer_string (filter, "Better luck next time.\n");
      status = TRUE;
      break;

    case 2:
      pf_buffer_string (filter, "I'm afraid you are dead!\n");
      status = TRUE;
      break;

    case 3:
      break;

    default:
      sc_fatal ("task_run_end_game_action: invalid type, %ld\n", var1);
    }

  /* Stop the game, and note that it's not resumeable. */
  game->is_running = FALSE;
  game->has_completed = TRUE;

  return status;
}


/*
 * task_run_task_action()
 *
 * Demultiplexer for task actions.
 */
static sc_bool
task_run_task_action (sc_gameref_t game, sc_int task, sc_int action)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[5];
  sc_int type, var1, var2, var3, var5;
  const sc_char *expr;
  sc_bool status = FALSE;

  /* Get the task action type. */
  vt_key[0].string = "Tasks";
  vt_key[1].integer = task;
  vt_key[2].string = "Actions";
  vt_key[3].integer = action;
  vt_key[4].string = "Type";
  type = prop_get_integer (bundle, "I<-sisis", vt_key);

  /* Demultiplex depending on type. */
  switch (type)
    {
    case 0:                    /* Move object. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var2";
      var2 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var3";
      var3 = prop_get_integer (bundle, "I<-sisis", vt_key);
      task_run_move_object_action (game, var1, var2, var3);
      break;

    case 1:                    /* Move player/NPC. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var2";
      var2 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var3";
      var3 = prop_get_integer (bundle, "I<-sisis", vt_key);
      task_run_move_npc_action (game, var1, var2, var3);
      break;

    case 2:                    /* Change object status. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var2";
      var2 = prop_get_integer (bundle, "I<-sisis", vt_key);
      task_run_change_object_status (game, var1, var2);
      break;

    case 3:                    /* Change variable. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var2";
      var2 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var3";
      var3 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Expr";
      expr = prop_get_string (bundle, "S<-sisis", vt_key);
      vt_key[4].string = "Var5";
      var5 = prop_get_integer (bundle, "I<-sisis", vt_key);
      task_run_change_variable_action (game, var1, var2, var3, expr, var5);
      break;

    case 4:                    /* Change score. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      task_run_change_score_action (game, task, var1);
      break;

    case 5:                    /* Execute/unset task. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      vt_key[4].string = "Var2";
      var2 = prop_get_integer (bundle, "I<-sisis", vt_key);
      status = task_run_set_task_action (game, var1, var2);
      break;

    case 6:                    /* End game. */
      vt_key[4].string = "Var1";
      var1 = prop_get_integer (bundle, "I<-sisis", vt_key);
      status = task_run_end_game_action (game, var1);
      break;

    case 7:                    /* Battle options, ignored for now... */
      break;

    default:
      sc_fatal ("task_run_task_action: unknown action type %ld\n", type);
    }

  return status;
}


/*
 * task_run_task_actions()
 *
 * Run every task action associated with the task.  If any action ends the
 * game, return immediately.  Returns TRUE if any action ran and itself
 * returned TRUE.
 */
static sc_bool
task_run_task_actions (sc_gameref_t game, sc_int task)
{
  const sc_filterref_t filter = gs_get_filter (game);
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[3];
  sc_int action_count, action;
  sc_bool status, muted;

  /* Get the count of task actions. */
  vt_key[0].string = "Tasks";
  vt_key[1].integer = task;
  vt_key[2].string = "Actions";
  action_count = prop_get_child_count (bundle, "I<-sis", vt_key);

  if (action_count > 0)
    {
      if (task_trace)
        {
          sc_trace ("Task: task %ld running %ld action%s\n",
                    task, action_count, action_count == 1 ? "" : "s");
        }
    }

  /*
   * Run all task actions, capturing any TRUE status returned.  If any task
   * ends the game, run the remaining tasks silently.
   *
   * This seems a little counterintuitive; a more conventional thing would be
   * to just exit the actions loop early.  However, Adrift appears to plough
   * on, and there may be an action that changes the score in here somewhere,
   * so we'll do the same.
   */
  status = FALSE;
  muted = FALSE;
  for (action = 0; action < action_count; action++)
    {
      sc_bool was_running;

      was_running = game->is_running;
      status |= task_run_task_action (game, task, action);

      /* Did this action end the game? */
      if (was_running && !game->is_running)
        {
          if (task_trace)
            {
              sc_trace ("Task: task %ld action %ld ended game\n",
                        task, action);
            }

          /* Mute the filter, and note that we did it, but continue. */
          pf_mute (filter);
          muted = TRUE;
        }
    }

  /* If this stack frame muted the filter, un-mute it now. */
  if (muted)
    pf_clear_mute (filter);

  /* Return TRUE if any task action returned TRUE. */
  return status;
}


/*
 * task_start_npc_walks()
 *
 * Start NPC walks based on alerts.
 */
static void
task_start_npc_walks (sc_gameref_t game, sc_int task)
{
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[4];
  sc_int alert_count, alert;

  /* Get a count of NPC walk alerts. */
  vt_key[0].string = "Tasks";
  vt_key[1].integer = task;
  vt_key[2].string = "NPCWalkAlert";
  alert_count = prop_get_child_count (bundle, "I<-sis", vt_key);

  /* Check alerts, and start any walks that need starting. */
  for (alert = 0; alert < alert_count; alert += 2)
    {
      sc_int npc, walk;

      vt_key[3].integer = alert;
      npc = prop_get_integer (bundle, "I<-sisi", vt_key);
      vt_key[3].integer = alert + 1;
      walk = prop_get_integer (bundle, "I<-sisi", vt_key);
      npc_start_npc_walk (game, npc, walk);
    }
}


/*
 * task_run_task_unrestricted()
 *
 * Run a task, providing restrictions permit, in the given direction.  Return
 * TRUE if the task ran, or we handled it in some complete way, for example by
 * outputting a message describing what prevented it, or why it couldn't be
 * done.
 */
static sc_bool
task_run_task_unrestricted (sc_gameref_t game, sc_int task, sc_bool forwards)
{
  const sc_filterref_t filter = gs_get_filter (game);
  const sc_prop_setref_t bundle = gs_get_bundle (game);
  sc_vartype_t vt_key[3];
  const sc_char *completetext, *additionalmessage;
  sc_int action_count, showroomdesc;
  sc_bool status;

  /* Start considering task output tracking. */
  status = FALSE;

  /*
   * If reversing, print any reverse message for the task, and undo the task,
   * then return.
   */
  if (!forwards)
    {
      const sc_char *reversemessage;

      /* If not yet done, we can hardly reverse it. */
      if (gs_task_done (game, task))
        {
          vt_key[0].string = "Tasks";
          vt_key[1].integer = task;
          vt_key[2].string = "ReverseMessage";
          reversemessage = prop_get_string (bundle, "S<-sis", vt_key);
          if (!sc_strempty (reversemessage))
            {
              pf_buffer_string (filter, reversemessage);
              pf_buffer_character (filter, '\n');
              status |= TRUE;
            }

          /* Undo the task. */
          gs_set_task_done (game, task, FALSE);
        }

      /* Return status of undo. */
      return status;
    }

  /* See if we are trying to repeat a task that's not repeatable. */
  if (gs_task_done (game, task))
    {
      sc_bool repeatable;

      vt_key[0].string = "Tasks";
      vt_key[1].integer = task;
      vt_key[2].string = "Repeatable";
      repeatable = prop_get_boolean (bundle, "B<-sis", vt_key);
      if (!repeatable)
        {
          const sc_char *repeattext;

          vt_key[2].string = "RepeatText";
          repeattext = prop_get_string (bundle, "S<-sis", vt_key);
          if (!sc_strempty (repeattext))
            {
              if (task_trace)
                {
                  sc_trace ("Task:"
                            " trying to repeat completed action, aborting\n");
                }

              pf_buffer_string (filter, repeattext);
              pf_buffer_character (filter, '\n');
              status |= TRUE;
              return status;
            }

          /*
           * Task done, yet not repeatable, so don't consider this case
           * handled.
           */
          return status;
        }
    }

  /* Mark the task as done. */
  gs_set_task_done (game, task, TRUE);

  /* Print any task completion text. */
  vt_key[0].string = "Tasks";
  vt_key[1].integer = task;
  vt_key[2].string = "CompleteText";
  completetext = prop_get_string (bundle, "S<-sis", vt_key);
  if (!sc_strempty (completetext))
    {
      pf_buffer_string (filter, completetext);
      pf_buffer_character (filter, '\n');
      status |= TRUE;
    }

  /* Handle any task completion resource. */
  vt_key[2].string = "Res";
  res_handle_resource (game, "sis", vt_key);

  /*
   * Things get slightly tricky here.  We need to filter the completion text
   * for the task using any final variable values generated or modified by
   * task actions, but other task text, run by actions, according to the
   * variable value in effect when it runs.
   *
   * To do this, we take a local copy of the filter's current buffer at this
   * point, remove it from the filter, run task actions with checkpointing,
   * then prepend it back into the filter after all the actions are done.
   *
   * As an optimization, we can avoid doing this if there are no task actions.
   */
  vt_key[2].string = "Actions";
  action_count = prop_get_child_count (bundle, "I<-sis", vt_key);
  if (action_count > 0)
    {
      sc_char *buffer;

      /*
       * Take ownership of the current filter buffer text, then start NPC
       * walks based on alerts, and run any and all task actions.  Note that
       * the buffer transferred out of the filter may be NULL if there is no
       * text currently in the filter.
       */
      buffer = pf_transfer_buffer (filter);
      task_start_npc_walks (game, task);
      status |= task_run_task_actions (game, task);

      /* Prepend the saved buffer data back onto the front of the filter. */
      if (buffer)
        {
          pf_prepend_string (filter, buffer);
          sc_free (buffer);
        }
    }
  else
    {
      /* Start NPC walks only; there are no task actions. */
      task_start_npc_walks (game, task);
    }

  /* Append any room description and additional message for the task. */
  vt_key[2].string = "ShowRoomDesc";
  showroomdesc = prop_get_integer (bundle, "I<-sis", vt_key);
  if (showroomdesc != 0)
    {
      lib_print_room_name (game, showroomdesc - 1);
      lib_print_room_description (game, showroomdesc - 1);
      status |= TRUE;
    }

  vt_key[2].string = "AdditionalMessage";
  additionalmessage = prop_get_string (bundle, "S<-sis", vt_key);
  if (!sc_strempty (additionalmessage))
    {
      pf_buffer_string (filter, additionalmessage);
      pf_buffer_character (filter, '\n');
      status |= TRUE;
    }

  /* Return status -- TRUE if matched and we output something. */
  return status;
}


/*
 * task_run_task()
 *
 * Run a task, providing restrictions permit, in the given direction.  At the
 * same time, check for signs of an infinite loop in game tasks, and fail the
 * task with an error message if we seem to be in one.  Checked by counting
 * the call depth.
 */
sc_bool
task_run_task (sc_gameref_t game, sc_int task, sc_bool forwards)
{
  static sc_int recursion_depth = 0;

  const sc_filterref_t filter = gs_get_filter (game);
  const sc_char *fail_message;
  sc_bool restrictions_passed, status;

  if (task_trace)
    {
      sc_trace ("Task: running task %ld %s, depth %ld\n",
                task, forwards ? "forwards" : "backwards", recursion_depth);
    }

  /* Check restrictions. */
  if (!restr_eval_task_restrictions (game, task,
                                     &restrictions_passed, &fail_message))
    {
      sc_error ("task_run_task: restrictions error, %ld\n", task);
      return FALSE;
    }
  if (!restrictions_passed)
    {
      if (task_trace)
        {
          sc_trace ("Task: restrictions failed, task %s\n",
                    fail_message ? "failed" : "aborted");
        }

      if (fail_message)
        {
          /*
           * Print a message, and return TRUE since we can consider this task
           * "done" (more accurately, we've output text, so the task command
           * searching in the main run loop can exit...).
           */
          pf_buffer_string (filter, fail_message);
          pf_buffer_character (filter, '\n');
          return TRUE;
        }

      /* Task not done; look for more possibilities. */
      return FALSE;
    }

  /* Check for infinite recursion. */
  if (recursion_depth > TASK_MAXIMUM_RECURSION)
    {
      sc_error ("task_run_task: maximum recursion depth exceeded --"
                " game task loop?\n");
      return FALSE;
    }

  /* Increment depth, run the task, then decrement depth. */
  recursion_depth++;
  status = task_run_task_unrestricted (game, task, forwards);
  recursion_depth--;

  if (task_trace)
    {
      sc_trace ("Task: task %ld finished, return %s, depth %ld\n",
                task, status ? "true" : "false", recursion_depth);
    }

  /* Return the task's status. */
  return status;
}


/*
 * task_debug_trace()
 *
 * Set task tracing on/off.
 */
void
task_debug_trace (sc_bool flag)
{
  task_trace = flag;
}
