System AreaEvent

Builder Bob

Live free or don't
Reaction score
249
JASS:
//====================================================================================================
//  AreaEvent -- by Builder Bob -- v1.0
//====================================================================================================


//====================================================================================================
//
//      ---------------------
//        Table of Content:
//      ---------------------
//
//      1. FUNCTION LIST
//
//      2. DOCUMENTATION
//
//      3. USER DEFINED CONSTANTS
//
//      4. CODE
//
//====================================================================================================


//====================================================================================================
//  1. FUNCTION LIST:
//====================================================================================================
//
//      ------------------------------------------------------
//        The following functions can be used from anywhere:
//      ------------------------------------------------------
//
//      RegisterAreaEventRect(real minx, real miny, real maxx, real maxy, boolexpr enterfunc, boolexpr leavefunc, integer data)
//      returns integer
//
//      RegisterAreaEventDisk(real x, real y, real radius, boolexpr enterfunc, boolexpr leavefunc, integer data)
//      returns integer
//
//      ...Suggest a new shape...
//
//      DestroyAreaEvent(integer index)
//      returns boolean
//
//      ------------------------------------------------------------------------------------------
//        The following functions should only be used within functions triggered by an AreaEvent
//      ------------------------------------------------------------------------------------------
//
//      GetAreaEventIndex()
//      returns integer
//
//      GetAreaEventTriggerUnit()
//      returns unit
//
//      GetAreaEventData()
//      returns integer
//
//
//----------------------------------------------------------------------------------------------------


//==============================================================================
//  2. DOCUMENTATION:
//==============================================================================
//
//  PURPOSE OF AREA EVENT:
//      * To provide a simple and precise way to handle enter/leave region events.
//
//      * The enter events also trigger on units already in the area you specify.
//          Normal enter events does not.
//
//  PROS:
//      * You get the power to create a region event of various shapes with a single function call.
//
//      * Integrated data attachment.
//
//      * The system should be highly resistant to leaks and double free errors.
//          However, it has only been alpha tested so far.
//
//      * All triggers and regions used are recycled.
//          DestroyTrigger() is not used, avoiding all bugs related to DestroyTrigger()
//
//  CONS:
//      * Once an area event has been registered, you have no access to the trigger or region involved.
//          This means you will have to destroy and recreate the event should you want to move it later.
//
//      * Incompatible with Warcraft 3 version 1.23a or lower
//          To use the system in lower versions you will need a compatibility system.
//
//      * A maximum of 8190 area events can exist at any time.
//
//  HOW TO USE:
//      * Register events with the RegisterAreaEvent... functions.
//
//      * There are mutiple ways of destroying an existing area event.
//          Any enter or leave function returning true will destroy the event.
//          DestroyAreaEvent(index) will destroy the event with that index if one exist.
//          Any attempt to destroy a non-existing event will simply be ignored.
//
//      * To aqcuire the index for an area event you can do one of the following:
//          All RegisterAreaEvent... functions will return the index of the registered event.
//          Calling GetAreaEventIndex() from within an enter or leave function
//          will give you the index of the event which called that function.
//
//      * Units already in the defined area will trigger as GetFilterUnit(),
//          while units entering/leaving will trigger as GetTriggerUnit().
//          Use GetAreaEventTriggerUnit() to get the right unit.
//          GetTriggerUnit() can safely be used inside leaveFunc.
//
//      * Attach data to an event with the data argument in the register functions.
//          Retrieve that data with GetAreaEventData() in enterFunc and leaveFunc.
//
//      * Be sure to set the user defined constant MAX_COLLISION_SIZE
//          to the collision size of the biggest unit in your map.
//
//  THANKS TO:
//      * Peppar - for RegionAddDisk and RegionClearDisk
//
//  HOW TO IMPORT:
//      * create a trigger
//      * convert it to text and replace the whole trigger text with this one
//
//==============================================================================


//==============================================================================
//  3. USER DEFINED CONSTANTS:
//==============================================================================
library AreaEvent initializer onInit

globals
    private constant real MAX_COLLISION_SIZE = 200.
endglobals

//==============================================================================
//  4. CODE:
//==============================================================================
private function interface EnumFunc takes nothing returns boolean
private function interface ClearFunc takes nothing returns boolean

globals
    private hashtable Hashtable = InitHashtable()
    private timer Timer = CreateTimer()
    private group Group = CreateGroup()
    private EnumFunc array EnumFuncs
    private ClearFunc array ClearFuncs
    private integer array Pos
    private integer array Indexes
    private trigger array EnterTriggers
    private trigger array LeaveTriggers
    private trigger array EnterExec
    private trigger array LeaveExec
    private real array MinX
    private real array MinY
    private real array MaxX
    private real array MaxY
    private real array Radius
    private rect array Rects
    private region array Regions
    private integer array Datas
    private integer TopIndex = 1
    private integer MaxIndex = 1
    
    private unit WhichUnit
    private rect WhichRect = Rect(0., 0., 0., 0.)
    private boolean TriggerEnabled
endglobals

globals
    private integer First = 0
    private integer This = 0
    private integer array Next
endglobals

private function True takes nothing returns boolean
    return true
endfunction

private function DisplayActive takes nothing returns nothing
debug    local string s = ""
debug    local integer i = 1
debug    local integer topIndex = TopIndex
debug    local integer maxIndex = MaxIndex
debug    if topIndex > 17 then
debug        set topIndex = 17
debug    endif
debug    if maxIndex > 17 then
debug        set maxIndex = 17
debug    endif
debug    loop
debug        exitwhen(i >= topIndex)
debug        set s = s + &quot;|c0000FF00[&quot; + I2S(Indexes<i>) + &quot;]|r&quot;
debug        set i = i + 1
debug    endloop
debug    loop
debug        exitwhen(i &gt;= maxIndex)
debug        set s = s + &quot;|c00FF0000[&quot; + I2S(Indexes<i>) + &quot;]|r&quot;
debug        set i = i + 1
debug    endloop
debug    call BJDebugMsg(s)
endfunction

private function RegionAddDisk takes region whichRegion, real x, real y, real radius returns nothing
    local real r = 16.
    local real w = 0.
    loop
        exitwhen(r &gt; radius)
        set w = Cos(Asin(r / radius)) * radius
        call SetRect(WhichRect, x - w, y + r - 16., x + w, y + r + 16.)
        call RegionAddRect(whichRegion, WhichRect)
        call SetRect(WhichRect, x - w, y - r - 16., x + w, y - r + 16.)
        call RegionAddRect(whichRegion, WhichRect)
        set r = r + 32.
    endloop
endfunction

private function RegionClearDisk takes region whichRegion, real x, real y, real radius returns nothing
    local real r = 16.
    local real w = 0.
    loop
        exitwhen(r &gt; radius)
        set w = Cos(Asin(r / radius)) * radius
        call SetRect(WhichRect, x - w, y + r - 16., x + w, y + r + 16.)
        call RegionClearRect(whichRegion, WhichRect)
        call SetRect(WhichRect, x - w, y - r - 16., x + w, y - r + 16.)
        call RegionClearRect(whichRegion, WhichRect)
        set r = r + 32.
    endloop
endfunction

private function RegionClearAreaRect takes nothing returns boolean
    call RegionClearRect(Regions[This], Rects[This])
    return false
endfunction

private function RegionClearAreaDisk takes nothing returns boolean
    call RegionClearDisk(Regions[This], MinX[This], MinY[This], Radius[This])
    return false
endfunction

function DestroyAreaEvent takes integer index returns boolean
    local integer pos = Pos[index]
    if pos != 0 and pos &lt; TopIndex then
        call ClearFuncs[index].evaluate()
        call DisableTrigger(EnterTriggers[index])
        call DisableTrigger(LeaveTriggers[index])
        //swap pos
        set TopIndex = TopIndex - 1
        set Indexes[pos] = Indexes[TopIndex]
        set Pos[Indexes[pos]] = pos
        set Indexes[TopIndex] = index
        set Pos[index] = TopIndex
        if This == index then
            set TriggerEnabled = false
        endif
        //debug call BJDebugMsg(&quot;|c00FF0000REM &quot; + I2S(index))
        //debug call DisplayActive()
        return true
    endif
    return false
endfunction

private function FilterOutsideRegion takes nothing returns boolean
    if TriggerEnabled then
        if IsUnitInRegion(Regions[This], GetFilterUnit()) then
            if TriggerEvaluate(EnterExec[This]) then
                call DestroyAreaEvent(This)
            endif
        endif
    endif
    return false
endfunction

private function Leave takes nothing returns boolean
    set This = LoadInteger(Hashtable, 0, GetHandleId(GetTriggeringTrigger()))
    if TriggerEvaluate(LeaveExec[This]) then
        call DestroyAreaEvent(This)
    endif
    return false
endfunction

private function Enter takes nothing returns boolean
    set This = LoadInteger(Hashtable, 0, GetHandleId(GetTriggeringTrigger()))
    if TriggerEvaluate(EnterExec[This]) then
        call DestroyAreaEvent(This)
    endif
    return false
endfunction

private function EnumAreaRect takes nothing returns boolean
    call SetRect(WhichRect, MinX[This] - MAX_COLLISION_SIZE, MinY[This] - MAX_COLLISION_SIZE, MaxX[This] + MAX_COLLISION_SIZE, MaxY[This] + MAX_COLLISION_SIZE)
    call GroupEnumUnitsInRect(Group, WhichRect, Condition(function FilterOutsideRegion))
    return false
endfunction

private function EnumAreaDisk takes nothing returns boolean
    call GroupEnumUnitsInRange(Group, MinX[This], MinY[This], Radius[This] + MAX_COLLISION_SIZE, Condition(function FilterOutsideRegion))
    return false
endfunction

private function Expire takes nothing returns boolean
    local integer next
    set This = First
    set First = 0
    loop
        exitwhen(This == 0)
        call EnableTrigger(EnterTriggers[This])
        call EnableTrigger(LeaveTriggers[This])
        set TriggerEnabled = true
        set next = Next[This]
        call EnumFuncs[This].evaluate()
        set This = next
    endloop
    return false
endfunction

private function NewIndex takes boolexpr enterFunc, boolexpr leaveFunc, integer data returns integer
    local integer index
    debug if TopIndex == 8191 then
        debug call BJDebugMsg(&quot;Warning: AreaEvent stack is full!&quot;)
        debug return 0
    debug endif
    if TopIndex &lt; MaxIndex then
        set index = Indexes[TopIndex]
        call TriggerClearConditions(EnterExec[index])
        call TriggerClearConditions(LeaveExec[index])
        set TopIndex = TopIndex + 1
        //debug call BJDebugMsg(&quot;|c00FFFF00REUSE &quot; + I2S(index))
        //debug call DisplayActive()
    else
        set index = TopIndex
        set EnterTriggers[index] = CreateTrigger()
        set LeaveTriggers[index] = CreateTrigger()
        set EnterExec[index] = CreateTrigger()
        set LeaveExec[index] = CreateTrigger()
        call DisableTrigger(EnterTriggers[index])
        call DisableTrigger(LeaveTriggers[index])
        set Rects[index] = Rect(0., 0., 0., 0.)
        set Regions[index] = CreateRegion()
        call TriggerRegisterEnterRegion(EnterTriggers[index], Regions[index], Condition(function True))
        call TriggerAddCondition(EnterTriggers[index], Condition(function Enter))
        call TriggerRegisterLeaveRegion(LeaveTriggers[index], Regions[index], Condition(function True))
        call TriggerAddCondition(LeaveTriggers[index], Condition(function Leave))
        call SaveInteger(Hashtable, 0, GetHandleId(EnterTriggers[index]), index)
        call SaveInteger(Hashtable, 0, GetHandleId(LeaveTriggers[index]), index)
        set Pos[index] = index
        set Indexes[index] = index
        set TopIndex = TopIndex + 1
        set MaxIndex = MaxIndex + 1
        //debug call BJDebugMsg(&quot;|c0000FF00NEW &quot; + I2S(index))
        //debug call DisplayActive()
    endif
    call TriggerAddCondition(EnterExec[index], enterFunc)
    call TriggerAddCondition(LeaveExec[index], leaveFunc)
    set Datas[index] = data
    
    set Next[index] = First
    set First = index
    call TimerStart(Timer, 0., false, function Expire)
    return index
endfunction

function RegisterAreaEventDisk takes real x, real y, real radius, boolexpr enterFunc, boolexpr leaveFunc, integer data returns integer
    local integer index = NewIndex(enterFunc, leaveFunc, data)
    debug if index == 0 then
        debug return 0
    debug endif
    set MinX[index] = x
    set MinY[index] = y
    set Radius[index] = radius
    set EnumFuncs[index] = EnumFunc.EnumAreaDisk
    set ClearFuncs[index] = ClearFunc.RegionClearAreaDisk
    call RegionAddDisk(Regions[index], x, y, radius)
    return index
endfunction

function RegisterAreaEventRect takes real minx, real miny, real maxx, real maxy, boolexpr enterFunc, boolexpr leaveFunc, integer data returns integer
    local integer index = NewIndex(enterFunc, leaveFunc, data)
    debug if index == 0 then
        debug return 0
    debug endif
    set MinX[index] = minx
    set MinY[index] = miny
    set MaxX[index] = maxx
    set MaxY[index] = maxy
    set EnumFuncs[index] = EnumFunc.EnumAreaRect
    set ClearFuncs[index] = ClearFunc.RegionClearAreaRect
    call SetRect(Rects[index], minx, miny, maxx, maxy)
    call RegionAddRect(Regions[index], Rects[index])
    return index
endfunction

function GetAreaEventIndex takes nothing returns integer
    return This
endfunction

function GetAreaEventData takes nothing returns integer
    return Datas[This]
endfunction

function GetAreaEventTriggerUnit takes nothing returns unit
    set WhichUnit = GetTriggerUnit()
    if WhichUnit != null then
        return WhichUnit
    endif
    return GetFilterUnit()
endfunction

private function AreaEventStats takes nothing returns boolean
debug    local integer i = 1
debug    local integer topIndex = TopIndex
debug    if topIndex &gt; 17 then
debug        set topIndex = 17
debug    endif
debug    loop
debug        exitwhen(i &gt;= topIndex)
debug        call PingMinimap(MinX[Indexes<i>], MinY[Indexes<i>], 1.)
debug        set i = i + 1
debug    endloop
debug    call BJDebugMsg(&quot;=============================================&quot;)
debug    call BJDebugMsg(&quot;Biggest index ever = &quot; + I2S(MaxIndex - 1))
debug    call BJDebugMsg(&quot;Indexes in use = &quot; + I2S(TopIndex - 1))
debug    call BJDebugMsg(&quot;Released indexes = &quot; + I2S(MaxIndex - TopIndex))
debug    call BJDebugMsg(&quot;=============================================&quot;)
debug    return false
endfunction

private function onInit takes nothing returns nothing
debug    local trigger trig = CreateTrigger()
debug    call TriggerRegisterPlayerChatEvent(trig, Player(0), &quot;-AreaEventStats&quot;, true)
debug    call TriggerAddCondition(trig, Condition(function AreaEventStats))
endfunction

endlibrary</i></i></i></i>


I will make a demo map if there is any interest.
 

Silvenon

New Member
Reaction score
19
Sounds cool to me.

It would be awesome if you develop this to a ultimate pwning system with all the shapes anyone could ever think of. This requires a fine knowledge of geometry though. You could also add a irregular rectangular? Maybe go into polygons too?

Anyways, I'm interested :D
 

Jesus4Lyf

Good Idea™
Reaction score
397
I was looking through the code to see how you handle events. So apparently, this isn't really like an event. It seems returning true in the boolexpr destroys the instance, returning false continues it. Right?

Seems appropriate. Having "Event" in the title is a little misleading, as this definitely doesn't follow an event style interface. :)

And if I was right, that should be in the documentation (maybe I missed it if it is). :)
 

Builder Bob

Live free or don't
Reaction score
249
I was looking through the code to see how you handle events. So apparently, this isn't really like an event.
(...)
Seems appropriate. Having "Event" in the title is a little misleading, as this definitely doesn't follow an event style interface. :)
How do you figure?

It functions very much like an event the way I look at an event. The same way an event in a trigger calls the condition/action functions, this will call the appropriate functions whenever a unit enters or leaves the defined area.

At an early iteration of this system the register function was something like TriggerRegisterAreaEvent(whichTrigger, whichRegion). This is closer to how the standard blizzard event register functions work. Not worrying if a user has destroyed the trigger or changed/removed the region simplifies a lot of things, so this is what I ended up with after some reiterations. It still works like an event would. The syntax for registering it is just different.

It seems returning true in the boolexpr destroys the instance, returning false continues it. Right?
Right. You can also destroy an instance at any time with DestroyAreaEventIndex(index). If you're making a spell that creates a timed effect over an area for example, that would be the best way to destroy it.


Sounds cool to me.

It would be awesome if you develop this to a ultimate pwning system with all the shapes anyone could ever think of. This requires a fine knowledge of geometry though. You could also add a irregular rectangular? Maybe go into polygons too?

Anyways, I'm interested :D

That's a good idea. I'm having a little trouble with creating a RegionAddPolygon() function but it's absolutely doable.

What do you mean with irregular rectangular? Like a trapezoid?
 

Jesus4Lyf

Good Idea™
Reaction score
397
Events are registered on triggers. Multiple events can be on one trigger. Multiple triggers can trigger off one event.

This is really more appropriate than an actual event kind of structure for this, though. And you can use the integer for data attachment instead of attaching to the trigger you'd usually register on, right? So this seems fairly useful. But I'd actually expose some kind of destroyable struct, containing the stuff. :D Up to you.
 

Builder Bob

Live free or don't
Reaction score
249
Events are registered on triggers. Multiple events can be on one trigger. Multiple triggers can trigger off one event.
All true. I'm going to keep calling it AreaEvent though. This system is meant to be used dynamically. Registering events on a trigger will only lead to you wanting to destroy that trigger later. Destroying triggers should be avoided if possible.

This is really more appropriate than an actual event kind of structure for this, though. And you can use the integer for data attachment instead of attaching to the trigger you'd usually register on, right?
You could, and there's also an argument for attaching data when you register the event. Attaching will almost always be required, so I tend to integrate it into every system which are allowed to call user defined functions.

So this seems fairly useful. But I'd actually expose some kind of destroyable struct, containing the stuff. :D Up to you.
I'm sure that could work as well. It sounds more flexible, but also more complex and prone to potential mistakes from the user. I made this system because I wanted fire and forget functions for registering enter/leave events without having to destroy triggers.


The system was meant for spell making. I'll make a demo map with a few spells later. Having lots of triggered area effect spells usually tend to slow down a map because of the spells periodically checking for units in the area. Using enter/leave events will not have the same slowdown because there are no periodic checks. It will also affect units precisely when they enter the effect and end when they leave.
 

Jesus4Lyf

Good Idea™
Reaction score
397
Well, cool. Looks like a nice and useful system. :)

JASS:
call RegionAddDisk(Regions[index], x, y, radius)

I'd be tempted to make this call RegionAddDisk.execute(... just because of the variable complexity. For example, if you had a map that is set in a city, and you wanted to register different areas as circles (quite large areas), you'd likely hit op-limit?

>set w = Cos(Asin(r / radius)) * radius
I don't know the meaning of that line, and hence cannot verify that it actually creates a circle. I don't see how a pattern involving only two rects per iteration could create a circle. Can you shed some light on this?

+Rep. Well done. :)
 

Builder Bob

Live free or don't
Reaction score
249
Well, cool. Looks like a nice and useful system. :)

JASS:
call RegionAddDisk(Regions[index], x, y, radius)

I'd be tempted to make this call RegionAddDisk.execute(... just because of the variable complexity. For example, if you had a map that is set in a city, and you wanted to register different areas as circles (quite large areas), you'd likely hit op-limit?

>set w = Cos(Asin(r / radius)) * radius
I don't know the meaning of that line, and hence cannot verify that it actually creates a circle. I don't see how a pattern involving only two rects per iteration could create a circle. Can you shed some light on this?

+Rep. Well done. :)

You're right. I will add .execute to that call.

As for the content of the function, It is made by Peppar at WC3Jass.com :: RegionAddCircle.

From what I understand it creates rects to form a pattern like this (just more close to a circle):
Code:
       [___________]
    [_________________]
  [_____________________]
 [_______________________]
[_________________________]
[_________________________]
[_________________________]
[_________________________]
 [_______________________]
  [_____________________]
    [_________________]
       [___________]
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • Ghan Ghan:
    Still lurking
    +3
  • The Helper The Helper:
    I am great and it is fantastic to see you my friend!
    +1
  • The Helper The Helper:
    If you are new to the site please check out the Recipe and Food Forum https://www.thehelper.net/forums/recipes-and-food.220/
  • Monovertex Monovertex:
    How come you're so into recipes lately? Never saw this much interest in this topic in the old days of TH.net
  • Monovertex Monovertex:
    Hmm, how do I change my signature?
  • tom_mai78101 tom_mai78101:
    Signatures can be edit in your account profile. As for the old stuffs, I'm thinking it's because Blizzard is now under Microsoft, and because of Microsoft Xbox going the way it is, it's dreadful.
  • The Helper The Helper:
    I am not big on the recipes I am just promoting them - I use the site as a practice place promoting stuff
    +2
  • Monovertex Monovertex:
    @tom_mai78101 I must be blind. If I go on my profile I don't see any area to edit the signature; If I go to account details (settings) I don't see any signature area either.
  • The Helper The Helper:
    You can get there if you click the bell icon (alerts) and choose preferences from the bottom, signature will be in the menu on the left there https://www.thehelper.net/account/preferences
  • The Helper The Helper:
    I think I need to split the Sci/Tech news forum into 2 one for Science and one for Tech but I am hating all the moving of posts I would have to do
  • The Helper The Helper:
    What is up Old Mountain Shadow?
  • The Helper The Helper:
    Happy Thursday!
    +1
  • Varine Varine:
    Crazy how much 3d printing has come in the last few years. Sad that it's not as easily modifiable though
  • Varine Varine:
    I bought an Ender 3 during the pandemic and tinkered with it all the time. Just bought a Sovol, not as easy. I'm trying to make it use a different nozzle because I have a fuck ton of Volcanos, and they use what is basically a modified volcano that is just a smidge longer, and almost every part on this thing needs to be redone to make it work
  • Varine Varine:
    Luckily I have a 3d printer for that, I guess. But it's ridiculous. The regular volcanos are 21mm, these Sovol versions are about 23.5mm
  • Varine Varine:
    So, 2.5mm longer. But the thing that measures the bed is about 1.5mm above the nozzle, so if I swap it with a volcano then I'm 1mm behind it. So cool, new bracket to swap that, but THEN the fan shroud to direct air at the part is ALSO going to be .5mm to low, and so I need to redo that, but by doing that it is a little bit off where it should be blowing and it's throwing it at the heating block instead of the part, and fuck man
  • Varine Varine:
    I didn't realize they designed this entire thing to NOT be modded. I would have just got a fucking Bambu if I knew that, the whole point was I could fuck with this. And no one else makes shit for Sovol so I have to go through them, and they have... interesting pricing models. So I have a new extruder altogether that I'm taking apart and going to just design a whole new one to use my nozzles. Dumb design.
  • Varine Varine:
    Can't just buy a new heatblock, you need to get a whole hotend - so block, heater cartridge, thermistor, heatbreak, and nozzle. And they put this fucking paste in there so I can't take the thermistor or cartridge out with any ease, that's 30 dollars. Or you can get the whole extrudor with the direct driver AND that heatblock for like 50, but you still can't get any of it to come apart
  • Varine Varine:
    Partsbuilt has individual parts I found but they're expensive. I think I can get bits swapped around and make this work with generic shit though
  • Ghan Ghan:
    Heard Houston got hit pretty bad by storms last night. Hope all is well with TH.
  • The Helper The Helper:
    Power back on finally - all is good here no damage
    +2
  • V-SNES V-SNES:
    Happy Friday!
    +1
  • The Helper The Helper:
    New recipe is another summer dessert Berry and Peach Cheesecake - https://www.thehelper.net/threads/recipe-berry-and-peach-cheesecake.194169/

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top