/*
** g_level.cpp
** Parses MAPINFO
**
**---------------------------------------------------------------------------
** Copyright 1998-2006 Randy Heit
** Copyright 2009 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

#include <assert.h>

#include "g_level.h"
#include "filesystem.h"
#include "cmdlib.h"
#include "v_video.h"
#include "p_lnspec.h"
#include "p_setup.h"
#include "gi.h"
#include "gstrings.h"
#include "p_acs.h"
#include "doomstat.h"
#include "d_player.h"
#include "autosegs.h"
#include "g_levellocals.h"
#include "events.h"
#include "i_system.h"
#include "screenjob.h"

static TArray<cluster_info_t> wadclusterinfos;
TArray<level_info_t> wadlevelinfos;

level_info_t TheDefaultLevelInfo;
static cluster_info_t TheDefaultClusterInfo;

TArray<FEpisode> AllEpisodes;

extern TMap<int, FString> HexenMusic;

TArray<int> ParsedLumps(8);

//==========================================================================
//
//
//==========================================================================

static int FindWadLevelInfo (const char *name)
{
	for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
	{
		if (!wadlevelinfos[i].MapName.CompareNoCase(name))
			return i;
	}
	return -1;
}

//==========================================================================
//
//
//==========================================================================

level_info_t *FindLevelInfo (const char *mapname, bool allowdefault)
{
	int i;

	if ((i = FindWadLevelInfo (mapname)) > -1)
	{
		return &wadlevelinfos[i];
	}
	else if (allowdefault)
	{
		if (TheDefaultLevelInfo.LevelName.IsEmpty())
		{
			TheDefaultLevelInfo.SkyPic2 = TheDefaultLevelInfo.SkyPic1 = "SKY1";
			TheDefaultLevelInfo.LevelName = "Unnamed";
		}
		return &TheDefaultLevelInfo;
	}
	return NULL;
}

//==========================================================================
//
//
//==========================================================================

level_info_t *FindLevelByNum (int num)
{
	for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
		if (wadlevelinfos[i].levelnum == num)
			return &wadlevelinfos[i];

	return NULL;
}


//==========================================================================
//
//
//==========================================================================

static level_info_t *FindLevelByWarpTrans (int num)
{
	for (unsigned i = wadlevelinfos.Size(); i-- != 0; )
		if (wadlevelinfos[i].WarpTrans == num)
			return &wadlevelinfos[i];

	return NULL;
}

//==========================================================================
//
//
//==========================================================================

bool CheckWarpTransMap (FString &mapname, bool substitute)
{
	if (mapname[0] == '&' && (mapname[1] & 0xDF) == 'W' &&
		(mapname[2] & 0xDF) == 'T' && mapname[3] == '@')
	{
		level_info_t *lev = FindLevelByWarpTrans (atoi (&mapname[4]));
		if (lev != NULL)
		{
			mapname = lev->MapName;
			return true;
		}
		else if (substitute)
		{
			char a = mapname[4], b = mapname[5];
			mapname = "MAP";
			mapname << a << b;
		}
	}
	return false;
}

//==========================================================================
//
//
//==========================================================================

bool SecretLevelVisited()
{
	for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
		if ((wadlevelinfos[i].flags3 & LEVEL3_SECRET) && (wadlevelinfos[i].flags & LEVEL_VISITED))
			return true;

	return false;
}

//==========================================================================
//
//
//==========================================================================

static int FindWadClusterInfo (int cluster)
{
	for (unsigned int i = 0; i < wadclusterinfos.Size(); i++)
		if (wadclusterinfos[i].cluster == cluster)
			return i;
		
	return -1;
}

//==========================================================================
//
//
//==========================================================================

cluster_info_t *FindClusterInfo (int cluster)
{
	int i;

	if ((i = FindWadClusterInfo (cluster)) > -1)
		return &wadclusterinfos[i];
	else
		return &TheDefaultClusterInfo;
}

//==========================================================================
//
//
//==========================================================================

void G_ClearSnapshots (void)
{
	for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
	{
		wadlevelinfos[i].Snapshot.Clean();
	}

	// Clear current levels' snapshots just in case they are not defined via MAPINFO,
	// so they were not handled by the loop above
	if (primaryLevel && primaryLevel->info)
		primaryLevel->info->Snapshot.Clean();
	if (currentVMLevel && currentVMLevel->info)
		currentVMLevel->info->Snapshot.Clean();

	// Since strings are only locked when snapshotting a level, unlock them
	// all now, since we got rid of all the snapshots that cared about them.
	GlobalACSStrings.UnlockAll();
}

//==========================================================================
//
// Remove any existing defereds
//
//==========================================================================

void P_RemoveDefereds (void)
{
	for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
	{
		wadlevelinfos[i].ClearDefered();
	}
}


//==========================================================================
//
//
//==========================================================================

void level_info_t::Reset()
{
	MapName = "";
	MapBackground = "";
	levelnum = 0;
	PName = "";
	NextMap = "";
	NextSecretMap = "";
	SkyPic1 = SkyPic2 = "-NOFLAT-";
	cluster = 0;
	partime = 0;
	sucktime = 0;
	flags = 0;
	if (gameinfo.gametype == GAME_Hexen)
		flags2 = 0;
	else
		flags2 = LEVEL2_LAXMONSTERACTIVATION;
	flags3 = 0;
	LightningSound = "world/thunder";
	Music = "";
	LevelName = "";
	AuthorName = "";
	MapLabel = "";
	FadeTable = "COLORMAP";
	CustomColorMap = "COLORMAP";
	WallHorizLight = -8;
	WallVertLight = +8;
	F1Pic = "";
	musicorder = 0;
	Snapshot = { 0,0,0,0,0,nullptr };
	deferred.Clear();
	skyspeed1 = skyspeed2 = 0.f;
	fadeto = 0;
	outsidefog = 0xff000000;
	cdtrack = 0;
	cdid = 0;
	gravity = 0.f;
	aircontrol = 0.f;
	WarpTrans = 0;
	airsupply = 20;
	compatflags = compatflags2 = 0;
	compatmask = compatmask2 = 0;
	Translator = "";
	RedirectType = NAME_None;
	RedirectMapName = "";
	RedirectCVAR = NAME_None;
	RedirectCVARMapName = "";
	EnterPic = "";
	ExitPic = "";
	EnterAnim = "";
	ExitAnim = "";
	Intermission = NAME_None;
	deathsequence = NAME_None;
	slideshow = NAME_None;
	InterMusic = "";
	intermusicorder = 0;
	SoundInfo = "";
	SndSeq = "";
	BorderTexture = "";
	teamdamage = 0.f;
	hazardcolor = 0xff004200;
	hazardflash = 0xff00ff00;
	fogdensity = 0;
	outsidefogdensity = 0;
	skyfog = 0;
	pixelstretch = 1.2f;

	specialactions.Clear();
	DefaultEnvironment = 0;
	PrecacheSounds.Clear();

	brightfog = -1;
	lightmode = ELightMode::NotSet;
	notexturefill = -1;
	lightadditivesurfaces = -1;
	skyrotatevector = FVector3(0, 0, 1);
	skyrotatevector2 = FVector3(0, 0, 1);
	lightblendmode = ELightBlendMode::DEFAULT;
	tonemap = ETonemapMode::None;
}


//==========================================================================
//
//
//==========================================================================

FString level_info_t::LookupLevelName(uint32_t *langtable)
{
	// All IWAD names that may be substituted by a graphics patch are declared as language strings.
	if (langtable) *langtable = 0;
	if (flags & LEVEL_LOOKUPLEVELNAME)
	{
		const char *thename;
		const char *lookedup = GStrings.CheckString(LevelName.GetChars(), langtable);
		if (lookedup == NULL)
		{
			thename = LevelName.GetChars();
		}
		else
		{
			char checkstring[32];

			// Strip out the header from the localized string
			if (MapName.Len() > 3 && MapName[0] == 'E' && MapName[2] == 'M')
			{
				mysnprintf (checkstring, countof(checkstring), "%s: ", MapName.GetChars());
			}
			else if (MapName.Len() > 3 && MapName[0] == 'M' && MapName[1] == 'A' && MapName[2] == 'P')
			{
				mysnprintf (checkstring, countof(checkstring), "%d: ", atoi(&MapName[3]));
			}
			else if (MapName.Len() > 5 && MapName[0] == 'L' && MapName[1] == 'E' && MapName[2] == 'V' && MapName[3] == 'E' && MapName[4] == 'L')
			{
				mysnprintf (checkstring, countof(checkstring), "%d: ", atoi(&MapName[5]));
			}
			else
			{
				// make sure nothing is stripped.
				checkstring[0] = '\0';
			}
			thename = strstr (lookedup, checkstring);
			if (thename == NULL)
			{
				thename = lookedup;
			}
			else
			{
				thename += strlen (checkstring);
			}
		}
		return thename;
	}
	else return LevelName;
}


//==========================================================================
//
//
//==========================================================================

level_info_t *level_info_t::CheckLevelRedirect ()
{
	if (RedirectType != NAME_None)
	{
		PClassActor *type = PClass::FindActor(RedirectType);
		if (type != NULL)
		{
			for (int i = 0; i < MAXPLAYERS; ++i)
			{
				if (playeringame[i] && players[i].mo->FindInventory(type))
				{
					// check for actual presence of the map.
					if (P_CheckMapData(RedirectMapName.GetChars()))
					{
						return FindLevelInfo(RedirectMapName.GetChars());
					}
					break;
				}
			}
		}
	}
	if (RedirectCVAR != NAME_None)
	{
		auto var = FindCVar(RedirectCVAR.GetChars(), NULL);
		if (var && (var->GetRealType() == CVAR_Bool) && !(var->GetFlags() & CVAR_IGNORE))	// only check Bool cvars that are currently defined
		{
			if (var->GetFlags() & CVAR_USERINFO)
			{
				// user sync'd cvar, check for all players
				for (int i = 0; i < MAXPLAYERS; ++i)
				{
					if (playeringame[i] && (var = GetCVar(i, RedirectCVAR.GetChars())))
					{
						if (var->ToInt())
							if (P_CheckMapData(RedirectCVARMapName.GetChars()))
								return FindLevelInfo(RedirectCVARMapName.GetChars());
					}
				}
			}
			else if (var->ToInt())
				if (P_CheckMapData(RedirectCVARMapName.GetChars()))
					return FindLevelInfo(RedirectCVARMapName.GetChars());
		}
	}
	return NULL;
}

//==========================================================================
//
//
//==========================================================================

bool level_info_t::isValid()
{
	return MapName.Len() != 0 || this == &TheDefaultLevelInfo;
}

//==========================================================================
//
//
//==========================================================================

void cluster_info_t::Reset()
{
	cluster = 0;
	FinaleFlat = "";
	ExitText = "";
	EnterText = "";
	MessageMusic = "";
	musicorder = 0;
	flags = 0;
	cdtrack = 0;
	ClusterName = "";
	cdid = 0;
}



//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseOpenBrace()
{
	switch(format_type)
	{
	default:
		format_type = sc.CheckString("{") ? FMT_New : FMT_Old;
		if (format_type == FMT_New) 
			sc.SetCMode(true);
		break;

	case FMT_Old:
		break;

	case FMT_New:
		sc.MustGetStringName("{");
		sc.SetCMode(true);
		break;
	}
}

//==========================================================================
//
//
//==========================================================================

bool FMapInfoParser::ParseCloseBrace()
{
	if (format_type == FMT_New)
	{
		return sc.Compare("}");
	}
	else
	{
		// We have to assume that the next keyword
		// starts a new top level block
		sc.UnGet();
		return true;
	}
}

//==========================================================================
//
//
//==========================================================================

bool FMapInfoParser::CheckAssign()
{
	if (format_type == FMT_New) return sc.CheckString("=");
	else return false;	// force explicit handling
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseAssign()
{
	if (format_type == FMT_New) sc.MustGetStringName("=");
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::MustParseAssign()
{
	if (format_type == FMT_New) sc.MustGetStringName("=");
	else sc.ScriptError(NULL);
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseComma()
{
	if (format_type == FMT_New) sc.MustGetStringName(",");
}

//==========================================================================
//
//
//==========================================================================

bool FMapInfoParser::CheckNumber()
{
	if (format_type == FMT_New) 
	{
		if (sc.CheckString(","))
		{
			sc.MustGetNumber();
			return true;
		}
		return false;
	}
	else return sc.CheckNumber();
}

//==========================================================================
//
//
//==========================================================================

bool FMapInfoParser::CheckFloat()
{
	if (format_type == FMT_New) 
	{
		if (sc.CheckString(","))
		{
			sc.MustGetFloat();
			return true;
		}
		return false;
	}
	else return sc.CheckFloat();
}

//==========================================================================
//
// skips an entire parameter list that's separated by commas
//
//==========================================================================

void FMapInfoParser::SkipToNext()
{
	if (sc.CheckString("="))
	{
		do
		{
			sc.MustGetString();
		}
		while (sc.CheckString(","));
	}
}


//==========================================================================
//
// checks if the current block was properly terminated
//
//==========================================================================

void FMapInfoParser::CheckEndOfFile(const char *block)
{
	if (format_type == FMT_New && sc.End)
	{
		sc.ScriptError("Unexpected end of file in %s definition", block);
	}
}

//==========================================================================
//
// ParseLookupname
// Parses a string that may reference the string table
//
//==========================================================================

bool FMapInfoParser::ParseLookupName(FString &dest)
{
	sc.MustGetString();
	if (sc.Compare("lookup"))
	{
		ParseComma();
		sc.MustGetString();
		dest = sc.String;
		return true;
	}
	else if (sc.String[0] == '$')
	{
		dest = sc.String+1;
		return true;
	}
	else if (format_type == FMT_Old)
	{
		dest = sc.String;
		return false;
	}
	else
	{
		sc.UnGet();
		dest = "";
		do
		{
			sc.MustGetString();
			dest << sc.String << '\n';
		}
		while (sc.CheckString(","));
		// strip off the last newline
		dest.Truncate(dest.Len()-1);
		return false;
	}
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseLumpOrTextureName(FString &name)
{
	sc.MustGetString();
	name = sc.String;
}


//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseExitText(FName formap, level_info_t *info)
{
	FString nexttext;
	bool nextlookup = ParseLookupName(nexttext);

	auto def = info->ExitMapTexts.CheckKey(formap);
	if (def != nullptr)
	{
		def->mText = nexttext;
		if (nextlookup) def->mDefined |= FExitText::DEF_LOOKUP;
		else def->mDefined &= ~FExitText::DEF_LOOKUP;
		def->mDefined |= FExitText::DEF_TEXT;
	}
	else
	{
		FExitText def;
		def.mText = nexttext;
		if (nextlookup) def.mDefined |= FExitText::DEF_LOOKUP;
		def.mDefined |= FExitText::DEF_TEXT;
		info->ExitMapTexts.Insert(formap, def);
	}
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseExitMusic(FName formap, level_info_t *info)
{
	FString music;
	int order;
	ParseMusic(music, order);

	auto def = info->ExitMapTexts.CheckKey(formap);
	if (def != nullptr)
	{
		def->mMusic = music;
		def->mOrder = order;
		def->mDefined |= FExitText::DEF_MUSIC;
	}
	else
	{
		FExitText def;
		def.mMusic = music;
		def.mOrder = order;
		def.mDefined |= FExitText::DEF_MUSIC;
		info->ExitMapTexts.Insert(formap, def);
	}
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseExitBackdrop(FName formap, level_info_t *info, bool ispic)
{
	FString drop;
	ParseLumpOrTextureName(drop);

	auto def = info->ExitMapTexts.CheckKey(formap);
	if (def != nullptr)
	{
		def->mBackdrop = drop;
		def->mDefined |= FExitText::DEF_BACKDROP;
		if (ispic) def->mDefined |= FExitText::DEF_PIC;
		else def->mDefined &= ~FExitText::DEF_PIC;
	}
	else
	{
		FExitText def;
		def.mBackdrop = drop;
		def.mDefined |= FExitText::DEF_BACKDROP;
		def.mDefined |= FExitText::DEF_MUSIC;
		if (ispic) def.mDefined |= FExitText::DEF_PIC;
		info->ExitMapTexts.Insert(formap, def);
	}
}

//==========================================================================
//
//
//==========================================================================

void FMapInfoParser::ParseMusic(FString &name, int &order)
{
	sc.MustGetString();

	order = 0;
	char *colon = strchr (sc.String, ':');
	if (colon)
	{
		order = atoi(colon+1);
		*colon = 0;
	}
	name = sc.String;
	if (!colon && CheckNumber())
	{
		order = sc.Number;
	}
}

//==========================================================================
//
//
//
//==========================================================================

void FMapInfoParser::ParseCutscene(CutsceneDef& cdef, bool allow_function)
{
	FString sound;
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		sc.MustGetString();
		if (sc.Compare("video")) { ParseAssign(); sc.MustGetString(); cdef.video = sc.String; cdef.function = ""; }
		else if (sc.Compare("function") && allow_function) { ParseAssign(); sc.SetCMode(false); sc.MustGetString(); sc.SetCMode(true); cdef.function = sc.String; cdef.video = ""; }
		else if (sc.Compare("sound")) { ParseAssign(); sc.MustGetString(); cdef.soundName = sc.String; }
		else if (sc.Compare("soundid")) { ParseAssign(); sc.MustGetNumber(); cdef.soundID = sc.Number; }
		else if (sc.Compare("fps")) { ParseAssign();  sc.MustGetNumber();  cdef.framespersec = sc.Number; }
		//else if (sc.Compare("transitiononly")) cdef.transitiononly = true;
		else if (sc.Compare("delete")) { cdef.function = "none"; cdef.video = ""; } // this means 'play nothing', not 'not defined'.
		else if (sc.Compare("clear")) cdef = {};
	}
}

//==========================================================================
//
// ParseCluster
// Parses a cluster definition
//
//==========================================================================

void FMapInfoParser::ParseCluster()
{
	sc.MustGetNumber ();
	int clusterindex = FindWadClusterInfo (sc.Number);
	if (clusterindex == -1)
	{
		clusterindex = wadclusterinfos.Reserve(1);
	}

	cluster_info_t *clusterinfo = &wadclusterinfos[clusterindex];
	clusterinfo->Reset();
	clusterinfo->cluster = sc.Number;

	ParseOpenBrace();

	while (sc.GetString())
	{
		if (sc.Compare("name"))
		{
			ParseAssign();
			if (ParseLookupName(clusterinfo->ClusterName))
				clusterinfo->flags |= CLUSTER_LOOKUPCLUSTERNAME;
		}
		else if (sc.Compare("entertext"))
		{
			ParseAssign();
			if (ParseLookupName(clusterinfo->EnterText))
				clusterinfo->flags |= CLUSTER_LOOKUPENTERTEXT;
			else
			{
				FStringf testlabel("CLUSTERENTER%d", clusterinfo->cluster);
				if (GStrings.MatchDefaultString(testlabel.GetChars(), clusterinfo->EnterText.GetChars()))
				{
					clusterinfo->EnterText = testlabel;
					clusterinfo->flags |= CLUSTER_LOOKUPENTERTEXT;
				}
			}
		}
		else if (sc.Compare("exittext"))
		{
			ParseAssign();
			if (ParseLookupName(clusterinfo->ExitText))
				clusterinfo->flags |= CLUSTER_LOOKUPEXITTEXT;
			else
			{
				FStringf testlabel("CLUSTEREXIT%d", clusterinfo->cluster);
				if (GStrings.MatchDefaultString(testlabel.GetChars(), clusterinfo->ExitText.GetChars()))
				{
					clusterinfo->ExitText = testlabel;
					clusterinfo->flags |= CLUSTER_LOOKUPEXITTEXT;
				}
			}
		}
		else if (sc.Compare("music"))
		{
			ParseAssign();
			ParseMusic(clusterinfo->MessageMusic, clusterinfo->musicorder);
		}
		else if (sc.Compare("flat"))
		{
			ParseAssign();
			ParseLumpOrTextureName(clusterinfo->FinaleFlat);
		}
		else if (sc.Compare("pic"))
		{
			ParseAssign();
			ParseLumpOrTextureName(clusterinfo->FinaleFlat);
			clusterinfo->flags |= CLUSTER_FINALEPIC;
		}
		else if (sc.Compare("hub"))
		{
			clusterinfo->flags |= CLUSTER_HUB;
		}
		else if (sc.Compare("allowintermission"))
		{
			clusterinfo->flags |= CLUSTER_ALLOWINTERMISSION;
		}
		else if (sc.Compare("cdtrack"))
		{
			ParseAssign();
			sc.MustGetNumber();
			clusterinfo->cdtrack = sc.Number;
		}
		else if (sc.Compare("cdid"))
		{
			ParseAssign();
			sc.MustGetString();
			clusterinfo->cdid = strtoul (sc.String, NULL, 16);
		}
		else if (sc.Compare("entertextislump"))
		{
			clusterinfo->flags |= CLUSTER_ENTERTEXTINLUMP;
		}
		else if (sc.Compare("exittextislump"))
		{
			clusterinfo->flags |= CLUSTER_EXITTEXTINLUMP;
		}
		else if (sc.Compare("intro"))
		{
			ParseCutscene(clusterinfo->intro);
		}
		else if (sc.Compare("outro"))
		{
			ParseCutscene(clusterinfo->outro);
		}
		else if (sc.Compare("gameover"))
		{
			ParseCutscene(clusterinfo->gameover);
		}
		else if (!ParseCloseBrace())
		{
			// Unknown
			sc.ScriptMessage("Unknown property '%s' found in map definition\n", sc.String);
			SkipToNext();
		}
		else
		{
			break;
		}
	}
	// Remap Hexen's CLUS?MSG lumps to the string table, if applicable. The code here only checks what can actually be in an IWAD.
	if (clusterinfo->flags & CLUSTER_EXITTEXTINLUMP)
	{
		int lump = fileSystem.CheckNumForFullName(clusterinfo->ExitText.GetChars(), true);
		if (lump > 0)
		{
			// Check if this comes from either Hexen.wad or Hexdd.wad and if so, map to the string table.
			int fileno = fileSystem.GetFileContainer(lump);
			auto fn = fileSystem.GetResourceFileName(fileno);
			if (fn && (!stricmp(fn, "HEXEN.WAD") || !stricmp(fn, "HEXDD.WAD")))
			{
				FStringf key("TXT_%.5s_%s", fn, clusterinfo->ExitText.GetChars());
				if (GStrings.exists(key.GetChars()))
				{
					clusterinfo->ExitText = key;
					clusterinfo->flags &= ~CLUSTER_EXITTEXTINLUMP;
					clusterinfo->flags |= CLUSTER_LOOKUPEXITTEXT;
				}
			}
		}

	}
	CheckEndOfFile("cluster");
}


//==========================================================================
//
// ParseNextMap
// Parses a next map field
//
//==========================================================================

void FMapInfoParser::ParseNextMap(FString &mapname)
{
	if (sc.CheckNumber())
	{
		if (HexenHack)
		{
			mapname.Format("&wt@%02d", sc.Number);
		}
		else
		{
			mapname.Format("MAP%02d", sc.Number);
		}
	}
	else
	{
		sc.MustGetString();
		mapname = sc.String;
		FName seq = CheckEndSequence();
		if (seq != NAME_None)
		{
			mapname.Format("enDSeQ%04x", seq.GetIndex());
		}
	}
}

//==========================================================================
//
// Map options
//
//==========================================================================

DEFINE_MAP_OPTION(levelnum, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->levelnum = parse.sc.Number;
}

DEFINE_MAP_OPTION(next, true)
{
	parse.ParseAssign();
	parse.ParseNextMap(info->NextMap);
}

DEFINE_MAP_OPTION(lightningsound, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->LightningSound = parse.sc.String;
}

DEFINE_MAP_OPTION(author, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->AuthorName = parse.sc.String;
}

DEFINE_MAP_OPTION(label, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->MapLabel = parse.sc.String;
}

DEFINE_MAP_OPTION(secretnext, true)
{
	parse.ParseAssign();
	parse.ParseNextMap(info->NextSecretMap);
}

DEFINE_MAP_OPTION(secret, true) // Just an alias for secretnext, for Vavoom compatibility
{
	parse.ParseAssign();
	parse.ParseNextMap(info->NextSecretMap);
}

DEFINE_MAP_OPTION(cluster, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->cluster = parse.sc.Number;

	// If this cluster hasn't been defined yet, add it. This is especially needed
	// for Hexen, because it doesn't have clusterdefs. If we don't do this, every
	// level on Hexen will sometimes be considered as being on the same hub,
	// depending on the check done.
	if (FindWadClusterInfo (parse.sc.Number) == -1)
	{
		unsigned int clusterindex = wadclusterinfos.Reserve(1);
		cluster_info_t *clusterinfo = &wadclusterinfos[clusterindex];
		clusterinfo->Reset();
		clusterinfo->cluster = parse.sc.Number;
		if (parse.HexenHack)
		{
			clusterinfo->flags |= CLUSTER_HUB;
		}
	}
}

DEFINE_MAP_OPTION(sky1, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->SkyPic1);
	if (parse.CheckFloat())
	{
		if (parse.HexenHack)
		{
			parse.sc.Float /= 256;
		}
		info->skyspeed1 = float(parse.sc.Float * (TICRATE / 1000.));
	}
}

DEFINE_MAP_OPTION(sky2, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->SkyPic2);
	if (parse.CheckFloat())
	{
		if (parse.HexenHack)
		{
			parse.sc.Float /= 256;
		}
		info->skyspeed2 = float(parse.sc.Float * (TICRATE / 1000.));
	}
}

// Vavoom compatibility
DEFINE_MAP_OPTION(skybox, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->SkyPic1);
	info->skyspeed1 = 0;
}

DEFINE_MAP_OPTION(fade, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->fadeto = V_GetColor(parse.sc);
}

DEFINE_MAP_OPTION(outsidefog, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->outsidefog = V_GetColor(parse.sc);
}

DEFINE_MAP_OPTION(titlepatch, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->PName);
	if (parse.format_type == FMapInfoParser::FMT_New)
	{
		if (parse.sc.CheckString(","))
		{
			parse.sc.MustGetNumber();
			if (parse.sc.Number) info->flags3 |= LEVEL3_HIDEAUTHORNAME;
			else info->flags3 &= ~LEVEL3_HIDEAUTHORNAME;
		}
	}
}

DEFINE_MAP_OPTION(partime, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->partime = parse.sc.Number;
}

DEFINE_MAP_OPTION(par, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->partime = parse.sc.Number;
}

DEFINE_MAP_OPTION(sucktime, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->sucktime = parse.sc.Number;
}

DEFINE_MAP_OPTION(music, true)
{
	parse.ParseAssign();
	parse.ParseMusic(info->Music, info->musicorder);
}

DEFINE_MAP_OPTION(intermusic, true)
{
	parse.ParseAssign();
	parse.ParseMusic(info->InterMusic, info->intermusicorder);
}

DEFINE_MAP_OPTION(mapintermusic, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	FString mapname = parse.sc.String;
	FString music;
	int order;
	parse.ParseComma();
	parse.ParseMusic(music, order);
	info->MapInterMusic[FName(mapname)] = std::make_pair(music, order);
}

DEFINE_MAP_OPTION(fadetable, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->FadeTable);
}

DEFINE_MAP_OPTION(colormap, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->CustomColorMap);
}

DEFINE_MAP_OPTION(evenlighting, true)
{
	info->WallVertLight = info->WallHorizLight = 0;
}

DEFINE_MAP_OPTION(cdtrack, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->cdtrack = parse.sc.Number;
}

DEFINE_MAP_OPTION(cdid, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->cdid = strtoul (parse.sc.String, NULL, 16);
}

DEFINE_MAP_OPTION(warptrans, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->WarpTrans = parse.sc.Number;
}

DEFINE_MAP_OPTION(vertwallshade, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->WallVertLight = (int8_t)clamp (parse.sc.Number / 2, -128, 127);
}

DEFINE_MAP_OPTION(horizwallshade, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->WallHorizLight = (int8_t)clamp (parse.sc.Number / 2, -128, 127);
}

DEFINE_MAP_OPTION(gravity, true)
{
	parse.ParseAssign();
	parse.sc.MustGetFloat();
	info->gravity = parse.sc.Float;
}

DEFINE_MAP_OPTION(nogravity, true)
{
	info->gravity = DBL_MAX;
}

DEFINE_MAP_OPTION(aircontrol, true)
{
	parse.ParseAssign();
	parse.sc.MustGetFloat();
	info->aircontrol = parse.sc.Float;
}

DEFINE_MAP_OPTION(airsupply, true)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->airsupply = parse.sc.Number;
}

DEFINE_MAP_OPTION(interpic, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->ExitPic = parse.sc.String;
}

DEFINE_MAP_OPTION(exitpic, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->ExitPic = parse.sc.String;
}

DEFINE_MAP_OPTION(enterpic, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->EnterPic = parse.sc.String;
}

DEFINE_MAP_OPTION(exitanim, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->ExitAnim = parse.sc.String;
}

DEFINE_MAP_OPTION(enteranim, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->EnterAnim = parse.sc.String;
}

DEFINE_MAP_OPTION(specialaction, true)
{
	parse.ParseAssign();

	FSpecialAction *sa = &info->specialactions[info->specialactions.Reserve(1)];
	int min_arg, max_arg;
	if (parse.format_type == parse.FMT_Old) parse.sc.SetCMode(true);
	parse.sc.MustGetString();
	sa->Type = FName(parse.sc.String);
	parse.sc.CheckString(",");
	parse.sc.MustGetString();
	sa->Action = P_FindLineSpecial(parse.sc.String, &min_arg, &max_arg);
	if (sa->Action == 0 || min_arg < 0)
	{
		parse.sc.ScriptError("Unknown specialaction '%s'", parse.sc.String);
	}
	int j = 0;
	while (j < 5 && parse.sc.CheckString(","))
	{
		parse.sc.MustGetNumber();
		sa->Args[j++] = parse.sc.Number;
	}
	if (parse.format_type == parse.FMT_Old) parse.sc.SetCMode(false);
}

DEFINE_MAP_OPTION(PrecacheSounds, true)
{
	parse.ParseAssign();

	do
	{
		parse.sc.MustGetString();
		FSoundID snd = S_FindSound(parse.sc.String);
		if (snd == NO_SOUND)
		{
			parse.sc.ScriptMessage("Unknown sound \"%s\"", parse.sc.String);
		}
		else
		{
			info->PrecacheSounds.Push(snd);
		}
	} while (parse.sc.CheckString(","));
}

DEFINE_MAP_OPTION(EventHandlers, true)
{
	parse.ParseAssign();

	do
	{
		parse.sc.MustGetString();
		info->EventHandlers.Push(parse.sc.String);
	} while (parse.sc.CheckString(","));
}

DEFINE_MAP_OPTION(PrecacheTextures, true)
{
	parse.ParseAssign();

	do
	{
		parse.sc.MustGetString();
		//the texture manager is not initialized here so all we can do is store the texture's name.
		info->PrecacheTextures.Push(parse.sc.String);
	} while (parse.sc.CheckString(","));
}

DEFINE_MAP_OPTION(PrecacheClasses, true)
{
	parse.ParseAssign();

	do
	{
		parse.sc.MustGetString();
		//the class list is not initialized here so all we can do is store the class's name.
		info->PrecacheClasses.Push(parse.sc.String);
	} while (parse.sc.CheckString(","));
}

DEFINE_MAP_OPTION(redirect, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->RedirectType = parse.sc.String;
	parse.ParseComma();
	parse.ParseNextMap(info->RedirectMapName);
}

DEFINE_MAP_OPTION(cvar_redirect, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->RedirectCVAR = parse.sc.String;
	parse.ParseComma();
	parse.ParseNextMap(info->RedirectCVARMapName);
}

DEFINE_MAP_OPTION(sndseq, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->SndSeq = parse.sc.String;
}

DEFINE_MAP_OPTION(sndinfo, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->SoundInfo = parse.sc.String;
}

DEFINE_MAP_OPTION(soundinfo, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->SoundInfo = parse.sc.String;
}

DEFINE_MAP_OPTION(translator, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->Translator = parse.sc.String;
}

DEFINE_MAP_OPTION(deathsequence, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->deathsequence = parse.sc.String;
}

DEFINE_MAP_OPTION(slideshow, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->slideshow = parse.sc.String;
}

DEFINE_MAP_OPTION(bordertexture, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->BorderTexture);
}

DEFINE_MAP_OPTION(f1, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->F1Pic);
}

DEFINE_MAP_OPTION(teamdamage, true)
{
	parse.ParseAssign();
	parse.sc.MustGetFloat();
	info->teamdamage = parse.sc.Float;
}

DEFINE_MAP_OPTION(mapbackground, true)
{
	parse.ParseAssign();
	parse.ParseLumpOrTextureName(info->MapBackground);
}

DEFINE_MAP_OPTION(exittext, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	FName nextmap = parse.sc.String;
	parse.ParseComma();
	parse.ParseExitText(nextmap, info);
}

DEFINE_MAP_OPTION(textmusic, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	FName nextmap = parse.sc.String;
	parse.ParseComma();
	parse.ParseExitMusic(nextmap, info);
}

DEFINE_MAP_OPTION(textflat, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	FName nextmap = parse.sc.String;
	parse.ParseComma();
	parse.ParseExitBackdrop(nextmap, info, false);
}

DEFINE_MAP_OPTION(textpic, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	FName nextmap = parse.sc.String;
	parse.ParseComma();
	parse.ParseExitBackdrop(nextmap, info, true);
}

DEFINE_MAP_OPTION(defaultenvironment, false)
{
	int id;

	parse.ParseAssign();
	if (parse.sc.CheckNumber())
	{ // Numeric ID XXX [, YYY]
		id = parse.sc.Number << 8;
		if (parse.CheckNumber())
		{
			id |= parse.sc.Number;
		}
	}
	else
	{ // Named environment
		parse.sc.MustGetString();
		ReverbContainer *reverb = S_FindEnvironment(parse.sc.String);
		if (reverb == NULL)
		{
			parse.sc.ScriptMessage("Unknown sound environment '%s'\n", parse.sc.String);
			id = 0;
		}
		else
		{
			id = reverb->ID;
		}
	}
	info->DefaultEnvironment = id;
}

DEFINE_MAP_OPTION(hazardcolor, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->hazardcolor = V_GetColor(parse.sc);
}

DEFINE_MAP_OPTION(hazardflash, true)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->hazardflash = V_GetColor(parse.sc);
}

DEFINE_MAP_OPTION(fogdensity, false)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->fogdensity = clamp(parse.sc.Number, 0, 512) >> 1;
}

DEFINE_MAP_OPTION(outsidefogdensity, false)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->outsidefogdensity = clamp(parse.sc.Number, 0, 512) >> 1;
}

DEFINE_MAP_OPTION(skyfog, false)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->skyfog = parse.sc.Number;
}

DEFINE_MAP_OPTION(pixelratio, false)
{
	parse.ParseAssign();
	parse.sc.MustGetFloat();
	info->pixelstretch = (float)parse.sc.Float;
}


DEFINE_MAP_OPTION(brightfog, false)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();
	info->brightfog = parse.sc.Number;
}

DEFINE_MAP_OPTION(lightmode, false)
{
	parse.ParseAssign();
	parse.sc.MustGetNumber();

	if (parse.sc.Number == 8 || parse.sc.Number == 16) info->lightmode = ELightMode::NotSet;
	else if (parse.sc.Number >= 0 && parse.sc.Number < 5)
	{
		info->lightmode = ELightMode(parse.sc.Number);
	}
	else
	{
		parse.sc.ScriptMessage("Invalid light mode %d", parse.sc.Number);
	}
}

DEFINE_MAP_OPTION(lightblendmode, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();

	if (parse.sc.Compare("Default") || parse.sc.Compare("Clamp"))
	{
		info->lightblendmode = ELightBlendMode::DEFAULT;
	}
	else if (parse.sc.Compare("ColoredClamp"))
	{
		info->lightblendmode = ELightBlendMode::CLAMP_COLOR;
	}
	else if (parse.sc.Compare("Unclamped"))
	{
		info->lightblendmode = ELightBlendMode::NOCLAMP;
		if(parse.sc.CheckString(","))
		{
			parse.sc.MustGetString();
			if (parse.sc.Compare("None"))
			{
				info->tonemap = ETonemapMode::None;
			}
			else if (parse.sc.Compare("Linear"))
			{
				info->tonemap = ETonemapMode::Linear;
			}
			else if (parse.sc.Compare("Uncharted2"))
			{
				info->tonemap = ETonemapMode::Uncharted2;
			}
			else if (parse.sc.Compare("HejlDawson"))
			{
				info->tonemap = ETonemapMode::HejlDawson;
			}
			else if (parse.sc.Compare("Reinhard"))
			{
				info->tonemap = ETonemapMode::Reinhard;
			}
			else
			{
				parse.sc.ScriptMessage("Invalid tonemap %s", parse.sc.String);
			}
		}
	}
	else
	{
		parse.sc.ScriptMessage("Invalid light blend mode %s", parse.sc.String);
	}
}

DEFINE_MAP_OPTION(notexturefill, false)
{
	if (parse.CheckAssign())
	{
		parse.sc.MustGetNumber();
		info->notexturefill = !!parse.sc.Number;
	}
	else
	{
		info->notexturefill = true;
	}
}

DEFINE_MAP_OPTION(lightadditivesurfaces, false)
{
	if (parse.CheckAssign())
	{
		parse.sc.MustGetNumber();
		info->lightadditivesurfaces = !!parse.sc.Number;
	}
	else
	{
		info->lightadditivesurfaces = true;
	}
}

DEFINE_MAP_OPTION(skyrotate, false)
{
	parse.ParseAssign();
	parse.sc.MustGetFloat();
	info->skyrotatevector.X = (float)parse.sc.Float;
	if (parse.format_type == FMapInfoParser::FMT_New) parse.sc.MustGetStringName(",");
	parse.sc.MustGetFloat();
	info->skyrotatevector.Y = (float)parse.sc.Float;
	if (parse.format_type == FMapInfoParser::FMT_New) parse.sc.MustGetStringName(",");
	parse.sc.MustGetFloat();
	info->skyrotatevector.Z = (float)parse.sc.Float;
	info->skyrotatevector.MakeUnit();
}

DEFINE_MAP_OPTION(skyrotate2, false)
{
	parse.ParseAssign();
	parse.sc.MustGetFloat();
	info->skyrotatevector2.X = (float)parse.sc.Float;
	if (parse.format_type == FMapInfoParser::FMT_New) parse.sc.MustGetStringName(",");
	parse.sc.MustGetFloat();
	info->skyrotatevector2.Y = (float)parse.sc.Float;
	if (parse.format_type == FMapInfoParser::FMT_New) parse.sc.MustGetStringName(",");
	parse.sc.MustGetFloat();
	info->skyrotatevector2.Z = (float)parse.sc.Float;
	info->skyrotatevector2.MakeUnit();
}

DEFINE_MAP_OPTION(fs_nocheckposition, false)
{
	if (parse.CheckAssign())
	{
		parse.sc.MustGetNumber();
		info->fs_nocheckposition = !!parse.sc.Number;
	}
	else
	{
		info->fs_nocheckposition = true;
	}
}

DEFINE_MAP_OPTION(edata, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->EDName = parse.sc.String;
}

DEFINE_MAP_OPTION(loadacs, false)
{
	parse.ParseAssign();
	parse.sc.MustGetString();
	info->acsName = parse.sc.String;
}

DEFINE_MAP_OPTION(intro, true)
{
	parse.ParseCutscene(info->intro);
}

DEFINE_MAP_OPTION(outro, true)
{
	parse.ParseCutscene(info->outro);
}


//==========================================================================
//
// All flag based map options 
//
//==========================================================================

enum EMIType
{
	MITYPE_IGNORE,
	MITYPE_EATNEXT,
	MITYPE_SETFLAG,
	MITYPE_CLRFLAG,
	MITYPE_SCFLAGS,
	MITYPE_SETFLAG2,
	MITYPE_CLRFLAG2,
	MITYPE_SCFLAGS2,
	MITYPE_SETFLAG3,
	MITYPE_CLRFLAG3,
	MITYPE_SCFLAGS3,
	MITYPE_COMPATFLAG,
	MITYPE_CLRCOMPATFLAG,
};

struct MapInfoFlagHandler
{
	const char *name;
	EMIType type;
	uint32_t data1, data2;
}
MapFlagHandlers[] =
{
	{ "nointermission",					MITYPE_SETFLAG,	LEVEL_NOINTERMISSION, 0 },
	{ "intermission",					MITYPE_CLRFLAG,	LEVEL_NOINTERMISSION, 0 },
	{ "doublesky",						MITYPE_SETFLAG,	LEVEL_DOUBLESKY, 0 },
	{ "nosoundclipping",				MITYPE_IGNORE,	0, 0 },	// was nosoundclipping
	{ "allowmonstertelefrags",			MITYPE_SETFLAG,	LEVEL_MONSTERSTELEFRAG, 0 },
	{ "map07special",					MITYPE_SETFLAG,	LEVEL_MAP07SPECIAL, 0 },
	{ "baronspecial",					MITYPE_SETFLAG,	LEVEL_BRUISERSPECIAL, 0 },
	{ "cyberdemonspecial",				MITYPE_SETFLAG,	LEVEL_CYBORGSPECIAL, 0 },
	{ "spidermastermindspecial",		MITYPE_SETFLAG,	LEVEL_SPIDERSPECIAL, 0 },
	{ "minotaurspecial",				MITYPE_SETFLAG,	LEVEL_MINOTAURSPECIAL, 0 },
	{ "dsparilspecial",					MITYPE_SETFLAG,	LEVEL_SORCERER2SPECIAL, 0 },
	{ "ironlichspecial",				MITYPE_SETFLAG,	LEVEL_HEADSPECIAL, 0 },
	{ "e1m8special",					MITYPE_SETFLAG3,LEVEL3_E1M8SPECIAL, 0 },
	{ "e2m8special",					MITYPE_SETFLAG3,LEVEL3_E2M8SPECIAL, 0 },
	{ "e3m8special",					MITYPE_SETFLAG3,LEVEL3_E3M8SPECIAL, 0 },
	{ "e4m8special",					MITYPE_SETFLAG3,LEVEL3_E4M8SPECIAL, 0 },
	{ "e4m6special",					MITYPE_SETFLAG3,LEVEL3_E4M6SPECIAL, 0 },
	{ "specialaction_exitlevel",		MITYPE_SCFLAGS,	0, ~LEVEL_SPECACTIONSMASK },
	{ "specialaction_opendoor",			MITYPE_SCFLAGS,	LEVEL_SPECOPENDOOR, ~LEVEL_SPECACTIONSMASK },
	{ "specialaction_lowerfloor",		MITYPE_SCFLAGS,	LEVEL_SPECLOWERFLOOR, ~LEVEL_SPECACTIONSMASK },
	{ "specialaction_lowerfloortohighest",MITYPE_SCFLAGS,LEVEL_SPECLOWERFLOORTOHIGHEST, ~LEVEL_SPECACTIONSMASK },
	{ "specialaction_killmonsters",		MITYPE_SETFLAG,	LEVEL_SPECKILLMONSTERS, 0 },
	{ "lightning",						MITYPE_SETFLAG,	LEVEL_STARTLIGHTNING, 0 },
	{ "smoothlighting",					MITYPE_SETFLAG2,	LEVEL2_SMOOTHLIGHTING, 0 },
	{ "noautosequences",				MITYPE_SETFLAG,	LEVEL_SNDSEQTOTALCTRL, 0 },
	{ "autosequences",					MITYPE_CLRFLAG,	LEVEL_SNDSEQTOTALCTRL, 0 },
	{ "forcenoskystretch",				MITYPE_SETFLAG,	LEVEL_FORCETILEDSKY, 0 },
	{ "skystretch",						MITYPE_CLRFLAG,	LEVEL_FORCETILEDSKY, 0 },
	{ "allowfreelook",					MITYPE_SCFLAGS,	LEVEL_FREELOOK_YES, ~LEVEL_FREELOOK_NO },
	{ "nofreelook",						MITYPE_SCFLAGS,	LEVEL_FREELOOK_NO, ~LEVEL_FREELOOK_YES },
	{ "allowjump",						MITYPE_CLRFLAG,	LEVEL_JUMP_NO, 0 },
	{ "nojump",							MITYPE_SETFLAG,	LEVEL_JUMP_NO, 0 },
	{ "fallingdamage",					MITYPE_SCFLAGS,	LEVEL_FALLDMG_HX, ~LEVEL_FALLDMG_ZD },
	{ "oldfallingdamage",				MITYPE_SCFLAGS,	LEVEL_FALLDMG_ZD, ~LEVEL_FALLDMG_HX },
	{ "forcefallingdamage",				MITYPE_SCFLAGS,	LEVEL_FALLDMG_ZD, ~LEVEL_FALLDMG_HX },
	{ "strifefallingdamage",			MITYPE_SETFLAG,	LEVEL_FALLDMG_ZD|LEVEL_FALLDMG_HX, 0 },
	{ "nofallingdamage",				MITYPE_SCFLAGS,	0, ~(LEVEL_FALLDMG_ZD|LEVEL_FALLDMG_HX) },
	{ "noallies",						MITYPE_SETFLAG,	LEVEL_NOALLIES, 0 },
	{ "filterstarts",					MITYPE_SETFLAG,	LEVEL_FILTERSTARTS, 0 },
	{ "useplayerstartz",				MITYPE_SETFLAG, LEVEL_USEPLAYERSTARTZ, 0 },
	{ "randomplayerstarts",				MITYPE_SETFLAG2, LEVEL2_RANDOMPLAYERSTARTS, 0 },
	{ "activateowndeathspecials",		MITYPE_SETFLAG,	LEVEL_ACTOWNSPECIAL, 0 },
	{ "killeractivatesdeathspecials",	MITYPE_CLRFLAG,	LEVEL_ACTOWNSPECIAL, 0 },
	{ "missilesactivateimpactlines",	MITYPE_SETFLAG2,	LEVEL2_MISSILESACTIVATEIMPACT, 0 },
	{ "missileshootersactivateimpactlines",MITYPE_CLRFLAG2,	LEVEL2_MISSILESACTIVATEIMPACT, 0 },
	{ "noinventorybar",					MITYPE_SETFLAG,	LEVEL_NOINVENTORYBAR, 0 },
	{ "deathslideshow",					MITYPE_IGNORE,		0, 0 },
	{ "strictmonsteractivation",		MITYPE_CLRFLAG2,	LEVEL2_LAXMONSTERACTIVATION, LEVEL2_LAXACTIVATIONMAPINFO },
	{ "laxmonsteractivation",			MITYPE_SETFLAG2,	LEVEL2_LAXMONSTERACTIVATION, LEVEL2_LAXACTIVATIONMAPINFO },
	{ "additive_scrollers",				MITYPE_COMPATFLAG, COMPATF_BOOMSCROLL, 0 },
	{ "keepfullinventory",				MITYPE_SETFLAG2,	LEVEL2_KEEPFULLINVENTORY, 0 },
	{ "resetitems",						MITYPE_SETFLAG3,	LEVEL3_REMOVEITEMS, 0 },
	{ "monsterfallingdamage",			MITYPE_SETFLAG2,	LEVEL2_MONSTERFALLINGDAMAGE, 0 },
	{ "nomonsterfallingdamage",			MITYPE_CLRFLAG2,	LEVEL2_MONSTERFALLINGDAMAGE, 0 },
	{ "clipmidtextures",				MITYPE_SETFLAG2,	LEVEL2_CLIPMIDTEX, 0 },
	{ "wrapmidtextures",				MITYPE_SETFLAG2,	LEVEL2_WRAPMIDTEX, 0 },
	{ "allowcrouch",					MITYPE_CLRFLAG,	LEVEL_CROUCH_NO, 0 },
	{ "nocrouch",						MITYPE_SETFLAG,	LEVEL_CROUCH_NO, 0 },
	{ "pausemusicinmenus",				MITYPE_SCFLAGS2,	LEVEL2_PAUSE_MUSIC_IN_MENUS, 0 },
	{ "noinfighting",					MITYPE_SCFLAGS2,	LEVEL2_NOINFIGHTING, ~LEVEL2_TOTALINFIGHTING },
	{ "normalinfighting",				MITYPE_SCFLAGS2,	0, ~(LEVEL2_NOINFIGHTING|LEVEL2_TOTALINFIGHTING)},
	{ "totalinfighting",				MITYPE_SCFLAGS2,	LEVEL2_TOTALINFIGHTING, ~LEVEL2_NOINFIGHTING },
	{ "infiniteflightpowerup",			MITYPE_SETFLAG2,	LEVEL2_INFINITE_FLIGHT, 0 },
	{ "noinfiniteflightpowerup",		MITYPE_CLRFLAG2,	LEVEL2_INFINITE_FLIGHT, 0 },
	{ "allowrespawn",					MITYPE_SETFLAG2,	LEVEL2_ALLOWRESPAWN, 0 },
	{ "teamplayon",						MITYPE_SCFLAGS2,	LEVEL2_FORCETEAMPLAYON, ~LEVEL2_FORCETEAMPLAYOFF },
	{ "teamplayoff",					MITYPE_SCFLAGS2,	LEVEL2_FORCETEAMPLAYOFF, ~LEVEL2_FORCETEAMPLAYON },
	{ "checkswitchrange",				MITYPE_SETFLAG2,	LEVEL2_CHECKSWITCHRANGE, 0 },
	{ "nocheckswitchrange",				MITYPE_CLRFLAG2,	LEVEL2_CHECKSWITCHRANGE, 0 },
	{ "grinding_polyobj",				MITYPE_SETFLAG2,	LEVEL2_POLYGRIND, 0 },
	{ "no_grinding_polyobj",			MITYPE_CLRFLAG2,	LEVEL2_POLYGRIND, 0 },
	{ "resetinventory",					MITYPE_SETFLAG2,	LEVEL2_RESETINVENTORY, 0 },
	{ "resethealth",					MITYPE_SETFLAG2,	LEVEL2_RESETHEALTH, 0 },
	{ "endofgame",						MITYPE_SETFLAG2,	LEVEL2_ENDGAME, 0 },
	{ "nostatistics",					MITYPE_SETFLAG2,	LEVEL2_NOSTATISTICS, 0 },
	{ "noautosavehint",					MITYPE_SETFLAG2,	LEVEL2_NOAUTOSAVEHINT, 0 },
	{ "forgetstate",					MITYPE_SETFLAG2,	LEVEL2_FORGETSTATE, 0 },
	{ "rememberstate",					MITYPE_CLRFLAG2,	LEVEL2_FORGETSTATE, 0 },
	{ "unfreezesingleplayerconversations",MITYPE_SETFLAG2,	LEVEL2_CONV_SINGLE_UNFREEZE, 0 },
	{ "spawnwithweaponraised",			MITYPE_SETFLAG2,	LEVEL2_PRERAISEWEAPON, 0 },
	{ "needclustertext",				MITYPE_SETFLAG2,	LEVEL2_NEEDCLUSTERTEXT, 0 },
	{ "noclustertext",					MITYPE_SETFLAG2,	LEVEL2_NOCLUSTERTEXT, 0 },	// Normally there shouldn't be a need to explicitly set this 
	{ "forcefakecontrast",				MITYPE_SETFLAG3,	LEVEL3_FORCEFAKECONTRAST, 0 },
	{ "nolightfade",					MITYPE_SETFLAG3,	LEVEL3_NOLIGHTFADE, 0 },
	{ "nocoloredspritelighting",		MITYPE_SETFLAG3,	LEVEL3_NOCOLOREDSPRITELIGHTING, 0 },
	{ "forceworldpanning",				MITYPE_SETFLAG3,	LEVEL3_FORCEWORLDPANNING, 0 },
	{ "propermonsterfallingdamage",		MITYPE_SETFLAG3,	LEVEL3_PROPERMONSTERFALLINGDAMAGE, 0 },
	{ "disableshadowmap",				MITYPE_SETFLAG3,	LEVEL3_NOSHADOWMAP, 0 },
	{ "enableshadowmap",				MITYPE_CLRFLAG3,	LEVEL3_NOSHADOWMAP, 0 },
	{ "enableskyboxao",					MITYPE_SETFLAG3,	LEVEL3_SKYBOXAO, 0 },
	{ "disableskyboxao",				MITYPE_CLRFLAG3,	LEVEL3_SKYBOXAO, 0 },
	{ "avoidmelee",						MITYPE_SETFLAG3,	LEVEL3_AVOIDMELEE, 0 },
	{ "attenuatelights",				MITYPE_SETFLAG3,	LEVEL3_ATTENUATE, 0 },
	{ "nofogofwar",					MITYPE_SETFLAG3,	LEVEL3_NOFOGOFWAR, 0 },
	{ "nobotnodes",						MITYPE_IGNORE,	0, 0 },		// Skulltag option: nobotnodes
	{ "nopassover",						MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },
	{ "passover",						MITYPE_CLRCOMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },
	{ "compat_shorttex",				MITYPE_COMPATFLAG, COMPATF_SHORTTEX, 0 },
	{ "compat_stairs",					MITYPE_COMPATFLAG, COMPATF_STAIRINDEX, 0 },
	{ "compat_limitpain",				MITYPE_COMPATFLAG, COMPATF_LIMITPAIN, 0 },
	{ "compat_nopassover",				MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },
	{ "compat_notossdrops",				MITYPE_COMPATFLAG, COMPATF_NOTOSSDROPS, 0 },
	{ "compat_useblocking", 			MITYPE_COMPATFLAG, COMPATF_USEBLOCKING, 0 },
	{ "compat_nodoorlight",				MITYPE_COMPATFLAG, COMPATF_NODOORLIGHT, 0 },
	{ "compat_ravenscroll",				MITYPE_COMPATFLAG, COMPATF_RAVENSCROLL, 0 },
	{ "compat_soundtarget",				MITYPE_COMPATFLAG, COMPATF_SOUNDTARGET, 0 },
	{ "compat_dehhealth",				MITYPE_COMPATFLAG, COMPATF_DEHHEALTH, 0 },
	{ "compat_trace",					MITYPE_COMPATFLAG, COMPATF_TRACE, 0 },
	{ "compat_dropoff",					MITYPE_COMPATFLAG, COMPATF_DROPOFF, 0 },
	{ "compat_boomscroll",				MITYPE_COMPATFLAG, COMPATF_BOOMSCROLL, 0 },
	{ "compat_invisibility",			MITYPE_COMPATFLAG, COMPATF_INVISIBILITY, 0 },
	{ "compat_silent_instant_floors",	MITYPE_COMPATFLAG, COMPATF_SILENT_INSTANT_FLOORS, 0 },
	{ "compat_sectorsounds",			MITYPE_COMPATFLAG, COMPATF_SECTORSOUNDS, 0 },
	{ "compat_missileclip",				MITYPE_COMPATFLAG, COMPATF_MISSILECLIP, 0 },
	{ "compat_crossdropoff",			MITYPE_COMPATFLAG, COMPATF_CROSSDROPOFF, 0 },
	{ "compat_anybossdeath",			MITYPE_COMPATFLAG, COMPATF_ANYBOSSDEATH, 0 },
	{ "compat_minotaur",				MITYPE_COMPATFLAG, COMPATF_MINOTAUR, 0 },
	{ "compat_mushroom",				MITYPE_COMPATFLAG, COMPATF_MUSHROOM, 0 },
	{ "compat_mbfmonstermove",			MITYPE_COMPATFLAG, COMPATF_MBFMONSTERMOVE, 0 },
	{ "compat_corpsegibs",				MITYPE_COMPATFLAG, 0, 0 },	// this flag no longer exists, but we need it here for old mapinfos.
	{ "compat_vileghosts",				MITYPE_COMPATFLAG, COMPATF_VILEGHOSTS, 0 },
	{ "compat_noblockfriends",			MITYPE_COMPATFLAG, COMPATF_NOBLOCKFRIENDS, 0 },
	{ "compat_spritesort",				MITYPE_COMPATFLAG, COMPATF_SPRITESORT, 0 },
	{ "compat_light",					MITYPE_COMPATFLAG, COMPATF_LIGHT, 0 },
	{ "compat_polyobj",					MITYPE_COMPATFLAG, COMPATF_POLYOBJ, 0 },
	{ "compat_maskedmidtex",			MITYPE_COMPATFLAG, COMPATF_MASKEDMIDTEX, 0 },
	{ "compat_badangles",				MITYPE_COMPATFLAG, 0, COMPATF2_BADANGLES },
	{ "compat_floormove",				MITYPE_COMPATFLAG, 0, COMPATF2_FLOORMOVE },
	{ "compat_soundcutoff",				MITYPE_COMPATFLAG, 0, COMPATF2_SOUNDCUTOFF },
	{ "compat_pointonline",				MITYPE_COMPATFLAG, 0, COMPATF2_POINTONLINE },
	{ "compat_multiexit",				MITYPE_COMPATFLAG, 0, COMPATF2_MULTIEXIT },
	{ "compat_teleport",				MITYPE_COMPATFLAG, 0, COMPATF2_TELEPORT },
	{ "compat_pushwindow",				MITYPE_COMPATFLAG, 0, COMPATF2_PUSHWINDOW },
	{ "compat_checkswitchrange",		MITYPE_COMPATFLAG, 0, COMPATF2_CHECKSWITCHRANGE },
	{ "compat_explode1",				MITYPE_COMPATFLAG, 0, COMPATF2_EXPLODE1 },
	{ "compat_explode2",				MITYPE_COMPATFLAG, 0, COMPATF2_EXPLODE2 },
	{ "compat_railing",					MITYPE_COMPATFLAG, 0, COMPATF2_RAILING },
	{ "compat_scriptwait",				MITYPE_COMPATFLAG, 0, COMPATF2_SCRIPTWAIT },
	{ "compat_avoidhazards",			MITYPE_COMPATFLAG, 0, COMPATF2_AVOID_HAZARDS },
	{ "compat_stayonlift",				MITYPE_COMPATFLAG, 0, COMPATF2_STAYONLIFT },
	{ "compat_nombf21",					MITYPE_COMPATFLAG, 0, COMPATF2_NOMBF21 },
	{ "compat_voodoozombies",			MITYPE_COMPATFLAG, 0, COMPATF2_VOODOO_ZOMBIES },
	{ "compat_noacsargcheck",			MITYPE_COMPATFLAG, 0, COMPATF2_NOACSARGCHECK },
	{ "cd_start_track",					MITYPE_EATNEXT,	0, 0 },
	{ "cd_end1_track",					MITYPE_EATNEXT,	0, 0 },
	{ "cd_end2_track",					MITYPE_EATNEXT,	0, 0 },
	{ "cd_end3_track",					MITYPE_EATNEXT,	0, 0 },
	{ "cd_intermission_track",			MITYPE_EATNEXT,	0, 0 },
	{ "cd_title_track",					MITYPE_EATNEXT,	0, 0 },
	{ NULL, MITYPE_IGNORE, 0, 0}
};

//==========================================================================
//
// ParseMapDefinition
// Parses the body of a map definition, including defaultmap etc.
//
//==========================================================================

void FMapInfoParser::ParseMapDefinition(level_info_t &info)
{
	int index;

	ParseOpenBrace();

	while (sc.GetString())
	{
		if ((index = sc.MatchString(&MapFlagHandlers->name, sizeof(*MapFlagHandlers))) >= 0)
		{
			MapInfoFlagHandler *handler = &MapFlagHandlers[index];
			switch (handler->type)
			{
			case MITYPE_EATNEXT:
				ParseAssign();
				sc.MustGetString();
				break;

			case MITYPE_IGNORE:
				break;

			case MITYPE_SETFLAG:
				if (!CheckAssign())
				{
					info.flags |= handler->data1;
				}
				else
				{
					sc.MustGetNumber();
					if (sc.Number) info.flags |= handler->data1;
					else info.flags &= ~handler->data1;
				}
				info.flags |= handler->data2;
				break;

			case MITYPE_CLRFLAG:
				info.flags &= ~handler->data1;
				info.flags |= handler->data2;
				break;

			case MITYPE_SCFLAGS:
				info.flags = (info.flags & handler->data2) | handler->data1;
				break;

			case MITYPE_SETFLAG2:
				if (!CheckAssign())
				{
					info.flags2 |= handler->data1;
				}
				else
				{
					sc.MustGetNumber();
					if (sc.Number) info.flags2 |= handler->data1;
					else info.flags2 &= ~handler->data1;
				}
				info.flags2 |= handler->data2;
				break;

			case MITYPE_CLRFLAG2:
				info.flags2 &= ~handler->data1;
				info.flags2 |= handler->data2;
				break;

			case MITYPE_SCFLAGS2:
				info.flags2 = (info.flags2 & handler->data2) | handler->data1;
				break;

			case MITYPE_SETFLAG3:
				if (!CheckAssign())
				{
					info.flags3 |= handler->data1;
				}
				else
				{
					sc.MustGetNumber();
					if (sc.Number) info.flags3 |= handler->data1;
					else info.flags3 &= ~handler->data1;
				}
				info.flags3 |= handler->data2;
				break;

			case MITYPE_CLRFLAG3:
				info.flags3 &= ~handler->data1;
				info.flags3 |= handler->data2;
				break;

			case MITYPE_SCFLAGS3:
				info.flags3 = (info.flags3 & handler->data2) | handler->data1;
				break;

			case MITYPE_CLRCOMPATFLAG:
				info.compatflags &= ~handler->data1;
				info.compatflags2 &= ~handler->data2;
				info.compatmask |= handler->data1;
				info.compatmask2 |= handler->data2;
				break;

			case MITYPE_COMPATFLAG:
			{
				int set = 1;
				if (format_type == FMT_New)
				{
					if (CheckAssign())
					{
						sc.MustGetNumber();
						set = sc.Number;
					}
				}
				else
				{
					if (sc.CheckNumber()) set = sc.Number;
				}

				if (set)
				{
					info.compatflags |= handler->data1;
					info.compatflags2 |= handler->data2;
				}
				else
				{
					info.compatflags &= ~handler->data1;
					info.compatflags2 &= ~handler->data2;
				}
				info.compatmask |= handler->data1;
				info.compatmask2 |= handler->data2;
			}
			break;

			default:
				// should never happen
				assert(false);
				break;
			}
		}
		else
		{
			bool success = false;

			AutoSegs::MapInfoOptions.ForEach([this, &success, &info](FMapOptInfo* option)
			{
				if (sc.Compare(option->name))
				{
					if (!option->old && format_type != FMT_New)
					{
						sc.ScriptError("MAPINFO option '%s' requires the new MAPINFO format", sc.String);
					}
					option->handler(*this, &info);
					success = true;
					return false;  // break
				}
				
				return true;  // continue
			});
			
			if (!success)
			{
				if (!ParseCloseBrace())
				{
					// Unknown
					sc.ScriptMessage("Unknown property '%s' found in map definition\n", sc.String);
					SkipToNext();
				}
				else
				{
					break;
				}
			}
		}
	}
	CheckEndOfFile("map");
}


//==========================================================================
//
// GetDefaultLevelNum
// Gets a default level num from a map name.
//
//==========================================================================

static int GetDefaultLevelNum(const char *mapname)
{
	if (!strnicmp (mapname, "MAP", 3) && strlen(mapname) <= 5)
	{
		int mapnum = atoi (mapname + 3);

		if (mapnum >= 1 && mapnum <= 99)
			return mapnum;
	}
	else if (mapname[0] == 'E' &&
			mapname[1] >= '0' && mapname[1] <= '9' &&
			mapname[2] == 'M' &&
			mapname[3] >= '0' && mapname[3] <= '9')
	{
		int epinum = mapname[1] - '1';
		int mapnum = mapname[3] - '0';
		return epinum*10 + mapnum;
	}
	return 0;
}

//==========================================================================
//
// ParseMapHeader
// Parses the header of a map definition ('map mapxx mapname')
//
//==========================================================================

level_info_t *FMapInfoParser::ParseMapHeader(level_info_t &defaultinfo)
{
	FName mapname;

	if (sc.CheckNumber())
	{	// MAPNAME is a number; assume a Hexen wad
		if (format_type == FMT_New)
		{
			mapname = sc.String;
		}
		else
		{
			char maptemp[8];
			mysnprintf(maptemp, countof(maptemp), "MAP%02d", sc.Number);
			mapname = maptemp;
			HexenHack = true;
			format_type = FMT_Old;
		}
	}
	else 
	{
		sc.MustGetString();
		mapname = sc.String;
	}
	int levelindex = FindWadLevelInfo (mapname.GetChars());
	if (levelindex == -1)
	{
		levelindex = wadlevelinfos.Reserve(1);
	}
	level_info_t *levelinfo = &wadlevelinfos[levelindex];
	*levelinfo = defaultinfo;
	if (HexenHack)
	{
		levelinfo->WallHorizLight = levelinfo->WallVertLight = 0;

		// Hexen levels are automatically nointermission,
		// no auto sound sequences, falling damage,
		// monsters activate their own specials, and missiles
		// are always the activators of impact lines.
		levelinfo->flags |= LEVEL_NOINTERMISSION
						 | LEVEL_SNDSEQTOTALCTRL
						 | LEVEL_FALLDMG_HX
						 | LEVEL_ACTOWNSPECIAL;
		levelinfo->flags2|= LEVEL2_HEXENHACK
						 | LEVEL2_INFINITE_FLIGHT
						 | LEVEL2_MISSILESACTIVATEIMPACT
						 | LEVEL2_MONSTERFALLINGDAMAGE;

	}

	levelinfo->MapName = mapname.GetChars();
	levelinfo->MapName.ToUpper();
	sc.MustGetString ();
	if (sc.String[0] == '$')
	{
		// For consistency with other definitions allow $Stringtablename here, too.
		levelinfo->flags |= LEVEL_LOOKUPLEVELNAME;
		levelinfo->LevelName = sc.String + 1;
	}
	else
	{
		if (sc.Compare ("lookup"))
		{
			sc.MustGetString ();
			levelinfo->flags |= LEVEL_LOOKUPLEVELNAME;
			levelinfo->LevelName = sc.String;
		}
		else
		{
			// Workaround to allow localizazion of IWADs which do not have a string label here (e.g. HACX.WAD)
			// This checks for a string labelled with the MapName and if that is identical to what got parsed here
			// the string table entry will be used.

			if (GStrings.MatchDefaultString(levelinfo->MapName.GetChars(), sc.String))
			{
				levelinfo->flags |= LEVEL_LOOKUPLEVELNAME;
				levelinfo->LevelName = levelinfo->MapName;
			}
			else
			{
				levelinfo->LevelName = sc.String;

				if (HexenHack)
				{
					// Try to localize Hexen's map names. This does not use the above feature to allow these names to be unique.
					int fileno = fileSystem.GetFileContainer(sc.LumpNum);
					auto fn = fileSystem.GetResourceFileName(fileno);
					if (fn && (!stricmp(fn, "HEXEN.WAD") || !stricmp(fn, "HEXDD.WAD")))
					{
						FStringf key("TXT_%.5s_%s", fn, levelinfo->MapName.GetChars());
						if (GStrings.exists(key.GetChars()))
						{
							levelinfo->flags |= LEVEL_LOOKUPLEVELNAME;
							levelinfo->LevelName = key;
						}
					}
				}
			}
		}
	}

	// Set up levelnum now so that you can use Teleport_NewMap specials
	// to teleport to maps with standard names without needing a levelnum.
	levelinfo->levelnum = GetDefaultLevelNum(levelinfo->MapName.GetChars());

	// Does this map have a song defined via SNDINFO's $map command?
	// Set that as this map's default music if it does.
	FString *song;
	if ((song = HexenMusic.CheckKey(levelinfo->levelnum)) != NULL)
	{
		levelinfo->Music = *song;
	}

	return levelinfo;
}


//==========================================================================
//
// Episode definitions start with the header "episode <start-map>"
// and then can be followed by any of the following:
//
// name "Episode name as text"
// picname "Picture to display the episode name"
// key "Shortcut key for the menu"
// noskillmenu
// remove
//
//==========================================================================

void FMapInfoParser::ParseEpisodeInfo ()
{
	unsigned int i;
	FString map;
	FString pic;
	FString name;
	bool remove = false;
	char key = 0;
	bool noskill = false;
	bool optional = false;
	bool extended = false;
	CutsceneDef introscene;

	// Get map name
	sc.MustGetString ();
	map = sc.String;

	if (sc.CheckString ("teaser"))
	{
		sc.MustGetString ();
		if (gameinfo.flags & GI_SHAREWARE)
		{
			map = sc.String;
		}
	}

	ParseOpenBrace();

	while (sc.GetString())
	{
		if (sc.Compare ("optional"))
		{
			// For M4 in Doom
			optional = true;
		}
		else if (sc.Compare ("extended"))
		{
			// For M4 and M5 in Heretic
			extended = true;
		}
		else if (sc.Compare ("name"))
		{
			ParseAssign();
			sc.MustGetString ();
			name = strbin1(sc.String);
		}
		else if (sc.Compare ("picname"))
		{
			ParseAssign();
			sc.MustGetString ();
			pic = sc.String;
			// If no name has been specified, synthesize a string table reference with the same name as the patch.
			if (name.IsEmpty()) name.Format("$%s", sc.String);
		}
		else if (sc.Compare ("remove"))
		{
			remove = true;
		}
		else if (sc.Compare ("key"))
		{
			ParseAssign();
			sc.MustGetString ();
			key = sc.String[0];
		}
		else if (sc.Compare("noskillmenu"))
		{
			noskill = true;
		}
		else if (sc.Compare("intro"))
		{
			ParseCutscene(introscene);
		}

		else if (!ParseCloseBrace())
		{
			// Unknown
			sc.ScriptMessage("Unknown property '%s' found in episode definition\n", sc.String);
			SkipToNext();
		}
		else
		{
			break;
		}
	}
	CheckEndOfFile("episode");

	if (extended && !(gameinfo.flags & GI_MENUHACK_EXTENDED))
	{ // If the episode is for the extended Heretic, but this is
	  // not the extended Heretic, ignore it.
		return;
	}

	if (optional && !remove)
	{
		if (!P_CheckMapData(map.GetChars()))
		{
			// If the episode is optional and the map does not exist
			// just ignore this episode definition.
			return;
		}
	}


	for (i = 0; i < AllEpisodes.Size(); i++)
	{
		if (AllEpisodes[i].mEpisodeMap.CompareNoCase(map) == 0)
		{
			break;
		}
	}

	if (remove)
	{
		// If the remove property is given for an episode, remove it.
		AllEpisodes.Delete(i);
	}
	else
	{
		// Only allocate a new entry if this doesn't replace an existing episode.
		if (i >= AllEpisodes.Size())
		{
			i = AllEpisodes.Reserve(1);
		}

		FEpisode *epi = &AllEpisodes[i];

		epi->mEpisodeMap = map;
		epi->mEpisodeName = name;
		epi->mPicName = pic;
		epi->mShortcut = tolower(key);
		epi->mNoSkill = noskill;
		epi->mIntro = introscene;
	}
}


//==========================================================================
//
// SetLevelNum
// Avoid duplicate levelnums. The level being set always has precedence.
//
//==========================================================================

static void SetLevelNum (level_info_t *info, int num)
{
	for (unsigned int i = 0; i < wadlevelinfos.Size(); ++i)
	{
		if (wadlevelinfos[i].levelnum == num)
			wadlevelinfos[i].levelnum = 0;
	}
	info->levelnum = num;
}

//==========================================================================
//
// G_DoParseMapInfo
// Parses a single MAPINFO lump
// data for wadlevelinfos and wadclusterinfos.
//
//==========================================================================

void FMapInfoParser::ParseMapInfo (int lump, level_info_t &gamedefaults, level_info_t &defaultinfo)
{
	sc.OpenLumpNum(lump);

	defaultinfo = gamedefaults;
	HexenHack = false;

	if (ParsedLumps.Find(lump) != ParsedLumps.Size())
	{
		sc.ScriptMessage("MAPINFO file is processed more than once\n");
	}
	else
	{
		ParsedLumps.Push(lump);
	}

	while (sc.GetString ())
	{
		if (sc.Compare("include"))
		{
			sc.MustGetString();
			int inclump = fileSystem.CheckNumForFullName(sc.String, true);
			if (inclump < 0)
			{
				sc.ScriptError("include file '%s' not found", sc.String);
			}
			if (fileSystem.GetFileContainer(sc.LumpNum) != fileSystem.GetFileContainer(inclump))
			{
				// Do not allow overriding includes from the default MAPINFO
				if (fileSystem.GetFileContainer(sc.LumpNum) == 0)
				{
					I_FatalError("File %s is overriding core lump %s.",
						fileSystem.GetResourceFileFullName(fileSystem.GetFileContainer(inclump)), sc.String);
				}
			}
			// use a new parser object to parse the include. Otherwise we'd have to save the entire FScanner in a local variable which is a lot more messy.
			FMapInfoParser includer(&sc);
			includer.format_type = format_type;
			includer.HexenHack = HexenHack;
			includer.ParseMapInfo(inclump, gamedefaults, defaultinfo);
		}
		else if (sc.Compare("gamedefaults"))
		{
			gamedefaults.Reset();
			ParseMapDefinition(gamedefaults);
			defaultinfo = gamedefaults;
		}
		else if (sc.Compare("defaultmap"))
		{
			defaultinfo = gamedefaults;
			ParseMapDefinition(defaultinfo);
		}
		else if (sc.Compare("adddefaultmap"))
		{
			// Same as above but adds to the existing definitions instead of replacing them completely
			ParseMapDefinition(defaultinfo);
		}
		else if (sc.Compare("map"))
		{
			level_info_t *levelinfo = ParseMapHeader(defaultinfo);

			ParseMapDefinition(*levelinfo);

			// When the second sky is -NOFLAT-, make it a copy of the first sky
			if (!levelinfo->SkyPic2.CompareNoCase("-NOFLAT-"))
			{
				levelinfo->SkyPic2 = levelinfo->SkyPic1;
			}
			SetLevelNum (levelinfo, levelinfo->levelnum);	// Wipe out matching levelnums from other maps.
		}
		// clusterdef is the old keyword but the new format has enough 
		// structuring that 'cluster' can be handled, too. The old format does not.
		else if (sc.Compare("clusterdef") || (format_type != FMT_Old && sc.Compare("cluster")))
		{
			ParseCluster();
		}
		else if (sc.Compare("episode"))
		{
			ParseEpisodeInfo();
		}
		else if (sc.Compare("clearepisodes"))
		{
			AllEpisodes.Clear();
		}
		else if (sc.Compare("skill"))
		{
			ParseSkill();
		}
		else if (sc.Compare("clearskills"))
		{
			AllSkills.Clear();
			DefaultSkill = -1;
		}
		else if (sc.Compare("gameinfo"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseGameInfo();
			}
			else
			{
				sc.ScriptError("gameinfo definitions not supported with old MAPINFO syntax");
			}
		}
		else if (sc.Compare("intermission"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseIntermission();
			}
			else
			{
				sc.ScriptError("intermission definitions not supported with old MAPINFO syntax");
			}
		}
		else if (sc.Compare("doomednums"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseDoomEdNums();
			}
			else
			{
				sc.ScriptError("doomednums definitions not supported with old MAPINFO syntax");
			}
		}
		else if (sc.Compare("damagetype"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseDamageDefinition();
			}
			else
			{
				sc.ScriptError("damagetype definitions not supported with old MAPINFO syntax");
			}
		}
		else if (sc.Compare("spawnnums"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseSpawnNums();
			}
			else
			{
				sc.ScriptError("spawnnums definitions not supported with old MAPINFO syntax");
			}
		}
		else if (sc.Compare("conversationids"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseConversationIDs();
			}
			else
			{
				sc.ScriptError("conversationids definitions not supported with old MAPINFO syntax");
			}
		}
		else if (sc.Compare("automap") || sc.Compare("automap_overlay"))
		{
			if (format_type != FMT_Old)
			{
				format_type = FMT_New;
				ParseAMColors(sc.Compare("automap_overlay"));
			}
			else
			{
				sc.ScriptError("automap colorset definitions not supported with old MAPINFO syntax");
			}
		}
		else
		{
			sc.ScriptError("%s: Unknown top level keyword", sc.String);
		}
	}
}

//==========================================================================
//
//
//
//==========================================================================

void DeinitIntermissions();

void G_ClearMapinfo()
{
	wadclusterinfos.Clear();
	wadlevelinfos.Clear();
	AllEpisodes.Clear();
	AllSkills.Clear();
	DefaultSkill = -1;
	DeinitIntermissions();
	primaryLevel->info = nullptr;
	primaryLevel->F1Pic = "";
	ParsedLumps.Clear();
}

//==========================================================================
//
// G_ParseMapInfo
// Parses the MAPINFO lumps of all loaded WADs and generates
// data for wadlevelinfos and wadclusterinfos.
//
//==========================================================================

void G_ParseMapInfo (FString basemapinfo)
{
	int lump, lastlump = 0;
	level_info_t gamedefaults;
	TArray<FString> secretMaps;

	int flags1 = 0, flags2 = 0;
	if (gameinfo.gametype == GAME_Doom)
	{
		int comp = fileSystem.CheckNumForName("COMPLVL");
		if (comp >= 0)
		{
			auto complvl = fileSystem.ReadFile(comp);
			auto data = complvl.string();
			int length = fileSystem.FileLength(comp);
			if (length == 7 && !strnicmp("vanilla", data, 7))
			{
				flags1 = 
					COMPATF_SHORTTEX | COMPATF_STAIRINDEX | COMPATF_USEBLOCKING | COMPATF_NODOORLIGHT | COMPATF_SPRITESORT |
					COMPATF_TRACE | COMPATF_MISSILECLIP | COMPATF_SOUNDTARGET | COMPATF_DEHHEALTH | COMPATF_CROSSDROPOFF |
					COMPATF_LIGHT | COMPATF_MASKEDMIDTEX |
					COMPATF_LIMITPAIN | COMPATF_INVISIBILITY | COMPATF_VILEGHOSTS;

				flags2 =
					COMPATF2_FLOORMOVE | COMPATF2_EXPLODE1 | COMPATF2_NOMBF21 | COMPATF2_POINTONLINE;
			}
			else if (length == 4 && !strnicmp("boom", data, 4))
			{
				flags1 =
					COMPATF_TRACE | COMPATF_SOUNDTARGET | COMPATF_BOOMSCROLL | COMPATF_MISSILECLIP | COMPATF_MASKEDMIDTEX |
					COMPATF_INVISIBILITY;

				flags2 =
					COMPATF2_EXPLODE1 | COMPATF2_NOMBF21 | COMPATF2_POINTONLINE;
			}
			else if (length == 3 && !strnicmp("mbf", data, 3))
			{
				flags1 =
					COMPATF_TRACE | COMPATF_SOUNDTARGET | COMPATF_BOOMSCROLL | COMPATF_MISSILECLIP | COMPATF_MUSHROOM |
					COMPATF_MBFMONSTERMOVE | COMPATF_NOBLOCKFRIENDS | COMPATF_MASKEDMIDTEX | COMPATF_INVISIBILITY;

				flags2 =
					COMPATF2_EXPLODE1 | COMPATF2_AVOID_HAZARDS | COMPATF2_STAYONLIFT | COMPATF2_NOMBF21 | COMPATF2_POINTONLINE;
			}
			else if (length == 5 && !strnicmp("mbf21", data, 5))
			{
				flags1 =
					COMPATF_TRACE | COMPATF_SOUNDTARGET | COMPATF_BOOMSCROLL | COMPATF_MISSILECLIP | COMPATF_MUSHROOM |
					COMPATF_MASKEDMIDTEX | COMPATF_INVISIBILITY;

				flags2 =
					COMPATF2_EXPLODE1 | COMPATF2_AVOID_HAZARDS | COMPATF2_STAYONLIFT | COMPATF2_POINTONLINE;
			}
		}
	}

	// Parse the default MAPINFO for the current game. This lump *MUST* come from zdoom.pk3.
	if (basemapinfo.IsNotEmpty())
	{
		FMapInfoParser parse;
		level_info_t defaultinfo;
		int baselump = fileSystem.GetNumForFullName(basemapinfo.GetChars());
		if (fileSystem.GetFileContainer(baselump) > 0)
		{
			I_FatalError("File %s is overriding core lump %s.", 
				fileSystem.GetResourceFileName(fileSystem.GetFileContainer(baselump)), basemapinfo.GetChars());
		}
		parse.ParseMapInfo(baselump, gamedefaults, defaultinfo);
	}
	gamedefaults.compatflags |= flags1;
	gamedefaults.compatmask |= flags1;
	gamedefaults.compatflags2 |= flags2;
	gamedefaults.compatmask2 |= flags2;
	
	static const char *mapinfonames[] = { "MAPINFO", "ZMAPINFO", "UMAPINFO", NULL };
	int nindex;

	// Parse any extra MAPINFOs.
	while ((lump = fileSystem.FindLumpMulti (mapinfonames, &lastlump, false, &nindex)) != -1)
	{
		if (nindex == 0)
		{
			// If this lump is named MAPINFO we need to check if the same WAD contains a ZMAPINFO lump.
			// If that exists we need to skip this one.

			int wad = fileSystem.GetFileContainer(lump);
			int altlump = fileSystem.CheckNumForName("ZMAPINFO", FileSys::ns_global, wad, true);

			if (altlump >= 0) continue;
		}
		else if (nindex == 2)
		{
			// MAPINFO and ZMAPINFO will override UMAPINFO if in the same WAD.
			int wad = fileSystem.GetFileContainer(lump);
			int altlump = fileSystem.CheckNumForName("ZMAPINFO", FileSys::ns_global, wad, true);
			if (altlump >= 0) continue;
			altlump = fileSystem.CheckNumForName("MAPINFO", FileSys::ns_global, wad, true);
			if (altlump >= 0) continue;
		}
		if (nindex != 2)
		{
			CommitUMapinfo(&gamedefaults);	// UMAPINFOs are collected until a regular MAPINFO is found so that they properly use the base settings.
			FMapInfoParser parse(nindex == 1 ? FMapInfoParser::FMT_New : FMapInfoParser::FMT_Unknown);
			level_info_t defaultinfo;
			parse.ParseMapInfo(lump, gamedefaults, defaultinfo);
		}
		else
		{
			ParseUMapInfo(lump);
		}
	}
	CommitUMapinfo(&gamedefaults);	// commit remaining UMPAINFOs.

	if (AllEpisodes.Size() == 0)
	{
		I_FatalError ("You cannot use clearepisodes in a MAPINFO if you do not define any new episodes after it.");
	}
	if (AllSkills.Size() == 0)
	{
		I_FatalError ("You cannot use clearskills in a MAPINFO if you do not define any new skills after it.");
	}

	// Find any and all secret maps.
	for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
	{
		if (wadlevelinfos[i].NextSecretMap.IsNotEmpty() && wadlevelinfos[i].NextSecretMap != wadlevelinfos[i].NextMap)
		{
			secretMaps.Push(wadlevelinfos[i].NextSecretMap);
		}
	}
	// ...and then mark them all as secret maps.
	for (unsigned int i = 0; i < secretMaps.Size(); i++)
	{
		auto* li = FindLevelInfo(secretMaps[i].GetChars(), false);
		if (li)
		{
			li->flags3 |= LEVEL3_SECRET;
		}
	}
}

