gzdoom/src/g_level.cpp
Dileep V. Reddy 25f1407228 Cleaning up implementation of isometric camera with optional orthographic projection.
All CVars, mapinfo variables, and playerinfo/playerpawn variables are gone.
A Camera actor named 'SpectatorCamera' is defined in warsrc/static/zscript/actors/shared/camera.zs

The following new flag bits were defined in the 'DViewPosition' struct for use with actor->ViewPos in src/playsim/actor.h:
	VPSF_ALLOWOUTOFBOUNDS =		1 << 3,			// Allow viewpoint to go out of bounds (hardware renderer only).
	VPSF_ORTHOGRAPHIC =		1 << 4,			// Use orthographic projection.
	VPSF_ISOMETRICSPRITES =		1 << 5,			// Displace sprites towards camera and don't billboard (drawn from isometric perspective).

Basically, spawn a SpectatorCamera actor with the appropriate flags and set it to player.camera.
See example template: https://www.mediafire.com/file/fv8rytayjt9l6g1/isometric_actor_wads.zip/file

Has been tested with multiplayer death and disconnection (and respawns). Not tested with portals.
Still only works with hardware renderer (mostly). But should be compatible with older mods and libraries.
2024-02-01 08:43:34 -05:00

2468 lines
64 KiB
C++

/*
** g_level.cpp
** controls movement between levels
**
**---------------------------------------------------------------------------
** Copyright 1998-2006 Randy Heit
** 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 "d_main.h"
#include "g_level.h"
#include "g_game.h"
#include "s_sound.h"
#include "d_event.h"
#include "m_random.h"
#include "engineerrors.h"
#include "doomstat.h"
#include "wi_stuff.h"
#include "filesystem.h"
#include "am_map.h"
#include "c_dispatch.h"
#include "i_interface.h"
#include "p_setup.h"
#include "p_local.h"
#include "r_sky.h"
#include "c_console.h"
#include "intermission/intermission.h"
#include "v_video.h"
#include "st_stuff.h"
#include "hu_stuff.h"
#include "p_saveg.h"
#include "p_acs.h"
#include "d_protocol.h"
#include "v_text.h"
#include "s_sndseq.h"
#include "b_bot.h"
#include "sbar.h"
#include "a_lightning.h"
#include "version.h"
#include "sbarinfo.h"
#include "p_lnspec.h"
#include "cmdlib.h"
#include "d_net.h"
#include "d_netinf.h"
#include "doommenu.h"
#include "a_sharedglobal.h"
#include "r_utility.h"
#include "p_spec.h"
#include "serializer_doom.h"
#include "vm.h"
#include "events.h"
#include "i_music.h"
#include "a_dynlight.h"
#include "p_conversation.h"
#include "p_effect.h"
#include "stringtable.h"
#include "c_buttons.h"
#include "screenjob.h"
#include "types.h"
#include "gstrings.h"
#include "gi.h"
#include "g_hub.h"
#include "g_levellocals.h"
#include "actorinlines.h"
#include "i_time.h"
#include "p_maputl.h"
#include "s_music.h"
#include "fragglescript/t_script.h"
#include "texturemanager.h"
void STAT_StartNewGame(const char *lev);
void STAT_ChangeLevel(const char *newl, FLevelLocals *Level);
FString STAT_EpisodeName();
EXTERN_CVAR(Bool, save_formatted)
EXTERN_CVAR (Float, sv_gravity)
EXTERN_CVAR (Float, sv_aircontrol)
EXTERN_CVAR (Int, disableautosave)
EXTERN_CVAR (String, playerclass)
extern uint8_t globalfreeze, globalchangefreeze;
#define SNAP_ID MAKE_ID('s','n','A','p')
#define DSNP_ID MAKE_ID('d','s','N','p')
#define VIST_ID MAKE_ID('v','i','S','t')
#define ACSD_ID MAKE_ID('a','c','S','d')
#define RCLS_ID MAKE_ID('r','c','L','s')
#define PCLS_ID MAKE_ID('p','c','L','s')
void G_VerifySkill();
void I_UpdateWindowTitle();
CUSTOM_CVAR(Bool, gl_brightfog, false, CVAR_ARCHIVE | CVAR_NOINITCALL)
{
for (auto Level : AllLevels())
{
if (Level->info == nullptr || Level->info->brightfog == -1) Level->brightfog = self;
}
}
CUSTOM_CVAR(Bool, gl_lightadditivesurfaces, false, CVAR_ARCHIVE | CVAR_NOINITCALL)
{
for (auto Level : AllLevels())
{
if (Level->info == nullptr || Level->info->lightadditivesurfaces == -1) Level->lightadditivesurfaces = self;
}
}
CUSTOM_CVAR(Bool, gl_notexturefill, false, CVAR_NOINITCALL)
{
for (auto Level : AllLevels())
{
if (Level->info == nullptr || Level->info->notexturefill == -1) Level->notexturefill = self;
}
}
CUSTOM_CVAR(Int, gl_maplightmode, -1, CVAR_NOINITCALL | CVAR_CHEAT) // this is just for testing. -1 means 'inactive'
{
if (self > 5 || self < -1) self = -1;
}
CUSTOM_CVARD(Int, gl_lightmode, 1, CVAR_ARCHIVE, "Select lighting mode. 2 is vanilla accurate, 1 is accurate to the ZDoom software renderer and 0 is a less demanding non-shader implementation")
{
if (self < 0 || self > 2) self = 1;
}
ELightMode getRealLightmode(FLevelLocals* Level, bool for3d)
{
// The rules are:
// 1) if the map sets a proper light mode, it is taken unconditionally.
if (Level->info->lightmode != ELightMode::NotSet) return Level->info->lightmode;
// 2) if the user sets gl_maplightmode, this is being used.
if (gl_maplightmode != -1) return (ELightMode)*gl_maplightmode;
// 3) if not for 3D use lightmode Doom. This is for the automap where the software light modes do not work
if (!for3d) return ELightMode::Doom;
// otherwise use lightmode Doom or software lighting based on user preferences.
if (gl_lightmode == 1) return ELightMode::ZDoomSoftware;
else if (gl_lightmode == 2) return ELightMode::DoomSoftware;
return ELightMode::Doom;
}
CVAR(Int, sv_alwaystally, 0, CVAR_SERVERINFO)
static FRandom pr_classchoice ("RandomPlayerClassChoice");
extern level_info_t TheDefaultLevelInfo;
extern bool timingdemo;
// Start time for timing demos
int starttime;
extern FString BackupSaveName;
bool savegamerestore;
int finishstate = FINISH_NoHub;
extern float mousex, mousey;
extern bool sendpause, sendsave, sendturn180, SendLand;
void *statcopy; // for statistics driver
FLevelLocals level; // info about current level
FLevelLocals *primaryLevel = &level; // level for which to display the user interface.
FLevelLocals *currentVMLevel = &level; // level which currently ticks. Used as global input to the VM and some functions called by it.
static PType* maprecordtype;
//=============================================================================
//
//
//
//=============================================================================
void Local_Job_Init()
{
maprecordtype = NewPointer(NewStruct("MapRecord", nullptr, true));
}
//=============================================================================
//
//
//
//=============================================================================
static void CallCreateMapFunction(const char* qname, DObject* runner, level_info_t* map)
{
auto func = LookupFunction(qname);
if (func->Proto->ArgumentTypes.Size() == 1) return CallCreateFunction(qname, runner); // accept functions without map parameter as well here.
if (func->Proto->ArgumentTypes.Size() != 2) I_Error("Bad map-cutscene function %s. Must receive precisely two arguments.", qname);
if (func->Proto->ArgumentTypes[0] != cutscene.runnerclasstype && func->Proto->ArgumentTypes[1] != maprecordtype)
I_Error("Bad cutscene function %s. Must receive ScreenJobRunner and LevelInfo reference.", qname);
VMValue val[2] = { runner, map };
VMCall(func, val, 2, nullptr, 0);
}
//=============================================================================
//
//
//
//=============================================================================
bool CreateCutscene(CutsceneDef* cs, DObject* runner, level_info_t* map)
{
if (cs->function.CompareNoCase("none") == 0)
return true; // play nothing but return as being validated
if (cs->function.IsNotEmpty())
{
CallCreateMapFunction(cs->function.GetChars(), runner, map);
return true;
}
else if (cs->video.IsNotEmpty())
{
AddGenericVideo(runner, cs->video, cs->GetSound(), cs->framespersec);
return true;
}
return false;
}
//==========================================================================
//
// G_InitNew
// Can be called by the startup code or the menu task,
// consoleplayer, playeringame[] should be set.
//
//==========================================================================
static FString d_mapname;
static int d_skill=-1;
void G_DeferedInitNew (const char *mapname, int newskill)
{
d_mapname = mapname;
d_skill = newskill;
CheckWarpTransMap (d_mapname, true);
gameaction = ga_newgame2;
}
void G_DeferedInitNew (FNewGameStartup *gs)
{
if (gs->hasPlayerClass) playerclass = gs->PlayerClass.GetChars();
d_mapname = AllEpisodes[gs->Episode].mEpisodeMap;
d_skill = gs->Skill;
CheckWarpTransMap (d_mapname, true);
gameaction = ga_newgame2;
finishstate = FINISH_NoHub;
if (AllEpisodes[gs->Episode].mIntro.isdefined())
{
cutscene.runner = CreateRunner(false);
GC::WriteBarrier(cutscene.runner);
if (!CreateCutscene(&AllEpisodes[gs->Episode].mIntro, cutscene.runner, nullptr))
{
return;
}
cutscene.completion = [](bool) { gameaction = ga_newgame2; };
if (!ScreenJobValidate())
{
DeleteScreenJob();
cutscene.completion = nullptr;
return;
}
gameaction = ga_intermission;
}
}
//==========================================================================
//
//
//==========================================================================
CCMD (map)
{
if (netgame)
{
Printf ("Use " TEXTCOLOR_BOLD "changemap" TEXTCOLOR_NORMAL " instead. " TEXTCOLOR_BOLD "Map"
TEXTCOLOR_NORMAL " is for single-player only.\n");
return;
}
if (argv.argc() > 1)
{
const char *mapname = argv[1];
if (!strcmp(mapname, "*")) mapname = primaryLevel->MapName.GetChars();
try
{
if (!P_CheckMapData(mapname))
{
Printf ("No map %s\n", mapname);
}
else
{
if (argv.argc() > 2 && stricmp(argv[2], "coop") == 0)
{
deathmatch = false;
multiplayernext = true;
}
else if (argv.argc() > 2 && stricmp(argv[2], "dm") == 0)
{
deathmatch = true;
multiplayernext = true;
}
G_DeferedInitNew (mapname);
}
}
catch(CRecoverableError &error)
{
if (error.GetMessage())
Printf("%s", error.GetMessage());
}
}
else
{
Printf ("Usage: map <map name> [coop|dm]\n");
}
}
//==========================================================================
//
//
//==========================================================================
UNSAFE_CCMD(recordmap)
{
if (netgame)
{
Printf("You cannot record a new game while in a netgame.\n");
return;
}
if (argv.argc() > 2)
{
const char *mapname = argv[2];
if (!strcmp(mapname, "*")) mapname = primaryLevel->MapName.GetChars();
try
{
if (!P_CheckMapData(mapname))
{
Printf("No map %s\n", mapname);
}
else
{
if (argv.argc() > 3 && stricmp(argv[3], "coop") == 0)
{
deathmatch = false;
multiplayernext = true;
}
else if (argv.argc() > 3 && stricmp(argv[3], "dm") == 0)
{
deathmatch = true;
multiplayernext = true;
}
G_DeferedInitNew(mapname);
gameaction = ga_recordgame;
newdemoname = argv[1];
newdemomap = mapname;
}
}
catch (CRecoverableError &error)
{
if (error.GetMessage())
Printf("%s", error.GetMessage());
}
}
else
{
Printf("Usage: recordmap <filename> <map name> [coop|dm]\n");
}
}
//==========================================================================
//
//
//==========================================================================
UNSAFE_CCMD (open)
{
if (netgame)
{
Printf ("You cannot use open in multiplayer games.\n");
return;
}
if (argv.argc() > 1)
{
d_mapname = "file:";
d_mapname += argv[1];
if (!P_CheckMapData(d_mapname.GetChars()))
{
Printf ("No map %s\n", d_mapname.GetChars());
}
else
{
if (argv.argc() > 2 && stricmp(argv[2], "coop") == 0)
{
deathmatch = false;
multiplayernext = true;
}
else if (argv.argc() > 2 && stricmp(argv[2], "dm") == 0)
{
deathmatch = true;
multiplayernext = true;
}
gameaction = ga_newgame2;
d_skill = -1;
}
}
else
{
Printf ("Usage: open <map file> [coop|dm]\n");
}
}
//==========================================================================
//
//
//==========================================================================
void G_NewInit ()
{
int i;
// Destory all old player refrences that may still exist
TThinkerIterator<AActor> it(primaryLevel, NAME_PlayerPawn, STAT_TRAVELLING);
AActor *pawn, *next;
next = it.Next();
while ((pawn = next) != NULL)
{
next = it.Next();
pawn->flags |= MF_NOSECTOR | MF_NOBLOCKMAP;
pawn->Destroy();
}
if (primaryLevel->FraggleScriptThinker) primaryLevel->FraggleScriptThinker->Destroy();
primaryLevel->FraggleScriptThinker = nullptr;
// Destroy thinkers that may remain after change level failure
// Usually, the list contains just a sentinel when such error occurred
primaryLevel->Thinkers.DestroyThinkersInList(STAT_TRAVELLING);
G_ClearSnapshots ();
netgame = false;
multiplayer = multiplayernext;
multiplayernext = false;
if (demoplayback)
{
C_RestoreCVars ();
demoplayback = false;
D_SetupUserInfo ();
}
for (i = 0; i < MAXPLAYERS; ++i)
{
player_t *p = &players[i];
userinfo_t saved_ui;
saved_ui.TransferFrom(p->userinfo);
const int chasecam = p->cheats & CF_CHASECAM;
const bool settings_controller = p->settings_controller;
p->~player_t();
::new(p) player_t;
p->settings_controller = settings_controller;
p->cheats |= chasecam;
p->playerstate = PST_DEAD;
p->userinfo.TransferFrom(saved_ui);
playeringame[i] = false;
}
BackupSaveName = "";
consoleplayer = 0;
NextSkill = -1;
}
//==========================================================================
//
//
//==========================================================================
void G_DoNewGame (void)
{
G_NewInit ();
playeringame[consoleplayer] = 1;
if (d_skill != -1)
{
gameskill = d_skill;
}
G_InitNew (d_mapname.GetChars(), false);
gameaction = ga_nothing;
}
//==========================================================================
//
// Initializes player classes in case they are random.
// This gets called at the start of a new game, and the classes
// chosen here are used for the remainder of a single-player
// or coop game. These are ignored for deathmatch.
//
//==========================================================================
static void InitPlayerClasses ()
{
if (!savegamerestore)
{
for (int i = 0; i < MAXPLAYERS; ++i)
{
SinglePlayerClass[i] = players[i].userinfo.GetPlayerClassNum();
if (SinglePlayerClass[i] < 0 || !playeringame[i])
{
SinglePlayerClass[i] = (pr_classchoice()) % PlayerClasses.Size ();
}
players[i].cls = NULL;
players[i].CurrentPlayerClass = SinglePlayerClass[i];
}
}
}
//==========================================================================
//
//
//==========================================================================
void G_InitNew (const char *mapname, bool bTitleLevel)
{
bool wantFast;
int i;
// did we have any level before?
if (primaryLevel->info != nullptr)
staticEventManager.WorldUnloaded(FString()); // [MK] don't pass the new map, as it's not a level transition
UnlatchCVars ();
if (!savegamerestore)
{
G_ClearHubInfo();
G_ClearSnapshots ();
P_RemoveDefereds ();
// [RH] Mark all levels as not visited
for (unsigned int i = 0; i < wadlevelinfos.Size(); i++)
wadlevelinfos[i].flags = wadlevelinfos[i].flags & ~LEVEL_VISITED;
auto redirectmap = FindLevelInfo(mapname);
if (redirectmap->RedirectCVAR != NAME_None)
redirectmap = redirectmap->CheckLevelRedirect();
if (redirectmap && redirectmap->MapName.IsNotEmpty())
mapname = redirectmap->MapName.GetChars();
}
G_VerifySkill();
UnlatchCVars ();
globalfreeze = globalchangefreeze = 0;
for (auto Level : AllLevels())
{
Level->Thinkers.DestroyThinkersInList(STAT_STATIC);
}
if (paused)
{
paused = 0;
S_ResumeSound (false);
}
ST_CreateStatusBar(bTitleLevel);
setsizeneeded = true;
// [RH] If this map doesn't exist, bomb out
if (!P_CheckMapData(mapname))
{
I_Error ("Could not find map %s\n", mapname);
}
wantFast = !!G_SkillProperty(SKILLP_FastMonsters);
GameSpeed = wantFast ? SPEED_Fast : SPEED_Normal;
if (!savegamerestore)
{
if (!netgame && !demorecording && !demoplayback)
{
// [RH] Change the random seed for each new single player game
// [ED850] The demo already sets the RNG.
rngseed = use_staticrng ? staticrngseed : (rngseed + 1);
}
FRandom::StaticClearRandom ();
P_ClearACSVars(true);
primaryLevel->time = 0;
primaryLevel->maptime = 0;
primaryLevel->totaltime = 0;
primaryLevel->spawnindex = 0;
if (!multiplayer || !deathmatch)
{
InitPlayerClasses ();
}
// force players to be initialized upon first level load
for (i = 0; i < MAXPLAYERS; i++)
players[i].playerstate = PST_ENTER; // [BC]
STAT_StartNewGame(mapname);
}
usergame = !bTitleLevel; // will be set false if a demo
paused = 0;
demoplayback = false;
automapactive = false;
viewactive = true;
//Added by MC: Initialize bots.
if (!deathmatch)
{
primaryLevel->BotInfo.Init ();
}
if (bTitleLevel)
{
gamestate = GS_TITLELEVEL;
}
else if (gamestate != GS_STARTUP)
{
gamestate = GS_LEVEL;
}
G_DoLoadLevel (mapname, 0, false, !savegamerestore);
if (!savegamerestore && (gameinfo.gametype == GAME_Strife || (SBarInfoScript[SCRIPT_CUSTOM] != nullptr && SBarInfoScript[SCRIPT_CUSTOM]->GetGameType() == GAME_Strife)))
{
// Set the initial quest log text for Strife.
for (i = 0; i < MAXPLAYERS; ++i)
{
if (playeringame[i])
players[i].SetLogText("$TXT_FINDHELP");
}
}
}
//
// G_DoCompleted
//
static FString nextlevel;
static int startpos; // [RH] Support for multiple starts per level
extern int NoWipe; // [RH] Don't wipe when travelling in hubs
static int changeflags;
static bool unloading;
//==========================================================================
//
// [RH] The position parameter to these next three functions should
// match the first parameter of the single player start spots
// that should appear in the next map.
//
//==========================================================================
EXTERN_CVAR(Bool, sv_singleplayerrespawn)
bool FLevelLocals::ShouldDoIntermission(cluster_info_t* nextcluster, cluster_info_t* thiscluster)
{
// this is here to remove some code duplication
if ((sv_alwaystally == 2) || (deathmatch))
return true;
if ((sv_alwaystally == 0) && (flags & LEVEL_NOINTERMISSION))
return false;
bool withinSameCluster = (nextcluster == thiscluster);
bool clusterIsHub = (thiscluster->flags & CLUSTER_HUB);
bool hubNoIntermission = !(thiscluster->flags & CLUSTER_ALLOWINTERMISSION);
if (withinSameCluster && clusterIsHub && hubNoIntermission)
return false;
return true;
}
void FLevelLocals::ChangeLevel(const char *levelname, int position, int inflags, int nextSkill)
{
if (!isPrimaryLevel()) return; // only the primary level may exit.
FString nextlevel;
level_info_t *nextinfo = nullptr;
if (unloading)
{
Printf (TEXTCOLOR_RED "Unloading scripts cannot exit the level again.\n");
return;
}
if (gameaction == ga_completed && !(i_compatflags2 & COMPATF2_MULTIEXIT)) // do not exit multiple times.
{
return;
}
if (levelname == NULL || *levelname == 0)
{
// end the game
levelname = NULL;
if (!NextMap.Compare("enDSeQ",6))
{
nextlevel = NextMap; // If there is already an end sequence please leave it alone!
}
else
{
nextlevel.Format("enDSeQ%04x", gameinfo.DefaultEndSequence.GetIndex());
}
}
else if (strncmp(levelname, "enDSeQ", 6) != 0)
{
FString reallevelname = levelname;
CheckWarpTransMap(reallevelname, true);
nextinfo = FindLevelInfo (reallevelname.GetChars(), false);
if (nextinfo != NULL)
{
level_info_t *nextredir = nextinfo->CheckLevelRedirect();
if (nextredir != NULL)
{
nextinfo = nextredir;
}
nextlevel = nextinfo->MapName;
}
else
{
nextlevel = levelname;
}
}
else
{
nextlevel = levelname;
}
if (nextSkill != -1)
NextSkill = (unsigned)nextSkill < AllSkills.Size() ? nextSkill : -1;
if (inflags & CHANGELEVEL_NOINTERMISSION)
{
flags |= LEVEL_NOINTERMISSION;
}
cluster_info_t *thiscluster = FindClusterInfo (cluster);
cluster_info_t *nextcluster = nextinfo? FindClusterInfo (nextinfo->cluster) : NULL;
startpos = position;
SetMusicVolume(1.0);
if (nextinfo != NULL)
{
if (thiscluster != nextcluster || (thiscluster && !(thiscluster->flags & CLUSTER_HUB)))
{
if (nextinfo->flags2 & LEVEL2_RESETINVENTORY)
{
inflags |= CHANGELEVEL_RESETINVENTORY;
}
if (nextinfo->flags2 & LEVEL2_RESETHEALTH)
{
inflags |= CHANGELEVEL_RESETHEALTH;
}
}
}
changeflags = inflags;
BotInfo.End(); //Added by MC:
// [RH] Give scripts a chance to do something
unloading = true;
Behaviors.StartTypedScripts (SCRIPT_Unloading, NULL, false, 0, true);
// [ZZ] safe world unload
for (auto Level : AllLevels())
{
// Todo: This must be exolicitly sandboxed!
Level->localEventManager->WorldUnloaded(nextlevel);
}
// [ZZ] unsafe world unload (changemap != map)
staticEventManager.WorldUnloaded(nextlevel);
unloading = false;
STAT_ChangeLevel(nextlevel.GetChars(), this);
if (thiscluster && (thiscluster->flags & CLUSTER_HUB))
{
if (!ShouldDoIntermission(nextcluster, thiscluster))
NoWipe = 35;
D_DrawIcon = "TELEICON";
}
for(int i = 0; i < MAXPLAYERS; i++)
{
if (PlayerInGame(i))
{
player_t *player = Players[i];
// Un-crouch all players here.
player->Uncrouch();
// If this is co-op, respawn any dead players now so they can
// keep their inventory on the next map.
if ((multiplayer || flags2 & LEVEL2_ALLOWRESPAWN || sv_singleplayerrespawn || !!G_SkillProperty(SKILLP_PlayerRespawn))
&& !deathmatch && player->playerstate == PST_DEAD)
{
// Copied from the end of P_DeathThink [[
player->cls = NULL; // Force a new class if the player is using a random class
player->playerstate = PST_REBORN;
if (player->mo->special1 > 2)
{
player->mo->special1 = 0;
}
// ]]
DoReborn(i, false);
}
}
}
// Set global transition state.
gameaction = ga_completed;
::nextlevel = nextlevel;
}
DEFINE_ACTION_FUNCTION(FLevelLocals, ChangeLevel)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_STRING(levelname);
PARAM_INT(position);
PARAM_INT(inflags);
PARAM_INT(nextSkill);
self->ChangeLevel(levelname.GetChars(), position, inflags, nextSkill);
return 0;
}
//==========================================================================
//
//
//==========================================================================
const char *FLevelLocals::GetSecretExitMap()
{
const char *nextmap = NextMap.GetChars();
if (NextSecretMap.Len() > 0)
{
if (NextSecretMap.Compare("enDSeQ", 6) == 0 || P_CheckMapData(NextSecretMap.GetChars()))
{
nextmap = NextSecretMap.GetChars();
}
}
return nextmap;
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::ExitLevel (int position, bool keepFacing)
{
flags3 |= LEVEL3_EXITNORMALUSED;
ChangeLevel(NextMap.GetChars(), position, keepFacing ? CHANGELEVEL_KEEPFACING : 0);
}
static void LevelLocals_ExitLevel(FLevelLocals *self, int position, bool keepFacing)
{
self->ExitLevel(position, keepFacing);
}
DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, ExitLevel, LevelLocals_ExitLevel)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_INT(position);
PARAM_INT(keepFacing);
self->ExitLevel(position, keepFacing);
return 0;
}
void FLevelLocals::SecretExitLevel (int position)
{
flags3 |= LEVEL3_EXITSECRETUSED;
ChangeLevel(GetSecretExitMap(), position, 0);
}
static void LevelLocals_SecretExitLevel(FLevelLocals *self, int position)
{
self->SecretExitLevel(position);
}
DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, SecretExitLevel, LevelLocals_SecretExitLevel)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_INT(position);
self->SecretExitLevel(position);
return 0;
}
//==========================================================================
//
//
//==========================================================================
static wbstartstruct_t staticWmInfo;
DIntermissionController* FLevelLocals::CreateIntermission()
{
DIntermissionController* controller = nullptr;
cluster_info_t *nextcluster;
cluster_info_t *thiscluster;
if (flags & LEVEL_CHANGEMAPCHEAT)
return nullptr;
thiscluster = FindClusterInfo (cluster);
bool endgame = strncmp (nextlevel.GetChars(), "enDSeQ", 6) == 0;
if (endgame)
{
FName endsequence = ENamedName(strtoll(nextlevel.GetChars()+6, NULL, 16));
// Strife needs a special case here to choose between good and sad ending. Bad is handled elsewhere.
if (endsequence == NAME_Inter_Strife)
{
if (Players[0]->mo->FindInventory (NAME_QuestItem25) ||
Players[0]->mo->FindInventory (NAME_QuestItem28))
{
endsequence = NAME_Inter_Strife_Good;
}
else
{
endsequence = NAME_Inter_Strife_Sad;
}
}
auto ext = info->ExitMapTexts.CheckKey(flags3 & LEVEL3_EXITSECRETUSED ? NAME_Secret : NAME_Normal);
if (ext != nullptr && (ext->mDefined & FExitText::DEF_TEXT))
{
controller = F_StartFinale(ext->mDefined & FExitText::DEF_MUSIC ? ext->mMusic.GetChars() : gameinfo.finaleMusic.GetChars(),
ext->mDefined & FExitText::DEF_MUSIC ? ext->mOrder : gameinfo.finaleOrder,
-1, 0,
ext->mDefined & FExitText::DEF_BACKDROP ? ext->mBackdrop.GetChars() : gameinfo.FinaleFlat.GetChars(),
ext->mText.GetChars(),
false,
ext->mDefined & FExitText::DEF_PIC,
ext->mDefined & FExitText::DEF_LOOKUP,
true, endsequence);
}
else if (!(info->flags2 & LEVEL2_NOCLUSTERTEXT))
{
controller = F_StartFinale(thiscluster->MessageMusic.GetChars(), thiscluster->musicorder,
thiscluster->cdtrack, thiscluster->cdid,
thiscluster->FinaleFlat.GetChars(), thiscluster->ExitText.GetChars(),
thiscluster->flags & CLUSTER_EXITTEXTINLUMP,
thiscluster->flags & CLUSTER_FINALEPIC,
thiscluster->flags & CLUSTER_LOOKUPEXITTEXT,
true, endsequence);
}
}
else if (!deathmatch)
{
FExitText *ext = nullptr;
if (flags3 & LEVEL3_EXITSECRETUSED) ext = info->ExitMapTexts.CheckKey(NAME_Secret);
else if (flags3 & LEVEL3_EXITNORMALUSED) ext = info->ExitMapTexts.CheckKey(NAME_Normal);
if (ext == nullptr) ext = info->ExitMapTexts.CheckKey(nextlevel);
if (ext != nullptr)
{
if ((ext->mDefined & FExitText::DEF_TEXT))
{
controller = F_StartFinale(ext->mDefined & FExitText::DEF_MUSIC ? ext->mMusic.GetChars() : gameinfo.finaleMusic.GetChars(),
ext->mDefined & FExitText::DEF_MUSIC ? ext->mOrder : gameinfo.finaleOrder,
-1, 0,
ext->mDefined & FExitText::DEF_BACKDROP ? ext->mBackdrop.GetChars() : gameinfo.FinaleFlat.GetChars(),
ext->mText.GetChars(),
false,
ext->mDefined & FExitText::DEF_PIC,
ext->mDefined & FExitText::DEF_LOOKUP,
false);
}
return controller;
}
nextcluster = FindClusterInfo (FindLevelInfo (nextlevel.GetChars())->cluster);
if (nextcluster->cluster != cluster && !(info->flags2 & LEVEL2_NOCLUSTERTEXT))
{
// Only start the finale if the next level's cluster is different
// than the current one and we're not in deathmatch.
if (nextcluster->EnterText.IsNotEmpty())
{
controller = F_StartFinale (nextcluster->MessageMusic.GetChars(), nextcluster->musicorder,
nextcluster->cdtrack, nextcluster->cdid,
nextcluster->FinaleFlat.GetChars(), nextcluster->EnterText.GetChars(),
nextcluster->flags & CLUSTER_ENTERTEXTINLUMP,
nextcluster->flags & CLUSTER_FINALEPIC,
nextcluster->flags & CLUSTER_LOOKUPENTERTEXT,
false);
}
else if (thiscluster->ExitText.IsNotEmpty())
{
controller = F_StartFinale (thiscluster->MessageMusic.GetChars(), thiscluster->musicorder,
thiscluster->cdtrack, nextcluster->cdid,
thiscluster->FinaleFlat.GetChars(), thiscluster->ExitText.GetChars(),
thiscluster->flags & CLUSTER_EXITTEXTINLUMP,
thiscluster->flags & CLUSTER_FINALEPIC,
thiscluster->flags & CLUSTER_LOOKUPEXITTEXT,
false);
}
}
}
return controller;
}
//=============================================================================
//
//
//
//=============================================================================
void RunIntermission(level_info_t* fromMap, level_info_t* toMap, DIntermissionController* intermissionScreen, DObject* statusScreen, std::function<void(bool)> completionf)
{
cutscene.runner = CreateRunner(false);
GC::WriteBarrier(cutscene.runner);
cutscene.completion = std::move(completionf);
// retrieve cluster relations for cluster-based cutscenes.
cluster_info_t* fromcluster = nullptr, *tocluster = nullptr;
if (fromMap) fromcluster = FindClusterInfo(fromMap->cluster);
if (toMap) tocluster = FindClusterInfo(toMap->cluster);
if (fromcluster == tocluster) fromcluster = tocluster = nullptr;
if (fromMap)
{
if (!CreateCutscene(&fromMap->outro, cutscene.runner, fromMap))
{
if (fromcluster != nullptr) CreateCutscene(&fromcluster->outro, cutscene.runner, fromMap);
}
}
auto func = LookupFunction("DoomCutscenes.BuildMapTransition");
if (func == nullptr)
{
I_Error("Script function 'DoomCutscenes.BuildMapTransition' not found");
}
VMValue val[3] = { cutscene.runner, intermissionScreen, statusScreen };
VMCall(func, val, 3, nullptr, 0);
if (toMap)
{
if (!CreateCutscene(&toMap->intro, cutscene.runner, toMap))
{
if (tocluster != nullptr) CreateCutscene(&tocluster->intro, cutscene.runner, toMap);
}
}
if (!ScreenJobValidate())
{
DeleteScreenJob();
if (cutscene.completion) cutscene.completion(false);
cutscene.completion = nullptr;
return;
}
gameaction = ga_intermission;
}
void G_DoCompleted (void)
{
gameaction = ga_nothing;
if ( gamestate == GS_DEMOSCREEN
|| gamestate == GS_FULLCONSOLE
|| gamestate == GS_STARTUP)
{
return;
}
if (gamestate == GS_TITLELEVEL)
{
G_DoLoadLevel (nextlevel, startpos, false, false);
startpos = 0;
viewactive = true;
return;
}
if (automapactive)
AM_Stop ();
// Close the conversation menu if open.
P_FreeStrifeConversations ();
bool playinter = primaryLevel->DoCompleted(nextlevel, staticWmInfo);
S_StopAllChannels();
for (auto Level : AllLevels())
{
SN_StopAllSequences(Level);
}
// todo: create end of level screenjob
DObject* statusScreen = nullptr;
DIntermissionController* intermissionScreen = nullptr;
if (playinter)
{
// [RH] If you ever get a statistics driver operational, adapt this.
// if (statcopy)
// memcpy (statcopy, &wminfo, sizeof(wminfo));
statusScreen = WI_Start (&staticWmInfo);
}
bool endgame = strncmp(nextlevel.GetChars(), "enDSeQ", 6) == 0;
intermissionScreen = primaryLevel->CreateIntermission();
auto nextinfo = !playinter || endgame? nullptr : FindLevelInfo(nextlevel.GetChars(), false);
RunIntermission(primaryLevel->info, nextinfo, intermissionScreen, statusScreen, [=](bool)
{
if (!endgame) primaryLevel->WorldDone();
else D_StartTitle();
});
}
//==========================================================================
//
// Prepare the level to be exited and
// set up the wminfo struct for the coming intermission screen
//
//==========================================================================
bool FLevelLocals::DoCompleted (FString nextlevel, wbstartstruct_t &wminfo)
{
int i;
// [RH] Mark this level as having been visited
if (!(flags & LEVEL_CHANGEMAPCHEAT))
info->flags |= LEVEL_VISITED;
uint32_t langtable[2] = {};
wminfo.finished_ep = cluster - 1;
wminfo.LName0 = TexMan.CheckForTexture(info->PName.GetChars(), ETextureType::MiscPatch);
wminfo.thisname = info->LookupLevelName(&langtable[0]); // re-get the name so we have more info about its origin.
if (!wminfo.LName0.isValid() || !(info->flags3 & LEVEL3_HIDEAUTHORNAME)) wminfo.thisauthor = info->AuthorName;
wminfo.current = MapName;
if (deathmatch &&
(*dmflags & DF_SAME_LEVEL) &&
!(flags & LEVEL_CHANGEMAPCHEAT))
{
wminfo.next = MapName;
wminfo.LName1 = wminfo.LName0;
wminfo.nextname = wminfo.thisname;
wminfo.nextauthor = wminfo.thisauthor;
}
else
{
level_info_t *nextinfo = FindLevelInfo (nextlevel.GetChars(), false);
if (nextinfo == NULL || strncmp (nextlevel.GetChars(), "enDSeQ", 6) == 0)
{
wminfo.next = "";
wminfo.LName1.SetInvalid();
wminfo.nextname = "";
wminfo.nextauthor = "";
}
else
{
wminfo.next = nextinfo->MapName;
wminfo.LName1 = TexMan.CheckForTexture(nextinfo->PName.GetChars(), ETextureType::MiscPatch);
wminfo.nextname = nextinfo->LookupLevelName(&langtable[1]);
if (!wminfo.LName1.isValid() || !(nextinfo->flags3 & LEVEL3_HIDEAUTHORNAME)) wminfo.nextauthor = nextinfo->AuthorName;
}
}
// This cannot use any common localization logic because it may not replace user content at all.
// Unlike the menus, replacements here do not merely change the style but also the content.
// On the other hand, the IWAD lumps may always be replaced with text, because they are the same style as the BigFont.
if (gameinfo.flags & GI_IGNORETITLEPATCHES)
{
FTextureID *texids[] = { &wminfo.LName0, &wminfo.LName1 };
for (int i = 0; i < 2; i++)
{
if (texids[i]->isValid() && langtable[i] != FStringTable::default_table)
{
FGameTexture *tex = TexMan.GetGameTexture(*texids[i]);
if (tex != nullptr && !tex->isUserContent())
{
texids[i]->SetInvalid();
}
}
}
}
CheckWarpTransMap (wminfo.next, true);
nextlevel = wminfo.next;
wminfo.next_ep = FindLevelInfo (wminfo.next.GetChars())->cluster - 1;
wminfo.totalkills = killed_monsters;
wminfo.maxkills = total_monsters;
wminfo.maxitems = total_items;
wminfo.maxsecret = total_secrets;
wminfo.maxfrags = 0;
wminfo.partime = TICRATE * partime;
wminfo.sucktime = sucktime;
wminfo.pnum = consoleplayer;
wminfo.totaltime = totaltime;
for (i=0 ; i<MAXPLAYERS ; i++)
{
wminfo.plyr[i].skills = Players[i]->killcount;
wminfo.plyr[i].sitems = Players[i]->itemcount;
wminfo.plyr[i].ssecret = Players[i]->secretcount;
wminfo.plyr[i].stime = time;
memcpy (wminfo.plyr[i].frags, Players[i]->frags, sizeof(wminfo.plyr[i].frags));
wminfo.plyr[i].fragcount = Players[i]->fragcount;
}
// [RH] If we're in a hub and staying within that hub, take a snapshot.
// If we're traveling to a new hub, take stuff from
// the player and clear the world vars. If this is just an
// ordinary cluster (not a hub), take stuff from the player, but
// leave the world vars alone.
cluster_info_t *thiscluster = FindClusterInfo (cluster);
cluster_info_t *nextcluster = FindClusterInfo (wminfo.next_ep+1); // next_ep is cluster-1
EFinishLevelType mode;
if (thiscluster != nextcluster || deathmatch ||
!(thiscluster->flags & CLUSTER_HUB))
{
if (nextcluster->flags & CLUSTER_HUB)
{
mode = FINISH_NextHub;
}
else
{
mode = FINISH_NoHub;
}
}
else
{
mode = FINISH_SameHub;
}
// Intermission stats for entire hubs
G_LeavingHub(this, mode, thiscluster, &wminfo);
// Do not allow playing sounds in here - they'd never be able to play properly.
soundEngine->BlockNewSounds(true);
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i])
{ // take away appropriate inventory
G_PlayerFinishLevel (i, mode, changeflags);
}
}
soundEngine->BlockNewSounds(false);
if (mode == FINISH_SameHub)
{ // Remember the level's state for re-entry.
if (!(flags2 & LEVEL2_FORGETSTATE))
{
SnapshotLevel ();
// Do not free any global strings this level might reference
// while it's not loaded.
Behaviors.LockLevelVarStrings(levelnum);
}
else
{ // Make sure we don't have a snapshot lying around from before.
info->Snapshot.Clean();
}
}
else
{ // Forget the states of all existing levels.
G_ClearSnapshots ();
if (mode == FINISH_NextHub)
{ // Reset world variables for the new hub.
P_ClearACSVars(false);
}
time = 0;
maptime = 0;
spawnindex = 0;
}
finishstate = mode;
return ShouldDoIntermission(nextcluster, thiscluster);
}
//==========================================================================
//
//
//==========================================================================
class DAutosaver : public DThinker
{
DECLARE_CLASS (DAutosaver, DThinker)
public:
void Construct() {}
void Tick ();
};
IMPLEMENT_CLASS(DAutosaver, false, false)
void DAutosaver::Tick ()
{
Net_WriteInt8 (DEM_CHECKAUTOSAVE);
Destroy ();
}
//==========================================================================
//
// G_DoLoadLevel
//
//==========================================================================
extern gamestate_t wipegamestate;
void G_DoLoadLevel(const FString &nextmapname, int position, bool autosave, bool newGame)
{
gamestate_t oldgs = gamestate;
// Here the new level needs to be allocated.
primaryLevel->DoLoadLevel(nextmapname, position, autosave, newGame);
// Reset the global state for the new level.
if (wipegamestate == GS_LEVEL)
wipegamestate = GS_FORCEWIPE;
if (gamestate != GS_TITLELEVEL)
{
gamestate = GS_LEVEL;
}
gameaction = ga_nothing;
// clear cmd building stuff
buttonMap.ResetButtonStates();
SendItemUse = nullptr;
SendItemDrop = nullptr;
mousex = mousey = 0;
sendpause = sendsave = sendturn180 = SendLand = false;
LocalViewAngle = 0;
LocalViewPitch = 0;
paused = 0;
if (demoplayback || oldgs == GS_STARTUP || oldgs == GS_TITLELEVEL)
C_HideConsole();
C_FlushDisplay();
P_ResetSightCounters(true);
I_UpdateWindowTitle();
}
void FLevelLocals::DoLoadLevel(const FString &nextmapname, int position, bool autosave, bool newGame)
{
MapName = nextmapname;
static int lastposition = 0;
int i;
if (NextSkill >= 0)
{
UCVarValue val;
val.Int = NextSkill;
gameskill->ForceSet (val, CVAR_Int);
NextSkill = -1;
}
if (position == -1)
position = lastposition;
else
lastposition = position;
Init();
StatusBar->DetachAllMessages ();
// Force 'teamplay' to 'true' if need be.
if (flags2 & LEVEL2_FORCETEAMPLAYON)
teamplay = true;
// Force 'teamplay' to 'false' if need be.
if (flags2 & LEVEL2_FORCETEAMPLAYOFF)
teamplay = false;
if (isPrimaryLevel())
{
FString mapname = nextmapname;
mapname.ToUpper();
Printf(PRINT_HIGH | PRINT_NONOTIFY, "\n" TEXTCOLOR_NORMAL "%s\n\n" TEXTCOLOR_BOLD "%s - %s\n\n", console_bar, mapname.GetChars(), LevelName.GetChars());
}
// Set the sky map.
// First thing, we have a dummy sky texture name,
// a flat. The data is in the WAD only because
// we look for an actual index, instead of simply
// setting one.
skyflatnum = TexMan.GetTextureID (gameinfo.SkyFlatName.GetChars(), ETextureType::Flat, FTextureManager::TEXMAN_Overridable);
// [RH] Set up details about sky rendering
InitSkyMap (this);
for (i = 0; i < MAXPLAYERS; i++)
{
if (PlayerInGame(i) && (deathmatch || Players[i]->playerstate == PST_DEAD))
Players[i]->playerstate = PST_ENTER; // [BC]
memset (Players[i]->frags,0,sizeof(Players[i]->frags));
if (!(dmflags2 & DF2_YES_KEEPFRAGS) && (alwaysapplydmflags || deathmatch))
Players[i]->fragcount = 0;
}
if (changeflags & CHANGELEVEL_NOMONSTERS)
{
flags2 |= LEVEL2_NOMONSTERS;
}
else
{
flags2 &= ~LEVEL2_NOMONSTERS;
}
if (changeflags & CHANGELEVEL_PRERAISEWEAPON)
{
flags2 |= LEVEL2_PRERAISEWEAPON;
}
maptime = 0;
if (newGame)
{
staticEventManager.NewGame();
}
P_SetupLevel (this, position, newGame);
//Added by MC: Initialize bots.
if (deathmatch)
{
BotInfo.Init ();
}
if (timingdemo)
{
static bool firstTime = true;
if (firstTime)
{
starttime = I_GetTime ();
firstTime = false;
}
}
starttime = gametic;
UnSnapshotLevel (!savegamerestore); // [RH] Restore the state of the
int pnumerr = FinishTravel ();
if (!FromSnapshot)
{
for (int i = 0; i<MAXPLAYERS; i++)
{
if (PlayerInGame(i) && Players[i]->mo != nullptr)
P_PlayerStartStomp(Players[i]->mo);
}
}
// For each player, if they are viewing through a player, make sure it is themselves.
for (int ii = 0; ii < MAXPLAYERS; ++ii)
{
if (PlayerInGame(ii))
{
if (Players[ii]->camera == nullptr || Players[ii]->camera->player != nullptr)
{
Players[ii]->camera = Players[ii]->mo;
}
if (savegamerestore)
{
continue;
}
const bool fromSnapshot = FromSnapshot;
localEventManager->PlayerEntered(ii, fromSnapshot && finishstate == FINISH_SameHub);
if (fromSnapshot)
{
// ENTER scripts are being handled when the player gets spawned, this cannot be changed due to its effect on voodoo dolls.
Behaviors.StartTypedScripts(SCRIPT_Return, Players[ii]->mo, true);
}
}
}
if (FromSnapshot)
{
// [Nash] run REOPEN scripts upon map re-entry
Behaviors.StartTypedScripts(SCRIPT_Reopen, NULL, false);
}
StatusBar->AttachToPlayer (&players[consoleplayer]);
// unsafe world load
staticEventManager.WorldLoaded();
// regular world load (savegames are handled internally)
localEventManager->WorldLoaded();
DoDeferedScripts (); // [RH] Do script actions that were triggered on another map.
// [RH] Always save the game when entering a new
if (autosave && !savegamerestore && disableautosave < 1)
{
CreateThinker<DAutosaver>();
}
if (pnumerr > 0)
{
I_Error("no start for player %d found.", pnumerr);
}
}
//==========================================================================
//
// G_WorldDone
//
//==========================================================================
void FLevelLocals::WorldDone (void)
{
gameaction = ga_worlddone;
//Added by mc
if (deathmatch)
{
BotInfo.RemoveAllBots(this, consoleplayer != Net_Arbitrator);
}
}
DEFINE_ACTION_FUNCTION(FLevelLocals, WorldDone)
{
// This is just a dummy to make old status screens happy.
return 0;
}
//==========================================================================
//
//
//==========================================================================
void G_DoWorldDone (void)
{
gamestate = GS_LEVEL;
if (nextlevel.IsEmpty())
{
// Don't crash if no next map is given. Just repeat the current one.
Printf ("No next map specified.\n");
nextlevel = primaryLevel->MapName;
}
primaryLevel->StartTravel ();
G_DoLoadLevel (nextlevel, startpos, true, false);
startpos = 0;
gameaction = ga_nothing;
viewactive = true;
}
//==========================================================================
//
// G_StartTravel
//
// Moves players (and eventually their inventory) to a different statnum,
// so they will not be destroyed when switching levels. This only applies
// to real players, not voodoo dolls.
//
//==========================================================================
void FLevelLocals::StartTravel ()
{
if (deathmatch)
return;
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (playeringame[i])
{
AActor *pawn = Players[i]->mo;
AActor *inv;
Players[i]->camera = nullptr;
// Only living players travel. Dead ones get a new body on the new level.
if (Players[i]->health > 0)
{
pawn->UnlinkFromWorld (nullptr);
int tid = pawn->tid; // Save TID
pawn->SetTID(0);
pawn->tid = tid; // Restore TID (but no longer linked into the hash chain)
pawn->ChangeStatNum (STAT_TRAVELLING);
pawn->DeleteAttachedLights();
for (inv = pawn->Inventory; inv != NULL; inv = inv->Inventory)
{
inv->ChangeStatNum (STAT_TRAVELLING);
inv->UnlinkFromWorld (nullptr);
inv->DeleteAttachedLights();
}
}
}
}
BotInfo.StartTravel ();
}
//==========================================================================
//
// G_FinishTravel
//
// Moves any travelling players so that they occupy their newly-spawned
// copies' locations, destroying the new players in the process (because
// they are really fake placeholders to show where the travelling players
// should go).
//
//==========================================================================
int FLevelLocals::FinishTravel ()
{
auto it = GetThinkerIterator<AActor>(NAME_PlayerPawn, STAT_TRAVELLING);
AActor *pawn, *pawndup, *oldpawn, *next;
AActor *inv;
FPlayerStart *start;
int pnum;
int failnum = 0;
//
AActor* pawns[MAXPLAYERS];
int pawnsnum = 0;
next = it.Next ();
while ( (pawn = next) != NULL)
{
next = it.Next ();
pnum = int(pawn->player - players);
pawn->ChangeStatNum (STAT_PLAYER);
pawndup = pawn->player->mo;
assert (pawn != pawndup);
start = PickPlayerStart(pnum, 0);
if (start == NULL)
{
if (pawndup != nullptr)
{
Printf(TEXTCOLOR_RED "No player %d start to travel to!\n", pnum + 1);
// Move to the coordinates this player had when they left the level.
pawn->SetXYZ(pawndup->Pos());
}
else
{
// Could not find a start for this player at all. This really should never happen but if it does, let's better abort.
if (failnum == 0) failnum = pnum + 1;
}
}
oldpawn = pawndup;
// The player being spawned here is a short lived dummy and
// must not start any ENTER script or big problems will happen.
pawndup = SpawnPlayer(start, pnum, SPF_TEMPPLAYER);
if (pawndup != NULL)
{
if (!(changeflags & CHANGELEVEL_KEEPFACING))
{
pawn->Angles = pawndup->Angles;
}
pawn->SetXYZ(pawndup->Pos());
pawn->Vel = pawndup->Vel;
pawn->Sector = pawndup->Sector;
pawn->floorz = pawndup->floorz;
pawn->ceilingz = pawndup->ceilingz;
pawn->dropoffz = pawndup->dropoffz;
pawn->floorsector = pawndup->floorsector;
pawn->floorpic = pawndup->floorpic;
pawn->floorterrain = pawndup->floorterrain;
pawn->ceilingsector = pawndup->ceilingsector;
pawn->ceilingpic = pawndup->ceilingpic;
pawn->Floorclip = pawndup->Floorclip;
pawn->waterlevel = pawndup->waterlevel;
pawn->waterdepth = pawndup->waterdepth;
}
else if (failnum == 0) // In the failure case this may run into some undefined data.
{
P_FindFloorCeiling(pawn);
}
pawn->target = nullptr;
pawn->lastenemy = nullptr;
pawn->player->mo = pawn;
pawn->player->camera = pawn;
pawn->player->viewheight = pawn->player->DefaultViewHeight();
pawn->flags2 &= ~MF2_BLASTED;
if (oldpawn != nullptr)
{
StaticPointerSubstitution (oldpawn, pawn);
oldpawn->Destroy();
}
if (pawndup != NULL)
{
pawndup->Destroy();
}
pawn->LinkToWorld (nullptr);
pawn->ClearInterpolation();
pawn->ClearFOVInterpolation();
const int tid = pawn->tid; // Save TID (actor isn't linked into the hash chain yet)
pawn->tid = 0; // Reset TID
pawn->SetTID(tid); // Set TID (and link actor into the hash chain)
pawn->SetState(pawn->SpawnState);
pawn->player->SendPitchLimits();
for (inv = pawn->Inventory; inv != NULL; inv = inv->Inventory)
{
inv->ChangeStatNum (STAT_INVENTORY);
inv->LinkToWorld (nullptr);
P_FindFloorCeiling(inv, FFCF_ONLYSPAWNPOS);
IFVIRTUALPTRNAME(inv, NAME_Inventory, Travelled)
{
VMValue params[1] = { inv };
VMCall(func, params, 1, nullptr, 0);
}
}
if (ib_compatflags & BCOMPATF_RESETPLAYERSPEED)
{
pawn->Speed = pawn->GetDefault()->Speed;
}
IFVIRTUALPTRNAME(pawn, NAME_PlayerPawn, Travelled)
{
VMValue params[1] = { pawn };
VMCall(func, params, 1, nullptr, 0);
}
// [ZZ] we probably don't want to fire any scripts before all players are in, especially with runNow = true.
pawns[pawnsnum++] = pawn;
}
BotInfo.FinishTravel ();
// make sure that, after travelling has completed, no travelling thinkers are left.
// Since this list is excluded from regular thinker cleaning, anything that may survive through here
// will endlessly multiply and severely break the following savegames or just simply crash on broken pointers.
Thinkers.DestroyThinkersInList(STAT_TRAVELLING);
return failnum;
}
//==========================================================================
//
//
//==========================================================================
FLevelLocals::FLevelLocals() : Behaviors(this), tagManager(this)
{
// Make sure that these point to the right data all the time.
// This will be needed for as long as it takes to completely separate global UI state from per-level play state.
for (int i = 0; i < MAXPLAYERS; i++)
{
Players[i] = &players[i];
}
localEventManager = new EventManager(this);
}
FLevelLocals::~FLevelLocals()
{
if (localEventManager) delete localEventManager;
if (aabbTree) delete aabbTree;
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::Init()
{
P_InitParticles(this);
P_ClearParticles(this);
gravity = sv_gravity * 35/TICRATE;
aircontrol = sv_aircontrol;
AirControlChanged();
teamdamage = ::teamdamage;
flags = 0;
flags2 = 0;
flags3 = 0;
ImpactDecalCount = 0;
frozenstate = 0;
info = FindLevelInfo (MapName.GetChars());
skyspeed1 = info->skyspeed1;
skyspeed2 = info->skyspeed2;
skytexture1 = TexMan.GetTextureID(info->SkyPic1.GetChars(), ETextureType::Wall, FTextureManager::TEXMAN_Overridable | FTextureManager::TEXMAN_ReturnFirst);
skytexture2 = TexMan.GetTextureID(info->SkyPic2.GetChars(), ETextureType::Wall, FTextureManager::TEXMAN_Overridable | FTextureManager::TEXMAN_ReturnFirst);
fadeto = info->fadeto;
cdtrack = info->cdtrack;
cdid = info->cdid;
FromSnapshot = false;
if (fadeto == 0)
{
if (strnicmp (info->FadeTable.GetChars(), "COLORMAP", 8) != 0)
{
flags |= LEVEL_HASFADETABLE;
}
}
globalcolormap = R_ColormapNumForName(info->CustomColorMap.GetChars());
airsupply = info->airsupply*TICRATE;
outsidefog = info->outsidefog;
WallVertLight = info->WallVertLight*2;
WallHorizLight = info->WallHorizLight*2;
if (info->gravity != 0.f)
{
if (info->gravity == DBL_MAX) gravity = 0;
else gravity = info->gravity * 35/TICRATE;
}
if (info->aircontrol != 0.f)
{
aircontrol = info->aircontrol;
}
if (info->teamdamage != 0.f)
{
teamdamage = info->teamdamage;
}
AirControlChanged ();
cluster_info_t *clus = FindClusterInfo (info->cluster);
partime = info->partime;
sucktime = info->sucktime;
cluster = info->cluster;
clusterflags = clus ? clus->flags : 0;
flags |= info->flags;
flags2 |= info->flags2;
flags3 |= info->flags3;
levelnum = info->levelnum;
LightningSound = info->LightningSound;
Music = info->Music;
musicorder = info->musicorder;
MusicVolume = 1.f;
HasHeightSecs = false;
LevelName = info->LookupLevelName();
NextMap = info->NextMap;
NextSecretMap = info->NextSecretMap;
F1Pic = info->F1Pic;
AuthorName = info->AuthorName;
hazardcolor = info->hazardcolor;
hazardflash = info->hazardflash;
// GL fog stuff modifiable by SetGlobalFogParameter.
fogdensity = info->fogdensity;
outsidefogdensity = info->outsidefogdensity;
skyfog = info->skyfog;
deathsequence = info->deathsequence;
pixelstretch = info->pixelstretch;
compatflags->Callback();
compatflags2->Callback();
DefaultEnvironment = info->DefaultEnvironment;
brightfog = info->brightfog < 0? gl_brightfog : !!info->brightfog;
lightadditivesurfaces = info->lightadditivesurfaces < 0 ? gl_lightadditivesurfaces : !!info->lightadditivesurfaces;
notexturefill = info->notexturefill < 0 ? gl_notexturefill : !!info->notexturefill;
}
//==========================================================================
//
//
//==========================================================================
FString CalcMapName (int episode, int level)
{
FString lumpname;
if (gameinfo.flags & GI_MAPxx)
{
lumpname.Format("MAP%02d", level);
}
else
{
lumpname.Format("E%01dM%01d", episode, level);
}
return lumpname;
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::AirControlChanged ()
{
if (aircontrol <= 1/256.)
{
airfriction = 1.;
}
else
{
// Friction is inversely proportional to the amount of control
airfriction = aircontrol * -0.0941 + 1.0004;
}
}
//==========================================================================
//
//
//==========================================================================
void G_WriteSnapshots(TArray<FString> &filenames, TArray<FCompressedBuffer> &buffers)
{
unsigned int i;
FString filename;
for (i = 0; i < wadlevelinfos.Size(); i++)
{
if (wadlevelinfos[i].Snapshot.mCompressedSize > 0)
{
filename.Format("%s.map.json", wadlevelinfos[i].MapName.GetChars());
filename.ToLower();
filenames.Push(filename);
buffers.Push(wadlevelinfos[i].Snapshot);
}
}
if (TheDefaultLevelInfo.Snapshot.mCompressedSize > 0)
{
filename.Format("%s.mapd.json", TheDefaultLevelInfo.MapName.GetChars());
filename.ToLower();
filenames.Push(filename);
buffers.Push(TheDefaultLevelInfo.Snapshot);
}
}
//==========================================================================
//
//
//==========================================================================
void G_WriteVisited(FSerializer &arc)
{
if (arc.BeginArray("visited"))
{
// Write out which levels have been visited
for (auto & wi : wadlevelinfos)
{
if (wi.flags & LEVEL_VISITED)
{
arc.AddString(nullptr, wi.MapName.GetChars());
}
}
arc.EndArray();
}
// Store player classes to be used when spawning a random class
if (multiplayer)
{
arc.Array("randomclasses", SinglePlayerClass, MAXPLAYERS);
}
if (arc.BeginObject("playerclasses"))
{
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (playeringame[i])
{
FStringf key("%d", i);
arc(key.GetChars(), players[i].cls);
}
}
arc.EndObject();
}
}
//==========================================================================
//
//
//==========================================================================
void G_ReadSnapshots(FResourceFile *resf)
{
FString MapName;
level_info_t *i;
G_ClearSnapshots();
for (unsigned j = 0; j < resf->EntryCount(); j++)
{
auto name = resf->getName(j);
auto ptr = strstr(name, ".map.json");
if (ptr != nullptr)
{
ptrdiff_t maplen = ptr - name;
FString mapname(name, (size_t)maplen);
i = FindLevelInfo(mapname.GetChars());
if (i != nullptr)
{
i->Snapshot = resf->GetRawData(j);
}
}
else
{
auto ptr = strstr(name, ".mapd.json");
if (ptr != nullptr)
{
ptrdiff_t maplen = ptr - name;
FString mapname(name, (size_t)maplen);
TheDefaultLevelInfo.Snapshot = resf->GetRawData(j);
}
}
}
}
//==========================================================================
//
//
//==========================================================================
void G_ReadVisited(FSerializer &arc)
{
if (arc.BeginArray("visited"))
{
for (int s = arc.ArraySize(); s > 0; s--)
{
FString str;
arc(nullptr, str);
auto i = FindLevelInfo(str.GetChars());
if (i != nullptr) i->flags |= LEVEL_VISITED;
}
arc.EndArray();
}
arc.Array("randomclasses", SinglePlayerClass, MAXPLAYERS);
if (arc.BeginObject("playerclasses"))
{
for (int i = 0; i < MAXPLAYERS; ++i)
{
FStringf key("%d", i);
arc(key.GetChars(), players[i].cls);
}
arc.EndObject();
}
}
//==========================================================================
//
//
//==========================================================================
void P_WriteACSDefereds (FSerializer &arc)
{
bool found = false;
// only write this stuff if needed
for (auto &wi : wadlevelinfos)
{
if (wi.deferred.Size() > 0)
{
found = true;
break;
}
}
if (found && arc.BeginObject("deferred"))
{
for (auto &wi : wadlevelinfos)
{
if (wi.deferred.Size() > 0)
{
if (wi.deferred.Size() > 0)
{
arc(wi.MapName.GetChars(), wi.deferred);
}
}
}
arc.EndObject();
}
}
//==========================================================================
//
//
//==========================================================================
void P_ReadACSDefereds (FSerializer &arc)
{
FString MapName;
P_RemoveDefereds ();
if (arc.BeginObject("deferred"))
{
const char *key;
while ((key = arc.GetKey()))
{
level_info_t *i = FindLevelInfo(key);
if (i == NULL)
{
I_Error("Unknown map '%s' in savegame", key);
}
arc(nullptr, i->deferred);
}
arc.EndObject();
}
}
//==========================================================================
//
// This object is responsible for marking sectors during the propagate
// stage. In case there are many, many sectors, it lets us break them
// up instead of marking them all at once.
//
//==========================================================================
class DSectorMarker : public DObject
{
enum
{
SECTORSTEPSIZE = 32,
POLYSTEPSIZE = 120,
SIDEDEFSTEPSIZE = 240
};
DECLARE_CLASS(DSectorMarker, DObject)
public:
DSectorMarker(FLevelLocals *l) : Level(l), SecNum(0),PolyNum(0),SideNum(0) {}
size_t PropagateMark();
FLevelLocals *Level;
int SecNum;
int PolyNum;
int SideNum;
};
IMPLEMENT_CLASS(DSectorMarker, true, false)
//==========================================================================
//
// DSectorMarker :: PropagateMark
//
// Propagates marks across a few sectors and reinserts itself into the
// gray list if it didn't do them all.
//
//==========================================================================
size_t DSectorMarker::PropagateMark()
{
int i;
int marked = 0;
bool moretodo = false;
int numsectors = Level->sectors.Size();
for (i = 0; i < SECTORSTEPSIZE && SecNum + i < numsectors; ++i)
{
sector_t *sec = &Level->sectors[SecNum + i];
GC::Mark(sec->SoundTarget);
GC::Mark(sec->SecActTarget);
GC::Mark(sec->floordata);
GC::Mark(sec->ceilingdata);
GC::Mark(sec->lightingdata);
for(int j = 0; j < 4; j++) GC::Mark(sec->interpolations[j]);
}
marked += i * sizeof(sector_t);
if (SecNum + i < numsectors)
{
SecNum += i;
moretodo = true;
}
if (!moretodo && Level->Polyobjects.Size() > 0)
{
for (i = 0; i < POLYSTEPSIZE && PolyNum + i < (int)Level->Polyobjects.Size(); ++i)
{
GC::Mark(Level->Polyobjects[PolyNum + i].interpolation);
}
marked += i * sizeof(FPolyObj);
if (PolyNum + i < (int)Level->Polyobjects.Size())
{
PolyNum += i;
moretodo = true;
}
}
if (!moretodo && Level->sides.Size() > 0)
{
for (i = 0; i < SIDEDEFSTEPSIZE && SideNum + i < (int)Level->sides.Size(); ++i)
{
side_t *side = &Level->sides[SideNum + i];
for (int j = 0; j < 3; j++) GC::Mark(side->textures[j].interpolation);
}
marked += i * sizeof(side_t);
if (SideNum + i < (int)Level->sides.Size())
{
SideNum += i;
moretodo = true;
}
}
// If there are more items to mark, put ourself back into the gray list.
if (moretodo)
{
Black2Gray();
GCNext = GC::Gray;
GC::Gray = this;
}
return marked;
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::Tick ()
{
// Reset carry sectors
if (Scrolls.Size() > 0)
{
memset (&Scrolls[0], 0, sizeof(Scrolls[0])*Scrolls.Size());
}
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::Mark()
{
if (SectorMarker == nullptr && (sectors.Size() > 0 || Polyobjects.Size() > 0 || sides.Size() > 0))
{
SectorMarker = Create<DSectorMarker>(this);
}
else if (sectors.Size() == 0 && Polyobjects.Size() == 0 && sides.Size() == 0)
{
SectorMarker = nullptr;
}
else
{
SectorMarker->SecNum = 0;
}
GC::Mark(SectorMarker);
GC::Mark(SpotState);
GC::Mark(FraggleScriptThinker);
GC::Mark(ACSThinker);
GC::Mark(automap);
GC::Mark(interpolator.Head);
GC::Mark(SequenceListHead);
GC::Mark(BotInfo.firstthing);
GC::Mark(BotInfo.body1);
GC::Mark(BotInfo.body2);
if (localEventManager)
{
GC::Mark(localEventManager->FirstEventHandler);
GC::Mark(localEventManager->LastEventHandler);
}
Thinkers.MarkRoots();
canvasTextureInfo.Mark();
for (auto &c : CorpseQueue)
{
GC::Mark(c);
}
for (auto &s : sectorPortals)
{
GC::Mark(s.mSkybox);
}
// Mark dead bodies.
for (auto &p : bodyque)
{
GC::Mark(p);
}
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::AddScroller (int secnum)
{
if (secnum < 0)
{
return;
}
if (Scrolls.Size() == 0)
{
Scrolls.Resize(sectors.Size());
memset(&Scrolls[0], 0, sizeof(Scrolls[0])*Scrolls.Size());
}
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::SetInterMusic(const char *nextmap)
{
auto mus = info->MapInterMusic.CheckKey(nextmap);
if (mus != nullptr)
S_ChangeMusic(mus->first.GetChars(), mus->second);
else if (info->InterMusic.IsNotEmpty())
S_ChangeMusic(info->InterMusic.GetChars(), info->intermusicorder);
else
S_ChangeMusic(gameinfo.intermissionMusic.GetChars(), gameinfo.intermissionOrder);
}
DEFINE_ACTION_FUNCTION(FLevelLocals, SetInterMusic)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_STRING(map);
self->SetInterMusic(map.GetChars());
return 0;
}
//==========================================================================
//
//
//==========================================================================
void FLevelLocals::SetMusicVolume(float f)
{
MusicVolume = f;
I_SetMusicVolume(f);
}
//============================================================================
//
//
//
//============================================================================
int FLevelLocals::GetInfighting()
{
if (flags2 & LEVEL2_TOTALINFIGHTING) return 1;
if (flags2 & LEVEL2_NOINFIGHTING) return -1;
return G_SkillProperty(SKILLP_Infight);
}
//============================================================================
//
// transfers the compatiblity flag for old PointOnLineSide to each line.
// This gets checked in a frequently called worker function and the closer
// this info is to the data this function works on, the better.
//
//============================================================================
void FLevelLocals::SetCompatLineOnSide(bool state)
{
int on = (state && (i_compatflags2 & COMPATF2_POINTONLINE));
if (on) for (auto &l : lines) l.flags |= ML_COMPATSIDE;
else for (auto &l : lines) l.flags &= ~ML_COMPATSIDE;
}
int FLevelLocals::GetCompatibility(int mask)
{
if (info == nullptr) return mask;
else return (mask & ~info->compatmask) | (info->compatflags & info->compatmask);
}
int FLevelLocals::GetCompatibility2(int mask)
{
return (info == nullptr) ? mask
: (mask & ~info->compatmask2) | (info->compatflags2 & info->compatmask2);
}
void FLevelLocals::ApplyCompatibility()
{
int old = i_compatflags;
i_compatflags = GetCompatibility(compatflags) | ii_compatflags;
if ((old ^ i_compatflags) & COMPATF_POLYOBJ)
{
ClearAllSubsectorLinks();
}
}
void FLevelLocals::ApplyCompatibility2()
{
i_compatflags2 = GetCompatibility2(compatflags2) | ii_compatflags2;
}
//==========================================================================
// IsPointInMap
//
// Checks to see if a point is inside the void or not.
// Made by dpJudas, modified and implemented by Major Cooke
//==========================================================================
int IsPointInMap(FLevelLocals *Level, double x, double y, double z)
{
// This uses the render nodes because those are guaranteed to be GL nodes, meaning all subsectors are closed.
subsector_t *subsector = Level->PointInRenderSubsector(FLOAT2FIXED(x), FLOAT2FIXED(y));
if (!subsector) return false;
for (uint32_t i = 0; i < subsector->numlines; i++)
{
// Skip double sided lines.
seg_t *seg = subsector->firstline + i;
if (seg->backsector != nullptr || seg->linedef == nullptr) continue;
divline_t dline;
P_MakeDivline(seg->linedef, &dline);
bool pol = P_PointOnDivlineSide(x, y, &dline) < 1;
if (!pol) return false;
}
double ceilingZ = subsector->sector->ceilingplane.ZatPoint(x, y);
if (z > ceilingZ) return false;
double floorZ = subsector->sector->floorplane.ZatPoint(x, y);
if (z < floorZ) return false;
return true;
}
void FLevelLocals::SetMusic()
{
S_ChangeMusic(Music.GetChars(), musicorder);
}
DEFINE_ACTION_FUNCTION(FLevelLocals, GetClusterName)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals)
cluster_info_t* cluster = FindClusterInfo(self->cluster);
FString retval;
if (cluster)
{
if (cluster->flags & CLUSTER_LOOKUPNAME)
retval = GStrings(cluster->ClusterName);
else
retval = cluster->ClusterName;
}
ACTION_RETURN_STRING(retval);
}
DEFINE_ACTION_FUNCTION(FLevelLocals, GetEpisodeName)
{
// this is a bit of a crapshoot because ZDoom never assigned a level to an episode
// and retroactively fixing this is not possible.
// This will need some heuristics to assign a proper episode to each existing level.
// Stuff for later. for now this just checks the STAT module for the currently running episode,
// which should be fine unless cheating.
ACTION_RETURN_STRING(GStrings.localize(STAT_EpisodeName().GetChars()));
}