/*
 * Copyright (C) 2003-2004  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 Adds new "types" to jAsea's property descriptor: 'M' for multiline
 *   strings, 'Z', 'F'/'T', and 'E' for defaulted integers, booleans, and
 *   strings, 'i', 'b', and 's' for ignored integers, booleans, and strings,
 *   and '{...}' and '|...|' for "special" descriptions and version fixups
 *   that can't be described as things stand.
 *
 * o Adds new 'G' expression test, to check Global boolean.
 *
 * o The stack "adjustment" stuff is a bit of a bother.
 */

#include <assert.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include <setjmp.h>

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


/* Assorted definitions and constants. */
static const sc_char	NUL			= '\0',
			COMMA			= ',';
enum {			PARSE_TEMP_LENGTH	= 256,
			PARSE_MAX_DEPTH		= 32 };
enum {			ROOMLIST_NO_ROOMS	= 0,
			ROOMLIST_ONE_ROOM	= 1,
			ROOMLIST_SOME_ROOMS	= 2,
			ROOMLIST_ALL_ROOMS	= 3,
			ROOMLIST_NPC_PART	= 4 };

/* Multiline separator sequences for the various versions supported. */
static const sc_byte	V400_SEPARATOR[3]	= { 0xBD, 0xD0, 0x00 },
			V390_SEPARATOR[3]	= { 0x2A, 0x2A, 0x00 },
			V380_SEPARATOR[3]	= { 0x2A, 0x2A, 0x00 };
static const sc_uint16	SEPARATOR_SIZE		= 3;

/* Enumeration of TAF file versions. */
enum {			TAF_VERSION_400		= 400,
			TAF_VERSION_390		= 390,
			TAF_VERSION_380		= 380 };

/* Enumerations of key element types and property types. */
static const sc_char	KEY_STRING		= 's',
			KEY_INTEGER		= 'i',
			PROP_INTEGER		= 'I',
			PROP_BOOLEAN		= 'B',
			PROP_STRING		= 'S';


/*
 * Tables of properties descriptors.  These strings define the structure of
 * a TAF file.  Field keys are:
 *
 * $,#,B,M	- string, integer, boolean, and multiline properties
 * E,F,T,Z	- string, integer, and boolean properties not in the TAF;
 *		  set to "", FALSE, TRUE and zero on parsing (version < 4)
 * i,s,b	- string, integer, and boolean in the TAF, but not stored
 * [num]	- arrays of property, fixed size to num
 * V		- variable sized array of property, size in input file
 * W		- like V, but size - 1 in input file (version < 4)
 * <class>	- class of property, separate parse target (recurse)
 * ?[!]expr:	- conditional property based on expr
 * G[!]expr:	- conditional property based on expr using globals
 * |...|	- fixup specials for versions < 4
 * {special}	- because some things just defy description
 */
typedef struct {
	sc_char		*class;
	sc_char		*descriptor;
} sc_parse_schema_t;

/* Version 4.0 TAF file properties descriptor table. */
static const	sc_parse_schema_t	V400_PARSE_SCHEMA[] = {
{ "_GAME_",	"<HEADER>Header <GLOBAL>Globals V<ROOM>Rooms V<OBJECT>Objects"
		" V<TASK>Tasks V<EVENT>Events V<NPC>NPCs"
		" V<ROOM_GROUP>RoomGroups V<SYNONYM>Synonyms"
		" V<VARIABLE>Variables V<ALR>ALRs BCustomFont"
		" ?BCustomFont:$FontNameSize $CompileDate" },
{ "HEADER",	"MStartupText #StartRoom MWinText" },
{ "GLOBAL",	"$GameName $GameAuthor $DontUnderstand #Perspective"
		" BShowExits #WaitTurns BDispFirstRoom BBattleSystem"
		" #MaxScore $PlayerName BPromptName $PlayerDesc #Task"
		" ?!#Task=0:$AltDesc #Position #ParentObject #PlayerGender"
		" #MaxSize #MaxWt"
		" ?GBattleSystem:iStaminaLo,iStaminaHi,iStrengthLo,iStrengthHi"
		",iAccuracyLo,iAccuracyHi,iDefenseLo,iDefenseHi"
		",iAgilityLo,iAgilityHi,iRecovery BEightPointCompass bNoDebug"
		" BNoScoreNotify BNoMap bNoAutoComplete bNoControlPanel"
		" bNoMouse BSound BGraphics"
		" <RESOURCE>IntroRes <RESOURCE>WinRes"
		" BStatusBox $StatusBoxText iUnk1 iUnk2 BEmbedded" },
{ "ROOM",	"$Short $Long"
		" ?GEightPointCompass:[12]<ROOM_EXIT>Exits"
		" ?!GEightPointCompass:[8]<ROOM_EXIT>Exits"
		" <RESOURCE>Res V<ROOM_ALT>Alts"
		" ?!GNoMap:bHideOnMap" },
{ "ROOM_EXIT",	"{V400_ROOM_EXIT:#Dest_#Var1_#Var2_#Var3}" },
{ "ROOM_ALT",	"$M1 #Type <RESOURCE>Res1 $M2 #Var2 <RESOURCE>Res2 #HideObjects"
		" $Changed #Var3 #DisplayRoom" },
{ "RESOURCE",	"?GSound:$SoundFile,#SoundLen,ZSoundOffset"
		" ?GGraphics:$GraphicFile,#GraphicLen,ZGraphicOffset"
		" {V400_RESOURCE}" },
{ "OBJECT",	"$Prefix $Short V$Alias BStatic $Description"
		" #InitialPosition #Task BTaskNotDone $AltDesc"
		" ?BStatic:<ROOM_LIST1>Where BContainer BSurface #Capacity"
		" ?!BStatic:BWearable,#SizeWeight,#Parent"
		" ?BStatic:{OBJECT:#Parent}"
		" #Openable ?#Openable=5:#Key ?#Openable=6:#Key"
		" ?#Openable=7:#Key #SitLie ?!BStatic:BEdible BReadable"
		" ?BReadable:$ReadText ?!BStatic:BWeapon #CurrentState"
		" ?!#CurrentState=0:$States,BStateMentioned"
		" BListFlag <RESOURCE>Res1 <RESOURCE>Res2"
		" ?GBattleSystem:iProtectionValue,iHitValue,iMethod,iAccuracy"
		" $InRoomDesc #OnlyWhenNotMoved" },
{ "ROOM_LIST1",	"#Type {ROOM_LIST1}" },
{ "TASK",	"V$Command $CompleteText $ReverseMessage $RepeatText"
		" $AdditionalMessage #ShowRoomDesc BRepeatable BReversible"
		" V$ReverseCommand <ROOM_LIST0>Where $Question"
		" ?$Question:$Hint1,$Hint2 V<TASK_RESTR>Restrictions"
		" V<TASK_ACTION>Actions $RestrMask <RESOURCE>Res" },
{ "TASK_RESTR",	"#Type ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2"
		" ?#Type=2:#Var1,#Var2 ?#Type=3:#Var1,#Var2,#Var3"
		" ?#Type=4:#Var1,#Var2,#Var3,$Var4 $FailMessage" },
{ "TASK_ACTION","#Type ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2,#Var3"
		" ?#Type=2:#Var1,#Var2 ?#Type=3:#Var1,#Var2,#Var3,$Expr,#Var5"
		" ?#Type=4:#Var1 ?#Type=5:#Var1,#Var2"
		" ?#Type=6:#Var1,#Var2,#Var3 ?#Type=7:iVar1,iVar2,iVar3" },
{ "ROOM_LIST0",	"#Type {ROOM_LIST0}" },
{ "EVENT",	"$Short #StarterType ?#StarterType=2:#StartTime,#EndTime"
		" ?#StarterType=3:#TaskNum #RestartType BTaskFinished #Time1"
		" #Time2 $StartText $LookText $FinishText <ROOM_LIST0>Where"
		" #PauseTask BPauserCompleted #PrefTime1 $PrefText1"
		" #ResumeTask BResumerCompleted #PrefTime2 $PrefText2"
		" #Obj2 #Obj2Dest #Obj3 #Obj3Dest #Obj1 #Obj1Dest"
		" #TaskAffected [5]<RESOURCE>Res" },
{ "NPC",	"$Name $Prefix V$Alias $Descr #StartRoom $AltText #Task"
		" V<TOPIC>Topics V<WALK>Walks BShowEnterExit"
		" ?BShowEnterExit:$EnterText,$ExitText $InRoomText #Gender"
		" [4]<RESOURCE>Res"
		" ?GBattleSystem:iAttitude,iStaminaLo,iStaminaHi"
		",iStrengthLo,iStrengthHi,iAccuracyLo,iAccuracyHi"
		",iDefenseLo,iDefenseHi,iAgilityLo,iAgilityHi,iSpeed"
		",iKilledTask,iRecovery,iStaminaTask" },
{ "TOPIC",	"$Subject $Reply #Task $AltReply" },
{ "WALK",	"#NumStops BLoop #StartTask #CharTask #MeetObject #ObjectTask"
		" #StoppingTask #MeetChar $ChangedDesc {WALK:#Rooms_#Times}" },
{ "ROOM_GROUP",	"$Name {ROOM_GROUP:[]BList}" },
{ "SYNONYM",	"$Replacement $Original" },
{ "VARIABLE",	"$Name #Type $Value" },
{ "ALR",	"$Original $Replacement" },
{ NULL,		NULL }
};

/* Version 3.9 TAF file properties descriptor table. */
static const	sc_parse_schema_t	V390_PARSE_SCHEMA[] = {
{ "_GAME_",	"<HEADER>Header <GLOBAL>Globals V<ROOM>Rooms V<OBJECT>Objects"
		" V<TASK>Tasks V<EVENT>Events V<NPC>NPCs"
		" V<ROOM_GROUP>RoomGroups V<SYNONYM>Synonyms"
		" V<VARIABLE>Variables V<ALR>ALRs BCustomFont"
		" ?BCustomFont:$FontNameSize $CompileDate sPassword" },
{ "HEADER",	"MStartupText #StartRoom MWinText" },
{ "GLOBAL",	"$GameName $GameAuthor $DontUnderstand #Perspective"
		" BShowExits #WaitTurns BDispFirstRoom BBattleSystem"
		" #MaxScore $PlayerName BPromptName $PlayerDesc #Task"
		" ?!#Task=0:$AltDesc #Position #ParentObject #PlayerGender"
		" #MaxSize #MaxWt"
		" ?GBattleSystem:iStamina,iStrength,iDefense"
		" BEightPointCompass bNoDebug"
		" BNoScoreNotify BNoMap bNoAutoComplete bNoControlPanel"
		" bNoMouse BSound BGraphics"
		" <RESOURCE>IntroRes <RESOURCE>WinRes"
		" FStatusBox EStatusBoxText iUnk1 iUnk2 FEmbedded" },
{ "ROOM",	"$Short $Long $LastDesc"
		" ?GEightPointCompass:[12]<ROOM_EXIT>Exits"
		" ?!GEightPointCompass:[8]<ROOM_EXIT>Exits"
		" $AddDesc1 #Task1 $AddDesc2 #Task2"
		" #Obj $AltDesc #TypeHideObjects"
		" <RESOURCE>Res"
		" <RESOURCE>LastRes <RESOURCE>Task1Res <RESOURCE>Task2Res"
		" <RESOURCE>AltRes"
		" ?!GNoMap:bHideOnMap"
		" |V390_ROOM:_Alts_|" },
{ "ROOM_EXIT",	"{V390_V380_ROOM_EXIT:#Dest_#Var1_#Var2_ZVar3}" },
{ "RESOURCE",	"?GSound:$SoundFile,ZSoundLen,ZSoundOffset"
		" ?GGraphics:$GraphicFile,ZGraphicLen,ZGraphicOffset" },
{ "OBJECT",	"$Prefix $Short"
		" [1]$Alias BStatic $Description"
		" #InitialPosition #Task BTaskNotDone $AltDesc"
		" ?BStatic:<ROOM_LIST1>Where BContainer BSurface #Capacity"
		" ?!BStatic:BWearable,#SizeWeight,#Parent"
		" ?BStatic:{OBJECT:#Parent}"
		" #Openable |V390_OBJECT:_Openable_,Key|"
		" #SitLie ?!BStatic:BEdible BReadable"
		" ?BReadable:$ReadText ?!BStatic:BWeapon ZCurrentState"
		" FListFlag <RESOURCE>Res1 <RESOURCE>Res2"
		" ?GBattleSystem:iProtectionValue,iHitValue,iMethod"
		" EInRoomDesc ZOnlyWhenNotMoved" },
{ "ROOM_LIST1",	"#Type {ROOM_LIST1}" },
{ "TASK",	"W$Command $CompleteText $ReverseMessage $RepeatText"
		" $AdditionalMessage #ShowRoomDesc BRepeatable BReversible"
		" W$ReverseCommand <ROOM_LIST0>Where $Question"
		" ?$Question:$Hint1,$Hint2 V<TASK_RESTR>Restrictions"
		" V<TASK_ACTION>Actions |V390_TASK:$RestrMask| <RESOURCE>Res" },
{ "TASK_RESTR",	"#Type ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2"
		" ?#Type=2:#Var1,#Var2 ?#Type=3:#Var1,#Var2,#Var3"
		" ?#Type=4:#Var1,#Var2,#Var3,EVar4"
		",|V390_TASK_RESTR:Var1>0?#Var1++|"
		" $FailMessage" },
{ "TASK_ACTION","#Type |V390_TASK_ACTION:Type>4?#Type++|"
		" ?#Type=0:#Var1,#Var2,#Var3 ?#Type=1:#Var1,#Var2,#Var3"
		" ?#Type=2:#Var1,#Var2"
		" ?#Type=3:#Var1,#Var2,#Var3,|V390_TASK_ACTION:$Expr_#Var5|"
		" ?#Type=4:#Var1"
		" ?#Type=6:#Var1,ZVar2,ZVar3 ?#Type=7:iVar1,iVar2,iVar3" },
{ "ROOM_LIST0",	"#Type {ROOM_LIST0}" },
{ "EVENT",	"$Short #StarterType ?#StarterType=2:#StartTime,#EndTime"
		" ?#StarterType=3:#TaskNum #RestartType BTaskFinished #Time1"
		" #Time2 $StartText $LookText $FinishText <ROOM_LIST0>Where"
		" #PauseTask BPauserCompleted #PrefTime1 $PrefText1"
		" #ResumeTask BResumerCompleted #PrefTime2 $PrefText2"
		" #Obj2 #Obj2Dest #Obj3 #Obj3Dest #Obj1 #Obj1Dest"
		" #TaskAffected [5]<RESOURCE>Res" },
{ "NPC",	"$Name $Prefix [1]$Alias $Descr #StartRoom $AltText #Task"
		" V<TOPIC>Topics V<WALK>Walks BShowEnterExit"
		" ?BShowEnterExit:$EnterText,$ExitText $InRoomText #Gender"
		" [4]<RESOURCE>Res"
		" ?GBattleSystem:iAttitude,iStamina,iStrength,iDefense"
		",iSpeed,iKilledTask" },
{ "TOPIC",	"$Subject $Reply #Task $AltReply" },
{ "WALK",	"#NumStops BLoop #StartTask #CharTask #MeetObject #ObjectTask"
		" #StoppingTask ZMeetChar $ChangedDesc {WALK:#Rooms_#Times}" },
{ "ROOM_GROUP",	"$Name {ROOM_GROUP:[]BList}" },
{ "SYNONYM",	"$Replacement $Original" },
{ "VARIABLE",	"$Name ZType $Value" },
{ "ALR",	"$Original $Replacement" },
{ NULL,		NULL }
};

/* Version 3.8 TAF file properties descriptor table. */
static const	sc_parse_schema_t	V380_PARSE_SCHEMA[] = {
{ "_GAME_",	"<HEADER>Header <GLOBAL>Globals V<ROOM>Rooms V<OBJECT>Objects"
		" V<TASK>Tasks V<EVENT>Events V<NPC>NPCs"
		" V<ROOM_GROUP>RoomGroups V<SYNONYM>Synonyms"
		" FCustomFont $CompileDate sPassword"
		" |V380_GLOBAL:_MaxScore_|"
		" |V380_OBJECT:_InitialPositions_|" },
{ "HEADER",	"MStartupText #StartRoom MWinText" },
{ "GLOBAL",	"$GameName $GameAuthor #MaxCarried |V380_MaxSize_MaxWt_|"
		" $DontUnderstand #Perspective"
		" BShowExits #WaitTurns FDispFirstRoom FBattleSystem"
		" EPlayerName FPromptName EPlayerDesc ZTask ZPlayerGender"
		" FEightPointCompass TNoScoreNotify"
		" FSound FGraphics FStatusBox EStatusBoxText FEmbedded" },
{ "ROOM",	"$Short $Long $LastDesc"
		" [8]<ROOM_EXIT>Exits $AddDesc1 #Task1 $AddDesc2 #Task2"
		" #Obj $AltDesc #TypeHideObjects"
		" |V380_ROOM:_Alts_|" },
{ "ROOM_EXIT",	"{V390_V380_ROOM_EXIT:#Dest_#Var1_#Var2_ZVar3}" },
{ "OBJECT",	"$Prefix $Short"
		" [1]$Alias BStatic $Description"
		" #InitialPosition #Task BTaskNotDone $AltDesc"
		" ?BStatic:<ROOM_LIST1>Where #SurfaceContainer"
		" FSurface ?#SurfaceContainer=2:TSurface"
		" FContainer ?#SurfaceContainer=1:TContainer"
		" #Capacity |V380_OBJECT:#Capacity*10+2|"
		" ?!BStatic:BWearable,#SizeWeight,#Parent"
		" ?BStatic:{OBJECT:#Parent}"
		" #Openable |V380_OBJECT:_Openable_,Key|"
		" #SitLie ?!BStatic:BEdible BReadable"
		" ?BReadable:$ReadText ?!BStatic:BWeapon ZCurrentState"
		" FListFlag  EInRoomDesc ZOnlyWhenNotMoved" },
{ "ROOM_LIST1",	"#Type {ROOM_LIST1}" },
{ "TASK",	"W$Command $CompleteText $ReverseMessage $RepeatText"
		" $AdditionalMessage #ShowRoomDesc BRepeatable"
		" #Score BSingleScore"
		" [6]<TASK_MOVE>Movements BReversible"
		" W$ReverseCommand #WearObj1 #WearObj2"
		" #HoldObj1 #HoldObj2 #HoldObj3"
		" #Obj1 #Task BTaskNotDone"
		" $TaskMsg $HoldMsg $WearMsg $CompanyMsg"
		" BNotInSameRoom #NPC $Obj1Msg #Obj1Room"
		" <ROOM_LIST0>Where BKillsPlayer BHoldingSameRoom"
		" $Question"
		" ?$Question:$Hint1,$Hint2 #Obj2"
		" ?!#Obj2=0:#Obj2Var1,#Obj2Var2,$Obj2Msg"
		" BWinGame"
		" |V380_TASK:_Actions_| |V380_TASK:_Restrictions_|" },
{ "TASK_MOVE",	"#Var1 #Var2 #Var3" },
{ "ROOM_LIST0",	"#Type {ROOM_LIST0}" },
{ "EVENT",	"$Short #StarterType ?#StarterType=2:#StartTime,#EndTime"
		" ?#StarterType=3:#TaskNum #RestartType BTaskFinished #Time1"
		" #Time2 $StartText $LookText $FinishText <ROOM_LIST0>Where"
		" #PauseTask BPauserCompleted #PrefTime1 $PrefText1"
		" #ResumeTask BResumerCompleted #PrefTime2 $PrefText2"
		" #Obj2 #Obj2Dest #Obj3 #Obj3Dest #Obj1 #Obj1Dest"
		" #TaskAffected" },
{ "NPC",	"$Name $Prefix [1]$Alias $Descr #StartRoom $AltText #Task"
		" V<TOPIC>Topics V<WALK>Walks BShowEnterExit"
		" ?BShowEnterExit:$EnterText,$ExitText $InRoomText ZGender" },
{ "TOPIC",	"$Subject $Reply #Task $AltReply" },
{ "WALK",	"#NumStops BLoop #StartTask #CharTask #MeetObject"
		" ?!#MeetObject=0:|V380_WALK:_MeetObject_| #ObjectTask"
		" ZMeetChar {WALK:#Rooms_#Times}"
		" ZStoppingTask EChangedDesc" },
{ "ROOM_GROUP",	"$Name {ROOM_GROUP:[]BList}" },
{ "SYNONYM",	"$Replacement $Original" },
{ NULL,		NULL }
};


/*
 * parse_select_schema()
 *
 * Select one of the parse schemata based on a TAF file.
 */
static const sc_parse_schema_t *
parse_select_schema (sc_tafref_t taf)
{
	/* Switch based on the TAF file version. */
	switch (taf_get_version (taf))
	    {
	    case TAF_VERSION_400:	return V400_PARSE_SCHEMA;
	    case TAF_VERSION_390:	return V390_PARSE_SCHEMA;
	    case TAF_VERSION_380:	return V380_PARSE_SCHEMA;
	    default:
		sc_fatal ("parse_select_schema: invalid TAF file version\n");
		return NULL;
	    }
}


/* The uncompressed TAF file from which we get all our data. */
static sc_tafref_t		parse_taf		= NULL;
static sc_uint32		parse_tafline		= 0;

/* The parse schema selected for this TAF file. */
static sc_parse_schema_t const	*parse_schema		= NULL;

/* Properties bundle and trace flag, set before parsing. */
static sc_prop_setref_t		parse_bundle		= NULL;
static sc_bool			parse_trace		= FALSE;

/*
 * Stack of property keys.  The stack is filled by parsing, and written
 * to the property store on parse terminals.
 */
static sc_vartype_t		parse_vt_key[PARSE_MAX_DEPTH];
static sc_char			parse_format[PARSE_MAX_DEPTH];
static sc_uint16		parse_stack	= 0;


/*
 * parse_push_key()
 * parse_pop_key()
 *
 * Push a key of the given type onto the property key stack, and pop a key
 * off on unwind.
 */
static void
parse_push_key (sc_vartype_t vt_key, sc_char type)
{
	/* Check the stack still has space. */
	if (parse_stack == PARSE_MAX_DEPTH)
		sc_fatal ("parse_push_key: stack overrun\n");

	/* Push the key, and it's associated type. */
	parse_vt_key[parse_stack]	= vt_key;
	parse_format[parse_stack]	= type;
	parse_stack++;
}

static void
parse_pop_key (void)
{
	/* Check the stack has something to pop, then pop it. */
	if (parse_stack == 0)
		sc_fatal ("parse_pop_key: stack underrun\n");
	parse_stack--;
}


/*
 * parse_stack_swap()
 * parse_adjust_stack()
 *
 * This is ugly.  The parse produces indexes before the things that they
 * index.  An expedient fix is to switch i-s keys before storing a
 * property value; note that the caller should take a safe copy of the
 * stack beforehand, and restore it before returning to the parser.
 */
static void
parse_stack_swap (sc_uint16 i, sc_uint16 j)
{
	sc_char		temp;
	sc_vartype_t	vt_temp;

	/* Standard swap of format and keys. */
	temp		= parse_format[i];
	parse_format[i]	= parse_format[j];
	parse_format[j]	= temp;
	vt_temp		= parse_vt_key[i];
	parse_vt_key[i]	= parse_vt_key[j];
	parse_vt_key[j]	= vt_temp;
}

static void
parse_adjust_stack (void)
{
	sc_uint16	index;

	/* Switch index-string key pairs. */
	for (index = 0; index < parse_stack - 1; index++)
	    {
		if (parse_format[index] == KEY_INTEGER
				&& parse_format[index + 1] == KEY_STRING)
		    {
			parse_stack_swap (index, index + 1);
			index++;
		    }
	    }
}


/*
 * parse_put_property()
 * parse_get_property()
 *
 * Write or read a property based on the keys amassed so far.
 */
static void
parse_put_property (sc_vartype_t vt_value, sc_char type)
{
	sc_vartype_t		safe_parse_vt_key[PARSE_MAX_DEPTH];
	sc_char			safe_parse_format[PARSE_MAX_DEPTH];
	sc_char			format[PARSE_MAX_DEPTH + 4];

	/* Adjust the stack for parse oddness. */
	memcpy (safe_parse_vt_key, parse_vt_key, sizeof (parse_vt_key));
	memcpy (safe_parse_format, parse_format, sizeof (parse_format));
	parse_adjust_stack ();

	/* Create the format for the property put. */
	format[0] = type; format[1] = '-'; format[2] = '>';
	memcpy (format + 3, parse_format, parse_stack);
	format[parse_stack + 3] = NUL;

	/* Store the property under the stacked keys. */
	assert (parse_bundle != NULL);
	prop_put (parse_bundle, format, vt_value, parse_vt_key);

	/* Unadjust the stack back to how we found it. */
	memcpy (parse_vt_key, safe_parse_vt_key, sizeof (parse_vt_key));
	memcpy (parse_format, safe_parse_format, sizeof (parse_format));
}

static sc_bool
parse_get_property (sc_vartype_t *vt_rvalue, sc_char type)
{
	sc_vartype_t		safe_parse_vt_key[PARSE_MAX_DEPTH];
	sc_char			safe_parse_format[PARSE_MAX_DEPTH];
	sc_char			format[PARSE_MAX_DEPTH + 4];
	sc_bool			status;

	/* Adjust the stack for parse oddness. */
	memcpy (safe_parse_vt_key, parse_vt_key, sizeof (parse_vt_key));
	memcpy (safe_parse_format, parse_format, sizeof (parse_format));
	parse_adjust_stack ();

	/* Create the format for the property get. */
	format[0] = type; format[1] = '<'; format[2] = '-';
	memcpy (format + 3, parse_format, parse_stack);
	format[parse_stack + 3] = NUL;

	/* Retrieve the property using the stacked keys. */
	assert (parse_bundle != NULL);
	status = prop_get (parse_bundle, format, vt_rvalue, parse_vt_key);

	/* Unadjust the stack back to how we found it. */
	memcpy (parse_vt_key, safe_parse_vt_key, sizeof (parse_vt_key));
	memcpy (parse_format, safe_parse_format, sizeof (parse_format));
	return status;
}


/*
 * parse_get_integer_property()
 * parse_get_boolean_property()
 * parse_get_string_property()
 *
 * Convenience forms of parse_get_property(), retrieve directly, and report
 * a fatal error if the property does not exist.
 */
static sc_int32
parse_get_integer_property (void)
{
	sc_vartype_t	vt_rvalue;

	if (!parse_get_property (&vt_rvalue, PROP_INTEGER))
		sc_fatal ("parse_get_integer_property: missing property\n");
	return vt_rvalue.integer;
}

static sc_bool
parse_get_boolean_property (void)
{
	sc_vartype_t	vt_rvalue;

	if (!parse_get_property (&vt_rvalue, PROP_BOOLEAN))
		sc_fatal ("parse_get_boolean_property: missing property\n");
	return vt_rvalue.boolean;
}

static sc_char *
parse_get_string_property (void)
{
	sc_vartype_t	vt_rvalue;

	if (!parse_get_property (&vt_rvalue, PROP_STRING))
		sc_fatal ("parse_get_string_property: missing property\n");
	return vt_rvalue.string;
}


/* Parse error jump buffer. */
static jmp_buf			parse_taf_error;

/* Pushback line, and pushback requested flag. */
static sc_char			*parse_pushback_line	= NULL;
static sc_bool			parse_use_pushback	= FALSE;

/*
 * parse_get_taf_string()
 * parse_get_taf_integer()
 * parse_get_taf_boolean()
 * parse_taf_pushback()
 *
 * Wrapper round obtaining the next TAF file line, with variants to convert
 * the line content into an integer or boolean, and a function for effective
 * TAF line pushback.
 */
static sc_char *
parse_get_taf_string (void)
{
	sc_char		*line;

	/* If pushback requested, use that instead of reading. */
	if (parse_use_pushback)
	    {
		/* Use the pushback line, and clear the request. */
		assert (parse_pushback_line != NULL);
		line = parse_pushback_line;
		parse_use_pushback = FALSE;
	    }
	else
	    {
		/* Get the next line, and complain if absent. */
		line = taf_next_line (parse_taf);
		if (line == NULL)
		    {
			sc_error ("parse_get_taf_string: out of TAF data"
					" at line %ld\n", parse_tafline);
			longjmp (parse_taf_error, 1);
		    }

		/* Note this line for possible pushback. */
		parse_pushback_line = line;
	    }

	/* Print out the line we're parsing if tracing. */
	if (parse_trace)
	    {
		sc_trace ("Parse: read in line %ld : %s\n",
					parse_tafline, line);
	    }

	parse_tafline++;
	return line;
}

static sc_int32
parse_get_taf_integer (void)
{
	sc_char		*line;
	sc_int32	integer;

	/* Get line, and scan for a single integer; return it. */
	line = parse_get_taf_string ();
	if (sscanf (line, "%ld", &integer) != 1)
	    {
		sc_error ("parse_get_taf_integer: invalid integer"
					" at line %ld\n", parse_tafline - 1);
		longjmp (parse_taf_error, 1);
	    }

	return integer;
}

static sc_bool
parse_get_taf_boolean (void)
{
	sc_char		*line;
	sc_bool		boolean;

	/*
	 * Get line, and scan for a single integer; check it's a valid-
	 * looking flag, and return it.
	 */
	line = parse_get_taf_string ();
	if (sscanf (line, "%hu", &boolean) != 1)
	    {
		sc_error ("parse_get_taf_boolean: invalid boolean"
					" at line %ld\n", parse_tafline - 1);
		longjmp (parse_taf_error, 1);
	    }
	if (boolean != 0 && boolean != 1)
	    {
		sc_error ("parse_get_taf_boolean: warning: suspect boolean"
					" at line %ld\n", parse_tafline - 1);
	    }

	return (boolean != 0);
}

static void
parse_taf_pushback (void)
{
	if (parse_use_pushback
			|| parse_pushback_line == NULL)
		sc_fatal ("parse_taf_pushback: too much pushback requested\n");

	/* Set pushback request, and decrement line counter. */
	parse_use_pushback = TRUE;
	parse_tafline--;

	/* Note pushback for tracing purposes. */
	if (parse_trace)
		sc_trace ("Parse: push back at line %ld\n", parse_tafline);
}


/* Enumerations of parse types found in the parse schema. */
enum {	PARSE_INTEGER		= '#',	PARSE_DEFAULT_ZERO	= 'Z',
	PARSE_BOOLEAN		= 'B',
	PARSE_DEFAULT_FALSE	= 'F',	PARSE_DEFAULT_TRUE	= 'T',
	PARSE_STRING		= '$',	PARSE_DEFAULT_EMPTY	= 'E',
	PARSE_MULTILINE		= 'M',
	PARSE_VECTOR		= 'V',	PARSE_VECTOR_ALTERNATE	= 'W',
	PARSE_ARRAY		= '[',
	PARSE_EXPRESSION	= '?',	PARSE_EXPRESSION_NOT	= '!',
	PARSE_GLOBAL_EXPRESSION	= 'G',
	PARSE_CLASS		= '<',	PARSE_FIXUP		= '|',
	PARSE_SPECIAL		= '{',
	PARSE_IGNORE_INTEGER	= 'i',	PARSE_IGNORE_BOOLEAN	= 'b',
	PARSE_IGNORE_STRING	= 's' };

/* Forward declarations of parse functions for recursion. */
static void	parse_element (sc_char *aelement);
static void	parse_class (sc_char *aclass);
static void	parse_descriptor (sc_char *descriptor);

/*
 * parse_array()
 *
 * Parse a descriptor [] array.
 */
static void
parse_array (sc_char *array)
{
	sc_uint32		count, index;
	sc_char			*nextp;
	sc_vartype_t		vt_key;

	if (parse_trace)
		sc_trace ("Parse: entering array %s\n", array);

	/* Find the count of elements in the array. */
	if (sscanf (array, "[%lu]", &count) != 1)
		sc_fatal ("parse_array: bad count in [], %s\n", array);

	/* Advance pointer to characters following []. */
	for (nextp = array + 1; isdigit (*nextp); )
		nextp++;
	nextp++;

	/* Parse the remaining part of the array count times, each a key. */
	for (index = 0; index < count; index++)
	   {
		vt_key.integer = index;
		parse_push_key (vt_key, KEY_INTEGER);

		parse_element (nextp);

		parse_pop_key ();
	   }

	if (parse_trace)
		sc_trace ("Parse: leaving array %s\n", array);
}


/*
 * parse_vector()
 *
 * Parse a variable-length vector of properties.
 */
static void
parse_vector (sc_char *vector, sc_bool is_offset)
{
	sc_uint32		count, index;
	sc_vartype_t		vt_key;

	if (parse_trace)
		sc_trace ("Parse: entering vector %s\n", vector);

	/*
	 * Find the count of elements in the vector, and adjust if this
	 * is a vector described by size - 1.
	 */
	count = parse_get_taf_integer ();
	if (is_offset)
		count++;

	/* Parse the vector property count times, pushing a key on each. */
	for (index = 0; index < count; index++)
	   {
		vt_key.integer = index;
		parse_push_key (vt_key, KEY_INTEGER);

		parse_element (vector + 1);

		parse_pop_key ();
	   }

	if (parse_trace)
		sc_trace ("Parse: leaving vector %s\n", vector);
}


/*
 * parse_test_expression()
 * parse_expression()
 *
 * Parse a conditional field definition, with runtime test.
 */
static sc_bool
parse_test_expression (sc_char *texpr)
{
	sc_vartype_t		vt_key;
	sc_char			plhs[PARSE_TEMP_LENGTH];
	sc_int32		rhs;
	sc_bool			retval = FALSE;

	/* Identify the type of expression to evaluate. */
	switch (texpr[0])
	    {
	    case PARSE_BOOLEAN:
		/* Read boolean property and return its value. */
		vt_key.string = texpr + 1;
		parse_push_key (vt_key, KEY_STRING);
		retval = parse_get_boolean_property ();
		parse_pop_key ();
		break;

	    case PARSE_INTEGER:
		/* Get the left and right sides of = comparison. */
		if (sscanf (texpr, "#%[^=]=%ld", plhs, &rhs) != 2)
		    {
			sc_fatal ("parse_test_expression:"
					" bad = compare, %s\n", texpr + 1);
		    }

		/* Read integer property and return comparison. */
		vt_key.string = plhs;
		parse_push_key (vt_key, KEY_STRING);
		retval = (parse_get_integer_property () == rhs);
		parse_pop_key ();
		break;

	    case PARSE_STRING:
		/* Read property and return TRUE if not an empty string. */
		vt_key.string = texpr + 1;
		parse_push_key (vt_key, KEY_STRING);
		retval = !sc_strempty (parse_get_string_property ());
		parse_pop_key ();
		break;

	    case PARSE_GLOBAL_EXPRESSION:
		{
		sc_vartype_t	vt_gkey[2];

		/* Read the given Global boolean property and return it. */
		vt_gkey[0].string = "Globals";
		vt_gkey[1].string = texpr + 1;
		retval = prop_get_boolean (parse_bundle, "B<-ss", vt_gkey);
		break;
		}

	    default:
		sc_fatal ("parse_test_expression:"
				" bad expression, %s\n", texpr + 1);
	    }

	if (parse_trace)
	    {
		sc_trace ("Parse: expression is %s\n",
					retval ? "TRUE" : "FALSE");
	    }
	return retval;
}

static void
parse_expression (sc_char *expr)
{
	sc_char		texpr[PARSE_TEMP_LENGTH];
	sc_bool		is_present;

	if (parse_trace)
		sc_trace ("Parse: entering expression %s\n", expr);

	/* Isolate the test part of the expression. */
	if (sscanf (expr, "?%[^:]", texpr) != 1)
		sc_fatal ("parse_expression: bad expression, %s\n", expr);

	/* Handle the remainder of the expression only if test passes. */
	is_present = (texpr[0] == PARSE_EXPRESSION_NOT)
					? !parse_test_expression (texpr + 1)
					:  parse_test_expression (texpr);
	if (is_present)
	    {
		sc_char		*nextp;

		/*
		 * Following the ':' may be a single element, or a comma-
		 * separated list.
		 */
		nextp = expr + strlen (texpr) + 2;
		while (*nextp != NUL)
		    {
			sc_char		element[PARSE_TEMP_LENGTH];

			/* Get the next individual element to parse. */
			if (sscanf (nextp, "%[^,]", element) != 1)
			    {
				sc_fatal ("parse_expression:"
						" bad list, %s\n", nextp);
			    }

			/* Parse it, and advance to start of next element. */
			parse_element (element);
			nextp += strlen (element);
			if (*nextp == COMMA)
				nextp++;
		    }
	    }

	if (parse_trace)
		sc_trace ("Parse: leaving expression %s\n", expr);
}


/*
 * parse_read_multiline()
 *
 * Helper for parse_terminal(), reads in a multiline string.  The return
 * string is malloc'ed, and the caller needs to handle that.
 */
static sc_char *
parse_read_multiline (void)
{
	const sc_byte	*separator = NULL;
	sc_char		*line, *mline;

	/* Select the appropriate multiline separator. */
	switch (taf_get_version (parse_taf))
	    {
	    case TAF_VERSION_400:	separator = V400_SEPARATOR;	break;
	    case TAF_VERSION_390:	separator = V390_SEPARATOR;	break;
	    case TAF_VERSION_380:	separator = V380_SEPARATOR;	break;
	    default:
		sc_fatal ("parse_read_multiline: invalid TAF file version\n");
		break;
	    }

	/* Take a simple copy of the first line. */
	line = parse_get_taf_string ();
	mline = sc_malloc (strlen (line) + 1);
	strcpy (mline, line);

	/* Now concatenate until separator found. */
	line = parse_get_taf_string ();
	while (memcmp (line, separator, SEPARATOR_SIZE) != 0)
	    {
		mline = sc_realloc (mline, strlen (mline) + strlen (line) + 2);
		strcat (mline, "\n");
		strcat (mline, line);
		line = parse_get_taf_string ();
	    }

	return mline;
}


/*
 * parse_terminal()
 *
 * Common handler for string, integer, boolean, and multiline parse
 * terminals.
 */
static void
parse_terminal (sc_char *terminal)
{
	sc_vartype_t	vt_key, vt_value;

	if (parse_trace)
		sc_trace ("Parse: entering terminal %s\n", terminal);

	/* Push the key string. */
	vt_key.string = terminal + 1;
	parse_push_key (vt_key, KEY_STRING);

	/* Retrieve, or invent, then store the value. */
	switch (terminal[0])
	    {
	    case PARSE_INTEGER:
		vt_value.integer = parse_get_taf_integer ();
		parse_put_property (vt_value, PROP_INTEGER);
		break;
	    case PARSE_DEFAULT_ZERO:
		vt_value.integer = 0;
		parse_put_property (vt_value, PROP_INTEGER);
		break;

	    case PARSE_BOOLEAN:
		vt_value.boolean = parse_get_taf_boolean ();
		parse_put_property (vt_value, PROP_BOOLEAN);
		break;
	    case PARSE_DEFAULT_FALSE: case PARSE_DEFAULT_TRUE:
		vt_value.boolean = (terminal[0] == PARSE_DEFAULT_TRUE);
		parse_put_property (vt_value, PROP_BOOLEAN);
		break;

	    case PARSE_STRING:
		vt_value.string = parse_get_taf_string ();
		parse_put_property (vt_value, PROP_STRING);
		break;
	    case PARSE_DEFAULT_EMPTY:
		vt_value.string = "";
		parse_put_property (vt_value, PROP_STRING);
		break;

	    case PARSE_MULTILINE:
		vt_value.string = parse_read_multiline ();
		parse_put_property (vt_value, PROP_STRING);

		assert (parse_bundle != NULL);
		prop_adopt (parse_bundle, vt_value.string);
		break;

	    case PARSE_IGNORE_INTEGER:
		(void) parse_get_taf_integer ();
		break;
	    case PARSE_IGNORE_BOOLEAN:
		(void) parse_get_taf_boolean ();
		break;
	    case PARSE_IGNORE_STRING:
		(void) parse_get_taf_string ();
		break;

	    default:
		sc_fatal ("parse_terminal: bad type, %c\n", terminal[0]);
	    }

	/* Pop terminal key. */
	parse_pop_key ();

	if (parse_trace)
		sc_trace ("Parse: leaving terminal %s\n", terminal);
}


/*
 * Resources table.  This table enables resource offsets to be calculated
 * for the various sound and graphic resources encountered on parsing
 * version 4.0 games.  It's unused if the version is not 4.0.
 */
typedef struct {
	sc_char		*name;
	sc_uint32	hash;
	sc_int32	length;
	sc_int32	offset;
} sc_parse_resource_t;

static const sc_uint32		RESOURCE_GROW_INCREMENT	= 32;
static sc_uint32		parse_resources_length	= 0;
static sc_uint32		parse_resources_size	= 0;
static sc_parse_resource_t	*parse_resources	= NULL;


/*
 * parse_clear_v400_resources_table()
 *
 * Free and clear down the version 4.0 resources table.
 */
static void
parse_clear_v400_resources_table (void)
{
	/* Free allocated memory and return to initial values. */
	if (parse_resources != NULL)
	    {
		sc_uint32	index;

		for (index = 0; index < parse_resources_length; index++)
			sc_free (parse_resources[index].name);

		sc_free (parse_resources);
		parse_resources = NULL;
	    }
	parse_resources_length = 0;
	parse_resources_size   = 0;
}


/*
 * parse_get_v400_resource_offset()
 *
 * Notes version 4.0 resource names encountered in the parse, and their
 * lengths, and builds up a list of resources with their data offsets.  The
 * function assumes that resources are appended to the TAF file in the
 * order in which they are encountered when reading through the TAF file.
 *
 * A warning -- this function may return a new length.  Resources that
 * have been seen once already have non-useful (though apparently non-zero)
 * lengths; this function needs to handle that.  The caller needs to compare
 * length with real_length to see if that happened.
 */
static sc_int32
parse_get_v400_resource_offset (sc_char *name,
		sc_int32 length, sc_int32 *real_length)
{
	sc_char		*clean_name;
	sc_uint32	hash, index;
	sc_int32	offset;

	/*
	 * Take a copy of the name, and remove any trailing "##" looping
	 * sound indicator flag.  Who thinks this junk up?
	 */
	clean_name = sc_malloc (strlen (name) + 1);
	strcpy (clean_name, name);
	if (!strcmp (clean_name + strlen (clean_name) - 2, "##"))
		clean_name[strlen (clean_name) - 2] = NUL;

	/*
	 * Scan the current resources list for a matching name, and if
	 * the resource is already known, return its offset.  The hash check
	 * is an attempt to improve the search times, relative to using only
	 * string comparisons -- the table's not fully hashed.  If found,
	 * we need to also pass back the corrected length.
	 */
	offset = -1;
	hash = sc_hash (clean_name);
	for (index = 0; index < parse_resources_length; index++)
	    {
		if (parse_resources[index].hash == hash
			&& !strcmp (parse_resources[index].name, clean_name))
		    {
			offset = parse_resources[index].offset;
			break;
		    }
	    }
	if (offset != -1)
	    {
		*real_length = parse_resources[index].length;
		sc_free (clean_name);
		return offset;
	    }

	/* Resize the resources table if required. */
	if (parse_resources_length == parse_resources_size)
	    {
		parse_resources_size += RESOURCE_GROW_INCREMENT;
		parse_resources = sc_realloc (parse_resources,
			parse_resources_size * sizeof (parse_resources[0]));
	    }

	/*
	 * Calculate the offset.  For the first resource, it's zero; for
	 * others, it's one after the prior entry's offset and length.
	 */
	if (parse_resources_length == 0)
		offset = 0;
	else
	    {
		offset = parse_resources[parse_resources_length - 1].offset
			+ parse_resources[parse_resources_length - 1].length
			+ 1;
	    }

	/* Add details to the table. */
	parse_resources[parse_resources_length].name   = clean_name;
	parse_resources[parse_resources_length].hash   = hash;
	parse_resources[parse_resources_length].offset = offset;
	parse_resources[parse_resources_length].length = length;
	parse_resources_length++;

	*real_length = length;
	return offset;
}


/*
 * parse_handle_v400_resources()
 *
 * Extra special handling for version 4.0 resources; extracts details of
 * the resource just parsed, and adds an offset property for each defined.
 *
 * A warning -- Adrift seems to use -ve numbers as lengths for resources
 * already parsed, where TAF files include the resource.  It's unclear
 * what the -ve values mean, so here we ignore them and work off the
 * resource file name given.  This means we have to look for length not
 * equal to zero, not just lengths greater than zero.
 *
 * TODO Work out what this means.  The -ve lengths look like a form of
 * 'resource number'; -(length+2) is tantalizingly close to the index into
 * our parse_resources table, but not always...
 */
static void
parse_handle_v400_resources (sc_bool has_sound, sc_bool has_graphics)
{
	sc_vartype_t	vt_key, vt_value;
	sc_char		*file;
	sc_int32	length, offset;

	/*
	 * Retrieve the file and length for the sound just parsed.  If
	 * there's a file of non-zero length, rewrite its offset.
	 */
	if (has_sound)
	    {
		/* Retrieve the file and length information. */
		vt_key.string = "SoundFile";
		parse_push_key (vt_key, KEY_STRING);
		file = parse_get_string_property ();
		parse_pop_key ();

		vt_key.string = "SoundLen";
		parse_push_key (vt_key, KEY_STRING);
		length = parse_get_integer_property ();
		parse_pop_key ();

		/*
		 * If defined and has a length, rewrite the offset, and
		 * also the length in case changed.
		 */
		if (!sc_strempty (file) && length != 0)
		    {
			sc_int32	real_length;

			offset = parse_get_v400_resource_offset
						(file, length, &real_length);
			vt_key.string = "SoundOffset";
			parse_push_key (vt_key, KEY_STRING);

			vt_value.integer = offset;
			parse_put_property (vt_value, PROP_INTEGER);

			parse_pop_key ();

			/* Rewrite length if changed. */
			if (real_length != length)
			    {
				vt_key.string = "SoundLen";
				parse_push_key (vt_key, KEY_STRING);

				vt_value.integer = real_length;
				parse_put_property (vt_value, PROP_INTEGER);

				parse_pop_key ();
			    }
		    }
	    }

	/* Now do the same thing for graphics. */
	if (has_graphics)
	    {
		/* Retrieve the file and length information. */
		vt_key.string = "GraphicFile";
		parse_push_key (vt_key, KEY_STRING);
		file = parse_get_string_property ();
		parse_pop_key ();

		vt_key.string = "GraphicLen";
		parse_push_key (vt_key, KEY_STRING);
		length = parse_get_integer_property ();
		parse_pop_key ();

		/*
		 * If defined and has a length, rewrite the offset, and
		 * also the length in case changed.
		 */
		if (!sc_strempty (file) && length != 0)
		    {
			sc_int32	real_length;

			offset = parse_get_v400_resource_offset
						(file, length, &real_length);
			vt_key.string = "GraphicOffset";
			parse_push_key (vt_key, KEY_STRING);

			vt_value.integer = offset;
			parse_put_property (vt_value, PROP_INTEGER);

			parse_pop_key ();

			/* Rewrite length if changed. */
			if (real_length != length)
			    {
				vt_key.string = "GraphicLen";
				parse_push_key (vt_key, KEY_STRING);

				vt_value.integer = real_length;
				parse_put_property (vt_value, PROP_INTEGER);

				parse_pop_key ();
			    }
		    }
	    }
}


/*
 * parse_special()
 *
 * Handler for special items that can't be described accurately, and
 * therefore need careful treatment.
 */
static void
parse_special (sc_char *special)
{
	if (parse_trace)
		sc_trace ("Parse: entering special %s\n", special);

	/* Special handling for version 4.0 resources. */
	if (!strcmp (special, "{V400_RESOURCE}"))
	    {
		sc_vartype_t	vt_key[2];
		sc_bool		has_sound, has_graphics;

		/* Get sound and graphics global flags. */
		vt_key[0].string = "Globals";
		vt_key[1].string = "Sound";
		has_sound = prop_get_boolean (parse_bundle, "B<-ss", vt_key);

		vt_key[1].string = "Graphics";
		has_graphics = prop_get_boolean (parse_bundle, "B<-ss", vt_key);

		/* Apply special handling to the resources. */
		parse_handle_v400_resources (has_sound, has_graphics);
	    }

	/* Parse a version 4.0 optional set of room exit information. */
	else if (!strcmp (special, "{V400_ROOM_EXIT:#Dest_#Var1_#Var2_#Var3}"))
	    {
		sc_int32		flag;

		/* Get next flag, and if true, pushback and parse. */
		flag = parse_get_taf_integer ();
		if (flag != 0)
		    {
			parse_taf_pushback ();
			parse_descriptor ("#Dest #Var1 #Var2 #Var3");
		    }
	    }

	/* Parse version 3.9 and version 3.8 optional room exit information. */
	else if (!strcmp (special,
			"{V390_V380_ROOM_EXIT:#Dest_#Var1_#Var2_ZVar3}"))
	    {
		sc_int32		flag;

		/* Get next flag, and if true, pushback and parse. */
		flag = parse_get_taf_integer ();
		if (flag != 0)
		    {
			parse_taf_pushback ();
			parse_descriptor ("#Dest #Var1 #Var2 ZVar3");
		    }
	    }

	/* Parse room lists, with optional extra room. */
	else if (!strcmp (special, "{ROOM_LIST0}")
			|| !strcmp (special, "{ROOM_LIST1}"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	room_count, num_rooms, type, index;

		/* Retrieve the room list type. */
		vt_key.string = "Type";
		parse_push_key (vt_key, KEY_STRING);
		type = parse_get_integer_property ();
		parse_pop_key ();

		/* Write remaining room list depending on the type. */
		switch (type)
		    {
		    case ROOMLIST_NO_ROOMS:
		    case ROOMLIST_ALL_ROOMS:
		    case ROOMLIST_NPC_PART:
			break;

		    case ROOMLIST_ONE_ROOM:
			/* Store this room as the single list entry. */
			parse_element ("#Room");
			break;

		    case ROOMLIST_SOME_ROOMS:
			/* Get count of rooms defined, add one if necessary. */
			vt_key.string = "Rooms";
			room_count = prop_get_integer
						(parse_bundle, "I<-s", &vt_key);

			if (!strcmp (special, "{ROOM_LIST1}"))
				num_rooms = room_count + 1;
			else
				num_rooms = room_count;

			/* Store an array of rooms flags for each room. */
			vt_key.string = "Rooms";
			parse_push_key (vt_key, KEY_STRING);
			for (index = 0; index < num_rooms; index++)
			    {
				sc_bool		this_room;

				/* Get flag for this room. */
				this_room = parse_get_taf_boolean ();

				/* Store flag directly. */
				vt_key.integer = index;
				parse_push_key (vt_key, KEY_INTEGER);
				vt_value.boolean = this_room;
				parse_put_property (vt_value, PROP_BOOLEAN);
				parse_pop_key ();
			    }
			parse_pop_key ();
			break;

		    default:
			sc_fatal ("parse_special: bad type, %ld\n", type);
		    }
	    }

	/* Parse Parent number iff this object is an NPC part. */
	else if (!strcmp (special, "{OBJECT:#Parent}"))
	    {
		sc_vartype_t	vt_key;
		sc_int32	type;

		/* Check object's Where room list Type for NPC part. */
		vt_key.string = "Where";
		parse_push_key (vt_key, KEY_STRING);
		vt_key.string = "Type";
		parse_push_key (vt_key, KEY_STRING);
		type = parse_get_integer_property ();
		parse_pop_key ();
		parse_pop_key ();

		/* Get Parent if the object is part of an NPC. */
		if (type == ROOMLIST_NPC_PART)
			parse_element ("#Parent");
	    }

	/* Parse a list of rooms and times for a walk. */
	else if (!strcmp (special, "{WALK:#Rooms_#Times}"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	num_stops, index;

		/* Obtain the count of stops in this walk. */
		vt_key.string = "NumStops";
		parse_push_key (vt_key, KEY_STRING);
		num_stops = parse_get_integer_property ();
		parse_pop_key ();

		/* Look for a room and time for each stop. */
		for (index = 0; index < num_stops; index++)
		    {
			sc_int32	room, time;

			/* Parse and store Rooms[index]. */
			vt_key.string = "Rooms";
			parse_push_key (vt_key, KEY_STRING);
			vt_key.integer = index;
			parse_push_key (vt_key, KEY_INTEGER);

			room = parse_get_taf_integer ();

			vt_value.integer = room;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
			parse_pop_key ();

			/* Parse and store Times[index]. */
			vt_key.string = "Times";
			parse_push_key (vt_key, KEY_STRING);
			vt_key.integer = index;
			parse_push_key (vt_key, KEY_INTEGER);

			time = parse_get_taf_integer ();

			vt_value.integer = time;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
			parse_pop_key ();
		    }
	    }

	/* Parse a room group variable size boolean list. */
	else if (!strcmp (special, "{ROOM_GROUP:[]BList}"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	num_rooms, index, l2index;
		sc_bool		in_group;

		/* Get the count of rooms defined. */
		vt_key.string = "Rooms";
		num_rooms = prop_get_integer (parse_bundle, "I<-s", &vt_key);

		/* Read a boolean for each room. */
		l2index = 0;
		for (index = 0; index < num_rooms; index++)
		    {
			in_group = parse_get_taf_boolean ();

			/* Store raw flag as List[index]. */
			vt_key.string = "List";
			parse_push_key (vt_key, KEY_STRING);
			vt_key.integer = index;
			parse_push_key (vt_key, KEY_INTEGER);
			vt_value.boolean = in_group;
			parse_put_property (vt_value, PROP_BOOLEAN);

			parse_pop_key ();
			parse_pop_key ();

			/* Store in-group index'es as List2[0..n]. */
			if (in_group)
			    {
				vt_key.string = "List2";
				parse_push_key (vt_key, KEY_STRING);
				vt_key.integer = l2index;
				parse_push_key (vt_key, KEY_INTEGER);
				vt_value.integer = index;
				parse_put_property (vt_value, PROP_INTEGER);

				parse_pop_key ();
				parse_pop_key ();

				l2index++;
			    }
		    }
	    }

	/* Error if no special handler available. */
	else
	    {
		sc_fatal ("parse_special: no handler for \"%s\"\n", special);
	    }

	if (parse_trace)
		sc_trace ("Parse: leaving special %s\n", special);
}


/*
 * parse_fixup_v390_v380_room_alt()
 *
 * Helper for parse_fixup_v390_v380_room_alts().  Handles creation of
 * version 4.0 room alts for version 3.9 and version 3.8 games.
 */
static void
parse_fixup_v390_v380_room_alt
		(sc_char *m1, sc_int32 type, sc_char *resource1,
		sc_char *m2, sc_int32 var2, sc_char *resource2,
		sc_int32 hide_objects, sc_char *changed,
		sc_int32 var3, sc_int32 display_room)
{
	sc_vartype_t	vt_key, vt_value, vt_rvalue, vt_gkey[2];
	sc_bool		has_sound, has_graphics;
	sc_int32	alt_count;
	sc_char		*soundfile1, *graphicfile1;
	sc_char		*soundfile2, *graphicfile2;

	/*
	 * Initialize resource files to empty, for cases where no resource
	 * is copied over from the main room (NULL resource1/2).
	 */
	soundfile1 = ""; graphicfile1 = "";
	soundfile2 = ""; graphicfile2 = "";

	/* Get sound and graphics flags, always FALSE for version 3.8. */
	vt_gkey[0].string = "Globals";
	vt_gkey[1].string = "Sound";
	has_sound = prop_get_boolean (parse_bundle, "B<-ss", vt_gkey);

	vt_gkey[1].string = "Graphics";
	has_graphics = prop_get_boolean (parse_bundle, "B<-ss", vt_gkey);

	/* Get a count of alts so far defined for the room. */
	vt_key.string = "Alts";
	parse_push_key (vt_key, KEY_STRING);
	alt_count = (parse_get_property (&vt_rvalue, PROP_INTEGER))
					? vt_rvalue.integer : 0;
	parse_pop_key ();

	/*
	 * Lookup any resource details now, and save them.  Because this is
	 * not version 4.0, we can ignore lengths, and set them to zero when
	 * needed.
	 */
	if (has_sound || has_graphics)
	    {
		if (resource1 != NULL)
		    {
			vt_key.string = resource1;
			parse_push_key (vt_key, KEY_STRING);
			if (has_sound)
			    {
				vt_key.string = "SoundFile";
				parse_push_key (vt_key, KEY_STRING);
				soundfile1 = parse_get_string_property ();
				parse_pop_key ();
			    }
			if (has_graphics)
			    {
				vt_key.string = "GraphicFile";
				parse_push_key (vt_key, KEY_STRING);
				graphicfile1 = parse_get_string_property ();
				parse_pop_key ();
			    }
			parse_pop_key ();
		    }

		if (resource2 != NULL)
		    {
			vt_key.string = resource2;
			parse_push_key (vt_key, KEY_STRING);
			if (has_sound)
			    {
				vt_key.string = "SoundFile";
				parse_push_key (vt_key, KEY_STRING);
				soundfile2 = parse_get_string_property ();
				parse_pop_key ();
			    }
			if (has_graphics)
			    {
				vt_key.string = "GraphicFile";
				parse_push_key (vt_key, KEY_STRING);
				graphicfile2 = parse_get_string_property ();
				parse_pop_key ();
			    }
			parse_pop_key ();
		    }
	    }

	/*
	 * Create a room alt to match data passed in.  Start with the Alts
	 * string and the index to the alt being written.  To correctly
	 * emulate the parse, we also have to reverse the "Alts" and the
	 * index, as parse_put_property() will swap them.  Madness.
	 */
	vt_key.integer = alt_count;
	parse_push_key (vt_key, KEY_INTEGER);
	vt_key.string = "Alts";
	parse_push_key (vt_key, KEY_STRING);

	/* Write M1 and Type. */
	vt_key.string = "M1";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.string = m1;
	parse_put_property (vt_value, PROP_STRING);
	parse_pop_key ();
	vt_key.string = "Type";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = type;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	/* If resources, add these as retrieved above. */
	if (has_sound || has_graphics)
	    {
		vt_key.string = "Res1";
		parse_push_key (vt_key, KEY_STRING);
		if (has_sound)
		    {
			vt_key.string = "SoundFile";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.string = soundfile1;
			parse_put_property (vt_value, PROP_STRING);
			parse_pop_key ();
			vt_key.string = "SoundLen";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.integer = 0;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
		if (has_graphics)
		    {
			vt_key.string = "GraphicFile";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.string = graphicfile1;
			parse_put_property (vt_value, PROP_STRING);
			parse_pop_key ();
			vt_key.string = "GraphicLen";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.integer = 0;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
		parse_pop_key ();
	    }

	/* Write M2 and Var2. */
	vt_key.string = "M2";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.string = m2;
	parse_put_property (vt_value, PROP_STRING);
	parse_pop_key ();
	vt_key.string = "Var2";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = var2;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	/* If resources, again add these as retrieved above. */
	if (has_sound || has_graphics)
	    {
		vt_key.string = "Res2";
		parse_push_key (vt_key, KEY_STRING);
		if (has_sound)
		    {
			vt_key.string = "SoundFile";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.string = soundfile2;
			parse_put_property (vt_value, PROP_STRING);
			parse_pop_key ();
			vt_key.string = "SoundLen";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.integer = 0;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
		if (has_graphics)
		    {
			vt_key.string = "GraphicFile";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.string = graphicfile2;
			parse_put_property (vt_value, PROP_STRING);
			parse_pop_key ();
			vt_key.string = "GraphicLen";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.integer = 0;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
		parse_pop_key ();
	    }

	/* Finish off with the last four alt properties. */
	vt_key.string = "HideObjects";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = hide_objects;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();
	vt_key.string = "Changed";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.string = changed;
	parse_put_property (vt_value, PROP_STRING);
	parse_pop_key ();
	vt_key.string = "Var3";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = var3;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();
	vt_key.string = "DisplayRoom";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = display_room;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	parse_pop_key ();
	parse_pop_key ();
}


/* Multiplier for combination AltDesc Type and HideObject values. */
enum {			V390_V380_ALT_TYPEHIDE_MULT	= 10 };

/*
 * parse_fixup_v390_v380_room_alts()
 *
 * Common helper function for parse_fixup_v390() and parse_fixup_v380(),
 * converts version 3.9 and version 3.8 fixed room description alts into
 * an equivalent array of version 4.0 style room alts.
 */
static void
parse_fixup_v390_v380_room_alts (void)
{
	sc_vartype_t	vt_key;
	sc_char		*m1, *m2, *changed;
	sc_int32	type, var2, hide_objects, var3, display_room;

	/* Room alt invariants. */
	m2		= "";			/* No else text */
	changed		= "";			/* No changed room name */

	/*
	 * Create a room alt to override all others, controlled by an
	 * object condition and with optional object hiding.
	 */
	type		= 2;			/* Object condition */
	display_room	= 0;			/* Override all others */

	vt_key.string = "Obj";
	parse_push_key (vt_key, KEY_STRING);
	var3 = parse_get_integer_property ();
	parse_pop_key ();

	if (var3 > 0)
	    {
		sc_int32	typehideobjects;

		vt_key.string = "AltDesc";
		parse_push_key (vt_key, KEY_STRING);
		m1 = parse_get_string_property ();
		parse_pop_key ();

		vt_key.string = "TypeHideObjects";
		parse_push_key (vt_key, KEY_STRING);
		typehideobjects = parse_get_integer_property ();
		parse_pop_key ();

		var2		= typehideobjects / V390_V380_ALT_TYPEHIDE_MULT;
		hide_objects	= typehideobjects % V390_V380_ALT_TYPEHIDE_MULT;

		parse_fixup_v390_v380_room_alt (m1, type, "AltRes",
						m2, var2, NULL,
						hide_objects, changed, var3,
						display_room);
	    }

	/*
	 * If a second task alternate description is defined, create a room
	 * alt to add after the main description, one that stops printing
	 * once done.
	 */
	type		= 0;			/* Task condition */
	display_room	= 1;			/* Print after main and stop */

	vt_key.string = "Task2";
	parse_push_key (vt_key, KEY_STRING);
	var2 = parse_get_integer_property ();
	parse_pop_key ();

	if (var2 > 0)
	    {
		vt_key.string = "AddDesc2";
		parse_push_key (vt_key, KEY_STRING);
		m1 = parse_get_string_property ();
		parse_pop_key ();

		var3		= 0;
		hide_objects	= 0;

		parse_fixup_v390_v380_room_alt (m1, type, "Task2Res",
						m2, var2, NULL,
						hide_objects, changed, var3,
						display_room);
	    }

	/* Do the same for any first task additional description. */
	type		= 0;			/* Task condition */
	display_room	= 1;			/* Print after main and stop */

	vt_key.string = "Task1";
	parse_push_key (vt_key, KEY_STRING);
	var2 = parse_get_integer_property ();
	parse_pop_key ();

	if (var2 > 0)
	    {
		vt_key.string = "AddDesc1";
		parse_push_key (vt_key, KEY_STRING);
		m1 = parse_get_string_property ();
		parse_pop_key ();

		var3		= 0;
		hide_objects	= 0;

		parse_fixup_v390_v380_room_alt (m1, type, "Task1Res",
						m2, var2, NULL,
						hide_objects, changed, var3,
						display_room);
	    }

	/*
	 * If still printing at this point, we need a catch-all room alt
	 * that will print.  So create one with an always true condition.
	 */
	type		= 0;			/* Task condition */
	display_room	= 2;			/* Lowest priority output */

	vt_key.string = "LastDesc";
	parse_push_key (vt_key, KEY_STRING);
	m1 = parse_get_string_property ();
	parse_pop_key ();

	if (!sc_strempty (m1))
	    {
		var2		= 0;		/* No task - always TRUE */
		var3		= 0;
		hide_objects	= 0;

		parse_fixup_v390_v380_room_alt (m1, type, "LastRes",
						m2, var2, NULL,
						hide_objects, changed, var3,
						display_room);
	    }
}


/*
 * parse_fixup_v390()
 *
 * Handler for fixup special items to help with conversions from TAF version
 * 3.9 format into version 4.0.
 */
static void
parse_fixup_v390 (sc_char *fixup)
{
	if (parse_trace)
		sc_trace ("Parse: entering version 3.9 fixup %s\n", fixup);

	/* Fixup a version 3.9 task action by incrementing Type > 4. */
	if (!strcmp (fixup, "|V390_TASK_ACTION:Type>4?#Type++|"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	type;

		/* Retrieve Type, and if > 4, increment. */
		vt_key.string = "Type";
		parse_push_key (vt_key, KEY_STRING);
		type = parse_get_integer_property ();

		if (type > 4)
		    {
			vt_value.integer = type + 1;
			parse_put_property (vt_value, PROP_INTEGER);
		    }

		parse_pop_key ();
	    }

	/* Handle either Expr or Var5 for version 3.9 task actions. */
	else if (!strcmp (fixup, "|V390_TASK_ACTION:$Expr_#Var5|"))
	    {
		sc_vartype_t	vt_key;
		sc_int32	var2;

		/* Either Expr or Var5, depending on Var2. */
		vt_key.string = "Var2";
		parse_push_key (vt_key, KEY_STRING);
		var2 = parse_get_integer_property ();
		parse_pop_key ();

		if (var2 == 5)
			parse_descriptor ("$Expr ZVar5");
		else
			parse_descriptor ("EExpr #Var5");
	    }

	/*
	 * Exchange openable values 5 and 6, and write -1 key for openable
	 * objects.
	 */
	else if (!strcmp (fixup, "|V390_OBJECT:_Openable_,Key|"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	openable;

		/* Retrieve Openable, and if 5 or 6, exchange. */
		vt_key.string = "Openable";
		parse_push_key (vt_key, KEY_STRING);
		openable = parse_get_integer_property ();

		if (openable == 5 || openable == 6)
		    {
			vt_value.integer = (openable == 5) ? 6 : 5;
			parse_put_property (vt_value, PROP_INTEGER);
		    }

		parse_pop_key ();

		/* For openable objects, store a Key of -1. */
		if (openable == 5 || openable == 6)
		    {
			vt_key.string = "Key";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.integer = -1;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
	    }

	/* Create a RestrMask that 'and's all the restrictions together. */
	else if (!strcmp (fixup, "|V390_TASK:$RestrMask|"))
	    {
		sc_vartype_t	vt_key, vt_value, vt_rvalue;
		sc_int32	restriction_count;

		/* Get a count of restrictions. */
		vt_key.string = "Restrictions";
		parse_push_key (vt_key, KEY_STRING);
		restriction_count = (parse_get_property
						(&vt_rvalue, PROP_INTEGER))
						? vt_rvalue.integer : 0;
		parse_pop_key ();

		/* Allocate and fill a new mask for these restrictions. */
		if (restriction_count > 0)
		    {
			sc_char		*restrmask;
			sc_int32	index;

			restrmask = sc_malloc (2 * restriction_count);
			strcpy (restrmask, "#");
			for (index = 1; index < restriction_count; index++)
				strcat (restrmask, "A#");

			vt_key.string = "RestrMask";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.string = restrmask;
			parse_put_property (vt_value, PROP_STRING);
			parse_pop_key ();

			prop_adopt (parse_bundle, restrmask);
		    }
	    }

	/*
	 * Increment var1 for variable restrictions to compensate for there
	 * being no referenced text comparison (no string variables).
	 */
	else if (!strcmp (fixup, "|V390_TASK_RESTR:Var1>0?#Var1++|"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	var1;

		/* Retrieve Var1, and if greater than zero, increment. */
		vt_key.string = "Var1";
		parse_push_key (vt_key, KEY_STRING);
		var1 = parse_get_integer_property ();

		if (var1 > 0)
		    {
			vt_value.integer = var1 + 1;
			parse_put_property (vt_value, PROP_INTEGER);
		    }

		parse_pop_key ();
	    }

	/* Convert version 3.9 fixed alts into a version 4.0 array. */
	else if (!strcmp (fixup, "|V390_ROOM:_Alts_|"))
	    {
		parse_fixup_v390_v380_room_alts ();
	    }

	/* Error if no fixup special handler available. */
	else
	    {
		sc_fatal ("parse_fixup_v390: no handler for \"%s\"\n", fixup);
	    }

	if (parse_trace)
		sc_trace ("Parse: leaving version 3.9 fixup %s\n", fixup);
}


/*
 * Object surface and container masks for version 3.8 object fixup, container
 * container capacity conversion factor and default object sizing, and the
 * count of task movements in a version 3.8 task.
 */
enum {			V380_OBJ_IS_SURFACE	= 2,
			V380_OBJ_IS_CONTAINER	= 1 };
enum {			V380_OBJ_CAPACITY_MULT	= 10,
			V380_OBJ_DEFAULT_SIZE	= 2 };
enum {			V380_TASK_MOVEMENTS	= 6 };

/*
 * parse_fixup_v380_action()
 *
 * Helper for parse_fixup_v380(), adds a task action.
 */
static void
parse_fixup_v380_action (sc_int32 type, sc_uint16 var_count,
		sc_int32 var1, sc_int32 var2, sc_int32 var3)
{
	sc_vartype_t	vt_key, vt_value, vt_rvalue;
	sc_int32	action_count;

	/* Get a count of actions so far defined for the task. */
	vt_key.string = "Actions";
	parse_push_key (vt_key, KEY_STRING);
	action_count = (parse_get_property (&vt_rvalue, PROP_INTEGER))
					? vt_rvalue.integer : 0;
	parse_pop_key ();

	/* Write actions key, reversed to emulate parse actions. */
	vt_key.integer = action_count;
	parse_push_key (vt_key, KEY_INTEGER);
	vt_key.string = "Actions";
	parse_push_key (vt_key, KEY_STRING);

	/* Write new action according to the given arguments. */
	vt_key.string = "Type";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = type;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	vt_key.string = "Var1";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = var1;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	if (var_count > 1)
	    {
		vt_key.string = "Var2";
		parse_push_key (vt_key, KEY_STRING);
		vt_value.integer = var2;
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();
	    }

	if (var_count > 2)
	    {
		vt_key.string = "Var3";
		parse_push_key (vt_key, KEY_STRING);
		vt_value.integer = var3;
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();
	    }

	parse_pop_key ();
	parse_pop_key ();
}


/*
 * parse_fixup_v380_movement()
 *
 * Helper for parse_fixup_v380(), converts a task movement into an action.
 */
static void
parse_fixup_v380_movement (sc_int32 mvar1, sc_int32 mvar2, sc_int32 mvar3)
{
	sc_int32	var1;

	/* If nothing was selected to move, ignore the call. */
	if (mvar1 == 0)
		return;

	/*
	 * Accept only player moves into rooms.  Other combinations, such
	 * as move player to worn by player, are unlikely.  And move player
	 * to same room as player isn't useful.
	 */
	if (mvar1 == 1)
	    {
		if (mvar3 == 0 && mvar2 >= 2)
			parse_fixup_v380_action (1, 3, 0, 0, mvar2 - 2);
		return;
	    }

	/*
	 * Convert movement var1 into action var1.  Var1 is the dynamic
	 * object + 3, or 2 for referenced object, or 0 for all held.
	 */
	switch (mvar1)
	    {
	    case 2:	var1 = 2;		break;	/* Referenced obj */
	    case 3:	var1 = 0;		break;	/* All held */
	    default:	var1 = mvar1 - 1;	break;	/* Dynamic obj */
	    }

	/* Dissect the rest of the movement. */
	switch (mvar3)
	    {
	    case 0:					/* To room */
		/*
		 * Convert movement var2 into action var2 and var3.  Var2 is
		 * 0 for move to room, 6 for move to player room.  Var3 is
		 * 0 for hidden, otherwise the room number plus one.
		 */
		if (mvar2 == 0)				/* Hidden */
			parse_fixup_v380_action (0, 3, var1, 0, 0);
		else if (mvar2 == 1)			/* Player room */
			parse_fixup_v380_action (0, 3, var1, 6, 0);
		else					/* Specified room */
			parse_fixup_v380_action (0, 3, var1, 0, mvar2 - 1);
		break;

	    case 1:					/* To inside */
	    case 2:					/* To onto */
		/*
		 * Convert movement var2 and var3 into action var3 and var2,
		 * a simple conversion, but check that var2 is not 'not
		 * selected' first.
		 */
		if (mvar2 > 0)
			parse_fixup_v380_action (0, 3, var1,
						mvar3 + 1, mvar2 - 1);
		break;

	    case 3:					/* To held by */
	    case 4:					/* To worn by */
		/*
		 * Convert movement var2 and var3 into action var3 and var2,
		 * in this case a simple conversion, since version 4.0 task
		 * actions are close here.
		 */
		parse_fixup_v380_action (0, 3, var1, mvar3 + 1, mvar2);
		break;

	    default:
		sc_fatal ("parse_fixup_v380_movement:"
					" invalid mvar3, %ld\n", mvar3);
	    }
}


/*
 * parse_fixup_v380_restr()
 *
 * Helper for parse_fixup_v380(), adds a task restriction.
 */
static void
parse_fixup_v380_restr (sc_int32 type, sc_uint16 var_count,
		sc_int32 var1, sc_int32 var2, sc_int32 var3,
		sc_char *failmessage)
{
	sc_vartype_t	vt_key, vt_value, vt_rvalue;
	sc_int32	restriction_count;

	/* Get a count of restrictions so far defined for the task. */
	vt_key.string = "Restrictions";
	parse_push_key (vt_key, KEY_STRING);
	restriction_count = (parse_get_property (&vt_rvalue, PROP_INTEGER))
					? vt_rvalue.integer : 0;
	parse_pop_key ();

	/* Write restrictions key, reversed to emulate parse actions. */
	vt_key.integer = restriction_count;
	parse_push_key (vt_key, KEY_INTEGER);
	vt_key.string = "Restrictions";
	parse_push_key (vt_key, KEY_STRING);

	/* Write new restriction according to the given arguments. */
	vt_key.string = "Type";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = type;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	vt_key.string = "Var1";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.integer = var1;
	parse_put_property (vt_value, PROP_INTEGER);
	parse_pop_key ();

	if (var_count > 1)
	    {
		vt_key.string = "Var2";
		parse_push_key (vt_key, KEY_STRING);
		vt_value.integer = var2;
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();
	    }

	if (var_count > 2)
	    {
		vt_key.string = "Var3";
		parse_push_key (vt_key, KEY_STRING);
		vt_value.integer = var3;
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();
	    }

	vt_key.string = "FailMessage";
	parse_push_key (vt_key, KEY_STRING);
	vt_value.string = failmessage;
	parse_put_property (vt_value, PROP_STRING);
	parse_pop_key ();

	parse_pop_key ();
	parse_pop_key ();
}


/*
 * parse_fixup_v380_obj_restr()
 * parse_fixup_v380_task_restr()
 * parse_fixup_v380_wear_restr()
 * parse_fixup_v380_npc_restr()
 * parse_fixup_v380_objroom_restr()
 * parse_fixup_v380_objstate_restr()
 *
 * Helper handlers for parse_fixup_v380(); create task restrictions.
 */
static void
parse_fixup_v380_obj_restr (sc_bool holding, sc_int32 holdobj,
		sc_char *failmessage)
{
	/* Ignore if no object selected. */
	if (holdobj > 0)
	    {
		sc_int32	var1, var2;

		/*
		 * Create version 4.0 task restriction to check for either
		 * the referenced object or a dynamic object being either
		 * held or in the same room (visible to player).
		 */
		var1 = (holdobj == 1) ? 2 : holdobj + 1;
		var2 = holding ? 1 : 3;
		parse_fixup_v380_restr (0, 3, var1, var2, 0, failmessage);
	    }
}

static void
parse_fixup_v380_task_restr (sc_bool tasknotdone, sc_int32 task,
		sc_char *failmessage)
{
	/* Ignore if no task selected. */
	if (task > 0)
	    {
		sc_int32	var2;

		/* Create version 4.0 restriction to check task state. */
		var2 = tasknotdone ? 1 : 0;
		parse_fixup_v380_restr (2, 2, task, var2, 0, failmessage);
	    }
}

static void
parse_fixup_v380_wear_restr (sc_int32 wearobj, sc_char *failmessage)
{
	/* Ignore if no object selected. */
	if (wearobj > 0)
	    {
		sc_vartype_t	vt_key[3], vt_rvalue;
		sc_int32	object_count, object, dynamic;

		/*
		 * Create version 4.0 restrictions for something or nothing
		 * worn by player.
		 */
		if (wearobj == 1)
		    {
			parse_fixup_v380_restr (0, 3, 1, 2, 0, failmessage);
			return;
		    }
		else if (wearobj == 2)
		    {
			parse_fixup_v380_restr (0, 3, 0, 2, 0, failmessage);
			return;
		    }

		/* Get the count of objects defined. */
		vt_key[0].string = "Objects";
		object_count = prop_get
				(parse_bundle, "I<-s", &vt_rvalue, vt_key)
					? vt_rvalue.integer : 0;

		/* Convert wearobj from worn index to dynamic index. */
		wearobj -= 2;
		dynamic = 0;
		for (object = 0;
			object < object_count && wearobj >= 0; object++)
		    {
			sc_bool		bstatic, wearable;

			vt_key[1].integer = object;
			vt_key[2].string = "Static";
			bstatic = prop_get_boolean
					(parse_bundle, "B<-sis", vt_key);
			if (!bstatic)
			    {
				dynamic++;

				vt_key[2].string = "Wearable";
				wearable = prop_get_boolean
					(parse_bundle, "B<-sis", vt_key);
				if (wearable)
					wearobj--;
			    }
		    }
		dynamic--;

		/* Create version 4.0 restriction for object worn by player. */
		parse_fixup_v380_restr (0, 3, dynamic + 3, 2, 0, failmessage);
	    }
}

static void
parse_fixup_v380_npc_restr (sc_bool notinsameroom, sc_int32 npc,
		sc_char *failmessage)
{
	/* Ignore if no NPC selected. */
	if (npc > 0)
	    {
		sc_int32	var2;

		if (npc == 1)
		    {
			/* Create restriction to look for alone, or not. */
			var2 = notinsameroom ? 3 : 2;
			parse_fixup_v380_restr (3, 3, 0, var2, 0, failmessage);
			return;
		    }

		/* Create restriction to look for company. */
		var2 = notinsameroom ? 1 : 0;
		parse_fixup_v380_restr (3, 3, 0, var2, npc + 1, failmessage);
	    }
}

static void
parse_fixup_v380_objroom_restr (sc_int32 obj, sc_int32 objroom,
		sc_char *failmessage)
{
	/* Ignore if no object selected. */
	if (obj > 0)
	    {
		/* Create version 4.0 restriction to check object in room. */
		parse_fixup_v380_restr (0, 3, obj + 1, 0, objroom, failmessage);
	    }
}

static void
parse_fixup_v380_objstate_restr (sc_int32 obj, sc_int32 ivar1, sc_int32 ivar2,
		sc_char *failmessage)
{
	sc_vartype_t	vt_key[3], vt_rvalue;
	sc_int32	object_count, object, dynamic, var2, var3;

	/* Unnecessary initializations to keep GCC happy. */
	var2 = -1;	var3 = -1;

	/* Ignore restrictions with no "type". */
	if (ivar1 == 0)
		return;

	/* Look for opened/closed restrictions, convert and return. */
	if (ivar1 == 3
		|| ivar1 == 4)
	    {
		sc_int32	stateful;

		/* Convert obj from object to openable (stateful) index. */
		stateful = 0;
		for (object = 0; object <= obj - 1; object++)
		    {
			sc_int32	openable;

			vt_key[0].string = "Objects";
			vt_key[1].integer = object;
			vt_key[2].string = "Openable";
			openable = prop_get_integer
					(parse_bundle, "I<-sis", vt_key);
			if (openable > 0)
				stateful++;
		    }
		stateful--;

		/*
		 * Create a version 4.0 restriction that checks that an
		 * object's state is open (var2 = 0) or closed (var2 = 1).
		 */
		var2 = (ivar1 == 3) ? 0 : 1;
		parse_fixup_v380_restr (1, 2, stateful + 1,
						var2, 0, failmessage);
		return;
	    }

	/* Get the count of objects defined. */
	vt_key[0].string = "Objects";
	object_count = prop_get (parse_bundle, "I<-s", &vt_rvalue, vt_key)
					? vt_rvalue.integer : 0;

	/* Convert obj from object to dynamic index. */
	dynamic = 0;
	for (object = 0; object <= obj - 1; object++)
	    {
		sc_bool		bstatic;

		vt_key[1].integer = object;
		vt_key[2].string = "Static";
		bstatic = prop_get_boolean (parse_bundle, "B<-sis", vt_key);
		if (!bstatic)
			dynamic++;
	    }
	dynamic--;

	/* Create version 4.0 object location restrictions for the rest. */
	switch (ivar1)
	    {
	    case 1:	var2 = 4; var3 = ivar2;		break;	/* Inside */
	    case 2:	var2 = 5; var3 = ivar2;		break;	/* On */
	    case 5:	var2 = 1; var3 = ivar2 + 1;	break;	/* Held by */
	    case 6:	var2 = 2; var3 = ivar2 + 1;	break;	/* Worn by */
	    default:
		sc_fatal ("parse_fixup_v380_objstate_restr:"
					" invalid ivar1, %ld\n", ivar1);
	    }
	parse_fixup_v380_restr (0, 3, dynamic + 3, var2, var3, failmessage);
}


/*
 * parse_fixup_v380()
 *
 * Handler for fixup special items to help with conversions from TAF version
 * 3.8 format into version 4.0.
 */
static void
parse_fixup_v380 (sc_char *fixup)
{
	if (parse_trace)
		sc_trace ("Parse: entering version 3.8 fixup %s\n", fixup);

	/* Convert container capacity attributes to version 4.0 values. */
	if (!strcmp (fixup, "|V380_OBJECT:#Capacity*10+2|"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	surfacecontainer;

		/* Get the object surface and container attributes. */
		vt_key.string = "SurfaceContainer";
		parse_push_key (vt_key, KEY_STRING);
		surfacecontainer = parse_get_integer_property ();
		parse_pop_key ();

		/* Convert capacity from version 3.8 format to version 4.0. */
		if (surfacecontainer == V380_OBJ_IS_CONTAINER)
		    {
			sc_int32	capacity;

			vt_key.string = "Capacity";
			parse_push_key (vt_key, KEY_STRING);
			capacity = parse_get_integer_property ();

			capacity = capacity * V380_OBJ_CAPACITY_MULT
						+ V380_OBJ_DEFAULT_SIZE;

			vt_value.integer = capacity;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
	    }

	/*
	 * Exchange openable values 5 and 6, watch for a possible 1 from a
	 * 3.8 game (interpret as 0), and write -1 key for openable objects.
	 */
	else if (!strcmp (fixup, "|V380_OBJECT:_Openable_,Key|"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	openable;

		/* Retrieve Openable, and if 5 or 6, exchange. */
		vt_key.string = "Openable";
		parse_push_key (vt_key, KEY_STRING);
		openable = parse_get_integer_property ();

		if (openable == 5 || openable == 6)
		    {
			vt_value.integer = (openable == 5) ? 6 : 5;
			parse_put_property (vt_value, PROP_INTEGER);
		    }

		/* If the odd value of 1, rewrite as zero. */
		else if (openable == 1)
		    {
			vt_value.integer = 0;
			parse_put_property (vt_value, PROP_INTEGER);
		    }

		parse_pop_key ();

		/* For openable objects, store a Key of -1. */
		if (openable == 5 || openable == 6)
		    {
			vt_key.string = "Key";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.integer = -1;
			parse_put_property (vt_value, PROP_INTEGER);
			parse_pop_key ();
		    }
	    }

	/* Create version 4.0 task actions from a version 3.8 task. */
	else if (!strcmp (fixup, "|V380_TASK:_Actions_|"))
	    {
		sc_vartype_t	vt_key;
		sc_int32	score;
		sc_bool		killsplayer, wingame;
		sc_uint16	movement;

		/* Retrieve the score change for the task. */
		vt_key.string = "Score";
		parse_push_key (vt_key, KEY_STRING);
		score = parse_get_integer_property ();
		parse_pop_key ();

		/* Create any appropriate score change action. */
		if (score != 0)
			parse_fixup_v380_action (4, 1, score, 0, 0);

		/* Get player death and game winning flags. */
		vt_key.string = "KillsPlayer";
		parse_push_key (vt_key, KEY_STRING);
		killsplayer = parse_get_integer_property ();
		parse_pop_key ();
		vt_key.string = "WinGame";
		parse_push_key (vt_key, KEY_STRING);
		wingame = parse_get_integer_property ();
		parse_pop_key ();

		/* Create any appropriate game ending actions. */
		if (killsplayer)
			parse_fixup_v380_action (6, 1, 2, 0, 0);
		if (wingame)
			parse_fixup_v380_action (6, 1, 0, 0, 0);

		/* Handle each defined movement for the task. */
		for (movement = 0; movement < V380_TASK_MOVEMENTS; movement++)
		    {
			sc_int32	mvar1, mvar2, mvar3;

			vt_key.integer = movement;
			parse_push_key (vt_key, KEY_INTEGER);
			vt_key.string = "Movements";
			parse_push_key (vt_key, KEY_STRING);

			/* Retrieve the movement parameters. */
			vt_key.string = "Var1";
			parse_push_key (vt_key, KEY_STRING);
			mvar1 = parse_get_integer_property ();
			parse_pop_key ();
			vt_key.string = "Var2";
			parse_push_key (vt_key, KEY_STRING);
			mvar2 = parse_get_integer_property ();
			parse_pop_key ();
			vt_key.string = "Var3";
			parse_push_key (vt_key, KEY_STRING);
			mvar3 = parse_get_integer_property ();
			parse_pop_key ();

			parse_pop_key ();
			parse_pop_key ();

			/* Create the corresponding task action. */
			parse_fixup_v380_movement (mvar1, mvar2, mvar3);
		    }
	    }

	/* Create version 4.0 task restrictions from a version 3.8 task. */
	else if (!strcmp (fixup, "|V380_TASK:_Restrictions_|"))
	    {
		sc_vartype_t	vt_key, vt_value, vt_rvalue;
		sc_bool		holding, tasknotdone, notinsameroom;
		sc_int32	holdobj1, holdobj2, holdobj3, task;
		sc_int32	wearobj1, wearobj2, npc, obj1, obj1room, obj2;
		sc_char		*holdmsg, *taskmsg, *wearmsg, *companymsg;
		sc_char		*obj1msg;
		sc_int32	restriction_count;

		/* Create restrictions for objects not held or absent. */
		vt_key.string = "HoldingSameRoom";
		parse_push_key (vt_key, KEY_STRING);
		holding = parse_get_boolean_property ();
		parse_pop_key ();

		vt_key.string = "HoldObj1";
		parse_push_key (vt_key, KEY_STRING);
		holdobj1 = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "HoldObj2";
		parse_push_key (vt_key, KEY_STRING);
		holdobj2 = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "HoldObj3";
		parse_push_key (vt_key, KEY_STRING);
		holdobj3 = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "HoldMsg";
		parse_push_key (vt_key, KEY_STRING);
		holdmsg = parse_get_string_property ();
		parse_pop_key ();

		parse_fixup_v380_obj_restr (holding, holdobj1, holdmsg);
		parse_fixup_v380_obj_restr (holding, holdobj2, holdmsg);
		parse_fixup_v380_obj_restr (holding, holdobj3, holdmsg);

		/* Create any task state restriction. */
		vt_key.string = "TaskNotDone";
		parse_push_key (vt_key, KEY_STRING);
		tasknotdone = parse_get_boolean_property ();
		parse_pop_key ();

		vt_key.string = "Task";
		parse_push_key (vt_key, KEY_STRING);
		task = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "TaskMsg";
		parse_push_key (vt_key, KEY_STRING);
		taskmsg = parse_get_string_property ();
		parse_pop_key ();

		parse_fixup_v380_task_restr (tasknotdone, task, taskmsg);

		/* Create any object not worn restrictions. */
		vt_key.string = "WearObj1";
		parse_push_key (vt_key, KEY_STRING);
		wearobj1 = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "WearObj2";
		parse_push_key (vt_key, KEY_STRING);
		wearobj2 = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "WearMsg";
		parse_push_key (vt_key, KEY_STRING);
		wearmsg = parse_get_string_property ();
		parse_pop_key ();

		parse_fixup_v380_wear_restr (wearobj1, wearmsg);
		parse_fixup_v380_wear_restr (wearobj2, wearmsg);

		/* Check for presence/absence of NPCs restriction. */
		vt_key.string = "NotInSameRoom";
		parse_push_key (vt_key, KEY_STRING);
		notinsameroom = parse_get_boolean_property ();
		parse_pop_key ();

		vt_key.string = "NPC";
		parse_push_key (vt_key, KEY_STRING);
		npc = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "CompanyMsg";
		parse_push_key (vt_key, KEY_STRING);
		companymsg = parse_get_string_property ();
		parse_pop_key ();

		parse_fixup_v380_npc_restr (notinsameroom, npc, companymsg);

		/* Create any object location restriction. */
		vt_key.string = "Obj1";
		parse_push_key (vt_key, KEY_STRING);
		obj1 = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "Obj1Room";
		parse_push_key (vt_key, KEY_STRING);
		obj1room = parse_get_integer_property ();
		parse_pop_key ();

		vt_key.string = "Obj1Msg";
		parse_push_key (vt_key, KEY_STRING);
		obj1msg = parse_get_string_property ();
		parse_pop_key ();

		parse_fixup_v380_objroom_restr (obj1, obj1room, obj1msg);

		/* And finally, any object state restriction. */
		vt_key.string = "Obj2";
		parse_push_key (vt_key, KEY_STRING);
		obj2 = parse_get_integer_property ();
		parse_pop_key ();

		if (obj2 > 0)
		    {
			sc_int32	var1, var2;
			sc_char		*obj2msg;

			vt_key.string = "Obj2Var1";
			parse_push_key (vt_key, KEY_STRING);
			var1 = parse_get_integer_property ();
			parse_pop_key ();

			vt_key.string = "Obj2Var2";
			parse_push_key (vt_key, KEY_STRING);
			var2 = parse_get_integer_property ();
			parse_pop_key ();

			vt_key.string = "Obj2Msg";
			parse_push_key (vt_key, KEY_STRING);
			obj2msg = parse_get_string_property ();
			parse_pop_key ();

			parse_fixup_v380_objstate_restr
						(obj2, var1, var2, obj2msg);
		    }

		/* Get a count of restrictions created. */
		vt_key.string = "Restrictions";
		parse_push_key (vt_key, KEY_STRING);
		restriction_count = (parse_get_property
						(&vt_rvalue, PROP_INTEGER))
						? vt_rvalue.integer : 0;
		parse_pop_key ();

		/* Allocate and fill a new mask for these restrictions. */
		if (restriction_count > 0)
		    {
			sc_char		*restrmask;
			sc_int32	index;

			restrmask = sc_malloc (2 * restriction_count);
			strcpy (restrmask, "#");
			for (index = 1; index < restriction_count; index++)
				strcat (restrmask, "A#");

			vt_key.string = "RestrMask";
			parse_push_key (vt_key, KEY_STRING);
			vt_value.string = restrmask;
			parse_put_property (vt_value, PROP_STRING);
			parse_pop_key ();

			prop_adopt (parse_bundle, restrmask);
		    }
	    }

	/*
	 * Adjust dynamic object initial positions and parents (where contained
	 * or on surfaces) into version 4.0 range.
	 */
	else if (!strcmp (fixup, "|V380_OBJECT:_InitialPositions_|"))
	    {
		sc_vartype_t	vt_key[3], vt_rvalue;
		sc_int32	object_count, object, *object_type;

		/* Get a count of objects. */
		vt_key[0].string = "Objects";
		object_count = prop_get (parse_bundle, "I<-s",
				&vt_rvalue, vt_key) ? vt_rvalue.integer : 0;

		/* Build an array of object container/surface types. */
		object_type = sc_malloc (object_count * sizeof (*object_type));
		for (object = 0; object < object_count; object++)
		    {
			vt_key[1].integer = object;
			vt_key[2].string = "SurfaceContainer";
			object_type[object] = prop_get_integer
					(parse_bundle, "I<-sis", vt_key);
		    }

		/* Adjust each object's initial position if necessary. */
		for (object = 0; object < object_count; object++)
		    {
			sc_vartype_t	vt_value;
			sc_bool		is_static;
			sc_int32	initialposition;

			/* Ignore static objects; we only want dynamic ones. */
			vt_key[1].integer = object;
			vt_key[2].string = "Static";
			is_static = prop_get_boolean
					(parse_bundle, "B<-sis", vt_key);
			if (is_static)
				continue;

			/* If initial position is above on/in, increment. */
			vt_key[1].integer = object;
			vt_key[2].string = "InitialPosition";
			initialposition = prop_get_integer
					(parse_bundle, "I<-sis", vt_key);
			if (initialposition > 2)
			    {
				vt_value.integer = initialposition + 1;
				prop_put (parse_bundle,
						"I->sis", vt_value, vt_key);
			    }

			/*
			 * If initial position is on or in, decide which,
			 * depending on the type of the parent.  From this,
			 * expand initial position into a version 4.0 value.
			 */
			if (initialposition == 2)
			    {
				sc_int32	count, parent, index;

				/* Get parent container/surface index. */
				vt_key[1].integer = object;
				vt_key[2].string = "Parent";
				count = prop_get_integer
					(parse_bundle, "I<-sis", vt_key);

				/* Convert container/surface index. */
				for (parent = 0;
					parent < object_count && count >= 0;
					parent++)
				    {
					if (object_type[parent]
							== V380_OBJ_IS_CONTAINER
						|| object_type[parent]
							== V380_OBJ_IS_SURFACE)
						count--;
				    }
				parent--;

				/* If parent is a surface, adjust position. */
				if (object_type[parent] == V380_OBJ_IS_SURFACE)
				    {
					vt_key[2].string = "InitialPosition";
					vt_value.integer = initialposition + 1;
					prop_put (parse_bundle,
						"I->sis", vt_value, vt_key);
				    }

				/*
				 * For both, adjust parent to be an object
				 * index for that type of object only.
				 */
				count = 0;
				for (index = 0; index < parent; index++)
				    {
					if (object_type[index]
							== object_type[parent])
						count++;
				    }
				vt_key[2].string = "Parent";
				vt_value.integer = count;
				prop_put (parse_bundle,
						"I->sis", vt_value, vt_key);
			    }
		    }

		/* Done with temporary array. */
		sc_free (object_type);
	    }

	/* Convert carry limit into version 4.0-like size and weight limits. */
	else if (!strcmp (fixup, "|V380_MaxSize_MaxWt_|"))
	    {
		sc_vartype_t	vt_key, vt_value;
		sc_int32	maxcarried;

		vt_key.string = "MaxCarried";
		parse_push_key (vt_key, KEY_STRING);
		maxcarried = parse_get_integer_property ();
		parse_pop_key ();

		vt_value.integer = maxcarried * V380_OBJ_CAPACITY_MULT
						+ V380_OBJ_DEFAULT_SIZE;

		vt_key.string = "MaxSize";
		parse_push_key (vt_key, KEY_STRING);
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();

		vt_key.string = "MaxWt";
		parse_push_key (vt_key, KEY_STRING);
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();
	    }

	/* Add up positive scoring tasks to arrive at max score. */
	else if (!strcmp (fixup, "|V380_GLOBAL:_MaxScore_|"))
	    {
		sc_vartype_t	vt_key[3], vt_value, vt_rvalue;
		sc_int32	task_count, maxscore, task;

		/* Get a count of tasks. */
		vt_key[0].string = "Tasks";
		task_count = prop_get (parse_bundle, "I<-s",
				&vt_rvalue, vt_key) ? vt_rvalue.integer : 0;

		/* Sum positive scoring tasks. */
		maxscore = 0;
		for (task = 0; task < task_count; task++)
		    {
			sc_int32	score;

			vt_key[1].integer = task;
			vt_key[2].string = "Score";
			score = prop_get_integer
					(parse_bundle, "I<-sis", vt_key);
			if (score > 0)
				maxscore += score;
		    }

		/* Write MaxScore global property. */
		vt_key[0].string = "Globals";
		vt_key[1].string = "MaxScore";
		vt_value.integer = maxscore;
		prop_put (parse_bundle, "I->ss", vt_value, vt_key);
	    }

	/* Convert walk meetobject from dynamic index to object. */
	else if (!strcmp (fixup, "|V380_WALK:_MeetObject_|"))
	    {
		sc_vartype_t	vt_key, vt_value, vt_rvalue, vt_gkey[3];
		sc_int32	meetobject, count, object_count, object;

		vt_key.string = "MeetObject";
		parse_push_key (vt_key, KEY_STRING);
		meetobject = parse_get_integer_property ();

		/* Get a count of objects. */
		vt_gkey[0].string = "Objects";
		object_count = prop_get (parse_bundle, "I<-s",
				&vt_rvalue, vt_gkey) ? vt_rvalue.integer : 0;

		/* Convert dynamic index to object, and rewrite. */
		count = meetobject - 1;
		for (object = 0;
			object < object_count && count >= 0; object++)
		    {
			sc_bool		bstatic;

			vt_gkey[1].integer = object;
			vt_gkey[2].string = "Static";
			bstatic = prop_get_boolean
					(parse_bundle, "B<-sis", vt_gkey);
			if (!bstatic)
				count--;
		    }
		object--;

		vt_value.integer = object;
		parse_put_property (vt_value, PROP_INTEGER);
		parse_pop_key ();
	    }

	/* Convert version 3.8 room data into a version 4.0 alts array. */
	else if (!strcmp (fixup, "|V380_ROOM:_Alts_|"))
	    {
		parse_fixup_v390_v380_room_alts ();
	    }

	/* Error if no fixup special handler available. */
	else
	    {
		sc_fatal ("parse_fixup_v380: no handler for \"%s\"\n", fixup);
	    }

	if (parse_trace)
		sc_trace ("Parse: leaving version 3.8 fixup %s\n", fixup);
}


/*
 * parse_fixup()
 *
 * Handler for fixup special items to help with conversions from TAF version
 * 3.9 and version 3.8 formats into version 4.0.
 */
static void
parse_fixup (sc_char *fixup)
{
	/*
	 * Pick a fixup handler specific to the TAF version.  This helps keep
	 * fixup code separate, rather than glommed into one large function.
	 */
	switch (taf_get_version (parse_taf))
	    {
	    case TAF_VERSION_400:
		sc_fatal ("parse_fixup: unexpected call\n");
		break;
	    case TAF_VERSION_390:	parse_fixup_v390 (fixup);	break;
	    case TAF_VERSION_380:	parse_fixup_v380 (fixup);	break;
	    default:
		sc_fatal ("parse_fixup: invalid TAF file version\n");
		break;
	    }
}


/*
 * parse_element()
 *
 * Parse a class descriptor element.
 */
static void
parse_element (sc_char *element)
{
	if (parse_trace)
		sc_trace ("Parse: entering element %s\n", element);

	/* Determine the element type from the first character. */
	switch (element[0])
	    {
	    case PARSE_ARRAY:		parse_array (element);		break;
	    case PARSE_VECTOR:		parse_vector (element, FALSE);	break;
	    case PARSE_VECTOR_ALTERNATE:parse_vector (element, TRUE);	break;
	    case PARSE_CLASS:		parse_class (element);		break;
	    case PARSE_EXPRESSION:	parse_expression (element);	break;
	    case PARSE_SPECIAL:		parse_special (element);	break;
	    case PARSE_FIXUP:		parse_fixup (element);		break;

	    case PARSE_INTEGER:			case PARSE_DEFAULT_ZERO:
	    case PARSE_BOOLEAN:			case PARSE_DEFAULT_TRUE:
	    case PARSE_DEFAULT_FALSE:
	    case PARSE_STRING:			case PARSE_DEFAULT_EMPTY:
	    case PARSE_IGNORE_INTEGER:		case PARSE_IGNORE_BOOLEAN:
	    case PARSE_IGNORE_STRING:
	    case PARSE_MULTILINE:	parse_terminal (element);	break;
	    default:
		sc_fatal ("parse_element: bad type, %c\n", element[0]);
	    }

	if (parse_trace)
		sc_trace ("Parse: leaving element %s\n", element);
}


/*
 * parse_descriptor()
 *
 * Parse a class's properties descriptor list.
 */
static void
parse_descriptor (sc_char *descriptor)
{
	sc_char		*nextp;

	/* Find and parse each element in the descriptor. */
	nextp = descriptor;
	while (*nextp != NUL)
	    {
		sc_char		element[PARSE_TEMP_LENGTH];

		/* Isolate the next descriptor element. */
		if (sscanf (nextp, "%[^ ]", element) != 1)
		    {
			sc_fatal ("parse_element: no element, %s\n", nextp);
		    }

		/* Parse this isolated element. */
		parse_element (element);

		/* Advance over the element and any trailing whitespace. */
		nextp += strlen (element);
		while (*nextp != NUL
				&& isspace (*nextp))
			nextp++;
	    }
}


/*
 * parse_class()
 *
 * Parse a class of properties.
 */
static void
parse_class (sc_char *aclass)
{
	sc_char			class[PARSE_TEMP_LENGTH];
	sc_uint16		index;

	/* Isolate the class name. */
	if (sscanf (aclass, "<%[^>]", class) != 1)
		sc_fatal ("parse_class: error in class, %s\n", aclass);
	if (parse_trace)
		sc_trace ("Parse: entering class %s\n", class);

	/* Find the class in the parse schema, and parse its descriptor. */
	for (index = 0; parse_schema[index].class != NULL; index++)
	    {
		if (!strcmp (parse_schema[index].class, class))
		    {
			sc_vartype_t	vt_key;

			/*
			 * Unless we are at the top level of the parse schema,
			 * push the class tag as a key.  The top level is
			 * "_GAME_", index 0, and isn't part of key formation.
			 */
			if (index > 0)
			    {
				vt_key.string = aclass + strlen (class) + 2;
				parse_push_key (vt_key, KEY_STRING);
			    }

			/* Parse each element in the descriptor. */
			parse_descriptor (parse_schema[index].descriptor);

			/* Parsed successfully. */
			if (index > 0)
				parse_pop_key ();
			if (parse_trace)
				sc_trace ("Parse: leaving class %s\n", class);
			return;
		    }
	    }

	/* Error in parse schema; class not found. */
	sc_fatal ("parse_class: class not described, %s\n", class);
}


/*
 * parse_add_walkalerts()
 *
 * Add a list of all NPC walks started by each task.  This is post-processing
 * that occurs after the TAF file has been successfully parsed.
 */
static void
parse_add_walkalerts (sc_prop_setref_t bundle)
{
	sc_vartype_t	vt_key[5], vt_rvalue;
	sc_int32	npcs_count, npc;

	/* Get the count of NPCs. */
	vt_key[0].string = "NPCs";
	npcs_count = (prop_get (bundle, "I<-s", &vt_rvalue, vt_key))
					? vt_rvalue.integer : 0;

	/* Set up each NPC. */
	for (npc = 0; npc < npcs_count; npc++)
	    {
		sc_int32	walk_count, walk;

		/* Get NPC walk details. */
		vt_key[1].integer = npc;
		vt_key[2].string = "Walks";
		walk_count = (prop_get (bundle, "I<-sis", &vt_rvalue, vt_key))
						? vt_rvalue.integer : 0;
		for (walk = 0; walk < walk_count; walk++)
		    {
			sc_int32	starttask;

			/* Get start task of walk. */
			vt_key[3].integer = walk;
			vt_key[4].string = "StartTask";
			starttask = prop_get_integer
					(bundle, "I<-sisis", vt_key) - 1;
			if (starttask >= 0)
			    {
				sc_vartype_t	vt_key2[4], vt_value;
				sc_int32	count;

				/* Count existing walkalerts for the task. */
				vt_key2[0].string = "Tasks";
				vt_key2[1].integer = starttask;
				vt_key2[2].string = "NPCWalkAlert";
				count = (prop_get (bundle,
						"I<-sis", &vt_rvalue, vt_key2))
						? vt_rvalue.integer : 0;

				/* Add two more -- NPC and walk. */
				vt_key2[3].integer = count;
				vt_value.integer = npc;
				prop_put (bundle, "I->sisi", vt_value, vt_key2);
				vt_key2[3].integer = count + 1;
				vt_value.integer = walk;
				prop_put (bundle, "I->sisi", vt_value, vt_key2);
			    }
		    }
	    }
}


/*
 * parse_add_movetimes()
 *
 * Add a list of move times to all NPC walks.  This is post-processing that
 * occurs after the TAF file has been successfully parsed.
 */
static void
parse_add_movetimes (sc_prop_setref_t bundle)
{
	sc_vartype_t	vt_key[6], vt_rvalue;
	sc_int32	npcs_count, npc;

	/* Get the count of NPCs. */
	vt_key[0].string = "NPCs";
	npcs_count = (prop_get (bundle, "I<-s", &vt_rvalue, vt_key))
					? vt_rvalue.integer : 0;

	/* Set up each NPC. */
	for (npc = 0; npc < npcs_count; npc++)
	    {
		sc_int32	walk_count, walk;

		/* Get NPC walk details. */
		vt_key[1].integer = npc;
		vt_key[2].string = "Walks";
		walk_count = (prop_get (bundle, "I<-sis", &vt_rvalue, vt_key))
						? vt_rvalue.integer : 0;
		for (walk = 0; walk < walk_count; walk++)
		    {
			sc_int32	waittimes;
			sc_int32	*movetimes, index;
			sc_vartype_t	vt_value;

			vt_key[3].integer = walk;
			vt_key[4].string = "Times";
			waittimes = (prop_get (bundle,
						"I<-sisis", &vt_rvalue, vt_key))
						? vt_rvalue.integer : 0;

			movetimes = sc_malloc ((waittimes + 1)
							* sizeof (*movetimes));
			memset (movetimes, 0, (waittimes + 1)
							* sizeof (*movetimes));
			for (index = waittimes - 1; index >= 0; index--)
			    {
				vt_key[4].string = "Times";
				vt_key[5].integer = index;
				movetimes[index] = prop_get_integer (bundle,
							"I<-sisisi", vt_key)
						+ movetimes[index + 1];
			    }
			movetimes[waittimes] = -2;

			for (index = 0; index <= waittimes; index++)
			    {
				vt_key[4].string = "MoveTimes";
				vt_key[5].integer = index;
				vt_value.integer = movetimes[index];
				prop_put (bundle,
						"I->sisisi", vt_value, vt_key);
			    }
			sc_free (movetimes);
		    }
	    }
}


/*
 * parse_add_alrs_index()
 *
 * Sort ALRs by original string length and store an indexer property, so
 * that ALR replacements look at longer strings before shorter ones.
 */
static void
parse_add_alrs_index (sc_prop_setref_t bundle)
{
	sc_vartype_t	vt_key[3], vt_rvalue;
	sc_int32	alr_count, index, alr;
	sc_int16	*alr_lengths, longest, shortest;
	sc_int16	length;

	/* Count ALRs, and set invariant part of properties key. */
	vt_key[0].string = "ALRs";
	alr_count = (prop_get (bundle, "I<-s", &vt_rvalue, vt_key))
						? vt_rvalue.integer : 0;

	/*
	 * Set up an array of the lengths of ALR original strings,
	 * and while at it, get the shortest and longest defined.
	 */
	alr_lengths = sc_malloc (alr_count * sizeof (*alr_lengths));
	shortest = SHRT_MAX; longest = 0;
	for (index = 0; index < alr_count; index++)
	    {
		sc_char		*original;

		vt_key[1].integer = index;
		vt_key[2].string = "Original";
		original = prop_get_string (bundle, "S<-sis", vt_key);
		length = strlen (original);

		alr_lengths[index] = length;
		if (length < shortest)
			shortest = length;
		if (length > longest)
			longest = length;
	    }

	/*
	 * Now write a set of secondary properties that define the order
	 * of handling for ALRs.  Our friend qsort() can't help here as
	 * it doesn't define the final ordering of equal members, and
	 * we need here to retain file ordering for ALR originals of the
	 * same length.
	 */
	vt_key[0].string = "ALRs2";
	alr = 0;
	for (length = longest; length >= shortest; length--)
	    {
		/* Find and add each ALR of this length. */
		for (index = 0; index < alr_count; index++)
		    {
			if (alr_lengths[index] == length)
			    {
				sc_vartype_t	vt_value;

				vt_key[1].integer = alr++;
				vt_key[2].string = "ALRIndex";
				vt_value.integer = index;
				prop_put (bundle, "I->sis", vt_value, vt_key);
			    }
		    }
	    }
	assert (alr == alr_count);

	/* Done with ALR lengths array. */
	sc_free (alr_lengths);
}


/*
 * parse_add_resources_offset()
 *
 * Add the resources offset to the properties as an extra game property
 * for version 4.0 games.  For version 3.9 and version 3.8 games, write
 * zero; only version 4.0 games can embed their resources into the TAF file.
 */
static void
parse_add_resources_offset (sc_prop_setref_t bundle, sc_tafref_t taf)
{
	sc_vartype_t	vt_key[2], vt_value;
	sc_bool		embedded;
	sc_int32	offset;

	/*
	 * Get the resources offset from the TAF, or default to zero.  The
	 * resources offset is one byte after the end of game data.
	 */
	vt_key[0].string = "Globals";
	vt_key[1].string = "Embedded";
	embedded = prop_get_boolean (bundle, "B<-ss", vt_key);
	offset = embedded ? taf_get_game_data_length (taf) + 1 : 0;

	/* Add this offset to the properties. */
	vt_key[0].string = "ResourceOffset";
	vt_value.integer = offset;
	prop_put (bundle, "I->s", vt_value, vt_key);
}


/*
 * parse_add_version()
 *
 * Add the TAF version to the properties, both integer and character forms
 * for convenience.
 */
static void
parse_add_version (sc_prop_setref_t bundle, sc_tafref_t taf)
{
	sc_vartype_t	vt_key, vt_value;

	/* Add the version integer to the properties. */
	vt_key.string = "Version";
	vt_value.integer = taf_get_version (taf);
	prop_put (bundle, "I->s", vt_value, &vt_key);

	/* Add the version string to the properties. */
	switch (taf_get_version (taf))
	    {
	    case TAF_VERSION_400:	vt_value.string = "4.00";	break;
	    case TAF_VERSION_390:	vt_value.string = "3.90";	break;
	    case TAF_VERSION_380:	vt_value.string = "3.80";	break;
	    default:
		sc_error ("parse_add_version_string:"
					" invalid TAF file version\n");
		vt_value.string = "[Unknown version]";			break;
	    }
	vt_key.string = "VersionString";
	prop_put (bundle, "S->s", vt_value, &vt_key);
}


/*
 * parse_game()
 *
 * Parse a game into a set properties.  Return TRUE on success, FALSE if
 * it encountered an error reading the TAF file.
 */
sc_bool
parse_game (sc_tafref_t taf, sc_prop_setref_t bundle)
{
	assert (taf != NULL);
	assert (bundle != NULL);

	/* Store the TAF to read from, and the bundle to store into. */
	parse_taf	= taf;
	parse_bundle	= bundle;
	parse_schema	= parse_select_schema (parse_taf);

	/* Set up error handling jump buffer. */
	if (setjmp (parse_taf_error) == 0)
	    {
		/* Parse a complete game. */
		taf_first_line (parse_taf);
		parse_tafline = 0;
		parse_class ("<_GAME_>");

		/* Free the accumulated version 4.0 resources details. */
		parse_clear_v400_resources_table ();

		/* See if we reached the end of the TAF. */
		if (taf_more_lines (parse_taf))
			sc_error ("parse_game: unexpected trailing data\n");

		/* Append post-processing walkalerts and move times. */
		parse_add_walkalerts (parse_bundle);
		parse_add_movetimes (parse_bundle);

		/* Append sorted ALR list and resources offset. */
		parse_add_alrs_index (parse_bundle);
		parse_add_resources_offset (parse_bundle, parse_taf);

		/* Add a note of the TAF file version. */
		parse_add_version (parse_bundle, parse_taf);

		/* Trim excess allocations from properties. */
		prop_solidify (parse_bundle);

		/* Return successfully. */
		parse_taf	= NULL;
		parse_bundle	= NULL;
		parse_schema	= NULL;
		return TRUE;
	    }

	/* Error with one of the TAF file lines. */
	parse_clear_v400_resources_table ();
	parse_taf	= NULL;
	parse_bundle	= NULL;
	parse_schema	= NULL;
	return FALSE;
}


/*
 * parse_debug_trace()
 *
 * Set parse tracing on/off.
 */
void
parse_debug_trace (sc_bool flag)
{
	parse_trace = flag;
}
