- Joined
- Nov 3, 2018
- Messages
- 80
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
but there is so much potentail here! hope it gets approved
--[[
=============================================================================================================================================================
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
]]
--[[
=============================================================================================================================================================
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.
=============================================================================================================================================================
]]
--[[
=============================================================================================================================================================
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.
=============================================================================================================================================================
]]
--[[
=============================================================================================================================================================
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.
=============================================================================================================================================================
]]
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
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
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
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
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