Reworked Travelling logic
Added safety barriers for STAT_STATIC and STAT_TRAVELLING to avoid potential exploits. Properly clear all internal fields pointing towards non-read barriered data and always relink properly when finishing travelling. Moved ZScript callback to after travelling has finished for all Thinkers, that way everything is initialized correctly. Allow arbitrary thinkers (excluding native ones) to be added to the travelling list (for Actors, their positioning will need to be handled by modders). Ensure lights get re-enabled after travelling.
This commit is contained in:
parent
7ccd0dd3cd
commit
d7d71177d7
10 changed files with 302 additions and 191 deletions
|
|
@ -28,6 +28,7 @@ enum EObjectFlags
|
|||
OF_Released = 1 << 13, // Object was released from the GC system and should not be processed by GC function
|
||||
OF_Networked = 1 << 14, // Object has a unique network identifier that makes it synchronizable between all clients.
|
||||
OF_ClientSide = 1 << 15, // Object is owned by a specific client rather than the server
|
||||
OF_Travelling = 1 << 16, // Object is currently moving from one level to another
|
||||
};
|
||||
|
||||
template<class T> class TObjPtr;
|
||||
|
|
|
|||
373
src/g_level.cpp
373
src/g_level.cpp
|
|
@ -1624,51 +1624,102 @@ void G_DoWorldDone (void)
|
|||
//
|
||||
// G_StartTravel
|
||||
//
|
||||
// Moves players (and eventually their inventory) to a different statnum,
|
||||
// Moves players (and eventually their owned Actors) 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 ()
|
||||
void FLevelLocals::UnlinkActorFromLevel(AActor& mo)
|
||||
{
|
||||
if (deathmatch)
|
||||
return;
|
||||
mo.UnlinkFromWorld(nullptr);
|
||||
mo.UnlinkBehaviorsFromLevel();
|
||||
const int tid = mo.tid;
|
||||
mo.SetTID(0);
|
||||
mo.tid = tid; // Restore the TID for later relinking.
|
||||
mo.DeleteAttachedLights();
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < MAXPLAYERS; ++i)
|
||||
void FLevelLocals::StartTravel()
|
||||
{
|
||||
bTravelling = true;
|
||||
|
||||
if (!deathmatch)
|
||||
{
|
||||
if (playeringame[i])
|
||||
for (size_t i = 0u; i < MAXPLAYERS; ++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);
|
||||
pawn->UnlinkBehaviorsFromLevel();
|
||||
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->UnlinkBehaviorsFromLevel();
|
||||
inv->DeleteAttachedLights();
|
||||
tid = inv->tid;
|
||||
inv->SetTID(0);
|
||||
inv->tid = tid;
|
||||
}
|
||||
}
|
||||
if (PlayerInGame(i) && Players[i]->health > 0 && !(Players[i]->mo->ObjectFlags & OF_EuthanizeMe))
|
||||
Players[i]->mo->ChangeStatNum(STAT_TRAVELLING);
|
||||
}
|
||||
}
|
||||
|
||||
BotInfo.StartTravel ();
|
||||
// Start building the list of everything that needs to travel. Since things can only be added
|
||||
// to STAT_TRAVELLING at this point, it's safe to assume nothing will be removed unless destroyed
|
||||
// (which will clean up anything it owns as well).
|
||||
auto it = GetThinkerIterator<DThinker>(NAME_None, STAT_TRAVELLING);
|
||||
DThinker* th = nullptr;
|
||||
unsigned count = 0u;
|
||||
do
|
||||
{
|
||||
count = 0u;
|
||||
it.Reinit();
|
||||
|
||||
while ((th = it.Next()) != nullptr)
|
||||
{
|
||||
// Already processed this Thinker.
|
||||
if (th->ObjectFlags & OF_Travelling)
|
||||
continue;
|
||||
|
||||
++count;
|
||||
auto mo = dyn_cast<AActor>(th);
|
||||
if (mo != nullptr)
|
||||
{
|
||||
if (mo->player != nullptr)
|
||||
{
|
||||
// No voodoo dolls allowed.
|
||||
if (mo->player->mo != mo)
|
||||
{
|
||||
mo->ChangeStatNum(STAT_PLAYER);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mo->player->Bot != nullptr)
|
||||
mo->player->Bot->ChangeStatNum(STAT_TRAVELLING);
|
||||
}
|
||||
|
||||
if (!mo->IsKindOf(NAME_Inventory) || mo->PointerVar<AActor>(NAME_Owner) == nullptr)
|
||||
{
|
||||
for (AActor* inv = mo->Inventory; inv != nullptr; inv = inv->Inventory)
|
||||
inv->ChangeStatNum(STAT_TRAVELLING);
|
||||
}
|
||||
|
||||
if (!(mo->flags & MF_UNMORPHED) && mo->alternative != nullptr)
|
||||
mo->alternative->ChangeStatNum(STAT_TRAVELLING);
|
||||
}
|
||||
|
||||
th->ObjectFlags |= OF_Travelling;
|
||||
IFOVERRIDENVIRTUALPTRNAME(th, NAME_Thinker, PreTravelled)
|
||||
VMCallVoid<DThinker*>(func, th);
|
||||
}
|
||||
} while (count);
|
||||
|
||||
// Now that the list is complete, unlink everything. We want to do this after the
|
||||
// callback so someone can't attempt to relink things.
|
||||
auto unlinker = GetThinkerIterator<AActor>(NAME_Actor, STAT_TRAVELLING);
|
||||
AActor* mo = nullptr;
|
||||
while ((mo = unlinker.Next()) != nullptr)
|
||||
{
|
||||
UnlinkActorFromLevel(*mo);
|
||||
mo->BlockingMobj = nullptr;
|
||||
mo->BlockingLine = mo->MovementBlockingLine = nullptr;
|
||||
mo->BlockingFloor = mo->BlockingCeiling = mo->Blocking3DFloor = nullptr;
|
||||
mo->Sector = nullptr;
|
||||
mo->subsector = nullptr;
|
||||
mo->section = nullptr;
|
||||
mo->floorsector = mo->ceilingsector = nullptr;
|
||||
}
|
||||
|
||||
bTravelling = false;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
|
|
@ -1682,136 +1733,174 @@ void FLevelLocals::StartTravel ()
|
|||
//
|
||||
//==========================================================================
|
||||
|
||||
int FLevelLocals::FinishTravel ()
|
||||
void FLevelLocals::LinkActorToLevel(AActor& mo)
|
||||
{
|
||||
auto it = GetThinkerIterator<AActor>(NAME_PlayerPawn, STAT_TRAVELLING);
|
||||
AActor *pawn, *pawndup, *oldpawn, *next;
|
||||
AActor *inv;
|
||||
FPlayerStart *start;
|
||||
int pnum;
|
||||
int failnum = 0;
|
||||
mo.LinkToWorld(nullptr);
|
||||
mo.LinkBehaviorsToLevel();
|
||||
const int tid = mo.tid;
|
||||
mo.tid = 0; // Allow the Actor to be linked back into the hashmap.
|
||||
mo.SetTID(tid);
|
||||
mo.SetDynamicLights();
|
||||
}
|
||||
|
||||
//
|
||||
AActor* pawns[MAXPLAYERS];
|
||||
int pawnsnum = 0;
|
||||
|
||||
next = it.Next ();
|
||||
while ( (pawn = next) != NULL)
|
||||
int FLevelLocals::FinishTravel()
|
||||
{
|
||||
// From this point onward things can't be added back into the list, only removed. Callbacks
|
||||
// are done after setting everything up so that the Thinkers have initialized objects to work with.
|
||||
TArray<DThinker*> toCallBack = {};
|
||||
int failNum = 0;
|
||||
auto it = GetThinkerIterator<DThinker>(NAME_None, STAT_TRAVELLING);
|
||||
DThinker* th = nullptr;
|
||||
while ((th = it.Next()) != nullptr)
|
||||
{
|
||||
next = it.Next ();
|
||||
pnum = int(pawn->player - players);
|
||||
pawn->ChangeStatNum (STAT_PLAYER);
|
||||
pawndup = pawn->player->mo;
|
||||
assert (pawn != pawndup);
|
||||
assert(th->ObjectFlags & OF_Travelling);
|
||||
|
||||
start = PickPlayerStart(pnum, 0);
|
||||
if (start == NULL)
|
||||
toCallBack.Push(th);
|
||||
|
||||
th->ObjectFlags &= ~OF_Travelling;
|
||||
auto mo = dyn_cast<AActor>(th);
|
||||
if (mo == nullptr)
|
||||
{
|
||||
if (pawndup != nullptr)
|
||||
if (th->IsKindOf(NAME_Bot))
|
||||
th->ChangeStatNum(STAT_BOT);
|
||||
else
|
||||
th->ChangeStatNum(STAT_DEFAULT);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the actual pawn type here since its player field isn't guaranteed
|
||||
// to be set e.g. unmorphed Actors.
|
||||
const bool player = mo->IsKindOf(NAME_PlayerPawn);
|
||||
if (player)
|
||||
mo->ChangeStatNum(STAT_PLAYER);
|
||||
else if (mo->IsKindOf(NAME_Inventory) && mo->PointerVar<AActor>(NAME_Owner) != nullptr)
|
||||
mo->ChangeStatNum(STAT_INVENTORY);
|
||||
else
|
||||
mo->ChangeStatNum(STAT_DEFAULT);
|
||||
|
||||
mo->flags2 &= ~MF2_BLASTED;
|
||||
mo->ClearFOVInterpolation();
|
||||
LinkActorToLevel(*mo);
|
||||
|
||||
if (!player || mo->player == nullptr)
|
||||
{
|
||||
// Do some basic relinking. Modders will figure out what to do with it
|
||||
// in the callback.
|
||||
if (mo->flags & MF_UNMORPHED)
|
||||
{
|
||||
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());
|
||||
mo->Angles = mo->alternative->Angles;
|
||||
mo->SetOrigin(mo->alternative->Pos(), true);
|
||||
}
|
||||
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;
|
||||
P_FindFloorCeiling(mo);
|
||||
}
|
||||
|
||||
mo->Vel.Zero();
|
||||
mo->ClearInterpolation();
|
||||
mo->UpdateWaterLevel(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
const int pNum = PlayerNum(mo->player);
|
||||
// This will be whatever previous pawn was in the level for this player, be it from a snapshot
|
||||
// or a map spawn.
|
||||
auto doll = mo->player->mo;
|
||||
assert(mo != doll);
|
||||
|
||||
auto start = PickPlayerStart(pNum, 0);
|
||||
if (start == nullptr)
|
||||
{
|
||||
if (doll != nullptr)
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "No player %d start to travel to\n", pNum + 1);
|
||||
mo->SetOrigin(doll->Pos(), true);
|
||||
}
|
||||
else if (failNum <= 0)
|
||||
{
|
||||
// Couldn't find a start for this player at all. This should never happen but if it does, let's abort.
|
||||
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)
|
||||
auto mapDoll = doll;
|
||||
|
||||
// Find the actual spawn location taking hub entrances into account. This player
|
||||
// is only meant to be short-lived so don't fire off any events unless there truly
|
||||
// was no other player spawned beforehand (can happen in co-op).
|
||||
doll = SpawnPlayer(start, pNum, SPF_TEMPPLAYER);
|
||||
if (doll != nullptr)
|
||||
{
|
||||
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)
|
||||
{
|
||||
PlayerPointerSubstitution (oldpawn, pawn, true);
|
||||
oldpawn->Destroy();
|
||||
}
|
||||
if (pawndup != NULL)
|
||||
{
|
||||
pawndup->Destroy();
|
||||
}
|
||||
pawn->LinkToWorld (nullptr);
|
||||
pawn->LinkBehaviorsToLevel();
|
||||
pawn->ClearInterpolation();
|
||||
pawn->ClearFOVInterpolation();
|
||||
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();
|
||||
mo->Angles = doll->Angles;
|
||||
|
||||
for (inv = pawn->Inventory; inv != NULL; inv = inv->Inventory)
|
||||
{
|
||||
inv->ChangeStatNum (STAT_INVENTORY);
|
||||
inv->LinkToWorld (nullptr);
|
||||
P_FindFloorCeiling(inv, FFCF_ONLYSPAWNPOS);
|
||||
inv->LinkBehaviorsToLevel();
|
||||
tid = inv->tid;
|
||||
inv->tid = 0;
|
||||
inv->SetTID(tid);
|
||||
|
||||
IFVIRTUALPTRNAME(inv, NAME_Inventory, Travelled)
|
||||
{
|
||||
VMValue params[1] = { inv };
|
||||
VMCall(func, params, 1, nullptr, 0);
|
||||
}
|
||||
mo->SetOrigin(doll->Pos(), true);
|
||||
mo->Vel = doll->Vel;
|
||||
}
|
||||
else
|
||||
{
|
||||
P_FindFloorCeiling(mo);
|
||||
}
|
||||
|
||||
mo->ClearInterpolation();
|
||||
mo->UpdateWaterLevel(false);
|
||||
|
||||
mo->target = nullptr;
|
||||
mo->lastenemy = nullptr;
|
||||
mo->player->mo = mo;
|
||||
mo->player->camera = mo;
|
||||
mo->player->viewheight = mo->player->DefaultViewHeight();
|
||||
|
||||
if (mapDoll != nullptr)
|
||||
{
|
||||
// Make sure anything targetting the previous pawn gets pointed back to the real one.
|
||||
PlayerPointerSubstitution(mapDoll, mo, true);
|
||||
mapDoll->Destroy();
|
||||
}
|
||||
// Don't propagate back any changes this might've made from ZScript since it's meant
|
||||
// to be temporary only.
|
||||
if (doll != nullptr)
|
||||
doll->Destroy();
|
||||
|
||||
mo->player->SendPitchLimits();
|
||||
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;
|
||||
mo->Speed = mo->GetDefault()->Speed;
|
||||
}
|
||||
|
||||
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);
|
||||
ClientsideThinkers.DestroyThinkersInList(STAT_TRAVELLING);
|
||||
return failnum;
|
||||
|
||||
// Some ZScript will be called here so we have to do this last.
|
||||
for (size_t i = 0u; i < MAXPLAYERS; ++i)
|
||||
{
|
||||
if (PlayerInGame(i))
|
||||
Players[i]->mo->SetState(Players[i]->mo->SpawnState);
|
||||
}
|
||||
|
||||
for (auto th : toCallBack)
|
||||
{
|
||||
if (!(th->ObjectFlags & OF_EuthanizeMe))
|
||||
{
|
||||
IFOVERRIDENVIRTUALPTRNAME(th, NAME_Thinker, Travelled)
|
||||
VMCallVoid<DThinker*>(func, th);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto th : toCallBack)
|
||||
{
|
||||
if (th->ObjectFlags & OF_EuthanizeMe)
|
||||
continue;
|
||||
|
||||
auto mo = dyn_cast<AActor>(th);
|
||||
if (mo != nullptr)
|
||||
{
|
||||
mo->ClearFOVInterpolation();
|
||||
mo->ClearInterpolation();
|
||||
}
|
||||
}
|
||||
|
||||
return failNum;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
|
|
|
|||
|
|
@ -271,6 +271,8 @@ public:
|
|||
bool DoCompleted(FString nextlevel, wbstartstruct_t &wminfo);
|
||||
void StartTravel();
|
||||
int FinishTravel();
|
||||
void UnlinkActorFromLevel(AActor& mo);
|
||||
void LinkActorToLevel(AActor& mo);
|
||||
void ChangeLevel(const char *levelname, int position, int flags, int nextSkill = -1);
|
||||
const char *GetSecretExitMap();
|
||||
void ExitLevel(int position, bool keepFacing);
|
||||
|
|
|
|||
|
|
@ -481,6 +481,7 @@ xx(Strength)
|
|||
xx(Mode)
|
||||
xx(PowerupType)
|
||||
xx(PlayerPawn)
|
||||
xx(Bot)
|
||||
xx(RipSound)
|
||||
xx(Archvile)
|
||||
xx(Obituary)
|
||||
|
|
|
|||
|
|
@ -290,14 +290,6 @@ void FLevelLocals::ClearPortals()
|
|||
void FLevelLocals::ClearLevelData(bool fullgc)
|
||||
{
|
||||
{
|
||||
auto it = GetThinkerIterator<AActor>(NAME_None, STAT_TRAVELLING);
|
||||
for (AActor *actor = it.Next(); actor != nullptr; actor = it.Next())
|
||||
{
|
||||
actor->BlockingMobj = nullptr;
|
||||
actor->BlockingLine = actor->MovementBlockingLine = nullptr;
|
||||
actor->BlockingFloor = actor->BlockingCeiling = actor->Blocking3DFloor = nullptr;
|
||||
}
|
||||
|
||||
// Make sure map data gets cleared appropriately so any leftover Objects aren't pointing
|
||||
// towards anything invalid.
|
||||
FName fieldTypes[] = { NAME_SectorPortal, NAME_LinePortal, NAME_Vertex, NAME_Side, NAME_Line, NAME_SecPlane, NAME_F3DFloor, NAME_Sector };
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ struct ProfileInfo
|
|||
static TMap<FName, ProfileInfo> Profiles;
|
||||
static unsigned int profilethinkers, profilelimit;
|
||||
DThinker *NextToThink;
|
||||
// Denote that non-travelling Thinkers are about to start doing so. While active, only
|
||||
// allow things to be added to the list, not removed.
|
||||
bool bTravelling = false;
|
||||
|
||||
//==========================================================================
|
||||
//
|
||||
|
|
@ -908,6 +911,64 @@ void DThinker::ChangeStatNum (int statnum)
|
|||
|
||||
static void ChangeStatNum(DThinker *thinker, int statnum)
|
||||
{
|
||||
// Wait until after these Thinkers are processed to allow modifying them,
|
||||
// otherwise relinking and callbacks will be completely messed up. This can
|
||||
// only happen from map objects spawning in intentionally trying to mess with
|
||||
// the list.
|
||||
if (thinker->ObjectFlags & OF_Travelling)
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Travelling Thinkers cannot have their statnum changed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// This will always break Actors, they should use STAT_TRAVELLING instead to
|
||||
// transition between levels.
|
||||
if (statnum == STAT_STATIC && thinker->IsKindOf(NAME_Actor))
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Actors cannot be added to STAT_STATIC\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bTravelling)
|
||||
{
|
||||
// Don't let things be moved out of the list, only into it.
|
||||
if (statnum != STAT_TRAVELLING)
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Thinkers can only be moved into STAT_TRAVELLING while changing levels\n");
|
||||
return;
|
||||
}
|
||||
// These should be handled by the owning Actor, otherwise they'll lose them and become useless anyway.
|
||||
if (thinker->IsKindOf(NAME_Inventory) && thinker->PointerVar<AActor>(NAME_Owner) != nullptr)
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Owned Inventory items must travel with their owner on level change\n");
|
||||
return;
|
||||
}
|
||||
if (thinker->IsKindOf(NAME_Bot))
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Bot Thinkers must travel with their owner on level change\n");
|
||||
return;
|
||||
}
|
||||
auto mo = dyn_cast<AActor>(thinker);
|
||||
if (mo != nullptr && (mo->flags & MF_UNMORPHED))
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Unmorphed Actors must travel with their owner on level change\n");
|
||||
return;
|
||||
}
|
||||
// These need to be locked down since they have native fields that won't be cleared
|
||||
// properly at the moment.
|
||||
auto cls = thinker->GetClass()->NativeClass();
|
||||
if (cls->TypeName != NAME_Thinker && cls->TypeName != NAME_Actor)
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Native thinkers cannot travel\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (statnum == STAT_TRAVELLING)
|
||||
{
|
||||
Printf(TEXTCOLOR_RED "Thinkers cannot be added to STAT_TRAVELLING while the game isn't changing levels\n");
|
||||
return;
|
||||
}
|
||||
|
||||
thinker->ChangeStatNum(statnum);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ class FThinkerIterator;
|
|||
|
||||
enum { MAX_STATNUM = 127 };
|
||||
|
||||
extern bool bTravelling;
|
||||
|
||||
// Doubly linked ring list of thinkers
|
||||
struct FThinkerList
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue