diff --git a/src/common/objects/dobjgc.h b/src/common/objects/dobjgc.h index 39f0011e0..4a1d31562 100644 --- a/src/common/objects/dobjgc.h +++ b/src/common/objects/dobjgc.h @@ -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 TObjPtr; diff --git a/src/g_level.cpp b/src/g_level.cpp index 8cac0c179..f44be4f84 100644 --- a/src/g_level.cpp +++ b/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(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(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(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(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(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(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 toCallBack = {}; + int failNum = 0; + auto it = GetThinkerIterator(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(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(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(func, th); + } + } + + for (auto th : toCallBack) + { + if (th->ObjectFlags & OF_EuthanizeMe) + continue; + + auto mo = dyn_cast(th); + if (mo != nullptr) + { + mo->ClearFOVInterpolation(); + mo->ClearInterpolation(); + } + } + + return failNum; } //========================================================================== diff --git a/src/g_levellocals.h b/src/g_levellocals.h index 6975aab5a..7f0bd7c57 100644 --- a/src/g_levellocals.h +++ b/src/g_levellocals.h @@ -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); diff --git a/src/namedef_custom.h b/src/namedef_custom.h index fd6518d53..e59cf0df8 100644 --- a/src/namedef_custom.h +++ b/src/namedef_custom.h @@ -481,6 +481,7 @@ xx(Strength) xx(Mode) xx(PowerupType) xx(PlayerPawn) +xx(Bot) xx(RipSound) xx(Archvile) xx(Obituary) diff --git a/src/p_setup.cpp b/src/p_setup.cpp index 4e1244384..0b652b916 100644 --- a/src/p_setup.cpp +++ b/src/p_setup.cpp @@ -290,14 +290,6 @@ void FLevelLocals::ClearPortals() void FLevelLocals::ClearLevelData(bool fullgc) { { - auto it = GetThinkerIterator(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 }; diff --git a/src/playsim/dthinker.cpp b/src/playsim/dthinker.cpp index a5cffd67a..cee15b99b 100644 --- a/src/playsim/dthinker.cpp +++ b/src/playsim/dthinker.cpp @@ -70,6 +70,9 @@ struct ProfileInfo static TMap 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(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(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); } diff --git a/src/playsim/dthinker.h b/src/playsim/dthinker.h index 8a446e461..163119200 100644 --- a/src/playsim/dthinker.h +++ b/src/playsim/dthinker.h @@ -50,6 +50,8 @@ class FThinkerIterator; enum { MAX_STATNUM = 127 }; +extern bool bTravelling; + // Doubly linked ring list of thinkers struct FThinkerList { diff --git a/wadsrc/static/zscript/actors/inventory/inventory.zs b/wadsrc/static/zscript/actors/inventory/inventory.zs index 986bf786d..25df150b9 100644 --- a/wadsrc/static/zscript/actors/inventory/inventory.zs +++ b/wadsrc/static/zscript/actors/inventory/inventory.zs @@ -968,28 +968,6 @@ class Inventory : Actor } } - //=========================================================================== - // - // Inventory :: PreTravelled - // - // Called when an item in somebody's inventory is about to be carried - // over to another map, in case it needs to do special clean-up. - // - //=========================================================================== - - virtual void PreTravelled() {} - - //=========================================================================== - // - // Inventory :: Travelled - // - // Called when an item in somebody's inventory is carried over to another - // map, in case it needs to do special reinitialization. - // - //=========================================================================== - - virtual void Travelled() {} - //=========================================================================== // // Inventory :: DoEffect diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index dde0fa117..a16183204 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -2271,15 +2271,6 @@ class PlayerPawn : Actor me.ClearInventory(); me.GiveDefaultInventory(); } - - // [MK] notify self and inventory that we're about to travel - // this must be called here so these functions can still have a - // chance to alter the world before a snapshot is done in hubs - me.PreTravelled(); - for (item = me.Inv; item != NULL; item = item.Inv) - { - item.PreTravelled(); - } } //=========================================================================== @@ -2664,30 +2655,6 @@ class PlayerPawn : Actor return wasdrowning; } - //=========================================================================== - // - // PlayerPawn :: PreTravelled - // - // Called before the player moves to another map, in case it needs to do - // special clean-up. This is called right before all carried items - // execute their respective PreTravelled() virtuals. - // - //=========================================================================== - - virtual void PreTravelled() {} - - //=========================================================================== - // - // PlayerPawn :: Travelled - // - // Called when the player moves to another map, in case it needs to do - // special reinitialization. This is called after all carried items have - // executed their respective Travelled() virtuals too. - // - //=========================================================================== - - virtual void Travelled() {} - //---------------------------------------------------------------------------- // // diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index fbfcb0247..b83ecb1fe 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -248,6 +248,24 @@ class Thinker : Object native play return int(tics / TICRATE); } + //=========================================================================== + // + // Called before the Thinker moves to another map, in case it needs to do + // special clean-up. + // + //=========================================================================== + + virtual void PreTravelled() {} + + //=========================================================================== + // + // Called after the Thinker moved to another map, in case it needs to do + // special reinitialization. + // + //=========================================================================== + + virtual void Travelled() {} + } class ThinkerIterator : Object native