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

Antares' Limitless Interaction Caller Engine

This bundle is marked as pending. It has not been reviewed by a staff member yet.
RbeQz1.png


Complex, performant interactions made easy

RbeQz1.png



RbeQz1.png

Antares' Limitless Interaction Caller Engine (A.L.I.C.E) is an engine that is heavily optimized for collision detection, but can be used for much more. Any type of system can be embedded into its framework, making it easier to set up and manage, as well as making it significantly more performant. The core of the system is optimized for speed and the outer shell provides an easy interface that allows any type of interaction to be set up quickly. ALICE makes full use of Lua's dynamically-typed nature, allowing you to pass anything from units, items, players, and your own classes as objects into the system.

How it works is that you register an object (unit, destructable, missile etc.) with ALICE and set up its interaction functions. It then monitors that object's position on the map and executes the functions when it comes into the interaction range with another object.

Motivation

Although the ALICE core was based on my particle system, my motivation for the development of this system was actually to create A.I. with it, where the interactions would be objects on the map sending data and being evaluated by a bot, who uses this data to calculate its next action. While writing it, I noticed that I'm not hardcoding any specifics into the outer framework, so I decided to make it into its own stand-alone system that could be used for anything (particles and A.I. are already quite different things :plol:).

The endless possibilities of Lua soon got me hooked and I started designing the system such that any types of objects, functions etc. could be passed, with more and more options added (I can't stop, please send help!).

Unfortunately, this means that ALICE is Lua-only and cannot be translated into JASS. It is therefore aimed at more advanced map makers. Its flexibility also comes with the cost that it doesn't just do one thing and does it as soon as you install it. However, I hope that in the future, there can be install-and-done systems created with it that have GUI support. ALICE would then provide the unique feature that all of these systems can easily be interconnected.

What is different about ALICE?

Unlike most other similar systems, ALICE's core avoids using enum functions entirely. Most missile systems, for example, would search for collisions with units by calling GroupEnumUnitsInRange. But while natives are optimized and written in lightning-fast C++, Lua is a fast language itself, and calling natives is expensive. For example, GetUnitX, which presumably does nothing but look up the number in a field, is about 10x slower than a Lua function call and 50x slower than a Lua table lookup. In addition, the process of calling the natives repeatedly is inefficient itself.

Think of it this way: Calling GroupEnumUnitsInRange on every iteration is like having the most advanced GPS system there is installed in your car, but then determining the time until arrival by having a kid on the back-seat ask "Are we there yet?" over and over again.

ALICE makes use of two different optimization methods:
  1. Variable interaction intervals: Updates between two objects don't necessarily have to be performed every step. If you want to check the collision between two objects that are two screens apart, you don't need to check again for quite some time, so doing another check on the very next iteration would be a waste of resources. The interaction interval is specified in the interaction function.
  2. Cells: The map is divided into a grid of cells. Objects only interact with other objects that share a cell. This optimization is similar to the quad-trees used for collision checks in Warcraft III, but simplified.
There are advantages and disadvantages to ALICE's approach. When it comes to units, it will be faster in some situations and slower in others. For most practical applications, it won't matter. But the main advantage is that ALICE can do not only object-unit interactions, but also object-object interactions. It combines both into a single system and blows every system using dummy units and enum functions completely out of the water when it comes to the latter task.

Ok, so besides allowing you to spawn thousands of missiles, which you may or may not feel the urge to do, what else does this system offer? Even if performance is not a primary concern, ALICE still has a lot to offer with regards to flexibility and convenience. They were just as important to me when designing this system.

In the showcase map, you will find a great variety of examples, where the interactions have all been set up with just a few lines of code.

Complementary Actor Templates

As mentioned earlier, ALICE by itself doesn't do a whole lot other than rearrange data in a ludicrous number of tables, but you can write systems with it that do. Therefore, packaged with ALICE are the CATs, small libraries that provide a good starting point. The units CAT automatically creates ALICE objects for units when they enter the map and destroys them when they die. The destructables CAT does the same for destructables.

I will add more such libraries, including one for missiles, over time.




Lua:
  --[[
    =============================================================================================================================================================
                                                        Antares' Limitless Interaction Caller Engine

							    A Lua system to easily create highly performant checks and interactions, between any type of objects.
						
								Requires:
								TotalInitialization			https://www.hiveworkshop.com/threads/total-initialization.317099/

    =============================================================================================================================================================
    Overview
    =============================================================================================================================================================

    To install this library, copy it into an empty script file in your map.

    To enable debug mode, import "CustomTooltip.toc" and "CustomTooltip.fdf".

    There are only few values to be set in the config. First, change the MAP_CREATORS field to your name.
	
	The main work is creating actors and their interaction functions. Let's say we want to create a new spell. It should create multiple orbs at different locations
    that each have an aura around them. In normal GUI/JASS/Lua, we would need a periodic timer, loop over our orbs, GroupEnumUnitsInRange to find units affected,
    filter out the units that aren't eligible, and then run our code. ALICE takes care of the entire control structure of our code with a network of its main
    internal object, the actor. The task of writing the correct control structure then transforms into assigning the actors the correct properties on creation.
	
    =============================================================================================================================================================
    What is an actor?
    =============================================================================================================================================================

    An actor is a class that is attached to any type of object and interacts with other actors that were added to the system. Two actors form a pair. Actors carry
    identifiers and function interfaces determining their interactions and with which other actors they form a pair.

    When two actors in a pair interact, an interaction function that you can specify will be called. After the pair's interaction is calculated, the next
    interaction is placed in the queue. It is up to you to set the time interval between the two interactions. This is done via the return value of the
    interactionFunc. The interactionFunc uses two input arguments which specify the "hosts" of the two actors; the objects the actors are attached to. Hosts can
	be any type of object - a unit, an item, a class, a player - whatever thing you want the actor to represent. A host can also be a string, an integer, or nil,
    which could represent some kind of global effect.
	
	Actors can both receive and initiate pairs. When a new actor is created, it checks if it should initiate a pair with each already existing actor and all
	existing actors will check if they should initiate a pair themselves. Two-way pairs are not possible.

    ALICE will automatically retrieve the position of an actor, if it's applicable. If it can't, it will create it as a global actor.

    =============================================================================================================================================================
    Schematic
    =============================================================================================================================================================

          PairingCheck     ↔      Identifier
            Identifier     ↔	  PairingCheck
                  ↑        ↓         ↑
    Object A → Actor A    Pair     Actor B ← Object B
                           ↓
                    *InteractionFunc  ← ←
                       ↓         ↓       ↑
				   Object A ↔ Object B   ↑
                            ↓            ↑
                   Time until next call →


    *inherited from the actor initiating the pair.

    =============================================================================================================================================================
    How to create an actor
    =============================================================================================================================================================
    To create an actor, do ALICE_Create(host, identifier, interactionFunc). There are additional values you can set, but they're not important for getting started.

    -----host-----
    Any type of object which the actor is supposed to represent. You can create multiple actors for one host.

    -----identifier-----
    A string or string sequence that can be used in a filter to identify different types of actors. Examples: "hero", "missile", or {"unit", "hero", "human", "Hpal"}.

    -----interactionFunc-----
    A function or table that specifies the function that is called when this object interacts with another object. If a function is passed, the actor will pair with
    everything (not recommended!). With a table, you can specify different interaction functions for different types of objects. For example: {milk = Drink, apple = Eat},
    will use the Drink function for all actors with the "milk" identifier and the Eat function for all actors with the "apple" identifier.
	
	Actors will only pair with other actors for which an interactionFunc exists, but you can use the "other" keyword to declare an interactionFunc that is used for
	all actors not included in your list.

    -----flags-----
    Flags are an optional fourth input argument. They are discussed in the advanced tutorial.


    -----ALICE_CreateFromClass-----
    ALICE_CreateFromClass(class) creates an actor for a class and retrieves all input arguments directly from that class.


    =============================================================================================================================================================
    How to create an interaction function
    =============================================================================================================================================================

    An interactionFunc must have two input arguments of any type, specifying the objects (not the actors), and one return value of type number, specifying the time
	until the next interaction (in seconds). The object that is the source of the interaction func will always be passed as the first argument.

    Within the interaction func, you have access to the Pair API.

    Example:

    function Explode(whichMine, whichUnit)
        local dist = ALICE_PairGetDistance()
        if dist < 100 then
            whichMine:destroy()
            KillUnit(whichUnit)
        end
        return (dist - 100)/300
    end
]]

Lua:
--[[
    =============================================================================================================================================================
    Self-Interaction Functions
    =============================================================================================================================================================

    Self-interaction is a convenient way to execute code that affects only one actor. By default, actors will never pair with themselves or another actor with the
    same host, but you can use the "self" keyword in an interactionFunc table to declare a self-interaction function. In the self-interaction function, the actor's
    host is passed as both arguments.
    
    Example:

    interactionFunc = {missile = DestroyMissile, self = Move}

    function Move(blast, __)
        blast.x = blast.x + blast.vx
        blast.y = blast.y + blast.vy
        BlzSetSpecialEffectPosition(blast.specialEffect, blast.x, blast.y, 0)
    end

    This creates a projectile that moves itself every step and presumably destroys missiles on contact.

    You can pass a table to define multiple self-interaction functions. Example:

    interactionFunc = {missile = DestroyMissile, self = {Move, Accelerate}}

    =============================================================================================================================================================
    EveryStep Functions
    =============================================================================================================================================================

    You can define an interactionFunc as an everyStep function. If you do, the interactions evaluated by that function will be executed every step instead of at a
    variable interval. This reduces the overhead required and improves performance. The interactionFunc no longer requires a return value. EveryStep functions are
    recommended for moving/accelerating objects.

    To declare a function as an everyStep function, do ALICE_SetEveryStep(myFunction).

    =============================================================================================================================================================
    Flags
    =============================================================================================================================================================

    Flags are a table that contains additional parameters that allow you to control the behavior of actors much more than what is possible with just the standard
    input arguments.

    The possible flags are (ordered by increasing niche-ness):

    radius
    pairsWith
    destroyOnDeath
    isStationary
    bindToBuff
    bindToOrder
    onActorDestroy
    cellCheckInterval
    priority
    anchor
    randomDelay
    isIndestructible

    Example: {isStationary = true, radius = 200}

    -----radius-----
    Overwrite the default object radius. The radius determines which cells the object is in. Objects can only interact with other objects with which they share
    at least one cell, so the radius should be at least the object's interaction range. Some additional leeway is recommended, since cells are not updated on
    every iteration.

    -----pairsWith-----
    A string, string sequence, or function that specifies which other actors the newly created actor has interactions with. For example: "unit" will pair with all
    actors that have the "unit" identifier. By default, an actor will pair with all other actors for which an interaction function exists, but the pairsWith flag
    adds another restriction. Using tables for interactionFuncs or using the pairsWith flag can achieve similar results, but the level of control with pairsWith
    is much higher. Example:
    ALICE_Create(host, "killer", {unit = KillUnit})
    ALICE_Create(host, "killer", KillUnit, {pairsWith = "unit"})
    are identical.

    How to create a pairsWith flag is explained under actor filters.

    -----destroyOnDeath-----
    Destroy the actor automatically when its host is destroyed. Works only for widgets.

    -----isStationary-----
    Disables cell checks for this object if it is stationary and cannot move to improve performance. Automatically enabled for destructables.

    -----bindToBuff / bindToOrder-----
    Automatically destroy the actor when the host unit no longer has the buff with the bindToBuff string or when its current order is no longer the string bindToOrder.

    -----onActorDestroy------
    A function that is executed when the actor is destroyed. Passes the host as the argument.

    -----cellCheckInterval-----
    Overwrite the default cell check interval and set how often it should be checked if the object has left a cell it was previously in. A fast-moving object
    should be checked more often than a slow-moving object.

    -----priority-----
    Determines which actor takes priority over the other when deciding which interactionFunc is called in a case where it would be ambiguous otherwise. If both
    actors pair with the other, the interactionFunc of the one with the higher priority is called. The default priority is 0.

    You can shroud an actor from other actors initiating pairs with it and only pair using its own pairing behavior by setting its priority to NO_INCOMING_PAIRS.

    -----anchor-----
    Overwrites the host as the source of the actor coordinates with the object provided in anchor. Also reroutes the destroyOnDeath trigger to the anchor and
    prevents actors anchored to the same object from paring. Useful if you want to pass as the host a class attached to a unit. Then you can anchor the actor to
    the unit directly.

    -----randomDelay-----
    If this flag is set, the first interaction will be delayed by a random amount between 0 and the specified number.

    -----isIndestructible-----
    If set to true, attempts to destroy this actor will throw an error. In addition, ALICE will never isolate this actor if it is causing crashes (it will still
    isolate the other actor in the pair).

    =============================================================================================================================================================
    How to create Actor Filters
    =============================================================================================================================================================

    Actor filters are used in enum functions, in ALICE_HasIdentifier and in the pairsWith flag. Actor filters can be strings, tables, or functions.

    A string will filter all actors that possess the identifier.

    In a table, every entry has to be a string except for the optional last entry, which is a parameter to specifiy how the list should be interpreted.
    {"A", "B", ..., MATCHING_TYPE_ANY}      Filter all actors that have at least one of the listed identifiers. Default if not specified.
    {"A", "B", ..., MATCHING_TYPE_ALL}      Filter all actors that have all the listed identifiers.
    {"A", "B", ..., MATCHING_TYPE_EXACT}    Filter all actors that have exactly the listed identifiers.
    {"A", "B", ..., MATCHING_TYPE_EXCLUDE}  Filter all actors that have none of the listed identifiers.

    A function must have one input argument that references the filtered actor's host and a boolean return value. Example: A newly created actor should only
    pair with nonhero, undead units. The filter function could be:

    function FilterNonheroUndead(filterHost)
        --Prevent calling natives on a non-unit host.
        if not ALICE_HasIdentifier(filterHost, "unit") then
            return false
        end
        if IsUnitType(filterHost, UNIT_TYPE_HERO) then
            return false
        end
        return IsUnitType(filterHost, UNIT_TYPE_UNDEAD)
    end

    This could of course also be achieved by setting all the necessary flags for each actor and using the table {"unit", "nonhero", "undead", MATCHING_TYPE_ALL}.

    -----Owner filter-----
    An actor created for a host that has an owner (a unit-type host or a class-type host with a CLASS_FIELD_OWNER field) can be filtered by table-type filters
    based on its owner. This is done by inserting an additional value at the end of the table. Valid arguments are:

    PLAYER_FILTER_TYPE_ALLY                 Pairs with all objects owned by an ally of the object's owner.
    PLAYER_FILTER_TYPE_ENEMY                Pairs with all objects owned by an enemy of the object's owner.
    PLAYER_FILTER_TYPE_OWNER                Pairs with all objects owned by the object's owner.
    PLAYER_FILTER_TYPE_NOT_OWNER            Pairs with all objects not owned by the object's owner.
    Player                                  Pairs with all objects owned by the specified player.

    Example: {"unit", "ground", MATCHING_TYPE_ALL, PLAYER_FILTER_TYPE_ENEMY}
    will pair with all ground units that are owned by an enemy of the new object's owner.

    Note that the owner does not get updated automatically when a unit switches sides, but you can do it manually with ALICE_UpdateOwner(). However, in those
    situations, it might be better to pair with all units regardless of their owner and simply put the owner check inside the interaction function.

    =============================================================================================================================================================
]]

Lua:
--[[
	=============================================================================================================================================================
																		    A P I
    =============================================================================================================================================================

                                    All object functions work with either a host or an actor as the input argument. Keyword
                                      parameter is optional and only needed if you create multiple actors for one host. It
                                                searches for the actor that has that keyword in its identifier.

    -----Main API------
    ALICE_Create(host, identifier, interactionFunc, flags)    		 				Create an actor for the object host and add it to the cycle.
    ALICE_CreateFromClass(whichClass)                                               Create an actor for a table host and retrieve all input arguments from that table.
    ALICE_Destroy(whichObject)                                                      Destroy the actor of the object and remove it from the cycle.


    -----Pair API-----
    ALICE_PairSetInteractionFunc(whichFunc)                                         Changes the interactionFunc of the pair currently being evaluated.
    ALICE_PairDisable()                                                             Disables interactions between the actors of the current pair after this one. Changing
                                                                                    the properties of one of the actors will reset.
    ALICE_PairLoadData()                                                            Returns a table unique to the pair currently being evaluated, which can be used to
                                                                                    read and write data. Optimal argument to initialize values from the specified class.
    ALICE_PairGetDistance()                                                         Returns the distance between the objects of the pair currently being evaluated in two
                                                                                    dimensions.
    ALICE_PairGetDistance3D()                                                       The same, but takes z into account.
    ALICE_PairGetAngle()                                                            Returns the angle from object A to object B of the pair currently being evaluated.
    ALICE_PairGetAngle3D()                                                          Returns the horizontal and vertical angles from object A to object B.
    ALICE_PairOnDestroy(callback)                                                   Executes the function callback(objectA, objectB, pairData) when either of the actors in
                                                                                    the current pair are destroyed. Note: Only one callback. Pair API is not accessible
                                                                                    within callback function. Avoid inlining callback function to avoid garbage build-up.
    ALICE_PairIsFirstContact()                                                      Returns true if this is the first time this function was invoked for the current pair,
                                                                                    otherwise false.
    ALICE_PairIsUnoccupied()                                                        Returns false if this function was invoked for another pair that has the same
                                                                                    interactionFunc and the same receiving actor. Otherwise, returns true. In other words,
                                                                                    only one pair can execute the code within an ALICE_PairIsUnoccupied() block. Useful for
                                                                                    creating non-stacking effects.
    ALICE_PairPersist()                                                             Changes the behavior of the current pair so that the interactions don't break when the
                                                                                    two objects leave their interaction range.
    ALICE_PairForget()                                                              Purge pair data, call onDestroy method and reset ALICE_PairIsFirstContact,
                                                                                    ALICE_PairPersist and ALICE_IsUnoccupied functions.


                                  Enum functions return a table with all objects that are in included in filter. If the filter
                                        specifies a player filter, you need to specify the whoFilters player. Example: 
                                  ALICE_EnumObjects({"unit", PLAYER_FILTER_TYPE_ALLY}, Player(0)) will return all units that are
                                                                    owned by an ally of red.

    -----Enum API-----
    ALICE_EnumObjects(filter, whoFilters)
    ALICE_EnumObjectsInRange(x, y, range, filter, whoFilters)
    ALICE_EnumObjectsInRect(minx, miny, maxx, maxy, filter, whoFilters)
    ALICE_ForAllObjectsDo(filter, whoFilters, action)
    ALICE_ForAllObjectsInRangeDo(x, y, range, filter, whoFilters, action)
    ALICE_ForAllObjectsInRectDo(minx, miny, maxx, maxy, filter, whoFilters, action)


    -----Object API------
    ALICE_HasActor(whichObject)                                                     Checks if an actor already exists with the object as its host.
    ALICE_GetActor(whichObject)                                                     Returns the actor stored to a host.
    ALICE_UpdateOwner(whichObject)                                                  Update owner of object if it has changed and reevaluate all pairings.
    ALICE_SetInteractionFunc(whichObject, identifier, func)                         Sets the interactionFunc of the object towards actors with the specified identifier.
    ALICE_Kill(whichObject)                                                         Calls the appropriate function to destroy the object. If the object is a table, the
                                                                                    whichObject:destroy() method will be called.


    -----Identifier API------
    ALICE_AddIdentifier(whichObject, whichIdentifier)                               Add identifier(s) to an object and pair it with all other objects it is now eligible
                                                                                    to be paired with. Slow function!
    ALICE_RemoveIdentifier(whichObject, whichIdentifier)                            Remove identifier(s) from an object and remove all pairings with objects it is no
                                                                                    longer eligible to be paired with. Slow function!
    ALICE_SetIdentifier(whichObject, newIdentifier)                                 Sets the object's identifier to a string or string sequence. Slow function!
    ALICE_HasIdentifier(whichObject, identifier)								    Checks if the object toCheck contains the identifier toMatch. ToMatch can be a string
                                                                                    or a string sequence with optional matching type entry (see under Pairing Logic).
	ALICE_PairsWith(objectA, objectB)                                               Checks if the objectA would pair with the objectB.
    ALICE_GetIdentifier(whichObject)                                                Compiles the identifiers of an object into a table.


    -----Misc API------
    ALICE_MIN_INTERVAL
    ALICE_MAX_INTERVAL


                                    Type "downtherabbithole" in-game to enable debug mode. In debug mode, you can click near an
                                      object to visualize its actor, its interactions, and see all attributes of that actor.

    -----Debug API------
    ALICE_ListGlobals()                                                             List all global actors.
    ALICE_GetFromUnique(number)                                                     Returns the actor with the specified unique number.
    ALICE_Select(whichActor)                                                        Select the specified actor. Pass integer to select by unique number. Requires debug
                                                                                    mode.
    ALICE_PairVisualize(duration)                                                   Create a lightning effect between the objects of the current pair. Useful to check
                                                                                    if objects are interacting as intended. Optional lightning type argument.
    ALICE_VisualizeCells(whichObject, enable)                                       Enable/disable visualization of the cells the object is currently in.
    ALICE_PrintIdentifier(whichObject)                                              Prints out the identifiers of an object.
    ALICE_Pause()                                                                   Pause the entire cycle.
    ALICE_SlowMotion(factor)                                                        Slow down the ALICE cycle by the specified factor. Use ALICE_TimeScale to refer to
                                                                                    the slow motion factor in external functions.
    ALICE_Resume()                                                                  Resume the entire cycle.
    ALICE_Statistics()                                                              Prints out statistics showing which actors are occupying which percentage of the
                                                                                    calculations.
    ALICE_PrintCounts()                                                             Continuously prints the number of actors, pair interactions and cell checks until
                                                                                    disabled.
    ALICE_VisualizeAllCells()                                                       Create lightning effects around all cells.
    ALICE_VisualizeAllActors()                                                      Creates arrows above all non-global actors.


    -----Optimization API-----
    ALICE_SetEveryStep(whichFunction)                                               Defines an interactionFunc as an everyStep function, which requires no return value.
    ALICE_SetMaxRange(whichFunction, maxRange)                                      All pairs using this function will get automatically disabled if both objects are
                                                                                    stationary and their distance is greater than the max range.
    ALICE_SetRadius(whichObject, newRadius)                                         Changes the radius of the object.
    ALICE_SetStationary(whichObject, enable)                                        Sets an object to stationary/not stationary.
    ALICE_SetCellCheckInterval(whichObject, newInterval)                            Changes how often ALICE checks if the object has entered a new cell.
	
    =============================================================================================================================================================
]]

Lua:
--[[
	=============================================================================================================================================================
    Troubleshooting
    =============================================================================================================================================================

    -----downtherabbithole-----
    In-game, you can type "downtherabbithole" to enable debug mode. While in debug mode, you can left-click on objects to select their actor and receive information
    about their attributes. In addition, their current cells and interactions will be visualized. You can combine debug mode with Eikonium's IngameConsole to execute
    code directly on misbehaving actors. Requires "CustomTooltip.toc" and "CustomTooltip.fdf". Set your interactionFuncs to global so that their names can get
    displayed.

    ALICE_GetActor() without an input argument will return the currently selected actor. ALICE_GetFromUnique(number) will return an actor with the specified unique
    number. The number is displayed in the tooltip. Example:
    ALICE_PairsWith(ALICE_GetActor(), ALICE_GetFromUnique(562))

    Global actors cannot be selected by clicking on them, but you can select them with ALICE_ListGlobals() and then ALICE_Select(number), where number is the unique
    number listed for that actor.


    -----Actors not interacting-----
    This can be due to actors not having the intended pairing behavior, because of interaction misses, or because of the an actor's radius being too small.
    
    To check if the pairing behavior is correct, check the actor tooltip in debug mode or do ALICE_PairsWith.

    An interaction miss occurs when two objects get into proximity with each other and should interact, but do not because the interval calculated during their
    last interaction was too long and they're still in the queue. To avoid interaction misses, set the interaction length sufficiently low, especially during
    debugging, even to 0 (the interval will be ceiled to MIN_INTERVAL).

    If you set the radius of an actor too low or forget to change it from its default value, the actors will interact, but only at short range. The radius should be
    set to the maximum interaction distance plus some leeway. The leeway should be equal to the maximum distance the object can travel during two cell checks
    (CELL_CHECK_INTERVAL). Select the actor in debug mode to see the cells the system thinks it's currently in.

    You can add lightnings with ALICE_PairVisualize(duration) in the interaction func. This will create a lightning between the hosts of two interacting actors
    whenever their interaction function is called. This does not work for global actors.


    -----Actors interacting when they shouldn't-----
    If you create a new actor, it will not only pair with other actors based on its own pairing logic, but other actors will also check if the new actor is included
    in their logic. For example, you place a few units on the map, then create an effect that should affect those units. Because there are no other actors on the
    map at that moment, you might not restrict how it pairs in any way. However, that actor will not only pair with those units but will continue to pair with every
    new actor that gets created.


    -----Attempting to do arithmetic on a nil value-----
    This error must likely means that you forgot the interactionFunc's return value.


    -----Thread crash-----
    If your custom code crashes the thread, ALICE will remove the pair that caused the crash and prevent the two actors from pairing again, then on the second crash
    caused by the same actor isolate that actor completely.
	
	
	    -----Never edit actor class fields-----
    ALICE_GetActor gives you access to the interal actor class, but you should still only edit its class fields with the API functions. Editing class fields can lead
    to all kinds of unforeseeable effects.


    -----Forgetting to destroy an actor-----
    Forgetting to destroy an actor when the associated host is destroyed will keep the pairs attached to that actor inside the cycle indefinitely. The pairs will
    continue to be evaluated, clogging up the system. Because the reference to the host is lost, depending on the interactionFunc you wrote, it might crash the
    function.

    You can also put checks into your interaction functions and destroy actors if any of their hosts are removed from the game. Keep in mind that functions still
    needs a return value, even if it's the last time they're executed.

    =============================================================================================================================================================
]]

Lua:
if Debug then Debug.beginFile "ALICE" end
do
    --[[
    =============================================================================================================================================================
                                                        Antares' Limitless Interaction Caller Engine

							    A Lua system to easily create highly performant checks and interactions, between any type of objects.
						
								Requires:
								TotalInitialization			https://www.hiveworkshop.com/threads/total-initialization.317099/

    =============================================================================================================================================================
																	    C O N F I G
    =============================================================================================================================================================
    ]]

    local MAP_CREATORS                  = {"Antares", "WorldEdit"} ---@type string[]
    --Print out warnings, errors, and enable the "downtherabbithole" cheat code for the players with these names. #XXXX not required.

	--Class Interface (important if you want to use tables as hosts, tells ALICE which fields it should look out for to retrieve information from your classes).
    local CLASS_FIELD_X                 = "x"       ---@type string
    local CLASS_FIELD_Y                 = "y"       ---@type string
    local CLASS_FIELD_Z                 = "z"       ---@type string
    --The label of the coordinates of class-type hosts. When an actor is created with a table as a host, ALICE searches for these fields to get the object's
    --position. If the fields are not present, the actor is created as a global actor.
    local CLASS_FIELD_OWNER             = "owner"   ---@type string
    --(Niche) The label of the owner of class-type hosts. If the field is not present, the actor will have no owner.

	--Debugging
    local FREEZE_PROTECTION_LIMIT       = 10000     ---@type integer
    --If a number of pairs greater than this limit is evaluated each step for a longer period of time, something clearly went wrong and the cycle will terminate,
    --preventing the game from freezing. You can then enable debug mode and better gather information about what caused the crash.
	
	--Optimization
	ALICE_MIN_INTERVAL                  = 0.02      ---@type number
	--Minimum interval between interactions in seconds. Sets the time step of the timer. All interaction intervals are an integer multiple of this value.
	ALICE_MAX_INTERVAL                  = 5.0       ---@type number
	--(Not important) Maximum interval between interactions in seconds.
    local NUM_CELLS_X                   = 35        ---@type integer
    local NUM_CELLS_Y                   = 35        ---@type integer
    --The playable map area is partitioned into this many cells. Objects only interact with other objects that share a cell with them. Larger maps require more
    --cells.
    local DEFAULT_CELL_CHECK_INTERVAL   = 0.1       ---@type number
    --How often the system checks if objects left their current cell. Can be overwritten during actor creation.
    local DEFAULT_OBJECT_RADIUS         = 75        ---@type number
    --How large an actor is when it comes to determining in which cells it is in. Can be overwritten during actor creation.
	
    --===========================================================================================================================================================

    local ceil = math.ceil
    local floor = math.floor
    local max = math.max
    local min = math.min
    local sqrt = math.sqrt
    local atan = math.atan

	local MASTER_TIMER = nil                    ---@type timer
	local MAX_STEPS = 0                         ---@type integer
	local CYCLE_LENGTH = 0                      ---@type integer
    local DO_NOT_EVALUATE = 0                   ---@type integer
    local USE_CELLS = NUM_CELLS_X > 0 and NUM_CELLS_Y > 0 ---@type boolean

    local MAP_MIN_X                             ---@type number
    local MAP_MAX_X                             ---@type number
    local MAP_MIN_Y                             ---@type number
    local MAP_MAX_Y                             ---@type number

    local CELL_MIN_X = {}                       ---@type number[]
    local CELL_MIN_Y = {}                       ---@type number[]
    local CELL_MAX_X = {}                       ---@type number[]
    local CELL_MAX_Y = {}                       ---@type number[]

    local CELL_LIST = {}                        ---@type table[]
    for i = 1, NUM_CELLS_X do
        CELL_LIST[i] = {}
    end

	local counter = 0                           ---@type integer
	local currentPair = nil                     ---@type Pair | nil
	local totalActors = 0                       ---@type integer
    local freezeCounter = 0                     ---@type number

	local numPairs = nil                        ---@type integer[]
	local whichPairs = nil                      ---@type table[]
    local numFixedPairs = 0                     ---@type integer
    local fixedPairs = nil                      ---@type Pair[]
	local actorList = nil                       ---@type Actor[]
    local globalActorList = nil                 ---@type Actor[]
    local pairList = {}                         ---@type Pair[]
    local pairingExcluded = {}                  ---@type table[]
    local numCellChecks = nil                   ---@type integer[]
    local cellCheckedActors = {}                ---@type Actor[]
	local unusedPairs = {}                      ---@type Pair[]
    local unusedActors = {}                     ---@type Actor[]
    local unusedTables = {}                     ---@type table[]
    local destroyedActors = {}                  ---@type Actor[]
	local actorOf = {}                          ---@type Actor[]
    local bindChecks = {}                       ---@type Actor[]
    local isEveryStepFunction = {}              ---@type boolean[]
    local functionMaxRange = {}                 ---@type number

    local selfInteractionActor = nil                       ---@type Actor

    local INV_MIN_INTERVAL = 1/ALICE_MIN_INTERVAL - 0.001 ---@type number
    ALICE_TimeScale = 1.0                       ---@type number

    NO_INCOMING_PAIRS = 2147483647              ---@type integer
    NO_OUTGOING_PAIRS = -2147483648             ---@type integer
    MATCHING_TYPE_ANY = 0                       ---@type integer
    MATCHING_TYPE_ALL = 1                       ---@type integer
    MATCHING_TYPE_EXACT = 2                     ---@type integer
    MATCHING_TYPE_EXCLUDE = 3                   ---@type integer
    HIGHEST_MATCHING_TYPE = 3                   ---@type integer
    PLAYER_FILTER_TYPE_ALLY = 4                 ---@type integer
    PLAYER_FILTER_TYPE_ENEMY = 5                ---@type integer
    PLAYER_FILTER_TYPE_OWNER = 6                ---@type integer
    PLAYER_FILTER_TYPE_NOT_OWNER = 7            ---@type integer
    PLAYER_FILTER_TYPE_ANY = 8                  ---@type integer

    local RECOGNIZED_FIELDS = {                 ---@type boolean[]
        pairsWith = true,
        priority = true,
        anchor = true,
        cellCheckInterval = true,
        isStationary = true,
        radius = true,
        destroyOnDeath = true,
        bindToBuff = true,
        bindToOrder = true,
        onActorDestroy = true,
        randomDelay = true,
        isIndestructible = true
    }

    local ActorA = 1                            ---@type integer
    local ActorB = 2                            ---@type integer
    local IndexA = 3                            ---@type integer
    local IndexB = 4                            ---@type integer
    local InteractionFunc = 5                   ---@type integer
    local CurrentPosition = 6                   ---@type integer
    local PositionInStep = 7                    ---@type integer
    local DestructionQueued = 8                 ---@type integer
    local HostA = 9                             ---@type integer
    local HostB = 10                            ---@type integer
    local EveryStep = 11                        ---@type integer
    local UserData = 12                         ---@type integer
    local Persists = 13                        ---@type integer

    local debugModeEnabled = false              ---@type boolean
    local mouseClickTrigger = nil               ---@type trigger
    local cycleSelectTrigger = nil              ---@type trigger
    local selectedActor = nil                   ---@type Actor | nil
    local debugTooltip = nil                    ---@type framehandle
    local debugTooltipText = nil                ---@type framehandle
    local debugTooltipTitle = nil               ---@type framehandle
    local moveableLoc = nil                     ---@type location

    local printCounts = false                   ---@type boolean
    local visualizeAllActors = false            ---@type boolean
    local visualizeAllCells = false             ---@type boolean

    local hash = InitHashtable()

	--===========================================================================================================================================================    

    local function Warning(whichWarning)
        for __, name in ipairs(MAP_CREATORS) do
            if string.find(GetPlayerName(GetLocalPlayer()), name) then
                print(whichWarning)
            end
        end
    end

	--===========================================================================================================================================================
    --Filter Functions
    --===========================================================================================================================================================

    ---@param whichIdentifier string | string[] | nil
    local function Identifier2String(whichIdentifier)
        if whichIdentifier == nil then
            return "()"
        elseif type(whichIdentifier) == "string" then
            return whichIdentifier
        elseif type(whichIdentifier) == "table" then
            local toString = "("
            local i = 1
            for key, __ in pairs(whichIdentifier) do
                if i > 1 then
                    toString = toString .. ", "
                end
                toString = toString .. key
                i = i + 1
            end
            toString = toString .. ")"
            return toString
        end
        return nil
    end

    ---@param toCheck table
    ---@param toMatch table
    ---@return boolean
    local function CheckIdentifierOverlap(toCheck, toMatch)
        local identifier = toCheck.identifier

        if identifier.noIdentifier then
            return false
        end

        local pairsWith = toMatch.pairsWith
        local matchingType = toMatch.matchingType
        if matchingType == MATCHING_TYPE_ANY then
            for key, __ in pairs(pairsWith) do
                if identifier[key] then
                    return true
                end
            end
            return false
        elseif matchingType == MATCHING_TYPE_ALL then
            for key, __ in pairs(pairsWith) do
                if not identifier[key] then
                    return false
                end
            end
            return true
        elseif matchingType == MATCHING_TYPE_EXACT then
            for key, __ in pairs(pairsWith) do
                if not identifier[key] then
                    return false
                end
            end
            for key, __ in pairs(identifier) do
                if not pairsWith[key] then
                    return false
                end
            end
            return true
        elseif matchingType == MATCHING_TYPE_EXCLUDE then
            for key, __ in pairs(pairsWith) do
                if identifier[key] then
                    return false
                end
            end
            return true
        else
            Warning("|cffff0000Warning:|r Invalid matching type specified...")
            return false
        end
    end

    ---@param toMatch any
    ---@param toCheck any
    ---@return boolean
    local function HasExactIdentifier(toMatch, toCheck)
        for key, __ in pairs(toMatch) do
            if not toCheck[key] then
                return false
            end
        end
        for key, __ in pairs(toCheck) do
            if not toMatch[key] then
                return false
            end
        end

        return true
    end

    ---@param whichPlayer player
    ---@param whichFilter integer
    ---@return boolean
    local function IsInPlayerFilter(whichPlayer, whichFilter, whoFilters)
        if whichPlayer == nil then
            return whichFilter == PLAYER_FILTER_TYPE_ANY
        elseif whichFilter == PLAYER_FILTER_TYPE_ANY then
            return true
        elseif type(whichFilter) == "userdata" then
            return whichPlayer == whichFilter
        elseif whoFilters == nil then
            return true
        elseif whichFilter == PLAYER_FILTER_TYPE_ALLY then
            return IsPlayerAlly(whichPlayer, whoFilters)
        elseif whichFilter == PLAYER_FILTER_TYPE_ENEMY then
            return IsPlayerEnemy(whichPlayer, whoFilters)
        elseif whichFilter == PLAYER_FILTER_TYPE_OWNER then
            return whichPlayer == whoFilters
        elseif whichFilter == PLAYER_FILTER_TYPE_NOT_OWNER then
            return whichPlayer ~= whoFilters
        end
        Warning("|cffff0000Warning:|r Unrecognized player filter type...")
        return true
    end

	--===========================================================================================================================================================
    --Utility
    --===========================================================================================================================================================

    local function GetUnusedTable()
        if #unusedTables == 0 then
            return {}
        else
            local returnTable = unusedTables[#unusedTables]
            unusedTables[#unusedTables] = nil
            return returnTable
        end
    end

    local function ReturnTable(whichTable)
        for key, __ in pairs(whichTable) do
            whichTable[key] = nil
        end
        unusedTables[#unusedTables + 1] = whichTable
        setmetatable(whichTable, nil)
    end

    ---@param object agent
    ---@return boolean
    local function IsWidget(object)
        SaveAgentHandle(hash, 0, 0, object)
        return LoadWidgetHandle(hash, 0, 0) ~= nil
    end

    ---@param object agent
    ---@return boolean
    local function IsUnit(object)
        SaveAgentHandle(hash, 0, 0, object)
        return LoadUnitHandle(hash, 0, 0) ~= nil
    end

    ---@param object agent
    ---@return boolean
    local function IsDestructable(object)
        SaveAgentHandle(hash, 0, 0, object)
        return LoadDestructableHandle(hash, 0, 0) ~= nil
    end

    ---@param object agent
    ---@return boolean
    local function IsItem(object)
        SaveAgentHandle(hash, 0, 0, object)
        return LoadItemHandle(hash, 0, 0) ~= nil
    end

    ---@param object agent
    ---@return boolean
    local function IsPlayer(object)
        SaveAgentHandle(hash, 0, 0, object)
        return LoadPlayerHandle(hash, 0, 0) ~= nil
    end

    ---@param object agent
    ---@return boolean
    local function IsEffect(object)
        SaveAgentHandle(hash, 0, 0, object)
        return LoadEffectHandle(hash, 0, 0) ~= nil
    end

    ---@param func function
    ---@return string
    local function FunctionToString(func)
        local string = string.gsub(tostring(func), "function: ", "")
        if string.sub(string,1,1) == "0" then
            return string.sub(string, string.len(string) - 3, string.len(string))
        else
            return string
        end
    end

    ---@param whichFunction function
    local function ExecuteInNewThread(whichFunction)
        --Certain functions must not be called from within interactionFuncs. This postpones their call until the loop is over.
        TimerStart(CreateTimer(), 0.0, false, function()
            whichFunction()
            DestroyTimer(GetExpiredTimer())
        end)
    end

	---@param whichPair Pair
    ---@param duration number
    ---@param lightningType string
	local function VisualizationLightning(whichPair, duration, lightningType)
		local A = whichPair[ActorA]
		local B = whichPair[ActorB]
		
		if A.isGlobal or B.isGlobal then
            return
        end

        local xa = A.getX(A.anchor)
        local ya = A.getY(A.anchor)
        local za = A.getZ(A.anchor, xa, ya)
        local xb = B.getX(B.anchor)
        local yb = B.getY(B.anchor)
        local zb = B.getZ(B.anchor, xb, yb)

        local visLight
        if za and zb then
            visLight = AddLightningEx(lightningType, true, xa, ya, za, xb, yb, zb)
        else
            visLight = AddLightning(lightningType, true, xa, ya, xb, yb)
        end
        TimerStart(CreateTimer(), duration*ALICE_TimeScale, false, function()
            DestroyLightning(visLight)
            DestroyTimer(GetExpiredTimer())
        end)
	end

    ---@param currentPairList table
    ---@return string
    local function GetPairStatistics(currentPairList)
        if #currentPairList == 0 then
            return "\nThere are no pairs currently being evaluated."
        end
        local countActivePairs = 0
        local pairTypes = {}
        
        for __, pair in ipairs(currentPairList) do
            if not pair[DestructionQueued] then
                local idA = pair[ActorA].identifier
                local idB = pair[ActorB].identifier
                local recognized = false
                for __, pairType in ipairs(pairTypes) do
                    if HasExactIdentifier(pairType[1], idA) and HasExactIdentifier(pairType[2], idB) then
                        recognized = true
                        pairType[3] = pairType[3] + 1
                        break
                    end
                end
                if not recognized then
                    table.insert(pairTypes, {idA, idB, 1})
                end
                countActivePairs = countActivePairs + 1
            end
        end
        local statistic = ""

        if countActivePairs == 0 then
            return "\nThere are no pairs currently being evaluated."
        end

        table.sort(pairTypes, function(a, b) return a[3] > b[3] end)

        for __, pairType in ipairs(pairTypes) do
            if floor(100*pairType[3]/countActivePairs) > 1 then
                statistic = statistic .. "\n" .. floor(100*pairType[3]/countActivePairs) .. "\x25 |cffffcc00" .. Identifier2String(pairType[1]) .. "|r, |cffaaaaff" .. Identifier2String(pairType[2]) .. "|r"
            end
        end

        return statistic
    end

    local function WillAlwaysBeOutOfRange(actorA, actorB, maxRange)
        if actorA.isGlobal or actorB.isGlobal then
            return false
        end
        if not actorA.isStationary or not actorB.isStationary then
            return false
        end

        local dx = actorA.getX(actorA.anchor) - actorB.getX(actorB.anchor)
        local dy = actorA.getY(actorA.anchor) - actorB.getY(actorB.anchor)

        return dx*dx + dy*dy > maxRange*maxRange
    end

    local function DoNothing(__,__)
        --Dummy function if correct interactionFunc could not be determined.
        return ALICE_MAX_INTERVAL
    end

	--===========================================================================================================================================================
    --Coordinate funcs
    --===========================================================================================================================================================

    ---@param x number
    ---@param y number
    ---@return number
    local function GetLocZ(x, y)
        MoveLocation(moveableLoc, x, y)
        return GetLocationZ(moveableLoc)
    end

    ---@param host table
    ---@return number
    local function GetClassX(host)
        return host[CLASS_FIELD_X]
    end

    ---@param host table
    ---@return number
    local function GetClassY(host)
        return host[CLASS_FIELD_Y]
    end

    ---@param host table
    ---@return number
    local function GetClassZ(host, __, __)
        return host[CLASS_FIELD_Z]
    end

    ---@param host unit
    ---@param x number
    ---@param y number
    ---@return number
    local function GetUnitZ(host, x, y)
        return GetLocZ(x, y) + GetUnitFlyHeight(host)
    end

    ---@param x number
    ---@param y number
    ---@return number
    local function GetItemZ(__, x, y)
        return GetLocZ(x, y)
    end

    --Enables actors as hosts (for whatever reason that is necessary).
    ---@param actor Actor
    ---@return function
    local function GetActorX(actor)
        return actor.getX
    end

    ---@param actor Actor
    ---@return function
    local function GetActorY(actor)
        return actor.getY
    end

    ---@param actor Actor
    ---@return function
    local function GetActorZ(actor)
        return actor.getZ
    end

	--===========================================================================================================================================================
    --Pair Class
    --===========================================================================================================================================================

	---@class Pair
	local Pair = {
		[ActorA] = nil,                   ---@type Actor
		[ActorB] = nil,                   ---@type Actor
		[IndexA] = 0,                     ---@type integer
		[IndexB] = 0,                     ---@type integer
		[InteractionFunc] = nil,          ---@type function
		[CurrentPosition] = 0,            ---@type integer
		[PositionInStep] = 0,             ---@type integer
        [DestructionQueued] = nil,        ---@type boolean
        [HostA] = nil,                    ---@type any
        [HostB] = nil,                    ---@type any
        [EveryStep] = nil,                ---@type boolean
        [UserData] = nil,                 ---@type table
        [Persists] = nil,                 ---@type boolean
	}

    --Removed class fields and integer keys to reduce memory usage.

    local function PairUserDataOnDestroy(self)
        if self[UserData].onDestroy ~= nil then
            self[UserData].onDestroy(self[HostA], self[HostB], self[UserData])
        end
        ReturnTable(self[UserData])
        self[UserData] = nil
    end

	---@param actorA Actor
	---@param actorB Actor
	---@param interactionFunc function
	local function CreatePair(actorA, actorB, interactionFunc)

        if interactionFunc == nil or actorA.isIsolated or actorB.isIsolated or actorA.host == actorB.host or actorA.anchor == actorB.anchor or (functionMaxRange[interactionFunc] ~= nil and WillAlwaysBeOutOfRange(actorA, actorB, functionMaxRange[interactionFunc])) then
            pairingExcluded[actorA][actorB] = true
            pairingExcluded[actorB][actorA] = true
            return
        end

		local self ---@type Pair
		if #unusedPairs == 0 then
			self = {} ---@type Pair
		else
			self = unusedPairs[#unusedPairs]
			unusedPairs[#unusedPairs] = nil
		end

		self[ActorA] = actorA
		self[ActorB] = actorB
        self[HostA] = actorA.host
        if actorB == selfInteractionActor then
            self[HostB] = actorA.host
        else
            self[HostB] = actorB.host
        end
		self[InteractionFunc] = interactionFunc
		self[IndexA] = #actorA.pairs + 1
		self[IndexB] = #actorB.pairs + 1
		actorA.pairs[self[IndexA]] = self
		actorB.pairs[self[IndexB]] = self
        self[DestructionQueued] = false
        self[UserData] = nil

        actorA.isPairedWith[actorB] = true
        actorB.isPairedWith[actorA] = true
        pairList[actorA][actorB] = self

        if isEveryStepFunction[interactionFunc] then
            numFixedPairs = numFixedPairs + 1
            fixedPairs[numFixedPairs] = self
            self[PositionInStep] = numFixedPairs
            self[EveryStep] = true
        else
            local firstStep
            if actorA.randomDelay == nil then
                firstStep = counter + 1
            else
                firstStep = counter + min(MAX_STEPS, ceil(actorA.randomDelay*math.random()*INV_MIN_INTERVAL))
            end
            if firstStep > CYCLE_LENGTH then
                firstStep = firstStep - CYCLE_LENGTH
            end
            numPairs[firstStep] = numPairs[firstStep] + 1
            whichPairs[firstStep][numPairs[firstStep]] = self
            self[CurrentPosition] = firstStep
            self[PositionInStep] = numPairs[firstStep]
            self[EveryStep] = nil
        end
	end

	local function DestroyPair(self)
        if self[EveryStep] then
            fixedPairs[numFixedPairs][PositionInStep] = self[PositionInStep]
            fixedPairs[self[PositionInStep]] = fixedPairs[numFixedPairs]
            numFixedPairs = numFixedPairs - 1
        else
            local currentPosition = self[CurrentPosition] ---@type integer
            local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
            whichPairs[currentPosition][self[PositionInStep]] = pairAtHighestPosition
            pairAtHighestPosition[PositionInStep] = self[PositionInStep]
            numPairs[currentPosition] = numPairs[currentPosition] - 1
        end

        local A = self[ActorA] ---@type Actor
        local B = self[ActorB] ---@type Actor

        if self[UserData] ~= nil then
            PairUserDataOnDestroy(self)
        end

        if self[ActorB][self[InteractionFunc]] == self then
            self[ActorB][self[InteractionFunc]] = nil
        end

        local pairAtHighestPosition = B.pairs[#B.pairs]
        if pairAtHighestPosition[ActorB] == B then
            pairAtHighestPosition[IndexB] = self[IndexB]
        else
            pairAtHighestPosition[IndexA] = self[IndexB]
        end
        B.pairs[self[IndexB]] = pairAtHighestPosition
        B.pairs[#B.pairs] = nil
        pairAtHighestPosition = A.pairs[#A.pairs]
        if pairAtHighestPosition[ActorA] == A then
            pairAtHighestPosition[IndexA] = self[IndexA]
        else
            pairAtHighestPosition[IndexB] = self[IndexA]
        end
        A.pairs[self[IndexA]] = pairAtHighestPosition
        A.pairs[#A.pairs] = nil
		
		unusedPairs[#unusedPairs + 1] = self

        A.isPairedWith[B] = nil
        B.isPairedWith[A] = nil
        pairList[A][B] = nil
        pairList[B][A] = nil
	end

    ---@param actorA Actor
    ---@param actorB Actor
    ---@param tryAgain boolean
    ---@return Actor, Actor, function | nil
    local function GetPairParameters(actorA, actorB, tryAgain)
        --First tries to find the interactionFunc from the actor with the higher priority,
        --then, if unsuccessful, calls itself with reversed argument order.
        --Returns interactionFunc and order of actors.
        local interactionFunc = actorA.interactionFunc
        if type(interactionFunc) == "table" then
            local foundFunc = nil
            for keyword, func in pairs(interactionFunc) do
                if actorB.identifier[keyword] then
                    if foundFunc == nil then
                        foundFunc = func
                    else
                        Warning("|cffff0000Warning:|r Interaction func ambiguous for actors with " .. Identifier2String(actorA.identifier) .. " and actors with identifier " .. Identifier2String(actorB.identifier) .. " ...")
                        return actorA, actorB, DoNothing
                    end
                end
            end
            if foundFunc ~= nil then
                return actorA, actorB, foundFunc
            end
            if interactionFunc.other ~= nil then
                return actorA, actorB, interactionFunc.other
            elseif tryAgain then
                return GetPairParameters(actorB, actorA, false)
            else
                return actorA, actorB, nil
            end
        else
            ---@diagnostic disable-next-line: return-type-mismatch
            if interactionFunc ~= nil then
                return actorA, actorB, interactionFunc
            elseif tryAgain then
                return GetPairParameters(actorB, actorA, false)
            else
                return actorA, actorB, nil
            end
        end
    end

    local pairingEvaluator = {}
    pairingEvaluator["boolean"] = function(__, actorB) return actorB.pairsWith end
    pairingEvaluator["string"] = function(actorA, actorB) return actorA.identifier[actorB.pairsWith] end
    pairingEvaluator["table"] = function(actorA, actorB) return CheckIdentifierOverlap(actorA, actorB) and IsInPlayerFilter(actorA.owner, actorB.playerFilterType, actorB.owner) end
    pairingEvaluator["function"] = function(actorA, actorB) return actorB.pairsWith(actorA.host) end
    pairingEvaluator["nil"] = function(__, __) return false end

	--===========================================================================================================================================================
    --Actor Class
    --===========================================================================================================================================================

	---@class Actor
	local Actor = {
		host = nil,                     ---@type any
        anchor = nil,                   ---@type any
        owner = nil,                    ---@type player | nil
        identifier = nil,               ---@type table
        priority = nil,                 ---@type integer
        pairsWith = nil,                ---@type string | table | function | boolean
        matchingType = nil,             ---@type integer | nil
        playerFilterType = nil,         ---@type integer | player | nil
        evaluator = nil,                ---@type function
        interactionFunc = nil,          ---@type function | table | nil
		pairs = nil,                    ---@type Pair[]
		index = nil,                    ---@type integer
		alreadyDestroyed = nil,         ---@type boolean
        isStationary = nil,             ---@type boolean | nil
		unique = nil,                   ---@type integer
        isPairedWith = nil,             ---@type boolean[]
        visualizer = nil,               ---@type effect
        everyStep = nil,                ---@type boolean | boolean[]
        bindToBuff = nil,               ---@type string | nil
        unit = nil,                     ---@type unit | nil
        waitingForBuff = nil,           ---@type boolean | nil
        bindToOrder = nil,              ---@type integer | nil
        onDestroy = nil,                ---@type function | nil
        causedCrash = nil,              ---@type boolean | nil
        isIsolated = nil,               ---@type boolean
        randomDelay = nil,              ---@type number | nil
        isIndestructible = nil,         ---@type boolean

        --Coordinates:
        getX = nil,                     ---@type function
        getY = nil,                     ---@type function
        getZ = nil,                     ---@type function

        --Cell interaction:
        isGlobal = nil,                 ---@type boolean
        radius = nil,                   ---@type number
        minX = nil,                     ---@type integer
        minY = nil,                     ---@type integer
        maxX = nil,                     ---@type integer
        maxY = nil,                     ---@type integer
        cellCheckInterval = nil,        ---@type integer
        nextCellCheck = nil,            ---@type integer
        positionInCellCheck = nil,      ---@type integer
        cells = nil,                    ---@type Cell[]
        cellIndex = nil,                ---@type integer[]
        cellsVisualized = nil,          ---@type boolean
        cellVisualizers = nil,          ---@type lightning[]
	}

    ActorMetatable = {__index = Actor} --Global because fuck no forward declaration.

    ---@param host any
	---@param identifier string | string[] | nil
	---@param interactionFunc function | table | nil
	---@param pairsWith function | string | table | nil
	---@param priority number | nil
    ---@param radius number | nil
    ---@param cellCheckInterval number | nil
    ---@param isStationary boolean | nil
    ---@param destroyOnDeath boolean | nil
    ---@param anchor any
    ---@param bindToBuff string | nil
    ---@param bindToOrder string | nil
    ---@param onActorDestroy function | nil
    ---@param randomDelay number | nil
    ---@param isIndestructible boolean | nil
	---@return Actor | nil
	function Actor.create(host, identifier, interactionFunc, pairsWith, priority, radius, cellCheckInterval, isStationary, destroyOnDeath, anchor, bindToBuff, bindToOrder, onActorDestroy, randomDelay, isIndestructible)

		if type(identifier) ~= "string" and type(identifier) ~= "table" and identifier ~= nil then
			Warning("|cffff0000Error:|r Invalid actor identifier...")
			return nil
		end

        local self ---@type Actor
        if #unusedActors == 0 then
            self = {}
		    setmetatable(self, ActorMetatable)
            self.identifier = {}
            self.isPairedWith = {}
            self.pairs = {}
            self.cells = {}
            self.cellIndex = {}
            pairList[self] = {}
            pairingExcluded[self] = {}
        else
            self = unusedActors[#unusedActors]
            unusedActors[#unusedActors] = nil
        end

		totalActors = totalActors + 1
		self.unique = totalActors
        self.causedCrash = nil

        if type(identifier) == "string" then
            self.identifier[identifier] = true
        elseif type(identifier) == "table" then
            for i = 1, #identifier do
                self.identifier[identifier[i]] = true
            end
        elseif identifier == nil then
            self.identifier.noIdentifier = true
        end

        if type(interactionFunc) == "table" then
            self.interactionFunc = GetUnusedTable()
            for keyword, func in pairs(interactionFunc) do
                self.interactionFunc[keyword] = func
            end
        else
            self.interactionFunc = interactionFunc
        end

        self:initPairingLogic(pairsWith)

        self.evaluator = pairingEvaluator[type(self.pairsWith)]

		self.host = host
        self.anchor = anchor or host
        self:setOwner()
        self.isStationary = isStationary --overwritten if host is destructable

        if self.anchor ~= nil then
            self:setCoordinateFuncs()
		end

        self.isGlobal = self.getX == nil

        priority = priority or 0
        self.priority = priority
        self.randomDelay = randomDelay

        local actorPairs
        local selfPairs
        for __, actor in ipairs(globalActorList) do
            if self.anchor ~= actor.anchor then
                selfPairs = actor.priority ~= NO_INCOMING_PAIRS and self.evaluator(actor, self)
                actorPairs = self.priority ~= NO_INCOMING_PAIRS and actor.evaluator(self, actor)
                if selfPairs and actorPairs then
                    if self.priority < actor.priority then
                        CreatePair(GetPairParameters(actor, self, true))
                    else
                        CreatePair(GetPairParameters(self, actor, true))
                    end
                elseif selfPairs then
                    CreatePair(GetPairParameters(self, actor, false))
                elseif actorPairs then
                    CreatePair(GetPairParameters(actor, self, false))
                end
            end
        end

        if type(self.interactionFunc) == "table" and self.interactionFunc.self ~= nil then
            if type(self.interactionFunc.self) == "table" then
                for __, func in ipairs(self.interactionFunc.self) do
                    CreatePair(self, selfInteractionActor, func)
                end
            else
                CreatePair(self, selfInteractionActor, self.interactionFunc.self)
            end
        end

        if not self.isGlobal then
            self.radius = radius or DEFAULT_OBJECT_RADIUS
            self:initCells()

            if self.isStationary then
                self.nextCellCheck = DO_NOT_EVALUATE
            else
                self:initCellChecks(cellCheckInterval or DEFAULT_CELL_CHECK_INTERVAL)
            end
        end

        if self.host ~= nil then
            self:createReference()
        end

        self.isIndestructible = isIndestructible == true

        if destroyOnDeath then
            self:createOnDeathTrigger()
        end

        if bindToBuff ~= nil then
            self:createBinds(bindToBuff, nil)
        elseif bindToOrder ~= nil then
            self:createBinds(nil, bindToOrder)
        end

        self.onDestroy = onActorDestroy

        if visualizeAllActors and not self.isGlobal then
            self:createVisualizer()
        end

        self.alreadyDestroyed = false

        actorList[#actorList + 1] = self
        if self.isGlobal then
            globalActorList[#globalActorList + 1] = self
        end
        self.index = #actorList

		return self
	end
	
	function Actor:destroy()
		if self == nil or self.alreadyDestroyed then
			return
		end

		self.alreadyDestroyed = true
        destroyedActors[#destroyedActors + 1] = self

        if self.onDestroy ~= nil then
            self.onDestroy(self.host)
            self.onDestroy = nil
        end

        actorList[#actorList].index = self.index
        actorList[self.index] = actorList[#actorList]
        actorList[#actorList] = nil
        if self.isGlobal then
            for i, actor in ipairs(globalActorList) do
                if self == actor then
                    globalActorList[i] = globalActorList[#globalActorList]
                    globalActorList[#globalActorList] = nil
                    break
                end
            end
        end

        if type(self.pairsWith) == "table" then
            ReturnTable(self.pairsWith)
        end

        if type(self.interactionFunc) == "table" then
            ReturnTable(self.interactionFunc)
        end

        for key, __ in pairs(self.identifier) do
            self.identifier[key] = nil
        end

        for __, pair in ipairs(self.pairs) do
            pair[DestructionQueued] = true
        end

        for key, __ in pairs(pairingExcluded[self]) do
            pairingExcluded[self][key] = nil
            pairingExcluded[key][self] = nil
        end

        if not self.isGlobal then
            if not self.isStationary then
                local nextCheck = self.nextCellCheck
                local actorAtHighestPosition = cellCheckedActors[nextCheck][numCellChecks[nextCheck]]
                actorAtHighestPosition.positionInCellCheck = self.positionInCellCheck
                cellCheckedActors[nextCheck][self.positionInCellCheck] = actorAtHighestPosition
                numCellChecks[nextCheck] = numCellChecks[nextCheck] - 1
            end
            for i = #self.cells, 1, -1 do
                self.cells[i]:remove(self)
            end
        end

        if self.host ~= nil then
            self:removeReference(self.host)
            if self.host ~= self.anchor then
                self:removeReference(self.anchor)
            end
            self.host = nil
            self.anchor = nil
        end

        if type(self.everyStep) == "table" then
            ReturnTable(self.everyStep)
        end
        self.everyStep = nil

        if self.cellsVisualized then
            for __, bolt in ipairs(self.cellVisualizers) do
                DestroyLightning(bolt)
            end
            self.cellsVisualized = nil
        end

        if visualizeAllActors and not self.isGlobal then
            DestroyEffect(self.visualizer)
        end

        if self == selectedActor then
            DestroyEffect(self.visualizer)
            selectedActor = nil
        end
        self.getX = nil
        self.getY = nil
        self.getZ = nil
        self.bindToBuff = nil
        self.bindToOrder = nil
        self.isIsolated = nil
	end

    function Actor:createReference()
        if actorOf[self.host] == nil then
            actorOf[self.host] = self
        elseif getmetatable(actorOf[self.host]) == ActorMetatable then
            actorOf[self.host] = {actorOf[self.host], self}
        else
            table.insert(actorOf[self.host], self)
        end
        if self.host ~= self.anchor then
            if actorOf[self.anchor] == nil then
                actorOf[self.anchor] = self
            elseif getmetatable(actorOf[self.anchor]) == ActorMetatable then
                actorOf[self.anchor] = {actorOf[self.anchor], self}
            else
                table.insert(actorOf[self.anchor], self)
            end
        end
    end

    function Actor:removeReference(object)
        if getmetatable(actorOf[object]) == ActorMetatable then
            actorOf[object] = nil
        else
            for j, v in ipairs(actorOf[object]) do
                if self == v then
                    table.remove(actorOf[object], j)
                end
            end
            if #actorOf[object] == 1 then
                actorOf[object] = actorOf[object][1]
            end
        end
    end

    function Actor:setCoordinateFuncs()
        if type(self.anchor) == "userdata" then
            if IsUnit(self.anchor) then
                self.getX = GetUnitX
                self.getY = GetUnitY
                self.getZ = GetUnitZ
            elseif IsItem(self.anchor) then
                self.getX = GetItemX
                self.getY = GetItemY
                self.getZ = GetItemZ
            elseif IsDestructable(self.anchor) then
                local x = GetDestructableX(self.anchor)
                local y = GetDestructableY(self.anchor)
                local z = GetLocZ(x, y)
                self.getX = function() return x end
                self.getY = function() return y end
                self.getZ = function() return z end
                self.isStationary = true
            end
        elseif type(self.anchor) == "table" then
            if self.anchor[CLASS_FIELD_X] ~= nil and self.anchor[CLASS_FIELD_Y] ~= nil then
                self.getX = GetClassX
                self.getY = GetClassY
                self.getZ = GetClassZ
            elseif getmetatable(self.anchor) == ActorMetatable then
                self.getX = GetActorX(self.anchor)
                self.getY = GetActorY(self.anchor)
                self.getZ = GetActorZ(self.anchor)
            end
        end
    end

    function Actor:setOwner()
        if type(self.host) == "userdata" then
            if IsUnit(self.host) then
                self.owner = GetOwningPlayer(self.host)
            elseif IsPlayer(self.host) then
                self.owner = self.host
            else
                self.owner = nil
            end
        elseif type(self.host) == "table" then
            self.owner = self.host[CLASS_FIELD_OWNER]
        end
    end

    function Actor:createOnDeathTrigger()
        if IsWidget(self.host) then
            local trig = CreateTrigger()
            TriggerRegisterDeathEvent(trig, self.host)
            TriggerAddAction(trig, function() self:destroy() end)
        elseif IsWidget(self.anchor) then
            local trig = CreateTrigger()
            TriggerRegisterDeathEvent(trig, self.anchor)
            TriggerAddAction(trig, function() self:destroy() end)
        else
            Warning("|cffff0000Warning:|r Cannot use automatic onDeath clean-up with non-widget hosts...")
        end
    end

    ---@param pairsWith table
    function Actor:initPairingLogic(pairsWith)
        --Transforms pairingLogic into form that can be used by internal functions.
        --From string sequence to boolean table with string keys.
        
        if type(pairsWith) == "table" then
            --Determine pairing logic from pairsWith.
            self.pairsWith = GetUnusedTable()

            local j = 1
            while type(pairsWith[j]) == "string" do
                self.pairsWith[pairsWith[j]] = true
                j = j + 1
            end

            for i = j, #pairsWith do
                if type(pairsWith[i]) == "number" then
                    if pairsWith[i] <= HIGHEST_MATCHING_TYPE then
                        self.matchingType = pairsWith[i]
                    else
                        self.playerFilterType = pairsWith[i]
                    end
                elseif type(pairsWith[i]) == "userdata" then
                    self.playerFilterType = pairsWith[i]
                end
            end
            if self.matchingType == nil then
                self.matchingType = MATCHING_TYPE_ANY
            end
            if self.playerFilterType == nil then
                self.playerFilterType = PLAYER_FILTER_TYPE_ANY
            end

            if self.interactionFunc == nil then
                Warning("|cffff0000Warning:|r No interaction func set for actor with identifiers " .. Identifier2String(self.identifier) .. "...")
            end
        elseif pairsWith == nil then
            --Determine pairing logic from interactionFunc.
            if type(self.interactionFunc) == "table" then
                if self.interactionFunc.other then
                    self.pairsWith = true
                else
                    self.pairsWith = GetUnusedTable()
                    for key, __ in pairs(self.interactionFunc) do
                        self.pairsWith[key] = true
                    end
                    self.matchingType = MATCHING_TYPE_ANY
                    self.playerFilterType = PLAYER_FILTER_TYPE_ANY
                end
            else
                self.pairsWith = self.interactionFunc ~= nil
            end
        else
            self.pairsWith = pairsWith
        end
    end

    function Actor:initCells()
        local x = self.getX(self.anchor)
        local y = self.getY(self.anchor)
        self.minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x - self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        self.minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y - self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
        self.maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x + self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        self.maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y + self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
        for X = self.minX, self.maxX do
            for Y = self.minY, self.maxY do
                CELL_LIST[X][Y]:enter(self)
            end
        end
    end

    ---@param interval number
    function Actor:initCellChecks(interval)
        self.cellCheckInterval = min(MAX_STEPS, max(1, ceil((interval - 0.001)*INV_MIN_INTERVAL)))
        local nextStep = counter + self.cellCheckInterval
        if nextStep > CYCLE_LENGTH then
            nextStep = nextStep - CYCLE_LENGTH
        end
        numCellChecks[nextStep] = numCellChecks[nextStep] + 1
        cellCheckedActors[nextStep][numCellChecks[nextStep]] = self
        self.nextCellCheck = nextStep
        self.positionInCellCheck = numCellChecks[nextStep]
    end
    
    ---@param x number
    ---@param y number
    function Actor:teleport(x, y)
        --Actor has left current cell but is not in adjacent cell.
        for i = #self.cells, 1, -1 do
            self.cells[i]:leave(self)
        end
        self.minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x - self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        self.minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y - self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
        self.maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x + self.radius - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        self.maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y + self.radius - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
        for X = self.minX, self.maxX do
            for Y = self.minY, self.maxY do
                CELL_LIST[X][Y]:enter(self, true)
            end
        end
    end

    ---@param actor Actor
    ---@return boolean
    function Actor:sharesCellWith(actor)
        for __, cellA in ipairs(self.cells) do
            for __, cellB in ipairs(actor.cells) do
                if cellA == cellB then
                    return true
                end
            end
        end
        return false
    end

    ---@param bindToBuff string | nil
    ---@param bindToOrder string | nil
    function Actor:createBinds(bindToBuff, bindToOrder)
        if bindToBuff ~= nil then
            self.bindToBuff = bindToBuff
            self.waitingForBuff = true
            table.insert(bindChecks, self)
        elseif bindToOrder ~= nil then
            self.bindToOrder = OrderId(bindToOrder)
            table.insert(bindChecks, self)
        end
        if type(self.host) == "userdata" and IsUnit(self.host) then
            self.unit = self.host
        elseif type(self.anchor) == "userdata" and IsUnit(self.anchor) then
            self.unit = self.anchor
        else
            Warning("|cffff0000Warning:|r Attempted to bind actor with identifier " .. Identifier2String(self.identifier) .. " to a buff or order, but that actor doesn't have a unit host...")
        end
    end

    function Actor:reevaluatePairings()
        local selfPairs
        local actorPairs
        for __, actor in ipairs(actorList) do
            if self ~= actor then
                if not self.isPairedWith[actor] then
                    pairingExcluded[self][actor] = nil
                    pairingExcluded[actor][self] = nil
                    if (self.isGlobal or actor.isGlobal or self:sharesCellWith(actor)) then
                        if actor.anchor == self.anchor then
                            selfPairs = actor.priority ~= NO_INCOMING_PAIRS and self.evaluator(actor, self)
                            actorPairs = self.priority ~= NO_INCOMING_PAIRS and actor.evaluator(self, actor)
                            if selfPairs and actorPairs then
                                if self.priority < actor.priority then
                                    CreatePair(GetPairParameters(actor, self, true))
                                else
                                    CreatePair(GetPairParameters(self, actor, true))
                                end
                            elseif selfPairs then
                                CreatePair(GetPairParameters(self, actor, false))
                            elseif actorPairs then
                                CreatePair(GetPairParameters(actor, self, false))
                            end
                        end
                    end
                elseif not ((actor.priority ~= NO_INCOMING_PAIRS and self.evaluator(actor, self)) or (self.priority ~= NO_INCOMING_PAIRS and actor.evaluator(self, actor))) then
                    DestroyPair(pairList[self][actor] or pairList[actor][self])
                end
            end
        end
    end

    ---@param newIdentifier string | string[]
    function Actor:addIdentifier(newIdentifier)
        if type(newIdentifier) == "string" then
            self.identifier[newIdentifier] = true
        else
            for __, keyword in ipairs(newIdentifier) do
                self.identifier[keyword] = true
            end
        end

        for i = 1, #self.identifier do
            for j = i + 1, #self.identifier do
                if self.identifier[i] == self.identifier[j] then
                    table.remove(self.identifier, j)
                end
            end
        end

        ExecuteInNewThread(function() self:reevaluatePairings() end)
    end

    ---@param toRemove string | string[]
    function Actor:removeIdentifier(toRemove)
        if type(toRemove) == "string" then
            self.identifier[toRemove] = nil
        else
            for __, keyword in ipairs(toRemove) do
                self.identifier[keyword] = nil
            end
        end

        ExecuteInNewThread(function() self:reevaluatePairings() end)
    end

    ---@param newIdentifier string | string[] | nil
    function Actor:setIdentifier(newIdentifier)
        for keyword, __ in pairs(self.identifier) do
            self.identifier[keyword] = nil
        end
        for __, keyword in ipairs(newIdentifier) do
            self.identifier[keyword] = true
        end

        ExecuteInNewThread(function() self:reevaluatePairings() end)
    end

    ---@param enable boolean
    function Actor:visualizeCells(enable)
        if enable == self.cellsVisualized then
            return
        end
        if self.cellsVisualized then
            self.cellsVisualized = false
            DestroyLightning(self.cellVisualizers[1])
            DestroyLightning(self.cellVisualizers[2])
            DestroyLightning(self.cellVisualizers[3])
            DestroyLightning(self.cellVisualizers[4])
        else
            self.cellVisualizers = {}
            self.cellsVisualized = true
            local minx = CELL_MIN_X[self.minX]
            local miny = CELL_MIN_Y[self.minY]
            local maxx = CELL_MAX_X[self.maxX]
            local maxy = CELL_MAX_Y[self.maxY]
            self.cellVisualizers[1] = AddLightning("LEAS", false, maxx, miny, maxx, maxy)
            self.cellVisualizers[2] = AddLightning("LEAS", false, maxx, maxy, minx, maxy)
            self.cellVisualizers[3] = AddLightning("LEAS", false, minx, maxy, minx, miny)
            self.cellVisualizers[4] = AddLightning("LEAS", false, minx, miny, maxx, miny)
        end
    end

    function Actor:redrawCellVisualizers()
        local minx = CELL_MIN_X[self.minX]
        local miny = CELL_MIN_Y[self.minY]
        local maxx = CELL_MAX_X[self.maxX]
        local maxy = CELL_MAX_Y[self.maxY]
        MoveLightning(self.cellVisualizers[1], false, maxx, miny, maxx, maxy)
        MoveLightning(self.cellVisualizers[2], false, maxx, maxy, minx, maxy)
        MoveLightning(self.cellVisualizers[3], false, minx, maxy, minx, miny)
        MoveLightning(self.cellVisualizers[4], false, minx, miny, maxx, miny)
    end

    function Actor:isolate()
        for __, actor in ipairs(actorList) do
            if pairList[actor][self] or pairList[self][actor] then
                DestroyPair(pairList[actor][self] or pairList[self][actor])
            end
        end
        self.isIsolated = true
        for __, actor in ipairs(actorList) do
            pairingExcluded[self][actor] = true
            pairingExcluded[actor][self] = true
        end
    end

    function Actor:deselect()
        selectedActor = nil
        if not visualizeAllActors then
            DestroyEffect(self.visualizer)
        end
        ALICE_VisualizeCells(self, false)
        BlzFrameSetVisible(debugTooltip, false)
    end

    ---@return string, string
    function Actor:getDescription()

        local description = "|cffffcc00Host:|r " .. tostring(self.host)
        if self.anchor ~= self.host then
            description = description .. "\n|cffffcc00Anchor:|r " .. tostring(self.anchor)
        end

        description = description .. "\n|cffffcc00InteractionFuncs:|r "
        if type(self.interactionFunc) == "table" then
            local first = true
            for key, func in pairs(self.interactionFunc) do
                if key ~= "self" then
                    if not first then
                        description = description .. ", "
                    end
                    description = description .. key .. " - " .. FunctionToString(func)
                    first = false
                end
            end
            if self.interactionFunc.self ~= nil then
                description = description .. "\n|cffffcc00Self-Interaction:|r "
                if type(self.interactionFunc.self) == "function" then
                    description = description .. FunctionToString(self.interactionFunc.self)
                elseif type(self.interactionFunc.self) == "table" then
                    local first = true
                    for __, func in ipairs(self.interactionFunc.self) do
                        if not first then
                            description = description .. ", "
                        end
                        description = description .. FunctionToString(func)
                        first = false
                    end
                else
                    description = description .. "|cffff0000Invalid type!|r"
                end
            end
        else
            description = description .. tostring(self.interactionFunc)
        end

        description = description .. "\n|cffffcc00Pairs with:|r "
        if type(self.pairsWith) == "table" then
            local count = 0
            for key, __ in pairs(self.pairsWith) do
                if key ~= "self" then
                    count = count + 1
                end
            end
            if count > 1 then
                if self.matchingType == MATCHING_TYPE_ANY then
                    description = description .. "Any of "
                elseif self.matchingType == MATCHING_TYPE_ALL then
                    description = description .. "All of "
                elseif self.matchingType == MATCHING_TYPE_EXACT then
                    description = description .. "Exactly "
                elseif self.matchingType == MATCHING_TYPE_EXCLUDE then
                    description = description .. "None of "
                end
            end
            local first = true
            for key, __ in pairs(self.pairsWith) do
                if key ~= "self" then
                    if not first then
                        description = description .. ", "
                    end
                    description = description .. key
                    if type(self.interactionFunc) == "table" and self.interactionFunc.other == nil and self.interactionFunc[key] == nil then
                        description = description .. " |cffaaaaaa(interactionFunc must exist)|r"
                    end
                    first = false
                end
            end
            if type(self.playerFilterType) == "userdata" then
                description = description .. " (all actors owned by player " .. GetPlayerId(self.playerFilterType) + 1 .. ")"
            elseif self.playerFilterType ~= PLAYER_FILTER_TYPE_ANY then
                if self.owner == nil then
                    description = description .. " |cffff0000(player filter requires owner!)|r"
                elseif self.playerFilterType == PLAYER_FILTER_TYPE_ENEMY then
                    description = description .. " (actor must be enemy)"
                elseif self.playerFilterType == PLAYER_FILTER_TYPE_OWNER then
                    description = description .. " (actor must have the same owner)"
                elseif self.playerFilterType == PLAYER_FILTER_TYPE_NOT_OWNER then
                    description = description .. " (actor must have a different owner)"
                end
            end
        elseif self.pairsWith == false or self.pairsWith == nil then
            description = description .. "Nothing"
        elseif self.pairsWith == true then
            if self.playerFilterType == PLAYER_FILTER_TYPE_ANY then
                description = description .. "Everything"
            elseif self.playerFilterType == PLAYER_FILTER_TYPE_ALLY then
                description = description .. "All allied actors."
            elseif self.playerFilterType == PLAYER_FILTER_TYPE_ENEMY then
                description = description .. "All enemy actors."
            elseif self.playerFilterType == PLAYER_FILTER_TYPE_OWNER then
                description = description .. "All actors with the same owner."
            elseif self.playerFilterType == PLAYER_FILTER_TYPE_NOT_OWNER then
                description = description .. "All actors with a different owner."
            elseif type(self.playerFilterType) == "userdata" then
                description = description .. "All actors owned by player " .. GetPlayerId(self.playerFilterType) + 1
            end
        elseif type(self.pairsWith) == "function" then
            description = description .. tostring(self.pairsWith)
        end

        if self.priority ~= 0 then
            description = description .. "\n|cffffcc00Priority:|r " .. self.priority
        end

        if self.cellCheckInterval ~= nil then
            if math.abs(self.cellCheckInterval*ALICE_MIN_INTERVAL - DEFAULT_CELL_CHECK_INTERVAL) > 0.001 then
                description = description .. "\n|cffffcc00Cell Check Interval:|r " .. self.cellCheckInterval*ALICE_MIN_INTERVAL
            end
        elseif not self.isGlobal then
            description = description .. "\n|cffffcc00Stationary:|r true"
        end

        if self.radius ~= nil and self.radius ~= DEFAULT_OBJECT_RADIUS then
            description = description .. "\n|cffffcc00Radius:|r " .. self.radius
        end

        if self.owner ~= nil then
            description = description .. "\n|cffffcc00Owner:|r Player " .. GetPlayerId(self.owner) + 1
        end

        if self.bindToBuff ~= nil then
            description = description .. "\n|cffffcc00Bound to buff:|r " .. self.bindToBuff
            if self.waitingForBuff then
                description = description .. " |cffaaaaaa(waiting for buff to be applied)|r"
            end
        end

        if self.bindToOrder ~= nil then
            description = description .. "\n|cffffcc00Bound to order:|r " .. self.bindToOrder .. " |cffaaaaaa(current order " .. GetUnitCurrentOrder(self.host)
        end

        if self.onDestroy ~= nil then
            description = description .. "\n|cffffcc00On Destroy:|r " .. FunctionToString(self.onDestroy)
        end

        description = description .. "\n\n|cffffcc00Unique Number:|r " .. self.unique
        local numOutgoing = 0
        local numIncoming = 0
        local hasError = false
        local outgoingFuncs = {}
        local incomingFuncs = {}
        for __, pair in ipairs(self.pairs) do
            if pair[ActorB] ~= selfInteractionActor then
                if pair[ActorA] == self then
                    numOutgoing = numOutgoing + 1
                    outgoingFuncs[pair[InteractionFunc]] = (outgoingFuncs[pair[InteractionFunc]] or 0) + 1
                elseif pair[ActorB] == self then
                    numIncoming = numIncoming + 1
                    incomingFuncs[pair[InteractionFunc]] = (incomingFuncs[pair[InteractionFunc]] or 0) + 1
                else
                    hasError = true
                end
            end
        end
        description = description .. "\n|cffffcc00Outgoing pairs:|r " .. numOutgoing
        if numOutgoing > 0 then
            local first = true
            description = description .. "|cffaaaaaa ("
            for key, number in pairs(outgoingFuncs) do
                if not first then
                    description = description .. ", |r"
                end
                description = description .. "|cffffcc00" .. number .. "|r |cffaaaaaa" .. FunctionToString(key)
                first = false
            end
            description = description .. ")|r"
        end
        description = description .. "\n|cffffcc00Incoming pairs:|r " .. numIncoming
        if numIncoming > 0 then
            local first = true
            description = description .. "|cffaaaaaa ("
            for key, number in pairs(incomingFuncs) do
                if not first then
                    description = description .. ", |r"
                end
                description = description .. "|cffffcc00" .. number .. "|r |cffaaaaaa" .. FunctionToString(key)
                first = false
            end
            description = description .. ")|r"
        end

        if hasError then
            description = description .. "\n\n|cffff0000DESYNCED PAIR DETECTED!|r"
        end

        if self.causedCrash then
            description = description .. "\n\n|cffff0000CAUSED CRASH!|r"
        end

        if self.isGlobal then
            return description, "|cff00bb00Global Actor|r: " .. Identifier2String(self.identifier)
        else
            if numOutgoing == 0 and numIncoming == 0 then
                return description, "|cffaaaaaaUnpaired Actor:|r " .. Identifier2String(self.identifier)
            else
                return description, "|cffffff00Actor|r: " .. Identifier2String(self.identifier)
            end
        end
    end

    function Actor:select()
        selectedActor = self
        local description, title = self:getDescription()

        BlzFrameSetText(debugTooltipText, description)
        BlzFrameSetText(debugTooltipTitle, title )
        BlzFrameSetSize(debugTooltipText, 0.28, 0.0)
        BlzFrameSetSize(debugTooltip, 0.29, BlzFrameGetHeight(debugTooltipText) + 0.0315)
        BlzFrameSetVisible(debugTooltip, true)

        if not self.isGlobal then
            ALICE_VisualizeCells(self, true)
            if not visualizeAllActors then
                self:createVisualizer()
            end
        end
    end

    function Actor:createVisualizer()
        local x = self.getX(self.anchor)
        local y = self.getY(self.anchor)
        self.visualizer = AddSpecialEffect("Abilities\\Spells\\Other\\Aneu\\AneuTarget.mdl", x, y)
        if self.owner ~= nil then
            BlzSetSpecialEffectColorByPlayer(self.visualizer, self.owner)
        else
            BlzSetSpecialEffectColorByPlayer(self.visualizer, Player(21))
        end
        if type(self.host) == "userdata" and IsWidget(self.host) then
            BlzSetSpecialEffectZ(self.visualizer, self.getZ(self.anchor, x, y) + 150)
        elseif self.getZ(self.anchor) == nil then
            BlzSetSpecialEffectZ(self.visualizer, (self.getZ(self.anchor, x, y) or GetLocZ(x, y)) + 75)
        end
    end

	--===========================================================================================================================================================
    --Cell Class
    --===========================================================================================================================================================

    ---@class Cell
    local Cell = {
        actors = nil,               ---@type Actor[]
        horizontalLightning = nil,  ---@type lightning
        verticalLightning = nil     ---@type lightning
    }

    local cellMt = {__index = Cell}

    ---@param X integer
    ---@param Y integer
    function Cell.create(X, Y)
        local new = {}
        setmetatable(new, cellMt)
        new.actors = {}
        CELL_LIST[X][Y] = new
        return new
    end

    function Cell:enter(actorA, wasTeleport)
        local aPairs
        local bPairs
        actorA.cells[#actorA.cells + 1] = self

        self.actors[#self.actors + 1] = actorA
        actorA.cellIndex[self] = #self.actors

        for __, actorB in ipairs(self.actors) do
			if actorA ~= actorB then
				local thisPair = pairList[actorA][actorB] or pairList[actorB][actorA]
				if thisPair ~= nil  then
                    if thisPair[EveryStep] then
                        if thisPair[PositionInStep] == 0 then
                            numFixedPairs = numFixedPairs + 1
                            fixedPairs[numFixedPairs] = thisPair
                            thisPair[PositionInStep] = numFixedPairs
                        end
                    else
                        --If an actor enters a new cell, it doesn't need to force an immediate interaction with another
                        --actor if their pair is already enabled. Unless it's a teleport.
                        if (thisPair[CurrentPosition] == DO_NOT_EVALUATE or wasTeleport) then
                            local currentPosition = thisPair[CurrentPosition]
                            local nextStep = counter + 1
                            if nextStep > CYCLE_LENGTH then
                                nextStep = nextStep - CYCLE_LENGTH
                            end

                            local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
                            pairAtHighestPosition[PositionInStep] = thisPair[PositionInStep]
                            whichPairs[currentPosition][thisPair[PositionInStep]] = pairAtHighestPosition
                            numPairs[currentPosition] = numPairs[currentPosition] - 1

                            numPairs[nextStep] = numPairs[nextStep] + 1
                            whichPairs[nextStep][numPairs[nextStep]] = thisPair
                            thisPair[CurrentPosition] = nextStep
                            thisPair[PositionInStep] = numPairs[nextStep]
                        end
                    end
                elseif not pairingExcluded[actorA][actorB] then
                    if actorA.anchor ~= actorB.anchor then
                        aPairs = (actorB.priority ~= NO_INCOMING_PAIRS and actorA.evaluator(actorB, actorA))
                        bPairs = (actorA.priority ~= NO_INCOMING_PAIRS and actorB.evaluator(actorA, actorB))
                        if aPairs and bPairs then
                            if actorA.priority < actorB.priority then
                                CreatePair(GetPairParameters(actorB, actorA, true))
                            else
                                CreatePair(GetPairParameters(actorA, actorB, true))
                            end
                        elseif aPairs then
                            CreatePair(GetPairParameters(actorA, actorB, false))
                        elseif bPairs then
                            CreatePair(GetPairParameters(actorB, actorA, false))
                        else
                            pairingExcluded[actorA][actorB] = true
                            pairingExcluded[actorB][actorA] = true
                        end
                    end
				end
			end
        end
    end

    function Cell:remove(actorA)
        for i, cell in ipairs(actorA.cells) do
            if self == cell then
                actorA.cells[i] = actorA.cells[#actorA.cells]
                actorA.cells[#actorA.cells] = nil
                break
            end
        end

        local actors = self.actors
        actors[#actors].cellIndex[self] = actorA.cellIndex[self]
        actors[actorA.cellIndex[self]] = actors[#actors]
        actors[#actors] = nil
    end

    function Cell:leave(actorA)
        self:remove(actorA)

        for __, actorB in ipairs(self.actors) do
			if actorA ~= actorB then
				local thisPair = pairList[actorA][actorB] or pairList[actorB][actorA]
				if thisPair ~= nil then
                    if thisPair[EveryStep] then
                        if thisPair[PositionInStep] ~= 0 and (actorA.maxX < actorB.minX or actorA.minX > actorB.maxX or actorA.maxY < actorB.minY or actorA.minY > actorB.maxY) and not thisPair[Persists] then
                            fixedPairs[thisPair[PositionInStep]] = fixedPairs[numFixedPairs]
                            numFixedPairs = numFixedPairs - 1
                            thisPair[PositionInStep] = 0
                        end
                    elseif thisPair[CurrentPosition] ~= DO_NOT_EVALUATE and (actorA.maxX < actorB.minX or actorA.minX > actorB.maxX or actorA.maxY < actorB.minY or actorA.minY > actorB.maxY) and not thisPair[Persists] then
                        local currentPosition = thisPair[CurrentPosition]
                        local nextStep = DO_NOT_EVALUATE

                        local pairAtHighestPosition = whichPairs[currentPosition][numPairs[currentPosition]]
                        pairAtHighestPosition[PositionInStep] = thisPair[PositionInStep]
                        whichPairs[currentPosition][thisPair[PositionInStep]] = pairAtHighestPosition
                        numPairs[currentPosition] = numPairs[currentPosition] - 1

                        numPairs[nextStep] = numPairs[nextStep] + 1
                        whichPairs[nextStep][numPairs[nextStep]] = thisPair
                        thisPair[CurrentPosition] = nextStep
                        thisPair[PositionInStep] = numPairs[nextStep]
                    end
				end
			end
        end
    end

	--===========================================================================================================================================================
    --Repair
    --===========================================================================================================================================================

    local function WipeCycle()
        Warning("\n|cffff0000Error:|r ALICE Cycle desynced. Attempting to repair...")
        for i = 1, DO_NOT_EVALUATE do
            numPairs[i] = 0
        end
        local nextStep = counter + 1
        if nextStep > CYCLE_LENGTH then
            nextStep = nextStep - CYCLE_LENGTH
        end
        for __, actor in ipairs(actorList) do
            for __, pair in ipairs(actor.pairs) do
                if actor == pair[ActorA] then
                    if pair[CurrentPosition] ~= DO_NOT_EVALUATE then
                        numPairs[nextStep] = numPairs[nextStep] + 1
                        whichPairs[nextStep][numPairs[nextStep]] = pair
                        pair[CurrentPosition] = nextStep
                        pair[PositionInStep] = numPairs[nextStep]
                    else
                        numPairs[DO_NOT_EVALUATE] = numPairs[DO_NOT_EVALUATE] + 1
                        whichPairs[DO_NOT_EVALUATE][numPairs[DO_NOT_EVALUATE]] = pair
                        pair[CurrentPosition] = DO_NOT_EVALUATE
                        pair[PositionInStep] = numPairs[DO_NOT_EVALUATE]
                    end
                end
            end
        end
    end

    local function CheckCycleIntegrity()
        for i = 1, CYCLE_LENGTH do
            if numPairs[i] < 0 then
                WipeCycle()
                return
            end
            for j = 1, numPairs[i] do
                local pair = whichPairs[i][j]
                if not pair[DestructionQueued] and (pair[CurrentPosition] ~= i or pair[PositionInStep] ~= j) then
                    WipeCycle()
                    return
                end
            end
        end
    end

    local function RepairCycle(firstPosition)
		local numSteps
		local nextStep

		for i = firstPosition, numPairs[counter] do
			currentPair = whichPairs[counter][i]
            if currentPair[DestructionQueued] then
                numSteps = MAX_STEPS
            else
			    numSteps = (currentPair[InteractionFunc](currentPair[HostA], currentPair[HostB])*INV_MIN_INTERVAL + 1) // 1
                if numSteps < 1 then
                    numSteps = 1
                elseif numSteps > MAX_STEPS then
                    numSteps = MAX_STEPS
                end
            end

            nextStep = counter + numSteps
            if nextStep > CYCLE_LENGTH then
                nextStep = nextStep - CYCLE_LENGTH
            end
		
			numPairs[nextStep] = numPairs[nextStep] + 1
			whichPairs[nextStep][numPairs[nextStep]] = currentPair
			currentPair[CurrentPosition] = nextStep
			currentPair[PositionInStep] = numPairs[nextStep]
		end

        numPairs[counter] = 0

        for i = 1, numFixedPairs do
            currentPair = fixedPairs[i]
            currentPair[InteractionFunc](currentPair[HostA], currentPair[HostB])
        end

		currentPair = nil
    end

    local function OnCrash()
        local crashingPair = currentPair
        if crashingPair == nil then --Just to shut up the diagnostics.
            return
        end

        --Remove pair and continue with cycle on the first crash. On the second crash, isolate actor completely.
        local A = crashingPair[ActorA]
        local B = crashingPair[ActorB]
        pairingExcluded[A][B] = true
        pairingExcluded[B][A] = true

        if not crashingPair[EveryStep] then
            Warning("\n|cffff0000Error:|r ALICE Cycle crashed during last execution. The identifiers of the actors responsible are "
            .. "|cffffcc00" .. Identifier2String(A.identifier) .. "|r and |cffaaaaff" .. Identifier2String(B.identifier) .. "|r. Unique numbers: " .. A.unique .. ", " .. B.unique .. ". The pair has been removed from the cycle...")

            local nextPosition = crashingPair[PositionInStep] + 1

            numPairs[DO_NOT_EVALUATE] = numPairs[DO_NOT_EVALUATE] + 1
            whichPairs[DO_NOT_EVALUATE][numPairs[DO_NOT_EVALUATE]] = crashingPair
            crashingPair[CurrentPosition] = DO_NOT_EVALUATE
            crashingPair[PositionInStep] = numPairs[DO_NOT_EVALUATE]

            DestroyPair(crashingPair)

            RepairCycle(nextPosition)
        else
            Warning("|cffff0000Error:|r ALICE Cycle crashed during last execution. The identifiers of the actors responsible are "
            .. "|cffffcc00" .. Identifier2String(A.identifier) .. "|r and |cffaaaaff" .. Identifier2String(B.identifier) .. "|r. Unique numbers: " .. A.unique .. ", " .. B.unique .. ". The pair has been removed from the cycle...")

            DestroyPair(crashingPair)
        end

        if A.causedCrash and not A.isIndestructible then
            Warning("\nActor with identifier " .. Identifier2String(A.identifier) .. ", unique number: " .. A.unique .. " is repeatedly causing crashes. Isolating...")
            A:isolate()
        elseif B.causedCrash and not B.isIndestructible then
            Warning("\nActor with identifier " .. Identifier2String(B.identifier) .. ", unique number: " .. B.unique .. " is repeatedly causing crashes. Isolating...")
            B:isolate()
        end

        A.causedCrash = true
        B.causedCrash = true

        CheckCycleIntegrity()
    end

	--===========================================================================================================================================================
    --Main Functions
    --===========================================================================================================================================================

    local function BindChecks()
        for i = #bindChecks, 1, -1 do
            local actor = bindChecks[i]

            if actor.bindToBuff ~= nil then
                if actor.waitingForBuff then
                    if GetUnitAbilityLevel(actor.unit, FourCC(actor.bindToBuff)) > 0 then
                        actor.waitingForBuff = nil
                    end
                elseif GetUnitAbilityLevel(actor.unit, FourCC(actor.bindToBuff)) == 0 then
                    ALICE_Destroy(actor)
                    bindChecks[i] = bindChecks[#bindChecks]
                    bindChecks[#bindChecks] = nil
                end
            elseif actor.bindToOrder ~= nil then
                if GetUnitCurrentOrder(actor.unit) ~= actor.bindToOrder then
                    ALICE_Destroy(actor)
                    bindChecks[i] = bindChecks[#bindChecks]
                    bindChecks[#bindChecks] = nil
                end
            end
        end
    end

    local function RemovePairsFromCycle()
        for i = #destroyedActors, 1, -1 do
            local actor = destroyedActors[i]
            if actor ~= nil then
                local pairs = actor.pairs
                for j = #pairs, 1, -1 do
                    DestroyPair(pairs[j])
                end
                unusedActors[#unusedActors + 1] = actor
            end
            destroyedActors[i] = nil
        end
    end

    local function CellCheck()
        local x
        local y
        local radius
        local minx
        local miny
        local maxx
        local maxy
        local actor
        local actorsThisStep = cellCheckedActors[counter]
		local nextStep

        for i = 1, numCellChecks[counter] do
            actor = actorsThisStep[i]
            x = actor.getX(actor.anchor)
            y = actor.getY(actor.anchor)
            radius = actor.radius
            minx = x - radius
            miny = y - radius
            maxx = x + radius
            maxy = y + radius

            if minx < CELL_MIN_X[actor.minX] and actor.minX > 1 then
                actor.minX = actor.minX - 1
                if minx < CELL_MIN_X[actor.minX] then
                    actor:teleport(x, y)
                else
                    for Y = actor.minY, actor.maxY do
                        CELL_LIST[actor.minX][Y]:enter(actor, false)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            elseif minx > CELL_MAX_X[actor.minX] and actor.minX < NUM_CELLS_X then
                actor.minX = actor.minX + 1
                if minx > CELL_MAX_X[actor.minX] then
                    actor:teleport(x, y)
                else
                    for Y = actor.minY, actor.maxY do
                        CELL_LIST[actor.minX - 1][Y]:leave(actor)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            end
            if miny < CELL_MIN_Y[actor.minY] and actor.minY > 1 then
                actor.minY = actor.minY - 1
                if miny < CELL_MIN_Y[actor.minY] then
                    actor:teleport(x, y)
                else
                    for X = actor.minX, actor.maxX do
                        CELL_LIST[X][actor.minY]:enter(actor, false)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            elseif miny > CELL_MAX_Y[actor.minY] and actor.minY < NUM_CELLS_Y then
                actor.minY = actor.minY + 1
                if miny > CELL_MAX_Y[actor.minY] then
                    actor:teleport(x, y)
                else
                    for X = actor.minX, actor.maxX do
                        CELL_LIST[X][actor.minY - 1]:leave(actor)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            end
            if maxx > CELL_MAX_X[actor.maxX] and actor.maxX < NUM_CELLS_X then
                actor.maxX = actor.maxX + 1
                if maxx > CELL_MAX_X[actor.maxX] then
                    actor:teleport(x, y)
                else
                    for Y = actor.minY, actor.maxY do
                        CELL_LIST[actor.maxX][Y]:enter(actor, false)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            elseif maxx < CELL_MIN_X[actor.maxX] and actor.maxX > 1 then
                actor.maxX = actor.maxX - 1
                if maxx < CELL_MIN_X[actor.maxX] then
                    actor:teleport(x, y)
                else
                    for Y = actor.minY, actor.maxY do
                        CELL_LIST[actor.maxX + 1][Y]:leave(actor)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            end
            if maxy > CELL_MAX_Y[actor.maxY] and actor.maxY < NUM_CELLS_Y then
                actor.maxY = actor.maxY + 1
                if maxy > CELL_MAX_Y[actor.maxY] then
                    actor:teleport(x, y)
                else
                    for X = actor.minX, actor.maxX do
                        CELL_LIST[X][actor.maxY]:enter(actor, false)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            elseif maxy < CELL_MIN_Y[actor.maxY] and actor.maxY > 1 then
                actor.maxY = actor.maxY - 1
                if maxy < CELL_MIN_Y[actor.maxY] then
                    actor:teleport(x, y)
                else
                    for X = actor.minX, actor.maxX do
                        CELL_LIST[X][actor.maxY + 1]:leave(actor)
                    end
                end
                if actor.cellsVisualized then
                    actor:redrawCellVisualizers()
                end
            end

            nextStep = counter + actor.cellCheckInterval
            if nextStep > CYCLE_LENGTH then
                nextStep = nextStep - CYCLE_LENGTH
            end

            numCellChecks[nextStep] = numCellChecks[nextStep] + 1
            cellCheckedActors[nextStep][numCellChecks[nextStep]] = actor
            actor.nextCellCheck = nextStep
            actor.positionInCellCheck = numCellChecks[nextStep]

            if visualizeAllActors then
                if type(actor.host) == "userdata" and IsWidget(actor.host) then
                    BlzSetSpecialEffectPosition(actor.visualizer, x, y, actor.getZ(actor.anchor, x, y) + 150)
                else
                    BlzSetSpecialEffectPosition(actor.visualizer, x, y, (actor.getZ(actor.anchor, x, y) or GetLocZ(x, y)) + 75)
                end
            end
        end
    end

    local function UpdateSelectedActor()
        if selectedActor ~= nil then
            if not selectedActor.isGlobal then
                local x = selectedActor.getX(selectedActor.anchor)
                local y = selectedActor.getY(selectedActor.anchor)
                if type(selectedActor.host) == "userdata" and IsWidget(selectedActor.host) then
                    BlzSetSpecialEffectPosition(selectedActor.visualizer, x, y, selectedActor.getZ(selectedActor.anchor, x, y) + 150)
                else
                    BlzSetSpecialEffectPosition(selectedActor.visualizer, x, y, (selectedActor.getZ(selectedActor.anchor, x, y) or GetLocZ(x, y)) + 75)
                end
                for __, pair in ipairs(selectedActor.pairs) do
                    if pair[EveryStep] or pair[CurrentPosition] == counter then
                        VisualizationLightning(pair, 0.03, "DRAL")
                    end
                end
            end

            local description, title = selectedActor:getDescription()
            BlzFrameSetText(debugTooltipText, description)
            BlzFrameSetText(debugTooltipTitle, title )
            BlzFrameSetSize(debugTooltipText, 0.28, 0.0)
            BlzFrameSetSize(debugTooltip, 0.29, BlzFrameGetHeight(debugTooltipText) + 0.0315)
        end
    end

    local function FreezeProtection()
        freezeCounter = freezeCounter + math.min(2*FREEZE_PROTECTION_LIMIT, (numPairs[counter] + numFixedPairs - FREEZE_PROTECTION_LIMIT))/10
        if freezeCounter < 0 then
            freezeCounter = 0
        elseif freezeCounter >= FREEZE_PROTECTION_LIMIT then
            Warning("|cffff0000Error:|r ALICE Cycle terminated due to load exceeding the specified freeze protection limit. The identifiers of the actors in the pairs responsible for the overload are:\n" .. GetPairStatistics(whichPairs[counter]))
            PauseTimer(MASTER_TIMER)
        end
    end

	--===========================================================================================================================================================
    --Main
    --===========================================================================================================================================================

	local function Main()
		local numSteps
		local nextStep

		if currentPair ~= nil then
            OnCrash()
		end

        BindChecks()

        RemovePairsFromCycle()

		counter = counter + 1
		if counter > CYCLE_LENGTH then
			counter = 1
		end

        CellCheck()

		if printCounts then
            print("actors: " .. #actorList .. ", pairs: " .. numPairs[counter] + numFixedPairs .. ", cell checks: " .. numCellChecks[counter])
        end

        numCellChecks[counter] = 0

        FreezeProtection()

        if debugModeEnabled then
            UpdateSelectedActor()
        end

        --Main
		for i = 1, numPairs[counter] do
			currentPair = whichPairs[counter][i]
            if currentPair[DestructionQueued] then
                numSteps = MAX_STEPS
            else
			    numSteps = (currentPair[InteractionFunc](currentPair[HostA], currentPair[HostB])*INV_MIN_INTERVAL + 1) // 1 --convert seconds to steps, then ceil.
                if numSteps < 1 then
                    numSteps = 1
                elseif numSteps > MAX_STEPS then
                    numSteps = MAX_STEPS
                end
            end

            nextStep = counter + numSteps
            if nextStep > CYCLE_LENGTH then
                nextStep = nextStep - CYCLE_LENGTH
            end
		
			numPairs[nextStep] = numPairs[nextStep] + 1
			whichPairs[nextStep][numPairs[nextStep]] = currentPair
			currentPair[CurrentPosition] = nextStep
			currentPair[PositionInStep] = numPairs[nextStep]
		end

        numPairs[counter] = 0

        --Every Step Cycle
        for i = 1, numFixedPairs do
            currentPair = fixedPairs[i]
            currentPair[InteractionFunc](currentPair[HostA], currentPair[HostB])
        end

		currentPair = nil
	end

   	--===========================================================================================================================================================
    --Debug Mode
    --===========================================================================================================================================================

    local function OnMouseClick()
        if BlzGetTriggerPlayerMouseButton() ~= MOUSE_BUTTON_TYPE_LEFT then
            return
        end

        if selectedActor ~= nil then
            selectedActor:deselect()
        end

        local x = BlzGetTriggerPlayerMouseX()
        local y = BlzGetTriggerPlayerMouseY()
        local objects = ALICE_EnumObjectsInRange(x, y, 100, true, nil)
        local closestDist = 100
        local closestObject = nil

        for __, object in ipairs(objects) do
            local actor = ALICE_GetActor(object)
            ---@diagnostic disable-next-line: need-check-nil
            local dx = actor.getX(actor.anchor) - x
            ---@diagnostic disable-next-line: need-check-nil
            local dy = actor.getY(actor.anchor) - y
            local dist = sqrt(dx*dx + dy*dy)
            if dist < closestDist then
                closestDist = dist
                ---@diagnostic disable-next-line: need-check-nil
                closestObject = actor.host
            end
        end

        if closestObject ~= nil then
            if getmetatable(actorOf[closestObject]) == ActorMetatable then
                actorOf[closestObject]:select()
            else
                print("Multiple actors exist for this object. Press |cffffcc00(C)|r to cycle through.")
                actorOf[closestObject][1]:select()
            end
        end
    end

    local function OnCKey()
        if selectedActor == nil then
            return
        end
        local selectedObject = selectedActor.anchor
        if getmetatable(actorOf[selectedObject]) == ActorMetatable then
            return
        end
        for index, actor in ipairs(actorOf[selectedObject]) do
            if selectedActor == actor then
                selectedActor:deselect()
                if actorOf[selectedObject][index + 1] ~= nil then
                    actorOf[selectedObject][index + 1]:select()
                    return
                else
                    actorOf[selectedObject][1]:select()
                    return
                end
            end
        end
    end
    
    local function EnableDebugMode()
        if not debugModeEnabled then
            Warning("\nDebug mode enabled. Left-click near an actor to display attributes and enable visualization. \n\nYou can use the Eikonium's IngameConsole to execute code on the selected actor. You refer to it with ALICE_GetActor().")
            debugModeEnabled = true

            BlzLoadTOCFile("CustomTooltip.toc")
            debugTooltip = BlzCreateFrame("CustomTooltip", BlzGetOriginFrame(ORIGIN_FRAME_WORLD_FRAME, 0), 0, 0)
            BlzFrameSetAbsPoint( debugTooltip , FRAMEPOINT_BOTTOMRIGHT , 0.8 , 0.165 )
            BlzFrameSetSize( debugTooltip , 0.32 , 0.0 )
            debugTooltipTitle = BlzFrameGetChild(debugTooltip,0)
            debugTooltipText = BlzFrameGetChild(debugTooltip,1)

            mouseClickTrigger = CreateTrigger()
            TriggerRegisterPlayerEvent(mouseClickTrigger, GetTriggerPlayer(), EVENT_PLAYER_MOUSE_DOWN)
            TriggerAddAction(mouseClickTrigger, OnMouseClick)

            cycleSelectTrigger = CreateTrigger()
            BlzTriggerRegisterPlayerKeyEvent(cycleSelectTrigger, GetTriggerPlayer(), OSKEY_C, 0, true)
            TriggerAddAction(cycleSelectTrigger, OnCKey)
        else
            Warning("\nDebug mode has been disabled.")
            if selectedActor ~= nil then
                selectedActor:deselect()
            end

            debugModeEnabled = false
            DestroyTrigger(mouseClickTrigger)
            DestroyTrigger(cycleSelectTrigger)
            BlzDestroyFrame(debugTooltip)
        end
    end

   	--===========================================================================================================================================================
    --Init
    --===========================================================================================================================================================

	OnInit.main(function()
		MASTER_TIMER = CreateTimer()
		MAX_STEPS = floor(ALICE_MAX_INTERVAL/ALICE_MIN_INTERVAL)
		CYCLE_LENGTH = MAX_STEPS + 1
        DO_NOT_EVALUATE = CYCLE_LENGTH + 1

		numPairs = {}
		whichPairs = {}
		actorList = {}
        globalActorList = {}
		for i = 1, DO_NOT_EVALUATE do
			numPairs[i] = 0
			whichPairs[i] = {}
		end

        fixedPairs = {}

        numCellChecks = {}
        for i = 1, CYCLE_LENGTH do
            numCellChecks[i] = 0
            cellCheckedActors[i] = {}
        end

        if USE_CELLS then
            local worldBounds = GetWorldBounds()
            MAP_MIN_X = GetRectMinX(worldBounds)
            MAP_MAX_X = GetRectMaxX(worldBounds)
            MAP_MIN_Y = GetRectMinY(worldBounds)
            MAP_MAX_Y = GetRectMaxY(worldBounds)
            for x = 1, NUM_CELLS_X do
                for y = 1, NUM_CELLS_Y do
                    Cell.create(x,y)
                end
            end
            for x = 1, NUM_CELLS_X do
                CELL_MIN_X[x] = MAP_MIN_X + (x-1)/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
                CELL_MAX_X[x] = MAP_MIN_X + x/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
            end
            for y = 1, NUM_CELLS_Y do
                CELL_MIN_Y[y] = MAP_MIN_Y + (y-1)/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
                CELL_MAX_Y[y] = MAP_MIN_Y + y/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
            end
        end

        local trig = CreateTrigger()
        for i = 0, 23 do
            for __, name in ipairs(MAP_CREATORS) do
                if string.find(GetPlayerName(Player(i)), name) then
                    TriggerRegisterPlayerChatEvent(trig, Player(i), "downtherabbithole", true)
                    TriggerRegisterPlayerChatEvent(trig, Player(i), "-downtherabbithole", true)
                end
            end
        end
        TriggerAddAction(trig, EnableDebugMode)

        moveableLoc = Location(0,0)

        selfInteractionActor = ALICE_Create(nil, "selfInteraction", nil, {isIndestructible = true})
        actorList[#actorList] = nil
        globalActorList[#globalActorList] = nil
        totalActors = totalActors - 1
        selfInteractionActor.unique = 0

		TimerStart(MASTER_TIMER, ALICE_MIN_INTERVAL, true, Main)
	end)

	--===========================================================================================================================================================
    --API
    --===========================================================================================================================================================

    ---@type Actor
    local actorDummy = {
        pairsWith = {},
        interactionFunc = DoNothing
    }

    setmetatable(actorDummy, ActorMetatable)

    --Main API
    --===========================================================================================================================================================

    ---@param host any
	---@param identifier string | string[] | nil
	---@param interactionFunc function | table | nil
    ---@param flags? table
    ---@return Actor | nil
    function ALICE_Create(host, identifier, interactionFunc, flags)
        if flags == nil then
            return Actor.create(host, identifier, interactionFunc)
        else
            for key, __ in pairs(flags) do
                if not RECOGNIZED_FIELDS[key] then
                    Warning("|cffff0000Warning:|r Unrecognized field |cffffcc00" .. key .. "|r in flags passed to function ALICE_Create...")
                end
            end
            return Actor.create(
                host,
                identifier,
                interactionFunc,
                flags.pairsWith,
                flags.priority,
                flags.radius,
                flags.cellCheckInterval,
                flags.isStationary,
                flags.destroyOnDeath,
                flags.anchor,
                flags.bindToBuff,
                flags.bindToOrder,
                flags.onActorDestroy,
                flags.randomDelay,
                flags.isIndestructible
            )
        end
    end

    ---@param whichClass table
    ---@return Actor | nil
    function ALICE_CreateFromClass(whichClass)
        return Actor.create(
            whichClass,
            whichClass.identifier,
            whichClass.interactionFunc,
            whichClass.pairsWith,
            whichClass.priority,
            whichClass.radius,
            whichClass.cellCheckInterval,
            whichClass.isStationary,
            whichClass.destroyOnDeath,
            whichClass.anchor,
            whichClass.bindToBuff,
            whichClass.bindToOrder,
            whichClass.onActorDestroy,
            whichClass.randomDelay,
            whichClass.isIndestructible
        )
    end

    ---@param object any
    ---@param keyword? string
    function ALICE_Destroy(object, keyword)
        object = ALICE_GetActor(object, keyword)
        if object ~= nil then
            if object.isIndestructible then
                Warning("|cffff0000Warning:|r Attempted to destroy indestructible actor with identifiers " .. Identifier2String(object) .. ", unique number: " .. object.unique)
            end
            object:destroy()
        end
    end

    --Object API
    --===========================================================================================================================================================

    ---@param object any
    ---@param keyword? string
    ---@return boolean
    function ALICE_HasActor(object, keyword)
        return ALICE_GetActor(object, keyword) ~= nil
    end

    ---@param object any
    ---@param keyword? string
    ---@return Actor | nil
    function ALICE_GetActor(object, keyword)
        if object == nil then
            if selectedActor ~= nil then
                return selectedActor
            end
            return nil
        end

        if getmetatable(object) == ActorMetatable then
            return object
        elseif actorOf[object] ~= nil then
            if getmetatable(actorOf[object]) == ActorMetatable then
                if keyword == nil or actorOf[object].identifier[keyword] then
                    return actorOf[object]
                else
                    return nil
                end
            elseif keyword == nil then
                return actorOf[object][1]
            else
                for __, actor in ipairs(actorOf[object]) do
                    if actor.identifier[keyword] then
                        return actor
                    end
                end
                return nil
            end
        else
            return nil
        end
    end

    ---@param object any
    function ALICE_UpdateOwner(object)
        if actorOf[object] ~= nil then
            if getmetatable(actorOf[object]) == ActorMetatable then
                actorOf[object]:setOwner()
                ExecuteInNewThread(function() actorOf[object]:reevaluatePairings() end)
            else
                for __, actor in ipairs(actorOf[object]) do
                    actor:setOwner()
                    ExecuteInNewThread(function() actor:reevaluatePairings() end)
                end
            end
        elseif getmetatable(actorOf[object]) == ActorMetatable then
            object:setOwner()
            ExecuteInNewThread(function() object:reevaluatePairings() end)
        end
    end

    ---@param object any
    ---@param identifier string
    ---@param newFunc function
    ---@param keyword? string
    function ALICE_SetInteractionFunc(object, identifier, newFunc, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        object.interactionFunc[identifier] = newFunc
        for __, pair in ipairs(object.pairs) do
            if object == pair[ActorA] then
                pair[InteractionFunc] = newFunc
            end
        end
    end

    ---@param object any
    function ALICE_Kill(object)
        object = ALICE_GetActor(object)
        if object == nil then
            Warning("|cffff0000Warning:|r Attempted to kill object without an actor...")
            return
        end
        
        if type(object.host) == "table" then
            if type(object.host.destroy) == "function" then
                object.host:destroy()
            else
                Warning("|cffff0000Warning:|r Attempted to destroy class that does not have a destroy method...")
            end
        elseif type(object.host) == "userdata" then
            if IsUnit(object.host) then
                KillUnit(object.host)
            elseif IsDestructable(object.host) then
                KillDestructable(object.host)
            elseif IsItem(object.host) then
                RemoveItem(object.host)
            elseif IsEffect(object.host) then
                DestroyEffect(object.host)
            else
                Warning("|cffff0000Warning:|r Attempted to kill host of a type for which no kill function was set yet...")
            end
        else
            Warning("|cffff0000Warning:|r Attempted to kill host of a type that cannot be killed...")
        end
    end

    --Identifier API
    --===========================================================================================================================================================

    ---@param object any
    ---@param newIdentifier string | string[]
    ---@param keyword? string
    function ALICE_AddIdentifier(object, newIdentifier, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        object:addIdentifier(newIdentifier)
    end

    ---@param object any
    ---@param toRemove string | string[]
    ---@param keyword? string
    function ALICE_RemoveIdentifier(object, toRemove, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        object:removeIdentifier(toRemove)
    end

    ---@param object any
    ---@param newIdentifier string | string[] | nil
    ---@param keyword? string
    function ALICE_SetIdentifier(object, newIdentifier, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        object:setIdentifier(newIdentifier)
    end

    ---@param whichObject any
    ---@param identifier string | table
    ---@param keyword? string
    ---@return boolean
    function ALICE_HasIdentifier(whichObject, identifier, keyword)
        whichObject = ALICE_GetActor(whichObject, keyword)
        if whichObject == nil then
            return false
        end

        if type(identifier) == "table" then
            for key, __ in pairs(actorDummy.pairsWith) do
                actorDummy.pairsWith[key] = nil
            end
            actorDummy:initPairingLogic(identifier)
            return CheckIdentifierOverlap(whichObject, actorDummy)
        else
            return whichObject.identifier[identifier]
        end
    end

    ---@param objectA any
    ---@param objectB any
    ---@return boolean
    function ALICE_PairsWith(objectA, objectB)
        objectA = ALICE_GetActor(objectA)
        if objectA == nil then
            return false
        end

        objectB = ALICE_GetActor(objectB)
        if objectB == nil then
            return false
        end

        return (objectB.priority ~= NO_INCOMING_PAIRS and objectA.evaluator(objectB, objectA)) or (objectA.priority ~= NO_INCOMING_PAIRS and objectB.evaluator(objectA, objectB))
    end

    ---@param object any
    ---@param keyword? string
    function ALICE_GetIdentifier(object, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        local returnTable = {}
        for key, __ in pairs(object.identifier) do
            table.insert(returnTable, key)
        end
        table.sort(returnTable, function(a, b) return tostring(a) < tostring(b) end)
        return returnTable
    end

    --Pair API
    --===========================================================================================================================================================

    ---@param whichFunc function
    function ALICE_PairSetInteractionFunc(whichFunc)
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return
        end

        currentPair[InteractionFunc] = whichFunc
    end

    function ALICE_PairDisable()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return
        end

        local thisPair = currentPair
        pairingExcluded[thisPair[ActorA]][thisPair[ActorB]] = true
        pairingExcluded[thisPair[ActorB]][thisPair[ActorA]] = true
        ExecuteInNewThread(function()
            DestroyPair(thisPair)
        end)
    end

    ---@param whichClass? table
    ---@return table
    function ALICE_PairLoadData(whichClass)
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return {}
        end

        if currentPair[UserData] == nil then
            currentPair[UserData] = GetUnusedTable()
            setmetatable(currentPair[UserData], whichClass)
        end
        return currentPair[UserData]
    end

    function ALICE_PairOnDestroy(callback)
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return {}
        end
        if currentPair[UserData] == nil then
            currentPair[UserData] = GetUnusedTable()
        end
        currentPair[UserData].onDestroy = callback
    end

    ---@return number
    function ALICE_PairGetDistance()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return 0
        end

        local objectA = currentPair[ActorA]
        local objectB = currentPair[ActorB]

        if objectA ~= nil and objectB == selfInteractionActor then
            return 0
        end

        if objectA.isGlobal or objectB.isGlobal then
            Warning("|cffff0000Error:|r Attempted to call ALICE_PairGetDistance on a pair with a global actor...")
            return 0
        end

        local dx = objectA.getX(objectA.anchor) - objectB.getX(objectB.anchor)
        local dy = objectA.getY(objectA.anchor) - objectB.getY(objectB.anchor)

        return sqrt(dx*dx + dy*dy)
    end

    ---@return number
    function ALICE_PairGetDistance3D()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return 0
        end

        local objectA = currentPair[ActorA]
        local objectB = currentPair[ActorB]

        if objectA ~= nil and objectB == selfInteractionActor then
            return 0
        end

        if objectA.isGlobal or objectB.isGlobal then
            Warning("|cffff0000Error:|r Attempted to call ALICE_PairGetDistance3D on a pair with a global actor...")
            return 0
        end

        local xa = objectA.getX(objectA.anchor)
        local ya = objectA.getY(objectA.anchor)
        local xb = objectA.getX(objectB.anchor)
        local yb = objectB.getY(objectB.anchor)
        local dz = objectA.getZ(objectA.anchor, xa, ya) - objectB.getZ(objectB.anchor, xb, yb)

        return sqrt((xa - xb)^2 + (ya - yb)^2 + dz*dz)
    end

    ---@return number
    function ALICE_PairGetAngle()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return 0
        end

        local objectA = currentPair[ActorA]
        local objectB = currentPair[ActorB]

        if objectA ~= nil and objectB == selfInteractionActor then
            return 0
        end

        if objectA.isGlobal or objectB.isGlobal then
            Warning("|cffff0000Error:|r Attempted to call ALICE_PairGetAngle on a pair with a global actor...")
            return 0
        end

        local dx = objectB.getX(objectB.anchor) - objectA.getX(objectA.anchor)
        local dy = objectB.getY(objectB.anchor) - objectA.getY(objectA.anchor)

        return atan(dy, dx)
    end

    ---@return number, number
    function ALICE_PairGetAngle3D()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return 0, 0
        end

        local objectA = currentPair[ActorA]
        local objectB = currentPair[ActorB]

        if objectA ~= nil and objectB == selfInteractionActor then
            return 0, 0
        end

        if objectA.isGlobal or objectB.isGlobal then
            Warning("|cffff0000Error:|r Attempted to call ALICE_PairGetAngle3D on a pair with a global actor...")
            return 0, 0
        end

        local xa = objectA.getX(objectA.anchor)
        local ya = objectA.getY(objectA.anchor)
        local xb = objectA.getX(objectB.anchor)
        local yb = objectB.getY(objectB.anchor)
        local dx = xb - xa
        local dy = yb - ya
        local dz = objectB.getZ(objectB.anchor, xb, yb) - objectA.getZ(objectA.anchor, xa, ya)

        return atan(dy, dx), atan(dz, sqrt(dx*dx + dy*dy))
    end

    ---@return boolean
    function ALICE_PairIsFirstContact()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return false
        end

        if currentPair[UserData] == nil then
            ALICE_PairLoadData()
            currentPair[UserData].hadContact = true
            return true
        elseif currentPair[UserData].hadContact then
            return false
        else
            currentPair[UserData].hadContact = true
            return true
        end
    end

    function ALICE_PairForget()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return false
        end

        if currentPair[ActorB][currentPair[InteractionFunc]] == currentPair then
            currentPair[ActorB][currentPair[InteractionFunc]] = nil
        end
        if currentPair[UserData] ~= nil then
            PairUserDataOnDestroy(currentPair)
        end
    end

    function ALICE_PairPersist()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return {}
        end
        currentPair[Persists] = true
    end

    function ALICE_PairIsUnoccupied()
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return false
        end

        if currentPair[ActorB][currentPair[InteractionFunc]] ~= nil and currentPair[ActorB][currentPair[InteractionFunc]] ~= currentPair then
            return false
        else
            --Store for the receiving actor at the key of the interaction func the current pair as occupying that slot, blocking other pairs.
            currentPair[ActorB][currentPair[InteractionFunc]] = currentPair
            return true
        end
    end

    --Enum API
    --===========================================================================================================================================================

    local enumExceptions = {}

    ---@param filter function | boolean | string | table | nil
    ---@param whoFilters player | nil
    ---@return table
    function ALICE_EnumObjects(filter, whoFilters)
        local returnTable = {}

        if filter == nil then
            filter = true
        end

        if type(filter) == "table" then
            actorDummy:initPairingLogic(filter)
        else
            actorDummy.pairsWith = filter
        end
        actorDummy.owner = whoFilters

        local evaluator = pairingEvaluator[type(filter)]
        for __, actor in ipairs(actorList) do
            if evaluator(actor, actorDummy) and not enumExceptions[actor.host] then
                returnTable[#returnTable + 1] = actor.host
                enumExceptions[actor.host] = true
            end
        end

        for key, __ in pairs(enumExceptions) do
            enumExceptions[key] = nil
        end

        return returnTable
    end

    ---@param filter function | boolean | string | table | nil
    ---@param whoFilters player | nil
    ---@param action function
    function ALICE_ForAllObjectsDo(filter, whoFilters, action)
        local list = ALICE_EnumObjects(filter, whoFilters)
        for __, object in ipairs(list) do
            action(object)
        end
    end

    ---@param x number
    ---@param y number
    ---@param range number
    ---@param filter function | boolean | string | table | nil
    ---@param whoFilters player | nil
    ---@return table
    function ALICE_EnumObjectsInRange(x, y, range, filter, whoFilters)
        local returnTable = {}

        if filter == nil then
            filter = true
        end

        local minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x - range - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        local minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y - range - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
        local maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(x + range - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        local maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(y + range - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))

        local dx
        local dy
        local rangeSquared = range*range

        if type(filter) == "table" then
            actorDummy:initPairingLogic(filter)
        else
            actorDummy.pairsWith = filter
        end
        actorDummy.owner = whoFilters

        local evaluator = pairingEvaluator[type(filter)]
        for X = minX, maxX do
            for Y = minY, maxY do
                for __, actor in ipairs(CELL_LIST[X][Y].actors) do
                    if not enumExceptions[actor.host] then
						if evaluator(actor, actorDummy) then
                            enumExceptions[actor.host] = true
							dx =  actor.getX(actor.anchor) - x
							dy = actor.getY(actor.anchor) - y
							if dx*dx + dy*dy < rangeSquared then
								returnTable[#returnTable + 1] = actor.host
							end
						end
                    end
                end
            end
        end

        for key, __ in pairs(enumExceptions) do
            enumExceptions[key] = nil
        end

        return returnTable
    end

    ---@param x number
    ---@param y number
    ---@param range number
    ---@param filter function | boolean | string | table | nil
    ---@param whoFilters player | nil
    ---@param action function
    function ALICE_ForAllObjectsInRangeDo(x, y, range, filter, whoFilters, action)
        local list = ALICE_EnumObjectsInRange(x, y, range, filter, whoFilters)
        for __, object in ipairs(list) do
            action(object)
        end
    end

    ---@param minx number
    ---@param miny number
    ---@param maxx number
    ---@param maxy number
    ---@param filter function | boolean | string | table | nil
    ---@param whoFilters? player | nil
    ---@return table
    function ALICE_EnumObjectsInRect(minx, miny, maxx, maxy, filter, whoFilters)
        local returnTable = {}

        if filter == nil then
            filter = true
        end

        local minX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(minx - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        local minY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(miny - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))
        local maxX = min(NUM_CELLS_X, max(1, ceil(NUM_CELLS_X*(maxx - MAP_MIN_X)/(MAP_MAX_X - MAP_MIN_X))))
        local maxY = min(NUM_CELLS_Y, max(1, ceil(NUM_CELLS_Y*(maxy - MAP_MIN_Y)/(MAP_MAX_Y - MAP_MIN_Y))))

        local x
        local y

        if type(filter) == "table" then
            actorDummy:initPairingLogic(filter)
        else
            actorDummy.pairsWith = filter
        end
        actorDummy.owner = whoFilters

        local evaluator = pairingEvaluator[type(filter)]
        for X = minX, maxX do
            for Y = minY, maxY do
                for __, actor in ipairs(CELL_LIST[X][Y].actors) do
                    if not enumExceptions[actor.host] then
						if evaluator(actor, actorDummy) then
                            enumExceptions[actor.host] = true
							x =  actor.getX(actor.anchor)
							y = actor.getY(actor.anchor)
							if x > minx and x < maxx and y > miny and y < maxy then
								returnTable[#returnTable + 1] = actor.host
							end
						end
                    end
                end
            end
        end

        for key, __ in pairs(enumExceptions) do
            enumExceptions[key] = nil
        end

        return returnTable
    end

    ---@param minx number
    ---@param miny number
    ---@param maxx number
    ---@param maxy number
    ---@param filter function | boolean | string | table | nil
    ---@param whoFilters player | nil
    ---@param action function
    function ALICE_ForAllObjectsInRectDo(minx, miny, maxx, maxy, filter, whoFilters, action)
        local list = ALICE_EnumObjectsInRect(minx, miny, maxx, maxy, filter, whoFilters)
        for __, object in ipairs(list) do
            action(object)
        end
    end

    --Debug API
    --===========================================================================================================================================================


    function ALICE_Pause()
		PauseTimer(MASTER_TIMER)
	end

    ---@param factor number
    function ALICE_SlowMotion(factor)
        ALICE_TimeScale = factor
        TimerStart(MASTER_TIMER, ALICE_MIN_INTERVAL*factor, true, Main)
    end

	function ALICE_Resume()
        ALICE_TimeScale = 1
		TimerStart(MASTER_TIMER, ALICE_MIN_INTERVAL, true, Main)
	end

    ---@param duration? number
    ---@param lightningType? string
    function ALICE_PairVisualize(duration, lightningType)
        if currentPair == nil then
            Warning("|cffff0000Error:|r Cannot call Pair API functions from outside of interaction func...")
            return
        end

        local objectA = currentPair[ActorA]
        local objectB = currentPair[ActorB]

        local xa = objectA.getX(objectA.anchor)
        local ya = objectA.getY(objectA.anchor)
        local xb = objectB.getX(objectB.anchor)
        local yb = objectB.getY(objectB.anchor)

		if xa and ya and xb and yb then
			local visLight = AddLightning(lightningType or "DRAL", true, xa, ya, xb, yb)
			TimerStart(CreateTimer(), duration or 0.03, false, function()
				DestroyLightning(visLight)
				DestroyTimer(GetExpiredTimer())
			end)
		end
    end

    ---@param whichObject any
    ---@param enable? boolean
    ---@param keyword? string
    function ALICE_VisualizeCells(whichObject, enable, keyword)
        whichObject = ALICE_GetActor(whichObject,keyword)
        if whichObject == nil then
            Warning("|cffff0000Warning:|r Could not find actor of argument in ALICE_VisualizeCells...")
            return
        end
        whichObject:visualizeCells((enable == nil) or enable)
    end

    function ALICE_Statistics()
        Warning("|cffffcc00Statistics:|r The identifiers of the actors in the pairs currently being evaluated are:\n" .. GetPairStatistics(whichPairs[counter+1]))
    end

    function ALICE_ListGlobals()
        local message = "List of all global actors:"
        for __, actor in ipairs(globalActorList) do
            message = message .. "\n" .. Identifier2String(actor.identifier) .. ", Unique: " .. actor.unique
        end
        Warning(message)
    end

    ---@param whichActor Actor | integer
    function ALICE_Select(whichActor)
        if not debugModeEnabled then
            Warning("|cffff0000Error:|r ALICE_Select only available in debug mode...")
            return
        end
        if type(whichActor) == "table" then
            whichActor:select()
        else
            ALICE_GetFromUnique(whichActor):select()
        end
    end

    ---@param number integer
    ---@return Actor | nil
    function ALICE_GetFromUnique(number)
        for __, actor in ipairs(actorList) do
            if actor.unique == number then
                return actor
            end
        end
        return nil
    end

    function ALICE_PrintCounts()
        printCounts = not printCounts
    end

    function ALICE_VisualizeAllCells()
        visualizeAllCells = not visualizeAllCells

        if visualizeAllCells then
            for X = 1, NUM_CELLS_X do
                for Y = 1, NUM_CELLS_Y do
                    local minx = MAP_MIN_X + (X-1)/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
                    local miny = MAP_MIN_Y + (Y-1)/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
                    local maxx = MAP_MIN_X + X/NUM_CELLS_X*(MAP_MAX_X - MAP_MIN_X)
                    local maxy = MAP_MIN_Y + Y/NUM_CELLS_Y*(MAP_MAX_Y - MAP_MIN_Y)
                    CELL_LIST[X][Y].horizontalLightning = AddLightning("DRAM", false, minx, miny, maxx, miny)
                    SetLightningColor(CELL_LIST[X][Y].horizontalLightning, 1, 1, 1, 0.35)
                    CELL_LIST[X][Y].verticalLightning = AddLightning("DRAM", false, maxx, miny, maxx, maxy)
                    SetLightningColor(CELL_LIST[X][Y].verticalLightning, 1, 1, 1, 0.35)
                end
            end
        else
            for X = 1, NUM_CELLS_X do
                for Y = 1, NUM_CELLS_Y do
                    DestroyLightning(CELL_LIST[X][Y].horizontalLightning)
                    DestroyLightning(CELL_LIST[X][Y].verticalLightning)
                end
            end
        end
    end

    function ALICE_VisualizeAllActors()
        visualizeAllActors = not visualizeAllActors

        if visualizeAllActors then
            for __, actor in ipairs(actorList) do
                if not actor.isGlobal and not actor ~= selectedActor then
                    actor:createVisualizer()
                end
            end
        else
            for __, actor in ipairs(actorList) do
                if not actor.isGlobal and not actor ~= selectedActor then
                    DestroyEffect(actor.visualizer)
                end
            end
        end
    end

    --Optimization API
    --===========================================================================================================================================================

    ---@param whichFunc function
    function ALICE_SetEveryStep(whichFunc)
        isEveryStepFunction[whichFunc] = true
    end

    ---@param whichFunc function
    ---@param maxRange number
    function ALICE_SetMaxRange(whichFunc, maxRange)
        functionMaxRange[whichFunc] = maxRange
    end

    ---@param object any
    ---@param newRadius number
    ---@param keyword? string
    function ALICE_SetRadius(object, newRadius, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        object.radius = newRadius
    end

    ---@param object any
    ---@param enable boolean
    ---@param keyword? string
    function ALICE_SetStationary(object, enable, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        if object.isStationary == enable then
            return
        end

        if enable then
            local nextCheck = object.nextCellCheck
            local actorAtHighestPosition = cellCheckedActors[nextCheck][numCellChecks[nextCheck]]
            actorAtHighestPosition.positionInCellCheck = object.positionInCellCheck
            cellCheckedActors[nextCheck][object.positionInCellCheck] = actorAtHighestPosition
            numCellChecks[nextCheck] = numCellChecks[nextCheck] - 1
            object.nextCellCheck = DO_NOT_EVALUATE
        else
            local nextStep = counter + 1
            numCellChecks[nextStep] = numCellChecks[nextStep] + 1
            cellCheckedActors[nextStep][numCellChecks[nextStep]] = object
            object.nextCellCheck = nextStep
            object.positionInCellCheck = numCellChecks[nextStep]
        end
    end

    ---@param object any
    ---@param newInterval number
    ---@param keyword? string
    function ALICE_SetCellCheckInterval(object, newInterval, keyword)
        object = ALICE_GetActor(object,keyword)
        if object == nil then
            return
        end
        object.cellCheckInterval = min(MAX_STEPS, max(1, ceil((newInterval - 0.001)*INV_MIN_INTERVAL)))
    end
end





Lua:
do
    --[[
    =============================================================================================================================================================
                                                                Complementary Actor Template

                                                    Functions required for units, items, and destructables CAT
    =============================================================================================================================================================
    ]]

    CAT_ExceptionList = {}
    CAT_WidgetIdRadius = {}
    CAT_WidgetIdIsStationary = {}
    CAT_WidgetIdCellCheckInterval = {}

    ---@param whichString string
    ---@return string
    function CAT_ToCamelCase(whichString)
        whichString = whichString:gsub("(\x25s)(\x25a)", function(__, letter) return letter:upper() end)
        return string.lower(string.sub(whichString,1,1)) .. string.sub(whichString,2)
    end

    ---@param widgetId integer
    ---@param whichList table
    ---@return boolean
    function CAT_IsWidgetIdInList(widgetId, whichList)
        for __, v in ipairs(whichList) do
            if v == widgetId then
                return true
            end
        end
        return false
    end

    ---@param widgetId integer
    function CAT_AddException(widgetId)
        if not CAT_IsWidgetIdInList(widgetId, CAT_ExceptionList) then
            table.insert(CAT_ExceptionList, widgetId)
        end
    end

    ---@param widgetId integer
    ---@param radius number
    function CAT_SetWidgetIdRadius(widgetId, radius)
        CAT_WidgetIdRadius[widgetId] = radius
    end

    ---@param widgetId integer
    ---@param interval number
    function CAT_SetWidgetCellCheckInterval(widgetId, interval)
        CAT_WidgetIdCellCheckInterval[widgetId] = interval
    end

    ---@param widgetId integer
    function CAT_SetWidgetIdStationary(widgetId)
        CAT_WidgetIdIsStationary[widgetId] = true
    end
end

Lua:
do
    --[[
    =============================================================================================================================================================
                                                                Complementary Actor Template
                                                                        by Antares

                                        Requires:
                                        ALICE 						https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
                                        CAT Shared Functions
                                        TotalInitialization 		https://www.hiveworkshop.com/threads/total-initialization.317099/
                                        Hook 						https://www.hiveworkshop.com/threads/hook.339153/

    =============================================================================================================================================================
                                                                        U N I T S
    =============================================================================================================================================================

    This template automatically creates an actor for each unit that enters the map (units with Locust are excluded). A unit actor will get the identifiers "unit"
    and "<unitName>" (transformed to camelCase formatting to be consistent with other identifiers). You can choose to add "hero"/"nonhero" and unit classifications
    to the identifiers.
    
    These actors will not initiate any pairs themselves. They will be destroyed on the unit's death.

    You can add exceptions with CAT_AddException(unitId). Units with ids that were added to the exception list will not get an actor.

    Customization of this library for your specific needs is encouraged.

    =============================================================================================================================================================
                                                                        C O N F I G
    =============================================================================================================================================================
    ]]

    local IGNORE_UNIT_UNLESS_IN_LIST            = false     ---@type boolean
    --If set to true, no actor will be created unless it has been enabled for that unit type with CAT_AddUnitException.

    local DO_REVIVE_CHECK                       = false     ---@type boolean
    --After a nonhero unit dies, periodically check if it has been revived until it is removed from the game. Revive checks can be added manually with
    --CAT_AddReviveCheck(whichUnit) (while the unit is still alive).
    local REVIVE_CHECK_INTERVAL                 = 0.25      ---@type number
    --How often the revive check is done on each unit.
    local ADD_HERO_CLASSIFICATION               = true      ---@type boolean
    --Add "hero"/"nonhero" identifier to the actor.
    local ADD_UNIT_CLASSIFICATIONS              = false     ---@type boolean
    --Add identifiers such as "mechanical"/"nonmechanical" to the actor.

    --[[
    =============================================================================================================================================================
                                                                           A P I
    =============================================================================================================================================================

    CAT_AddUnitActor(whichUnit)
    CAT_AddException(unitId)
    CAT_AddReviveCheck(whichUnit)

    CAT_SetWidgetIdRadius(unitId, radius)
    CAT_SetWidgetCellCheckInterval(unitId, interval)
    CAT_SetWidgetIdStationary(unitId)

    =============================================================================================================================================================
    ]]
    
    local reviveCheckList = {}
    local actorFlags = {}
    local identifiers = {}

    ---@param u unit
    ---@param ignoreExceptions boolean
    local function CreateUnitActor(u, ignoreExceptions)
        local id = GetUnitTypeId(u)
        
        if not ignoreExceptions then
            if GetUnitAbilityLevel(u, FourCC("Aloc")) > 0 then --Locust
                return
            end

            if CAT_IsWidgetIdInList(id, CAT_ExceptionList) ~= IGNORE_UNIT_UNLESS_IN_LIST then
                return
            end
        end
        
        for key, __ in pairs(identifiers) do
            identifiers[key] = nil
        end
        table.insert(identifiers, "CAT")
        table.insert(identifiers, "unit")
        table.insert(identifiers, CAT_ToCamelCase(GetUnitName(u)))

        if ADD_HERO_CLASSIFICATION then
            if IsUnitType(u, UNIT_TYPE_HERO) then
                table.insert(identifiers, "hero")
            else
                table.insert(identifiers, "nonhero")
            end
        end

        if ADD_UNIT_CLASSIFICATIONS then
            if IsUnitType(u, UNIT_TYPE_UNDEAD) then
                table.insert(identifiers, "undead")
            else
                table.insert(identifiers, "nonundead")
            end
            if IsUnitType(u, UNIT_TYPE_MECHANICAL) then
                table.insert(identifiers, "mechanical")
            else
                table.insert(identifiers, "nonmechanical")
            end
            if IsUnitType(u, UNIT_TYPE_FLYING) then
                table.insert(identifiers, "flying")
            else
                table.insert(identifiers, "ground")
            end
            if IsUnitType(u, UNIT_TYPE_STRUCTURE) then
                table.insert(identifiers, "structure")
            else
                table.insert(identifiers, "nonstructure")
            end
            if IsUnitType(u, UNIT_TYPE_ANCIENT) then
                table.insert(identifiers, "ancient")
            else
                table.insert(identifiers, "nonancient")
            end
            if IsUnitType(u, UNIT_TYPE_SAPPER) then
                table.insert(identifiers, "sapper")
            else
                table.insert(identifiers, "nonsapper")
            end
        end

        actorFlags.isStationary = CAT_WidgetIdIsStationary[id]
        actorFlags.radius = CAT_WidgetIdRadius[id]
        actorFlags.cellCheckInterval = CAT_WidgetIdCellCheckInterval[id]

        ALICE_Create(u, identifiers, nil, actorFlags)
    end

    local function CAT_ReviveCheck(u)
        if GetUnitTypeId(u) == 0 then
            reviveCheckList[u] = nil
            ALICE_Destroy(u, "CAT")
        elseif UnitAlive(u) then
            ALICE_Destroy(u, "CAT")
            CreateUnitActor(u, false)
        end
        return REVIVE_CHECK_INTERVAL
    end

    local function OnUnitDeath()
        local u = GetTriggerUnit()
        if not ALICE_HasActor(u, "CAT") then
            return
        end
        ALICE_Destroy(u, "CAT")
        if (DO_REVIVE_CHECK and not IsUnitType(u, UNIT_TYPE_HERO)) or reviveCheckList[u] then
            ALICE_Create(u, {"CAT", "corpse"}, {self = CAT_ReviveCheck}, {isStationary = true})
        end
    end

    local function OnUnitEnter()
        CreateUnitActor(GetTriggerUnit(), false)
    end

    function Hook:RemoveUnit(whichUnit)
        ALICE_Destroy(whichUnit)
        self.old(whichUnit)
    end

    function Hook:SetUnitOwner(whichUnit, newOwner)
        self.old(whichUnit, newOwner)
        ALICE_UpdateOwner(whichUnit)
    end
    
    local function InitCAT()
        local reg = CreateRegion()
        RegionAddRect(reg, GetWorldBounds())
        local trig = CreateTrigger()
        TriggerRegisterEnterRegion(trig, reg, nil)
        TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_HERO_REVIVE_FINISH)
        TriggerAddAction(trig, OnUnitEnter)

        trig = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DEATH)
        TriggerAddAction(trig, OnUnitDeath)
        
        local G = CreateGroup()
        GroupEnumUnitsInRect(G, GetPlayableMapRect(), nil)
        ForGroup(G, function() CreateUnitActor(GetEnumUnit(), false) end)
        DestroyGroup(G)

        local reviveCheckerClass = {
            identifier = "reviveChecker",
            interactionFunc = {revivableCorpse = CAT_ReviveCheck},
            isIndestructible = true
        }
        ALICE_CreateFromClass(reviveCheckerClass)
    end

    OnInit.final(InitCAT)

    --===========================================================================================================================================================

    ---@param unitId integer
    function CAT_AddUnitException(unitId)
        if not CAT_IsWidgetIdInList(unitId, CAT_ExceptionList) then
            table.insert(CAT_ExceptionList, unitId)
        end
    end

    ---@param whichUnit unit
    function CAT_AddReviveCheck(whichUnit)
        reviveCheckList[whichUnit] = true
    end

    ---@param whichUnit unit
    function CAT_AddUnitActor(whichUnit)
        CreateUnitActor(whichUnit, true)
    end
end

Lua:
do
    --[[
    =============================================================================================================================================================
                                                                Complementary Actor Template
                                                                        by Antares

                                        Requires:
                                        ALICE 						https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
                                        CAT Shared Functions
                                        TotalInitialization 		https://www.hiveworkshop.com/threads/total-initialization.317099/
                                        Hook 						https://www.hiveworkshop.com/threads/hook.339153/

    =============================================================================================================================================================
                                                                         I T E M S
    =============================================================================================================================================================

    This template automatically creates an actor for each item. An item actor will get the identifiers "item" and <itemName> (transformed to camelCase formatting
    to be consistent with other identifiers).
    
    These actors will not initiate any pairs themselves. They will be destroyed on the item's death.

    You can add exceptions with CAT_AddException(itemId). Items with ids that were added to the exception list will not get an actor.

    Customization of this library for your specific needs is encouraged.

    =============================================================================================================================================================
                                                                        C O N F I G
    =============================================================================================================================================================
    ]]

    local IGNORE_ITEM_UNLESS_IN_LIST    = false     ---@type boolean
    --If set to true, no actor will be created unless it has been enabled for that item type with CAT_AddException.

    --[[
    =============================================================================================================================================================
                                                                           A P I
    =============================================================================================================================================================

    CAT_AddItemActor(whichItem)
    CAT_AddException(itemId)
    CAT_SetWidgetIdRadius(itemId, radius)
    CAT_SetWidgetCellCheckInterval(itemId, interval)

    --===========================================================================================================================================================
    ]]

    local actorFlags = {destroyOnDeath = true}
    local identifiers = {}

    ---@param i item
    ---@param ignoreExceptions boolean
    local function CreateItemActor(i, ignoreExceptions)
        local id = GetItemTypeId(i)
        
        if not ignoreExceptions then
            if CAT_IsWidgetIdInList(id, CAT_ExceptionList) ~= IGNORE_ITEM_UNLESS_IN_LIST then
                return
            end
        end

        for key, __ in pairs(identifiers) do
            identifiers[key] = nil
        end
        table.insert(identifiers, "CAT")
        table.insert(identifiers, "item")
        local name = GetItemName(i)
        table.insert(identifiers, CAT_ToCamelCase(name))

        actorFlags.radius = CAT_WidgetIdRadius[id]
        ALICE_Create(i, identifiers, nil, actorFlags)
    end

    if Hook then
        function Hook:CreateItem(...)
            local newItem
            newItem = self.old(...)
            CreateItemActor(newItem, false)
            return newItem
        end
    end

    local function OnItemPickup()
        ALICE_Destroy(GetManipulatedItem(), "CAT")
    end

    local function OnItemDrop()
        CreateItemActor(GetManipulatedItem(), false)
    end

    function InitCAT()
        EnumItemsInRect(GetPlayableMapRect(), nil, function() CreateItemActor(GetEnumItem(), false) end)

        local trig = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DROP_ITEM)
        TriggerAddAction(trig, OnItemDrop)

        trig = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_PICKUP_ITEM)
        TriggerAddAction(trig, OnItemPickup)
    end

    OnInit.final(InitCAT)

    ---@param whichItem item
    function CAT_AddItemActor(whichItem)
        CreateItemActor(whichItem, true)
    end
end

Lua:
do
    --[[
    =============================================================================================================================================================
                                                                Complementary Actor Template
                                                                        by Antares

                                        Requires:
                                        ALICE 						https://www.hiveworkshop.com/threads/a-l-i-c-e-interaction-engine.353126/
                                        CAT Shared Functions
                                        TotalInitialization 		https://www.hiveworkshop.com/threads/total-initialization.317099/
                                        Hook 						https://www.hiveworkshop.com/threads/hook.339153/

    =============================================================================================================================================================
                                                                 D E S T R U C T A B L E S
    =============================================================================================================================================================

    This template automatically creates an actor for each destructable. A destructable actor will get the identifiers "destructable" and <destructableName>
    (transformed to camelCase formatting to be consistent with other identifiers). CAT also searches for "Tree", "Rock", "Gate", "Bridge", and "Blocker" in its
    name and adds those identifiers if it finds them.
    
    These actors will not initiate any pairs themselves. They will be destroyed on the destructable's death.

    You can add exceptions with CAT_AddException(destructableId). Destructables with ids that were added to the exception list will not get an actor.

    Customization of this library for your specific needs is encouraged.

    Limitations: Revived destructables do not get an actor.

    =============================================================================================================================================================
                                                                        C O N F I G
    =============================================================================================================================================================
    ]]

    local IGNORE_DESTRUCTABLE_UNLESS_IN_LIST    = false     ---@type boolean
    --If set to true, no actor will be created unless it has been enabled for that destructable type with CAT_AddException.

    --[[
    =============================================================================================================================================================
                                                                           A P I
    =============================================================================================================================================================

    CAT_AddDestructableActor(whichDestructable)
    CAT_AddException(destructableId)
    CAT_SetWidgetIdRadius(destructableId, radius)

    --===========================================================================================================================================================
    ]]

    local actorFlags = {destroyOnDeath = true}
    local identifiers = {}

    ---@param d destructable
    ---@param ignoreExceptions boolean
    local function CreateDestructableActor(d, ignoreExceptions)
        local id = GetDestructableTypeId(d)
        
        if not ignoreExceptions then
            if CAT_IsWidgetIdInList(id, CAT_ExceptionList) ~= IGNORE_DESTRUCTABLE_UNLESS_IN_LIST then
                return
            end
        end

        for key, __ in pairs(identifiers) do
            identifiers[key] = nil
        end
        table.insert(identifiers, "CAT")
        table.insert(identifiers, "destructable")
        local name = GetDestructableName(d)
        table.insert(identifiers, CAT_ToCamelCase(name))
        if string.find(name, "Tree") then
            table.insert(identifiers, "tree")
        elseif string.find(name, "Rock") then
            table.insert(identifiers, "rock")
        elseif string.find(name, "Blocker") then
            table.insert(identifiers, "blocker")
        elseif string.find(name, "Gate") then
            table.insert(identifiers, "gate")
        elseif string.find(name, "Bridge") then
            table.insert(identifiers, "bridge")
        end

        actorFlags.radius = CAT_WidgetIdRadius[id]
        ALICE_Create(d, identifiers, nil, actorFlags)
    end

    if Hook then
        function Hook:CreateDestructable(...)
            local newDestructable
            newDestructable = self.old(...)
            CreateDestructableActor(newDestructable, false)
            return newDestructable
        end
        
        function Hook:CreateDestructableZ(...)
            local newDestructable
            newDestructable = self.old(...)
            CreateDestructableActor(newDestructable, false)
            return newDestructable
        end
        
        function Hook:BlzCreateDestructableWithSkin(...)
            local newDestructable
            newDestructable = self.old(...)
            CreateDestructableActor(newDestructable, false)
            return newDestructable
        end
        
        function Hook:BlzCreateDestructableZWithSkin(...)
            local newDestructable
            newDestructable = self.old(...)
            CreateDestructableActor(newDestructable, false)
            return newDestructable
        end
    end

    function InitCAT()
        EnumDestructablesInRect(GetPlayableMapRect(), nil, function() CreateDestructableActor(GetEnumDestructable(), false) end)
    end

    OnInit.final(InitCAT)

    ---@param whichDestructable destructable
    function CAT_AddDestructableActor(whichDestructable)
        CreateDestructableActor(whichDestructable, true)
    end
end



Credits:
Starsphere by ILH
Trees by Fingolfin
Missiles by Vinz
Plasma Blast model by nGy
Starcraft model ports by Daratrix
Dwarven Arsonist model by Maximal
Flamethrower model by Vinz
TotalInitialization by Bribe
DebugUtils by Eikonium
Previews
Contents

ALICE (Map)

as a casual user with knowledge only to gui this looks like basic chinese to me
but there is so much potentail here! hope it gets approved :peasant-thumbs-up-cheers:
Thanks :plol: Yea, it's a bit too technical mumbo-jumbo right now, but I hope there can be a GUI-friendly version in the future, or something built with it that is GUI-friendly.
 
Saying this resource is extensive is a severe understatement. I am glad to see someone else using spatial hashing... The performance you can get from these algorithms are amazing. I actually have the problem where the effect models themselves are more expensive to compute than the collision/pathing logic. With that being said we can reduce the number of table lookups from table[x][y] to table[index]. You can do this by doing:
index = x + y * 1e7 (works for any map size)

Edit:
You should consider doing effects all the way down. I've done it and the performance increase is stupendous.
Untitled.png
 
Saying this resource is extensive is a severe understatement. I am glad to see someone else using spatial hashing... The performance you can get from these algorithms are amazing. I actually have the problem where the effect models themselves are more expensive to compute than the collision/pathing logic. With that being said we can reduce the number of table lookups from table[x][y] to table[index]. You can do this by doing:
index = x + y * 1e7 (works for any map size)
Thank you, yes, the performance increase after I added the cell optimization was quite substantial. With my vJASS version, I was hardlocked at 181 objects (sqrt(32768), the max array size), but I estimate the maximum I could do without that restriction was 250. After translating it to Lua, I went up to 1000, so a ~16x performance increase. Then, after adding the cells, I went to 3000, so another ~10x performance increase, but probably much more, because, like you said, at such a high number of objects, rendering and moving the objects starts to take the majority of the resources.

You're right that I could improve the performance by doing linear indexing. However, I have to make sure that there are no bugs and I don't want to change anything about the core, because after converting it to linear indizes, the code becomes quite unreadable. Currently, I'm focused on getting the API to always work in the way you'd intuitively expect it to. I'm creating different scenarios and when I encounter a problem I can't solve, I change the engine such that I can.

I'm also not sure if linear indexing would make sense everywhere, because the math to get the index could be slower than the table lookup itself. I'd have to make some tests. Did you do tests on that?

Edit:
You should consider doing effects all the way down. I've done it and the performance increase is stupendous.
View attachment 468609
Holy ****, that's a lot of skeletons! :peek: With effects all the way down you mean replace units etc. with special effects? Well, I did make a map where there's like 5 units on the map and everything else are special effects... :plol: But, of course, that's not always feasible.
 
Level 2
Joined
Sep 3, 2023
Messages
6
Is there a way to automatically assign an actor for a destructible upon its creation? I can do it with unit by using a Unit Indexer library, but so far there is no function to detect a destructible entering the map if I am correct.
Does your system work on particles with different collision sizes and masses? I mean does it work if I have two spheres with radii 10000 and 100 colliding with each other?
 
Is there a way to automatically assign an actor for a destructible upon its creation? I can do it with unit by using a Unit Indexer library, but so far there is no function to detect a destructible entering the map if I am correct.
Yes, there is no trigger to detect a destructable entering, but, in the destructables library, I created hooks to create actors when the CreateDestructable functions are called. On map initialization, I loop over all destructables. A destructable being revived won't create one, though. That's a problem I haven't solved yet.

Does your system work on particles with different collision sizes and masses? I mean does it work if I have two spheres with radii 10000 and 100 colliding with each other?
Yes, that's not a problem. You can see in the video I posted above your post that there are rocks with different sizes and masses colliding with each other.

You can store the collision sizes of your particles in the table, then in your collision function do:
Lua:
local distance = ALICE_PairGetDistance()
if distance < particle1.collisionSize + particle2.collisionSize then
    DoStuff()
end
However, the engine also needs to know at which range it should start checking for collisions. That's done with the .radius field. You can take a look at the Cells example in the showcase map to see how that works.

Welcome to hive btw :plol:
 
Top