og-odamex/common/g_umapinfo.cpp

579 lines
13 KiB
C++

// Emacs style mode select -*- C++ -*-
//-----------------------------------------------------------------------------
//
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// Functions regarding reading and interpreting UMAPINFO lumps.
//
//-----------------------------------------------------------------------------
#include "odamex.h"
#include "g_episode.h"
#include "oscanner.h"
#include "w_wad.h"
#include "infomap.h"
#include "g_mapinfo.h" // G_MapNameToLevelNum
bool ValidateMapName(const OLumpName& mapname, int* pEpi = NULL, int* pMap = NULL)
{
// Check if the given map name can be expressed as a gameepisode/gamemap pair and be
// reconstructed from it.
OLumpName lumpname;
int epi = -1, map = -1;
if (gamemode != commercial)
{
if (sscanf(mapname.c_str(), "E%dM%d", &epi, &map) != 2)
return false;
lumpname = fmt::format("E{}M{}", epi, map);
}
else
{
if (sscanf(mapname.c_str(), "MAP%d", &map) != 1)
return false;
lumpname = fmt::format("MAP{:02d}", map);
epi = 1;
}
if (pEpi)
*pEpi = epi;
if (pMap)
*pMap = map;
return mapname == lumpname;
}
// used for munching the strings in UMAPINFO
std::string ParseMultiString(OScanner& os)
{
os.scan();
if (!os.isQuotedString())
{
if (os.compareTokenNoCase("clear"))
return "-"; // this was explicitly deleted to override the default.
os.error("Either 'clear' or quoted string expected");
}
os.unScan();
std::string build;
do
{
os.mustScan();
build += os.getToken(); // Concatenate the new line onto the existing text
build += '\n'; // Add newline
os.scan();
} while (os.compareToken(","));
os.unScan();
return build;
}
void ParseOLumpName(OScanner& os, OLumpName& buffer)
{
os.mustScan(8);
buffer = os.getToken();
}
void MustGetIdentifier(OScanner& os)
{
os.mustScan();
if (!os.isIdentifier())
{
os.error("Expected identifier (unexpected end of file).");
}
}
enum struct player_action_t
{
DISALLOW,
ALLOW,
REQUIRE,
};
player_action_t ParsePlayerAction(OScanner& os)
{
MustGetIdentifier(os);
if (os.compareTokenNoCase("disallow"))
return player_action_t::DISALLOW;
if (os.compareTokenNoCase("allow"))
return player_action_t::ALLOW;
if (os.compareTokenNoCase("require"))
return player_action_t::REQUIRE;
os.error("Expected 'disable', 'allow' or 'require', got '{}'.", os.getToken());
}
bool pnamemodified;
bool ParseStandardUmapInfoProperty(OScanner& os, level_pwad_info_t* mape)
{
// find the next line with content.
// this line is no property.
if (!os.isIdentifier())
{
os.error("Expected identifier, got \"{}\".", os.getToken());
}
std::string pname = os.getToken();
os.mustScan();
os.assertTokenNoCaseIs("=");
if (!stricmp(pname.c_str(), "levelname"))
{
os.mustScan();
mape->level_name = os.getToken();
if (!pnamemodified) // only want to clear pname if its *not* from the umapinfo
mape->pname.clear();
}
else if (!stricmp(pname.c_str(), "label"))
{
os.mustScan();
if (!os.isQuotedString() && os.compareTokenNoCase("clear"))
{
mape->clearlabel = true;
}
else
{
mape->label = os.getToken();
}
}
else if (!stricmp(pname.c_str(), "author"))
{
os.mustScan();
mape->author = os.getToken();
}
else if (!stricmp(pname.c_str(), "next"))
{
ParseOLumpName(os, mape->nextmap);
if (!ValidateMapName(mape->nextmap))
{
os.error("Invalid map name {}", mape->nextmap);
return false;
}
}
else if (!stricmp(pname.c_str(), "nextsecret"))
{
ParseOLumpName(os, mape->secretmap);
if (!ValidateMapName(mape->secretmap))
{
os.error("Invalid map name {}", mape->nextmap);
return false;
}
}
else if (!stricmp(pname.c_str(), "levelpic"))
{
ParseOLumpName(os, mape->pname);
pnamemodified = true;
}
else if (!stricmp(pname.c_str(), "skytexture"))
{
ParseOLumpName(os, mape->skypic);
}
else if (!stricmp(pname.c_str(), "music"))
{
os.mustScan(8);
const std::string musicname = os.getToken();
if (W_CheckNumForName(musicname.c_str()) != -1)
{
mape->music = musicname;
}
}
else if (!stricmp(pname.c_str(), "endpic"))
{
ParseOLumpName(os, mape->endpic);
mape->nextmap = "EndGame1";
}
else if (!stricmp(pname.c_str(), "endcast"))
{
os.mustScanBool();
if (os.getTokenBool())
mape->nextmap = "EndGameC";
else
mape->endpic.clear();
}
else if (!stricmp(pname.c_str(), "endbunny"))
{
os.mustScanBool();
if (os.getTokenBool())
mape->nextmap = "EndGame3";
else
mape->endpic.clear();
}
else if (!stricmp(pname.c_str(), "endgame"))
{
os.mustScanBool();
if (os.getTokenBool())
{
mape->endpic = "!";
}
else
{
mape->endpic.clear();
}
}
else if (!stricmp(pname.c_str(), "exitpic"))
{
ParseOLumpName(os, mape->exitpic);
}
else if (!stricmp(pname.c_str(), "enterpic"))
{
ParseOLumpName(os, mape->enterpic);
}
else if (!stricmp(pname.c_str(), "exitanim"))
{
ParseOLumpName(os, mape->exitanim);
}
else if (!stricmp(pname.c_str(), "enteranim"))
{
ParseOLumpName(os, mape->enteranim);
}
else if (!stricmp(pname.c_str(), "nointermission"))
{
os.mustScanBool();
if (os.getTokenBool())
{
mape->flags |= LEVEL_NOINTERMISSION;
}
}
else if (!stricmp(pname.c_str(), "partime"))
{
os.mustScanInt();
mape->partime = os.getTokenInt();
}
else if (!stricmp(pname.c_str(), "intertext"))
{
const std::string lname = ParseMultiString(os);
if (lname.empty())
return false;
mape->intertext = lname;
}
else if (!stricmp(pname.c_str(), "intertextsecret"))
{
const std::string lname = ParseMultiString(os);
if (lname.empty())
return false;
mape->intertextsecret = lname;
}
else if (!stricmp(pname.c_str(), "interbackdrop"))
{
ParseOLumpName(os, mape->interbackdrop);
}
else if (!stricmp(pname.c_str(), "intermusic"))
{
os.mustScan(8);
const std::string musicname = os.getToken();
if (W_CheckNumForName(musicname.c_str()) != -1)
mape->intermusic = musicname;
}
else if (!stricmp(pname.c_str(), "episode"))
{
if (!episodes_modified && gamemode == commercial)
{
episodenum = 0;
episodes_modified = true;
}
const std::string lname = ParseMultiString(os);
if (lname.empty())
return false;
if (lname == "-") // means "clear"
{
episodenum = 0;
}
else
{
const StringTokens tokens = TokenizeString(lname, "\n");
if (episodenum >= MAX_EPISODES)
{
os.error("Maximum episode definitions ({}) exceeded.", MAX_EPISODES);
return false;
}
EpisodeMaps[episodenum] = mape->mapname;
EpisodeInfos[episodenum].pic_name = tokens[0];
EpisodeInfos[episodenum].menu_name = tokens[1];
EpisodeInfos[episodenum].fulltext = false;
EpisodeInfos[episodenum].noskillmenu = false;
EpisodeInfos[episodenum].key = (tokens.size() > 2) ? tokens[2][0] : 0;
++episodenum;
}
}
else if (!stricmp(pname.c_str(), "bossaction"))
{
MustGetIdentifier(os);
if (!stricmp(os.getToken().c_str(), "clear"))
{
// mark level free of boss actions
mape->bossactions.clear();
}
else
{
const std::string actor_name = os.getToken();
const mobjtype_t i = P_INameToMobj(actor_name);
if (i == MT_NULL)
{
os.error("Unknown thing type {}", os.getToken());
return false;
}
// skip comma token
os.mustScan();
os.assertTokenNoCaseIs(",");
os.mustScanInt();
const int special = os.getTokenInt();
os.mustScan();
os.assertTokenNoCaseIs(",");
os.mustScanInt();
const int tag = os.getTokenInt();
// allow no 0-tag specials here, unless a level exit.
if (tag != 0 || special == 11 || special == 51 || special == 52 ||
special == 124)
{
bossaction_t new_bossaction;
new_bossaction.special = static_cast<short>(special);
new_bossaction.tag = static_cast<short>(tag);
new_bossaction.type = i;
mape->bossactions.push_back(new_bossaction);
}
}
}
else if (!stricmp(pname.c_str(), "bossactionednum"))
{
os.mustScan();
if (os.compareTokenNoCase("clear"))
{
// mark level free of boss actions
mape->bossactions.clear();
}
else
{
const int actor_ednum = os.getTokenInt();
const auto it = spawn_map.find(actor_ednum);
int32_t type;
if (it == spawn_map.end())
{
os.error("Unknown thing ednum {}", os.getToken());
return 0;
}
else
{
type = it->second->type;
}
// skip comma token
os.mustScan();
os.assertTokenNoCaseIs(",");
os.mustScanInt();
const int special = os.getTokenInt();
os.mustScan();
os.assertTokenNoCaseIs(",");
os.mustScanInt();
const int tag = os.getTokenInt();
// allow no 0-tag specials here, unless a level exit.
if (tag != 0 || special == 11 || special == 51 || special == 52 ||
special == 124)
{
bossaction_t new_bossaction;
new_bossaction.special = static_cast<short>(special);
new_bossaction.tag = static_cast<short>(tag);
new_bossaction.type = type;
mape->bossactions.push_back(new_bossaction);
}
}
}
else if (!stricmp(pname.c_str(), "jumping"))
{
switch (ParsePlayerAction(os))
{
case player_action_t::DISALLOW:
mape->flags |= LEVEL_JUMP_NO;
mape->flags &= ~LEVEL_JUMP_YES;
break;
case player_action_t::ALLOW:
mape->flags &= ~(LEVEL_JUMP_NO | LEVEL_JUMP_YES);
break;
case player_action_t::REQUIRE:
mape->flags &= ~LEVEL_JUMP_NO;
mape->flags |= LEVEL_JUMP_YES;
break;
}
}
else if (!stricmp(pname.c_str(), "freeaim"))
{
switch (ParsePlayerAction(os))
{
case player_action_t::DISALLOW:
mape->flags |= LEVEL_FREELOOK_NO;
mape->flags &= ~LEVEL_FREELOOK_YES;
break;
case player_action_t::ALLOW:
mape->flags &= ~(LEVEL_FREELOOK_NO | LEVEL_FREELOOK_YES);
break;
case player_action_t::REQUIRE:
mape->flags &= ~LEVEL_FREELOOK_NO;
mape->flags |= LEVEL_FREELOOK_YES;
break;
}
}
else if (!stricmp(pname.c_str(), "crouching"))
{
if (ParsePlayerAction(os) == player_action_t::REQUIRE)
os.warning("Crouching is not supported in Odamex. Map {} may not work as intended.", mape->mapname);
}
else
{
do
{
if (!IsRealNum(os.getToken().c_str()))
os.scan();
} while (os.compareToken(","));
}
os.scan();
return 1;
}
void ParseUMapInfoLump(int lump, const OLumpName& lumpname)
{
LevelInfos& levels = getLevelInfos();
const char* buffer = static_cast<char*>(W_CacheLumpNum(lump, PU_STATIC));
const OScannerConfig config = {
lumpname, // lumpName
false, // semiComments
true, // cComments
};
OScanner os = OScanner::openBuffer(config, buffer, buffer + W_LumpLength(lump));
while (os.scan())
{
if (!os.compareTokenNoCase("map"))
{
os.error("Expected map definition, got {}", os.getToken());
}
os.mustScan(8);
const OLumpName mapname = os.getToken();
if (!ValidateMapName(mapname))
{
os.error("Invalid map name {}", mapname);
}
// Find the level.
level_pwad_info_t& info = (levels.findByName(mapname).exists())
? levels.findByName(mapname)
: levels.create();
// if no sky is defined, it will show texture 0 (aastinky/aashitty)
// so instead, lets just try to give it the first defined sky in the level set.
if (levels.size() > 0 && info.skypic.empty())
{
level_pwad_info_t& def = levels.at(0);
info.skypic = def.skypic;
}
pnamemodified = false;
info.mapname = mapname;
G_MapNameToLevelNum(info);
G_MapNameToID24LevelNum(info);
os.mustScan();
os.assertTokenNoCaseIs("{");
os.scan();
while (!os.compareToken("}"))
{
// TODO: should this be actually checking the return value here?
ParseStandardUmapInfoProperty(os, &info);
}
// Set default level progression here to simplify the checks elsewhere.
// Doing this lets us skip all normal code for this if nothing has been defined.
if (!info.nextmap[0] && !info.endpic[0])
{
if (info.mapname == "MAP30")
{
info.endpic = "$CAST";
info.nextmap = "EndGameC";
}
else if (info.mapname == "E1M8")
{
info.endpic = gamemode == retail ? "CREDIT" : "HELP2";
info.nextmap = "EndGameC";
}
else if (info.mapname == "E2M8")
{
info.endpic = "VICTORY";
info.nextmap = "EndGame2";
}
else if (info.mapname == "E3M8")
{
info.endpic = "$BUNNY";
info.nextmap = "EndGame3";
}
else if (info.mapname == "E4M8")
{
info.endpic = "ENDPIC";
info.nextmap = "EndGame4";
}
else if (gamemission == chex && info.mapname == "E1M5")
{
info.endpic = "CREDIT";
info.nextmap = "EndGame1";
}
else
{
int ep, map;
ValidateMapName(info.mapname, &ep, &map);
map++;
if (gamemode == commercial)
{
info.nextmap = fmt::format("MAP{:02d}", map);
}
else
{
info.nextmap = fmt::format("E{}M{}", ep, map);
}
}
}
}
// if an episode title patch is missing or invalid, fall back on text name
for (auto& episode : EpisodeInfos) {
episode.fulltext = episode.pic_name.empty() || W_CheckNumForName(episode.pic_name) == -1;
}
}