• 🏆 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!

CustomWaygate v1.04

full




Uses SmartTrack






JASS:
library CustomWaygate/*

CustomWaygate v1.04
By Spellbound


/*/*==== DESCRIPTION ====*/*/

Custom Waygate allows two units to be connected via one-way 'tunnels'. A unit can have
multiple tunnels, which creates a network that allows the player to chose, through
right-clicking, the destination portal.


/*/*==== INSTALLATION ====*/*/

• Copy-paste this trigger into your map.
• Copy-paste the required libraries into your map.
• The system is installed! Copy-paste the onCustomWaygate Event trigger or make your own
  event trigger and start coding.
 
*/ requires /*
  
    */ SmartTrack,          /* https://www.hiveworkshop.com/threads/smarttrack-v1-02-1.299957/
    */ TimerUtils,          /* http://www.wc3c.net/showthread.php?t=101322
    */ Table                /* https://www.hiveworkshop.com/threads/snippet-new-table.188084/
  
*/ optional /*

    */ BlizzardMessage      /* https://www.hiveworkshop.com/threads/snippet-blizzardmessage.237845/
      
      
/*/*==== API ====*/*/


/*SETUP________*/

CustomWaygate.createTunnel( takes unit source, unit target, real teleport_distance, real timeout )
    Sets up a tunnel that will teleport unit from the source to the target. The teleport_dsitance
    determines how close to the source the teleporting units have to be to use it. The timeout
    determines if there is a channeling time before teleportation itself occurs.
  
    To create a two-way teleporter, simply repeat this function but set the target as the source
    and the source as the target.
  
    A waygate become a network the moment it has more than one tunnel.
  
CustomWaygate.destroyTunnel( takes unit source, unit target )
    Destroys a tunnel. This will only destroy the tunnel going from the source to the target.
    If the target has a tunnel going to the source, it will be unaffected.
  
  
/*EVENTS________*/

RegisterNativeEvent( integer whichEvent, code whichFunction )
    This uses RegisterNativeEvent's event-creation to register CustomWaygate's many events. They
    are as followed:

    EVENT_WAYGATE_PRE_TELEPORT ---- Fires right before a unit is moved.
    EVENT_WAYGATE_POST_TELEPORT --- Fires directly after moving a unit.
    EVENT_WAYGATE_DURATION_START -- When teleportation has a duration, this fires as that duration
                                    starts. It ends EVENT_WAYGATE_PRE_TELEPORT
    EVENT_WAYGATE_NETWORK_PRIMED -- When teleportation is primed and ready to execute in a network.
                                    Useful if you want to attach special effects to your units to
                                    indicate readiness, for example.
    EVENT_WAYGATE_CANCELLED ------- When teleportation has been cancelled, either manually or
                                    through StopTeleport(). See below.
                              
RegisterIndexNativeEvent( integer index, integer whichEvent, code whichFunction )
    Same as above, but per-player or per-unit. If you wish for CustomWaygate to only work for
    specific players, set index to the player id. For units, set index to the handle of the unit.

GetWaygateSource()
    The waygate unit itself. Origin waygate.
  
GetWaygateTarget()
    The waygate your units teleport to. Destination waygate.
  
GetWaygateTraveller()
    The units that are using the waygate.
  
CountEntrances( unit whichGate )
    Returns an integer of how many gates have whichGate as a destination.
  
CountExits( unit whichGate )
    Returns an integer of how many destination gates whichGate has.
  
StopTeleport()
    Call this when WAYGATE_EVENT == 0.50 (pre-teleportaton) if you wish to interrupt the
    process and handle teleportation yourself. Such situations are, eg, if you want to have
    your teleporter shoot your traveller as a missile towards the destination instead of just
    instantly displacing them. It's also useful if your teleporter has 'charges' so that you can
    prevent teleportation altogether if the charges are insufficient.

*/

globals

    private constant real NETWORK_TIMEOUT = .03125
    private constant real DESTINATION_MESSAGE_COOLDOWN = 5.     // see PreventMessageSpam below
    private constant string MSG_INVALID = "Invalid target."     // the message that shows up if you click on a unit that's not part of the network your unit is currently in.
    private constant string MSG_PICK_DESTINATION = "Right-click on your destination to teleport."
  
    private integer array NumberOfEntrances
    private integer array NumberOfExits
    private integer array EntrancesNetworkInstance
    private integer array ExitsNetworkInstance
    private integer array TeleportTimeInstance
    private integer array TravellerInstance
    private integer array NumberOfInstances
    private boolean array isPlayMessageOnCooldown
  
    //Events
    integer EVENT_WAYGATE_PRE_TELEPORT
    integer EVENT_WAYGATE_POST_TELEPORT
    integer EVENT_WAYGATE_DURATION_START
    integer EVENT_WAYGATE_NETWORK_PRIMED
    integer EVENT_WAYGATE_CANCELLED
  
    private unit waygateSource = null
    private unit waygateTarget = null
    private unit waygateTraveller = null
    private boolean interruptTeleport = false
endglobals

// G stands for Gate
private struct G
    static Table EXITS
    static Table ENTRANCES
    static Table TIMEOUT
endstruct

// Getters and interruption
function GetWaygateSource takes nothing returns unit
    return waygateSource
endfunction

function GetWaygateTarget takes nothing returns unit
    return waygateTarget
endfunction

function GetWaygateTraveller takes nothing returns unit
    return waygateTraveller
endfunction

function StopTeleport takes nothing returns nothing
    set interruptTeleport = true
endfunction

function CountEntrances takes unit u returns integer
    return NumberOfEntrances[GetUnitUserData(u)]
endfunction

function CountExits takes unit u returns integer
    return NumberOfExits[GetUnitUserData(u)]
endfunction

// This is the function that controls what trigger is run.
private function FireEvent takes unit source, unit target, unit traveller, integer ev returns nothing
    local integer playerId = GetPlayerId(GetOwningPlayer(traveller))
    local integer id = GetHandleId(traveller)
    local unit prevSource = waygateSource
    local unit prevTarget = waygateTarget
    local unit prevTraveller = waygateTraveller
  
    set waygateSource = source
    set waygateTarget = target
    set waygateTraveller = traveller
    call TriggerEvaluate(GetNativeEventTrigger(ev))
    if IsNativeEventRegistered(playerId, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, ev))
    endif
    if IsNativeEventRegistered(id, ev) then
        call TriggerEvaluate(GetIndexNativeEventTrigger(id, ev))
    endif
    set waygateSource = prevSource
    set waygateTarget = prevTarget
    set waygateTraveller = prevTraveller
  
    set prevSource = null
    set prevTarget = null
    set prevTraveller = null
endfunction

// A little quality of life struct that prevents an error message from showing up for every single
// invalid teleportation. Instead, it shows up once and then not for another DESTINATION_MESSAGE_COOLDOWN seconds.
private struct PreventMessageSpam
  
    integer play_num
  
    private method destroy takes nothing returns nothing
        call this.deallocate()
    endmethod
  
    private static method Timer takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local thistype this = GetTimerData(t)
        set isPlayMessageOnCooldown[this.play_num] = false
        call ReleaseTimer(t)
        call this.destroy()
        set t = null
    endmethod
  
    static method start takes integer player_number returns nothing
        local thistype this = allocate()
        set this.play_num = player_number
        set isPlayMessageOnCooldown[player_number] = true
        call TimerStart(NewTimerEx(this), DESTINATION_MESSAGE_COOLDOWN, false, function thistype.Timer)
    endmethod
  
endstruct

struct orderStunTimer
  
    unit u
    boolean b
  
    // ST_IGNORE_ORDERS is specific to SmartTrack that prevents orders given by this library to
    // interfere with it.
    private static method zeroTimerStun takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local thistype this = GetTimerData(t)
        local CustomWaygate ti = TravellerInstance[GetUnitUserData(this.u)]
        set ti.ignoreOrder = true
        set SmartTrack_ignoreOrders = this.b
        call IssueImmediateOrderById(this.u, 851973)//order stunned
        set SmartTrack_ignoreOrders = false
        set ti.ignoreOrder = false
        set this.u = null
        set this.b = false
        call ReleaseTimer(t)
        call this.deallocate()
        set t = null
    endmethod
  
    static method start takes unit u, boolean b returns nothing
        local thistype this = allocate()
        set this.u = u
        set this.b = b
        call TimerStart(NewTimerEx(this), 0., false, function thistype.zeroTimerStun)
    endmethod
  
endstruct


struct CustomWaygate
  
    unit source
    unit target
    unit traveller
    real countdown
    trigger trig
    trigger networkTrig
    boolean isTeleportCancelled
    boolean ignoreOrder
    boolean teleportReady
    integer timeoutSlot
    integer tunnelInstance
    timer clock
  
  
    // destruction method for teleport instances, not tunnels.
    private method destroy takes nothing returns nothing
        set this.source = null
        set this.target = null
        set this.traveller = null
        if this.trig != null then
            call DestroyTrigger(this.trig)
            set this.trig = null
        endif
        if this.clock != null then
            call ReleaseTimer(this.clock)
            set this.clock = null
        endif
        set this.networkTrig = null
        call this.deallocate()
    endmethod
  
  
    // core teleporter method. Relocates units to the destination by keeping their relative x/y
    // position to the source.
    private static method simpleTeleport takes unit source, unit target, unit traveller returns nothing
        local real      xSource
        local real      ySource
        local real      xTraveller
        local real      yTraveller
        local real      xDestination
        local real      yDestination
        local real      angle
        local real      distance
        local thistype  this = allocate()
      
        set xTraveller = GetUnitX(traveller)
        set yTraveller = GetUnitY(traveller)
        set xSource = GetUnitX(source)
        set ySource = GetUnitY(source)
        set angle = Atan2(yTraveller - ySource, xTraveller - xSource)
        set distance = SquareRoot( (xTraveller - xSource)*(xTraveller - xSource) + (yTraveller - ySource)*(yTraveller - ySource) )
        set xDestination = GetUnitX(target) + Cos(angle) * distance
        set yDestination = GetUnitY(target) + Sin(angle) * distance
  
        call SetUnitX(traveller, xDestination)
        call SetUnitY(traveller, yDestination)
      
        call orderStunTimer.start(traveller, true)
    endmethod
  
  
    // this function handles triggers created for each teleport instance that catches orders
    // given. This is exclusively for non-network teleportations.
    private static method cancelTeleport takes nothing returns nothing
        local unit traveller = GetTriggerUnit()
        local unit source = GetOrderTargetUnit()
        local integer id_T = GetUnitUserData(traveller)
        local thistype this = TravellerInstance[id_T]
      
        // if there is a source mismatch (aka you've right-clicked on another teleporter while
        // already in a teleport instance) or the order was not a right-click, cancel teleportation.
        if not (source == this.source and GetIssuedOrderId() == ORDER_SMART) then
          
            call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_CANCELLED)
          
            if this.clock == null then
                call this.destroy()
            else
                set this.isTeleportCancelled = true
                //This instance is bound to isTeleportCancelled, which doesn't repeat and will
                //terminate on its own when it expires.
            endif
      
        endif
      
        set source = null
        set traveller = null
    endmethod
  
  
    // this function handles the timer for non-network teleportations.
    private static method teleportTimer takes nothing returns nothing
        local timer     t = GetExpiredTimer()
        local thistype  this = GetTimerData(t)
        local integer   id_T = GetUnitUserData(this.traveller)
      
        call DestroyTrigger(this.trig)
        set this.trig = null
      
        set G.EXITS = this.tunnelInstance
        if not this.isTeleportCancelled and UnitAlive(this.source) and UnitAlive(this.traveller) and G.EXITS.unit[1] != null and TravellerInstance[id_T] == this then
          
            //Events
            call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_PRE_TELEPORT)
          
            if not interruptTeleport then
                call CustomWaygate.simpleTeleport(this.source, this.target, this.traveller)
                call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_POST_TELEPORT)
            else
                call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_CANCELLED)
            endif
            set interruptTeleport = false
          
        endif
      
        set NumberOfInstances[id_T] = NumberOfInstances[id_T] - 1
        if NumberOfInstances[id_T] == 0 then
            set TravellerInstance[id_T] = 0
        endif
        call this.destroy()
        set t = null
    endmethod
  
  
    // if the teleporter has a timeout, create a trigger to track orders given and fire an initiate teleport event.
    static method initiateTeleportation takes unit source, unit target, unit traveller, real timeout, integer tunnel_id returns nothing
      
        local integer id_T
        local thistype this
      
        if timeout > 0. then
          
            set this = allocate()
            set id_T = GetUnitUserData(traveller)
            set NumberOfInstances[id_T] = NumberOfInstances[id_T] + 1
            set TravellerInstance[id_T] = this
            set this.source = source
            set this.target = target
            set this.traveller = traveller
            set this.isTeleportCancelled = false
            set this.tunnelInstance = tunnel_id
            set this.trig = CreateTrigger()
            call TriggerRegisterUnitEvent(this.trig, traveller, EVENT_UNIT_ISSUED_TARGET_ORDER)
            call TriggerRegisterUnitEvent(this.trig, traveller, EVENT_UNIT_ISSUED_POINT_ORDER)
            call TriggerRegisterUnitEvent(this.trig, traveller, EVENT_UNIT_ISSUED_ORDER)
            call TriggerAddCondition(this.trig, function thistype.cancelTeleport)
            set this.clock = NewTimerEx(this)
            call TimerStart(this.clock, timeout, false, function thistype.teleportTimer)
          
            //Events
            call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_DURATION_START)
          
        else
          
            //Events
            call FireEvent(source, target, traveller, EVENT_WAYGATE_PRE_TELEPORT)
          
            if not interruptTeleport then
                call CustomWaygate.simpleTeleport(source, target, traveller)
                call FireEvent(source, target, traveller, EVENT_WAYGATE_POST_TELEPORT)
            else
                call FireEvent(source, target, traveller, EVENT_WAYGATE_CANCELLED)
            endif
            set interruptTeleport = false
          
        endif
    endmethod
  
  
    //NETWORK TELEPORT
    // this function allows units to right-click on possible destinations in a network without
    // terminating their teleport instance. It catches orders exclusively for network teleportations.
    static method networkTeleport takes nothing returns nothing
        local unit      traveller   = GetTriggerUnit()
        local unit      target      = GetOrderTargetUnit()
        local thistype  this        = TravellerInstance[GetUnitUserData(traveller)]
        local integer   id_S        = GetUnitUserData(this.source)
        local integer   i           = 0
        local boolean   isUnitValid = false
        local player play
      
        // this.ignoredOrder is used in orderStunTimer.start() to prevent the stun order from
        // affecting the unit and thus accidentally interrupting the teleportation.
        if not this.ignoreOrder then
            if target != null and GetIssuedOrderId() == ORDER_SMART then
                if target != this.source then
                    set G.EXITS = ExitsNetworkInstance[id_S]
                    loop
                        set i = i + 1
                        if target == G.EXITS.unit[i] then
                            set isUnitValid = true
                        endif
                        exitwhen i > NumberOfExits[id_S] or isUnitValid
                    endloop
                  
                    // if the destination unit is viable, the right-clicker is stunned to prevent
                    // it from moving.
                    if isUnitValid then
                        set this.target = target
                        set this.timeoutSlot = i
                        call orderStunTimer.start(traveller, false)
                    else
                        if SmartTrack_IsATracker[GetUnitUserData(target)] then
                            set this.isTeleportCancelled = true
                        else
                            static if LIBRARY_BlizzardMessage then
                                call BlizzardMessage(MSG_INVALID, "|cffffcc00", 31, GetOwningPlayer(traveller))
                            else
                                set play = GetOwningPlayer(traveller)
                                if play == GetLocalPlayer() then
                                    call DisplayTimedTextToPlayer(play, 0, 0, 5., MSG_INVALID)
                                endif
                            endif
                            call orderStunTimer.start(traveller, false)
                        endif
                    endif
                endif
            else
                set this.isTeleportCancelled = true
            endif
        endif
        set traveller = null
        set target = null
    endmethod
  
  
    // this function handles the countdown for network-type teleporter. Right-click orders are
    private static method networkTeleportCountdown takes nothing returns nothing
        local timer     t       = GetExpiredTimer()
        local thistype  this    = GetTimerData(t)
        local integer   id_T    = GetUnitUserData(this.traveller)
      
        // this.teleportReady will turn to true after the countdown has elapsed. When it turns
        // true, a network-teleport-is-event will fire.
        if not this.teleportReady then
            set this.countdown = this.countdown - NETWORK_TIMEOUT
            if this.countdown <= 0. then
                set this.teleportReady = true
              
                //Events
                call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_NETWORK_PRIMED)
            endif
        endif
      
        // if the teleportation has been cancelled, terminate the teleportation isntance.
        if this.isTeleportCancelled then
      
            set NumberOfInstances[id_T] = NumberOfInstances[id_T] - 1
            if NumberOfInstances[id_T] == 0 then
                set TravellerInstance[id_T] = 0
            endif
            call DestroyTrigger(this.networkTrig)
            set this.networkTrig = null
          
            //Events
            call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_CANCELLED)
              
            call this.destroy()
      
        // if there is a teleport target (aka a destination) and this.teleportReady is true, call
        // pre-teleportation, terminate the teleport instance and if no interruption is called,
        // proceed with the teleportation.
        elseif this.target != null and this.teleportReady then
              
            //Events
            call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_PRE_TELEPORT)
          
            set NumberOfInstances[id_T] = NumberOfInstances[id_T] - 1
            if NumberOfInstances[id_T] == 0 then
                set TravellerInstance[id_T] = 0
            endif
            call DestroyTrigger(this.networkTrig)
            set this.networkTrig = null
          
            if not interruptTeleport then
                call CustomWaygate.simpleTeleport(this.source, this.target, this.traveller)
                call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_POST_TELEPORT)
            else
                call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_CANCELLED)
            endif
            set interruptTeleport = false
          
            call this.destroy()
          
        endif
        set t = null
    endmethod
  
  
    // This is the first method that is called when a right-clicker comes in range of a teleporter.
    static method preTeleportation takes nothing returns nothing
        local unit      source = GetTracker()
        local unit      traveller = GetSmartUnit()
        local integer   id_S = GetUnitUserData(source) //The Waygate
        local integer   id_T = GetUnitUserData(traveller)
        local integer   player_number
        local integer   i
        local player    play
        local thistype  this = TravellerInstance[id_T]
      
        if this.source != source then
          
            if this != 0 then
                set this.isTeleportCancelled = true
            endif
          
            set G.EXITS = ExitsNetworkInstance[id_S]
            set G.TIMEOUT = TeleportTimeInstance[id_S]
          
            // if there's only one exit connected to this source, then it's not a network. Initiate teleportation directly.
            if NumberOfExits[id_S] == 1 then
                call initiateTeleportation(source, G.EXITS.unit[1], traveller, G.TIMEOUT.real[1], G.EXITS)
              
            // if ther are multiple exits, the unit has to then chose the destination with another right-click. Queued orders work well for that.
            elseif NumberOfExits[id_S] > 1 then
                set this = allocate()
                set NumberOfInstances[id_T] = NumberOfInstances[id_T] + 1
                set TravellerInstance[id_T] = this
                set this.source = source
                set this.target = null
                set this.traveller = traveller
                set this.isTeleportCancelled = false
                set this.teleportReady = false
                set this.ignoreOrder = false
                set this.countdown = G.TIMEOUT.real[1]
                set this.networkTrig = CreateTrigger()
                call TriggerRegisterUnitEvent(this.networkTrig, traveller, EVENT_UNIT_ISSUED_TARGET_ORDER)
                call TriggerRegisterUnitEvent(this.networkTrig, traveller, EVENT_UNIT_ISSUED_POINT_ORDER)
                call TriggerRegisterUnitEvent(this.networkTrig, traveller, EVENT_UNIT_ISSUED_ORDER)
                call TriggerAddCondition(this.networkTrig, function thistype.networkTeleport)
                set this.clock = NewTimerEx(this)
                call TimerStart(this.clock, NETWORK_TIMEOUT, true, function thistype.networkTeleportCountdown)
              
                set play = GetOwningPlayer(traveller)
                set player_number = GetPlayerId(play)
                if not isPlayMessageOnCooldown[player_number] then
                    static if LIBRARY_BlizzardMessage then
                        call BlizzardMessage(MSG_PICK_DESTINATION, "|cffffcc00", 132, play)
                    else
                        if play == GetLocalPlayer() then
                            call DisplayTimedTextToPlayer(play, 0, 0, 5., MSG_PICK_DESTINATION)
                        endif
                    endif
                    call PreventMessageSpam.start(player_number)
                endif
              
                //Events
                call FireEvent(this.source, this.target, this.traveller, EVENT_WAYGATE_DURATION_START)
              
            endif
          
        endif
      
        set source = null
        set traveller = null
    endmethod
  
  
    // this method destroys a SINGLE tunnel going from unit A to unit B. If there's another
    // tunnel that goes from B to A, this tunnel is unaffected.
    static method destroyTunnel takes unit source, unit target returns nothing
        local integer id_S = GetUnitUserData(source)
        local integer id_T = GetUnitUserData(target)
        local integer i = 0
        local boolean isUnitValid = false
      
        //Source
        if ExitsNetworkInstance[id_S] != 0 then
            set G.EXITS = ExitsNetworkInstance[id_S]
            set G.TIMEOUT = TeleportTimeInstance[id_S]
            loop // loop through all of the source's exits and return true on isUnitValid if there's a match.
                set i = i + 1
                if target == G.EXITS.unit[i] then
                    set isUnitValid = true
                endif
                exitwhen i > NumberOfExits[id_S] or isUnitValid
            endloop
            if isUnitValid then // if isUnitValid, clear all associated variables and cascade all elements of the list down by 1.
                call G.EXITS.remove(i)
                call G.TIMEOUT.remove(i)
                loop
                    exitwhen i > NumberOfExits[id_S]
                    set G.EXITS.unit[i] = G.EXITS.unit[i + 1]
                    set G.TIMEOUT.real[i] = G.TIMEOUT.real[i + 1]
                    set i = i + 1
                endloop
                call G.EXITS.remove(NumberOfExits[id_S])
                call G.TIMEOUT.remove(NumberOfExits[id_S])
                set NumberOfExits[id_S] = NumberOfExits[id_S] - 1
            endif
          
            // if the source no longer has any exits, the Tables are destroyed.
            if NumberOfExits[id_S] < 1 then
                call G.EXITS.destroy()
                call G.TIMEOUT.destroy()
                set ExitsNetworkInstance[id_S] = 0
                set TeleportTimeInstance[id_S] = 0
            endif
          
            set i = 0
            set isUnitValid = false
        endif
      
        //Target
        if EntrancesNetworkInstance[id_T] != 0 then
            set G.ENTRANCES = EntrancesNetworkInstance[id_T]
            loop // just like above loop checks if the source matches any registered entrances.
                set i = i + 1
                if source == G.ENTRANCES.unit[i] then
                    set isUnitValid = true
                endif
                exitwhen i > NumberOfEntrances[id_T] or isUnitValid
            endloop
            if isUnitValid then
                call G.ENTRANCES.remove(i)
                loop
                    exitwhen i > NumberOfEntrances[id_T]
                    set G.ENTRANCES.unit[i] = G.ENTRANCES.unit[i + 1]
                    set i = i + 1
                endloop
                call G.ENTRANCES.remove(NumberOfEntrances[id_T])
                set NumberOfEntrances[id_T] = NumberOfEntrances[id_T] - 1
            endif
          
            if NumberOfEntrances[id_T] < 1 then
                call G.ENTRANCES.destroy()
                set EntrancesNetworkInstance[id_T] = 0
            endif
        endif
      
    endmethod
  
  
    // This method will establish a ONE-WAY connection between two units called a tunnel. The
    // tunnel is not automatically destroyed on death so it's up to the user to determine how
    // they are removed.
    static method createTunnel takes unit source, unit target, real teleport_distance, real timeout returns nothing
        local integer id_S = GetUnitUserData(source)
        local integer id_T = GetUnitUserData(target)
        local integer i
      
        // if either the source unit or the destination unit are null, this trigger does nothing.
        if source != null and target != null then
      
            //Source
            set G.EXITS = ExitsNetworkInstance[id_S]
            if G.EXITS == 0 then // this identifies whether the source has any prior connections
                set G.EXITS = Table.create()
                set G.TIMEOUT = Table.create()
                set ExitsNetworkInstance[id_S] = G.EXITS
                set TeleportTimeInstance[id_S] = G.TIMEOUT
                call CreateSmartOrderTracker(source, teleport_distance)
            else
                // if the source already has at least 1 tunnel, check if the destination unit
                // is already one of its registered targets.
                set i = 1
                loop
                    // if destination is already registered, end the function.
                    if target == G.EXITS.unit[i] then
                        return
                    endif
                    exitwhen i > NumberOfExits[id_S]
                    set i = i + 1
                endloop
            endif
          
            set NumberOfExits[id_S] = NumberOfExits[id_S] + 1
            set G.EXITS.unit[NumberOfExits[id_S]] = target
            set G.TIMEOUT.real[NumberOfExits[id_S]] = timeout
          
            //Target
            set NumberOfEntrances[id_T] = NumberOfEntrances[id_T] + 1
            set G.ENTRANCES = EntrancesNetworkInstance[id_T]
            if G.ENTRANCES == 0 then
                set G.ENTRANCES = Table.create()
                set EntrancesNetworkInstance[id_T] = G.ENTRANCES
            endif
            set G.ENTRANCES.unit[NumberOfEntrances[id_T]] = source
          
        endif
      
    endmethod
  
endstruct


// here a SmartTrack event is registered to detect units that come in range of a teleporter.
// Whenever SmartTrack fires a ST_UNIT_IN_RANGE, CustomWaygate.preTeleportation will run.
private module CustomWaygateInit
    private static method onInit takes nothing returns nothing
  
        set EVENT_WAYGATE_PRE_TELEPORT      = CreateNativeEvent()
        set EVENT_WAYGATE_POST_TELEPORT     = CreateNativeEvent()
        set EVENT_WAYGATE_DURATION_START    = CreateNativeEvent()
        set EVENT_WAYGATE_NETWORK_PRIMED    = CreateNativeEvent()
        set EVENT_WAYGATE_CANCELLED         = CreateNativeEvent()
      
        call RegisterNativeEvent(EVENT_SMART_TRACK_IN_RANGE, function CustomWaygate.preTeleportation)
      
    endmethod
endmodule

private struct init
    implement CustomWaygateInit
endstruct

endlibrary
[LEFT]

JASS:
[/LEFT]
scope onCustomWaygateEvents initializer init
  

    globals
        private effect array TeleportFX
        private effect array TeleportReadyFX
    endglobals
  
  
    private function onTrackStart takes nothing returns nothing
      
        local unit smarty = GetSmartUnit()
      
        /*
            The following are examples of how to filter out units.
        */
      
        // Player restriction
        if GetUnitTypeId(GetTracker()) == 'emow' and GetUnitTypeId(smarty) != 'ewsp' then
            call SmartTrack.stop(smarty, true)// if true, the units will stop in their tracks
            call BJDebugMsg("Only Wisps may teleport through Moon Wells.")
        endif
      
        set smarty = null
      
    endfunction
  
  
    // Pre-Teleportation
    private function onPreTeleport takes nothing returns nothing
      
        local unit source = GetWaygateSource()
        local unit traveller = GetWaygateTraveller()
        local real xTraveller = GetUnitX(traveller)
        local real yTraveller = GetUnitY(traveller)
        local integer id_traveller = GetUnitUserData(traveller)
      
        // call StopTeleport() if you wish to cancel the internal teleportation
        // And do you're own here.
      
        if TeleportReadyFX[id_traveller] != null then
            call DestroyEffect(TeleportReadyFX[id_traveller])
            set TeleportReadyFX[id_traveller] = null
        endif
      
        // Arcane Vault (Moon Bowl)
        if GetUnitTypeId(source) == 'hvlt' then
            if NumberOfRunes[GetUnitUserData(source)] > 0 then
                call ManaRunes.consumeRune(source)
                call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Human\\MassTeleport\\MassTeleportTarget.mdl", xTraveller, yTraveller))
            else
                call StopTeleport()
                call BlizzardMessage("Insufficient Mana Runes", "|cffffcc00", 31, GetOwningPlayer(traveller))
            endif
          
        // Arcane Tower
        elseif GetUnitTypeId(source) == 'hatw' then
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\NightElf\\Blink\\BlinkTarget.mdl", xTraveller, yTraveller))
          
        // Moon Well
        elseif GetUnitTypeId(source) == 'emow' then
            call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\NightElf\\EntBirthTarget\\EntBirthTarget.mdl", xTraveller, yTraveller))
          
        // Orc Burrow
        elseif GetUnitTypeId(source) == 'otrb' then
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Other\\StrongDrink\\BrewmasterMissile.mdl", xTraveller, yTraveller))
            call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", xTraveller, yTraveller))
        endif
      
        set source = null
        set traveller = null
  
    endfunction
  
  
    // Post-teleportation
    private function onPostTeleport takes nothing returns nothing
      
        local unit source = GetWaygateSource()
        local unit traveller = GetWaygateTraveller()
        local real xTraveller = GetUnitX(traveller)
        local real yTraveller = GetUnitY(traveller)
        local integer id_traveller = GetUnitUserData(traveller)
      
        if TeleportFX[id_traveller] != null then
            call DestroyEffect(TeleportFX[id_traveller])
            set TeleportFX[id_traveller] = null
        endif
      
        if TeleportReadyFX[id_traveller] != null then
            call DestroyEffect(TeleportReadyFX[id_traveller])
            set TeleportReadyFX[id_traveller] = null
        endif
      
        // Arcane Vault (Moon Bowl)
        if GetUnitTypeId(source) == 'hvlt' then
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Human\\MassTeleport\\MassTeleportTarget.mdl", xTraveller, yTraveller))
          
        // Arcane Tower
        elseif GetUnitTypeId(source) == 'hatw' then
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\NightElf\\Blink\\BlinkCaster.mdl", xTraveller, yTraveller))
          
        // Moon Well
        elseif GetUnitTypeId(source) == 'emow' then
            call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\NightElf\\EntBirthTarget\\EntBirthTarget.mdl", xTraveller, yTraveller))
          
        // Orb Burrow
        elseif GetUnitTypeId(source) == 'otrb' then
            call DestroyEffect(AddSpecialEffect("Abilities\\Spells\\Other\\StrongDrink\\BrewmasterMissile.mdl", xTraveller, yTraveller))
            call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", xTraveller, yTraveller))
        endif
      
        set source = null
        set traveller = null
      
    endfunction
      
      
    // Teleport Duration Start
    private function onTeleportDuration takes nothing returns nothing
      
        // There is no Target in this event for networks.
      
        local unit source = GetWaygateSource()
        local unit traveller = GetWaygateTraveller()
        local integer id_traveller = GetUnitUserData(traveller)
      
        if TeleportFX[id_traveller] != null then
            call DestroyEffect(TeleportFX[id_traveller])
            set TeleportFX[id_traveller] = null
        endif
      
        if TeleportReadyFX[id_traveller] != null then
            call DestroyEffect(TeleportReadyFX[id_traveller])
            set TeleportReadyFX[id_traveller] = null
        endif
      
        // Arcane Tower
        if GetUnitTypeId(source) == 'hatw' then
            set TeleportFX[id_traveller] = AddSpecialEffectTarget("war3mapImported\\WarpingIn_NoGeo.mdx", traveller, "origin")
          
        // Orc Burrow
        elseif GetUnitTypeId(source) == 'otrb' then
            set TeleportFX[id_traveller] = AddSpecialEffectTarget("war3mapImported\\DustyMissile.mdx", traveller, "origin")
        endif
              
        set source = null
        set traveller = null
            
    endfunction
  
  
    // Network primed
    private function onNetworkPrimed takes nothing returns nothing
      
        local unit traveller = GetWaygateTraveller()
        local integer id_traveller = GetUnitUserData(traveller)
      
        if TeleportReadyFX[id_traveller] != null then
            call DestroyEffect(TeleportReadyFX[id_traveller])
            set TeleportReadyFX[id_traveller] = null
        endif
      
        // Arcane Tower
        if GetUnitTypeId(GetWaygateSource()) == 'hatw' then
            set TeleportReadyFX[GetUnitUserData(traveller)] = AddSpecialEffectTarget("Abilities\\Spells\\Human\\ManaFlare\\ManaFlareTarget.mdl", traveller, "overhead")
        endif
      
        set traveller = null
      
    endfunction
  
  
    // Teleport interrupted
    private function onTeleportInterrupted takes nothing returns nothing
  
        local integer id_traveller = GetUnitUserData(GetWaygateTraveller())
      
        if TeleportFX[id_traveller] != null then
            call DestroyEffect(TeleportFX[id_traveller])
            set TeleportFX[id_traveller] = null
        endif
      
        if TeleportReadyFX[id_traveller] != null then
            call DestroyEffect(TeleportReadyFX[id_traveller])
            set TeleportReadyFX[id_traveller] = null
        endif
      
    endfunction


    //===========================================================================
    private function init takes nothing returns nothing      
        call RegisterNativeEvent(EVENT_WAYGATE_PRE_TELEPORT, function onPreTeleport)
        call RegisterNativeEvent(EVENT_WAYGATE_POST_TELEPORT, function onPostTeleport)
        call RegisterNativeEvent(EVENT_WAYGATE_DURATION_START, function onTeleportDuration)
        call RegisterNativeEvent(EVENT_WAYGATE_NETWORK_PRIMED, function onNetworkPrimed)
        call RegisterNativeEvent(EVENT_WAYGATE_CANCELLED, function onTeleportInterrupted)
        // Filter - use SmartTrack
        call RegisterNativeEvent(EVENT_SMART_TRACK_STARTED, function onTrackStart)
    endfunction

endscope
[LEFT]

- v1.04 Now uses RegisterNativeEvent for teleportation. Comes with compatibility plugin under optional requirements that still uses TriggerRegisterVariableEvent. View triggers to see.
- v1.03 skipped (internal).
- v1.02 now uses SmartTrack v1.03. Event unit variables now private and only accessible via wrapper functions: GetWaygateSource(), GetTargetSource(), GetWaygateTraveller(). INTERRUPT_TELEPORT is also behind a wrapper StopTeleport(). Demo has been updated accordingly.
- v1.01 fixed a couple of locals that were leaking, cleaned up code a bit, added comments and improved documentation with missing information. SmartTrack updated to 1.02.1.
- v1.00 initial release
Contents

CustomWaygate v1.04 (Map)

Reviews
MyPad
This is ready to be approved. All issues pointed out by the previous reviewer have been resolved to an extent, and does not accurately reflect the current state of quality of the bundle. Nitpicks: Struct orderStunTimer isn't following the ProperCase...
Pros and Cons of CustomWaygate over vanilla waygates:

Pros:
  • Network functionality
  • Teleportation on a timer (interruptible)
  • Ability to interrupt teleport and customise it
  • Can be customised to only teleport player units, etc.
Cons:
  • Does not automatically detect shortest route through waygates for units to use.
 
Last edited:

Dr Super Good

Spell Reviewer
Level 63
Joined
Jan 18, 2005
Messages
27,180
Required for Approval:
  • Update the Smart Track system. The one contained in this map appears to be an old version before the leaks were fixed.
  • Fix the leak in the method Custom Waygate/CustomWaygate/networkTeleport. The local unit variables traveller and target may contain values but are not nulled before function return.
  • Fix the leak in the function onCustomWaygate Events/Actions. The local unit variable traveller might not be null before function return.
Feedback and Suggestions:
  • Custom Waygate/CustomWaygate/networkTeleport declares the local timer variable t but never appears to use it as far as I could see.
  • Delete the method Mana Runes/ManaRunes/init. This method does nothing.
  • Add some comments as to the purpose and use of every method and function. This improves the readability and maintainability of the code.
 
but out of curiosity how slower is a TriggerEvaluate compared to a TriggerRegisterVariableEvent?
Executing triggers seems for me as a clearer and better approach to fire custom events in jass than using TriggerRegisterVariableEvent, and you can control them dynamically if needed, like making structering and re-structering for trigger groups instead of having multiple real variable events.(probably also faster, but it should not be so important)
TRVE's positive side is for GUI-orientated code, as they might directly access it.
 
This is ready to be approved. All issues pointed out by the previous reviewer have been resolved to an extent, and does not accurately reflect the current state of quality of the bundle.

Nitpicks:

  • Struct orderStunTimer isn't following the ProperCase convention. This can easily be updated, though. (thus being only a minor issue to the JPAG)

Status:

  • Approved
 
Level 1
Joined
Apr 8, 2020
Messages
110
Will units treat this custom waygate the same as the normal waygates, as in units ordered to move to a spot that requires this waygate to reach will automatically use the waygate to teleport to the spot? I understand the shortest route is not accounted for, but does that feature have any actual bearing on the automation?
 
CustomWaygates does not compute pathfinding. All usage will have to be done manually.

Something you could do, if you have clearly delineated areas connected by waygates, you can have your units order-smart on a waygate depending on the area they were ordered to move to, depending on the area they are in (use regions, for example).
 
Top