Get Line Special/Action? Telekinesis Ability

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!)
Azagthoth
Posts: 61
Joined: Wed Jan 20, 2021 4:06 pm

Get Line Special/Action? Telekinesis Ability

Post by Azagthoth »

Is this possible? I'm trying to make a sort of telekinesis ability that activates all line action specials in line of sight (or in a radius). I'm thinking I would need a way to get the special action so I can handle each one properly.

I tried achieving the same thing by having a bunch of dummy actors run over lines to activate them but this is way too inconsistent. Works maybe 20% off the time. There's gotta be a better way, right?

Edit:
Just to clarify what I'm trying to do. I found a hacky solution that kinda works but there are some issues. Main issue is its triggering things I don't want it to trigger like Exit Level buttons, teleporters, etc.

Edit2:
I tried adding A_RearrangePointers(AAPTR_NULL, AAPTR_NULL, AAPTR_NULL, PTROP_NOSAFEGUARDS) to make teleporters not trigger on the player at least but it still does somehow. How do the teleporters know to teleport the player if it's not done with pointers? Or am I misunderstanding how A_RearrangePointers works?

This is what I got so far:

Code: Select all

script "SetLineActivationProjectileImpact" (void)
{
	for(int i = 0; i < 65536; i++)
	{
		SetLineActivation (i, GetLineActivation(i)|SPAC_Impact);
	}
}

Code: Select all

ACTOR Telekinesis : Weapon
{
	Weapon.SlotNumber 0
	Tag "Telekinesis"
	+NoAlert
	States
	{
		Ready:
			PUNG A 1 A_WeaponReady
			Loop
		Deselect:
			PUNG A 1 A_Lower
			Loop
		Select:
			PUNG A 1 A_Raise
			TNT1 A 0 ACS_NamedExecute("SetLineActivationProjectileImpact",0)
			Loop
		Fire:
			PUNG B 1
			PUNG C 1 A_SpawnItemEx("LineImpacter",0,0,0,200,0,0,0,0,0,8000)
			PUNG D 1
			PUNG C 1
			PUNG B 1 A_ReFire
			Goto Ready
	}
}

ACTOR LineImpacter
{
	Radius 250
	Height 500
	Scale 41.0
	Projectile
	+FLOORHUGGER
	+NOCLIP
	States
	{
		Spawn:
			BAL1 A 1 Bright A_RearrangePointers(AAPTR_NULL, AAPTR_NULL, AAPTR_NULL, PTROP_NOSAFEGUARDS) 
			BAL1 B 1 Bright A_ChangeFlag("NOCLIP", false)
			Loop
		Death:
			BAL1 A 1 Bright A_JumpIf(TID>8025, "DeathFinal")
			BAL1 B 1 Bright A_SpawnItemEx("LineImpacter",0,0,0,200,0,0,0,0,0,TID+1)
		DeathFinal:
			TNT1 A 0
			Stop
	}
}
Last edited by Azagthoth on Wed Aug 03, 2022 8:31 am, edited 1 time in total.
User avatar
Player701
 
 
Posts: 1401
Joined: Wed May 13, 2009 3:15 am
Discord: Player701#8214
Graphics Processor: nVidia with Vulkan support
Location: Russia

Re: Get Line Special/Action? Telekinesis Ability

Post by Player701 »

This looks like a job for ZScript's [wiki]BlockLinesIterator[/wiki]. Here's some example code that will activate all player-activatable lines in a certain height and radius (by default 256), except exits and teleporters. I think I've accounted for all possible cases, but if you find any bugs, please let me know. If ZScript cannot be used, then I'm afraid you're out of luck because there's no way to retrieve the line special with ACS or DECORATE.

Code: Select all

class Telekinesis : Weapon
{
    Default
    {
        Weapon.SlotNumber 0;
        Tag "Telekinesis";
        +WEAPON.NOALERT;
    }

    States
    {
        Ready:
            PUNG A 1 A_WeaponReady;
            Loop;
        Deselect:
            PUNG A 1 A_Lower;
            Loop;
        Select:
            PUNG A 1 A_Raise;
            Loop;
        Fire:
            PUNG B 1;
            PUNG C 1 A_ActivateLines;
            PUNG D 1;
            PUNG C 1;
            PUNG B 1 A_ReFire;
            Goto Ready;
    }

    action void A_ActivateLines(int dist = 256)
    {
        // This will activate all lines in a radius (and height) of 256 map units (adjustable)
        let it = BlockLinesIterator.CreateFromPos(Pos, dist, dist);

        while (it.Next())
        {
            let l = it.CurLine;

            // Activate all player-activatable lines (NB: SPAC_UseThrough is not an activation type)
            uint activationMask = SPAC_PlayerActivate & ~SPAC_UseThrough;

            if ((l.activation & activationMask) && !invoker.IsExitOrTeleporter(l.special))
            {
                // Make sure activation is possible from this side
                // Remove this code if you want to activate from any side
                // regardless of the player's position relative to the line

                // d > 0 if the player is on the front side,
                // and < 0 if on the back side
                double d = (Pos.X - l.v1.p.X)*l.delta.Y - (Pos.Y - l.v1.p.Y)*l.delta.X;

                // If the line has ONLY some of these flags,
                // it means it can only be activated with the "use" action.
                int usemask = SPAC_Use | SPAC_UseThrough | SPAC_UseBack;
                bool useOnly = ((l.activation & SPAC_PlayerActivate) | usemask) == usemask;

                // Use-only lines can be set up to be activatable from one side only, or from both.
                // These conditions need to be checked for.
                bool activatesFromFront = useOnly ? (l.activation & SPAC_Use) : true;
                bool activatesFromBack = useOnly ? (l.activation & SPAC_UseBack) : true;

                // If we're on the front side, OR the line can also be activated from the back side,
                // then activate the line.
                if (d > 0 && activatesFromFront || d < 0 && activatesFromBack)
                {
                    int side = d > 0 ? Line.front : Line.back;

                    // Remove either the front or back use activation type
                    // to ensure GetLineActivationType picks the correct one
                    int activation = d > 0 ? (l.activation & ~SPAC_USEBACK) : (l.activation & ~SPAC_USE);
                    int activationType = invoker.GetLineActivationType(activation);

                    // Activate the line
                    l.Activate(self, side, activationType);
                }
            }
        }
    }

    // This function returns true if the provided line special number is a level exit or a teleporter.
    // References from: https://zdoom.org/wiki/Category:Teleport_specials,
    // and https://zdoom.org/wiki/Category:Exit_specials
    private bool IsExitOrTeleporter(int special)
    {
        static const int ExitAndTeleporterSpecials[] = 
        {
            Exit_Normal,
            Exit_Secret,
            Teleport_EndGame,
            Teleport_NewMap,
            TeleportGroup,
            Teleport_Line,
            Teleport_NoFog,
            Teleport_NoStop,
            TeleportOther,
            TeleportInSector,
            70, // Teleport is a ZScript function, which overrides the special name and causes an error
            Teleport_ZombieChanger
        };

        for (int i = 0; i < ExitAndTeleporterSpecials.Size(); i++)
        {
            if (special == ExitAndTeleporterSpecials[i])
            {
                return true;
            }
        }

        return false;
    }

    // If a line has multiple activation types, this function returns the most appropriate one.
    private int GetLineActivationType(int activation)
    {
        // Rearrange the values in this array as you see fit.
        // The activation types are selected from top to bottom,
        // e.g. if the line has both "Use" and "Walk over" activations,
        // "Use" will be picked first.
        static const int SortedActivationTypes[] =
        {
            SPAC_Use,
            SPAC_UseBack,
            SPAC_Push,
            SPAC_Impact,
            SPAC_Cross,
            SPAC_AnyCross
        };

        for (int i = 0; i < SortedActivationTypes.Size(); i++)
        {
            int result = SortedActivationTypes[i];

            if (activation & result)
            {
                return result;
            }
        }

        // NB: this should never be reached
        ThrowAbortException("%s: Could not determine activation type for.", GetClassName());
        return -1;
    }
}
Azagthoth
Posts: 61
Joined: Wed Jan 20, 2021 4:06 pm

Re: Get Line Special/Action? Telekinesis Ability

Post by Azagthoth »

Thanks for the answer. I'm new to ZScript tho so bear with me, please. What exactly do I need to remove in the script if I want to make it work on both sides of a line? I assumed removing "if (d > 0 && activatesFromFront || d < 0 && activatesFromBack)" would be it but now its getting some weird things from the lines. I tried adding the missing Line Activations to "SortedActivationTypes" but apparently, it's getting something completely different. Can you make any sense out of this?

Code: Select all

VM execution aborted: Telekinesis: Could not determine activation type for.
Called from Telekinesis.GetLineActivationType at GZ_GCTH.pk3:zscript.zs, line 153
Called from Telekinesis.A_ActivateLines at GZ_GCTH.pk3:zscript.zs, line 77
Called from PSprite.Tick at gzdoom.pk3:zscript/actors/player/player.zs, line 2650
Called from PlayerPawn.TickPSprites at gzdoom.pk3:zscript/actors/player/player.zs, line 504
Called from PlayerPawn.PlayerThink at gzdoom.pk3:zscript/actors/player/player.zs, line 1654
Called from Object.ThrowAbortException [Native]
Called from weapon state Telekinesis.4 in Telekinesis
Called from PSprite.SetState [Native]
User avatar
Player701
 
 
Posts: 1401
Joined: Wed May 13, 2009 3:15 am
Discord: Player701#8214
Graphics Processor: nVidia with Vulkan support
Location: Russia

Re: Get Line Special/Action? Telekinesis Ability

Post by Player701 »

Yeah, sorry, that comment about removing the code was a bit misplaced, since I added it during an earlier revision. This is what A_ActivateLines should look like if activation from any side is to be allowed:

Code: Select all

action void A_ActivateLines(int dist = 256)
{
    // This will activate all lines in a radius (and height) of 256 map units (adjustable),
    let it = BlockLinesIterator.CreateFromPos(Pos, dist, dist);

    while (it.Next())
    {
        let l = it.CurLine;

        // Activate all player-activatable lines (NB: SPAC_UseThrough is not an activation type)
        uint activationMask = SPAC_PlayerActivate & ~SPAC_UseThrough;

        if ((l.activation & activationMask) && !invoker.IsExitOrTeleporter(l.special))
        {
            // If the line has ONLY some of these flags,
            // it means it can only be activated with the "use" action.
            int usemask = SPAC_Use | SPAC_UseThrough | SPAC_UseBack;
            bool useOnly = ((l.activation & SPAC_PlayerActivate) | usemask) == usemask;

            // Use-only lines can be set up to be activatable from one side only, or from both.
            // These conditions need to be checked for.
            bool activatesFromFront = useOnly ? (l.activation & SPAC_Use) : true;
            bool activatesFromBack = useOnly ? (l.activation & SPAC_UseBack) : true;

            int side = activatesFromFront ? Line.front : Line.back;

            // Remove either the front or back use activation type
            // to ensure GetLineActivationType picks the correct one
            int activation = activatesFromFront ? (l.activation & ~SPAC_USEBACK) : (l.activation & ~SPAC_USE);
            int activationType = invoker.GetLineActivationType(activation);

            // Activate the line
            l.Activate(self, side, activationType);
        }
    }
}
Please let me know if you get any VM aborts with this (and the error message should of course be "Could not determine activation type for line").
Azagthoth
Posts: 61
Joined: Wed Jan 20, 2021 4:06 pm

Re: Get Line Special/Action? Telekinesis Ability

Post by Azagthoth »

No more VM aborts but it doesn't work on doors now. Do you know why?
User avatar
Player701
 
 
Posts: 1401
Joined: Wed May 13, 2009 3:15 am
Discord: Player701#8214
Graphics Processor: nVidia with Vulkan support
Location: Russia

Re: Get Line Special/Action? Telekinesis Ability

Post by Player701 »

It's probably happening because both sides of the door get activated simultaneously and cancel each other out as a result. Not sure if there is an easy way to solve this, perhaps I'll be able to provide some code tomorrow (unless someone else manages to do it before then).
Azagthoth
Posts: 61
Joined: Wed Jan 20, 2021 4:06 pm

Re: Get Line Special/Action? Telekinesis Ability

Post by Azagthoth »

Player701 wrote:It's probably happening because both sides of the door get activated simultaneously and cancel each other out as a result.
Your right. I added back the code from the first version so it triggers doors three times instead. Quite ugly but it works most of the time. Still, some weird doors don't work so there's probably a much better solution.

BTW. I noticed with Eviternity map02 that it exits immediately when I use the script. What's strange is that it uses a line action called 52 "W1 Exit Level" but the ZDoom wiki says that action special 52 is "Scroll_Wall". What's up with that?
User avatar
Player701
 
 
Posts: 1401
Joined: Wed May 13, 2009 3:15 am
Discord: Player701#8214
Graphics Processor: nVidia with Vulkan support
Location: Russia

Re: Get Line Special/Action? Telekinesis Ability

Post by Player701 »

Updated code for A_ActivateLines, should work with doors. I've also added a check to make sure lines that have had their special cleared do not get activated again (their activation wouldn't do anything anyhow).

Code: Select all

action void A_ActivateLines(int dist = 256)
{
    // This will activate all lines in a radius (and height) of 256 map units (adjustable)
    let it = BlockLinesIterator.CreateFromPos(Pos, dist, dist);

    // Store an array of sectors that have been activated as generic doors
    Array<Sector> doors;

    while (it.Next())
    {
        let l = it.CurLine;

        // Activate all player-activatable lines (NB: SPAC_UseThrough is not an activation type)
        uint activationMask = SPAC_PlayerActivate & ~SPAC_UseThrough;

        if (l.special != 0 && (l.activation & activationMask) && !invoker.IsExitOrTeleporter(l.special) && !invoker.IsActivatedGenericDoor(l, doors))
        {
            // If the line has ONLY some of these flags,
            // it means it can only be activated with the "use" action.
            int usemask = SPAC_Use | SPAC_UseThrough | SPAC_UseBack;
            bool useOnly = ((l.activation & SPAC_PlayerActivate) | usemask) == usemask;

            // Use-only lines can be set up to be activatable from one side only, or from both.
            // These conditions need to be checked for.
            bool activatesFromFront = useOnly ? (l.activation & SPAC_Use) : true;
            bool activatesFromBack = useOnly ? (l.activation & SPAC_UseBack) : true;

            int side = activatesFromFront ? Line.front : Line.back;

            // Remove either the front or back use activation type
            // to ensure GetLineActivationType picks the correct one
            int activation = activatesFromFront ? (l.activation & ~SPAC_USEBACK) : (l.activation & ~SPAC_USE);
            int activationType = invoker.GetLineActivationType(activation);

            // Activate the line
            l.Activate(self, side, activationType);
        }
    }
}

// This function checks if the line has a generic door special
// with a tag of 0 that has already been triggered before.
// This should prevent most doors from being activated more than once.
private bool IsActivatedGenericDoor(Line l, Array<Sector> doors)
{
    // Check for generic doors with zero tag
    if (!(l.special == Door_Raise || l.special == Door_LockedRaise) || l.args[0] != 0 || !(l.flags & Line.ML_TWOSIDED))
    {
        return false;
    }

    let sec = l.sidedef[1].sector;

    if (doors.Find(sec) == doors.Size())
    {
        // Not triggered this door yet
        doors.Push(sec);
        return false;
    }

    // Already triggered this door before
    return true;
}
Azagthoth wrote:BTW. I noticed with Eviternity map02 that it exits immediately when I use the script. What's strange is that it uses a line action called 52 "W1 Exit Level" but the ZDoom wiki says that action special 52 is "Scroll_Wall". What's up with that?
Regarding special numbers: the wiki gives the numbers used internally by GZDoom (this is also known as "Hexen" or "ZDoom" format), and vanilla numbers like 52 "W1 Exit Level" are always converted on-the-fly to the ZDoom format (in this particular case, it becomes Exit_Normal(0) with walkover activation).

Regarding Eviternity MAP02: I could not reproduce this, perhaps I have an older version of the WAD in question. But do note that it is impossible to account for all special cases that might lead to triggering unwanted actions. E.g. if the code triggers a line that starts an ACS script, it is by definition impossible to know what is going to happen. In addition to that, there are so-called voodoo scripts featured in Boom-compatible and even some vanilla maps, and interfering with them may lead to unexpected results (like not being able to progress in the map altogether).
Azagthoth
Posts: 61
Joined: Wed Jan 20, 2021 4:06 pm

Re: Get Line Special/Action? Telekinesis Ability

Post by Azagthoth »

Player701 wrote:Updated code for A_ActivateLines
Thanks a lot. Works great!
Player701 wrote:In addition to that, there are so-called voodoo scripts
Yeah, that's definitely what it is looking a bit closer at the map.

Return to “Scripting”