mirror of
https://github.com/DrBeef/Raze.git
synced 2024-11-23 04:22:24 +00:00
1788 lines
45 KiB
C++
1788 lines
45 KiB
C++
/*
|
|
** g_level.cpp
|
|
** Parses MAPINFO
|
|
**
|
|
**---------------------------------------------------------------------------
|
|
** Copyright 1998-2016 Randy Heit
|
|
** Copyright 2009-2021 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 "mapinfo.h"
|
|
#include "g_mapinfo.h"
|
|
|
|
#include "filesystem.h"
|
|
#include "cmdlib.h"
|
|
#include "v_video.h"
|
|
#include "gi.h"
|
|
#include "gstrings.h"
|
|
#include "autosegs.h"
|
|
#include "i_system.h"
|
|
#include "gamecontrol.h"
|
|
#include "coreactor.h"
|
|
#include "texinfo.h"
|
|
|
|
#include "buildtiles.h"
|
|
|
|
extern TArray<ClusterDef> clusters;
|
|
extern TArray<VolumeRecord> volumes;
|
|
extern TArray<TPointer<MapRecord>> mapList; // must be allocated as pointers because it can whack the currentlLevel pointer if this was a flat array.
|
|
|
|
static MapRecord TheDefaultLevelInfo;
|
|
static ClusterDef TheDefaultClusterInfo;
|
|
|
|
TArray<int> ParsedLumps(8);
|
|
|
|
constexpr int texlookupflags = FTextureManager::TEXMAN_ReturnAll | FTextureManager::TEXMAN_TryAny | FTextureManager::TEXMAN_ForceLookup;
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseOpenBrace()
|
|
{
|
|
sc.MustGetStringName("{");
|
|
sc.SetCMode(true);
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
bool FMapInfoParser::ParseCloseBrace()
|
|
{
|
|
return sc.Compare("}");
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
bool FMapInfoParser::CheckAssign()
|
|
{
|
|
return sc.CheckString("=");
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseAssign()
|
|
{
|
|
sc.MustGetStringName("=");
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::MustParseAssign()
|
|
{
|
|
sc.MustGetStringName("=");
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseComma()
|
|
{
|
|
sc.MustGetStringName(",");
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
bool FMapInfoParser::CheckNumber()
|
|
{
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetNumber();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
bool FMapInfoParser::CheckFloat()
|
|
{
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetFloat();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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 (sc.End)
|
|
{
|
|
sc.ScriptError("Unexpected end of file in %s definition", block);
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// ParseLookupname
|
|
//
|
|
//==========================================================================
|
|
|
|
bool FMapInfoParser::ParseLookupName(FString &dest)
|
|
{
|
|
sc.MustGetString();
|
|
dest = sc.String;
|
|
return true;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseLumpOrTextureName(FString &name)
|
|
{
|
|
sc.MustGetString();
|
|
name = sc.String;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseMusic(FString &name, int &order)
|
|
{
|
|
sc.MustGetString();
|
|
name = sc.String;
|
|
if (CheckNumber())
|
|
{
|
|
order = sc.Number;
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseConstants()
|
|
{
|
|
int num = -1;
|
|
|
|
// this block deliberately uses a 'flag = texture, texture...' syntax because it is a lot easier to handle than doing the reverse
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
// Do not use internal lookup here because this code must be able to gracefully skip the definition if the flag constant does not exist.
|
|
// This also blocks passing in literal numbers which is quite intentional.
|
|
sc.MustGetString();
|
|
FString cname = sc.String;
|
|
ParseAssign();
|
|
sc.MustGetNumber(true);
|
|
sc.AddSymbol(cname, sc.Number);
|
|
|
|
} while (sc.CheckString(","));
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseSpawnClasses()
|
|
{
|
|
FString fn;
|
|
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
// This will need some reworking once we can use real textures.
|
|
int clipdist = -1;
|
|
int num = -1;
|
|
int base = -1;
|
|
int basetex = -1;
|
|
int brokentex = -1;
|
|
int fullbright = 0;
|
|
int flags = 0;
|
|
FSoundID sound = NO_SOUND;
|
|
PClassActor* actor = nullptr;
|
|
|
|
sc.MustGetString();
|
|
char* p;
|
|
num = (int)strtol(sc.String, &p, 10);
|
|
if (num < 0 || *p)
|
|
{
|
|
sc.ScriptMessage("Invalid spawn number. Must be positive integer, but got '%s'", sc.String);
|
|
SkipToNext();
|
|
continue;
|
|
}
|
|
ParseAssign();
|
|
sc.MustGetString();
|
|
actor = PClass::FindActor(sc.String);
|
|
if (actor == nullptr)
|
|
{
|
|
sc.ScriptMessage("Unknown actor class '%s' for spawn ID # %d", sc.String, num);
|
|
|
|
}
|
|
|
|
if (sc.CheckString(","))
|
|
{
|
|
// prefixing the texture names here with a '*' will render them fullbright.
|
|
sc.MustGetString();
|
|
const char* p = sc.String;
|
|
if (*p == '*') { fullbright |= 1; p++; }
|
|
basetex = tileForName(p);
|
|
if (basetex < 0) sc.ScriptMessage("Unknown texture '%s' in definition for spawn ID # %d", sc.String, num);
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
const char* p = sc.String;
|
|
if (*p)
|
|
{
|
|
if (*p == '*') { fullbright |= 2; p++; }
|
|
brokentex = tileForName(p);
|
|
if (brokentex < 0) sc.ScriptMessage("Unknown texture '%s' in definition for spawn ID # %d", sc.String, num);
|
|
}
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
|
|
sound = S_FindSound(sc.String);
|
|
if (*sc.String && !sound.isvalid()) Printf(TEXTCOLOR_RED "Unknown sound '%s' in definition for spawn ID # %d\n", sc.String, num);
|
|
if (sc.CheckString(","))
|
|
{
|
|
bool cont = true;
|
|
if (sc.CheckNumber())
|
|
{
|
|
clipdist = sc.Number;
|
|
cont = sc.CheckString(",");
|
|
}
|
|
if (cont) do
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.Compare("damaging")) flags |= 1;
|
|
else if (sc.Compare("solid") || sc.Compare("blocking")) flags |= 2;
|
|
else if (sc.Compare("unblocking")) flags |= 4;
|
|
else if (sc.Compare("spawnglass")) flags |= 8;
|
|
else if (sc.Compare("spawnscrap")) flags |= 16;
|
|
else if (sc.Compare("spawnsmoke")) flags |= 32;
|
|
else if (sc.Compare("spawnglass2")) flags |= 64; // Duke has 2 ways of spawning glass debris...
|
|
else sc.ScriptMessage("'%s': Unknown actor class flag", sc.String);
|
|
} while (sc.CheckString(","));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (actor != 0 && num >= 0)
|
|
{
|
|
// todo: check for proper base class
|
|
spawnMap.Insert(num, { actor, basetex, brokentex, sound, int8_t(fullbright), int8_t(clipdist), int16_t(flags) });
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseBreakWall()
|
|
{
|
|
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
FTextureID basetile = FNullTextureID();
|
|
FTextureID breaktile = FNullTextureID();
|
|
int flags = 0;
|
|
FSoundID sound = NO_SOUND;
|
|
VMFunction* handler = nullptr;
|
|
|
|
sc.MustGetString();
|
|
FString basename = sc.String; // save for printing error messages.
|
|
basetile = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
if (!basetile.isValid())
|
|
{
|
|
sc.ScriptMessage("Unknown texture '%s' in breakwall definition", sc.String);
|
|
SkipToNext();
|
|
}
|
|
ParseAssign();
|
|
sc.MustGetString();
|
|
breaktile = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
if (*sc.String && !breaktile.isValid())
|
|
sc.ScriptMessage("Unknown texture '%s' in breakwall definition", sc.String);
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
sound = S_FindSound(sc.String);
|
|
if (*sc.String && !sound.isvalid()) sc.ScriptMessage("Unknown sound '%s' in definition for breakable wall '5s'\n", basename.GetChars());
|
|
|
|
auto saved = sc.SavePos();
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
|
|
size_t p = strcspn(sc.String, ".");
|
|
if (sc.String[p] != 0)
|
|
{
|
|
FName clsname(sc.String, p, false);
|
|
FName funcname = sc.String + p + 1;
|
|
handler = PClass::FindFunction(clsname, funcname);
|
|
if (handler == nullptr)
|
|
sc.ScriptMessage("Call to undefined function %s", sc.String);
|
|
// todo: validate the function's signature. Must be (walltype, TextureID, Sound, DukeActor)
|
|
}
|
|
else sc.RestorePos(saved);
|
|
while (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.Compare("twosided")) flags |= 1;
|
|
else if (sc.Compare("maskedonly")) flags |= 2;
|
|
else sc.ScriptMessage("'%s': Unknown breakable flag", sc.String);
|
|
}
|
|
}
|
|
}
|
|
breakWallMap.Insert(basetile.GetIndex(), {breaktile, sound, handler, flags});
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseBreakCeiling()
|
|
{
|
|
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
FTextureID basetile = FNullTextureID();
|
|
FTextureID breaktile = FNullTextureID();
|
|
int flags = 0;
|
|
FSoundID sound = NO_SOUND;
|
|
VMFunction* handler = nullptr;
|
|
|
|
sc.MustGetString();
|
|
FString basename = sc.String; // save for printing error messages.
|
|
basetile = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
if (!basetile.isValid())
|
|
{
|
|
sc.ScriptMessage("Unknown texture '%s' in breakceiling definition", sc.String);
|
|
SkipToNext();
|
|
}
|
|
ParseAssign();
|
|
sc.MustGetString();
|
|
breaktile = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
if (*sc.String && !breaktile.isValid()) sc.ScriptMessage("Unknown texture '%s' in breakceiling definition", sc.String);
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
sound = S_FindSound(sc.String);
|
|
if (*sc.String && !sound.isvalid()) sc.ScriptMessage("Unknown sound '%s' in definition for breakable ceiling '5s'\n", basename.GetChars());
|
|
|
|
auto saved = sc.SavePos();
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
|
|
size_t p = strcspn(sc.String, ".");
|
|
if (sc.String[p] != 0)
|
|
{
|
|
FName clsname(sc.String, p, false);
|
|
FName funcname = sc.String + p + 1;
|
|
handler = PClass::FindFunction(clsname, funcname);
|
|
if (handler == nullptr)
|
|
sc.ScriptMessage("Call to undefined function %s", sc.String);
|
|
// todo: validate the function's signature. Must be (sectortype)
|
|
}
|
|
else sc.RestorePos(saved);
|
|
while (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.Compare("lightsout")) flags |= 1; // all internal definitions have these two flags.
|
|
else if (sc.Compare("ceilingglass")) flags |= 2;
|
|
else sc.ScriptMessage("'%s': Unknown breakable flag", sc.String);
|
|
}
|
|
}
|
|
}
|
|
breakCeilingMap.Insert(basetile.GetIndex(), {breaktile, sound, handler, flags});
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseSwitches()
|
|
{
|
|
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
SwitchDef sd{};
|
|
if (switches.Size() == 0) switches.Push(sd); // entry 0 is a non-switch
|
|
sc.MustGetString();
|
|
static const char* types[] = { "switch", "comboswitch", "multiswitch", "accessswitch", nullptr };
|
|
int type = sc.MatchString(types);
|
|
int count = type == 2 ? 4 : 2;
|
|
sd.type = type + 1;
|
|
|
|
bool more = false;
|
|
int state = 0;
|
|
ParseAssign();
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
next:
|
|
sc.MustGetString();
|
|
auto thisframe = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
if (!thisframe.isValid())
|
|
{
|
|
sc.ScriptMessage("Unknown texture '%s' in switch definition", sc.String);
|
|
}
|
|
sd.states[state++] = thisframe;
|
|
if (!sc.CheckString(","))
|
|
{
|
|
more = false;
|
|
if (i < count - 1)
|
|
{
|
|
sc.ScriptMessage("Insufficient arguments in switch definition");
|
|
goto next;
|
|
}
|
|
}
|
|
else more = true;
|
|
}
|
|
if (more)
|
|
{
|
|
do
|
|
{
|
|
sc.MustGetString();
|
|
if (more)
|
|
{
|
|
// check if this is a sound
|
|
auto sound = S_FindSound(sc.String);
|
|
if (sound == NO_SOUND) more = false;
|
|
sd.soundid = sound;
|
|
}
|
|
if (!more)
|
|
{
|
|
if (sc.Compare("shootable"))
|
|
{
|
|
sd.flags |= SwitchDef::shootable;
|
|
}
|
|
else if (sc.Compare("oneway"))
|
|
{
|
|
sd.flags |= SwitchDef::oneway;
|
|
}
|
|
else if (sc.Compare("resettable"))
|
|
{
|
|
sd.flags |= SwitchDef::resettable;
|
|
}
|
|
else if (sc.Compare("nofilter"))
|
|
{
|
|
sd.flags |= SwitchDef::nofilter;
|
|
}
|
|
else
|
|
{
|
|
sc.ScriptMessage("%s: Unknown switch flag ", sc.String);
|
|
}
|
|
}
|
|
more = false;
|
|
} while (sc.CheckString(","));
|
|
}
|
|
unsigned ndx = switches.Push(sd);
|
|
if (sd.flags & SwitchDef::oneway)
|
|
{
|
|
count = 1;
|
|
}
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
AccessExtInfo(sd.states[i]).switchindex = ndx;
|
|
AccessExtInfo(sd.states[i]).switchphase = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseTextureFlags()
|
|
{
|
|
int num = -1;
|
|
|
|
// this block deliberately uses a 'flag = texture, texture...' syntax because it is a lot easier to handle than doing the reverse
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
// Do not use internal lookup here because this code must be able to gracefully skip the definition if the flag constant does not exist.
|
|
// This also blocks passing in literal numbers which is quite intentional.
|
|
sc.MustGetString();
|
|
FName cname(sc.String, true);
|
|
auto lookup = cname == NAME_None ? nullptr : sc.LookupSymbol(cname);
|
|
num = 0;
|
|
if (lookup) num = int(lookup->Number);
|
|
else
|
|
sc.ScriptMessage("'%s': Unknown texture flag", sc.String);
|
|
ParseAssign();
|
|
do
|
|
{
|
|
sc.MustGetString();
|
|
// this must also get null textures and ones not yet loaded.
|
|
auto tex = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
|
|
if (!tex.isValid())
|
|
{
|
|
sc.ScriptMessage("textureflags:Unknown texture name '%s'", sc.String);
|
|
}
|
|
else
|
|
{
|
|
AccessExtInfo(tex).flags |= num;
|
|
}
|
|
|
|
} while (sc.CheckString(","));
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseSurfaceTypes()
|
|
{
|
|
int num = -1;
|
|
|
|
// this block deliberately uses a 'type = texture, texture...' syntax because it is a lot easier to handle than doing the reverse
|
|
sc.MustGetStringName("{");
|
|
while (!sc.CheckString("}"))
|
|
{
|
|
// Do not use internal lookup here because this code must be able to gracefully skip the definition if the flag constant does not exist.
|
|
// This also blocks passing in literal numbers which is quite intentional.
|
|
sc.MustGetString();
|
|
FName cname(sc.String, true);
|
|
auto lookup = cname == NAME_None ? nullptr : sc.LookupSymbol(cname);
|
|
num = 0;
|
|
if (lookup) num = int(lookup->Number);
|
|
else
|
|
sc.ScriptMessage("'%s': Unknown surface type", sc.String);
|
|
ParseAssign();
|
|
do
|
|
{
|
|
sc.MustGetString();
|
|
// this must also get null textures and ones not yet loaded.
|
|
auto tex = TexMan.CheckForTexture(sc.String, ETextureType::Any, texlookupflags);
|
|
|
|
if (!tex.isValid())
|
|
{
|
|
sc.ScriptMessage("textureflags:Unknown texture name '%s'", sc.String);
|
|
}
|
|
else
|
|
{
|
|
AccessExtInfo(tex).surftype = num;
|
|
}
|
|
|
|
} while (sc.CheckString(","));
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseCutscene(CutsceneDef& cdef)
|
|
{
|
|
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")) { 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 = {};
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseCluster()
|
|
{
|
|
sc.MustGetNumber ();
|
|
auto clusterinfo = MustFindCluster(sc.Number);
|
|
|
|
ParseOpenBrace();
|
|
while (sc.GetString())
|
|
{
|
|
if (sc.Compare("clear"))
|
|
{
|
|
*clusterinfo = {};
|
|
}
|
|
else if (sc.Compare("name"))
|
|
{
|
|
ParseAssign();
|
|
ParseLookupName(clusterinfo->name);
|
|
}
|
|
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 (sc.Compare("interbackground"))
|
|
{
|
|
ParseAssign();
|
|
ParseLookupName(clusterinfo->InterBackground);
|
|
}
|
|
else if (!ParseCloseBrace())
|
|
{
|
|
// Unknown
|
|
sc.ScriptMessage("Unknown property '%s' found in cluster definition\n", sc.String);
|
|
SkipToNext();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
CheckEndOfFile("cluster");
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// allow modification of maps defined through legacy means.
|
|
//
|
|
//==========================================================================
|
|
|
|
bool FMapInfoParser::CheckLegacyMapDefinition(FString& mapname)
|
|
{
|
|
if (Internal && (g_gameType & (GAMEFLAG_BLOOD | GAMEFLAG_DUKECOMPAT | GAMEFLAG_SW)) && sc.CheckString("{"))
|
|
{
|
|
sc.MustGetNumber();
|
|
int vol = sc.Number;
|
|
if (!isSWALL())
|
|
{
|
|
// Blood and Duke use volume/level pairs
|
|
sc.MustGetStringName(",");
|
|
sc.MustGetNumber();
|
|
int indx = sc.Number;
|
|
auto map = FindMapByIndexOnly(vol, indx);
|
|
if (!map) mapname = "";
|
|
else mapname = map->labelName;
|
|
}
|
|
else
|
|
{
|
|
// SW only uses the level number
|
|
auto map = FindMapByLevelNum(vol);
|
|
if (!map) mapname = "";
|
|
else mapname = map->labelName;
|
|
}
|
|
sc.MustGetStringName("}");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// ParseNextMap
|
|
// Parses a next map field
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseMapName(FString &mapname)
|
|
{
|
|
if (!CheckLegacyMapDefinition(mapname))
|
|
{
|
|
sc.MustGetString();
|
|
mapname = ExtractFileBase(sc.String);
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// Map options
|
|
//
|
|
//==========================================================================
|
|
|
|
DEFINE_MAP_OPTION(clear, true)
|
|
{
|
|
// Save the names, reset and restore the names
|
|
FString fn = info->fileName;
|
|
FString dn = info->name;
|
|
FString ln = info->labelName;
|
|
*info = *parse.defaultinfoptr;
|
|
info->fileName = fn;
|
|
info->name = dn;
|
|
info->labelName = ln;
|
|
}
|
|
|
|
|
|
DEFINE_MAP_OPTION(levelnum, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->levelNumber = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(next, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseMapName(info->NextMap);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(author, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->Author = parse.sc.String;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(secretnext, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseMapName(info->NextSecret);
|
|
}
|
|
|
|
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.
|
|
MustFindCluster(info->cluster);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(fade, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->fadeto = V_GetColor(parse.sc);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(partime, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->parTime = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(designertime, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->designerTime = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(music, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseMusic(info->music, info->musicorder);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(cdtrack, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->cdSongId = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(intro, true)
|
|
{
|
|
parse.ParseCutscene(info->intro);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(outro, true)
|
|
{
|
|
parse.ParseCutscene(info->outro);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(interbackground, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->InterBackground = parse.sc.String;
|
|
}
|
|
|
|
|
|
/* currently all sounds are precached. This requires significant work on sound management and info collection.
|
|
DEFINE_MAP_OPTION(PrecacheSounds, true)
|
|
{
|
|
parse.ParseAssign();
|
|
|
|
do
|
|
{
|
|
parse.sc.MustGetString();
|
|
FSoundID snd = parse.sc.String;
|
|
if (snd == 0)
|
|
{
|
|
parse.sc.ScriptMessage("Unknown sound \"%s\"", parse.sc.String);
|
|
}
|
|
else
|
|
{
|
|
info->PrecacheSounds.Push(snd);
|
|
}
|
|
} 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(bordertexture, true)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.ParseLumpOrTextureName(info->BorderTexture);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(fogdensity, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->fogdensity = 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(message, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
if (parse.sc.Number < 1 || parse.sc.Number > MAX_MESSAGES) parse.sc.ScriptError("Invalid message ID %d - must be 1..32", parse.sc.Number);
|
|
int num = parse.sc.Number;
|
|
parse.ParseComma();
|
|
parse.sc.MustGetString();
|
|
info->messages[num] = parse.sc.String;
|
|
}
|
|
|
|
/* stuff for later when the new renderer is done.
|
|
DEFINE_MAP_OPTION(lightmode, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
|
|
if ((parse.sc.Number >= 0 && parse.sc.Number <= 4) || parse.sc.Number == 8 || parse.sc.Number == 16)
|
|
{
|
|
info->lightmode = ELightMode(parse.sc.Number);
|
|
}
|
|
else
|
|
{
|
|
parse.sc.ScriptMessage("Invalid light mode %d", parse.sc.Number);
|
|
}
|
|
}
|
|
*/
|
|
|
|
DEFINE_MAP_OPTION(skyrotate, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetFloat();
|
|
info->skyrotatevector.X = (float)parse.sc.Float;
|
|
parse.sc.MustGetStringName(",");
|
|
parse.sc.MustGetFloat();
|
|
info->skyrotatevector.Y = (float)parse.sc.Float;
|
|
parse.sc.MustGetStringName(",");
|
|
parse.sc.MustGetFloat();
|
|
info->skyrotatevector.Z = (float)parse.sc.Float;
|
|
info->skyrotatevector.W = 0;
|
|
info->skyrotatevector.MakeUnit();
|
|
parse.sc.MustGetStringName(",");
|
|
parse.sc.MustGetFloat();
|
|
info->skyrotatevector.W = (float)parse.sc.Float; // W is the rotation speed. This must not be normalized
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(rr_startsound, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->rr_startsound = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(rr_mamaspawn, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->rr_mamaspawn = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(ex_ramses_horiz, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->ex_ramses_horiz = maphoriz(parse.sc.Number);
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(ex_ramses_cdtrack, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetNumber();
|
|
info->ex_ramses_cdtrack = parse.sc.Number;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(ex_ramses_pup, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->ex_ramses_pup = parse.sc.String;
|
|
}
|
|
|
|
DEFINE_MAP_OPTION(ex_ramses_text, false)
|
|
{
|
|
parse.ParseAssign();
|
|
parse.sc.MustGetString();
|
|
info->ex_ramses_text = parse.sc.String;
|
|
}
|
|
|
|
FString ex_ramses_pup;
|
|
FString ex_ramses_text;
|
|
|
|
//==========================================================================
|
|
//
|
|
// All flag based map options
|
|
//
|
|
//==========================================================================
|
|
|
|
enum EMIType
|
|
{
|
|
MITYPE_IGNORE,
|
|
MITYPE_EATNEXT,
|
|
MITYPE_SETFLAG,
|
|
MITYPE_CLRFLAG,
|
|
MITYPE_SCFLAGS,
|
|
MITYPE_SETFLAGG,
|
|
MITYPE_CLRFLAGG,
|
|
MITYPE_SCFLAGSG,
|
|
MITYPE_COMPATFLAG,
|
|
};
|
|
|
|
struct MapInfoFlagHandler
|
|
{
|
|
const char *name;
|
|
EMIType type;
|
|
uint32_t data1, data2;
|
|
int gameflagmask;
|
|
}
|
|
MapFlagHandlers[] =
|
|
{
|
|
{ "nointermission", MITYPE_SETFLAG, LEVEL_NOINTERMISSION, 0, -1 },
|
|
{ "secretexitoverride", MITYPE_SETFLAG, LEVEL_SECRETEXITOVERRIDE, 0, -1 },
|
|
{ "clearinventory", MITYPE_SETFLAG, LEVEL_CLEARINVENTORY, 0, -1 },
|
|
{ "clearweapons", MITYPE_SETFLAG, LEVEL_CLEARWEAPONS, 0, -1 },
|
|
{ "forcenoeog", MITYPE_SETFLAG, LEVEL_FORCENOEOG, 0, -1 },
|
|
{ "wt_bossspawn", MITYPE_SETFLAG, LEVEL_WT_BOSSSPAWN, 0, -1 },
|
|
{ "rrra_hulkspawn", MITYPE_SETFLAGG,LEVEL_RR_HULKSPAWN, 0, GAMEFLAG_RRRA },
|
|
{ "rr_clearmoonshine", MITYPE_SETFLAGG,LEVEL_RR_CLEARMOONSHINE, 0, GAMEFLAG_RR },
|
|
{ "ex_training", MITYPE_SETFLAGG,LEVEL_EX_TRAINING, 0, GAMEFLAG_PSEXHUMED },
|
|
{ "ex_altsound", MITYPE_SETFLAGG,LEVEL_EX_ALTSOUND, 0, GAMEFLAG_PSEXHUMED },
|
|
{ "ex_countdown", MITYPE_SETFLAGG,LEVEL_EX_COUNTDOWN, 0, GAMEFLAG_PSEXHUMED },
|
|
{ "ex_multi", MITYPE_SETFLAGG,LEVEL_EX_MULTI, 0, GAMEFLAG_PSEXHUMED },
|
|
{ "sw_bossmeter_serpent", MITYPE_SETFLAGG,LEVEL_SW_BOSSMETER_SERPENT, 0, GAMEFLAG_SW },
|
|
{ "sw_bossmeter_sumo", MITYPE_SETFLAGG,LEVEL_SW_BOSSMETER_SUMO, 0, GAMEFLAG_SW },
|
|
{ "sw_bossmeter_zilla", MITYPE_SETFLAGG,LEVEL_SW_BOSSMETER_ZILLA, 0, GAMEFLAG_SW },
|
|
{ "sw_deathexit_serpent", MITYPE_SETFLAGG,LEVEL_SW_DEATHEXIT_SERPENT, 0, GAMEFLAG_SW },
|
|
{ "sw_deathexit_serpent_next", MITYPE_SETFLAGG,LEVEL_SW_DEATHEXIT_SERPENT | LEVEL_SW_DEATHEXIT_SERPENT_NEXT, 0, GAMEFLAG_SW },
|
|
{ "sw_deathexit_sumo", MITYPE_SETFLAGG,LEVEL_SW_DEATHEXIT_SUMO, 0, GAMEFLAG_SW },
|
|
{ "sw_deathexit_zilla", MITYPE_SETFLAGG,LEVEL_SW_DEATHEXIT_ZILLA, 0, GAMEFLAG_SW },
|
|
{ "sw_spawnmines", MITYPE_SETFLAGG,LEVEL_SW_SPAWNMINES, 0, GAMEFLAG_SW },
|
|
{ "bossonlycutscene", MITYPE_SETFLAGG,LEVEL_BOSSONLYCUTSCENE, 0, -1 },
|
|
|
|
{ NULL, MITYPE_IGNORE, 0, 0}
|
|
};
|
|
|
|
void PrintCutscene(const char* name, CutsceneDef& cut)
|
|
{
|
|
if (cut.function.IsEmpty() && cut.video.IsEmpty()) return;
|
|
Printf("\t%s\n\t{\n", name);
|
|
if (cut.function.IsNotEmpty())
|
|
{
|
|
Printf("\t\tfunction = %s\n", cut.function.GetChars());
|
|
}
|
|
if (cut.video.IsNotEmpty())
|
|
{
|
|
Printf("\t\tvideo = \"%s\"\n", cut.video.GetChars());
|
|
}
|
|
if (cut.soundName.IsNotEmpty())
|
|
{
|
|
Printf("\t\tsound = \"%s\"\n", cut.soundName.GetChars());
|
|
}
|
|
Printf("\t}\n");
|
|
}
|
|
|
|
CCMD(mapinfo)
|
|
{
|
|
for (auto& vol : volumes)
|
|
{
|
|
Printf("episode %s\n{\n", vol.startmap.GetChars());
|
|
if (vol.name.IsNotEmpty()) Printf("\tname = \"%s\"\n", vol.name.GetChars());
|
|
if (vol.subtitle.IsNotEmpty()) Printf("\tsubtitle = \"%s\"\n{\n", vol.subtitle.GetChars());
|
|
Printf("}\n");
|
|
}
|
|
for (auto& clust : clusters)
|
|
{
|
|
Printf("cluster %d\n{\n", clust.index);
|
|
if (clust.name.IsNotEmpty()) Printf("\tname = \"%s\"\n", clust.name.GetChars());
|
|
if (clust.InterBackground.IsNotEmpty()) Printf("\tInterBackground = %s\n", clust.InterBackground.GetChars());
|
|
PrintCutscene("intro", clust.intro);
|
|
PrintCutscene("outro", clust.outro);
|
|
PrintCutscene("gameover", clust.gameover);
|
|
Printf("}\n");
|
|
}
|
|
for (auto& map : mapList)
|
|
{
|
|
int lump = fileSystem.FindFile(map->fileName);
|
|
if (lump >= 0)
|
|
{
|
|
Printf("map %s \"%s\"\n{\n", map->labelName.GetChars(), map->DisplayName());
|
|
Printf("\tlevelnum = %d\n\tCluster = %d\n", map->levelNumber, map->cluster);
|
|
if (map->Author.IsNotEmpty())
|
|
{
|
|
FString auth = map->Author;
|
|
auth.Substitute("\"", "\\\"");
|
|
Printf("\tAuthor = \"%s\"\n", auth.GetChars());
|
|
}
|
|
if (map->NextMap.IsNotEmpty()) Printf("\tNext = %s\n", map->NextMap.GetChars());
|
|
if (map->NextSecret.IsNotEmpty()) Printf("\tSecretNext = %s\n", map->NextSecret.GetChars());
|
|
if (map->InterBackground.IsNotEmpty()) Printf("\tInterBackground = %s\n", map->InterBackground.GetChars());
|
|
if (map->music.IsNotEmpty()) Printf("\tMusic = \"%s\"\n", map->music.GetChars());
|
|
if (map->musicorder > 0) Printf("\tMusicorder = %d\n", map->musicorder);
|
|
if (map->cdSongId > 0) Printf("\tCDtrack = %d\n", map->cdSongId);
|
|
if (map->parTime) Printf("\tParTime = %d\n", map->parTime);
|
|
if (map->designerTime) Printf("\tDesignerTime = %d\n", map->designerTime);
|
|
for (int i = 0; i < MAX_MESSAGES; i++)
|
|
{
|
|
if (map->messages[i].IsNotEmpty()) Printf("\tMessage = %d, \"%s\"\n", i + 1, map->messages[i].GetChars());
|
|
}
|
|
|
|
for (auto& flagh : MapFlagHandlers)
|
|
{
|
|
if (flagh.type == MITYPE_SETFLAG)
|
|
{
|
|
if (map->flags & flagh.data1) Printf("\t%s\n", flagh.name);
|
|
}
|
|
if (flagh.type == MITYPE_SETFLAGG)
|
|
{
|
|
if (map->gameflags & flagh.data1) Printf("\t%s\n", flagh.name);
|
|
}
|
|
}
|
|
PrintCutscene("intro", map->intro);
|
|
PrintCutscene("outro", map->outro);
|
|
Printf("}\n");
|
|
}
|
|
else
|
|
{
|
|
//Printf("%s - %s (defined but does not exist)\n", map->fileName.GetChars(), map->DisplayName());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// ParseMapDefinition
|
|
// Parses the body of a map definition, including defaultmap etc.
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseMapDefinition(MapRecord &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_SETFLAGG:
|
|
if (!CheckAssign())
|
|
{
|
|
info.gameflags |= handler->data1;
|
|
}
|
|
else
|
|
{
|
|
sc.MustGetNumber();
|
|
if (sc.Number) info.gameflags |= handler->data1;
|
|
else info.gameflags &= ~handler->data1;
|
|
}
|
|
info.gameflags |= handler->data2;
|
|
break;
|
|
|
|
case MITYPE_CLRFLAGG:
|
|
info.gameflags &= ~handler->data1;
|
|
info.gameflags |= handler->data2;
|
|
break;
|
|
case MITYPE_SCFLAGSG:
|
|
info.gameflags = (info.gameflags & handler->data2) | handler->data1;
|
|
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))
|
|
{
|
|
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) || !strnicmp(mapname, "LEV", 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[2] == 'L') &&
|
|
mapname[3] >= '0' && mapname[3] <= '9')
|
|
{
|
|
int epinum = mapname[1] - '0';
|
|
int mapnum = mapname[3] - '0';
|
|
return makelevelnum(epinum, mapnum);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// ParseMapHeader
|
|
// Parses the header of a map definition ('map mapxx mapname')
|
|
//
|
|
//==========================================================================
|
|
static MapRecord sink;
|
|
|
|
MapRecord *FMapInfoParser::ParseMapHeader(MapRecord &defaultinfo)
|
|
{
|
|
FString mapname;
|
|
MapRecord* map;
|
|
|
|
if (!CheckLegacyMapDefinition(mapname))
|
|
{
|
|
ParseLookupName(mapname);
|
|
}
|
|
|
|
if (mapname.IsEmpty())
|
|
{
|
|
map = &sink; // parse over the entire definition but discard the result.
|
|
}
|
|
else
|
|
{
|
|
map = FindMapByName(mapname);
|
|
if (!map)
|
|
{
|
|
map = AllocateMap();
|
|
*map = defaultinfo;
|
|
DefaultExtension(mapname, ".map");
|
|
map->SetFileName(mapname);
|
|
}
|
|
}
|
|
|
|
if (!sc.CheckString("{"))
|
|
{
|
|
sc.MustGetString();
|
|
map->name = sc.String;
|
|
}
|
|
else
|
|
{
|
|
if (map != &sink && map->name.IsEmpty()) sc.ScriptError("Missing level name");
|
|
sc.UnGet();
|
|
}
|
|
if (map->levelNumber <= 0) map->levelNumber = GetDefaultLevelNum(map->labelName);
|
|
return map;
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// 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;
|
|
int flags = 0;
|
|
|
|
// Get map name
|
|
sc.MustGetString ();
|
|
map = sc.String;
|
|
|
|
ParseOpenBrace();
|
|
|
|
while (sc.GetString())
|
|
{
|
|
if (sc.Compare ("optional"))
|
|
{
|
|
flags |= VF_OPTIONAL;
|
|
}
|
|
else if (sc.Compare("sharewarelock"))
|
|
{
|
|
flags |= VF_SHAREWARELOCK;
|
|
}
|
|
else if (sc.Compare ("name"))
|
|
{
|
|
ParseAssign();
|
|
sc.MustGetString ();
|
|
name = sc.String;
|
|
}
|
|
else if (sc.Compare ("remove"))
|
|
{
|
|
remove = true;
|
|
}
|
|
else if (sc.Compare ("key"))
|
|
{
|
|
ParseAssign();
|
|
sc.MustGetString ();
|
|
if (!(sc.String[0] & 0x80)) key = sc.String[0];
|
|
}
|
|
else if (sc.Compare("noskillmenu"))
|
|
{
|
|
flags |= VF_NOSKILL;
|
|
}
|
|
else if (!ParseCloseBrace())
|
|
{
|
|
// Unknown
|
|
sc.ScriptMessage("Unknown property '%s' found in episode definition\n", sc.String);
|
|
SkipToNext();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
CheckEndOfFile("episode");
|
|
|
|
|
|
for (i = 0; i < volumes.Size(); i++)
|
|
{
|
|
if (volumes[i].startmap.CompareNoCase(map) == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (remove)
|
|
{
|
|
// If the remove property is given for an episode, remove it.
|
|
volumes.Delete(i);
|
|
}
|
|
else
|
|
{
|
|
// Only allocate a new entry if this doesn't replace an existing episode.
|
|
if (i >= volumes.Size())
|
|
{
|
|
i = volumes.Reserve(1);
|
|
}
|
|
|
|
auto epi = &volumes[i];
|
|
|
|
epi->startmap = map;
|
|
epi->name = name;
|
|
epi->shortcut = tolower(key);
|
|
epi->flags = flags;
|
|
epi->index = i;
|
|
}
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseCutsceneInfo()
|
|
{
|
|
FString map;
|
|
FString pic;
|
|
FString name;
|
|
|
|
ParseOpenBrace();
|
|
|
|
while (sc.GetString())
|
|
{
|
|
if (sc.Compare("intro"))
|
|
{
|
|
ParseCutscene(globalCutscenes.Intro);
|
|
}
|
|
else if (sc.Compare("defaultmapintro"))
|
|
{
|
|
ParseCutscene(globalCutscenes.DefaultMapIntro);
|
|
}
|
|
else if (sc.Compare("defaultmapoutro"))
|
|
{
|
|
ParseCutscene(globalCutscenes.DefaultMapOutro);
|
|
}
|
|
else if (sc.Compare("defaultgameover"))
|
|
{
|
|
ParseCutscene(globalCutscenes.DefaultGameover);
|
|
}
|
|
else if (sc.Compare("sharewareend"))
|
|
{
|
|
ParseCutscene(globalCutscenes.SharewareEnd);
|
|
}
|
|
else if (sc.Compare("loadscreen"))
|
|
{
|
|
ParseCutscene(globalCutscenes.LoadingScreen);
|
|
}
|
|
else if (!ParseCloseBrace())
|
|
{
|
|
// Unknown
|
|
sc.ScriptMessage("Unknown property '%s' found in cutscene definition\n", sc.String);
|
|
SkipToNext();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
CheckEndOfFile("cutscenes");
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseGameInfo()
|
|
{
|
|
FString map;
|
|
FString pic;
|
|
FString name;
|
|
|
|
ParseOpenBrace();
|
|
|
|
while (sc.GetString())
|
|
{
|
|
if (sc.Compare("summaryscreen"))
|
|
{
|
|
ParseAssign();
|
|
sc.SetCMode(false);
|
|
sc.MustGetString();
|
|
sc.SetCMode(true);
|
|
globalCutscenes.SummaryScreen = sc.String;
|
|
}
|
|
else if (sc.Compare("mpsummaryscreen"))
|
|
{
|
|
ParseAssign();
|
|
sc.SetCMode(false);
|
|
sc.MustGetString();
|
|
sc.SetCMode(true);
|
|
globalCutscenes.MPSummaryScreen = sc.String;
|
|
}
|
|
else if (sc.Compare("statusbarclass"))
|
|
{
|
|
ParseAssign();
|
|
sc.SetCMode(false);
|
|
sc.MustGetString();
|
|
sc.SetCMode(true);
|
|
globalCutscenes.StatusBarClass = sc.String;
|
|
}
|
|
else if (!ParseCloseBrace())
|
|
{
|
|
// Unknown
|
|
sc.ScriptMessage("Unknown property '%s' found in gameinfo definition\n", sc.String);
|
|
SkipToNext();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
CheckEndOfFile("cutscenes");
|
|
}
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// SetLevelNum
|
|
// Avoid duplicate levelnums. The level being set always has precedence.
|
|
//
|
|
//==========================================================================
|
|
|
|
void SetLevelNum (MapRecord *info, int num)
|
|
{
|
|
for (auto& map : mapList)
|
|
{
|
|
|
|
if (map->levelNumber == num)
|
|
map->levelNumber = 0;
|
|
}
|
|
info->levelNumber = num;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
// G_DoParseMapInfo
|
|
// Parses a single MAPINFO lump
|
|
// data for wadlevelinfos and wadclusterinfos.
|
|
//
|
|
//==========================================================================
|
|
|
|
void FMapInfoParser::ParseMapInfo (int lump, MapRecord &gamedefaults, MapRecord &defaultinfo)
|
|
{
|
|
sc.OpenLumpNum(lump);
|
|
Internal = (fileSystem.GetFileContainer(lump) == 0);
|
|
|
|
defaultinfo = gamedefaults;
|
|
defaultinfoptr = &defaultinfo;
|
|
|
|
#if 0 // this check is too dumb and affects constant defining includes as well.
|
|
if (ParsedLumps.Find(lump) != ParsedLumps.Size())
|
|
{
|
|
sc.ScriptMessage("MAPINFO file is processed more than once\n");
|
|
}
|
|
else
|
|
{
|
|
ParsedLumps.Push(lump);
|
|
}
|
|
#endif
|
|
sc.SetCMode(true);
|
|
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.ParseMapInfo(inclump, gamedefaults, defaultinfo);
|
|
}
|
|
else if (sc.Compare("gamedefaults"))
|
|
{
|
|
gamedefaults = {};
|
|
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"))
|
|
{
|
|
auto levelinfo = ParseMapHeader(defaultinfo);
|
|
|
|
ParseMapDefinition(*levelinfo);
|
|
SetLevelNum (levelinfo, levelinfo->levelNumber); // 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("cluster"))
|
|
{
|
|
ParseCluster();
|
|
}
|
|
else if (sc.Compare("episode"))
|
|
{
|
|
ParseEpisodeInfo();
|
|
}
|
|
else if (sc.Compare("clearepisodes"))
|
|
{
|
|
volumes.Clear();
|
|
}
|
|
else if (sc.Compare("clearall"))
|
|
{
|
|
// Wipe out all legacy content to start a fresh definition.
|
|
volumes.Clear();
|
|
mapList.Clear();
|
|
clusters.Clear();
|
|
}
|
|
else if (sc.Compare("cutscenes"))
|
|
{
|
|
ParseCutsceneInfo();
|
|
}
|
|
else if (sc.Compare("gameinfo"))
|
|
{
|
|
ParseGameInfo();
|
|
}
|
|
else if (sc.Compare("spawnclasses"))
|
|
{
|
|
ParseSpawnClasses();
|
|
}
|
|
else if (sc.Compare("breakwalls"))
|
|
{
|
|
ParseBreakWall();
|
|
}
|
|
else if (sc.Compare("breakceiling"))
|
|
{
|
|
ParseBreakCeiling();
|
|
}
|
|
else if (sc.Compare("switches"))
|
|
{
|
|
ParseSwitches();
|
|
}
|
|
else if (sc.Compare("textureflags"))
|
|
{
|
|
ParseTextureFlags();
|
|
}
|
|
else if (sc.Compare("surfacetypes"))
|
|
{
|
|
ParseSurfaceTypes();
|
|
}
|
|
else if (sc.Compare("constants"))
|
|
{
|
|
ParseConstants();
|
|
}
|
|
else if (sc.Compare("clearall"))
|
|
{
|
|
// clears all map and progression related data, so that a mod can start with a clean slate.
|
|
mapList.Clear();
|
|
volumes.Clear();
|
|
clusters.Clear();
|
|
globalCutscenes.DefaultMapIntro = {};
|
|
globalCutscenes.DefaultMapOutro = {};
|
|
globalCutscenes.DefaultGameover = {};
|
|
}
|
|
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 ()
|
|
{
|
|
int lump, lastlump = 0;
|
|
MapRecord gamedefaults;
|
|
|
|
// Parse internal RMAPINFOs.
|
|
while ((lump = fileSystem.FindLumpFullName("engine/rmapinfo.txt", &lastlump, false)) != -1)
|
|
{
|
|
if (fileSystem.GetFileContainer(lump) > 0) break; // only load from raze.pk3
|
|
|
|
FMapInfoParser parse;
|
|
MapRecord defaultinfo;
|
|
parse.ParseMapInfo(lump, gamedefaults, defaultinfo);
|
|
}
|
|
|
|
// Parse any extra RMAPINFOs.
|
|
lastlump = 0;
|
|
while ((lump = fileSystem.FindLump ("RMAPINFO", &lastlump, false)) != -1)
|
|
{
|
|
FMapInfoParser parse;
|
|
MapRecord defaultinfo;
|
|
parse.ParseMapInfo(lump, gamedefaults, defaultinfo);
|
|
}
|
|
|
|
if (volumes.Size() == 0)
|
|
{
|
|
I_FatalError ("No volumes defined.");
|
|
}
|
|
}
|