[ZScript] Making copies of actors or keeping informations

Ask about ACS, DECORATE, ZScript, or any other scripting questions here!

Moderator: GZDoom Developers

Forum rules
Before asking on how to use a ZDoom feature, read the ZDoom wiki first. If you still don't understand how to use a feature, then ask here.

Please bear in mind that the people helping you do not automatically know how much you know. You may be asked to upload your project file to look at. Don't be afraid to ask questions about what things mean, but also please be patient with the people trying to help you. (And helpers, please be patient with the person you're trying to help!)
User avatar
DevilBlackDeath
Posts: 170
Joined: Fri Sep 06, 2013 2:40 am

[ZScript] Making copies of actors or keeping informations

Post by DevilBlackDeath »

Hello,

So I'd need to be able, particularly on a death exit, to keep a copy of an old actor (when the new map is loaded, the "old" player is already destroyed, and the new player only spawns then) or at the very least be able to keep its inventory. Now my idea was to create a "temporary" actor to give the inventory to, BUT any attempt at creating actor arrays or variables ended up with it being null (which makes sense really). Is there any way to have some sort of temporary actor to transfer the inventory to, with that actor being kept "alive" until after the new level is loaded ?

For reference, I'm working in a StaticEventHandler, in case this would be helpful in creating a temporary actor to keep alive.

Thanks in advance :)
User avatar
DevilBlackDeath
Posts: 170
Joined: Fri Sep 06, 2013 2:40 am

Re: [ZScript] Making copies of actors or keeping information

Post by DevilBlackDeath »

Some further information. Following the source code of ObtainInventory my new idea was getting the pointer to the first inventory, then setting myself as owner. But as I expected, Owner can only be an actor, which both EventHandler and StaticEventHandler are not. My problem remains then the same being able to have a temporary actor that is not destroyed between level loads (intermission).

Edit : I ended up making the camera the owning actor, but the Inventory still gets deleted somehow in the intermission.
User avatar
Cherno
Posts: 1297
Joined: Tue Dec 06, 2016 11:25 am

Re: [ZScript] Making copies of actors or keeping information

Post by Cherno »

You could have the playerpawn have an array of a custom class or struct, which in turn contains variables for the name of the inventory class and the amount. Fill this array by converting the inventory information of the actor before map changing, and give the actor new items based on the contents of the array afterwards.
User avatar
DevilBlackDeath
Posts: 170
Joined: Fri Sep 06, 2013 2:40 am

Re: [ZScript] Making copies of actors or keeping information

Post by DevilBlackDeath »

Cherno wrote:You could have the playerpawn have an array of a custom class or struct, which in turn contains variables for the name of the inventory class and the amount. Fill this array by converting the inventory information of the actor before map changing, and give the actor new items based on the contents of the array afterwards.
Would I be able to extend the base vanilla PlayerPawn to reflect the change on all classes inheriting from it (the minimod is meant to be universal, so as to work with mods) ?

I think I also might have found an easier solution for my particular purpose. Will post about it if it works ! I'll also leave the post for future references.
User avatar
MartinHowe
Posts: 1978
Joined: Mon Aug 11, 2003 1:50 pm
Location: Waveney, United Kingdom

Re: [ZScript] Making copies of actors or keeping information

Post by MartinHowe »

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");
            }
        }
    }
}
User avatar
DevilBlackDeath
Posts: 170
Joined: Fri Sep 06, 2013 2:40 am

Re: [ZScript] Making copies of actors or keeping information

Post by DevilBlackDeath »

So as far as I can tell you use OnDestroy to "realize" it was destroyed at map load and recreate itself ? And the token serves the purpose of keeping information about the cat itself ?

That's a pretty good way of handling that for allies ! Didn't even realize companion mods needed to mind that but it makes sense now. Unfortunately in my case the issue was that the inventory items themselves didn't persist after forced death exits. changing ownership somehow didn't solve it (despite making the owner the camera, which should theoretically remain the same between levels ? unless it's set to null during the intermission, in which case the items would have no owner at map load hmmm =/ )

The dirty trick I used (which I explained in the minimod release topic) was to actually resurrect players on WorldUnloaded. This way the player remains intact when starting the next level. Actually anyone wanting to carry over any amount of information contained in the player's inventory over a death exit could use that since you can simply remove from the inventory what's needed in WorldUnloaded. You could keep inventory items but remove weapons for example.

Return to “Scripting”