@@ -3767,6 +3767,22 @@ void AActor::Tick ()
3767
3767
static const uint8_t HereticScrollDirs[4 ] = { 6 , 9 , 1 , 4 };
3768
3768
static const uint8_t HereticSpeedMuls[5 ] = { 5 , 10 , 25 , 30 , 35 };
3769
3769
3770
+ // Check for Actor unmorphing, but only on the thing that is the morphed Actor.
3771
+ // Players do their own special checking for this.
3772
+ if (alternative != nullptr && !(flags & MF_UNMORPHED) && player == nullptr )
3773
+ {
3774
+ int res = false ;
3775
+ IFVIRTUAL (AActor, CheckUnmorph)
3776
+ {
3777
+ VMValue params[] = { this };
3778
+ VMReturn ret[] = { &res };
3779
+ VMCall (func, params, 1 , ret, 1 );
3780
+ }
3781
+
3782
+ if (res)
3783
+ return ;
3784
+ }
3785
+
3770
3786
if (freezetics > 0 )
3771
3787
{
3772
3788
freezetics--;
@@ -5062,6 +5078,16 @@ void AActor::CallDeactivate(AActor *activator)
5062
5078
5063
5079
void AActor::OnDestroy ()
5064
5080
{
5081
+ // If the Actor is leaving behind a premorph Actor, make sure it gets cleaned up as
5082
+ // well so it's not stuck in the map.
5083
+ if (alternative != nullptr && !(flags & MF_UNMORPHED))
5084
+ {
5085
+ alternative->ClearCounters ();
5086
+ alternative->alternative = nullptr ;
5087
+ alternative->Destroy ();
5088
+ alternative = nullptr ;
5089
+ }
5090
+
5065
5091
// [ZZ] call destroy event hook.
5066
5092
// note that this differs from ThingSpawned in that you can actually override OnDestroy to avoid calling the hook.
5067
5093
// but you can't really do that without utterly breaking the game, so it's ok.
@@ -5183,59 +5209,191 @@ extern bool demonew;
5183
5209
5184
5210
// ==========================================================================
5185
5211
//
5186
- // This once was the main method for pointer cleanup, but
5187
- // nowadays its only use is swapping out PlayerPawns.
5188
- // This requires pointer fixing throughout all objects and a few
5189
- // global variables, but it only needs to look at pointers that
5190
- // can point to a player .
5212
+ // This function is dangerous and only designed for swapping player pawns
5213
+ // over to their new ones upon changing levels or respawning. It SHOULD NOT be
5214
+ // used for anything else! Do not export this functionality as it's
5215
+ // meant strictly for internal usage. Only swap pointers if the thing being swapped
5216
+ // to is a type of the thing being swapped from .
5191
5217
//
5192
5218
// ==========================================================================
5193
5219
5194
- void StaticPointerSubstitution (AActor* old , AActor* notOld )
5220
+ void PlayerPointerSubstitution (AActor* oldPlayer , AActor* newPlayer )
5195
5221
{
5196
- DObject* probe;
5197
- size_t changed = 0 ;
5198
- int i;
5222
+ if (oldPlayer == nullptr || newPlayer == nullptr || oldPlayer == newPlayer
5223
+ || !oldPlayer->IsKindOf (NAME_PlayerPawn) || !newPlayer->IsKindOf (NAME_PlayerPawn))
5224
+ {
5225
+ return ;
5226
+ }
5199
5227
5200
- if (old == nullptr ) return ;
5228
+ // Swap over the inventory.
5229
+ auto func = dyn_cast<PFunction>(newPlayer->GetClass ()->FindSymbol (NAME_ObtainInventory, true ));
5230
+ if (func)
5231
+ {
5232
+ VMValue params[] = { newPlayer, oldPlayer };
5233
+ VMCall (func->Variants [0 ].Implementation , params, 2 , nullptr , 0 );
5234
+ }
5201
5235
5202
- // This is only allowed to replace players or swap out morphed monsters
5203
- if (!old-> IsKindOf (NAME_PlayerPawn) || (notOld != nullptr && !notOld-> IsKindOf (NAME_PlayerPawn)) )
5236
+ // Go through player infos.
5237
+ for ( int i = 0 ; i < MAXPLAYERS; ++i )
5204
5238
{
5205
- if (notOld == nullptr ) return ;
5206
- if (!old->IsKindOf (NAME_MorphedMonster) && !notOld->IsKindOf (NAME_MorphedMonster)) return ;
5239
+ if (!oldPlayer->Level ->PlayerInGame (i))
5240
+ continue ;
5241
+
5242
+ auto p = oldPlayer->Level ->Players [i];
5243
+
5244
+ if (p->mo == oldPlayer)
5245
+ p->mo = newPlayer;
5246
+ if (p->poisoner == oldPlayer)
5247
+ p->poisoner = newPlayer;
5248
+ if (p->attacker == oldPlayer)
5249
+ p->attacker = newPlayer;
5250
+ if (p->camera == oldPlayer)
5251
+ p->camera = newPlayer;
5252
+ if (p->ConversationNPC == oldPlayer)
5253
+ p->ConversationNPC = newPlayer;
5254
+ if (p->ConversationPC == oldPlayer)
5255
+ p->ConversationPC = newPlayer;
5207
5256
}
5208
- // Go through all objects.
5209
- i = 0 ; DObject* last = 0 ;
5210
- for (probe = GC::Root; probe != NULL ; probe = probe-> ObjNext )
5257
+
5258
+ // Go through sectors.
5259
+ for (auto & sec : oldPlayer-> Level -> sectors )
5211
5260
{
5212
- i++;
5213
- changed += probe->PointerSubstitution (old, notOld);
5214
- last = probe;
5261
+ if (sec.SoundTarget == oldPlayer)
5262
+ sec.SoundTarget = newPlayer;
5215
5263
}
5216
5264
5217
- // Go through players.
5218
- for (i = 0 ; i < MAXPLAYERS; i++)
5265
+ // Update all the remaining object pointers. This is dangerous but needed to ensure
5266
+ // everything functions correctly when respawning or changing levels.
5267
+ for (DObject* probe = GC::Root; probe != nullptr ; probe = probe->ObjNext )
5268
+ probe->PointerSubstitution (oldPlayer, newPlayer);
5269
+ }
5270
+
5271
+ // ==========================================================================
5272
+ //
5273
+ // This function is much safer than PlayerPointerSubstition as it only truly
5274
+ // swaps a few safe pointers. This has some extra barriers to it to allow
5275
+ // Actors to freely morph into other Actors which is its main usage.
5276
+ // Previously this used raw pointer substitutions but that's far too
5277
+ // volatile to use with modder-provided information. It also allows morphing
5278
+ // to be more extendable from ZScript.
5279
+ //
5280
+ // ==========================================================================
5281
+
5282
+ int MorphPointerSubstitution (AActor* from, AActor* to)
5283
+ {
5284
+ // Special care is taken here to make sure things marked as a dummy Actor for a morphed thing aren't
5285
+ // allowed to be changed into other things. Anything being morphed into that's considered a player
5286
+ // is automatically out of the question to ensure modders aren't swapping clients around.
5287
+ if (from == nullptr || to == nullptr || from == to || to->player != nullptr
5288
+ || (from->flags & MF_UNMORPHED) // Another thing's dummy Actor, unmorphing the wrong way, etc.
5289
+ || (from->alternative == nullptr && to->alternative != nullptr ) // Morphing into something that's already morphed.
5290
+ || (from->alternative != nullptr && from->alternative != to)) // Only allow something morphed to unmorph.
5219
5291
{
5220
- if (playeringame[i])
5221
- {
5222
- AActor* replacement = notOld;
5223
- auto & p = players[i];
5292
+ return false ;
5293
+ }
5224
5294
5225
- if (p.mo == old) p.mo = replacement, changed++;
5226
- if (p.poisoner .ForceGet () == old) p.poisoner = replacement, changed++;
5227
- if (p.attacker .ForceGet () == old) p.attacker = replacement, changed++;
5228
- if (p.camera .ForceGet () == old) p.camera = replacement, changed++;
5229
- if (p.ConversationNPC .ForceGet () == old) p.ConversationNPC = replacement, changed++;
5230
- if (p.ConversationPC .ForceGet () == old) p.ConversationPC = replacement, changed++;
5231
- }
5295
+ const bool toIsPlayer = to->IsKindOf (NAME_PlayerPawn);
5296
+ if (from->IsKindOf (NAME_PlayerPawn))
5297
+ {
5298
+ // Players are only allowed to turn into other valid player pawns. For
5299
+ // valid pawns, make sure an actual player is changing into an empty one.
5300
+ // Voodoo dolls aren't allowed to morph since that should be passed to
5301
+ // the main player directly.
5302
+ if (!toIsPlayer || from->player == nullptr || from->player ->mo != from)
5303
+ return false ;
5304
+ }
5305
+ else if (toIsPlayer || from->player != nullptr
5306
+ || (from->IsKindOf (NAME_Inventory) && from->PointerVar <AActor>(NAME_Owner) != nullptr )
5307
+ || (to->IsKindOf (NAME_Inventory) && to->PointerVar <AActor>(NAME_Owner) != nullptr ))
5308
+ {
5309
+ // Only allow items to be swapped around if they aren't currently owned. Also prevent non-players from
5310
+ // turning into fake players.
5311
+ return false ;
5312
+ }
5313
+
5314
+ // Since the check is good, move the inventory items over. This should always be done when
5315
+ // morphing to emulate Heretic/Hexen's behavior since those stored the inventory in their
5316
+ // player structs.
5317
+ auto func = dyn_cast<PFunction>(to->GetClass ()->FindSymbol (NAME_ObtainInventory, true ));
5318
+ if (func)
5319
+ {
5320
+ VMValue params[] = { to, from };
5321
+ VMCall (func->Variants [0 ].Implementation , params, 2 , nullptr , 0 );
5322
+ }
5323
+
5324
+ // Only change some gameplay-related pointers that we know we can safely swap to whatever
5325
+ // new Actor class is present.
5326
+ AActor* mo = nullptr ;
5327
+ auto it = from->Level ->GetThinkerIterator <AActor>();
5328
+ while ((mo = it.Next ()) != nullptr )
5329
+ {
5330
+ if (mo->target == from)
5331
+ mo->target = to;
5332
+ if (mo->tracer == from)
5333
+ mo->tracer = to;
5334
+ if (mo->master == from)
5335
+ mo->master = to;
5336
+ if (mo->goal == from)
5337
+ mo->goal = to;
5338
+ if (mo->lastenemy == from)
5339
+ mo->lastenemy = to;
5340
+ if (mo->LastHeard == from)
5341
+ mo->LastHeard = to;
5342
+ if (mo->LastLookActor == from)
5343
+ mo->LastLookActor = to;
5344
+ if (mo->Poisoner == from)
5345
+ mo->Poisoner = to;
5346
+ }
5347
+
5348
+ // Go through player infos.
5349
+ for (int i = 0 ; i < MAXPLAYERS; ++i)
5350
+ {
5351
+ if (!from->Level ->PlayerInGame (i))
5352
+ continue ;
5353
+
5354
+ auto p = from->Level ->Players [i];
5355
+
5356
+ if (p->mo == from)
5357
+ p->mo = to;
5358
+ if (p->poisoner == from)
5359
+ p->poisoner = to;
5360
+ if (p->attacker == from)
5361
+ p->attacker = to;
5362
+ if (p->camera == from)
5363
+ p->camera = to;
5364
+ if (p->ConversationNPC == from)
5365
+ p->ConversationNPC = to;
5366
+ if (p->ConversationPC == from)
5367
+ p->ConversationPC = to;
5368
+ }
5369
+
5370
+ // Go through sectors.
5371
+ for (auto & sec : from->Level ->sectors )
5372
+ {
5373
+ if (sec.SoundTarget == from)
5374
+ sec.SoundTarget = to;
5375
+ }
5376
+
5377
+ // Remaining maintenance related to morphing.
5378
+ if (from->player != nullptr )
5379
+ {
5380
+ to->player = from->player ;
5381
+ from->player = nullptr ;
5232
5382
}
5233
5383
5234
- // Go through sectors. Only the level this actor belongs to is relevant.
5235
- for (auto & sec : old->Level ->sectors )
5384
+ if (from->alternative != nullptr )
5236
5385
{
5237
- if (sec.SoundTarget == old) sec.SoundTarget = notOld;
5386
+ to->flags &= ~MF_UNMORPHED;
5387
+ to->alternative = from->alternative = nullptr ;
5238
5388
}
5389
+ else
5390
+ {
5391
+ from->flags |= MF_UNMORPHED;
5392
+ from->alternative = to;
5393
+ to->alternative = from;
5394
+ }
5395
+
5396
+ return true ;
5239
5397
}
5240
5398
5241
5399
void FLevelLocals::PlayerSpawnPickClass (int playernum)
@@ -5508,7 +5666,7 @@ AActor *FLevelLocals::SpawnPlayer (FPlayerStart *mthing, int playernum, int flag
5508
5666
if (sec.SoundTarget == oldactor) sec.SoundTarget = nullptr ;
5509
5667
}
5510
5668
5511
- StaticPointerSubstitution (oldactor, p->mo );
5669
+ PlayerPointerSubstitution (oldactor, p->mo );
5512
5670
5513
5671
localEventManager->PlayerRespawned (PlayerNum (p));
5514
5672
Behaviors.StartTypedScripts (SCRIPT_Respawn, p->mo , true );
0 commit comments