I don't know if this is any help, but this is what I did for the cats mod so cats can 'follow' the player between levels. Feel free to rip code and adapt as you like:
Singleton with STAT_STATIC so it persists between levels:
Code: Select all
// Martin Howe's "Back Cat" - An NPC mod for ZScript.
// Implements a persistent but partial list of all cats.
// Thanks to the ([GQL])?ZDoom teams for their work :D
// This software is distributed under BSD License 2.0.
// Copyright (c) 2020, Martin Howe; all rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of Martin Howe nor the names of any contributors to
// his work may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 MARTIN HOWE 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
Class BlackCatsToken : Inventory
{
override void Travelled()
{
if (owner && owner.player)
{
int playerNumber = PlayerPawn(owner).PlayerNumber();
BlackCatMapTracker.Get().RecreateCats(playerNumber);
}
}
}
class BlackCatMapTrack
{
// Saved data about each cat
class <BlackCat> catClass;
int startHealth;
int currentHealth;
int friendPlayer;
uint catNumber;
int hubNumber;
bool followToMap;
bool followInHub;
bool followToHub;
// Only used during recreation; identifies an
// original instance, identified by global cat
// number, when returning to a hub level.
BlackCat original;
// Initialises a new instance of the class
BlackCatMapTrack Init(BlackCat aCat)
{
catClass = aCat.GetClass();
startHealth = aCat.StartHealth;
currentHealth = aCat.Health;
friendPlayer = aCat.FriendPlayer;
catNumber = aCat.globalCatNumber;
hubNumber = (Level.ClusterFlags & CLUSTER_HUB) ? Level.Cluster : -1;
followToMap = aCat.bFollowPlayerToMap;
followInHub = aCat.bFollowPlayerInHub;
followToHub = aCat.bFollowPlayerToHub;
original = null;
return self;
}
}
class BlackCatMapTracker : Thinker
{
// Global cat tracking ID
protected uint globalCatID;
// Gets the next cat tracking ID
uint GetNextCatTrackingID()
{
globalCatID++;
return globalCatID;
}
// List of cats to be tracked
protected Array<BlackCatMapTrack> cats;
// Changes to list since last purge
protected uint changes;
// Changes in list allowed before next purge
const purgeThreshold = 8;
// Level time of last cat destructor call
int levelTime;
// Returns the number of cats being tracked
uint Size()
{
return cats.Size();
}
// Removes all cats from the collection
void Clear()
{
levelTime = level.maptime;
let count = cats.size();
// GC?
for (uint index = 0; index < count; index++)
{
cats[index] = null;
}
cats.Clear();
changes = 0;
}
// Checks for trailing null entries in the
// list and removes any that are found.
//
// This is a simple form of GC, used to keep the
// list size to within reasonable limits.
protected void Purge()
{
// Get the number of cats
let count = cats.size();
// If there are none, there's nothing to do
if (count == 0)
{
return;
}
// Count null entries
let removedCount = 0;
for (uint index = 0; index < count; index++)
{
if (!cats[index])
{
removedCount++;
}
}
// If all cats were removed then it is done
if (removedCount == count)
{
cats.Clear();
return;
}
// If no cats were removed then it is done
if (!removedCount)
{
return;
}
// We now know that there is at least one each
// of null and non-null entries and thus that
// count and removedCount are both non-zero.
// Find the first null entry. Due to the above,
// we know this code block will *always* exit
// with a valid firstNull index in the array.
let firstNull = 0;
while (firstNull < count)
{
if (!cats[firstNull])
{
break;
}
firstNull++;
}
if (firstNull == count)
{
ThrowAbortException("**** FATAL: EINVAL(BlackCatMapTracker.Purge: condition \"\'firstNull\' (%u) == \'count\' (%u)\" should be impossible)", firstNull, count);
}
// Purge null entries
let destination = firstNull;
let source = firstNull + 1;
while (source < count)
{
// Find the next non-null entry (if any)
while ((source < count) && !cats[source])
{
source++;
}
// If reached the end then it is completed
if (source == count)
{
break;
}
// Move the entry down
cats[destination] = cats[source];
destination++;
source++;
}
// Destination is the *index* of the next
// *empty* entry (if any); so at the end, it
// is also the *count* of *non-empty* entries.
if (destination != (count - removedCount))
{
ThrowAbortException("**** FATAL: EINCON(BlackCatMapTracker.Purge: condition \"\'destination\' (%u) != \'count\' (%u) - \'removedCount\' (%u)\" should be impossible)", destination, count, removedCount);
}
// Shrink the array and exit
cats.Resize(destination);
return;
}
// Cat uses this to notify the tracker that a cat
// is about to be destroyed and that enough of its
// properties should be saved to recreate it when
// the next level is started. This method treats
// dead cats as a fatal error; only live cats
// should be trying to follow the player across
// level changes (i.e., via end-level teleporter).
//
// The time of the cat notification is checked, to
// avoid doing this for cats manually removed from
// the map via a script; cats destroyed with a
// higher level time remove previous timed cats,
// so that ultimately only cats destroyed due to
// level end remain; if there is any possibility
// that a cat destroyed by a script could be the
// last cat destroyed, the script should clear
// the bFollowPlayerToLevels flag on the cat.
//
// This also means that by the time Travelled() is
// called on any CatsToken, the list is finalised
// all of the cats in it are from the same level.
//
void NotifyCatDestroying(BlackCat aCat)
{
if (!aCat) { ThrowAbortException("**** FATAL: EISNUL(BlackCatMapTracker.NotifyCatDestroying: argument \'aCat\' (null) should specify an \'Actor\'"); }
if (!(aCat is "BlackCat")) { ThrowAbortException("**** FATAL: ERANGE(BlackCatMapTracker.NotifyCatDestroying: argument \'aCat\' (of class: \"%s\")should specify an actor derived from \'BlackCat\'", aCat.GetClassName()); }
if (aCat.Health < 1) { ThrowAbortException("**** FATAL: ERANGE(BlackCatMapTracker.NotifyCatDestroying: argument \'aCat\' (of health: %d) should specify a cat that is alive", aCat.Health); }
if (level.maptime > levelTime)
{
self.Clear();
}
cats.Push(new("BlackCatMapTrack").Init(aCat));
changes++;
if (changes > purgeThreshold)
{
self.Purge();
changes = 0;
}
}
// Spawns the cats in a specific cats list in
// front of the player, spreading them around as
// far as necessary to spawn them, while taking
// care to ensure they do not spawn in the void.
const yspread = 256;
void SpawnCats(actor player, array<BlackCatMapTrack> catsToSpawn)
{
uint count = catsToSpawn.Size();
if (!count)
{
return;
}
int yofs = -(yspread / 2);
int ystep = yspread / count;
double a = player.angle;
double s = sin(a);
double c = cos(a);
double z = player.pos.z;
// Offset code adapted from that in A_SpawnItemEx
for (int cat = 0 ; cat < count ; cat++)
{
BlackCatMapTrack track = catsToSpawn[cat];
// Destroy the original cat, if any, so
// that the "clone" seems in-game to have
// followed the player back to the level.
if (track.original)
{
BlackCat oCat = track.original;
oCat.bFollowPlayerToMap = false;
oCat.bFollowPlayerInHub = false;
oCat.bFollowPlayerToHub = false;
oCat.Destroy();
}
// Recreate the cat
for (int attempt = 0; attempt < 32; attempt++)
{
int xofs = 32 * attempt;
vector3 newpos = player.Vec2OffsetZ(xofs * c + yofs * s, xofs * s - yofs * c, z);
if (Level.IsPointInLevel(newpos))
{
bool wasSpawned;
actor whatWasSpawned;
[wasSpawned, whatWasSpawned] = player.A_SpawnItemEx(track.catClass, xofs, yofs, 0);
if (wasSpawned && whatWasSpawned)
{
BlackCat aCat = BlackCat(whatWasSpawned);
aCat.StartHealth = track.startHealth;
aCat.Health = track.currentHealth;
aCat.FriendPlayer = track.FriendPlayer;
aCat.bSpawnWithTeleportFog = true;
aCat.globalCatNumber = track.catNumber;
break;
}
}
}
yofs = yofs + ystep;
}
}
// Clones each cat belonging to the specified
// player; in game it looks like the same cats;
// the data is removed from the list after use.
void RecreateCats(int playerNumber)
{
if (playerNumber >= MAXPLAYERS) { ThrowAbortException("**** FATAL: ERANGE(BlackCatMapTracker.RecreateCats: argument \'playerNumber\' (index %d) should be less than MAXPLAYERS (%d)", playerNumber, MAXPLAYERS); }
Purge();
uint count = cats.Size();
if (!count)
{
return;
}
// If any cats on this new level have the same
// global cat number as any in the collection,
// means we are returning to a level in a hub
// from one in the same hub and their original
// instances have been recreated by the game.
//
// This cannot happen between hubs or non-hub
// levels, as everything on the previous level
// is already discarded by the game; thus this
// only needs checking if this is a hub level.
//
// Strictly, we should check the cat for the
// same hub number as well, but since we would
// still need to check the global cat number,
// which is sufficient by itself, doing so is
// pointless and incurs a performance penalty.
//
// In these cases, the original instances must
// be destroyed, so that the "clones" seem to
// have followed the player back to the level.
if (Level.ClusterFlags & CLUSTER_HUB)
{
ThinkerIterator catScanner = ThinkerIterator.Create("BlackCat", STAT_DEFAULT);
let somePerson = actor(catScanner.Next());
while (somePerson)
{
BlackCat aCat = BlackCat(somePerson);
for (uint i = 0; i < count; i++)
{
BlackCatMapTrack track = cats[i];
if (track.catNumber == aCat.globalCatNumber)
{
track.original = aCat;
break;
}
}
somePerson = actor(catScanner.Next());
}
}
array <BlackCatMapTrack> catsToSpawn;
for (uint i = 0; i < count; i++)
{
BlackCatMapTrack cat = cats[i];
if (cat.friendPlayer == 1 + playerNumber)
{
if ( (cat.followToMap && (cat.hubNumber == -1)) ||
(cat.followInHub && (cat.hubNumber == Level.Cluster)) ||
(cat.followToHub)
)
{
catsToSpawn.Push(cats[i]);
cats[i] = null;
}
}
}
Purge();
SpawnCats(players[playerNumber].mo, catsToSpawn);
}
// Initialises a new instance of the class
protected BlackCatMapTracker Init()
{
globalCatID = 0;
ChangeStatNum(STAT_STATIC);
self.Clear();
return self;
}
// Gets the (only) instance of the class;
// creates it if it does not exist, unless
// only required to check if it has been.
static BlackCatMapTracker Get(bool checkOnly = false)
{
ThinkerIterator i = ThinkerIterator.Create("BlackCatMapTracker", STAT_STATIC);
let p = BlackCatMapTracker(i.Next());
if (!p && !checkOnly)
{
p = new("BlackCatMapTracker").Init();
}
return p;
}
}
It uses OnDestroy:
Code: Select all
// Martin Howe's "Back Cat" - An NPC mod for ZScript.
// Implements cats following player between levels.
// Thanks to the ([GQL])?ZDoom teams for their work :D
// This software is distributed under BSD License 2.0.
// Copyright (c) 2020, Martin Howe; all rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * 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.
// * Neither the name of Martin Howe nor the names of any contributors to
// his work may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 MARTIN HOWE 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 "classes/BlackCatMapTracker.zds"
mixin class BlackCat_Following
{
// Specifies whether the cat was initialised
//
// This is required as cats might be created with
// spawn functions that create an actor and then
// destroy it if there is no room; the custom
// destructor virtual should act only if this cat
// was truly considered to be created and only
// if it must follow the player between levels.
//
protected bool initialised;
// Global number for cats to identify themselves
// when checking if they should be recreated;
// each cat asks global cat tracker for a number;
// this is only needed for following within a hub.
//
// THIS DEPENDS ON THE DEFAULT UINT BEING ZERO!
//
uint globalCatNumber;
// Ensure that local variables are initialised
override void PostBeginPlay()
{
initialised = false;
//obalCatNumber = 0;
}
// If must try to follow player to new level, and
// if truly created, and if still alive, request
// recreation at the next level start (if any).
override void OnDestroy()
{
if (initialised && (Health > 0))
{
bool isHub = !!(Level.ClusterFlags & CLUSTER_HUB);
if ( (bFollowPlayerToMap && !isHub) ||
(bFollowPlayerInHub && isHub) ||
(bFollowPlayerToHub && isHub)
)
{
BlackCatMapTracker.Get().NotifyCatDestroying(self);
}
}
super.OnDestroy();
}
// Call from user code SPAWN
// state or PostBeginPlay.
//
// Gives each player a BlackCatsToken object if
// they haven't already got one, as that detects
// level changes and handles them. This must
// always be done, as the "follow player" flags
// might be changed after the cat is spawned.
//
void BlackCat_Following_Init()
{
initialised = true;
// Only generate new number if not already
// set; otherwise, it means that this cat was
// recreated and then given the number of the
// original instance it is a recreation of;
// this would have been done by the creator
// before the first tick of the cat's thinker.
if (!globalCatNumber)
{
globalCatNumber = BlackCatMapTracker.Get().GetNextCatTrackingID();
}
for (uint i = 0; i < MAXPLAYERS; i++)
{
actor player = players[i].mo;
if (player && !player.FindInventory("BlackCatsToken"))
{
player.GiveInventoryType("BlackCatsToken");
}
}
}
}