Skip to content

Commit

Permalink
Reworked Morphing
Browse files Browse the repository at this point in the history
Removed StaticPointerSubstitution in favor of a much safer function that only changes select pointers. As a result the ability to properly modify morphing has been opened back up to ZScript. Many missing virtual callbacks were amended and MorphedDeath has been reworked to only be called back on an actual morphed death. MorphedMonster is no longer required to morph an Actor. CheckUnmorph virtual added that gets called back on morphed Actors. Fixed numerous bugs related to morph behavior.
  • Loading branch information
Boondorl authored and madame-rachelle committed Mar 1, 2024
1 parent b469770 commit 2c09a44
Show file tree
Hide file tree
Showing 15 changed files with 694 additions and 669 deletions.
2 changes: 1 addition & 1 deletion src/g_level.cpp
Expand Up @@ -1711,7 +1711,7 @@ int FLevelLocals::FinishTravel ()
pawn->flags2 &= ~MF2_BLASTED;
if (oldpawn != nullptr)
{
StaticPointerSubstitution (oldpawn, pawn);
PlayerPointerSubstitution (oldpawn, pawn);
oldpawn->Destroy();
}
if (pawndup != NULL)
Expand Down
2 changes: 2 additions & 0 deletions src/namedef_custom.h
Expand Up @@ -145,6 +145,7 @@ xx(Reflection)
xx(CustomInventory)
xx(Inventory)
xx(StateProvider)
xx(ObtainInventory)
xx(CallTryPickup)
xx(QuestItem25)
xx(QuestItem28)
Expand Down Expand Up @@ -465,6 +466,7 @@ xx(MonsterClass)
xx(MorphedMonster)
xx(Wi_NoAutostartMap)

xx(MorphFlags)
xx(Duration)
xx(MorphStyle)
xx(MorphFlash)
Expand Down
4 changes: 2 additions & 2 deletions src/playsim/actor.h
Expand Up @@ -1735,8 +1735,8 @@ struct FTranslatedLineTarget
bool unlinked; // found by a trace that went through an unlinked portal.
};


void StaticPointerSubstitution(AActor* old, AActor* notOld);
void PlayerPointerSubstitution(AActor* oldPlayer, AActor* newPlayer);
int MorphPointerSubstitution(AActor* from, AActor* to);

#define S_FREETARGMOBJ 1

Expand Down
36 changes: 19 additions & 17 deletions src/playsim/p_interaction.cpp
Expand Up @@ -313,36 +313,38 @@ EXTERN_CVAR (Int, fraglimit)

void AActor::Die (AActor *source, AActor *inflictor, int dmgflags, FName MeansOfDeath)
{
// Handle possible unmorph on death
bool wasgibbed = (health < GetGibHealth());

// Check to see if unmorph Actors need to be killed as well. Originally this was always
// called but that puts an unnecessary burden on the modder to determine whether it's
// a valid call or not.
if (alternative != nullptr && !(flags & MF_UNMORPHED))
{
IFVIRTUAL(AActor, MorphedDeath)
{
AActor *realthis = NULL;
int realstyle = 0;
int realhealth = 0;
// Return values are no longer used to ensure things stay properly managed.
AActor* const realMo = alternative;
const int morphStyle = player != nullptr ? player->MorphStyle : IntVar(NAME_MorphFlags);

VMValue params[] = { this };
VMReturn returns[3];
returns[0].PointerAt((void**)&realthis);
returns[1].IntAt(&realstyle);
returns[2].IntAt(&realhealth);
VMCall(func, params, 1, returns, 3);
VMCall(func, params, 1, nullptr, 0);

if (realthis && !(realstyle & MORPH_UNDOBYDEATHSAVES))
// Always kill the dummy Actor if it didn't unmorph, otherwise checking the morph flags.
if (realMo != nullptr && (alternative != nullptr || !(morphStyle & MORPH_UNDOBYDEATHSAVES)))
{
if (wasgibbed)
{
int realgibhealth = realthis->GetGibHealth();
if (realthis->health >= realgibhealth)
{
realthis->health = realgibhealth - 1; // if morphed was gibbed, so must original be (where allowed)l
}
const int realGibHealth = realMo->GetGibHealth();
if (realMo->health >= realGibHealth)
realMo->health = realGibHealth - 1; // If morphed was gibbed, so must original be (where allowed).
}
else if (realMo->health > 0)
{
realMo->health = 0;
}
realthis->CallDie(source, inflictor, dmgflags, MeansOfDeath);
}

realMo->CallDie(source, inflictor, dmgflags, MeansOfDeath);
}
}
}

Expand Down
232 changes: 195 additions & 37 deletions src/playsim/p_mobj.cpp
Expand Up @@ -3767,6 +3767,22 @@ void AActor::Tick ()
static const uint8_t HereticScrollDirs[4] = { 6, 9, 1, 4 };
static const uint8_t HereticSpeedMuls[5] = { 5, 10, 25, 30, 35 };

// Check for Actor unmorphing, but only on the thing that is the morphed Actor.
// Players do their own special checking for this.
if (alternative != nullptr && !(flags & MF_UNMORPHED) && player == nullptr)
{
int res = false;
IFVIRTUAL(AActor, CheckUnmorph)
{
VMValue params[] = { this };
VMReturn ret[] = { &res };
VMCall(func, params, 1, ret, 1);
}

if (res)
return;
}

if (freezetics > 0)
{
freezetics--;
Expand Down Expand Up @@ -5062,6 +5078,16 @@ void AActor::CallDeactivate(AActor *activator)

void AActor::OnDestroy ()
{
// If the Actor is leaving behind a premorph Actor, make sure it gets cleaned up as
// well so it's not stuck in the map.
if (alternative != nullptr && !(flags & MF_UNMORPHED))
{
alternative->ClearCounters();
alternative->alternative = nullptr;
alternative->Destroy();
alternative = nullptr;
}

// [ZZ] call destroy event hook.
// note that this differs from ThingSpawned in that you can actually override OnDestroy to avoid calling the hook.
// but you can't really do that without utterly breaking the game, so it's ok.
Expand Down Expand Up @@ -5183,59 +5209,191 @@ extern bool demonew;

//==========================================================================
//
// This once was the main method for pointer cleanup, but
// nowadays its only use is swapping out PlayerPawns.
// This requires pointer fixing throughout all objects and a few
// global variables, but it only needs to look at pointers that
// can point to a player.
// This function is dangerous and only designed for swapping player pawns
// over to their new ones upon changing levels or respawning. It SHOULD NOT be
// used for anything else! Do not export this functionality as it's
// meant strictly for internal usage. Only swap pointers if the thing being swapped
// to is a type of the thing being swapped from.
//
//==========================================================================

void StaticPointerSubstitution(AActor* old, AActor* notOld)
void PlayerPointerSubstitution(AActor* oldPlayer, AActor* newPlayer)
{
DObject* probe;
size_t changed = 0;
int i;
if (oldPlayer == nullptr || newPlayer == nullptr || oldPlayer == newPlayer
|| !oldPlayer->IsKindOf(NAME_PlayerPawn) || !newPlayer->IsKindOf(NAME_PlayerPawn))
{
return;
}

if (old == nullptr) return;
// Swap over the inventory.
auto func = dyn_cast<PFunction>(newPlayer->GetClass()->FindSymbol(NAME_ObtainInventory, true));
if (func)
{
VMValue params[] = { newPlayer, oldPlayer };
VMCall(func->Variants[0].Implementation, params, 2, nullptr, 0);
}

// This is only allowed to replace players or swap out morphed monsters
if (!old->IsKindOf(NAME_PlayerPawn) || (notOld != nullptr && !notOld->IsKindOf(NAME_PlayerPawn)))
// Go through player infos.
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (notOld == nullptr) return;
if (!old->IsKindOf(NAME_MorphedMonster) && !notOld->IsKindOf(NAME_MorphedMonster)) return;
if (!oldPlayer->Level->PlayerInGame(i))
continue;

auto p = oldPlayer->Level->Players[i];

if (p->mo == oldPlayer)
p->mo = newPlayer;
if (p->poisoner == oldPlayer)
p->poisoner = newPlayer;
if (p->attacker == oldPlayer)
p->attacker = newPlayer;
if (p->camera == oldPlayer)
p->camera = newPlayer;
if (p->ConversationNPC == oldPlayer)
p->ConversationNPC = newPlayer;
if (p->ConversationPC == oldPlayer)
p->ConversationPC = newPlayer;
}
// Go through all objects.
i = 0; DObject* last = 0;
for (probe = GC::Root; probe != NULL; probe = probe->ObjNext)

// Go through sectors.
for (auto& sec : oldPlayer->Level->sectors)
{
i++;
changed += probe->PointerSubstitution(old, notOld);
last = probe;
if (sec.SoundTarget == oldPlayer)
sec.SoundTarget = newPlayer;
}

// Go through players.
for (i = 0; i < MAXPLAYERS; i++)
// Update all the remaining object pointers. This is dangerous but needed to ensure
// everything functions correctly when respawning or changing levels.
for (DObject* probe = GC::Root; probe != nullptr; probe = probe->ObjNext)
probe->PointerSubstitution(oldPlayer, newPlayer);
}

//==========================================================================
//
// This function is much safer than PlayerPointerSubstition as it only truly
// swaps a few safe pointers. This has some extra barriers to it to allow
// Actors to freely morph into other Actors which is its main usage.
// Previously this used raw pointer substitutions but that's far too
// volatile to use with modder-provided information. It also allows morphing
// to be more extendable from ZScript.
//
//==========================================================================

int MorphPointerSubstitution(AActor* from, AActor* to)
{
// Special care is taken here to make sure things marked as a dummy Actor for a morphed thing aren't
// allowed to be changed into other things. Anything being morphed into that's considered a player
// is automatically out of the question to ensure modders aren't swapping clients around.
if (from == nullptr || to == nullptr || from == to || to->player != nullptr
|| (from->flags & MF_UNMORPHED) // Another thing's dummy Actor, unmorphing the wrong way, etc.
|| (from->alternative == nullptr && to->alternative != nullptr) // Morphing into something that's already morphed.
|| (from->alternative != nullptr && from->alternative != to)) // Only allow something morphed to unmorph.
{
if (playeringame[i])
{
AActor* replacement = notOld;
auto& p = players[i];
return false;
}

if (p.mo == old) p.mo = replacement, changed++;
if (p.poisoner.ForceGet() == old) p.poisoner = replacement, changed++;
if (p.attacker.ForceGet() == old) p.attacker = replacement, changed++;
if (p.camera.ForceGet() == old) p.camera = replacement, changed++;
if (p.ConversationNPC.ForceGet() == old) p.ConversationNPC = replacement, changed++;
if (p.ConversationPC.ForceGet() == old) p.ConversationPC = replacement, changed++;
}
const bool toIsPlayer = to->IsKindOf(NAME_PlayerPawn);
if (from->IsKindOf(NAME_PlayerPawn))
{
// Players are only allowed to turn into other valid player pawns. For
// valid pawns, make sure an actual player is changing into an empty one.
// Voodoo dolls aren't allowed to morph since that should be passed to
// the main player directly.
if (!toIsPlayer || from->player == nullptr || from->player->mo != from)
return false;
}
else if (toIsPlayer || from->player != nullptr
|| (from->IsKindOf(NAME_Inventory) && from->PointerVar<AActor>(NAME_Owner) != nullptr)
|| (to->IsKindOf(NAME_Inventory) && to->PointerVar<AActor>(NAME_Owner) != nullptr))
{
// Only allow items to be swapped around if they aren't currently owned. Also prevent non-players from
// turning into fake players.
return false;
}

// Since the check is good, move the inventory items over. This should always be done when
// morphing to emulate Heretic/Hexen's behavior since those stored the inventory in their
// player structs.
auto func = dyn_cast<PFunction>(to->GetClass()->FindSymbol(NAME_ObtainInventory, true));
if (func)
{
VMValue params[] = { to, from };
VMCall(func->Variants[0].Implementation, params, 2, nullptr, 0);
}

// Only change some gameplay-related pointers that we know we can safely swap to whatever
// new Actor class is present.
AActor* mo = nullptr;
auto it = from->Level->GetThinkerIterator<AActor>();
while ((mo = it.Next()) != nullptr)
{
if (mo->target == from)
mo->target = to;
if (mo->tracer == from)
mo->tracer = to;
if (mo->master == from)
mo->master = to;
if (mo->goal == from)
mo->goal = to;
if (mo->lastenemy == from)
mo->lastenemy = to;
if (mo->LastHeard == from)
mo->LastHeard = to;
if (mo->LastLookActor == from)
mo->LastLookActor = to;
if (mo->Poisoner == from)
mo->Poisoner = to;
}

// Go through player infos.
for (int i = 0; i < MAXPLAYERS; ++i)
{
if (!from->Level->PlayerInGame(i))
continue;

auto p = from->Level->Players[i];

if (p->mo == from)
p->mo = to;
if (p->poisoner == from)
p->poisoner = to;
if (p->attacker == from)
p->attacker = to;
if (p->camera == from)
p->camera = to;
if (p->ConversationNPC == from)
p->ConversationNPC = to;
if (p->ConversationPC == from)
p->ConversationPC = to;
}

// Go through sectors.
for (auto& sec : from->Level->sectors)
{
if (sec.SoundTarget == from)
sec.SoundTarget = to;
}

// Remaining maintenance related to morphing.
if (from->player != nullptr)
{
to->player = from->player;
from->player = nullptr;
}

// Go through sectors. Only the level this actor belongs to is relevant.
for (auto& sec : old->Level->sectors)
if (from->alternative != nullptr)
{
if (sec.SoundTarget == old) sec.SoundTarget = notOld;
to->flags &= ~MF_UNMORPHED;
to->alternative = from->alternative = nullptr;
}
else
{
from->flags |= MF_UNMORPHED;
from->alternative = to;
to->alternative = from;
}

return true;
}

void FLevelLocals::PlayerSpawnPickClass (int playernum)
Expand Down Expand Up @@ -5508,7 +5666,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag
if (sec.SoundTarget == oldactor) sec.SoundTarget = nullptr;
}

StaticPointerSubstitution (oldactor, p->mo);
PlayerPointerSubstitution (oldactor, p->mo);

localEventManager->PlayerRespawned(PlayerNum(p));
Behaviors.StartTypedScripts (SCRIPT_Respawn, p->mo, true);
Expand Down
9 changes: 9 additions & 0 deletions src/scripting/thingdef_properties.cpp
Expand Up @@ -1814,6 +1814,15 @@ DEFINE_CLASS_PROPERTY(playerclass, S, PowerMorph)
defaults->PointerVar<PClassActor>(NAME_PlayerClass) = FindClassTentative(str, RUNTIME_CLASS(AActor), bag.fromDecorate);
}

//==========================================================================
//
//==========================================================================
DEFINE_CLASS_PROPERTY(monsterclass, S, PowerMorph)
{
PROP_STRING_PARM(str, 0);
defaults->PointerVar<PClassActor>(NAME_MonsterClass) = FindClassTentative(str, RUNTIME_CLASS(AActor), bag.fromDecorate);
}

//==========================================================================
//
//==========================================================================
Expand Down

0 comments on commit 2c09a44

Please sign in to comment.