// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file  deh_soc.c
/// \brief Load SOC file and change tables and text

#include "doomdef.h"
#include "d_main.h" // for srb2home
#include "g_game.h"
#include "sounds.h"
#include "info.h"
#include "d_think.h"
#include "m_argv.h"
#include "z_zone.h"
#include "w_wad.h"
#include "y_inter.h"
#include "m_menu.h"
#include "m_misc.h"
#include "f_finale.h"
#include "st_stuff.h"
#include "i_system.h"
#include "p_setup.h"
#include "r_data.h"
#include "r_textures.h"
#include "r_draw.h"
#include "r_picformats.h"
#include "r_things.h" // R_Char2Frame
#include "r_sky.h"
#include "lua_script.h" // Reluctantly included for LUA_EvalMath
#include "d_clisrv.h"
#include "v_video.h"

#ifdef HWRENDER
#include "hardware/hw_light.h"
#endif

#include "m_cond.h"

#include "dehacked.h"
#include "deh_soc.h"
#include "deh_lua.h" // included due to some LUA_SetLuaAction hack smh
// also used for LUA_UpdateSprName
#include "deh_tables.h"

// SRB2Kart
#include "filesrch.h" // refreshdirmenu
#include "k_follower.h"
#include "doomstat.h" // MAXMUSNAMES
#include "k_items.h"

// Loops through every constant and operation in word and performs its calculations, returning the final value.
fixed_t get_number(const char *word)
{
	return LUA_EvalMath(word);

	/*// DESPERATELY NEEDED: Order of operations support! :x
	fixed_t i = find_const(&word);
	INT32 o;
	while(*word) {
		o = operation_pad(&word);
		if (o != -1)
			i = OPERATIONS[o].v(i,find_const(&word));
		else
			break;
	}
	return i;*/
}

#define PARAMCHECK(n) do { if (!params[n]) { deh_warning("Too few parameters, need %d", n); return; }} while (0)

/* ======================================================================== */
// Load a dehacked file format
/* ======================================================================== */
/* a sample to see
                   Thing 1 (Player)       {           // MT_PLAYER
INT32 doomednum;     ID # = 3232              -1,             // doomednum
INT32 spawnstate;    Initial frame = 32       "PLAY",         // spawnstate
INT32 spawnhealth;   Hit points = 3232        100,            // spawnhealth
INT32 seestate;      First moving frame = 32  "PLAY_RUN1",    // seestate
INT32 seesound;      Alert sound = 32         sfx_None,       // seesound
INT32 reactiontime;  Reaction time = 3232     0,              // reactiontime
INT32 attacksound;   Attack sound = 32        sfx_None,       // attacksound
INT32 painstate;     Injury frame = 32        "PLAY_PAIN",    // painstate
INT32 painchance;    Pain chance = 3232       255,            // painchance
INT32 painsound;     Pain sound = 32          sfx_plpain,     // painsound
INT32 meleestate;    Close attack frame = 32  "NULL",         // meleestate
INT32 missilestate;  Far attack frame = 32    "PLAY_ATK1",    // missilestate
INT32 deathstate;    Death frame = 32         "PLAY_DIE1",    // deathstate
INT32 xdeathstate;   Exploding frame = 32     "PLAY_XDIE1",   // xdeathstate
INT32 deathsound;    Death sound = 32         sfx_pldeth,     // deathsound
INT32 speed;         Speed = 3232             0,              // speed
INT32 radius;        Width = 211812352        16*FRACUNIT,    // radius
INT32 height;        Height = 211812352       56*FRACUNIT,    // height
INT32 dispoffset;    DispOffset = 0           0,              // dispoffset
INT32 mass;          Mass = 3232              100,            // mass
INT32 damage;        Missile damage = 3232    0,              // damage
INT32 activesound;   Action sound = 32        sfx_None,       // activesound
INT32 flags;         Bits = 3232              MF_SOLID|MF_SHOOTABLE|MF_DROPOFF|MF_PICKUP|MF_NOTDMATCH,
INT32 raisestate;    Respawn frame = 32       S_NULL          // raisestate
                                         }, */

#ifdef HWRENDER
static INT32 searchvalue(const char *s)
{
	while (s[0] != '=' && s[0])
		s++;
	if (s[0] == '=')
		return atoi(&s[1]);
	else
	{
		deh_warning("No value found");
		return 0;
	}
}

static float searchfvalue(const char *s)
{
	while (s[0] != '=' && s[0])
		s++;
	if (s[0] == '=')
		return (float)atof(&s[1]);
	else
	{
		deh_warning("No value found");
		return 0;
	}
}
#endif

// These are for clearing all of various things
void clear_emblems(void)
{
	INT32 i;

	for (i = 0; i < MAXEMBLEMS; ++i)
	{
		Z_Free(emblemlocations[i].level);
		emblemlocations[i].level = NULL;

		Z_Free(emblemlocations[i].stringVar);
		emblemlocations[i].stringVar = NULL;
	}

	memset(&emblemlocations, 0, sizeof(emblemlocations));
	numemblems = 0;
}

void clear_unlockables(void)
{
	INT32 i;

	for (i = 0; i < MAXUNLOCKABLES; ++i)
	{
		Z_Free(unlockables[i].stringVar);
		unlockables[i].stringVar = NULL;
	}

	memset(&unlockables, 0, sizeof(unlockables));
}

void clear_conditionsets(void)
{
	UINT8 i;
	for (i = 0; i < MAXCONDITIONSETS; ++i)
		M_ClearConditionSet(i+1);
}

void clear_levels(void)
{
	// This is potentially dangerous but if we're resetting these headers,
	// we may as well try to save some memory, right?
	while (nummapheaders > 0)
	{
		nummapheaders--;

		if (!mapheaderinfo[nummapheaders])
			continue;

		// Custom map header info
		// (no need to set num to 0, we're freeing the entire header shortly)
		Z_Free(mapheaderinfo[nummapheaders]->customopts);

		P_DeleteFlickies(nummapheaders);

		Patch_Free(mapheaderinfo[nummapheaders]->thumbnailPic);
		Patch_Free(mapheaderinfo[nummapheaders]->minimapPic);

		Z_Free(mapheaderinfo[nummapheaders]->lumpname);

		Z_Free(mapheaderinfo[nummapheaders]);
		mapheaderinfo[nummapheaders] = NULL;
	}

	// Clear out the cache
	{
		cupheader_t *cup = kartcupheaders;
		UINT8 i;

		while (cup)
		{
			for (i = 0; i < CUPCACHE_MAX; i++)
			{
				cup->cachedlevels[i] = NEXTMAP_INVALID;
			}
			cup = cup->next;
		}
	}

	// Exit the current gamemap as a safeguard
	if (Playing())
		COM_BufAddText("exitgame"); // Command_ExitGame_f() but delayed
}

// TODO: Figure out how to do undolines for this....
// TODO: Warnings for running out of freeslots
void readfreeslots(MYFILE *f)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word,*type;
	char *tmp;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			type = strtok(s, "_");
			if (type)
				strupr(type);
			else
				break;

			word = strtok(NULL, "\n");
			if (word)
				strupr(word);
			else
				break;

			INT32 index;
			if (DEH_ReadFreeslot(type, word, &index) == -2)
				deh_warning("Freeslots: unknown enum class '%s' for '%s_%s'", type, type, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

void readthing(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word, *word2;
	char *tmp;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			word2 = strtok(NULL, " = ");
			if (word2)
				strupr(word2);
			else
				break;
			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';

			if (fastcmp(word, "MAPTHINGNUM") || fastcmp(word, "DOOMEDNUM"))
			{
				mobjinfo[num].doomednum = (INT32)atoi(word2);
			}
			else if (fastcmp(word, "SPAWNSTATE"))
			{
				mobjinfo[num].spawnstate = get_number(word2);
			}
			else if (fastcmp(word, "SPAWNHEALTH"))
			{
				mobjinfo[num].spawnhealth = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "SEESTATE"))
			{
				mobjinfo[num].seestate = get_number(word2);
			}
			else if (fastcmp(word, "SEESOUND"))
			{
				mobjinfo[num].seesound = get_number(word2);
			}
			else if (fastcmp(word, "REACTIONTIME"))
			{
				mobjinfo[num].reactiontime = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "ATTACKSOUND"))
			{
				mobjinfo[num].attacksound = get_number(word2);
			}
			else if (fastcmp(word, "PAINSTATE"))
			{
				mobjinfo[num].painstate = get_number(word2);
			}
			else if (fastcmp(word, "PAINCHANCE"))
			{
				mobjinfo[num].painchance = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "PAINSOUND"))
			{
				mobjinfo[num].painsound = get_number(word2);
			}
			else if (fastcmp(word, "MELEESTATE"))
			{
				mobjinfo[num].meleestate = get_number(word2);
			}
			else if (fastcmp(word, "MISSILESTATE"))
			{
				mobjinfo[num].missilestate = get_number(word2);
			}
			else if (fastcmp(word, "DEATHSTATE"))
			{
				mobjinfo[num].deathstate = get_number(word2);
			}
			else if (fastcmp(word, "DEATHSOUND"))
			{
				mobjinfo[num].deathsound = get_number(word2);
			}
			else if (fastcmp(word, "XDEATHSTATE"))
			{
				mobjinfo[num].xdeathstate = get_number(word2);
			}
			else if (fastcmp(word, "SPEED"))
			{
				mobjinfo[num].speed = get_number(word2);
			}
			else if (fastcmp(word, "RADIUS"))
			{
				mobjinfo[num].radius = get_number(word2);
			}
			else if (fastcmp(word, "HEIGHT"))
			{
				mobjinfo[num].height = get_number(word2);
			}
			else if (fastcmp(word, "DISPOFFSET"))
			{
				mobjinfo[num].dispoffset = get_number(word2);
			}
			else if (fastcmp(word, "MASS"))
			{
				mobjinfo[num].mass = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "DAMAGE"))
			{
				mobjinfo[num].damage = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "ACTIVESOUND"))
			{
				mobjinfo[num].activesound = get_number(word2);
			}
			else if (fastcmp(word, "FLAGS"))
			{
				mobjinfo[num].flags = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "RAISESTATE"))
			{
				mobjinfo[num].raisestate = get_number(word2);
			}
			else
				deh_warning("Thing %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	if (wadfiles[f->wad]->compatmode)
		mobjinfo[num].compatmode = true;
	else
		mobjinfo[num].compatmode = false;

	Z_Free(s);
}

void readskincolor(MYFILE *f, INT32 num, boolean mainfile)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;

	Color_cons_t[num].value = num;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;

			if (fastcmp(word, "NAME"))
			{
				size_t namesize = sizeof(skincolors[num].name);
				char truncword[namesize];
				UINT16 dupecheck;

				deh_strlcpy(truncword, word2, namesize, va("Skincolor %d: name", num)); // truncate here to check for dupes
				dupecheck = R_GetColorByName(truncword);
				if (truncword[0] != '\0' && (fasticmp(truncword, skincolors[SKINCOLOR_NONE].name) || (dupecheck && dupecheck != num)))
				{
					size_t lastchar = strlen(truncword);
					char oldword[lastchar+1];
					char dupenum = '1';

					strlcpy(oldword, truncword, lastchar+1);
					lastchar--;
					if (lastchar == namesize-2) // exactly max length, replace last character with 0
						truncword[lastchar] = '0';
					else // append 0
					{
						strcat(truncword, "0");
						lastchar++;
					}

					while (R_GetColorByName(truncword))
					{
						truncword[lastchar] = dupenum;
						if (dupenum == '9')
							dupenum = 'A';
						else if (dupenum == 'Z') // give up :?
							break;
						else
							dupenum++;
					}

					deh_warning("Skincolor %d: name %s is a duplicate of another skincolor's name - renamed to %s", num, oldword, truncword);
				}

				strlcpy(skincolors[num].name, truncword, namesize); // already truncated
			}
			else if (fastcmp(word, "RAMP"))
			{
				UINT8 i;
				tmp = strtok(word2,",");
				for (i = 0; i < COLORRAMPSIZE; i++) {
					skincolors[num].ramp[i] = (UINT8)get_number(tmp);
					if ((tmp = strtok(NULL,",")) == NULL)
						break;
				}
				skincolor_modified[num] = true;
			}
			else if (fastcmp(word, "INVCOLOR"))
			{
				UINT16 v = (UINT16)get_number(word2);
				if (v < numskincolors)
					skincolors[num].invcolor = v;
				else
					skincolors[num].invcolor = SKINCOLOR_GREEN;
			}
			else if (fastcmp(word, "INVSHADE"))
			{
				skincolors[num].invshade = get_number(word2)%COLORRAMPSIZE;
			}
			else if (fastcmp(word, "CHATCOLOR"))
			{
				skincolors[num].chatcolor = get_number(word2);
			}
			else if (fastcmp(word, "ACCESSIBLE"))
			{
				if (mainfile || num > FIRSTSUPERCOLOR)
					skincolors[num].accessible = (boolean)(atoi(word2) || word2[0] == 'T' || word2[0] == 'Y');
			}
			else
				deh_warning("Skincolor %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

#ifdef HWRENDER
void readlight(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *tmp;
	INT32 value;
	float fvalue;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			fvalue = searchfvalue(s);
			value = searchvalue(s);

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			if (fastcmp(word, "TYPE"))
			{
				lspr[num].type = (UINT16)value;
			}
			else if (fastcmp(word, "OFFSETX"))
			{
				lspr[num].light_xoffset = fvalue;
			}
			else if (fastcmp(word, "OFFSETY"))
			{
				lspr[num].light_yoffset = fvalue;
			}
			else if (fastcmp(word, "CORONACOLOR"))
			{
				lspr[num].corona_color = value;
			}
			else if (fastcmp(word, "CORONARADIUS"))
			{
				lspr[num].corona_radius = fvalue;
			}
			else if (fastcmp(word, "DYNAMICCOLOR"))
			{
				lspr[num].dynamic_color = value;
			}
			else if (fastcmp(word, "DYNAMICRADIUS"))
			{
				lspr[num].dynamic_radius = fvalue;

				/// \note Update the sqrradius! unnecessary?
				lspr[num].dynamic_sqrradius = fvalue * fvalue;
			}
			else
				deh_warning("Light %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}
#endif // HWRENDER

void readsprite2(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word, *word2;
	char *tmp;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			word2 = strtok(NULL, " = ");
			if (word2)
				strupr(word2);
			else
				break;
			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';

			if (fastcmp(word, "DEFAULT"))
				spr2defaults[num] = get_number(word2);
			else
				deh_warning("Sprite2 %s: unknown word '%s'", spr2names[num], word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

// copypasted from readPlayer :]
void readgametype(MYFILE *f, char *gtname)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2, *word2lwr = NULL;
	char *tmp;
	INT32 i, j;

	gametype_t *newgametype = NULL;
	UINT32 newgtrules = 0;
	UINT32 newgttol = 0;
	INT32 newgtpointlimit = 0;
	INT32 newgttimelimit = 0;
	INT32 newgtcolor = V_YELLOWMAP;
	UINT8 newgtinttype = 0;
	char gtconst[MAXLINELEN];

	// Empty strings.
	gtconst[0] = '\0';

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			word2 = strtok(NULL, " = ");
			if (word2)
			{
				if (!word2lwr)
					word2lwr = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
				strcpy(word2lwr, word2);
				strupr(word2);
			}
			else
				break;

			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';
			i = atoi(word2);

			// Game type rules
			if (fastcmp(word, "RULES"))
			{
				// GTR_
				newgtrules = (UINT32)get_number(word2);
			}
			// Identifier
			else if (fastcmp(word, "IDENTIFIER"))
			{
				// GT_
				strncpy(gtconst, word2, MAXLINELEN);
			}
			// Point and time limits
			else if (fastcmp(word, "DEFAULTPOINTLIMIT"))
				newgtpointlimit = (INT32)i;
			else if (fastcmp(word, "DEFAULTTIMELIMIT"))
				newgttimelimit = (INT32)i;
			// Intermission type
			else if (fastcmp(word, "INTERMISSIONTYPE"))
			{
				// Case sensitive
				newgtinttype = (int)get_number(word2lwr);
			}
			// Type of level
			else if (fastcmp(word, "TYPEOFLEVEL"))
			{
				if (i) // it's just a number
					newgttol = (UINT32)i;
				else
				{
					UINT32 tol = 0;
					tmp = strtok(word2,",");
					do {
						for (i = 0; TYPEOFLEVEL[i].name; i++)
							if (fasticmp(tmp, TYPEOFLEVEL[i].name))
								break;
						if (!TYPEOFLEVEL[i].name)
							deh_warning("readgametype %s: unknown typeoflevel flag %s\n", gtname, tmp);
						tol |= TYPEOFLEVEL[i].flag;
					} while((tmp = strtok(NULL,",")) != NULL);
					newgttol = tol;
				}
			}
			// Menu Color
			else if (fastcmp(word, "MENUCOLOR"))
			{
				INT32 color = (INT32)get_number(word2);
				// Mask out other flags so they aren't passed into menu code.
				newgtcolor = color & V_CHARCOLORMASK; // V_
			}
			// The SOC probably provided gametype rules as words,
			// instead of using the RULES keyword.
			// Like for example "NOSPECTATORSPAWN = TRUE".
			// This is completely valid, and looks better anyway.
			else
			{
				UINT32 wordgt = 0;
				for (j = 0; GAMETYPERULE_LIST[j]; j++)
					if (fastcmp(word, GAMETYPERULE_LIST[j])) {
						wordgt |= (1<<j);
						if (i || word2[0] == 'T' || word2[0] == 'Y')
							newgtrules |= wordgt;
						break;
					}
				if (!wordgt)
					deh_warning("readgametype %s: unknown word '%s'", gtname, word);
			}
		}
	} while (!myfeof(f)); // finish when the line is empty

	// Free strings.
	Z_Free(s);
	if (word2lwr)
		Z_Free(word2lwr);

	// Ran out of gametype slots
	if (numgametypes == GT_LASTFREESLOT)
	{
		I_Error("Out of Gametype Freeslots while allocating \"%s\"\nLoad less addons to fix this.", gtname);
	}

	// Add the new gametype
	newgametype = Z_Calloc(sizeof (gametype_t), PU_STATIC, NULL);
	if (!newgametype)
	{
		I_Error("Out of memory allocating gametype \"%s\"", gtname);
	}

	if (gtconst[0] == '\0')
		strncpy(gtconst, gtname, MAXLINELEN);

	newgametype->name = Z_StrDup((const char *)gtname);
	newgametype->rules = newgtrules;
	newgametype->constant = G_PrepareGametypeConstant((const char *)gtconst);
	newgametype->tol = newgttol;
	newgametype->intermission = newgtinttype;
	newgametype->pointlimit = newgtpointlimit;
	newgametype->timelimit = newgttimelimit;
	newgametype->color = newgtcolor;

	gametypes[numgametypes++] = newgametype;

	// Update gametype_cons_t accordingly.
	G_UpdateGametypeSelections();

	CONS_Printf("Added gametype %s\n", gtname);
}

static mapheader_lighting_t *usemaplighting(INT32 mapnum, const char *word)
{
	if (fastncmp(word, "ENCORE", 6))
	{
		mapheaderinfo[mapnum]->use_encore_lighting = true;

		return &mapheaderinfo[mapnum]->lighting_encore;
	}
	else
	{
		return &mapheaderinfo[mapnum]->lighting;
	}
}

void readlevelheader(MYFILE *f, char * name)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);

	char *word;
	char *word2;
	//char *word3; // Non-uppercase version of word2

	char *tmp;
	INT32 i;

	// convert old map names to MAPxx
	INT32 exnum = atoi(name);
	if (exnum > 0 && exnum <= NUMMAPS)
	{
		if (exnum < 100)
			name = va("MAP%02d", exnum);
		else
			// one liner extended names :^)
			name = va("MAP%c%c", 'A' + (exnum - 100)/36, ((exnum - 100) % 36 < 10 ? '0' : 'A'-10) + (exnum - 100) % 36);
	}
	else if (strlen(name) == 2 && isalpha(name[0]) && isalnum(name[1]))
	{
		exnum = (name[0] - 'A')*36 + (isalpha(name[1]) ? name[1] - 'A'+10 : name[1] - '0') + 100;
		name = va("MAP%s", name);
	}

	INT32 num = G_MapNumber(name);

	if (num >= nummapheaders)
	{
		P_AllocMapHeader((INT16)(num = nummapheaders));
	}
	else if (f->wad >= NUMMAINWADS)
	{
		// only mark as a major mod if it replaces an already-existing mapheaderinfo
		G_SetGameModified(multiplayer, true);
	}

	// compatmode map number system:
	// 0-1034 correspond to MAP01-MAPZZ,
	// longname maps are appended starting at NUMMAPS
	if (!exnum)
	{
		exnum = ++nextexnum;
		if (exnum >= NEXTMAP_SPECIAL)
			I_Error("Ran out of compatibility map slots");
	}
	kartmap2native[exnum-1] = num+1;
	nativemap2kart[num] = exnum-1;

	if (mapheaderinfo[num]->lumpname == NULL)
	{
		mapheaderinfo[num]->lumpname = Z_StrDup(name);
		mapheaderinfo[num]->lumpnamehash = FNV1a_QuickCaseHash(mapheaderinfo[num]->lumpname, MAXMAPLUMPNAME);
	}

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Set / reset word, because some things (Lua.) move it
			word = s;

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			i = atoi(word2); // used for numerical settings

			// The following support mixed/lower case.

			if (fastcmp(word, "LEVELNAME"))
			{
				deh_strlcpy(mapheaderinfo[num]->lvlttl, word2,
					sizeof(mapheaderinfo[num]->lvlttl), va("Level header %d: levelname", num));
				continue;
			}

			if (fastcmp(word, "ZONETITLE"))
			{
				deh_strlcpy(mapheaderinfo[num]->zonttl, word2,
					sizeof(mapheaderinfo[num]->zonttl), va("Level header %d: zonetitle", num));
				continue;
			}

			if (fastcmp(word, "SUBTITLE"))
			{
				deh_strlcpy(mapheaderinfo[num]->subttl, word2,
					sizeof(mapheaderinfo[num]->subttl), va("Level header %d: subtitle", num));
				continue;
			}

			// Lua custom options also go above, contents may be case sensitive.
			if (fastncmp(word, "LUA.", 4))
			{
				UINT8 j;
				customoption_t *modoption;

				// Note: we actualy strlwr word here, so things are made a little easier for Lua
				strlwr(word);
				word += 4; // move past "lua."

				// ... and do a simple name sanity check; the name must start with a letter
				if (*word < 'a' || *word > 'z')
				{
					deh_warning("Level header %d: invalid custom option name \"%s\"", num, word);
					continue;
				}

				// Sanity limit of 128 params
				if (mapheaderinfo[num]->numCustomOptions == 128)
				{
					deh_warning("Level header %d: too many custom parameters", num);
					continue;
				}
				j = mapheaderinfo[num]->numCustomOptions++;

				mapheaderinfo[num]->customopts =
					Z_Realloc(mapheaderinfo[num]->customopts,
						sizeof(customoption_t) * mapheaderinfo[num]->numCustomOptions, PU_STATIC, NULL);

				// Newly allocated
				modoption = &mapheaderinfo[num]->customopts[j];

				strncpy(modoption->option, word,  31);
				modoption->option[31] = '\0';
				strncpy(modoption->value,  word2, 255);
				modoption->value[255] = '\0';
				continue;
			}

			// Now go to uppercase
			strupr(word2);

			// List of flickies that are be freed in this map
			if (fastcmp(word, "FLICKYLIST") || fastcmp(word, "ANIMALLIST"))
			{
				if (fastcmp(word2, "NONE"))
					P_DeleteFlickies(num);
				else if (fastcmp(word2, "DEMO"))
					P_SetDemoFlickies(num);
				else if (fastcmp(word2, "ALL"))
				{
					mobjtype_t tmpflickies[MAXFLICKIES];

					for (mapheaderinfo[num]->numFlickies = 0;
					((mapheaderinfo[num]->numFlickies < MAXFLICKIES) && FLICKYTYPES[mapheaderinfo[num]->numFlickies].type);
					mapheaderinfo[num]->numFlickies++)
						tmpflickies[mapheaderinfo[num]->numFlickies] = FLICKYTYPES[mapheaderinfo[num]->numFlickies].type;

					if (mapheaderinfo[num]->numFlickies) // just in case...
					{
						size_t newsize = sizeof(mobjtype_t) * mapheaderinfo[num]->numFlickies;
						mapheaderinfo[num]->flickies = Z_Realloc(mapheaderinfo[num]->flickies, newsize, PU_STATIC, NULL);
						memcpy(mapheaderinfo[num]->flickies, tmpflickies, newsize);
					}
				}
				else
				{
					mobjtype_t tmpflickies[MAXFLICKIES];
					mapheaderinfo[num]->numFlickies = 0;
					tmp = strtok(word2,",");
					// get up to the first MAXFLICKIES flickies
					do {
						if (mapheaderinfo[num]->numFlickies == MAXFLICKIES) // never going to get above that number
						{
							deh_warning("Level header %d: too many flickies\n", num);
							break;
						}

						if (fastncmp(tmp, "MT_", 3)) // support for specified mobjtypes...
						{
							i = get_mobjtype(tmp);
							if (i == NUMMOBJTYPES)
							{
								//deh_warning("Level header %d: unknown flicky mobj type %s\n", num, tmp); -- no need for this line as get_mobjtype complains too
								continue;
							}
							tmpflickies[mapheaderinfo[num]->numFlickies] = i;
						}
						else // ...or a quick, limited selection of default flickies!
						{
							for (i = 0; FLICKYTYPES[i].name; i++)
								if (fastcmp(tmp, FLICKYTYPES[i].name))
									break;

							if (!FLICKYTYPES[i].name)
							{
								deh_warning("Level header %d: unknown flicky selection %s\n", num, tmp);
								continue;
							}
							tmpflickies[mapheaderinfo[num]->numFlickies] = FLICKYTYPES[i].type;
						}
						mapheaderinfo[num]->numFlickies++;
					} while ((tmp = strtok(NULL,",")) != NULL);

					if (mapheaderinfo[num]->numFlickies)
					{
						size_t newsize = sizeof(mobjtype_t) * mapheaderinfo[num]->numFlickies;
						mapheaderinfo[num]->flickies = Z_Realloc(mapheaderinfo[num]->flickies, newsize, PU_STATIC, NULL);
						// now we add them to the list!
						memcpy(mapheaderinfo[num]->flickies, tmpflickies, newsize);
					}
					else
						deh_warning("Level header %d: no valid flicky types found\n", num);
				}
			}

			// Strings that can be truncated
			else if (fastcmp(word, "SCRIPTNAME"))
			{
				deh_strlcpy(mapheaderinfo[num]->scriptname, word2,
					sizeof(mapheaderinfo[num]->scriptname), va("Level header %d: scriptname", num));
			}
			else if (fastcmp(word, "RUNSOC"))
			{
				deh_strlcpy(mapheaderinfo[num]->runsoc, word2,
					sizeof(mapheaderinfo[num]->runsoc), va("Level header %d: runsoc", num));
			}
			else if (fastcmp(word, "ACT"))
			{
				/*if (i >= 0 && i < 20) // 0 for no act number, TTL1 through TTL19
					mapheaderinfo[num]->actnum = (UINT8)i;
				else
					deh_warning("Level header %d: invalid act number %d", num, i);*/
				deh_strlcpy(mapheaderinfo[num]->actnum, word2,
					sizeof(mapheaderinfo[num]->actnum), va("Level header %d: actnum", num));
			}
			else if (fastcmp(word, "TYPEOFLEVEL"))
			{
				if (i) // it's just a number
					mapheaderinfo[num]->typeoflevel = (UINT32)i;
				else
				{
					UINT32 tol = 0;
					tmp = strtok(word2,",");
					do {
						for (i = 0; TYPEOFLEVEL[i].name; i++)
							if (fastcmp(tmp, TYPEOFLEVEL[i].name))
								break;
						if (!TYPEOFLEVEL[i].name)
							deh_warning("Level header %d: unknown typeoflevel flag %s\n", num, tmp);
						tol |= TYPEOFLEVEL[i].flag;
					} while((tmp = strtok(NULL,",")) != NULL);
					mapheaderinfo[num]->typeoflevel = tol;
				}
			}
			else if (fastcmp(word, "KEYWORDS"))
			{
				deh_strlcpy(mapheaderinfo[num]->keywords, word2,
						sizeof(mapheaderinfo[num]->keywords), va("Level header %d: keywords", num));
			}
			else if (fastcmp(word, "MUSIC"))
			{
				if (fastcmp(word2, "NONE"))
				{
					mapheaderinfo[num]->musname[0][0] = 0; // becomes empty string
					mapheaderinfo[num]->musname_size = 0;
				}
				else
				{
					UINT8 j = 0; // i was declared elsewhere
					tmp = strtok(word2, ",");
					do {
						if (j >= MAXMUSNAMES)
							break;
						deh_strlcpy(mapheaderinfo[num]->musname[j], tmp,
							sizeof(mapheaderinfo[num]->musname[j]), va("Level header %d: music", num));
						j++;
					} while ((tmp = strtok(NULL,",")) != NULL);
					
					if (tmp != NULL)
						deh_warning("Level header %d: additional music slots past %d discarded", num, MAXMUSNAMES);
					mapheaderinfo[num]->musname_size = j;
				}
			}
			else if (fastcmp(word, "MUSICSLOT"))
				deh_warning("Level header %d: MusicSlot parameter is deprecated and will be removed.\nUse \"Music\" instead.", num);
			else if (fastcmp(word, "MUSICTRACK"))
				mapheaderinfo[num]->mustrack = ((UINT16)i - 1);
			else if (fastcmp(word, "MUSICPOS"))
				mapheaderinfo[num]->muspos = (UINT32)get_number(word2);
			else if (fastcmp(word, "WEATHER"))
				mapheaderinfo[num]->weather = get_precip(word2);
			else if (fastcmp(word, "SKYTEXTURE"))
				deh_strlcpy(mapheaderinfo[num]->skytexture, word2,
					sizeof(mapheaderinfo[num]->skytexture), va("Level header %d: sky texture", num));
			else if (fastcmp(word, "SKYNUM"))
			{
				char namebuf[9];

				sprintf(namebuf, "SKY%.5s", word2);
				deh_strlcpy(mapheaderinfo[num]->skytexture, namebuf,
					sizeof(mapheaderinfo[num]->skytexture), va("Level header %d: sky texture", num));
			}
			else if (fastcmp(word, "PRECUTSCENENUM"))
				mapheaderinfo[num]->precutscenenum = (UINT8)i;
			else if (fastcmp(word, "CUTSCENENUM"))
				mapheaderinfo[num]->cutscenenum = (UINT8)i;
			else if (fastcmp(word, "PALETTE"))
				mapheaderinfo[num]->palette = (UINT16)i;
			else if (fastcmp(word, "ENCOREPAL"))
				mapheaderinfo[num]->encorepal = (UINT16)i;
			else if (fastcmp(word, "NUMLAPS"))
				mapheaderinfo[num]->numlaps = (UINT8)i;
			else if (fastcmp(word, "BASETRACKCOMPLEXITY"))
				mapheaderinfo[num]->base_track_complexity = i;
			else if (fastcmp(word, "TRACKMODIFIERMAX"))
				mapheaderinfo[num]->track_modifier_max = FloatToFixed(atof(word2));
			else if (fastcmp(word, "LAPSPERSECTION"))
				mapheaderinfo[num]->lapspersection = max((UINT8)i, 1u);
			else if (fastcmp(word, "UNLOCKABLE"))
			{
				if (i >= 0 && i <= MAXUNLOCKABLES) // 0 for no unlock required, anything else requires something
					mapheaderinfo[num]->unlockrequired = (SINT8)i - 1;
				else
					deh_warning("Level header %d: invalid unlockable number %d", num, i);
			}
			else if (fastcmp(word, "LEVELSELECT"))
				mapheaderinfo[num]->levelselect = (UINT8)i;
			else if (fastcmp(word, "SKYBOXSCALE"))
				mapheaderinfo[num]->skybox_scalex = mapheaderinfo[num]->skybox_scaley = mapheaderinfo[num]->skybox_scalez = (INT16)i;
			else if (fastcmp(word, "SKYBOXSCALEX"))
				mapheaderinfo[num]->skybox_scalex = (INT16)i;
			else if (fastcmp(word, "SKYBOXSCALEY"))
				mapheaderinfo[num]->skybox_scaley = (INT16)i;
			else if (fastcmp(word, "SKYBOXSCALEZ"))
				mapheaderinfo[num]->skybox_scalez = (INT16)i;
			else if (fastcmp(word, "LEVELFLAGS"))
				mapheaderinfo[num]->levelflags = get_number(word2);
			else if (fastcmp(word, "MENUFLAGS"))
				mapheaderinfo[num]->menuflags = get_number(word2);
			// SRB2Kart
			else if (fastcmp(word, "MOBJSCALE"))
				mapheaderinfo[num]->mobj_scale = get_number(word2);
			else if (fastcmp(word, "DEFAULTWAYPOINTRADIUS"))
				mapheaderinfo[num]->default_waypoint_radius = get_number(word2);
			else if (fastcmp(word, "LIGHTCONTRAST"))
			{
				usemaplighting(num, word)->light_contrast = (UINT8)i;
				usemaplighting(num, word)->use_custom_light = true;
			}
			else if (fastcmp(word, "SPRITEBACKLIGHT") || fastcmp(word, "ENCORESPRITEBACKLIGHT"))
			{
				usemaplighting(num, word)->sprite_backlight = (SINT8)i;
				usemaplighting(num, word)->use_custom_light = true;
			}
			else if (fastcmp(word, "LIGHTANGLE") || fastcmp(word, "ENCORELIGHTANGLE"))
			{
				mapheader_lighting_t *lighting = usemaplighting(num, word);

				if (fastcmp(word2, "EVEN"))
				{
					lighting->use_light_angle = false;
					lighting->light_angle = 0;
				}
				else
				{
					lighting->use_light_angle = true;
					lighting->light_angle = FixedAngle(FloatToFixed(atof(word2)));
				}
				usemaplighting(num, word)->use_custom_light = true;
			}
			// Individual triggers for level flags, for ease of use (and 2.0 compatibility)
			else if (fastcmp(word, "SCRIPTISFILE"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->levelflags |= LF_SCRIPTISFILE;
				else
					mapheaderinfo[num]->levelflags &= ~LF_SCRIPTISFILE;
			}
			else if (fastcmp(word, "NOZONE"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->levelflags |= LF_NOZONE;
				else
					mapheaderinfo[num]->levelflags &= ~LF_NOZONE;
			}
			else if (fastcmp(word, "SECTIONRACE"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->levelflags |= LF_SECTIONRACE;
				else
					mapheaderinfo[num]->levelflags &= ~LF_SECTIONRACE;
			}
			else if (fastcmp(word, "SUBTRACTNUM"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->levelflags |= LF_SUBTRACTNUM;
				else
					mapheaderinfo[num]->levelflags &= ~LF_SUBTRACTNUM;
			}

			// Individual triggers for menu flags
			else if (fastcmp(word, "HIDDEN"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->menuflags |= LF2_HIDEINMENU;
				else
					mapheaderinfo[num]->menuflags &= ~LF2_HIDEINMENU;
			}
			else if (fastcmp(word, "HIDEINSTATS"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->menuflags |= LF2_HIDEINSTATS;
				else
					mapheaderinfo[num]->menuflags &= ~LF2_HIDEINSTATS;
			}
			else if (fastcmp(word, "NOTIMEATTACK") || fastcmp(word, "NORECORDATTACK"))
			{ // RECORDATTACK is an accepted alias
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->menuflags |= LF2_NOTIMEATTACK;
				else
					mapheaderinfo[num]->menuflags &= ~LF2_NOTIMEATTACK;
			}
			else if (fastcmp(word, "VISITNEEDED"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->menuflags |= LF2_VISITNEEDED;
				else
					mapheaderinfo[num]->menuflags &= ~LF2_VISITNEEDED;
			}
			else if (fastcmp(word, "NOVISITNEEDED"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->menuflags &= ~LF2_VISITNEEDED;
				else
					mapheaderinfo[num]->menuflags |= LF2_VISITNEEDED;
			}
			else if (fastcmp(word, "GRAVITY"))
				mapheaderinfo[num]->gravity = FLOAT_TO_FIXED(atof(word2));
			else if (fastcmp(word, "WALLTRANSFER") || fastcmp(word, "WALLRUNNING"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->use_walltransfer = true;
				else
					mapheaderinfo[num]->use_walltransfer = false;
			}
			else if (fastcmp(word, "TERRAIN") || fastcmp(word, "TERRAINDEF"))
			{
				if (i || word2[0] == 'T' || word2[0] == 'Y')
					mapheaderinfo[num]->use_terrain = true;
				else
					mapheaderinfo[num]->use_terrain = false;
			}
			// Global EFX
			else if (fastcmp(word, "GLOBALEFX"))
			{
				mapheaderinfo[num]->globalEFX->type = get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR1"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var1 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR2"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var2 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR3"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var3 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR4"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var4 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR5"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var5 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR6"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var6 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR7"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var7 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR8"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var8 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR9"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var9 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR10"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var10 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR11"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var11 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR12"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var12 = flonum ? flonum : get_number(word2);
			}
			else if (fastcmp(word, "GLOBALEFXVAR13"))
			{
				float flonum = atof(word2);
				mapheaderinfo[num]->globalEFX->var13 = flonum ? flonum : get_number(word2);
			}

			 // ignored for compatibility
			else if (fastcmp(word, "NEXTLEVEL") || fastcmp(word, "TIMEATTACK") || fastcmp(word, "RECORDATTACK"))
				continue;
			else
				deh_warning("Level header %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

static void readcutscenescene(MYFILE *f, INT32 num, INT32 scenenum)
{
	char *s = Z_Calloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	INT32 i;
	UINT16 usi;
	UINT8 picid;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			if (fastcmp(word, "SCENETEXT"))
			{
				char *scenetext = NULL;
				char *buffer;
				const int bufferlen = 4096;

				for (i = 0; i < MAXLINELEN; i++)
				{
					if (s[i] == '=')
					{
						scenetext = &s[i+2];
						break;
					}
				}

				if (!scenetext)
				{
					Z_Free(cutscenes[num]->scene[scenenum].text);
					cutscenes[num]->scene[scenenum].text = NULL;
					continue;
				}

				for (i = 0; i < MAXLINELEN; i++)
				{
					if (s[i] == '\0')
					{
						s[i] = '\n';
						s[i+1] = '\0';
						break;
					}
				}

				buffer = Z_Malloc(4096, PU_STATIC, NULL);
				strcpy(buffer, scenetext);

				strcat(buffer,
					myhashfgets(scenetext, bufferlen
					- strlen(buffer) - 1, f));

				// A cutscene overwriting another one...
				Z_Free(cutscenes[num]->scene[scenenum].text);

				cutscenes[num]->scene[scenenum].text = Z_StrDup(buffer);

				Z_Free(buffer);

				continue;
			}

			word2 = strtok(NULL, " = ");
			if (word2)
				strupr(word2);
			else
				break;

			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';
			i = atoi(word2);
			usi = (UINT16)i;


			if (fastcmp(word, "NUMBEROFPICS"))
			{
				cutscenes[num]->scene[scenenum].numpics = (UINT8)i;
			}
			else if (fastncmp(word, "PIC", 3))
			{
				picid = (UINT8)atoi(word + 3);
				if (picid > 8 || picid == 0)
				{
					deh_warning("CutSceneScene %d: unknown word '%s'", num, word);
					continue;
				}
				--picid;

				if (fastcmp(word+4, "NAME"))
				{
					strncpy(cutscenes[num]->scene[scenenum].picname[picid], word2, 8);
				}
				else if (fastcmp(word+4, "HIRES"))
				{
					cutscenes[num]->scene[scenenum].pichires[picid] = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
				}
				else if (fastcmp(word+4, "DURATION"))
				{
					cutscenes[num]->scene[scenenum].picduration[picid] = usi;
				}
				else if (fastcmp(word+4, "XCOORD"))
				{
					cutscenes[num]->scene[scenenum].xcoord[picid] = usi;
				}
				else if (fastcmp(word+4, "YCOORD"))
				{
					cutscenes[num]->scene[scenenum].ycoord[picid] = usi;
				}
				else
					deh_warning("CutSceneScene %d: unknown word '%s'", num, word);
			}
			else if (fastcmp(word, "MUSIC"))
			{
				strncpy(cutscenes[num]->scene[scenenum].musswitch, word2, 7);
				cutscenes[num]->scene[scenenum].musswitch[6] = 0;
			}
			else if (fastcmp(word, "MUSICTRACK"))
			{
				cutscenes[num]->scene[scenenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
			}
			else if (fastcmp(word, "MUSICPOS"))
			{
				cutscenes[num]->scene[scenenum].musswitchposition = (UINT32)get_number(word2);
			}
			else if (fastcmp(word, "MUSICLOOP"))
			{
				cutscenes[num]->scene[scenenum].musicloop = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
			}
			else if (fastcmp(word, "TEXTXPOS"))
			{
				cutscenes[num]->scene[scenenum].textxpos = usi;
			}
			else if (fastcmp(word, "TEXTYPOS"))
			{
				cutscenes[num]->scene[scenenum].textypos = usi;
			}
			else if (fastcmp(word, "FADEINID"))
			{
				cutscenes[num]->scene[scenenum].fadeinid = (UINT8)i;
			}
			else if (fastcmp(word, "FADEOUTID"))
			{
				cutscenes[num]->scene[scenenum].fadeoutid = (UINT8)i;
			}
			else if (fastcmp(word, "FADECOLOR"))
			{
				cutscenes[num]->scene[scenenum].fadecolor = (UINT8)i;
			}
			else
				deh_warning("CutSceneScene %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

void readcutscene(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	char *tmp;
	INT32 value;

	// Allocate memory for this cutscene if we don't yet have any
	if (!cutscenes[num])
		cutscenes[num] = Z_Calloc(sizeof (cutscene_t), PU_STATIC, NULL);

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			word2 = strtok(NULL, " ");
			if (word2)
				value = atoi(word2);
			else
			{
				deh_warning("No value for token %s", word);
				continue;
			}

			if (fastcmp(word, "NUMSCENES"))
			{
				cutscenes[num]->numscenes = value;
			}
			else if (fastcmp(word, "SCENE"))
			{
				if (1 <= value && value <= 128)
				{
					readcutscenescene(f, num, value - 1);
				}
				else
					deh_warning("Scene number %d out of range (1 - 128)", value);

			}
			else
				deh_warning("Cutscene %d: unknown word '%s', Scene <num> expected.", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

static void readtextpromptpage(MYFILE *f, INT32 num, INT32 pagenum)
{
	char *s = Z_Calloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	INT32 i;
	UINT16 usi;
	UINT8 picid;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			if (fastcmp(word, "PAGETEXT"))
			{
				char *pagetext = NULL;
				char *buffer;
				const int bufferlen = 4096;

				for (i = 0; i < MAXLINELEN; i++)
				{
					if (s[i] == '=')
					{
						pagetext = &s[i+2];
						break;
					}
				}

				if (!pagetext)
				{
					Z_Free(textprompts[num]->page[pagenum].text);
					textprompts[num]->page[pagenum].text = NULL;
					continue;
				}

				for (i = 0; i < MAXLINELEN; i++)
				{
					if (s[i] == '\0')
					{
						s[i] = '\n';
						s[i+1] = '\0';
						break;
					}
				}

				buffer = Z_Malloc(4096, PU_STATIC, NULL);
				strcpy(buffer, pagetext);

				// \todo trim trailing whitespace before the #
				// and also support # at the end of a PAGETEXT with no line break

				strcat(buffer,
					myhashfgets(pagetext, bufferlen
					- strlen(buffer) - 1, f));

				// A text prompt overwriting another one...
				Z_Free(textprompts[num]->page[pagenum].text);

				textprompts[num]->page[pagenum].text = Z_StrDup(buffer);

				Z_Free(buffer);

				continue;
			}

			word2 = strtok(NULL, " = ");
			if (word2)
				strupr(word2);
			else
				break;

			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';
			i = atoi(word2);
			usi = (UINT16)i;

			// copypasta from readcutscenescene
			if (fastcmp(word, "NUMBEROFPICS"))
			{
				textprompts[num]->page[pagenum].numpics = (UINT8)i;
			}
			else if (fastcmp(word, "PICMODE"))
			{
				UINT8 picmode = 0; // PROMPT_PIC_PERSIST
				if (usi == 1 || word2[0] == 'L') picmode = PROMPT_PIC_LOOP;
				else if (usi == 2 || word2[0] == 'D' || word2[0] == 'H') picmode = PROMPT_PIC_DESTROY;
				textprompts[num]->page[pagenum].picmode = picmode;
			}
			else if (fastcmp(word, "PICTOLOOP"))
				textprompts[num]->page[pagenum].pictoloop = (UINT8)i;
			else if (fastcmp(word, "PICTOSTART"))
				textprompts[num]->page[pagenum].pictostart = (UINT8)i;
			else if (fastcmp(word, "PICSMETAPAGE"))
			{
				if (usi && usi <= textprompts[num]->numpages)
				{
					UINT8 metapagenum = usi - 1;

					textprompts[num]->page[pagenum].numpics = textprompts[num]->page[metapagenum].numpics;
					textprompts[num]->page[pagenum].picmode = textprompts[num]->page[metapagenum].picmode;
					textprompts[num]->page[pagenum].pictoloop = textprompts[num]->page[metapagenum].pictoloop;
					textprompts[num]->page[pagenum].pictostart = textprompts[num]->page[metapagenum].pictostart;

					for (picid = 0; picid < MAX_PROMPT_PICS; picid++)
					{
						strncpy(textprompts[num]->page[pagenum].picname[picid], textprompts[num]->page[metapagenum].picname[picid], 8);
						textprompts[num]->page[pagenum].pichires[picid] = textprompts[num]->page[metapagenum].pichires[picid];
						textprompts[num]->page[pagenum].picduration[picid] = textprompts[num]->page[metapagenum].picduration[picid];
						textprompts[num]->page[pagenum].xcoord[picid] = textprompts[num]->page[metapagenum].xcoord[picid];
						textprompts[num]->page[pagenum].ycoord[picid] = textprompts[num]->page[metapagenum].ycoord[picid];
					}
				}
			}
			else if (fastncmp(word, "PIC", 3))
			{
				picid = (UINT8)atoi(word + 3);
				if (picid > MAX_PROMPT_PICS || picid == 0)
				{
					deh_warning("textpromptscene %d: unknown word '%s'", num, word);
					continue;
				}
				--picid;

				if (fastcmp(word+4, "NAME"))
				{
					strncpy(textprompts[num]->page[pagenum].picname[picid], word2, 8);
				}
				else if (fastcmp(word+4, "HIRES"))
				{
					textprompts[num]->page[pagenum].pichires[picid] = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
				}
				else if (fastcmp(word+4, "DURATION"))
				{
					textprompts[num]->page[pagenum].picduration[picid] = usi;
				}
				else if (fastcmp(word+4, "XCOORD"))
				{
					textprompts[num]->page[pagenum].xcoord[picid] = usi;
				}
				else if (fastcmp(word+4, "YCOORD"))
				{
					textprompts[num]->page[pagenum].ycoord[picid] = usi;
				}
				else
					deh_warning("textpromptscene %d: unknown word '%s'", num, word);
			}
			else if (fastcmp(word, "MUSIC"))
			{
				strncpy(textprompts[num]->page[pagenum].musswitch, word2, 7);
				textprompts[num]->page[pagenum].musswitch[6] = 0;
			}
			else if (fastcmp(word, "MUSICTRACK"))
			{
				textprompts[num]->page[pagenum].musswitchflags = ((UINT16)i) & MUSIC_TRACKMASK;
			}
			else if (fastcmp(word, "MUSICLOOP"))
			{
				textprompts[num]->page[pagenum].musicloop = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
			}
			// end copypasta from readcutscenescene
			else if (fastcmp(word, "NAME"))
			{
				if (*word2 != '\0')
				{
					INT32 j;

					// HACK: Add yellow control char now
					// so the drawing function doesn't call it repeatedly
					char name[34];
					name[0] = '\x82'; // color yellow
					name[1] = 0;
					strncat(name, word2, 33);
					name[33] = 0;

					// Replace _ with ' '
					for (j = 0; j < 32 && name[j]; j++)
					{
						if (name[j] == '_')
							name[j] = ' ';
					}

					strncpy(textprompts[num]->page[pagenum].name, name, 32);
				}
				else
					*textprompts[num]->page[pagenum].name = '\0';
			}
			else if (fastcmp(word, "ICON"))
				strncpy(textprompts[num]->page[pagenum].iconname, word2, 8);
			else if (fastcmp(word, "ICONALIGN"))
				textprompts[num]->page[pagenum].rightside = (i || word2[0] == 'R');
			else if (fastcmp(word, "ICONFLIP"))
				textprompts[num]->page[pagenum].iconflip = (i || word2[0] == 'T' || word2[0] == 'Y');
			else if (fastcmp(word, "LINES"))
				textprompts[num]->page[pagenum].lines = usi;
			else if (fastcmp(word, "BACKCOLOR"))
			{
				INT32 backcolor;
				if      (i == 0 || fastcmp(word2, "WHITE")) backcolor = 0;
				else if (i == 1 || fastcmp(word2, "GRAY") || fastcmp(word2, "GREY") ||
					fastcmp(word2, "BLACK")) backcolor = 1;
				else if (i == 2 || fastcmp(word2, "SEPIA")) backcolor = 2;
				else if (i == 3 || fastcmp(word2, "BROWN")) backcolor = 3;
				else if (i == 4 || fastcmp(word2, "PINK")) backcolor = 4;
				else if (i == 5 || fastcmp(word2, "RASPBERRY")) backcolor = 5;
				else if (i == 6 || fastcmp(word2, "RED")) backcolor = 6;
				else if (i == 7 || fastcmp(word2, "CREAMSICLE")) backcolor = 7;
				else if (i == 8 || fastcmp(word2, "ORANGE")) backcolor = 8;
				else if (i == 9 || fastcmp(word2, "GOLD")) backcolor = 9;
				else if (i == 10 || fastcmp(word2, "YELLOW")) backcolor = 10;
				else if (i == 11 || fastcmp(word2, "EMERALD")) backcolor = 11;
				else if (i == 12 || fastcmp(word2, "GREEN")) backcolor = 12;
				else if (i == 13 || fastcmp(word2, "CYAN") || fastcmp(word2, "AQUA")) backcolor = 13;
				else if (i == 14 || fastcmp(word2, "STEEL")) backcolor = 14;
				else if (i == 15 || fastcmp(word2, "PERIWINKLE")) backcolor = 15;
				else if (i == 16 || fastcmp(word2, "BLUE")) backcolor = 16;
				else if (i == 17 || fastcmp(word2, "PURPLE")) backcolor = 17;
				else if (i == 18 || fastcmp(word2, "LAVENDER")) backcolor = 18;
				else if (i >= 256 && i < 512) backcolor = i; // non-transparent palette index
				else if (i < 0) backcolor = INT32_MAX; // CONS_BACKCOLOR user-configured
				else backcolor = 1; // default gray
				textprompts[num]->page[pagenum].backcolor = backcolor;
			}
			else if (fastcmp(word, "ALIGN"))
			{
				UINT8 align = 0; // left
				if (usi == 1 || word2[0] == 'R') align = 1;
				else if (usi == 2 || word2[0] == 'C' || word2[0] == 'M') align = 2;
				textprompts[num]->page[pagenum].align = align;
			}
			else if (fastcmp(word, "VERTICALALIGN"))
			{
				UINT8 align = 0; // top
				if (usi == 1 || word2[0] == 'B') align = 1;
				else if (usi == 2 || word2[0] == 'C' || word2[0] == 'M') align = 2;
				textprompts[num]->page[pagenum].verticalalign = align;
			}
			else if (fastcmp(word, "TEXTSPEED"))
				textprompts[num]->page[pagenum].textspeed = get_number(word2);
			else if (fastcmp(word, "TEXTSFX"))
				textprompts[num]->page[pagenum].textsfx = get_number(word2);
			else if (fastcmp(word, "HIDEHUD"))
			{
				UINT8 hidehud = 0;
				if ((word2[0] == 'F' && (word2[1] == 'A' || !word2[1])) || word2[0] == 'N') hidehud = 0; // false
				else if (usi == 1 || word2[0] == 'T' || word2[0] == 'Y') hidehud = 1; // true (hide appropriate HUD elements)
				else if (usi == 2 || word2[0] == 'A' || (word2[0] == 'F' && word2[1] == 'O')) hidehud = 2; // force (hide all HUD elements)
				textprompts[num]->page[pagenum].hidehud = hidehud;
			}
			else if (fastcmp(word, "METAPAGE"))
			{
				if (usi && usi <= textprompts[num]->numpages)
				{
					UINT8 metapagenum = usi - 1;

					strncpy(textprompts[num]->page[pagenum].name, textprompts[num]->page[metapagenum].name, 32);
					strncpy(textprompts[num]->page[pagenum].iconname, textprompts[num]->page[metapagenum].iconname, 8);
					textprompts[num]->page[pagenum].rightside = textprompts[num]->page[metapagenum].rightside;
					textprompts[num]->page[pagenum].iconflip = textprompts[num]->page[metapagenum].iconflip;
					textprompts[num]->page[pagenum].lines = textprompts[num]->page[metapagenum].lines;
					textprompts[num]->page[pagenum].backcolor = textprompts[num]->page[metapagenum].backcolor;
					textprompts[num]->page[pagenum].align = textprompts[num]->page[metapagenum].align;
					textprompts[num]->page[pagenum].verticalalign = textprompts[num]->page[metapagenum].verticalalign;
					textprompts[num]->page[pagenum].textspeed = textprompts[num]->page[metapagenum].textspeed;
					textprompts[num]->page[pagenum].textsfx = textprompts[num]->page[metapagenum].textsfx;
					textprompts[num]->page[pagenum].hidehud = textprompts[num]->page[metapagenum].hidehud;

					// music: don't copy, else each page change may reset the music
				}
			}
			else if (fastcmp(word, "TAG"))
				strncpy(textprompts[num]->page[pagenum].tag, word2, 33);
			else if (fastcmp(word, "NEXTPROMPT"))
				textprompts[num]->page[pagenum].nextprompt = usi;
			else if (fastcmp(word, "NEXTPAGE"))
				textprompts[num]->page[pagenum].nextpage = usi;
			else if (fastcmp(word, "NEXTTAG"))
				strncpy(textprompts[num]->page[pagenum].nexttag, word2, 33);
			else if (fastcmp(word, "TIMETONEXT"))
				textprompts[num]->page[pagenum].timetonext = get_number(word2);
			else
				deh_warning("PromptPage %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

void readtextprompt(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	char *tmp;
	INT32 value;

	// Allocate memory for this prompt if we don't yet have any
	if (!textprompts[num])
		textprompts[num] = Z_Calloc(sizeof (textprompt_t), PU_STATIC, NULL);

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			word2 = strtok(NULL, " ");
			if (word2)
				value = atoi(word2);
			else
			{
				deh_warning("No value for token %s", word);
				continue;
			}

			if (fastcmp(word, "NUMPAGES"))
			{
				textprompts[num]->numpages = min(max(value, 0), MAX_PAGES);
			}
			else if (fastcmp(word, "PAGE"))
			{
				if (1 <= value && value <= MAX_PAGES)
				{
					textprompts[num]->page[value - 1].backcolor = 1; // default to gray
					textprompts[num]->page[value - 1].hidehud = 1; // hide appropriate HUD elements
					readtextpromptpage(f, num, value - 1);
				}
				else
					deh_warning("Page number %d out of range (1 - %d)", value, MAX_PAGES);

			}
			else
				deh_warning("Prompt %d: unknown word '%s', Page <num> expected.", num, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

// super secret menu cvars... :shushing_face:
static struct { const char *name; consvar_t *var; } HIDDENVARS[] = {
	{ "CHOOSESKIN", &cv_chooseskin },
	{ "DUMMYGPDIFFICULTY", &cv_dummygpdifficulty },
	{ "DUMMYGPENCORE", &cv_dummygpencore },
	{ "NEXTMAP", &cv_nextmap },
	{ "NEXTCUP", &cv_nextcup },
	{ "NEWGAMETYPE", &cv_newgametype },
	{ "DUMMYMENUPLAYER", &cv_dummymenuplayer },
	{ "DUMMYTEAM", &cv_dummyteam },
	{ "DUMMYSPECTATE", &cv_dummyspectate },
	{ "DUMMYSCRAMBLE", &cv_dummyscramble },
	{ "DUMMYSTAFF", &cv_dummystaff },
	{ "DUMMYMULTIPLAYER", &cv_dummymultiplayer },
	{ "DUMMYIP", &cv_dummyip },
	{ "DUMMYNAME", &cv_dummyname },
	{ "DUMMYFOLLOWER", &cv_dummyfollower },
	{ "DUMMYCOLOR", &cv_dummycolor },
	{ "DUMMYSERVERPAGE", &cv_dummyserverpage },
	{ "LEVELSORT", &cv_levelsort },
	{ NULL, NULL }
};

// parses an item style in the form of "<font>-SMALL-<align>-<highlight>-STICKER"
// each component is optional, and the style cannot be written in arbitrary order
// returns IT_HIDDEN if style is invalid
static menuitemflags_t parsestyle(const char *p)
{
	menuitemflags_t style = 0;

	// yeah yeah, macro hell, but it's more economical than 100 entries in a table
#define STYLE(string, flag) \
	(!strncmp(p, string, sizeof(string)-1)) { \
		style |= flag; \
		p += sizeof(string)-1; \
		if (*p == '\0') return style; \
		if (*p++ != '-') return IT_HIDDEN; \
	}

	if STYLE("THIN2", ITF_THIN2)
	else if STYLE("THIN", ITF_THIN)
	else if STYLE("HEADER", ITF_HEADER|ITH_HIGHLIGHT)
	else if STYLE("PATCH", ITF_PATCH)
	else if STYLE("MAPTHUMBNAIL", ITF_THUMBNAIL)
	else if STYLE("FILL", ITF_FILL)

	if STYLE("SMALL", IT_SMALL)

	if STYLE("CENTER", IT_CENTER)
	else if STYLE("RIGHT", IT_RIGHT)

	if STYLE("HIGHLIGHT", ITH_HIGHLIGHT)
	else if STYLE("RECOMMENDEDHIGHLIGHT", ITH_RECOMMEND)
	else if STYLE("WARNINGHIGHLIGHT", ITH_WARNING)

	if STYLE("STICKER", IT_STICKER)

	return IT_HIDDEN;
#undef STYLE
}

#define WARN(str, ...) deh_warning("MenuItem %s: " str, strbuf_get(menunames, menuitem->info.nameofs), __VA_ARGS__)
#define WARN0(str) deh_warning("MenuItem %s: " str, strbuf_get(menunames, menuitem->info.nameofs))
static void readmenuitem(MYFILE *f, menuitem_t *menuitem)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;

	menuitemflags_t status = 0;

	// taking quite possibly the only opportunity i'll ever get
	// to avoid three tabs of indentation...
	do if (myfgets(s, MAXLINELEN, f))
	{
		if (s[0] == '\n')
			break;

		// First remove trailing newline, if there is one
		tmp = strchr(s, '\n');
		if (tmp)
			*tmp = '\0';

		tmp = strchr(s, '#');
		if (tmp)
			*tmp = '\0';
		if (s == tmp)
			continue; // Skip comment lines, but don't break.

		// Get the part before the " = "
		tmp = strchr(s, '=');
		if (tmp)
			*(tmp-1) = '\0';
		else
			break;
		strupr(word);

		// Now get the part after
		word2 = tmp + 2;
		//strupr(word2);

		if (fastcmp(word, "X"))
		{
			menuitem->x = get_number(word2);
		}
		else if (fastcmp(word, "OFSX"))
		{
			menuitem->x = get_number(word2);
			status |= IT_OFSX;
		}
		else if (fastcmp(word, "Y"))
		{
			menuitem->y = get_number(word2);
		}
		else if (fastcmp(word, "OFSY"))
		{
			menuitem->y = get_number(word2);
			status |= IT_OFSY;
		}
		else if (fastcmp(word, "OVERLAY"))
		{
			status |= IT_OVERLAY;
		}
		else if (fastcmp(word, "TEMPOFFSET"))
		{
			status |= IT_TEMPORARY;
		}
		else if (fastcmp(word, "ARGUMENT"))
		{
			menuitem->argument = get_number(word2);
		}
		else if (fastcmp(word, "TEXT"))
		{
			if (menuitem->text)
				Z_Free(menuitem->text);
			menuitem->text = Z_StrDup(word2);
		}
		else if (fastcmp(word, "PATCH"))
		{
			if (menuitem->patch)
				Z_Free(menuitem->patch);
			menuitem->patch = Z_StrDup(word2);
		}
		else if (fastcmp(word, "TOOLTIP"))
		{
			if (menuitem->tooltip)
				Z_Free(menuitem->tooltip);
			menuitem->tooltip = Z_StrDup(word2);
		}
		else if (fastcmp(word, "STYLE"))
		{
			menuitemflags_t style = 0;
			if (status & IT_STYLE)
			{
				WARN0("style already set!");
				continue;
			}
			strupr(word2);
			style = parsestyle(word2);
			if (style == IT_HIDDEN)
				WARN("unknown style '%s'", word2);
			else
				status |= style;
		}
		else if (fastcmp(word, "CVAR") || fastcmp(word, "SLIDER"))
		{
			consvar_t *cvar = CV_FindVar(word2);
			if (!cvar)
				for (size_t i = 0; HIDDENVARS[i].name; i++)
					if (fasticmp(word2, HIDDENVARS[i].name))
					{
						cvar = HIDDENVARS[i].var;
						break;
					}
			if (!cvar)
			{
				WARN("unable to find cvar '%s'", word2);
				continue;
			}
			status |= IT_INTERACT;
			if (word[0] == 'S')
				status |= IT_SLIDER;
			menuitem->cvar = cvar;
		}
		else if (fastcmp(word, "SUBMENU"))
		{
#ifndef HAVE_DISCORDRPC
			{
				const char *testname = word2;
				if (fastncmp("MN_", testname, 3))
					testname += 3;
				if (fastcmp(testname, "OP_DISCORD") || fastcmp(testname, "MISC_DISCORDREQUESTS"))
				{
					continue;
				}
			}
#endif
			menutype_t mn = get_menutype(word2);
			if (mn == MAXMENUTYPES)
			{
				WARN("unknown menu '%s'", word2);
				continue;
			}
			status |= IT_INTERACT;
			menuitem->submenu = mn;
		}
		else if (fastcmp(word, "CALL") || fastcmp(word, "ARROWS"))
		{
			menufunc_f *routine = get_menuroutine(word2);
			if (!routine)
			{
				WARN("unknown call routine '%s'", word2);
				continue;
			}
			status |= IT_INTERACT;
			if (word[0] == 'A')
				status |= IT_ARROWS;
			menuitem->routine = routine;
		}
		else
			WARN("unknown word '%s'", word);
	}
	while (!myfeof(f)); // finish when the line is empty

	if (status)
		menuitem->status = status;
	Z_Free(s);
}
#undef WARN
#undef WARN0

#define WARN(str, ...) deh_warning("Menu %s: " str, DEH_MenutypeName(num), __VA_ARGS__)
#define WARN0(str) deh_warning("Menu %s: " str, DEH_MenutypeName(num))
void readmenu(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	INT32 value;

	menu_t *menudef = &menudefs[num];

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
			{
				// ...or get the word after the space. yay special syntax!
				word2 = strchr(s, ' ');
				if (!word2 || *word2 == '\0' || word2[1] == ' ') // trailing space(s) after MenuItem doesn't count
				{
					WARN0("missing name for menuitem");
					continue;
				}
				*word2++ = '\0';
				strupr(word2);
				strupr(word);

				if (fastcmp(word, "MENUITEM"))
				{
					menuitem_t *item = M_CheckMenuItem(num, word2);
					if (item == NULL)
					{
						menudef->menuitems = Z_Realloc(menudef->menuitems, sizeof(menuitem_t)*(menudef->numitems+1), PU_STATIC, NULL);
						item = menudef->menuitems + menudef->numitems++;
						DEH_Link(word2, &item->info, &menunames);
					}
					readmenuitem(f, item);
				}
				else
					WARN("unknown word '%s'", word);
				continue;
			}
			strupr(word);

			// Now get the part after
			word2 = (tmp += 2);
			strupr(word2);

			value = atoi(word2); // used for numerical settings

			if (fastcmp(word, "BACKGROUNDNAME"))
			{
				strncpy(menudefs[num].bgname, word2, 8);
				titlechanged = true;
			}
			else if (fastcmp(word, "HIDEBACKGROUND"))
			{
				menudefs[num].bghide = (boolean)(value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "BACKGROUNDCOLOR"))
			{
				menudefs[num].bgcolor = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "HIDETITLEPICS") || fastcmp(word, "HIDEPICS") || fastcmp(word, "TITLEPICSHIDE"))
			{
				// true by default, except MM_MAIN
				menudefs[num].hidetitlepics = (boolean)(value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSMODE"))
			{
				if (fastcmp(word2, "USER"))
					menudefs[num].ttmode = TTMODE_USER;
				else if (fastcmp(word2, "HIDE") || fastcmp(word2, "HIDDEN") || fastcmp(word2, "NONE"))
				{
					menudefs[num].ttmode = TTMODE_USER;
					menudefs[num].ttname[0] = 0;
					menudefs[num].hidetitlepics = true;
				}
				else if (fastcmp(word2, "KART"))
					menudefs[num].ttmode = TTMODE_KART;
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSSCALE"))
			{
				// Don't handle Alacroix special case here; see Maincfg section.
				menudefs[num].ttscale = max(1, min(8, (UINT8)get_number(word2)));
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSNAME"))
			{
				strncpy(menudefs[num].ttname, word2, 9);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSX"))
			{
				menudefs[num].ttx = (INT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSY"))
			{
				menudefs[num].tty = (INT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSLOOP"))
			{
				menudefs[num].ttloop = (INT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSTICS"))
			{
				menudefs[num].tttics = (UINT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLESCROLLSPEED") || fastcmp(word, "TITLESCROLLXSPEED")
				|| fastcmp(word, "SCROLLSPEED") || fastcmp(word, "SCROLLXSPEED"))
			{
				menudefs[num].titlescrollxspeed = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLESCROLLYSPEED") || fastcmp(word, "SCROLLYSPEED"))
			{
				menudefs[num].titlescrollyspeed = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "MUSIC"))
			{
				strncpy(menudefs[num].musname, word2, 7);
				menudefs[num].musname[6] = 0;
				titlechanged = true;
			}
			else if (fastcmp(word, "MUSICTRACK"))
			{
				menudefs[num].mustrack = ((UINT16)value - 1);
				titlechanged = true;
			}
			else if (fastcmp(word, "MUSICLOOP"))
			{
				// true by default except MM_MAIN
				menudefs[num].muslooping = (value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "NOMUSIC"))
			{
				menudefs[num].musstop = (value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "IGNOREMUSIC"))
			{
				menudefs[num].musignore = (value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "MUSICCREDITSHOW"))
			{
				menudefs[num].muscreditshow = (value || word2[0] == 'T' || word2[0] == 'Y');
			}
			else if (fastcmp(word, "MUSICCREDITYOFFSET"))
			{
				menudefs[num].muscredity = get_number(word2);
			}
			else if (fastcmp(word, "MUSICCREDITANIMTIME") || fastcmp(word, "MUSICCREDITANIMATIONTIME"))
			{
				menudefs[num].muscreditanimtime = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "MUSICCREDITSNAPFLAGS"))
			{
				menudefs[num].muscreditsnapflags = (INT32)get_number(word2);
			}
			else if (fastcmp(word, "FADESTRENGTH"))
			{
				// one-based, <= 0 means use default value. 1-32
				menudefs[num].fadestrength = (get_number(word2) - 1) % 32;
				titlechanged = true;
			}
			else if (fastcmp(word, "NOENTERBUBBLE"))
			{
				menudefs[num].enterbubble = !(value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "NOEXITBUBBLE"))
			{
				menudefs[num].exitbubble = !(value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "ENTERTAG"))
			{
				menudefs[num].entertag = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "EXITTAG"))
			{
				menudefs[num].exittag = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "ENTERWIPE"))
			{
				menudefs[num].enterwipe = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "EXITWIPE"))
			{
				menudefs[num].exitwipe = get_number(word2);
				titlechanged = true;
			}
			// MENUDEF STARTS HERE
			else if (fastcmp(word, "HEADERPIC"))
			{
				if (menudef->headerpic)
					Z_Free(menudef->headerpic);
				menudef->headerpic = Z_StrDup(word2);
			}
			else if (fastcmp(word, "DRAWROUTINE"))
			{
				menudrawer_f *drawer = get_menudrawer(word2);
				if (drawer == NULL)
				{
					WARN("unknown draw routine '%s'", word2);
					continue;
				}
				menudef->drawroutine = drawer;
			}
			else if (fastcmp(word, "X"))
			{
				menudef->x = get_number(word2);
			}
			else if (fastcmp(word, "Y"))
			{
				menudef->y = get_number(word2);
			}
			else if (fastcmp(word, "SCROLLHEIGHT"))
			{
				menudef->scrollheight = get_number(word2);
			}
			else if (fastcmp(word, "CURSOROFFSET"))
			{
				menudef->cursoroffset = get_number(word2);
			}
			else if (fastcmp(word, "LINEHEIGHT"))
			{
				menudef->lineheight = get_number(word2);
			}
			else if (fastcmp(word, "ENTERROUTINE"))
			{
				menufunc_f *routine = get_menuroutine(word2);
				if (!routine)
				{
					WARN("unknown enter routine '%s'", word2);
					continue;
				}
				menudef->enterroutine = routine;
			}
			else if (fastcmp(word, "QUITROUTINE"))
			{
				menufunc_f *routine = get_menuroutine(word2);
				if (!routine)
				{
					WARN("unknown quit routine '%s'", word2);
					continue;
				}
				menudef->quitroutine = routine;
			}
			else if (fastcmp(word, "KEYHANDLER"))
			{
				menufunc_f *routine = get_menuroutine(word2);
				if (!routine)
				{
					WARN("unknown key handler '%s'", word2);
					continue;
				}
				menudef->keyhandler = routine;
			}
			else
				WARN("unknown word '%s'", word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	// default line height
	if (!menudef->lineheight)
		menudef->lineheight = 8;

	Z_Free(s);
}
#undef WARN
#undef WARN0

void readframe(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word1;
	char *word2 = NULL;
	char *tmp;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			word1 = strtok(s, " ");
			if (word1)
				strupr(word1);
			else
				break;

			word2 = strtok(NULL, " = ");
			if (word2)
				strupr(word2);
			else
				break;
			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';

			if (fastcmp(word1, "SPRITENUMBER") || fastcmp(word1, "SPRITENAME"))
			{
				states[num].sprite = get_sprite(word2);
			}
			else if (fastcmp(word1, "SPRITESUBNUMBER") || fastcmp(word1, "SPRITEFRAME"))
			{
				states[num].frame = (INT32)get_number(word2); // So the FF_ flags get calculated
			}
			else if (fastcmp(word1, "DURATION"))
			{
				states[num].tics = (INT32)get_number(word2); // So TICRATE can be used
			}
			else if (fastcmp(word1, "NEXT"))
			{
				statenum_t state = get_state(word2);
				states[num].nextstate = state == NUMSTATES ? S_NULL : state;
			}
			else if (fastcmp(word1, "VAR1"))
			{
				states[num].var1 = (INT32)get_number(word2);
			}
			else if (fastcmp(word1, "VAR2"))
			{
				states[num].var2 = (INT32)get_number(word2);
			}
			else if (fastcmp(word1, "ACTION"))
			{
				size_t z;
				boolean found = false;
				char actiontocompare[32];

				memset(actiontocompare, 0x00, sizeof(actiontocompare));
				strlcpy(actiontocompare, word2, sizeof (actiontocompare));
				strupr(actiontocompare);

				for (z = 0; z < 32; z++)
				{
					if (actiontocompare[z] == '\n' || actiontocompare[z] == '\r')
					{
						actiontocompare[z] = '\0';
						break;
					}
				}

				for (z = 0; actionpointers[z].name; z++)
				{
					if (actionpointers[z].action == states[num].action)
						break;
				}

				z = 0;
				found = LUA_SetLuaAction(&states[num], actiontocompare);
				if (!found)
				{
					while (actionpointers[z].name)
					{
						if (fastcmp(actiontocompare, actionpointers[z].name))
						{
							states[num].action = actionpointers[z].action;
							found = true;
							break;
						}
						z++;
					}
				}

				if (!found)
					deh_warning("Unknown action %s", actiontocompare);
			}
			else
				deh_warning("Frame %d: unknown word '%s'", num, word1);
		}
	} while (!myfeof(f));

	Z_Free(s);
}

void readsound(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	char *tmp;
	INT32 value;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Set / reset word
			word = s;

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			value = atoi(word2); // used for numerical settings

			if (fastcmp(word, "SINGULAR"))
			{
				S_sfx[num].singularity = value;
			}
			else if (fastcmp(word, "PRIORITY"))
			{
				S_sfx[num].priority = value;
			}
			else if (fastcmp(word, "FLAGS"))
			{
				S_sfx[num].flags = value;
			}
			else if (fastcmp(word, "VOLUME"))
			{
				S_sfx[num].volume = value;
			}
			else if (fastcmp(word, "SKINSOUND"))
			{
				// ignore this for the sake of not needing an asset update
				// this was only exposed in blan anyway
				//S_sfx[num].skinsound = get_number(word2);
			}
			else if (fastcmp(word, "CAPTION") || fastcmp(word, "DESCRIPTION"))
			{
				deh_strlcpy(S_sfx[num].caption, word2,
					sizeof(S_sfx[num].caption), va("Sound effect %d: caption", num));
			}
			else
				deh_warning("Sound %d : unknown word '%s'",num,word);
		}
	} while (!myfeof(f));

	Z_Free(s);
}

/** Checks if a game data file name for a mod is good.
 * "Good" means that it contains only alphanumerics, _, and -;
 * ends in ".dat"; has at least one character before the ".dat";
 * and is not "gamedata.dat" (tested case-insensitively).
 *
 * Assumption: that gamedata.dat is the only .dat file that will
 * ever be treated specially by the game.
 *
 * Note: Check for the tail ".dat" case-insensitively since at
 * present, we get passed the filename in all uppercase.
 *
 * \param s Filename string to check.
 * \return True if the filename is good.
 * \sa readmaincfg()
 * \author Graue <graue@oceanbase.org>
 */
/*static boolean GoodDataFileName(const char *s)
{
	const char *p;
	const char *tail = ".dat";

	for (p = s; *p != '\0'; p++)
		if (!isalnum(*p) && *p != '_' && *p != '-' && *p != '.')
			return false;

	p = s + strlen(s) - strlen(tail);
	if (p <= s) return false; // too short
	if (!fasticmp(p, tail)) return false; // doesn't end in .dat
	if (fasticmp(s, "gamedata.dat")) return false;

	return true;
}*/

void reademblemdata(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	INT32 value;

	memset(&emblemlocations[num-1], 0, sizeof(emblem_t));

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			value = atoi(word2); // used for numerical settings

			// Up here to allow lowercase in hints
			if (fastcmp(word, "HINT"))
			{
				while ((tmp = strchr(word2, '\\')))
					*tmp = '\n';
				deh_strlcpy(emblemlocations[num-1].hint, word2, sizeof (emblemlocations[num-1].hint), va("Emblem %d: hint", num));
				continue;
			}
			strupr(word2);

			if (fastcmp(word, "TYPE"))
			{
				if (fastcmp(word2, "GLOBAL"))
					emblemlocations[num-1].type = ET_GLOBAL;
				else if (fastcmp(word2, "MAP"))
					emblemlocations[num-1].type = ET_MAP;
				else if (fastcmp(word2, "TIME"))
					emblemlocations[num-1].type = ET_TIME;
				else
					emblemlocations[num-1].type = (UINT8)value;
			}
			else if (fastcmp(word, "TAG"))
				emblemlocations[num-1].tag = (INT16)value;
			else if (fastcmp(word, "MAPNAME"))
			{
				emblemlocations[num-1].level = Z_StrDup(word2);
			}
			else if (fastcmp(word, "SPRITE"))
			{
				if (word2[0] >= 'A' && word2[0] <= 'Z')
					value = word2[0];
				else
					value += 'A'-1;

				if (value < 'A' || value > 'Z')
					deh_warning("Emblem %d: sprite must be from A - Z (1 - 26)", num);
				else
					emblemlocations[num-1].sprite = (UINT8)value;
			}
			else if (fastcmp(word, "COLOR"))
				emblemlocations[num-1].color = get_number(word2);
			else if (fastcmp(word, "VAR"))
			{
				Z_Free(emblemlocations[num-1].stringVar);
				emblemlocations[num-1].stringVar = Z_StrDup(word2);

				emblemlocations[num-1].var = get_number(word2);
			}
			else
				deh_warning("Emblem %d: unknown word '%s'", num, word);
		}
	} while (!myfeof(f));

	// Default sprite and color definitions for lazy people like me
	if (!emblemlocations[num-1].sprite) switch (emblemlocations[num-1].type)
	{
		case ET_TIME:
			emblemlocations[num-1].sprite = 'B'; break;
		default:
			emblemlocations[num-1].sprite = 'A'; break;
	}
	if (!emblemlocations[num-1].color) switch (emblemlocations[num-1].type)
	{
		case ET_TIME: //case ET_NTIME:
			emblemlocations[num-1].color = SKINCOLOR_GREY; break;
		default:
			emblemlocations[num-1].color = SKINCOLOR_GOLD; break;
	}

	Z_Free(s);
}

void readextraemblemdata(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	INT32 value;

	memset(&extraemblems[num-1], 0, sizeof(extraemblem_t));

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;

			value = atoi(word2); // used for numerical settings

			if (fastcmp(word, "NAME"))
				deh_strlcpy(extraemblems[num-1].name, word2,
					sizeof (extraemblems[num-1].name), va("Extra emblem %d: name", num));
			else if (fastcmp(word, "OBJECTIVE"))
				deh_strlcpy(extraemblems[num-1].description, word2,
					sizeof (extraemblems[num-1].description), va("Extra emblem %d: objective", num));
			else if (fastcmp(word, "CONDITIONSET"))
				extraemblems[num-1].conditionset = (UINT8)value;
			else if (fastcmp(word, "SHOWCONDITIONSET"))
				extraemblems[num-1].showconditionset = (UINT8)value;
			else
			{
				strupr(word2);
				if (fastcmp(word, "SPRITE"))
				{
					if (word2[0] >= 'A' && word2[0] <= 'Z')
						value = word2[0];
					else
						value += 'A'-1;

					if (value < 'A' || value > 'Z')
						deh_warning("Emblem %d: sprite must be from A - Z (1 - 26)", num);
					else
						extraemblems[num-1].sprite = (UINT8)value;
				}
				else if (fastcmp(word, "COLOR"))
					extraemblems[num-1].color = get_number(word2);
				else
					deh_warning("Extra emblem %d: unknown word '%s'", num, word);
			}
		}
	} while (!myfeof(f));

	if (!extraemblems[num-1].sprite)
		extraemblems[num-1].sprite = 'C';
	if (!extraemblems[num-1].color)
		extraemblems[num-1].color = SKINCOLOR_RED;

	Z_Free(s);
}

void readunlockable(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	INT32 i;

	memset(&unlockables[num], 0, sizeof(unlockable_t));
	unlockables[num].objective[0] = '/';

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;

			i = atoi(word2); // used for numerical settings

			if (fastcmp(word, "NAME"))
				deh_strlcpy(unlockables[num].name, word2,
					sizeof (unlockables[num].name), va("Unlockable %d: name", num));
			else if (fastcmp(word, "OBJECTIVE"))
				deh_strlcpy(unlockables[num].objective, word2,
					sizeof (unlockables[num].objective), va("Unlockable %d: objective", num));
			else
			{
				strupr(word2);

				if (fastcmp(word, "CONDITIONSET"))
					unlockables[num].conditionset = (UINT8)i;
				else if (fastcmp(word, "SHOWCONDITIONSET"))
					unlockables[num].showconditionset = (UINT8)i;
				else if (fastcmp(word, "NOCECHO"))
					unlockables[num].nocecho = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
				else if (fastcmp(word, "NOCHECKLIST"))
					unlockables[num].nochecklist = (UINT8)(i || word2[0] == 'T' || word2[0] == 'Y');
				else if (fastcmp(word, "TYPE"))
				{
					if (fastcmp(word2, "NONE"))
						unlockables[num].type = SECRET_NONE;
					else if (fastcmp(word2, "HEADER"))
						unlockables[num].type = SECRET_HEADER;
					else if (fastcmp(word2, "SKIN"))
						unlockables[num].type = SECRET_SKIN;
					else if (fastcmp(word2, "WARP"))
						unlockables[num].type = SECRET_WARP;
					else if (fastcmp(word2, "LEVELSELECT"))
						unlockables[num].type = SECRET_LEVELSELECT;
					else if (fastcmp(word2, "TIMEATTACK"))
						unlockables[num].type = SECRET_TIMEATTACK;
					else if (fastcmp(word2, "ITEMBREAKER"))
						unlockables[num].type = SECRET_ITEMBREAKER;
					else if (fastcmp(word2, "SOUNDTEST"))
						unlockables[num].type = SECRET_SOUNDTEST;
					else if (fastcmp(word2, "CREDITS"))
						unlockables[num].type = SECRET_CREDITS;
					else if (fastcmp(word2, "ITEMFINDER"))
						unlockables[num].type = SECRET_ITEMFINDER;
					else if (fastcmp(word2, "EMBLEMHINTS"))
						unlockables[num].type = SECRET_EMBLEMHINTS;
					else if (fastcmp(word2, "ENCORE"))
						unlockables[num].type = SECRET_ENCORE;
					else if (fastcmp(word2, "EXPERTSPEED"))
						unlockables[num].type = SECRET_EXPERTSPEED;
					else if (fastcmp(word2, "HELLATTACK"))
						unlockables[num].type = SECRET_HELLATTACK;
					else if (fastcmp(word2, "PANDORA"))
						unlockables[num].type = SECRET_PANDORA;
					else
						unlockables[num].type = (INT16)i;
				}
				else if (fastcmp(word, "VAR"))
				{
					Z_Free(unlockables[num].stringVar);
					unlockables[num].stringVar = Z_StrDup(word2);
					unlockables[num].variable = (INT16)G_MapNumber(word2);
				}
				else
					deh_warning("Unlockable %d: unknown word '%s'", num+1, word);
			}
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

static void readcondition(UINT8 set, UINT32 id, char *word2)
{
	INT32 i;
	char *params[4]; // condition, requirement, extra info, extra info
	char *spos;

	conditiontype_t ty;
	INT32 re;
	INT16 x1 = 0, x2 = 0;

	INT32 offset = 0;

	spos = strtok(word2, " ");

	for (i = 0; i < 4; ++i)
	{
		if (spos != NULL)
		{
			params[i] = spos;
			spos = strtok(NULL, " ");
		}
		else
			params[i] = NULL;
	}

	if (!params[0])
	{
		deh_warning("condition line is empty for condition ID %d", id);
		return;
	}

	if				(fastcmp(params[0], "PLAYTIME")
	|| (++offset && fastcmp(params[0], "MATCHESPLAYED")))
	{
		PARAMCHECK(1);
		ty = UC_PLAYTIME + offset;
		re = atoi(params[1]);
	}
	else if (fastcmp(params[0], "POWERLEVEL"))
	{
		PARAMCHECK(2);
		ty = UC_POWERLEVEL;
		re = atoi(params[1]);
		x1 = atoi(params[2]);

		if (x1 < 0 || x1 >= PWRLV_NUMTYPES)
		{
			deh_warning("Power level type %d out of range (0 - %d) for condition ID %d", x1, PWRLV_NUMTYPES-1, id);
			return;
		}
	}
	else if (fastcmp(params[0], "GAMECLEAR"))
	{
		ty = UC_GAMECLEAR;
		re = (params[1]) ? atoi(params[1]) : 1;
	}
	else if (fastcmp(params[0], "OVERALLTIME"))
	{
		PARAMCHECK(1);
		ty = UC_OVERALLTIME;
		re = atoi(params[1]);
	}
	else if ((offset=0) || fastcmp(params[0], "MAPVISITED")
	||        (++offset && fastcmp(params[0], "MAPBEATEN")))
	{
		PARAMCHECK(1);
		ty = UC_MAPVISITED + offset;
		re = G_MapNumber(params[1]);

		if (re >= nummapheaders)
		{
			deh_warning("Invalid level %s for condition ID %d", params[1], id);
			return;
		}
	}
	else if (fastcmp(params[0], "MAPTIME"))
	{
		PARAMCHECK(2);
		ty = UC_MAPTIME;
		re = atoi(params[2]);
		x1 = G_MapNumber(params[1]);

		if (x1 >= nummapheaders)
		{
			deh_warning("Invalid level %s for condition ID %d", params[1], id);
			return;
		}
	}
	else if (fastcmp(params[0], "TRIGGER"))
	{
		PARAMCHECK(1);
		ty = UC_TRIGGER;
		re = atoi(params[1]);

		// constrained by 32 bits
		if (re < 0 || re > 31)
		{
			deh_warning("Trigger ID %d out of range (0 - 31) for condition ID %d", re, id);
			return;
		}
	}
	else if (fastcmp(params[0], "TOTALEMBLEMS"))
	{
		PARAMCHECK(1);
		ty = UC_TOTALEMBLEMS;
		re = atoi(params[1]);
	}
	else if (fastcmp(params[0], "EMBLEM"))
	{
		PARAMCHECK(1);
		ty = UC_EMBLEM;
		re = atoi(params[1]);

		if (re <= 0 || re > MAXEMBLEMS)
		{
			deh_warning("Emblem %d out of range (1 - %d) for condition ID %d", re, MAXEMBLEMS, id);
			return;
		}
	}
	else if (fastcmp(params[0], "EXTRAEMBLEM"))
	{
		PARAMCHECK(1);
		ty = UC_EXTRAEMBLEM;
		re = atoi(params[1]);

		if (re <= 0 || re > MAXEXTRAEMBLEMS)
		{
			deh_warning("Extra emblem %d out of range (1 - %d) for condition ID %d", re, MAXEXTRAEMBLEMS, id);
			return;
		}
	}
	else if (fastcmp(params[0], "CONDITIONSET"))
	{
		PARAMCHECK(1);
		ty = UC_CONDITIONSET;
		re = atoi(params[1]);

		if (re <= 0 || re > MAXCONDITIONSETS)
		{
			deh_warning("Condition set %d out of range (1 - %d) for condition ID %d", re, MAXCONDITIONSETS, id);
			return;
		}
	}
	else
	{
		deh_warning("Invalid condition name %s for condition ID %d", params[0], id);
		return;
	}

	M_AddRawCondition(set, (UINT8)id, ty, re, x1, x2);
}

void readconditionset(MYFILE *f, UINT8 setnum)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	UINT8 id;
	UINT8 previd = 0;

	M_ClearConditionSet(setnum);

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			strupr(word2);

			if (fastncmp(word, "CONDITION", 9))
			{
				id = (UINT8)atoi(word + 9);
				if (id == 0)
				{
					deh_warning("Condition set %d: unknown word '%s'", setnum, word);
					continue;
				}
				else if (previd > id)
				{
					// out of order conditions can cause problems, so enforce proper order
					deh_warning("Condition set %d: conditions are out of order, ignoring this line", setnum);
					continue;
				}
				previd = id;

				readcondition(setnum, id, word2);
			}
			else
				deh_warning("Condition set %d: unknown word '%s'", setnum, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

void readmaincfg(MYFILE *f)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	INT32 value;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			strupr(word2);

			value = atoi(word2); // used for numerical settings

			if (fastcmp(word, "EXECCFG"))
			{
				if (strchr(word2, '.'))
					COM_BufAddText(va("exec \"%s\" -immediate\n", word2));
				else
				{
					lumpnum_t lumpnum;
					char newname[9];

					strncpy(newname, word2, 8);

					newname[8] = '\0';

					lumpnum = W_CheckNumForName(newname);

					if (lumpnum == LUMPERROR || W_LumpLength(lumpnum) == 0)
						CONS_Debug(DBG_SETUP, "SOC Error: script lump %s not found/not valid.\n", newname);
					else
						COM_BufInsertText(W_CacheLumpNum(lumpnum, PU_CACHE));
				}
			}
			else if (fastcmp(word, "REDTEAM"))
			{
				skincolor_redteam = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "BLUETEAM"))
			{
				skincolor_blueteam = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "REDRING"))
			{
				skincolor_redring = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "BLUERING"))
			{
				skincolor_bluering = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "INVULNTICS"))
			{
				invulntics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "SNEAKERTICS"))
			{
				sneakertics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "FLASHINGTICS"))
			{
				flashingtics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "TAILSFLYTICS"))
			{
				tailsflytics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "UNDERWATERTICS"))
			{
				underwatertics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "SPACETIMETICS"))
			{
				spacetimetics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "EXTRALIFETICS"))
			{
				extralifetics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "NIGHTSLINKTICS"))
			{
				nightslinktics = (UINT16)get_number(word2);
			}
			else if (fastcmp(word, "GAMEOVERTICS"))
			{
				gameovertics = get_number(word2);
			}
			else if (fastcmp(word, "AMMOREMOVALTICS"))
			{
				ammoremovaltics = get_number(word2);
			}
			// BlanKart: Easy ways to set these for balance purposes
			else if (fastcmp(word, "HYUDOROTIME"))
			{
				hyudorotime = get_number(word2);
			}
			else if (fastcmp(word, "STEALTIME"))
			{
				stealtime = get_number(word2);
			}
			else if (fastcmp(word, "WATERPANELTIME"))
			{
				waterpaneltime = get_number(word2);
			}
			else if (fastcmp(word, "SNEAKERTIME"))
			{
				sneakertime = get_number(word2);
			}
			else if (fastcmp(word, "BUBBLETIME"))
			{
				bubbletime = get_number(word2);
			}
			else if (fastcmp(word, "FLAMETIME"))
			{
				flametime = get_number(word2);
			}
			else if (fastcmp(word, "COMEBACKTIME"))
			{
				comebacktime = get_number(word2);
			}
			else if (fastcmp(word, "GREASETICS"))
			{
				greasetics = get_number(word2);
			}
			else if (fastcmp(word, "WIPEOUTSLOWTIME"))
			{
				wipeoutslowtime = get_number(word2);
			}
			else if (fastcmp(word, "WANTEDREDUCE"))
			{
				wantedreduce = get_number(word2);
			}
			else if (fastcmp(word, "WANTEDFREQUENCY"))
			{
				wantedfrequency = get_number(word2);
			}
			else if (fastcmp(word, "INTROTOPLAY"))
			{
				introtoplay = (UINT8)get_number(word2);
				// range check, you morons.
				if (introtoplay > 128)
					introtoplay = 128;
				introchanged = true;
			}
			else if (fastcmp(word, "CREDITSCUTSCENE"))
			{
				creditscutscene = (UINT8)get_number(word2);
				// range check, you morons.
				if (creditscutscene > 128)
					creditscutscene = 128;
			}
			else if (fastcmp(word, "LOOPTITLE"))
			{
				looptitle = (value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEMAP"))
			{
				titlemap = Z_StrDup(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "HIDETITLEPICS") || fastcmp(word, "TITLEPICSHIDE"))
			{
				hidetitlepics = (boolean)(value || word2[0] == 'T' || word2[0] == 'Y');
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSMODE"))
			{
				if (fastcmp(word2, "USER"))
					ttmode = TTMODE_USER;
				else if (fastcmp(word2, "HIDE") || fastcmp(word2, "HIDDEN") || fastcmp(word2, "NONE"))
				{
					ttmode = TTMODE_USER;
					ttname[0] = 0;
					hidetitlepics = true;
				}
				else if (fastcmp(word2, "KART"))
					ttmode = TTMODE_KART;
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSNAME"))
			{
				strncpy(ttname, word2, 9);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSX"))
			{
				ttx = (INT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSY"))
			{
				tty = (INT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSLOOP"))
			{
				ttloop = (INT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLEPICSTICS"))
			{
				tttics = (UINT16)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLESCROLLSPEED") || fastcmp(word, "TITLESCROLLXSPEED"))
			{
				titlescrollxspeed = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "TITLESCROLLYSPEED"))
			{
				titlescrollyspeed = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "NUMDEMOS"))
			{
				numDemos = (UINT8)get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "DEMODELAYTIME"))
			{
				demoDelayTime = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "DEMOIDLETIME"))
			{
				demoIdleTime = get_number(word2);
				titlechanged = true;
			}
			else if (fastcmp(word, "USE1UPSOUND"))
			{
				use1upSound = (UINT8)(value || word2[0] == 'T' || word2[0] == 'Y');
			}
			else if (fastcmp(word, "MAXXTRALIFE"))
			{
				maxXtraLife = (UINT8)get_number(word2);
			}

			else if (fastcmp(word, "GAMEDATA"))
			{
				// just ignore it but dont throw a warning
				/*size_t filenamelen;

				// Check the data filename so that mods
				// can't write arbitrary files.
				if (!GoodDataFileName(word2))
					I_Error("Maincfg: bad data file name '%s'\n", word2);

				G_SaveGameData();
				strlcpy(gamedatafilename, word2, sizeof (gamedatafilename));
				strlwr(gamedatafilename);
				savemoddata = true;
				majormods = false;

				// Also save a time attack folder
				filenamelen = strlen(gamedatafilename)-4;  // Strip off the extension
				filenamelen = min(filenamelen, sizeof (timeattackfolder));
				strncpy(timeattackfolder, gamedatafilename, filenamelen);
				timeattackfolder[min(filenamelen, sizeof (timeattackfolder) - 1)] = '\0';

				strcpy(savegamename, timeattackfolder);
				strlcat(savegamename, "%u.ssg", sizeof(savegamename));
				// can't use sprintf since there is %u in savegamename
				strcatbf(savegamename, srb2home, PATHSEP);

				strcpy(liveeventbackup, va("live%s.bkp", timeattackfolder));
				strcatbf(liveeventbackup, srb2home, PATHSEP);

				refreshdirmenu |= REFRESHDIR_GAMEDATA;
				gamedataadded = true;
				titlechanged = true;*/
			}
			else if (fastcmp(word, "RESETDATA"))
			{
				P_ResetData(value);
				titlechanged = true;
			}
			else if (fastcmp(word, "CUSTOMVERSION"))
			{
				strlcpy(customversionstring, word2, sizeof (customversionstring));
				//titlechanged = true;
			}
			else if (fastcmp(word, "BOOTMAP"))
			{
				bootmap = Z_StrDup(word2);
				//titlechanged = true;
			}
			else if (fastcmp(word, "TUTORIALMAP"))
			{
				tutorialmap = Z_StrDup(word2);
			}
			else
				deh_warning("Maincfg: unknown word '%s'", word);
		}
	} while (!myfeof(f));

	Z_Free(s);
}

void readwipes(MYFILE *f)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *pword;
	char *word2;
	char *tmp;
	INT32 value;
	INT32 wipeoffset;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			value = atoi(word2); // used for numerical settings

			if (value < -1 || value > 99)
			{
				deh_warning("Wipes: bad value '%s'", word2);
				continue;
			}
			else if (value == -1)
				value = UINT8_MAX;

			// error catching
			wipeoffset = -1;

			if (fastncmp(word, "LEVEL_", 6))
			{
				pword = word + 6;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_level_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_level_final;
			}
			else if (fastncmp(word, "INTERMISSION_", 13))
			{
				pword = word + 13;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_intermission_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_intermission_final;
			}
			else if (fastncmp(word, "VOTING_", 7))
			{
				pword = word + 7;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_voting_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_voting_final;
			}
			else if (fastncmp(word, "TITLESCREEN_", 12))
			{
				pword = word + 12;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_titlescreen_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_titlescreen_final;
			}
			else if (fastncmp(word, "TIMEATTACK_", 11))
			{
				pword = word + 11;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_timeattack_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_timeattack_final;
			}
			else if (fastncmp(word, "CREDITS_", 8))
			{
				pword = word + 8;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_credits_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_credits_final;
				else if (fastcmp(pword, "INTERMEDIATE"))
					wipeoffset = wipe_credits_intermediate;
			}
			else if (fastncmp(word, "EVALUATION_", 11))
			{
				pword = word + 11;
				if (fastcmp(pword, "TOBLACK"))
					wipeoffset = wipe_evaluation_toblack;
				else if (fastcmp(pword, "FINAL"))
					wipeoffset = wipe_evaluation_final;
			}
			else if (fastncmp(word, "ENCORE_", 7))
			{
				pword = word + 7;
				if (fastcmp(pword, "TOINVERT"))
					wipeoffset = wipe_encore_toinvert;
				else if (fastcmp(pword, "TOWHITE"))
					wipeoffset = wipe_encore_towhite;
			}

			if (wipeoffset < 0)
			{
				deh_warning("Wipes: unknown word '%s'", word);
				continue;
			}

			if (value == UINT8_MAX
			 && (wipeoffset <= wipe_level_toblack || wipeoffset >= wipe_encore_toinvert))
			{
				 // Cannot disable non-toblack wipes
				 // (or the level toblack wipe, or the special encore wipe)
				deh_warning("Wipes: can't disable wipe of type '%s'", word);
				continue;
			}

			wipedefs[wipeoffset] = (UINT8)value;
		}
	} while (!myfeof(f));

	Z_Free(s);
}

//
// SRB2KART
//

void readcupheader(MYFILE *f, cupheader_t *cup)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	char *tmp;
	INT32 i;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Set / reset word, because some things (Lua.) move it
			word = s;

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;
			i = atoi(word2); // used for numerical settings
			strupr(word2);

			if (fastcmp(word, "MONITOR"))
			{
				if (i > 0 && i < 10)
					cup->monitor = i;
				else if (!word2[0] || word2[1] != '\0' || word2[0] == '0')
					deh_warning("%s Cup: Invalid monitor type \"%s\" (should be 1-9 or A-Z)\n", cup->name, word2);
				else
					cup->monitor = (word2[0] - 'A') + 10;
			}
			else if (fastcmp(word, "ICON"))
			{
				deh_strlcpy(cup->icon, word2,
					sizeof(cup->icon), va("%s Cup: icon", cup->name));
			}
			else if (fastcmp(word, "LEVELLIST"))
			{
				while (cup->numlevels > 0)
				{
					cup->numlevels--;
					Z_Free(cup->levellist[cup->numlevels]);
					cup->levellist[cup->numlevels] = NULL;
					if (cup->cachedlevels[cup->numlevels] == NEXTMAP_INVALID)
						continue;
					mapheaderinfo[cup->cachedlevels[cup->numlevels]]->cup = NULL;
				}

				tmp = strtok(word2,",");
				do {
					if (cup->numlevels >= MAXLEVELLIST)
					{
						deh_warning("%s Cup: reached max levellist (%d)\n", cup->name, MAXLEVELLIST);
						break;
					}

					cup->levellist[cup->numlevels] = Z_StrDup(tmp);
					cup->cachedlevels[cup->numlevels] = NEXTMAP_INVALID;
					cup->numlevels++;
				} while((tmp = strtok(NULL,",")) != NULL);
			}
			else if (fastcmp(word, "BONUSGAME"))
			{
				Z_Free(cup->levellist[CUPCACHE_BONUS]);
				cup->levellist[CUPCACHE_BONUS] = Z_StrDup(word2);
				cup->cachedlevels[CUPCACHE_BONUS] = NEXTMAP_INVALID;
			}
			else if (fastcmp(word, "SPECIALSTAGE"))
			{
				Z_Free(cup->levellist[CUPCACHE_SPECIAL]);
				cup->levellist[CUPCACHE_SPECIAL] = Z_StrDup(word2);
				cup->cachedlevels[CUPCACHE_SPECIAL] = NEXTMAP_INVALID;
			}
			else if (fastcmp(word, "EMERALDNUM"))
			{
				if (i >= 0 && i <= 14)
					cup->emeraldnum = (UINT8)i;
				else
					deh_warning("%s Cup: invalid emerald number %d", cup->name, i);
			}
			else if (fastcmp(word, "UNLOCKABLE"))
			{
				if (i >= 0 && i <= MAXUNLOCKABLES) // 0 for no unlock required, anything else requires something
					cup->unlockrequired = (SINT8)i - 1;
				else
					deh_warning("%s Cup: invalid unlockable number %d", cup->name, i);
			}
			else
				deh_warning("%s Cup: unknown word '%s'", cup->name, word);
		}
	} while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}

void readfollower(MYFILE *f)
{
	char *s;
	char *word, *word2, dname[SKINNAMESIZE+1];
	char *tmp;
	char testname[SKINNAMESIZE+1];

	boolean nameset;
	INT32 fallbackstate = 0;
	INT32 res;
	INT32 i;

	if (numfollowers > MAXFOLLOWERS)
	{
		deh_warning("Error: Too many followers, cannot add anymore.\n");
		return;
	}

	s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);

	// Ready the default variables for followers. We will overwrite them as we go! We won't set the name or states RIGHT HERE as this is handled down instead.
	followers[numfollowers].mode = FOLLOWERMODE_FLOAT;
	followers[numfollowers].scale = FRACUNIT;
	followers[numfollowers].bubblescale = 0; // No bubble by default
	followers[numfollowers].atangle = FixedAngle(230 * FRACUNIT);
	followers[numfollowers].dist = 32*FRACUNIT; // changed from 16 to 32 to better account for ogl models
	followers[numfollowers].height = 16*FRACUNIT;
	followers[numfollowers].zoffs = 32*FRACUNIT;
	followers[numfollowers].horzlag = 3*FRACUNIT;
	followers[numfollowers].vertlag = 6*FRACUNIT;
	followers[numfollowers].anglelag = 8*FRACUNIT;
	followers[numfollowers].bobspeed = TICRATE*2;
	followers[numfollowers].bobamp = 4*FRACUNIT;
	followers[numfollowers].hitconfirmtime = TICRATE;
	followers[numfollowers].horntime = TICRATE;
	followers[numfollowers].defaultcolor = FOLLOWERCOLOR_MATCH;
	followers[numfollowers].hornsound = sfx_horn00;
	strcpy(followers[numfollowers].icon, "MISSING");

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			word = strtok(s, " ");
			if (word)
				strupr(word);
			else
				break;

			word2 = strtok(NULL, " = ");

			if (!word2)
				break;

			if (word2[strlen(word2)-1] == '\n')
				word2[strlen(word2)-1] = '\0';

			if (fastcmp(word, "NAME"))
			{
				strlcpy(followers[numfollowers].name, word2, SKINNAMESIZE+1);
				nameset = true;
			}
			else if (fastcmp(word, "ICON"))
			{
				strlcpy(followers[numfollowers].icon, word2, 8+1);
				nameset = true;
			}
			else if (fastcmp(word, "MODE"))
			{
				if (word2)
					strupr(word2);

				if (fastcmp(word2, "FLOAT") || fastcmp(word2, "DEFAULT"))
					followers[numfollowers].mode = FOLLOWERMODE_FLOAT;
				else if (fastcmp(word2, "GROUND"))
					followers[numfollowers].mode = FOLLOWERMODE_GROUND;
				else
					deh_warning("Follower %d: unknown follower mode '%s'", numfollowers, word2);
			}
			else if (fastcmp(word, "DEFAULTCOLOR"))
			{
				if (word2)
					strupr(word2);

				if (fastcmp(word2, "MATCH"))
					followers[numfollowers].defaultcolor = FOLLOWERCOLOR_MATCH;
				else if (fastcmp(word2, "OPPOSITE"))
					followers[numfollowers].defaultcolor = FOLLOWERCOLOR_OPPOSITE;
				else
				{
					skincolornum_t color = get_skincolor(word2);
					followers[numfollowers].defaultcolor = color == MAXSKINCOLORS ? SKINCOLOR_GREEN : color;
				}
			}
			else if (fastcmp(word, "HORNSOUND"))
			{
				followers[numfollowers].hornsound = get_number(word2);
			}
			else if (fastcmp(word, "SCALE"))
			{
				followers[numfollowers].scale = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "BUBBLESCALE"))
			{
				followers[numfollowers].bubblescale = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "ATANGLE"))
			{
				followers[numfollowers].atangle = (angle_t)(get_number(word2) * ANG1);
			}
			else if (fastcmp(word, "HORZLAG"))
			{
				followers[numfollowers].horzlag = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "VERTLAG"))
			{
				followers[numfollowers].vertlag = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "ANGLELAG"))
			{
				followers[numfollowers].anglelag = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "BOBSPEED"))
			{
				followers[numfollowers].bobspeed = (tic_t)get_number(word2);
			}
			else if (fastcmp(word, "BOBAMP"))
			{
				followers[numfollowers].bobamp = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "ZOFFSET") || (fastcmp(word, "ZOFFS")))
			{
				followers[numfollowers].zoffs = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "DISTANCE") || (fastcmp(word, "DIST")))
			{
				followers[numfollowers].dist = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "HEIGHT"))
			{
				followers[numfollowers].height = (fixed_t)get_number(word2);
			}
			else if (fastcmp(word, "IDLESTATE"))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].idlestate = get_number(word2);
				fallbackstate = followers[numfollowers].idlestate;
			}
			else if (fastcmp(word, "FOLLOWSTATE"))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].followstate = get_number(word2);
			}
			else if (fastcmp(word, "HURTSTATE"))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].hurtstate = get_number(word2);
			}
			else if (fastcmp(word, "LOSESTATE"))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].losestate = get_number(word2);
			}
			else if (fastcmp(word, "WINSTATE"))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].winstate = get_number(word2);
			}
			else if (fastcmp(word, "HITSTATE") || (fastcmp(word, "HITCONFIRMSTATE")))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].hitconfirmstate = get_number(word2);
			}
			else if (fastcmp(word, "HITTIME") || (fastcmp(word, "HITCONFIRMTIME")))
			{
				followers[numfollowers].hitconfirmtime = (tic_t)get_number(word2);
			}
			else if (fastcmp(word, "HORNSTATE"))
			{
				if (word2)
					strupr(word2);
				followers[numfollowers].hornstate = get_number(word2);
			}
			else if (fastcmp(word, "HORNTIME"))
			{
				followers[numfollowers].horntime = (tic_t)get_number(word2);
			}
			else
			{
				deh_warning("Follower %d: unknown word '%s'", numfollowers, word);
			}
		}
	} while (!myfeof(f)); // finish when the line is empty

	if (!nameset)
	{
		// well this is problematic.
		strlcpy(followers[numfollowers].name, va("Follower%d", numfollowers), SKINNAMESIZE+1); // this is lazy, so what
	}

	// set skin name (this is just the follower's name in lowercases):
	// but before we do, let's... actually check if another follower isn't doing the same shit...

	strlcpy(testname, followers[numfollowers].name, SKINNAMESIZE+1);

	// lower testname for skin checks...
	strlwr(testname);
	res = K_FollowerAvailable(testname);
	if (res > -1)	// yikes, someone else has stolen our name already
	{
		INT32 startlen = strlen(testname);
		char cpy[2];
		//deh_warning("There was already a follower with the same name. (%s)", testname);	This warning probably isn't necessary anymore?
		sprintf(cpy, "%d", numfollowers);
		memcpy(&testname[startlen], cpy, 2);
		// in that case, we'll be very lazy and copy numfollowers to the end of our skin name.
	}

	strlcpy(followers[numfollowers].skinname, testname, SKINNAMESIZE+1);
	strlcpy(dname, followers[numfollowers].skinname, SKINNAMESIZE+1);	// display name, just used for printing succesful stuff or errors later down the line.

	// now that the skin name is ready, post process the actual name to turn the underscores into spaces!
	for (i = 0; followers[numfollowers].name[i]; i++)
	{
		if (followers[numfollowers].name[i] == '_')
			followers[numfollowers].name[i] = ' ';
	}

	// fallbacks for variables
	// Print a warning if the variable is on a weird value and set it back to the minimum available if that's the case.

	if (followers[numfollowers].mode < FOLLOWERMODE_FLOAT || followers[numfollowers].mode >= FOLLOWERMODE__MAX)
	{
		followers[numfollowers].mode = FOLLOWERMODE_FLOAT;
		deh_warning("Follower '%s': Value for 'mode' should be between %d and %d.", dname, FOLLOWERMODE_FLOAT, FOLLOWERMODE__MAX-1);
	}

#define FALLBACK(field, field2, threshold, set) \
if ((signed)followers[numfollowers].field < threshold) \
{ \
	followers[numfollowers].field = set; \
	deh_warning("Follower '%s': Value for '%s' is too low! Minimum should be %d. Value was overwritten to %d.", dname, field2, threshold, set); \
} \

	FALLBACK(dist, "DIST", 0, 0);
	FALLBACK(height, "HEIGHT", 1, 1);
	FALLBACK(zoffs, "ZOFFS", 0, 0);
	FALLBACK(horzlag, "HORZLAG", FRACUNIT, FRACUNIT);
	FALLBACK(vertlag, "VERTLAG", FRACUNIT, FRACUNIT);
	FALLBACK(anglelag, "ANGLELAG", FRACUNIT, FRACUNIT);
	FALLBACK(bobamp, "BOBAMP", 0, 0);
	FALLBACK(bobspeed, "BOBSPEED", 0, 0);
	FALLBACK(hitconfirmtime, "HITCONFIRMTIME", 1, 1);
	FALLBACK(horntime, "HORNTIME", 1, 1);
	FALLBACK(scale, "SCALE", 1, 1);				// No null/negative scale
	FALLBACK(bubblescale, "BUBBLESCALE", 0, 0);	// No negative scale

#undef FALLBACK

	// Special case for color I suppose
	if (followers[numfollowers].defaultcolor > (unsigned)(numskincolors-1)
	 && followers[numfollowers].defaultcolor != FOLLOWERCOLOR_MATCH
	 && followers[numfollowers].defaultcolor != FOLLOWERCOLOR_OPPOSITE)
	{
		followers[numfollowers].defaultcolor = FOLLOWERCOLOR_MATCH;
		deh_warning("Follower \'%s\': Value for 'color' should be between 1 and %d.\n", dname, numskincolors-1);
	}

	// also check if we forgot states. If we did, we will set any missing state to the follower's idlestate.
	// Print a warning in case we don't have a fallback and set the state to S_INVISIBLE (rather than S_NULL) if unavailable.

#define NOSTATE(field, field2) \
if (!followers[numfollowers].field) \
{ \
	followers[numfollowers].field = fallbackstate ? fallbackstate : S_INVISIBLE; \
	if (!fallbackstate) \
		deh_warning("Follower '%s' is missing state definition for '%s', no idlestate fallback was found", dname, field2); \
} \

	NOSTATE(idlestate, "IDLESTATE");
	NOSTATE(followstate, "FOLLOWSTATE");
	NOSTATE(hurtstate, "HURTSTATE");
	NOSTATE(losestate, "LOSESTATE");
	NOSTATE(winstate, "WINSTATE");
	NOSTATE(hitconfirmstate, "HITCONFIRMSTATE");
	NOSTATE(hornstate, "HORNSTATE");
#undef NOSTATE

	CONS_Printf("Added follower '%s'\n", dname);
	numfollowers++; // add 1 follower
	Z_Free(s);
}

void readweather(MYFILE *f, INT32 num)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word;
	char *word2;
	char *tmp;

	do
	{
		if (myfgets(s, MAXLINELEN, f))
		{
			if (s[0] == '\n')
				break;

			// First remove trailing newline, if there is one
			tmp = strchr(s, '\n');
			if (tmp)
				*tmp = '\0';

			tmp = strchr(s, '#');
			if (tmp)
				*tmp = '\0';
			if (s == tmp)
				continue; // Skip comment lines, but don't break.

			// Set / reset word
			word = s;

			// Get the part before the " = "
			tmp = strchr(s, '=');
			if (tmp)
				*(tmp-1) = '\0';
			else
				break;
			strupr(word);

			// Now get the part after
			word2 = tmp + 2;

			if (fastcmp(word, "TYPE"))
			{
				mobjtype_t mt = get_mobjtype(word2);
				precipprops[num].type = mt == NUMMOBJTYPES ? MT_NULL : mt;
			}
			else if (fastcmp(word, "EFFECTS"))
			{
				precipprops[num].effects = get_number(word2);
			}
			else
				deh_warning("Weather %d : unknown word '%s'", num, word);
		}
	} while (!myfeof(f));

	Z_Free(s);
}

#define WARN(str, ...) deh_warning("KartItem %s: " str, strbuf_get(kartitemnames, item->info.nameofs), __VA_ARGS__)
#define WARN0(str) deh_warning("KartItem %s: " str, strbuf_get(kartitemnames, item->info.nameofs))
void readkartitem(MYFILE *f, kartitem_t *item)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;
	INT32 i;

	do if (myfgets(s, MAXLINELEN, f))
	{
		if (s[0] == '\n')
			break;

		// First remove trailing newline, if there is one
		tmp = strchr(s, '\n');
		if (tmp)
			*tmp = '\0';

		tmp = strchr(s, '#');
		if (tmp)
			*tmp = '\0';
		if (s == tmp)
			continue; // Skip comment lines, but don't break.

		// Get the part before the " = "
		tmp = strchr(s, '=');
		if (tmp)
			*(tmp-1) = '\0';
		else
			break;
		strupr(word);

		// Now get the part after
		word2 = (tmp += 2);
		strupr(word2);

		if (fastcmp(word, "BIGPATCHES") || fastcmp(word, "SMALLPATCHES") ||
		    fastcmp(word, "BIGALTPATCHES") || fastcmp(word, "SMALLALTPATCHES"))
		{
			UINT8 patch_idx = (word[0] == 'B') ? 0 : 1;

			// Alt. patches are stored in the same array, just in their own slots.
			patch_idx += (word[3 + (patch_idx << 1)] == 'A') ? 2 : 0;

			kartitemgraphics_t* graphics = &item->graphics[patch_idx];

			if (graphics == &item->graphics[0] && item->spritedef.numframes > 0)
			{
				Z_Free(item->spritedef.spriteframes);
				memset(&item->spritedef, 0, sizeof(item->spritedef));
			}
			for (i = 0; i < graphics->numpatches; i++)
			{
				Z_Free(graphics->patchnames[i]);
				Patch_Free(graphics->patches[i]);
				graphics->patches[i] = NULL;
			}

			tmp = strtok(word2, ",");
			for (graphics->numpatches = 0; graphics->numpatches < MAXITEMPATCHES;)
			{
				graphics->patchnames[graphics->numpatches++] = Z_StrDup(tmp);

				if ((tmp = strtok(NULL, ",")) == NULL)
					break;
			}
		}
		else if (fastcmp(word, "FLAGS") || fastcmp(word, "FLAGSALT"))
		{
			item->flags[word[5] == 'A' ? 1 : 0] = get_number(word2);
		}
		else if (fastcmp(word, "EQUIP") || fastcmp(word, "EQUIPALT"))
		{
			item->equipstyle[word[5] == 'A' ? 1 : 0] = get_number(word2);
		}
		else
			WARN("unknown word '%s'", word);
	}
	while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}
#undef WARN
#undef WARN0

#define WARN(str, ...) deh_warning("KartResult %s: " str, result->cvar->name, __VA_ARGS__)
#define WARN0(str) deh_warning("KartResult %s: " str, result->cvar->name)
static CV_PossibleValue_t kartrunneraugment_cons_t[] = {{0, "MIN"}, {30000, "MAX"}, {0, NULL}};

void readkartresult(MYFILE *f, kartresult_t *result)
{
	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
	char *word = s;
	char *word2;
	char *tmp;

	do if (myfgets(s, MAXLINELEN, f))
	{
		if (s[0] == '\n')
			break;

		// First remove trailing newline, if there is one
		tmp = strchr(s, '\n');
		if (tmp)
			*tmp = '\0';

		tmp = strchr(s, '#');
		if (tmp)
			*tmp = '\0';
		if (s == tmp)
			continue; // Skip comment lines, but don't break.

		// Get the part before the " = "
		tmp = strchr(s, '=');
		if (tmp)
			*(tmp-1) = '\0';
		else
			break;
		strupr(word);

		// Now get the part after
		word2 = tmp + 2;
		//strupr(word2);

		if (fastcmp(word, "TYPE"))
		{
			if (result->isalt)
			{
				WARN0("can't set type for alternate result");
				continue;
			}
			result->type = get_kartitem(word2);
		}
		else if (fastcmp(word, "AMOUNT"))
		{
			if (result->isalt)
			{
				WARN0("can't set amount for alternate result");
				continue;
			}
			result->amount = get_number(word2);
		}
		else if (fastcmp(word, "ISALT"))
		{
			result->isalt = (boolean)(atoi(word2) || word2[0] == 'T' || word2[0] == 'Y');
		}
		else if (fastcmp(word, "FLAGS"))
		{
			result->flags = get_number(word2);
		}
		else if (fastncmp(word, "RUNNERDISTANCE", 14))
		{
			const UINT8 idx = (fastcmp(word+14, "LEGACY") ? 1 : 0);

			result->baserunneraug[idx] = get_number(word2);

			if (result->augcvar[idx] == NULL)
			{
				char *cvname;

				UINT16 distprefixlen = strlen(result->isalt ? "runnerdistalt" : "runnerdist");

				if (idx)
				{
					cvname = malloc(strlen(result->cvar->name) + 9 + distprefixlen);
					sprintf(cvname, "%s_%s_legacy", result->isalt ? "runnerdistalt" : "runnerdist", result->cvar->name);
					strlwr(cvname);
				}
				else
				{
					cvname = malloc(strlen(result->cvar->name) + 2 + distprefixlen);
					sprintf(cvname, "%s_%s", result->isalt ? "runnerdistalt" : "runnerdist", result->cvar->name);
					strlwr(cvname);
				}

				consvar_t *var = Z_Calloc(sizeof(consvar_t), PU_STATIC, &result->augcvar[idx]);
				var->name = cvname;

				char *defval = malloc(5+1); // UINT16_MAX + NUL
				snprintf(defval, 6, "%u", result->baserunneraug[idx]);
				var->defaultvalue = defval;

				var->flags = CV_NETVAR|CV_CHEAT;
				var->PossibleValue = kartrunneraugment_cons_t;
				var->func = NULL;
				CV_RegisterVar(var);
			}
			else
			{
				char *defval; // uh oh! const cast!
				memcpy(&defval, &result->augcvar[idx]->defaultvalue, sizeof(defval));
				snprintf(defval, 6, "%u", result->baserunneraug[idx]);
				CV_StealthSet(result->augcvar[idx], result->augcvar[idx]->defaultvalue);
			}
		}
		else if (fastncmp(word, "ODDS", 4))
		{
			UINT8 oddstable;
			if (fastcmp(word+4, "RACE"))
				oddstable = ODDS_RACE;
			else if (fastcmp(word+4, "BATTLE"))
				oddstable = ODDS_BATTLE;
			else if (fastcmp(word+4, "SPECIAL"))
				oddstable = ODDS_SPECIAL;
			else
			{
				WARN("unknown word '%s'", word);
				continue;
			}

			UINT8 *odds = result->odds[oddstable];
			memset(odds, 0, sizeof(*odds)*MAXODDS);
			tmp = strtok(word2,",");
			for (UINT8 i = 0; i <= MAXODDS; i++)
			{
				if (i == oddstablemax[oddstable])
				{
					WARN("odds table %s is limited to %d odds", word+4, oddstablemax[oddstable]);
					continue;
				}
				odds[i] = atoi(tmp);
				if ((tmp = strtok(NULL,",")) == NULL)
					break;
			}
		}
		else if (fastncmp(word, "UNIQUEODDS", 10))
		{
			UINT8 oddstable;
			if (fastcmp(word+10, "RACE"))
				oddstable = ODDS_RACE;
			else if (fastcmp(word+10, "BATTLE"))
				oddstable = ODDS_BATTLE;
			else if (fastcmp(word+10, "SPECIAL"))
				oddstable = ODDS_SPECIAL;

			else
			{
				WARN("unknown word '%s'", word);
				continue;
			}

			result->unique_odds[oddstable] = get_useoddsfunc(word2);
		}
		else if (fastcmp(word, "COOLDOWNTIME"))
		{
			result->basecooldown = atoi(word2) * TICRATE;
		}
		else if (fastcmp(word, "DISPLAYNAME"))
		{
			result->displayname = Z_StrDup(word2);
		}
		else
			WARN("unknown word '%s'", word);
	}
	while (!myfeof(f)); // finish when the line is empty

	Z_Free(s);
}
#undef WARN
#undef WARN0

//
//
//

mobjtype_t get_mobjtype(const char *word)
{ // Returns the value of MT_ enumerations
	mobjtype_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("MT_",word,3))
		word += 3; // take off the MT_
	i = DEH_FindMobjtype(word);
	if (i == NUMMOBJTYPES)
		deh_warning("Couldn't find mobjtype named 'MT_%s'", word);
	return i;
}

statenum_t get_state(const char *word)
{ // Returns the value of S_ enumerations
	statenum_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("S_",word,2))
		word += 2; // take off the S_
	i = DEH_FindState(word);
	if (i == NUMSTATES)
		deh_warning("Couldn't find state named 'S_%s'", word);
	return i;
}

skincolornum_t get_skincolor(const char *word)
{ // Returns the value of SKINCOLOR_ enumerations
	skincolornum_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("SKINCOLOR_",word,10))
		word += 10; // take off the SKINCOLOR_
	i = DEH_FindSkincolor(word);
	if (i == MAXSKINCOLORS)
		deh_warning("Couldn't find skincolor named 'SKINCOLOR_%s'", word);
	return i;
}

spritenum_t get_sprite(const char *word)
{ // Returns the value of SPR_ enumerations
	spritenum_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("SPR_",word,4))
		word += 4; // take off the SPR_
	for (i = 0; i < NUMSPRITES; i++)
		if (memcmp(word,sprnames[i],4)==0)
			return i;
	deh_warning("Couldn't find sprite named 'SPR_%s'",word);
	return SPR_NULL;
}

playersprite_t get_sprite2(const char *word)
{ // Returns the value of SPR2_ enumerations
	playersprite_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("SPR2_",word,5))
		word += 5; // take off the SPR2_
	for (i = 0; i < NUMPLAYERSPRITES; i++)
		if (!spr2names[i][4] && memcmp(word,spr2names[i],4)==0)
			return i;
	deh_warning("Couldn't find sprite named 'SPR2_%s'",word);
	return SPR2_STIN;
}

sfxenum_t get_sfx(const char *word)
{ // Returns the value of SFX_ enumerations
	sfxenum_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("SFX_",word,4))
		word += 4; // take off the SFX_
	else if (fastncmp("DS",word,2))
		word += 2; // take off the DS
	for (i = 0; i < NUMSFX; i++)
		if (S_sfx[i].name && fasticmp(word, S_sfx[i].name))
			return i;
	deh_warning("Couldn't find sfx named 'SFX_%s'",word);
	return NUMSFX;
}

menutype_t get_menutype(const char *word)
{ // Returns the value of MN_ enumerations
	menutype_t i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("MN_",word,3))
		word += 3; // take off the MN_
	i = DEH_FindMenutype(word);
	if (i == MAXMENUTYPES)
		deh_warning("Couldn't find menutype named 'MN_%s'", word);
	return i;
}

menufunc_f *get_menuroutine(const char *word)
{ // Returns the value of MR_ enumerations
	size_t i;
	if (fastncmp("MR_",word,3))
		word += 3; // take off the MR_
	for (i = 0; MENU_ROUTINES[i].name; i++)
		if (fasticmp(word, MENU_ROUTINES[i].name))
			return MENU_ROUTINES[i].routine;
	deh_warning("Couldn't find menu routine named 'MR_%s'",word);
	return NULL;
}

menudrawer_f *get_menudrawer(const char *word)
{ // Returns the value of MD_ enumerations
	size_t i;
	if (fastncmp("MD_",word,3))
		word += 3; // take off the MD_
	for (i = 0; MENU_DRAWERS[i].name; i++)
		if (fasticmp(word, MENU_DRAWERS[i].name))
			return MENU_DRAWERS[i].drawer;
	deh_warning("Couldn't find menu drawer named 'MD_%s'",word);
	return NULL;
}

/*static INT16 get_gametype(const char *word)
{ // Returns the value of GT_ enumerations
	INT16 i;
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("GT_",word,3))
		word += 3; // take off the GT_
	for (i = 0; i < MAXGAMETYPES; i++)
		if (fastcmp(word, Gametype_ConstantNames[i]+3))
			return i;
	deh_warning("Couldn't find gametype named 'GT_%s'",word);
	return GT_COOP;
}*/

preciptype_t get_precip(const char *word)
{
	// Returns the value of PRECIP_ enumerations
	if (*word >= '0' && *word <= '9')
		return atoi(word);
	if (fastncmp("PRECIP_",word,7))
	{
		fixed_t eval = LUA_EvalMath(word);

		if (eval)
			return eval;

		if (fasticmp("PRECIP_NONE",word))
			return PRECIP_NONE;
	}
	deh_warning("Couldn't find weather type named 'PRECIP_%s'",word);
	return PRECIP_NONE;
}

kartitemtype_e get_kartitem(const char *word)
{ // Returns the value of KITEM_ enumerations
	kartitemtype_e i;
	if (fastncmp("KITEM_",word,6))
		word += 6; // take off the KITEM_
	i = DEH_FindKartItem(word);
	if (i == MAXKARTITEMS)
		deh_warning("Couldn't find kartitem named 'KITEM_%s'", word);
	return i;
}

useoddsfunc_f *get_useoddsfunc(const char *word)
{ // Returns the value of KO_ enumerations
	size_t i;
	if (fastncmp("KO_",word,3))
		word += 3; // take off the KO_
	for (i = 0; USEODDS_FUNCS[i].name; i++)
		if (fasticmp(word, USEODDS_FUNCS[i].name))
			return USEODDS_FUNCS[i].func;
	deh_warning("Couldn't find odds function named 'KO_%s'",word);
	return NULL;
}

/// \todo Make ANY of this completely over-the-top math craziness obey the order of operations.
static fixed_t op_mul(fixed_t a, fixed_t b) { return a*b; }
static fixed_t op_div(fixed_t a, fixed_t b) { return a/b; }
static fixed_t op_add(fixed_t a, fixed_t b) { return a+b; }
static fixed_t op_sub(fixed_t a, fixed_t b) { return a-b; }
static fixed_t op_or(fixed_t a, fixed_t b) { return a|b; }
static fixed_t op_and(fixed_t a, fixed_t b) { return a&b; }
static fixed_t op_lshift(fixed_t a, fixed_t b) { return a<<b; }
static fixed_t op_rshift(fixed_t a, fixed_t b) { return a>>b; }

struct {
	const char c;
	fixed_t (*v)(fixed_t,fixed_t);
} OPERATIONS[] = {
	{'*',op_mul},
	{'/',op_div},
	{'+',op_add},
	{'-',op_sub},
	{'|',op_or},
	{'&',op_and},
	{'<',op_lshift},
	{'>',op_rshift},
	{0,NULL}
};

// Returns the full word, cut at the first symbol or whitespace
/*static char *read_word(const char *line)
{
	// Part 1: You got the start of the word, now find the end.
  const char *p;
	INT32 i;
	for (p = line+1; *p; p++) {
		if (*p == ' ' || *p == '\t')
			break;
		for (i = 0; OPERATIONS[i].c; i++)
			if (*p == OPERATIONS[i].c) {
				i = -1;
				break;
			}
		if (i == -1)
			break;
	}

	// Part 2: Make a copy of the word and return it.
	{
		size_t len = (p-line);
		char *word = malloc(len+1);
		memcpy(word,line,len);
		word[len] = '\0';
		return word;
	}
}

static INT32 operation_pad(const char **word)
{ // Brings word the next operation and returns the operation number.
	INT32 i;
	for (; **word; (*word)++) {
		if (**word == ' ' || **word == '\t')
			continue;
		for (i = 0; OPERATIONS[i].c; i++)
			if (**word == OPERATIONS[i].c)
			{
				if ((**word == '<' && *(*word+1) == '<') || (**word == '>' && *(*word+1) == '>')) (*word)++; // These operations are two characters long.
				else if (**word == '<' || **word == '>') continue; // ... do not accept one character long.
				(*word)++;
				return i;
			}
		deh_warning("Unknown operation '%c'",**word);
		return -1;
	}
	return -1;
}

static void const_warning(const char *type, const char *word)
{
	deh_warning("Couldn't find %s named '%s'",type,word);
}

static fixed_t find_const(const char **rword)
{ // Finds the value of constants and returns it, bringing word to the next operation.
	INT32 i;
	fixed_t r;
	char *word = read_word(*rword);
	*rword += strlen(word);
	if ((*word >= '0' && *word <= '9') || *word == '-') { // Parse a number
		r = atoi(word);
		free(word);
		return r;
	}
	if (!*(word+1) && // Turn a single A-z symbol into numbers, like sprite frames.
	 ((*word >= 'A' && *word <= 'Z') || (*word >= 'a' && *word <= 'z'))) {
		r = R_Char2Frame(*word);
		free(word);
		return r;
	}
	if (fastncmp("MF_", word, 3)) {
		char *p = word+3;
		for (i = 0; MOBJFLAG_LIST[i]; i++)
			if (fastcmp(p, MOBJFLAG_LIST[i])) {
				free(word);
				return (1<<i);
			}

		// Not found error
		const_warning("mobj flag",word);
		free(word);
		return 0;
	}
	else if (fastncmp("MF2_", word, 4)) {
		char *p = word+4;
		for (i = 0; MOBJFLAG2_LIST[i]; i++)
			if (fastcmp(p, MOBJFLAG2_LIST[i])) {
				free(word);
				return (1<<i);
			}

		// Not found error
		const_warning("mobj flag2",word);
		free(word);
		return 0;
	}
	else if (fastncmp("MFE_", word, 4)) {
		char *p = word+4;
		for (i = 0; MOBJEFLAG_LIST[i]; i++)
			if (fastcmp(p, MOBJEFLAG_LIST[i])) {
				free(word);
				return (1<<i);
			}

		// Not found error
		const_warning("mobj eflag",word);
		free(word);
		return 0;
	}
	else if (fastncmp("PF_", word, 3)) {
		char *p = word+3;
		for (i = 0; PLAYERFLAG_LIST[i]; i++)
			if (fastcmp(p, PLAYERFLAG_LIST[i])) {
				free(word);
				return (1<<i);
			}
		if (fastcmp(p, "FULLSTASIS"))
			return PF_FULLSTASIS;

		// Not found error
		const_warning("player flag",word);
		free(word);
		return 0;
	}
	else if (fastncmp("S_",word,2)) {
		r = get_state(word);
		free(word);
		return r;
	}
	else if (fastncmp("SKINCOLOR_",word,10)) {
		r = get_skincolor(word);
		free(word);
		return r;
	}
	else if (fastncmp("MT_",word,3)) {
		r = get_mobjtype(word);
		free(word);
		return r;
	}
	else if (fastncmp("SPR_",word,4)) {
		r = get_sprite(word);
		free(word);
		return r;
	}
	else if (fastncmp("SFX_",word,4) || fastncmp("DS",word,2)) {
		r = get_sfx(word);
		free(word);
		return r;
	}
	else if (fastncmp("PW_",word,3)) {
		r = get_power(word);
		free(word);
		return r;
	}
	else if (fastncmp("MN_",word,3)) {
		r = get_menutype(word);
		free(word);
		return r;
	}
	else if (fastncmp("GT_",word,3)) {
		r = get_gametype(word);
		free(word);
		return r;
	}
	else if (fastncmp("GTR_", word, 4)) {
		char *p = word+4;
		for (i = 0; GAMETYPERULE_LIST[i]; i++)
			if (fastcmp(p, GAMETYPERULE_LIST[i])) {
				free(word);
				return (1<<i);
			}

		// Not found error
		const_warning("game type rule",word);
		free(word);
		return 0;
	}
	else if (fastncmp("TOL_", word, 4)) {
		char *p = word+4;
		for (i = 0; TYPEOFLEVEL[i].name; i++)
			if (fastcmp(p, TYPEOFLEVEL[i].name)) {
				free(word);
				return TYPEOFLEVEL[i].flag;
			}

		// Not found error
		const_warning("typeoflevel",word);
		free(word);
		return 0;
	}
	for (i = 0; INT_CONST[i].n; i++)
		if (fastcmp(word,INT_CONST[i].n)) {
			free(word);
			return INT_CONST[i].v;
		}

	if fastcmp(word,"MAXSKINCOLORS")
	{
		free(word);
		return MAXSKINCOLORS;
	}

	// Not found error.
	const_warning("constant",word);
	free(word);
	return 0;
}*/
