mirror of
https://github.com/ZDoom/qzdoom.git
synced 2024-11-29 15:32:54 +00:00
579502ab74
SVN r2768 (trunk)
1977 lines
50 KiB
C++
1977 lines
50 KiB
C++
/*
|
|
** 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 "templates.h"
|
|
#include "g_level.h"
|
|
#include "sc_man.h"
|
|
#include "w_wad.h"
|
|
#include "cmdlib.h"
|
|
#include "v_video.h"
|
|
#include "p_lnspec.h"
|
|
#include "p_setup.h"
|
|
#include "i_system.h"
|
|
#include "gi.h"
|
|
#include "gstrings.h"
|
|
#include "farchive.h"
|
|
#include "p_acs.h"
|
|
#include "doomstat.h"
|
|
#include "d_player.h"
|
|
#include "autosegs.h"
|
|
#include "version.h"
|
|
#include "v_text.h"
|
|
|
|
int FindEndSequence (int type, const char *picname);
|
|
|
|
|
|
TArray<cluster_info_t> wadclusterinfos;
|
|
TArray<level_info_t> wadlevelinfos;
|
|
|
|
level_info_t TheDefaultLevelInfo;
|
|
static cluster_info_t TheDefaultClusterInfo;
|
|
|
|
TArray<FEpisode> AllEpisodes;
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
static int FindWadLevelInfo (const char *name)
|
|
{
|
|
for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
|
|
if (!strnicmp (name, wadlevelinfos[i].mapname, 8))
|
|
return i;
|
|
|
|
return -1;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
level_info_t *FindLevelInfo (const char *mapname)
|
|
{
|
|
int i;
|
|
|
|
if ((i = FindWadLevelInfo (mapname)) > -1)
|
|
{
|
|
return &wadlevelinfos[i];
|
|
}
|
|
else
|
|
{
|
|
if (TheDefaultLevelInfo.LevelName.IsEmpty())
|
|
{
|
|
uppercopy(TheDefaultLevelInfo.skypic1, "SKY1");
|
|
uppercopy(TheDefaultLevelInfo.skypic2, "SKY1");
|
|
TheDefaultLevelInfo.LevelName = "Unnamed";
|
|
}
|
|
return &TheDefaultLevelInfo;
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
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;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
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].ClearSnapshot();
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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[0] = 0;
|
|
mapbg[0] = 0;
|
|
levelnum = 0;
|
|
pname[0] = 0;
|
|
nextmap[0] = 0;
|
|
secretmap[0] = 0;
|
|
strcpy (skypic1, "-NOFLAT-");
|
|
strcpy (skypic2, "-NOFLAT-");
|
|
cluster = 0;
|
|
partime = 0;
|
|
sucktime = 0;
|
|
flags = 0;
|
|
flags2 = gameinfo.gametype == GAME_Hexen? 0 : LEVEL2_LAXMONSTERACTIVATION;
|
|
Music = "";
|
|
LevelName = "";
|
|
strcpy (fadetable, "COLORMAP");
|
|
WallHorizLight = -8;
|
|
WallVertLight = +8;
|
|
f1[0] = 0;
|
|
musicorder = 0;
|
|
snapshot = NULL;
|
|
snapshotVer = 0;
|
|
defered = 0;
|
|
skyspeed1 = skyspeed2 = 0.f;
|
|
fadeto = 0;
|
|
outsidefog = 0xff000000;
|
|
cdtrack = 0;
|
|
cdid = 0;
|
|
gravity = 0.f;
|
|
aircontrol = 0.f;
|
|
WarpTrans = 0;
|
|
airsupply = 20;
|
|
compatflags = 0;
|
|
compatmask = 0;
|
|
Translator = "";
|
|
RedirectType = 0;
|
|
RedirectMap[0] = 0;
|
|
EnterPic = "";
|
|
ExitPic = "";
|
|
InterMusic = "";
|
|
intermusicorder = 0;
|
|
SoundInfo = "";
|
|
SndSeq = "";
|
|
bordertexture[0] = 0;
|
|
teamdamage = 0.f;
|
|
specialactions.Clear();
|
|
DefaultEnvironment = 0;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
FString level_info_t::LookupLevelName()
|
|
{
|
|
if (flags & LEVEL_LOOKUPLEVELNAME)
|
|
{
|
|
const char *thename;
|
|
const char *lookedup;
|
|
|
|
lookedup = GStrings[LevelName];
|
|
if (lookedup == NULL)
|
|
{
|
|
thename = LevelName;
|
|
}
|
|
else
|
|
{
|
|
char checkstring[32];
|
|
|
|
// Strip out the header from the localized string
|
|
if (mapname[0] == 'E' && mapname[2] == 'M')
|
|
{
|
|
mysnprintf (checkstring, countof(checkstring), "%s: ", mapname);
|
|
}
|
|
else if (mapname[0] == 'M' && mapname[1] == 'A' && mapname[2] == 'P')
|
|
{
|
|
mysnprintf (checkstring, countof(checkstring), "%d: ", atoi(mapname + 3));
|
|
}
|
|
thename = strstr (lookedup, checkstring);
|
|
if (thename == NULL)
|
|
{
|
|
thename = lookedup;
|
|
}
|
|
else
|
|
{
|
|
thename += strlen (checkstring);
|
|
}
|
|
}
|
|
return thename;
|
|
}
|
|
else return LevelName;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void level_info_t::ClearSnapshot()
|
|
{
|
|
if (snapshot != NULL) delete snapshot;
|
|
snapshot = NULL;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void level_info_t::ClearDefered()
|
|
{
|
|
acsdefered_t *def = defered;
|
|
while (def)
|
|
{
|
|
acsdefered_t *next = def->next;
|
|
delete def;
|
|
def = next;
|
|
}
|
|
defered = NULL;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
level_info_t *level_info_t::CheckLevelRedirect ()
|
|
{
|
|
if (RedirectType != NAME_None)
|
|
{
|
|
const PClass *type = PClass::FindClass(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(RedirectMap))
|
|
{
|
|
return FindLevelInfo(RedirectMap);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
bool level_info_t::isValid()
|
|
{
|
|
return mapname[0] != 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.Compare("}"))
|
|
{
|
|
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(long(dest.Len()-1));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseLumpOrTextureName(char *name)
|
|
{
|
|
sc.MustGetString();
|
|
uppercopy(name, sc.String);
|
|
name[8]=0;
|
|
}
|
|
|
|
void FMapInfoParser::ParseLumpOrTextureName(FString &name)
|
|
{
|
|
sc.MustGetString();
|
|
name = sc.String;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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 if (sc.Compare("exittext"))
|
|
{
|
|
ParseAssign();
|
|
if (ParseLookupName(clusterinfo->ExitText))
|
|
clusterinfo->flags |= CLUSTER_LOOKUPEXITTEXT;
|
|
}
|
|
else if (sc.Compare("music"))
|
|
{
|
|
int order = 0;
|
|
|
|
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("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 (!ParseCloseBrace())
|
|
{
|
|
// Unknown
|
|
sc.ScriptMessage("Unknown property '%s' found in map definition\n", sc.String);
|
|
SkipToNext();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
CheckEndOfFile("cluster");
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// ParseNextMap
|
|
// Parses a next map field
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseNextMap(char *mapname)
|
|
{
|
|
EndSequence newSeq;
|
|
bool useseq = false;
|
|
|
|
if (sc.CheckNumber())
|
|
{
|
|
if (HexenHack)
|
|
{
|
|
mysnprintf (mapname, 9, "&wt@%02d", sc.Number);
|
|
}
|
|
else
|
|
{
|
|
mysnprintf (mapname, 9, "MAP%02d", sc.Number);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
|
|
sc.MustGetString();
|
|
if (sc.Compare("endgame"))
|
|
{
|
|
if (!sc.CheckString("{"))
|
|
{
|
|
// Make Demon Eclipse work again
|
|
sc.UnGet();
|
|
goto standard_endgame;
|
|
}
|
|
newSeq.Advanced = true;
|
|
newSeq.EndType = END_Pic1;
|
|
newSeq.PlayTheEnd = false;
|
|
newSeq.MusicLooping = true;
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.Compare("pic"))
|
|
{
|
|
ParseAssign();
|
|
sc.MustGetString();
|
|
newSeq.EndType = END_Pic;
|
|
newSeq.PicName = sc.String;
|
|
}
|
|
else if (sc.Compare("hscroll"))
|
|
{
|
|
ParseAssign();
|
|
newSeq.EndType = END_Bunny;
|
|
sc.MustGetString();
|
|
newSeq.PicName = sc.String;
|
|
ParseComma();
|
|
sc.MustGetString();
|
|
newSeq.PicName2 = sc.String;
|
|
if (CheckNumber())
|
|
newSeq.PlayTheEnd = !!sc.Number;
|
|
}
|
|
else if (sc.Compare("vscroll"))
|
|
{
|
|
ParseAssign();
|
|
newSeq.EndType = END_Demon;
|
|
sc.MustGetString();
|
|
newSeq.PicName = sc.String;
|
|
ParseComma();
|
|
sc.MustGetString();
|
|
newSeq.PicName2 = sc.String;
|
|
}
|
|
else if (sc.Compare("cast"))
|
|
{
|
|
newSeq.EndType = END_Cast;
|
|
}
|
|
else if (sc.Compare("music"))
|
|
{
|
|
ParseAssign();
|
|
sc.MustGetString();
|
|
newSeq.Music = sc.String;
|
|
if (CheckNumber())
|
|
{
|
|
newSeq.MusicLooping = !!sc.Number;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (format_type == FMT_New)
|
|
{
|
|
// Unknown
|
|
sc.ScriptMessage("Unknown property '%s' found in endgame definition\n", sc.String);
|
|
SkipToNext();
|
|
}
|
|
else
|
|
{
|
|
sc.ScriptError("Unknown property '%s' found in endgame definition\n", sc.String);
|
|
}
|
|
|
|
}
|
|
}
|
|
useseq = true;
|
|
}
|
|
else if (strnicmp (sc.String, "EndGame", 7) == 0)
|
|
{
|
|
int type;
|
|
switch (sc.String[7])
|
|
{
|
|
case '1': type = END_Pic1; break;
|
|
case '2': type = END_Pic2; break;
|
|
case '3': type = END_Bunny; break;
|
|
case 'C': type = END_Cast; break;
|
|
case 'W': type = END_Underwater; break;
|
|
case 'S': type = END_Strife; break;
|
|
standard_endgame:
|
|
default: type = END_Pic3; break;
|
|
}
|
|
newSeq.EndType = type;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endpic"))
|
|
{
|
|
ParseComma();
|
|
sc.MustGetString ();
|
|
newSeq.EndType = END_Pic;
|
|
newSeq.PicName = sc.String;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endbunny"))
|
|
{
|
|
newSeq.EndType = END_Bunny;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endcast"))
|
|
{
|
|
newSeq.EndType = END_Cast;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("enddemon"))
|
|
{
|
|
newSeq.EndType = END_Demon;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endchess"))
|
|
{
|
|
newSeq.EndType = END_Chess;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endunderwater"))
|
|
{
|
|
newSeq.EndType = END_Underwater;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endbuystrife"))
|
|
{
|
|
newSeq.EndType = END_BuyStrife;
|
|
useseq = true;
|
|
}
|
|
else if (sc.Compare("endtitle"))
|
|
{
|
|
newSeq.EndType = END_TitleScreen;
|
|
useseq = true;
|
|
}
|
|
else
|
|
{
|
|
strncpy (mapname, sc.String, 8);
|
|
mapname[8] = 0;
|
|
}
|
|
if (useseq)
|
|
{
|
|
int seqnum = -1;
|
|
|
|
if (!newSeq.Advanced)
|
|
{
|
|
seqnum = FindEndSequence (newSeq.EndType, newSeq.PicName);
|
|
}
|
|
|
|
if (seqnum == -1)
|
|
{
|
|
seqnum = (int)EndSequences.Push (newSeq);
|
|
}
|
|
// mapname can point to nextmap and secretmap which are both 12 characters long
|
|
mysnprintf(mapname, 11, "enDSeQ%04x", (WORD)seqnum);
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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(secretnext, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseNextMap(info->secretmap);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(secret, true) // Just an alias for secretnext, for Vavoom compatibility
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseNextMap(info->secretmap);
|
|
}
|
|
|
|
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 * (35. / 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 * (35. / 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(NULL, parse.sc.String);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(outsidefog, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->outsidefog = V_GetColor(NULL, parse.sc.String);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(titlepatch, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseLumpOrTextureName(info->pname);
|
|
}
|
|
|
|
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);
|
|
// Flag the level so that the $MAP command doesn't override this.
|
|
info->flags2 |= LEVEL2_MUSICDEFINED;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(intermusic, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseMusic(info->InterMusic, info->intermusicorder);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(fadetable, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseLumpOrTextureName(info->fadetable);
|
|
}
|
|
|
|
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 = (SBYTE)clamp (parse.sc.Number / 2, -128, 127);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(horizwallshade, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->WallHorizLight = (SBYTE)clamp (parse.sc.Number / 2, -128, 127);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(gravity, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetFloat();
|
|
info->gravity = float(parse.sc.Float);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(aircontrol, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetFloat();
|
|
info->aircontrol = float(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(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'");
|
|
}
|
|
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(redirect, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->RedirectType = parse.sc.String;
|
|
parse.ParseComma();
|
|
parse.ParseLumpOrTextureName(info->RedirectMap);
|
|
}
|
|
|
|
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(bordertexture, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseLumpOrTextureName(info->bordertexture);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(f1, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseLumpOrTextureName(info->f1);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(teamdamage, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetFloat();
|
|
info->teamdamage = float(parse.sc.Float);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(mapbackground, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseLumpOrTextureName(info->mapbg);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// All flag based map options
|
|
//
|
|
//==========================================================================
|
|
|
|
enum EMIType
|
|
{
|
|
MITYPE_IGNORE,
|
|
MITYPE_EATNEXT,
|
|
MITYPE_SETFLAG,
|
|
MITYPE_CLRFLAG,
|
|
MITYPE_SCFLAGS,
|
|
MITYPE_SETFLAG2,
|
|
MITYPE_CLRFLAG2,
|
|
MITYPE_SCFLAGS2,
|
|
MITYPE_COMPATFLAG,
|
|
};
|
|
|
|
struct MapInfoFlagHandler
|
|
{
|
|
const char *name;
|
|
EMIType type;
|
|
DWORD 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 },
|
|
{ "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_FORCENOSKYSTRETCH, 0 },
|
|
{ "skystretch", MITYPE_CLRFLAG, LEVEL_FORCENOSKYSTRETCH, 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 },
|
|
{ "activateowndeathspecials", MITYPE_SETFLAG, LEVEL_ACTOWNSPECIAL, 0 },
|
|
{ "killeractivatesdeathspecials", MITYPE_CLRFLAG, LEVEL_ACTOWNSPECIAL, 0 },
|
|
{ "missilesactivateimpactlines", MITYPE_SETFLAG2, LEVEL2_MISSILESACTIVATEIMPACT, 0 },
|
|
{ "missileshootersactivetimpactlines",MITYPE_CLRFLAG2, LEVEL2_MISSILESACTIVATEIMPACT, 0 },
|
|
{ "noinventorybar", MITYPE_SETFLAG, LEVEL_NOINVENTORYBAR, 0 },
|
|
{ "deathslideshow", MITYPE_SETFLAG2, LEVEL2_DEATHSLIDESHOW, 0 },
|
|
{ "strictmonsteractivation", MITYPE_CLRFLAG2, LEVEL2_LAXMONSTERACTIVATION, LEVEL2_LAXACTIVATIONMAPINFO },
|
|
{ "laxmonsteractivation", MITYPE_SETFLAG2, LEVEL2_LAXMONSTERACTIVATION, LEVEL2_LAXACTIVATIONMAPINFO },
|
|
{ "additive_scrollers", MITYPE_COMPATFLAG, COMPATF_BOOMSCROLL},
|
|
{ "keepfullinventory", MITYPE_SETFLAG2, LEVEL2_KEEPFULLINVENTORY, 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 },
|
|
{ "unfreezesingleplayerconversations",MITYPE_SETFLAG2, LEVEL2_CONV_SINGLE_UNFREEZE, 0 },
|
|
{ "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes
|
|
{ "compat_shorttex", MITYPE_COMPATFLAG, COMPATF_SHORTTEX},
|
|
{ "compat_stairs", MITYPE_COMPATFLAG, COMPATF_STAIRINDEX},
|
|
{ "compat_limitpain", MITYPE_COMPATFLAG, COMPATF_LIMITPAIN},
|
|
{ "compat_nopassover", MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ},
|
|
{ "compat_notossdrops", MITYPE_COMPATFLAG, COMPATF_NOTOSSDROPS},
|
|
{ "compat_useblocking", MITYPE_COMPATFLAG, COMPATF_USEBLOCKING},
|
|
{ "compat_nodoorlight", MITYPE_COMPATFLAG, COMPATF_NODOORLIGHT},
|
|
{ "compat_ravenscroll", MITYPE_COMPATFLAG, COMPATF_RAVENSCROLL},
|
|
{ "compat_soundtarget", MITYPE_COMPATFLAG, COMPATF_SOUNDTARGET},
|
|
{ "compat_dehhealth", MITYPE_COMPATFLAG, COMPATF_DEHHEALTH},
|
|
{ "compat_trace", MITYPE_COMPATFLAG, COMPATF_TRACE},
|
|
{ "compat_dropoff", MITYPE_COMPATFLAG, COMPATF_DROPOFF},
|
|
{ "compat_boomscroll", MITYPE_COMPATFLAG, COMPATF_BOOMSCROLL},
|
|
{ "compat_invisibility", MITYPE_COMPATFLAG, COMPATF_INVISIBILITY},
|
|
{ "compat_silent_instant_floors", MITYPE_COMPATFLAG, COMPATF_SILENT_INSTANT_FLOORS},
|
|
{ "compat_sectorsounds", MITYPE_COMPATFLAG, COMPATF_SECTORSOUNDS},
|
|
{ "compat_missileclip", MITYPE_COMPATFLAG, COMPATF_MISSILECLIP},
|
|
{ "compat_crossdropoff", MITYPE_COMPATFLAG, COMPATF_CROSSDROPOFF},
|
|
{ "compat_anybossdeath", MITYPE_COMPATFLAG, COMPATF_ANYBOSSDEATH},
|
|
{ "compat_minotaur", MITYPE_COMPATFLAG, COMPATF_MINOTAUR},
|
|
{ "compat_mushroom", MITYPE_COMPATFLAG, COMPATF_MUSHROOM},
|
|
{ "compat_mbfmonstermove", MITYPE_COMPATFLAG, COMPATF_MBFMONSTERMOVE},
|
|
{ "compat_corpsegibs", MITYPE_COMPATFLAG, COMPATF_CORPSEGIBS},
|
|
{ "compat_noblockfriends", MITYPE_COMPATFLAG, COMPATF_NOBLOCKFRIENDS},
|
|
{ "compat_spritesort", MITYPE_COMPATFLAG, COMPATF_SPRITESORT},
|
|
{ "compat_light", MITYPE_COMPATFLAG, COMPATF_LIGHT},
|
|
{ "compat_polyobj", MITYPE_COMPATFLAG, COMPATF_POLYOBJ},
|
|
{ "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}
|
|
};
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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:
|
|
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:
|
|
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_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;
|
|
else info.compatflags &= ~handler->data1;
|
|
info.compatmask |= handler->data1;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// should never happen
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FAutoSegIterator probe(YRegHead, YRegTail);
|
|
bool success = false;
|
|
|
|
while (*++probe != NULL)
|
|
{
|
|
if (sc.Compare(((FMapOptInfo *)(*probe))->name))
|
|
{
|
|
if (!((FMapOptInfo *)(*probe))->old && format_type != FMT_New)
|
|
{
|
|
sc.ScriptError("MAPINFO option '%s' requires the new MAPINFO format", sc.String);
|
|
}
|
|
((FMapOptInfo *)(*probe))->handler(*this, &info);
|
|
success = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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
|
|
char maptemp[8];
|
|
mysnprintf (maptemp, countof(maptemp), "MAP%02d", sc.Number);
|
|
mapname = maptemp;
|
|
HexenHack = true;
|
|
}
|
|
else
|
|
{
|
|
sc.MustGetString();
|
|
mapname = sc.String;
|
|
}
|
|
int levelindex = FindWadLevelInfo (mapname);
|
|
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;
|
|
|
|
}
|
|
|
|
uppercopy (levelinfo->mapname, mapname);
|
|
levelinfo->mapname[8] = 0;
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
|
|
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;
|
|
char map[9];
|
|
FString pic;
|
|
FString name;
|
|
bool remove = false;
|
|
char key = 0;
|
|
bool noskill = false;
|
|
bool optional = false;
|
|
bool extended = false;
|
|
|
|
// Get map name
|
|
sc.MustGetString ();
|
|
uppercopy (map, sc.String);
|
|
map[8] = 0;
|
|
|
|
if (sc.CheckString ("teaser"))
|
|
{
|
|
sc.MustGetString ();
|
|
if (gameinfo.flags & GI_SHAREWARE)
|
|
{
|
|
uppercopy (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 = sc.String;
|
|
}
|
|
else if (sc.Compare ("picname"))
|
|
{
|
|
ParseAssign();
|
|
sc.MustGetString ();
|
|
pic = 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 (!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))
|
|
{
|
|
// 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
|
|
{
|
|
FEpisode *epi = &AllEpisodes[AllEpisodes.Reserve(1)];
|
|
|
|
epi->mEpisodeMap = map;
|
|
epi->mEpisodeName = name;
|
|
epi->mPicName = pic;
|
|
epi->mShortcut = tolower(key);
|
|
epi->mNoSkill = noskill;
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// Clears episode definitions
|
|
//
|
|
//==========================================================================
|
|
|
|
void ClearEpisodes()
|
|
{
|
|
AllEpisodes.Clear();
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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;
|
|
|
|
while (sc.GetString ())
|
|
{
|
|
if (sc.Compare("include"))
|
|
{
|
|
sc.MustGetString();
|
|
int inclump = Wads.CheckNumForFullName(sc.String, true);
|
|
if (inclump < 0)
|
|
{
|
|
sc.ScriptError("include file '%s' not found", sc.String);
|
|
}
|
|
if (Wads.GetLumpFile(sc.LumpNum) != Wads.GetLumpFile(inclump))
|
|
{
|
|
// Do not allow overriding includes from the default MAPINFO
|
|
if (Wads.GetLumpFile(sc.LumpNum) == 0)
|
|
{
|
|
I_FatalError("File %s is overriding core lump %s.",
|
|
Wads.GetWadFullName(Wads.GetLumpFile(inclump)), sc.String);
|
|
}
|
|
}
|
|
FScanner saved_sc = sc;
|
|
ParseMapInfo(inclump, gamedefaults, defaultinfo);
|
|
sc = saved_sc;
|
|
}
|
|
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 (strcmp (levelinfo->skypic2, "-NOFLAT-") == 0)
|
|
{
|
|
strcpy (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"))
|
|
{
|
|
ClearEpisodes();
|
|
}
|
|
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
|
|
{
|
|
sc.ScriptError("%s: Unknown top level keyword", sc.String);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// G_ParseMapInfo
|
|
// Parses the MAPINFO lumps of all loaded WADs and generates
|
|
// data for wadlevelinfos and wadclusterinfos.
|
|
//
|
|
//==========================================================================
|
|
|
|
void G_ParseMapInfo (const char *basemapinfo)
|
|
{
|
|
int lump, lastlump = 0;
|
|
level_info_t gamedefaults;
|
|
|
|
atterm(ClearEpisodes);
|
|
|
|
// Parse the default MAPINFO for the current game. This lump *MUST* come from zdoom.pk3.
|
|
if (basemapinfo != NULL)
|
|
{
|
|
FMapInfoParser parse;
|
|
level_info_t defaultinfo;
|
|
int baselump = Wads.GetNumForFullName(basemapinfo);
|
|
if (Wads.GetLumpFile(baselump) > 0)
|
|
{
|
|
I_FatalError("File %s is overriding core lump %s.",
|
|
Wads.GetWadFullName(Wads.GetLumpFile(baselump)), basemapinfo);
|
|
}
|
|
parse.ParseMapInfo(baselump, gamedefaults, defaultinfo);
|
|
}
|
|
|
|
static const char *mapinfonames[] = { "MAPINFO", "ZMAPINFO", NULL };
|
|
int nindex;
|
|
|
|
// Parse any extra MAPINFOs.
|
|
while ((lump = Wads.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 = Wads.GetLumpFile(lump);
|
|
int altlump = Wads.CheckNumForName("ZMAPINFO", ns_global, wad, true);
|
|
|
|
if (altlump >= 0) continue;
|
|
}
|
|
FMapInfoParser parse(nindex == 1? FMapInfoParser::FMT_New : FMapInfoParser::FMT_Unknown);
|
|
level_info_t defaultinfo;
|
|
parse.ParseMapInfo(lump, gamedefaults, defaultinfo);
|
|
}
|
|
EndSequences.ShrinkToFit ();
|
|
|
|
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.");
|
|
}
|
|
}
|
|
|