• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua , vJass, GUI, TypeScript] UPROOT / ROOT events workaround

Status
Not open for further replies.
Level 12
Joined
Jan 30, 2020
Messages
875
Hello there, just a quick code to help people trying to catch when a building with root ability is actually rooted / uprooted.

To be honest, detecting an uproot is really straightforward as the effect is immediate.
But catching when a building has finished re-rooting is more complex.


After a long chat with people on Disocrd (I added names to the first line of code), we realized that the root ability changes the unit's boolean field called "UNIT_BF_IS_A_BUILDING".

So, by starting a repeating timer, with an interval that does not need to be too small (I chose 0.3s but it could be slower), we will be checking if the unit is considered a building again.


We also need to intercept all other orders the building is given after the root order, as it cancels it.


Lua VERSION

Add this function in your map script or in a Custom Script, and add your actions where the comments say
"Do SOMETHING WITH YOUR ____ BUILDING u", your building being saved in the local variable u.


Lua:
-- Initial idea by Macadamia
-- Credits for suggestions that made this possible (alphabetical order) : Boar, Jim-panse, spellweaver, SwissDerd, Templarfreak, Zed

function ManageRoot()
    local upRoot, reRoot = CreateTrigger(), CreateTrigger()
    for i = 1,  GetPlayers() do
        local p = Player(i-1)
        if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING) then
            TriggerRegisterPlayerUnitEvent(upRoot, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, nil)
            TriggerRegisterPlayerUnitEvent(reRoot, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, nil)
        end
    end
    TriggerAddCondition(upRoot, Condition(function()
        if (GetIssuedOrderId() == OrderId("unroot")) then
            local u = GetOrderedUnit()
            -- DO SOMETHING WITH YOUR UPROOTED UBUILDING u
        end
        return false
    end))
    TriggerAddCondition(reRoot, Condition(function()
        if (GetIssuedOrderId() == OrderId("root")) then
            local u,  t,  cancel = GetOrderedUnit(),  CreateTimer(),  CreateTrigger()
            TriggerRegisterUnitEvent(cancel, u, EVENT_UNIT_ISSUED_ORDER)
            TriggerRegisterUnitEvent(cancel, u, EVENT_UNIT_ISSUED_POINT_ORDER)
            TriggerRegisterUnitEvent(cancel, u, EVENT_UNIT_ISSUED_TARGET_ORDER)
            TriggerAddCondition(cancel, Condition(function()
            -- We need to destroy the timer and cancel trigger, they will be re created on next "root" order
                PauseTimer(t)
                DestroyTimer(t)
                DisableTrigger(cancel)
                DestroyTrigger(cancel)
            end))
            TimerStart(t, 0.3, true, function()
                if not u then
                    PauseTimer(t)
                    DestroyTimer(t)
                    DisableTrigger(cancel)
                    DestroyTrigger(cancel)
                elseif BlzGetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING) then
                    PauseTimer(t)
                    DestroyTimer(t)
                    DisableTrigger(cancel)
                    DestroyTrigger(cancel)
                   -- DO SOMETHING WITH YOUR RE-ROOTED BUILDING u
                end
            end)
        end
        return false
    end))
end

OldInitBlizzard = InitBlizzard
InitBlizzard = function()
    OldInitBlizzard()
    ManageRoot()
end

vJass VERSION, added by @SwissDerd

Same here, just add the script in your map and replace the related comments with your actions, knowing the building is saved in the local variable u


vJASS:
// Initial idea by Macadamia
// Credits for suggestions that made this possible (alphabetical order) : Boar, Jim-panse, spellweaver, SwissDerd, Templarfreak, Zed
// Code converted to vJass by SwissDerd

library ManageRoot initializer Init

    globals
        private trigger UprootTrig = CreateTrigger()
        private trigger RootTrig = CreateTrigger()
        private hashtable Table = InitHashtable()
    endglobals

    private function OnUproot takes nothing returns nothing
        local unit u
        if (GetIssuedOrderId()== OrderId("unroot")) then
            set u = GetTriggerUnit()
            //
            // Do whatever you want with the uprooted building u
            //
        endif
    endfunction

    private function OnCancel takes nothing returns nothing
        local trigger trig = GetTriggeringTrigger()
        local integer id = GetHandleId(trig)
        local unit u = LoadUnitHandle(Table, id, 0)
        local timer t = LoadTimerHandle(Table, id, 1)
        if (t != null) then
            call FlushChildHashtable(Table, id)
            call FlushChildHashtable(Table, GetHandleId(u))
            call FlushChildHashtable(Table, GetHandleId(t))
            call PauseTimer(t)
            call DestroyTimer(t)
            call DisableTrigger(trig)
            call DestroyTrigger(trig)
        endif
        set trig = null
        set u = null
        set t = null
    endfunction

    private function OnUpdate takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local integer id = GetHandleId(t)
        local unit u = LoadUnitHandle(Table, id, 0)
        local trigger cancelTrig
        if (u == null) then
            set cancelTrig = LoadTriggerHandle(Table, id, 1)
            call FlushChildHashtable(Table, id)
            call FlushChildHashtable(Table, GetHandleId(cancelTrig))
            call FlushChildHashtable(Table, GetHandleId(u))
            call PauseTimer(t)
            call DestroyTimer(t)
            call DisableTrigger(cancelTrig)
            call DestroyTrigger(cancelTrig)
        else if BlzGetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING) then
            set cancelTrig = LoadTriggerHandle(Table, id, 1)
            call FlushChildHashtable(Table, id)
            call FlushChildHashtable(Table, GetHandleId(cancelTrig))
            call FlushChildHashtable(Table, GetHandleId(u))
            call PauseTimer(t)
            call DestroyTimer(t)
            call DisableTrigger(cancelTrig)
            call DestroyTrigger(cancelTrig)
            //
            // Do whatever you want with your rooted building u
            //
        endif
        set t = null
        set u = null
        set cancelTrig = null
    endfunction

    private function OnRoot takes nothing returns nothing
        local unit u
        local timer t
        local trigger cancelTrig
        local integer id
        if (GetIssuedOrderId() == OrderId("root")) then
            set u = GetTriggerUnit()
            set id = GetHandleId(u)
            if (not LoadBoolean(Table, id, 0)) then
                call SaveBoolean(Table, id, 0, true)
                set cancelTrig = CreateTrigger()
                set t = CreateTimer()

                set id = GetHandleId(t)
                call SaveUnitHandle(Table, id, 0, u)
                call SaveTriggerHandle(Table, id, 1, cancelTrig)
                call TimerStart(t, 0.30, true, function OnUpdate)

                set id = GetHandleId(cancelTrig)
                call SaveUnitHandle(Table, id, 0, u)
                call SaveTimerHandle(Table, id, 1, t)
                call TriggerRegisterUnitEvent(cancelTrig, u, EVENT_UNIT_ISSUED_ORDER)
                call TriggerRegisterUnitEvent(cancelTrig, u, EVENT_UNIT_ISSUED_POINT_ORDER)
                call TriggerRegisterUnitEvent(cancelTrig, u, EVENT_UNIT_ISSUED_TARGET_ORDER)
                call TriggerAddCondition(cancelTrig, Condition(function OnCancel))
            endif
        endif
        set u = null
        set t = null
        set cancelTrig = null
    endfunction

    private function Init takes nothing returns nothing
        local integer i = 0
        loop
            if (GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING) then
                call TriggerRegisterPlayerUnitEvent(UprootTrig, Player(i), EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(RootTrig, Player(i), EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
            endif
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
         endloop
         call TriggerAddCondition(UprootTrig, Condition(function OnUproot))
         call TriggerAddCondition(RootTrig, Condition(function OnRoot))
    endfunction

endlibrary

GUI VERSION, added by @crunch

- First you have to create a global unit variable in your Trigger Editor, called ROOT_UNIT.

- Then you need a slightly altered version of the script. Chose the scripting language according to your map Options. Just create a new Custom Script to put it in or add it to the main map script by clicking your map name on top of the triggers in the list.


Lua:
-- Initial idea by Macadamia
-- Credits for suggestions that made this possible (alphabetical order) : Boar, Jim-panse, spellweaver, SwissDerd, Templarfreak, Zed
-- GUI adaptation by Crunch and Macadamia

function ManageRoot()
    local upRoot, reRoot = CreateTrigger(), CreateTrigger()
    for i = 1,  GetPlayers() do
        local p = Player(i-1)
        if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING) then
            TriggerRegisterPlayerUnitEvent(upRoot, p, EVENT_PLAYER_UNIT_ISSUED_ORDER, nil)
            TriggerRegisterPlayerUnitEvent(reRoot, p, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, nil)
        end
    end
    TriggerAddCondition(upRoot, Condition(function()
        if (GetIssuedOrderId() == OrderId("unroot")) then
            local u = GetOrderedUnit()
            -- Execute GUI Uproot Trigger, checking its conditions
            udg_ROOT_UNIT = u
            if TriggerEvaluate(gg_trg_UprootTrigger) then
                TriggerExecute(gg_trg_UprootTrigger)
            end
        end
        return false
    end))
    TriggerAddCondition(reRoot, Condition(function()
        if (GetIssuedOrderId() == OrderId("root")) then
            local u,  t,  cancel = GetOrderedUnit(),  CreateTimer(),  CreateTrigger()
            TriggerRegisterUnitEvent(cancel, u, EVENT_UNIT_ISSUED_ORDER)
            TriggerRegisterUnitEvent(cancel, u, EVENT_UNIT_ISSUED_POINT_ORDER)
            TriggerRegisterUnitEvent(cancel, u, EVENT_UNIT_ISSUED_TARGET_ORDER)
            TriggerAddCondition(cancel, Condition(function()
            -- We need to destroy the timer and cancel trigger, they will be re created on next "root" order
                PauseTimer(t)
                DestroyTimer(t)
                DisableTrigger(cancel)
                DestroyTrigger(cancel)
            end))
            TimerStart(t, 0.3, true, function()
                if not u then
                    PauseTimer(t)
                    DestroyTimer(t)
                    DisableTrigger(cancel)
                    DestroyTrigger(cancel)
                elseif BlzGetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING) then
                    PauseTimer(t)
                    DestroyTimer(t)
                    DisableTrigger(cancel)
                    DestroyTrigger(cancel)
                    -- Execute GUI RootTrigger, checking its conditions
                    udg_ROOT_UNIT = u
                   if TriggerEvaluate(gg_trg_RootTrigger) then
                       TriggerExecute(gg_trg_RootTrigger)
                   end
                end
            end)
        end
        return false
    end))
end

OldInitBlizzard = InitBlizzard
InitBlizzard = function()
    OldInitBlizzard()
    ManageRoot()
end



vJASS:
// Initial idea by Macadamia
// Credits for suggestions that made this possible (alphabetical order) : Boar, Jim-panse, spellweaver, SwissDerd, Templarfreak, Zed
// Code converted to vJass by SwissDerd
// GUI adaptation by Crunch and Macadamia

native UnitAlive takes unit id  returns boolean

library ManageRoot initializer Init

    globals
        private trigger UprootTrig = CreateTrigger()
        private trigger RootTrig = CreateTrigger()
        private hashtable Table = InitHashtable()
    endglobals

    private function OnUproot takes nothing returns nothing
        local unit u
        if (GetIssuedOrderId()== OrderId("unroot")) then
            set u = GetTriggerUnit()
            // Execute GUI Uproot Trigger, checking its conditions
            set udg_ROOT_UNIT = u
            if TriggerEvaluate(gg_trg_UprootTrigger) then
                TriggerExecute(gg_trg_UprootTrigger)
            endif
        endif
    endfunction

    private function OnCancel takes nothing returns nothing
        local trigger trig = GetTriggeringTrigger()
        local integer id = GetHandleId(trig)
        local unit u = LoadUnitHandle(Table, id, 0)
        local timer t = LoadTimerHandle(Table, id, 1)
        if (t != null) then
            call FlushChildHashtable(Table, id)
            call FlushChildHashtable(Table, GetHandleId(u))
            call FlushChildHashtable(Table, GetHandleId(t))
            call PauseTimer(t)
            call DestroyTimer(t)
            call DisableTrigger(trig)
            call DestroyTrigger(trig)
        endif
        set trig = null
        set u = null
        set t = null
    endfunction

    private function OnUpdate takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local integer id = GetHandleId(t)
        local unit u = LoadUnitHandle(Table, id, 0)
        local trigger cancelTrig
        if (u == null) or (GetUnitTypeId(u) == 0) or not(UnitAlive(u)) then
            set cancelTrig = LoadTriggerHandle(Table, id, 1)
            call FlushChildHashtable(Table, id)
            call FlushChildHashtable(Table, GetHandleId(cancelTrig))
            call FlushChildHashtable(Table, GetHandleId(u))
            call PauseTimer(t)
            call DestroyTimer(t)
            call DisableTrigger(cancelTrig)
            call DestroyTrigger(cancelTrig)
        else if BlzGetUnitBooleanField(u, UNIT_BF_IS_A_BUILDING) then
            set cancelTrig = LoadTriggerHandle(Table, id, 1)
            call FlushChildHashtable(Table, id)
            call FlushChildHashtable(Table, GetHandleId(cancelTrig))
            call FlushChildHashtable(Table, GetHandleId(u))
            call PauseTimer(t)
            call DestroyTimer(t)
            call DisableTrigger(cancelTrig)
            call DestroyTrigger(cancelTrig)
            // Execute GUI RootTrigger, checking its conditions
            set udg_ROOT_UNIT = u
            if TriggerEvaluate(gg_trg_RootTrigger) then
                TriggerExecute(gg_trg_RootTrigger)
            endif
        endif
        set t = null
        set u = null
        set cancelTrig = null
    endfunction

    private function OnRoot takes nothing returns nothing
        local unit u
        local timer t
        local trigger cancelTrig
        local integer id
        if (GetIssuedOrderId() == OrderId("root")) then
            set u = GetTriggerUnit()
            set id = GetHandleId(u)
            if (not LoadBoolean(Table, id, 0)) then
                call SaveBoolean(Table, id, 0, true)
                set cancelTrig = CreateTrigger()
                set t = CreateTimer()

                set id = GetHandleId(t)
                call SaveUnitHandle(Table, id, 0, u)
                call SaveTriggerHandle(Table, id, 1, cancelTrig)
                call TimerStart(t, 0.30, true, function OnUpdate)

                set id = GetHandleId(cancelTrig)
                call SaveUnitHandle(Table, id, 0, u)
                call SaveTimerHandle(Table, id, 1, t)
                call TriggerRegisterUnitEvent(cancelTrig, u, EVENT_UNIT_ISSUED_ORDER)
                call TriggerRegisterUnitEvent(cancelTrig, u, EVENT_UNIT_ISSUED_POINT_ORDER)
                call TriggerRegisterUnitEvent(cancelTrig, u, EVENT_UNIT_ISSUED_TARGET_ORDER)
                call TriggerAddCondition(cancelTrig, Condition(function OnCancel))
            endif
        endif
        set u = null
        set t = null
        set cancelTrig = null
    endfunction

    private function Init takes nothing returns nothing
        local integer i = 0
        loop
            if (GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING) then
                call TriggerRegisterPlayerUnitEvent(UprootTrig, Player(i), EVENT_PLAYER_UNIT_ISSUED_ORDER, null)
                call TriggerRegisterPlayerUnitEvent(RootTrig, Player(i), EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, null)
            endif
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
         endloop
         call TriggerAddCondition(UprootTrig, Condition(function OnUproot))
         call TriggerAddCondition(RootTrig, Condition(function OnRoot))
    endfunction

endlibrary


Once done, create 2 triggers :

- one called UprootTrigger,. In there, add Conditions and Actions you want to run when the building is UPROOTED
- a second one called RootTrigger. In there ,add Conditions and Actions you want to run when the building is ROOTED

Important : remember to use ROOT_UNIT instead of Triggering Unit in your conditions and actions in these 2 triggers.

Thats all.

I have attached an example map made by @crunch to demonstrate this with the map scripting language set to Lua (but the vJass version works identically).

TypeScript VERSION, added by @Boar

- Posted by himself in the first comment.
 

Attachments

  • Root_and_Uproot_GUI.w3x
    17.3 KB · Views: 64
Last edited:
Level 12
Joined
Oct 10, 2009
Messages
438
Here's the Typescript version
TypeScript:
// Using TriggerHappy's w3ts lib as it makes everything easier
import { Unit, Trigger, Timer } from "w3ts/index";


const ROOT_OBSERVE_TICK = 0.3;
export class RootTracker {

    private rootTrigger: Trigger;
    private unrootTrigger: Trigger;

    private onRoot: (whichUnit: Unit) => any;
    private onUproot: (whichUnit: Unit) => any;

    constructor() {
        this.rootTrigger = new Trigger();
        this.rootTrigger.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER);
        this.rootTrigger.addCondition(Condition(() => GetIssuedOrderId() === OrderId("unroot")));
        this.rootTrigger.addAction(() => this.onRoot && this.onRoot(Unit.fromHandle(GetTriggerUnit())));

        this.unrootTrigger = new Trigger();
        this.unrootTrigger.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER);
        this.unrootTrigger.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER);
        this.unrootTrigger.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER);
        this.unrootTrigger.addCondition(Condition(() => GetIssuedOrderId() === OrderId("root")));
        this.unrootTrigger.addAction(() => {
            // A timer used to observe building status
            // Is used below.
            const isBuildingChecker = new Timer();

            // Observe incoming orders and ensure the root action isn't interrupted
            const interruptObserver = new Trigger();
            interruptObserver.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER);
            interruptObserver.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER);
            interruptObserver.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER);
            interruptObserver.addAction(() => {
                isBuildingChecker.pause();
                isBuildingChecker.destroy();
                interruptObserver.destroy();
            });

            // Use isBuildingChecker to observe state
            const triggerUnit =  Unit.fromHandle(GetTriggerUnit());
            isBuildingChecker.start(ROOT_OBSERVE_TICK, true, () => {
                if (!triggerUnit.isAlive()) {
                    isBuildingChecker.pause();
                    isBuildingChecker.destroy();
                    interruptObserver.destroy();
                }
                else if (triggerUnit.getField(UNIT_BF_IS_A_BUILDING)) {
                    isBuildingChecker.pause();
                    isBuildingChecker.destroy();
                    interruptObserver.destroy();
                    // We are valid, now call our callback
                    if (this.onUproot) this.onUproot(triggerUnit);
                }
            })
        });
    }


    /**
     * Used similar as an "Action" of a trigger, just call RootTracker.setOnRoot(yourActionFunctionHere)
     * @param toCallback
     */
    public setOnRoot(toCallback: (whichUnit: Unit) => any) {
        this.onRoot = toCallback;
    }

    /**
     * Used similar as an "Action" of a trigger, just call RootTracker.setOnUproot(yourActionFunctionHere)
     * @param toCallback
     */
    public setOnUproot(toCallback: (whichUnit: Unit) => any) {
        this.onUproot = toCallback;
    }

    /**
     * Removes the listener
     * Call this to avoid memory leaks if no longer needed
     */
    destroy() {
        this.rootTrigger.destroy();
        this.unrootTrigger.destroy();
    }
}

And this is how you use it:
TypeScript:
// How to use
function myRootTrackerTest() {
    // Create a new tracker
    const tracker = new RootTracker();

    tracker.setOnRoot((rootingUnit) => {
        // Place your actions here
        // Heal unit by 20 hp
        rootingUnit.life += 20;
        // Make unit larger
        rootingUnit.setScale(200, 200, 200);
        // Change unit name
        rootingUnit.name = "I am Root.";
    });

    tracker.setOnUproot((uprootingUnit) => {
        // Place your actions here
        // Damage unit by 20 hp
        uprootingUnit.life -= 20;
        // Make unit smaller
        uprootingUnit.setScale(100, 100, 100);
        // Change unit name
        uprootingUnit.name = "I am Unroot.";
    ]})
}
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
If I am not mistaken, the Root ability has an stop-cast event that gets deployed once the rooting finishes. I would recommend trying both Stops casting as well as Finishes casting in case there are different variations on it.

I was writing with Spellbound about it a few years back:

GUI Unit Event v2.5.2.0
 
Level 12
Joined
Jan 30, 2020
Messages
875
Oh thats really instructive.

I had never thought of checking any stop cast event for root, as uproot didn't even have a start the effect of an ability event.

Will try to test this soon and this if this could help improve this script.


EDIT :
Oh well no luck.
I tried with a basic test map that has a Tree Of Eternity, then added 2GUI triggers :
  • BeginCast
    • Events
      • Unit - A unit Begins casting an ability
    • Conditions
    • Actions
      • Game - Display to (All players) the text: (Name of (Ability being cast))
  • StopCast
    • Events
      • Unit - A unit Stops casting an ability
    • Conditions
    • Actions
      • Game - Display to (All players) the text: (Name of (Ability being cast))
And nothing displayed weather I rooted or uprooted the Tree Of Eternity.

Could this be a change introduced with Reforged ?
I didn't have the chance to test Warcraft after 1.24 and before Reforged, so I supposed there is quite a lot I have missed.

EDIT 2 :
Out of doubt, I also tried with an ancient protector as it's a different Root version, and with
Unit - A unit Finishes casting an ability, but still no reaction, while all other abilities worked as expected.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
That is quite odd indeed. Alright well thanks for trying.

I know they aren't firing off any kind of Undefend ability when they root or uproot, but I wonder if it is because of its interaction with structures in general. Perhaps there is a way to catch it if I add a Defend ability to the uprooted tree and maybe it will deploy an Undefend order once it gets rooted again (this trick works to detect when a unit transforms into a different unit type at least).

I will run some tests.
 
Level 12
Joined
Jan 30, 2020
Messages
875
Well if it works, that would be game changer.

I mean these events we came up with are just a compromise between a real solution we didn't have and a repeating timer... that is usually not proper code in an event driven scripting language.

Any easily implementable alternative that would allow to use a real event would be godsend !
 
Status
Not open for further replies.
Top